@elixpo/lixsketch 5.4.1 → 5.4.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/SketchEngine.js +7 -0
- package/src/core/CopyPaste.js +1 -1
- package/src/core/EventDispatcher.js +39 -20
- package/src/core/SceneSerializer.js +12 -4
- package/src/core/Selection.js +57 -26
- package/src/core/ZoomPan.js +15 -9
- package/src/shapes/Arrow.js +3 -3
- package/src/shapes/Circle.js +3 -3
- package/src/shapes/CodeShape.js +5 -4
- package/src/shapes/Frame.js +5 -5
- package/src/shapes/Line.js +3 -3
- package/src/shapes/Rectangle.js +3 -3
- package/src/shapes/TextShape.js +11 -5
- package/src/tools/arrowTool.js +3 -3
- package/src/tools/codeTool.js +17 -16
- package/src/tools/freehandTool.js +4 -4
- package/src/tools/iconTool.js +112 -99
- package/src/tools/imageTool.js +25 -25
- package/src/tools/textTool.js +40 -23
package/package.json
CHANGED
package/src/SketchEngine.js
CHANGED
|
@@ -19,6 +19,9 @@ class SketchEngine {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
this.svg = svgElement;
|
|
22
|
+
// Prevent default touch scrolling/gestures on the canvas to allow for custom pointer events
|
|
23
|
+
this.svg.style.touchAction = 'none';
|
|
24
|
+
|
|
22
25
|
this.options = {
|
|
23
26
|
initialZoom: 1,
|
|
24
27
|
minZoom: 0.4,
|
|
@@ -388,6 +391,10 @@ class SketchEngine {
|
|
|
388
391
|
if (typeof window.forceCleanupEraserTrail === 'function') {
|
|
389
392
|
window.forceCleanupEraserTrail();
|
|
390
393
|
}
|
|
394
|
+
// Clean up any lingering icon miniature/drag state
|
|
395
|
+
if (typeof window.__cleanupIconTool === 'function') {
|
|
396
|
+
window.__cleanupIconTool();
|
|
397
|
+
}
|
|
391
398
|
|
|
392
399
|
window.isPaintToolActive = false;
|
|
393
400
|
window.isSquareToolActive = false;
|
package/src/core/CopyPaste.js
CHANGED
|
@@ -644,7 +644,7 @@ function placeImageFromDataUrl(dataUrl) {
|
|
|
644
644
|
// ============================================================
|
|
645
645
|
function initCopyPaste() {
|
|
646
646
|
document.addEventListener('keydown', handleCopyPasteKeydown);
|
|
647
|
-
document.addEventListener('
|
|
647
|
+
document.addEventListener('pointermove', handleMouseMoveForPaste);
|
|
648
648
|
document.addEventListener('paste', handlePasteEvent);
|
|
649
649
|
|
|
650
650
|
// Expose for context menu
|
|
@@ -8,7 +8,7 @@ import { handleMouseDownCircle, handleMouseMoveCircle, handleMouseUpCircle } fro
|
|
|
8
8
|
import { handleMouseUpImage, handleMouseDownImage, handleMouseMoveImage } from '../tools/imageTool.js';
|
|
9
9
|
import { handleMouseDownLine, handleMouseMoveLine, handleMouseUpLine } from '../tools/lineTool.js';
|
|
10
10
|
import { handleFreehandMouseDown, handleFreehandMouseMove, handleFreehandMouseUp } from '../tools/freehandTool.js';
|
|
11
|
-
import { handleTextMouseDown, handleTextMouseMove, handleTextMouseUp } from '../tools/textTool.js';
|
|
11
|
+
import { handleTextMouseDown, handleTextMouseMove, handleTextMouseUp, enterEditMode } from '../tools/textTool.js';
|
|
12
12
|
import { handleMouseDownFrame, handleMouseMoveFrame, handleMouseUpFrame } from '../tools/frameTool.js';
|
|
13
13
|
import { handleMultiSelectionMouseDown, handleMultiSelectionMouseMove, handleMultiSelectionMouseUp, removeMultiSelectionRect, multiSelection, isMultiSelecting} from './Selection.js';
|
|
14
14
|
import { handleMouseDownIcon, handleMouseMoveIcon, handleMouseUpIcon } from '../tools/iconTool.js';
|
|
@@ -80,7 +80,7 @@ function _onDocumentDragMove(e) {
|
|
|
80
80
|
const rect = svg.getBoundingClientRect();
|
|
81
81
|
const clampedX = Math.max(rect.left, Math.min(rect.right, e.clientX));
|
|
82
82
|
const clampedY = Math.max(rect.top, Math.min(rect.bottom, e.clientY));
|
|
83
|
-
const clampedEvent = new
|
|
83
|
+
const clampedEvent = new PointerEvent(e.type, {
|
|
84
84
|
clientX: clampedX,
|
|
85
85
|
clientY: clampedY,
|
|
86
86
|
buttons: e.buttons,
|
|
@@ -95,14 +95,21 @@ function _onDocumentDragMove(e) {
|
|
|
95
95
|
|
|
96
96
|
function _onDocumentDragUp(e) {
|
|
97
97
|
_stopAutoScroll();
|
|
98
|
-
document.removeEventListener('
|
|
99
|
-
document.removeEventListener('
|
|
98
|
+
document.removeEventListener('pointermove', _onDocumentDragMove);
|
|
99
|
+
document.removeEventListener('pointerup', _onDocumentDragUp);
|
|
100
100
|
_documentDragActive = false;
|
|
101
101
|
// Finalize the operation
|
|
102
102
|
handleMainMouseUp(e);
|
|
103
103
|
}
|
|
104
104
|
|
|
105
105
|
const handleMainMouseDown = (e) => {
|
|
106
|
+
if (!e.target) return;
|
|
107
|
+
// Middle / right mouse buttons are reserved for panning and the
|
|
108
|
+
// browser context menu; they should NOT trigger tool actions or the
|
|
109
|
+
// multi-selection rectangle. Without this guard, middle-click-drag
|
|
110
|
+
// simultaneously pans (handled in ZoomPan) and starts a selection
|
|
111
|
+
// marquee.
|
|
112
|
+
if (e.button === 1 || e.button === 2) return;
|
|
106
113
|
// Safety: remove any stray selection rectangle from a previous interrupted drag
|
|
107
114
|
removeMultiSelectionRect();
|
|
108
115
|
|
|
@@ -424,8 +431,8 @@ const handleMainMouseLeave = (e) => {
|
|
|
424
431
|
// Listen on document to continue receiving move events outside the SVG
|
|
425
432
|
if (!_documentDragActive) {
|
|
426
433
|
_documentDragActive = true;
|
|
427
|
-
document.addEventListener('
|
|
428
|
-
document.addEventListener('
|
|
434
|
+
document.addEventListener('pointermove', _onDocumentDragMove);
|
|
435
|
+
document.addEventListener('pointerup', _onDocumentDragUp);
|
|
429
436
|
}
|
|
430
437
|
return;
|
|
431
438
|
}
|
|
@@ -458,36 +465,48 @@ let _boundSvg = null;
|
|
|
458
465
|
function _onMouseEnter(e) {
|
|
459
466
|
// Cursor re-entered the SVG — stop document-level tracking, SVG handles events again
|
|
460
467
|
if (_documentDragActive) {
|
|
461
|
-
document.removeEventListener('
|
|
462
|
-
document.removeEventListener('
|
|
468
|
+
document.removeEventListener('pointermove', _onDocumentDragMove);
|
|
469
|
+
document.removeEventListener('pointerup', _onDocumentDragUp);
|
|
463
470
|
_documentDragActive = false;
|
|
464
471
|
}
|
|
465
472
|
}
|
|
466
473
|
|
|
474
|
+
const handleMainDblClick = (e) => {
|
|
475
|
+
if (!e.target) return;
|
|
476
|
+
// Double-click on a text group from any tool: enter text edit mode
|
|
477
|
+
const targetTextGroup = e.target.closest('g[data-type="text-group"]');
|
|
478
|
+
if (targetTextGroup) {
|
|
479
|
+
e.stopPropagation();
|
|
480
|
+
enterEditMode(targetTextGroup);
|
|
481
|
+
}
|
|
482
|
+
};
|
|
483
|
+
|
|
467
484
|
function initEventDispatcher(svgEl) {
|
|
468
485
|
if (_boundSvg) cleanupEventDispatcher();
|
|
469
486
|
const target = svgEl || svg;
|
|
470
|
-
target.addEventListener('
|
|
471
|
-
target.addEventListener('
|
|
472
|
-
target.addEventListener('
|
|
473
|
-
target.addEventListener('
|
|
474
|
-
target.addEventListener('
|
|
487
|
+
target.addEventListener('pointerdown', handleMainMouseDown);
|
|
488
|
+
target.addEventListener('pointermove', handleMainMouseMove);
|
|
489
|
+
target.addEventListener('pointerup', handleMainMouseUp);
|
|
490
|
+
target.addEventListener('pointerleave', handleMainMouseLeave);
|
|
491
|
+
target.addEventListener('pointerenter', _onMouseEnter);
|
|
492
|
+
target.addEventListener('dblclick', handleMainDblClick);
|
|
475
493
|
_boundSvg = target;
|
|
476
494
|
}
|
|
477
495
|
|
|
478
496
|
function cleanupEventDispatcher() {
|
|
479
497
|
_stopAutoScroll();
|
|
480
498
|
if (_documentDragActive) {
|
|
481
|
-
document.removeEventListener('
|
|
482
|
-
document.removeEventListener('
|
|
499
|
+
document.removeEventListener('pointermove', _onDocumentDragMove);
|
|
500
|
+
document.removeEventListener('pointerup', _onDocumentDragUp);
|
|
483
501
|
_documentDragActive = false;
|
|
484
502
|
}
|
|
485
503
|
if (_boundSvg) {
|
|
486
|
-
_boundSvg.removeEventListener('
|
|
487
|
-
_boundSvg.removeEventListener('
|
|
488
|
-
_boundSvg.removeEventListener('
|
|
489
|
-
_boundSvg.removeEventListener('
|
|
490
|
-
_boundSvg.removeEventListener('
|
|
504
|
+
_boundSvg.removeEventListener('pointerdown', handleMainMouseDown);
|
|
505
|
+
_boundSvg.removeEventListener('pointermove', handleMainMouseMove);
|
|
506
|
+
_boundSvg.removeEventListener('pointerup', handleMainMouseUp);
|
|
507
|
+
_boundSvg.removeEventListener('pointerleave', handleMainMouseLeave);
|
|
508
|
+
_boundSvg.removeEventListener('pointerenter', _onMouseEnter);
|
|
509
|
+
_boundSvg.removeEventListener('dblclick', handleMainDblClick);
|
|
491
510
|
_boundSvg = null;
|
|
492
511
|
}
|
|
493
512
|
}
|
|
@@ -42,6 +42,10 @@ function serializeShape(shape) {
|
|
|
42
42
|
const base = {
|
|
43
43
|
shapeID: shape.shapeID,
|
|
44
44
|
parentFrame: shape.parentFrame ? shape.parentFrame.shapeID : null,
|
|
45
|
+
// Group membership — null when the shape isn't part of a group.
|
|
46
|
+
// All shapes sharing a non-null groupId move/resize/rotate as a
|
|
47
|
+
// unit (see Selection.handleMultiSelectionMouseDown).
|
|
48
|
+
groupId: shape.groupId || null,
|
|
45
49
|
};
|
|
46
50
|
|
|
47
51
|
switch (shape.shapeName) {
|
|
@@ -284,10 +288,11 @@ function deserializeShape(data) {
|
|
|
284
288
|
case 'icon': {
|
|
285
289
|
if (!data.elementHTML) return null;
|
|
286
290
|
const parser = new DOMParser();
|
|
287
|
-
|
|
288
|
-
const
|
|
289
|
-
|
|
290
|
-
|
|
291
|
+
// Wrap in <svg> so the parser treats the <g> as a valid child, not a document root
|
|
292
|
+
const doc = parser.parseFromString(`<svg xmlns="${ns}">${data.elementHTML}</svg>`, 'image/svg+xml');
|
|
293
|
+
const iconGroup = doc.querySelector('g');
|
|
294
|
+
if (!iconGroup) return null;
|
|
295
|
+
const imported = svgEl.ownerDocument.importNode(iconGroup, true);
|
|
291
296
|
svgEl.appendChild(imported);
|
|
292
297
|
const shape = new IconShape(imported);
|
|
293
298
|
if (data.shapeID) shape.shapeID = data.shapeID;
|
|
@@ -376,6 +381,8 @@ export function loadScene(sceneData) {
|
|
|
376
381
|
for (const data of frameData) {
|
|
377
382
|
const shape = deserializeShape(data);
|
|
378
383
|
if (shape) {
|
|
384
|
+
// Restore group membership if present.
|
|
385
|
+
if (data.groupId) shape.groupId = data.groupId;
|
|
379
386
|
window.shapes.push(shape);
|
|
380
387
|
if (data.shapeID) idMap.set(data.shapeID, shape);
|
|
381
388
|
}
|
|
@@ -385,6 +392,7 @@ export function loadScene(sceneData) {
|
|
|
385
392
|
for (const data of otherData) {
|
|
386
393
|
const shape = deserializeShape(data);
|
|
387
394
|
if (shape) {
|
|
395
|
+
if (data.groupId) shape.groupId = data.groupId;
|
|
388
396
|
window.shapes.push(shape);
|
|
389
397
|
if (data.shapeID) idMap.set(data.shapeID, shape);
|
|
390
398
|
}
|
package/src/core/Selection.js
CHANGED
|
@@ -80,9 +80,10 @@ function highlightShapesInSelectionRect(currentX, currentY) {
|
|
|
80
80
|
shapes.forEach(shape => {
|
|
81
81
|
if (isShapeInSelectionRect(shape, selBounds)) {
|
|
82
82
|
// Add a semi-transparent overlay to the shape's group
|
|
83
|
-
if (shape.group) {
|
|
83
|
+
if (shape.group && typeof shape.group.getBBox === 'function') {
|
|
84
|
+
let bbox;
|
|
85
|
+
try { bbox = shape.group.getBBox(); } catch { return; }
|
|
84
86
|
const overlay = document.createElementNS('http://www.w3.org/2000/svg', 'rect');
|
|
85
|
-
const bbox = shape.group.getBBox();
|
|
86
87
|
overlay.setAttribute('x', bbox.x - 2);
|
|
87
88
|
overlay.setAttribute('y', bbox.y - 2);
|
|
88
89
|
overlay.setAttribute('width', bbox.width + 4);
|
|
@@ -158,17 +159,22 @@ function isShapeInSelectionRect(shape, selectionBounds) {
|
|
|
158
159
|
};
|
|
159
160
|
break;
|
|
160
161
|
case 'text':
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
162
|
+
case 'code':
|
|
163
|
+
const textOrCodeEl = shape.group ? shape.group.querySelector('text') : null;
|
|
164
|
+
if (textOrCodeEl && shape.group.style.display !== 'none') {
|
|
165
|
+
try {
|
|
166
|
+
const bbox = textOrCodeEl.getBBox();
|
|
167
|
+
const transform = shape.group.transform.baseVal.consolidate();
|
|
168
|
+
const matrix = transform ? transform.matrix : { e: 0, f: 0 };
|
|
169
|
+
shapeBounds = {
|
|
170
|
+
x: bbox.x + matrix.e,
|
|
171
|
+
y: bbox.y + matrix.f,
|
|
172
|
+
width: bbox.width,
|
|
173
|
+
height: bbox.height
|
|
174
|
+
};
|
|
175
|
+
} catch {
|
|
176
|
+
shapeBounds = { x: 0, y: 0, width: 0, height: 0 };
|
|
177
|
+
}
|
|
172
178
|
} else {
|
|
173
179
|
shapeBounds = { x: 0, y: 0, width: 0, height: 0 };
|
|
174
180
|
}
|
|
@@ -502,7 +508,7 @@ class MultiSelection {
|
|
|
502
508
|
const cursors = ['nw-resize', 'ne-resize', 'sw-resize', 'se-resize', 'n-resize', 's-resize', 'w-resize', 'e-resize'];
|
|
503
509
|
anchor.style.cursor = cursors[pos.index];
|
|
504
510
|
|
|
505
|
-
anchor.addEventListener('
|
|
511
|
+
anchor.addEventListener('pointerdown', (e) => this.startResize(e, pos.index));
|
|
506
512
|
|
|
507
513
|
this.group.appendChild(anchor);
|
|
508
514
|
this.anchors.push(anchor);
|
|
@@ -524,7 +530,7 @@ class MultiSelection {
|
|
|
524
530
|
this.rotationAnchor.setAttribute('vector-effect', 'non-scaling-stroke');
|
|
525
531
|
this.rotationAnchor.setAttribute('style', 'pointer-events: all; cursor: grab;');
|
|
526
532
|
|
|
527
|
-
this.rotationAnchor.addEventListener('
|
|
533
|
+
this.rotationAnchor.addEventListener('pointerdown', (e) => this.startRotation(e));
|
|
528
534
|
|
|
529
535
|
this.rotationLine = document.createElementNS('http://www.w3.org/2000/svg', 'line');
|
|
530
536
|
this.rotationLine.setAttribute('x1', rotationAnchorPos.x);
|
|
@@ -614,15 +620,15 @@ class MultiSelection {
|
|
|
614
620
|
this._pushUndoForAll();
|
|
615
621
|
|
|
616
622
|
if (typeof svg !== 'undefined') {
|
|
617
|
-
svg.removeEventListener('
|
|
618
|
-
svg.removeEventListener('
|
|
623
|
+
svg.removeEventListener('pointermove', onMouseMove);
|
|
624
|
+
svg.removeEventListener('pointerup', onMouseUp);
|
|
619
625
|
svg.style.cursor = 'default';
|
|
620
626
|
}
|
|
621
627
|
};
|
|
622
628
|
|
|
623
629
|
if (typeof svg !== 'undefined') {
|
|
624
|
-
svg.addEventListener('
|
|
625
|
-
svg.addEventListener('
|
|
630
|
+
svg.addEventListener('pointermove', onMouseMove);
|
|
631
|
+
svg.addEventListener('pointerup', onMouseUp);
|
|
626
632
|
svg.style.cursor = 'grabbing';
|
|
627
633
|
}
|
|
628
634
|
}
|
|
@@ -1096,15 +1102,15 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1096
1102
|
this._pushUndoForAll();
|
|
1097
1103
|
|
|
1098
1104
|
if (typeof svg !== 'undefined') {
|
|
1099
|
-
svg.removeEventListener('
|
|
1100
|
-
svg.removeEventListener('
|
|
1105
|
+
svg.removeEventListener('pointermove', onMouseMove);
|
|
1106
|
+
svg.removeEventListener('pointerup', onMouseUp);
|
|
1101
1107
|
svg.style.cursor = 'default';
|
|
1102
1108
|
}
|
|
1103
1109
|
};
|
|
1104
1110
|
|
|
1105
1111
|
if (typeof svg !== 'undefined') {
|
|
1106
|
-
svg.addEventListener('
|
|
1107
|
-
svg.addEventListener('
|
|
1112
|
+
svg.addEventListener('pointermove', onMouseMove);
|
|
1113
|
+
svg.addEventListener('pointerup', onMouseUp);
|
|
1108
1114
|
}
|
|
1109
1115
|
}
|
|
1110
1116
|
|
|
@@ -1409,6 +1415,7 @@ function moveSelectedShapes(dx, dy) {
|
|
|
1409
1415
|
}
|
|
1410
1416
|
|
|
1411
1417
|
function handleMultiSelectionMouseDown(e) {
|
|
1418
|
+
if (!e.target) return false;
|
|
1412
1419
|
const { x, y } = getSVGCoordsFromMouse(e);
|
|
1413
1420
|
|
|
1414
1421
|
// Only handle multi-selection operations if we have multiple shapes selected
|
|
@@ -1443,8 +1450,14 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1443
1450
|
return true;
|
|
1444
1451
|
}
|
|
1445
1452
|
|
|
1446
|
-
//
|
|
1447
|
-
//
|
|
1453
|
+
// Click is within the multi-selection bounding box but not directly on a shape —
|
|
1454
|
+
// still start dragging the group (prevents accidental deselection)
|
|
1455
|
+
if (multiSelection.isPointInBounds(x, y)) {
|
|
1456
|
+
multiSelection.startDrag(e);
|
|
1457
|
+
return true;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
// Click is outside the selection bounds — fall through to deselect / start new selection
|
|
1448
1461
|
}
|
|
1449
1462
|
|
|
1450
1463
|
// Check if clicking on individual shape anchors - let them handle it
|
|
@@ -1527,6 +1540,21 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1527
1540
|
// Clear multi-selection
|
|
1528
1541
|
multiSelection.clearSelection();
|
|
1529
1542
|
|
|
1543
|
+
// ── Group expansion ────────────────────────────────────────────
|
|
1544
|
+
// If the clicked shape is part of a group, select ALL of its
|
|
1545
|
+
// group-mates as a multi-selection instead of single-selecting.
|
|
1546
|
+
// Subsequent drag/resize/rotate then operates on the whole group
|
|
1547
|
+
// via the existing multi-selection plumbing.
|
|
1548
|
+
if (clickedShape.groupId && typeof shapes !== 'undefined') {
|
|
1549
|
+
const mates = shapes.filter(s => s.groupId === clickedShape.groupId);
|
|
1550
|
+
if (mates.length > 1) {
|
|
1551
|
+
for (const m of mates) multiSelection.addShape(m);
|
|
1552
|
+
currentShape = null;
|
|
1553
|
+
multiSelection.startDrag(e);
|
|
1554
|
+
return true;
|
|
1555
|
+
}
|
|
1556
|
+
}
|
|
1557
|
+
|
|
1530
1558
|
// Set new current shape
|
|
1531
1559
|
currentShape = clickedShape;
|
|
1532
1560
|
|
|
@@ -1693,6 +1721,9 @@ function handleMultiSelectionMouseUp(e) {
|
|
|
1693
1721
|
} else if (typeof selectedShape.createSelection === 'function') {
|
|
1694
1722
|
selectedShape.createSelection();
|
|
1695
1723
|
selectedShape.isSelected = true;
|
|
1724
|
+
} else if (typeof selectedShape.selectShape === 'function') {
|
|
1725
|
+
selectedShape.selectShape();
|
|
1726
|
+
selectedShape.isSelected = true;
|
|
1696
1727
|
}
|
|
1697
1728
|
|
|
1698
1729
|
if (typeof selectedShape.updateSidebar === 'function') {
|
|
@@ -1730,7 +1761,7 @@ function handleMultiSelectionMouseUp(e) {
|
|
|
1730
1761
|
|
|
1731
1762
|
|
|
1732
1763
|
// Safety net: clean up selection rect if mouse is released outside the SVG
|
|
1733
|
-
window.addEventListener('
|
|
1764
|
+
window.addEventListener('pointerup', () => {
|
|
1734
1765
|
if (isMultiSelecting) {
|
|
1735
1766
|
removeMultiSelectionRect();
|
|
1736
1767
|
isMultiSelecting = false;
|
package/src/core/ZoomPan.js
CHANGED
|
@@ -99,26 +99,32 @@ freehandCanvas.addEventListener("wheel", function(e) {
|
|
|
99
99
|
if (newZoom < minScale) newZoom = minScale;
|
|
100
100
|
if (newZoom > maxScale) newZoom = maxScale;
|
|
101
101
|
|
|
102
|
-
// Get canvas bounding rect
|
|
102
|
+
// Get canvas bounding rect — this MUST be the SVG element's actual
|
|
103
|
+
// size, not the window's. In split mode the canvas pane is narrower
|
|
104
|
+
// than the viewport; using window.innerWidth here would produce a
|
|
105
|
+
// viewBox aspect that doesn't match the element, and (with
|
|
106
|
+
// preserveAspectRatio="none" on the host) the content gets stretched
|
|
107
|
+
// / squeezed on every zoom step.
|
|
103
108
|
const rect = freehandCanvas.getBoundingClientRect();
|
|
104
|
-
|
|
109
|
+
|
|
105
110
|
// Calculate mouse position relative to the canvas in pixels
|
|
106
111
|
const mouseX = e.clientX - rect.left;
|
|
107
112
|
const mouseY = e.clientY - rect.top;
|
|
108
|
-
|
|
113
|
+
|
|
109
114
|
// Determine what fraction of the canvas the mouse is at
|
|
110
115
|
const mouseFracX = mouseX / rect.width;
|
|
111
116
|
const mouseFracY = mouseY / rect.height;
|
|
112
|
-
|
|
117
|
+
|
|
113
118
|
// Compute the current viewBox coordinate under the mouse.
|
|
114
119
|
// currentViewBox.width and .height represent the current viewBox dimensions.
|
|
115
120
|
const anchorViewBoxX = currentViewBox.x + mouseFracX * currentViewBox.width;
|
|
116
121
|
const anchorViewBoxY = currentViewBox.y + mouseFracY * currentViewBox.height;
|
|
117
|
-
|
|
118
|
-
//
|
|
119
|
-
//
|
|
120
|
-
|
|
121
|
-
const
|
|
122
|
+
|
|
123
|
+
// New viewBox dimensions sized to the *element*, scaled by the new
|
|
124
|
+
// zoom. This keeps the viewBox aspect ratio matching the element so
|
|
125
|
+
// shapes stay undistorted.
|
|
126
|
+
const newViewBoxWidth = rect.width / newZoom;
|
|
127
|
+
const newViewBoxHeight = rect.height / newZoom;
|
|
122
128
|
|
|
123
129
|
// Compute the new viewBox's x and y so that the anchor remains at the same screen fraction.
|
|
124
130
|
// That means:
|
package/src/shapes/Arrow.js
CHANGED
|
@@ -592,9 +592,9 @@ class Arrow {
|
|
|
592
592
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
593
593
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
594
594
|
});
|
|
595
|
-
input.addEventListener('
|
|
596
|
-
input.addEventListener('
|
|
597
|
-
input.addEventListener('
|
|
595
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
596
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
597
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
598
598
|
}
|
|
599
599
|
|
|
600
600
|
setLabel(text, color, fontSize) {
|
package/src/shapes/Circle.js
CHANGED
|
@@ -726,9 +726,9 @@ class Circle {
|
|
|
726
726
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
727
727
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
728
728
|
});
|
|
729
|
-
input.addEventListener('
|
|
730
|
-
input.addEventListener('
|
|
731
|
-
input.addEventListener('
|
|
729
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
730
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
731
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
732
732
|
}
|
|
733
733
|
|
|
734
734
|
setLabel(text, color, fontSize) {
|
package/src/shapes/CodeShape.js
CHANGED
|
@@ -202,11 +202,12 @@ class CodeShape {
|
|
|
202
202
|
|
|
203
203
|
contains(x, y) {
|
|
204
204
|
const codeElement = this.group.querySelector('text');
|
|
205
|
-
if (!codeElement) return false;
|
|
206
|
-
|
|
207
|
-
|
|
205
|
+
if (!codeElement || typeof codeElement.getBBox !== 'function') return false;
|
|
206
|
+
|
|
207
|
+
let bbox;
|
|
208
|
+
try { bbox = codeElement.getBBox(); } catch { return false; }
|
|
208
209
|
const padding = 8; // Selection padding
|
|
209
|
-
|
|
210
|
+
|
|
210
211
|
const CTM = this.group.getCTM();
|
|
211
212
|
if (!CTM) return false;
|
|
212
213
|
|
package/src/shapes/Frame.js
CHANGED
|
@@ -590,9 +590,9 @@ startLabelEdit(labelElement) {
|
|
|
590
590
|
});
|
|
591
591
|
|
|
592
592
|
// Prevent the input from interfering with other interactions
|
|
593
|
-
input.addEventListener('
|
|
594
|
-
input.addEventListener('
|
|
595
|
-
input.addEventListener('
|
|
593
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
594
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
595
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
596
596
|
}
|
|
597
597
|
selectFrame() {
|
|
598
598
|
this.isSelected = true;
|
|
@@ -626,7 +626,7 @@ startLabelEdit(labelElement) {
|
|
|
626
626
|
newInput.addEventListener('keydown', (e) => {
|
|
627
627
|
if (e.key === 'Enter') newInput.blur();
|
|
628
628
|
});
|
|
629
|
-
newInput.addEventListener('
|
|
629
|
+
newInput.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
630
630
|
}
|
|
631
631
|
|
|
632
632
|
if (resizeBtn) {
|
|
@@ -790,7 +790,7 @@ startLabelEdit(labelElement) {
|
|
|
790
790
|
}
|
|
791
791
|
|
|
792
792
|
anchor.style.cursor = position.cursor;
|
|
793
|
-
anchor.addEventListener('
|
|
793
|
+
anchor.addEventListener('pointerdown', (e) => this.startAnchorDrag(e, index));
|
|
794
794
|
|
|
795
795
|
this.anchors.push(anchor);
|
|
796
796
|
this.group.appendChild(anchor);
|
package/src/shapes/Line.js
CHANGED
|
@@ -352,9 +352,9 @@ class Line {
|
|
|
352
352
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
353
353
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
354
354
|
});
|
|
355
|
-
input.addEventListener('
|
|
356
|
-
input.addEventListener('
|
|
357
|
-
input.addEventListener('
|
|
355
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
356
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
357
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
358
358
|
}
|
|
359
359
|
|
|
360
360
|
setLabel(text, color, fontSize) {
|
package/src/shapes/Rectangle.js
CHANGED
|
@@ -362,9 +362,9 @@ class Rectangle {
|
|
|
362
362
|
if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); input.blur(); }
|
|
363
363
|
if (e.key === 'Escape') { input.textContent = this.label; input.blur(); }
|
|
364
364
|
});
|
|
365
|
-
input.addEventListener('
|
|
366
|
-
input.addEventListener('
|
|
367
|
-
input.addEventListener('
|
|
365
|
+
input.addEventListener('pointerdown', (e) => e.stopPropagation());
|
|
366
|
+
input.addEventListener('pointermove', (e) => e.stopPropagation());
|
|
367
|
+
input.addEventListener('pointerup', (e) => e.stopPropagation());
|
|
368
368
|
}
|
|
369
369
|
|
|
370
370
|
setLabel(text, color, fontSize) {
|
package/src/shapes/TextShape.js
CHANGED
|
@@ -182,11 +182,12 @@ class TextShape {
|
|
|
182
182
|
|
|
183
183
|
contains(x, y) {
|
|
184
184
|
const textElement = this.group.querySelector('text');
|
|
185
|
-
if (!textElement) return false;
|
|
186
|
-
|
|
187
|
-
|
|
185
|
+
if (!textElement || typeof textElement.getBBox !== 'function') return false;
|
|
186
|
+
|
|
187
|
+
let bbox;
|
|
188
|
+
try { bbox = textElement.getBBox(); } catch { return false; }
|
|
188
189
|
const padding = 8; // Selection padding
|
|
189
|
-
|
|
190
|
+
|
|
190
191
|
const CTM = this.group.getCTM();
|
|
191
192
|
if (!CTM) return false;
|
|
192
193
|
|
|
@@ -218,7 +219,12 @@ class TextShape {
|
|
|
218
219
|
}
|
|
219
220
|
|
|
220
221
|
selectShape() {
|
|
221
|
-
selectElement(
|
|
222
|
+
// Use the real textTool selectElement (with selection feedback) if available
|
|
223
|
+
if (typeof window !== 'undefined' && window.__selectTextElement) {
|
|
224
|
+
window.__selectTextElement(this.group);
|
|
225
|
+
} else {
|
|
226
|
+
selectElement(this.group);
|
|
227
|
+
}
|
|
222
228
|
}
|
|
223
229
|
}
|
|
224
230
|
|
package/src/tools/arrowTool.js
CHANGED
|
@@ -386,9 +386,9 @@ svg.style.cursor = 'default';
|
|
|
386
386
|
};
|
|
387
387
|
|
|
388
388
|
// Remove old event listeners and add new ones
|
|
389
|
-
svg.removeEventListener('
|
|
390
|
-
svg.removeEventListener('
|
|
391
|
-
svg.removeEventListener('
|
|
389
|
+
svg.removeEventListener('pointerdown', handleMouseDown);
|
|
390
|
+
svg.removeEventListener('pointermove', handleMouseMove);
|
|
391
|
+
svg.removeEventListener('pointerup', handleMouseUp);
|
|
392
392
|
|
|
393
393
|
|
|
394
394
|
// Updated style handlers with undo/redo support
|