@elixpo/lixsketch 5.4.0 → 5.4.2
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/core/EventDispatcher.js +36 -12
- package/src/core/Selection.js +39 -29
- package/src/shapes/Arrow.js +32 -0
- package/src/shapes/CodeShape.js +5 -4
- package/src/shapes/FreehandStroke.js +31 -55
- package/src/shapes/TextShape.js +5 -4
- package/src/tools/freehandTool.js +45 -3
- package/src/tools/textTool.js +16 -2
package/package.json
CHANGED
|
@@ -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';
|
|
@@ -75,9 +75,22 @@ let _documentDragActive = false;
|
|
|
75
75
|
|
|
76
76
|
function _onDocumentDragMove(e) {
|
|
77
77
|
_lastDragEvent = e;
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
|
|
78
|
+
// Clamp mouse coordinates to SVG bounds before forwarding to tool handlers,
|
|
79
|
+
// so getSVGCoordsFromMouse doesn't produce extreme values when cursor is outside.
|
|
80
|
+
const rect = svg.getBoundingClientRect();
|
|
81
|
+
const clampedX = Math.max(rect.left, Math.min(rect.right, e.clientX));
|
|
82
|
+
const clampedY = Math.max(rect.top, Math.min(rect.bottom, e.clientY));
|
|
83
|
+
const clampedEvent = new MouseEvent(e.type, {
|
|
84
|
+
clientX: clampedX,
|
|
85
|
+
clientY: clampedY,
|
|
86
|
+
buttons: e.buttons,
|
|
87
|
+
button: e.button,
|
|
88
|
+
ctrlKey: e.ctrlKey,
|
|
89
|
+
shiftKey: e.shiftKey,
|
|
90
|
+
altKey: e.altKey,
|
|
91
|
+
metaKey: e.metaKey,
|
|
92
|
+
});
|
|
93
|
+
handleMainMouseMove(clampedEvent);
|
|
81
94
|
}
|
|
82
95
|
|
|
83
96
|
function _onDocumentDragUp(e) {
|
|
@@ -231,6 +244,14 @@ const handleMainMouseDown = (e) => {
|
|
|
231
244
|
};
|
|
232
245
|
|
|
233
246
|
const handleMainMouseMove = (e) => {
|
|
247
|
+
// Auto-scroll when dragging near/past viewport edges (checked first so early returns don't skip it)
|
|
248
|
+
if (e.buttons & 1) {
|
|
249
|
+
_lastDragEvent = e;
|
|
250
|
+
_startAutoScroll();
|
|
251
|
+
} else {
|
|
252
|
+
_stopAutoScroll();
|
|
253
|
+
}
|
|
254
|
+
|
|
234
255
|
if (isSquareToolActive) {
|
|
235
256
|
handleMouseMoveRect(e);
|
|
236
257
|
} else if (isArrowToolActive) {
|
|
@@ -311,14 +332,6 @@ const handleMainMouseMove = (e) => {
|
|
|
311
332
|
handleCodeMouseMove(e);
|
|
312
333
|
}
|
|
313
334
|
}
|
|
314
|
-
|
|
315
|
-
// Auto-scroll when dragging near/past viewport edges
|
|
316
|
-
if (e.buttons & 1) {
|
|
317
|
-
_lastDragEvent = e;
|
|
318
|
-
_startAutoScroll();
|
|
319
|
-
} else {
|
|
320
|
-
_stopAutoScroll();
|
|
321
|
-
}
|
|
322
335
|
};
|
|
323
336
|
|
|
324
337
|
const handleMainMouseUp = (e) => {
|
|
@@ -451,6 +464,15 @@ function _onMouseEnter(e) {
|
|
|
451
464
|
}
|
|
452
465
|
}
|
|
453
466
|
|
|
467
|
+
const handleMainDblClick = (e) => {
|
|
468
|
+
// Double-click on a text group from any tool: enter text edit mode
|
|
469
|
+
const targetTextGroup = e.target.closest('g[data-type="text-group"]');
|
|
470
|
+
if (targetTextGroup) {
|
|
471
|
+
e.stopPropagation();
|
|
472
|
+
enterEditMode(targetTextGroup);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
454
476
|
function initEventDispatcher(svgEl) {
|
|
455
477
|
if (_boundSvg) cleanupEventDispatcher();
|
|
456
478
|
const target = svgEl || svg;
|
|
@@ -459,6 +481,7 @@ function initEventDispatcher(svgEl) {
|
|
|
459
481
|
target.addEventListener('mouseup', handleMainMouseUp);
|
|
460
482
|
target.addEventListener('mouseleave', handleMainMouseLeave);
|
|
461
483
|
target.addEventListener('mouseenter', _onMouseEnter);
|
|
484
|
+
target.addEventListener('dblclick', handleMainDblClick);
|
|
462
485
|
_boundSvg = target;
|
|
463
486
|
}
|
|
464
487
|
|
|
@@ -475,6 +498,7 @@ function cleanupEventDispatcher() {
|
|
|
475
498
|
_boundSvg.removeEventListener('mouseup', handleMainMouseUp);
|
|
476
499
|
_boundSvg.removeEventListener('mouseleave', handleMainMouseLeave);
|
|
477
500
|
_boundSvg.removeEventListener('mouseenter', _onMouseEnter);
|
|
501
|
+
_boundSvg.removeEventListener('dblclick', handleMainDblClick);
|
|
478
502
|
_boundSvg = null;
|
|
479
503
|
}
|
|
480
504
|
}
|
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);
|
|
@@ -151,10 +152,10 @@ function isShapeInSelectionRect(shape, selectionBounds) {
|
|
|
151
152
|
break;
|
|
152
153
|
case 'freehandStroke':
|
|
153
154
|
shapeBounds = {
|
|
154
|
-
x: shape.
|
|
155
|
-
y: shape.
|
|
156
|
-
width: shape.
|
|
157
|
-
height: shape.
|
|
155
|
+
x: shape.x,
|
|
156
|
+
y: shape.y,
|
|
157
|
+
width: shape.width,
|
|
158
|
+
height: shape.height
|
|
158
159
|
};
|
|
159
160
|
break;
|
|
160
161
|
case 'text':
|
|
@@ -389,10 +390,10 @@ class MultiSelection {
|
|
|
389
390
|
};
|
|
390
391
|
case 'freehandStroke':
|
|
391
392
|
return {
|
|
392
|
-
x: shape.
|
|
393
|
-
y: shape.
|
|
394
|
-
width: shape.
|
|
395
|
-
height: shape.
|
|
393
|
+
x: shape.x,
|
|
394
|
+
y: shape.y,
|
|
395
|
+
width: shape.width,
|
|
396
|
+
height: shape.height
|
|
396
397
|
};
|
|
397
398
|
case 'text':
|
|
398
399
|
const textElement = shape.group ? shape.group.querySelector('text') : null;
|
|
@@ -728,7 +729,7 @@ class MultiSelection {
|
|
|
728
729
|
shape.updateBoundingBox();
|
|
729
730
|
}
|
|
730
731
|
|
|
731
|
-
const boundingBox =
|
|
732
|
+
const boundingBox = this.getShapeBounds(shape);
|
|
732
733
|
|
|
733
734
|
shapeData = {
|
|
734
735
|
x: boundingBox.x || 0,
|
|
@@ -1360,6 +1361,8 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1360
1361
|
if (typeof shape.finalizeMove === 'function') {
|
|
1361
1362
|
shape.finalizeMove();
|
|
1362
1363
|
}
|
|
1364
|
+
// Restore isSelected flag (startDrag's removeSelection clears it)
|
|
1365
|
+
shape.isSelected = true;
|
|
1363
1366
|
});
|
|
1364
1367
|
|
|
1365
1368
|
this.initialPositions.clear();
|
|
@@ -1367,6 +1370,9 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1367
1370
|
// Push undo for all moved shapes
|
|
1368
1371
|
this._pushUndoForAll();
|
|
1369
1372
|
|
|
1373
|
+
// Refresh bounds and controls after finalizeMove may have changed positions
|
|
1374
|
+
this.updateControls();
|
|
1375
|
+
|
|
1370
1376
|
if (typeof svg !== 'undefined') {
|
|
1371
1377
|
svg.style.cursor = 'default';
|
|
1372
1378
|
}
|
|
@@ -1420,28 +1426,26 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1420
1426
|
return true;
|
|
1421
1427
|
}
|
|
1422
1428
|
|
|
1423
|
-
if
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
clickedOnSelectedShape = shape;
|
|
1430
|
-
break;
|
|
1431
|
-
}
|
|
1429
|
+
// Check if clicking on any shape that's part of the selection
|
|
1430
|
+
let clickedOnSelectedShape = null;
|
|
1431
|
+
for (const shape of multiSelection.selectedShapes) {
|
|
1432
|
+
if (shape.contains && shape.contains(x, y)) {
|
|
1433
|
+
clickedOnSelectedShape = shape;
|
|
1434
|
+
break;
|
|
1432
1435
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
1437
|
-
|
|
1438
|
-
}
|
|
1439
|
-
multiSelection.startDrag(e);
|
|
1436
|
+
}
|
|
1437
|
+
if (clickedOnSelectedShape) {
|
|
1438
|
+
// Ctrl+Click: toggle shape out of multi-selection
|
|
1439
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1440
|
+
multiSelection.removeShape(clickedOnSelectedShape);
|
|
1440
1441
|
return true;
|
|
1441
1442
|
}
|
|
1442
|
-
|
|
1443
|
-
|
|
1443
|
+
multiSelection.startDrag(e);
|
|
1444
|
+
return true;
|
|
1444
1445
|
}
|
|
1446
|
+
|
|
1447
|
+
// If click is within the selection bounds but not on a shape,
|
|
1448
|
+
// fall through to allow clicking on other shapes or starting a new selection
|
|
1445
1449
|
}
|
|
1446
1450
|
|
|
1447
1451
|
// Check if clicking on individual shape anchors - let them handle it
|
|
@@ -1501,6 +1505,12 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1501
1505
|
return true;
|
|
1502
1506
|
}
|
|
1503
1507
|
|
|
1508
|
+
// If the clicked shape is part of the multi-selection, start dragging instead of clearing
|
|
1509
|
+
if (multiSelection.selectedShapes.size > 1 && multiSelection.selectedShapes.has(clickedShape)) {
|
|
1510
|
+
multiSelection.startDrag(e);
|
|
1511
|
+
return true;
|
|
1512
|
+
}
|
|
1513
|
+
|
|
1504
1514
|
// If it's the same shape that's already selected, let individual handlers manage it
|
|
1505
1515
|
if (currentShape === clickedShape) {
|
|
1506
1516
|
// Clear any other multi-selected shapes so only this one remains selected
|
package/src/shapes/Arrow.js
CHANGED
|
@@ -418,6 +418,37 @@ class Arrow {
|
|
|
418
418
|
};
|
|
419
419
|
}
|
|
420
420
|
|
|
421
|
+
_updateAnchorPositions() {
|
|
422
|
+
if (!this.anchors || this.anchors.length === 0) return;
|
|
423
|
+
|
|
424
|
+
const anchorSize = 5 / currentZoom;
|
|
425
|
+
let anchorPositions = [this.startPoint, this.endPoint];
|
|
426
|
+
|
|
427
|
+
if (this.arrowCurved === "curved" && this.controlPoint1 && this.controlPoint2) {
|
|
428
|
+
const midOnCurve = this.getCubicBezierPoint(0.5);
|
|
429
|
+
anchorPositions.push(midOnCurve);
|
|
430
|
+
} else if (this.arrowCurved === "elbow") {
|
|
431
|
+
const elbowXVal = this.elbowX !== null ? this.elbowX : (this.startPoint.x + this.endPoint.x) / 2;
|
|
432
|
+
const midY = (this.startPoint.y + this.endPoint.y) / 2;
|
|
433
|
+
anchorPositions.push({ x: elbowXVal, y: midY });
|
|
434
|
+
} else {
|
|
435
|
+
// straight — offset end anchor past arrowhead
|
|
436
|
+
const arrowAngle = Math.atan2(this.endPoint.y - this.startPoint.y, this.endPoint.x - this.startPoint.x);
|
|
437
|
+
const arrowHeadClearance = this.arrowHeadLength + anchorSize - 10;
|
|
438
|
+
anchorPositions[1] = {
|
|
439
|
+
x: this.endPoint.x + arrowHeadClearance * Math.cos(arrowAngle),
|
|
440
|
+
y: this.endPoint.y + arrowHeadClearance * Math.sin(arrowAngle)
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
anchorPositions.forEach((point, index) => {
|
|
445
|
+
if (this.anchors[index]) {
|
|
446
|
+
this.anchors[index].setAttribute('cx', point.x);
|
|
447
|
+
this.anchors[index].setAttribute('cy', point.y);
|
|
448
|
+
}
|
|
449
|
+
});
|
|
450
|
+
}
|
|
451
|
+
|
|
421
452
|
_updateLabelElement() {
|
|
422
453
|
if (!this.label) {
|
|
423
454
|
if (this.labelElement && this.labelElement.parentNode === this.group) {
|
|
@@ -1633,6 +1664,7 @@ static getFrameAttachmentPoint(point, frame, tolerance = 20) {
|
|
|
1633
1664
|
this._updatePathElement();
|
|
1634
1665
|
this._updateHitArea();
|
|
1635
1666
|
this._updateLabelElement();
|
|
1667
|
+
this._updateAnchorPositions();
|
|
1636
1668
|
|
|
1637
1669
|
// Only update frame containment if we're actively dragging the shape itself
|
|
1638
1670
|
// and not being moved by a parent frame
|
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
|
|
|
@@ -63,11 +63,14 @@ class FreehandStroke {
|
|
|
63
63
|
this.group = document.createElementNS('http://www.w3.org/2000/svg', 'g');
|
|
64
64
|
this.anchors = [];
|
|
65
65
|
this.rotationAnchor = null;
|
|
66
|
+
this.rotationLine = null;
|
|
66
67
|
this.selectionPadding = 8;
|
|
67
68
|
this.selectionOutline = null;
|
|
68
69
|
this.boundingBox = { x: 0, y: 0, width: 0, height: 0 };
|
|
69
70
|
this.shapeName = "freehandStroke";
|
|
70
|
-
|
|
71
|
+
this._moveOffsetX = 0;
|
|
72
|
+
this._moveOffsetY = 0;
|
|
73
|
+
|
|
71
74
|
// Frame attachment properties
|
|
72
75
|
this.parentFrame = null;
|
|
73
76
|
|
|
@@ -76,25 +79,25 @@ class FreehandStroke {
|
|
|
76
79
|
}
|
|
77
80
|
|
|
78
81
|
// Add position and dimension properties for frame compatibility
|
|
82
|
+
// Getters include pending move offset so callers see the visual position
|
|
79
83
|
get x() {
|
|
80
|
-
return this.boundingBox.x;
|
|
84
|
+
return this.boundingBox.x + (this._moveOffsetX || 0);
|
|
81
85
|
}
|
|
82
|
-
|
|
86
|
+
|
|
83
87
|
set x(value) {
|
|
84
|
-
const dx = value - this.boundingBox.x;
|
|
85
|
-
|
|
86
|
-
this.
|
|
87
|
-
this.boundingBox.x = value;
|
|
88
|
+
const dx = value - this.boundingBox.x - (this._moveOffsetX || 0);
|
|
89
|
+
this.points = this.points.map(point => [point[0] + dx, point[1], point[2] || 0.5]);
|
|
90
|
+
this.boundingBox.x = value - (this._moveOffsetX || 0);
|
|
88
91
|
}
|
|
89
|
-
|
|
92
|
+
|
|
90
93
|
get y() {
|
|
91
|
-
return this.boundingBox.y;
|
|
94
|
+
return this.boundingBox.y + (this._moveOffsetY || 0);
|
|
92
95
|
}
|
|
93
|
-
|
|
96
|
+
|
|
94
97
|
set y(value) {
|
|
95
|
-
const dy = value - this.boundingBox.y;
|
|
98
|
+
const dy = value - this.boundingBox.y - (this._moveOffsetY || 0);
|
|
96
99
|
this.points = this.points.map(point => [point[0], point[1] + dy, point[2] || 0.5]);
|
|
97
|
-
this.boundingBox.y = value;
|
|
100
|
+
this.boundingBox.y = value - (this._moveOffsetY || 0);
|
|
98
101
|
}
|
|
99
102
|
|
|
100
103
|
get width() {
|
|
@@ -343,14 +346,6 @@ class FreehandStroke {
|
|
|
343
346
|
const centerY = this.boundingBox.y + this.boundingBox.height / 2;
|
|
344
347
|
const rot = this.rotation ? `rotate(${this.rotation} ${centerX} ${centerY})` : '';
|
|
345
348
|
this.group.setAttribute('transform', `translate(${this._moveOffsetX}, ${this._moveOffsetY}) ${rot}`);
|
|
346
|
-
|
|
347
|
-
// Only update frame containment if we're actively dragging the shape itself
|
|
348
|
-
// and not being moved by a parent frame
|
|
349
|
-
if (isDraggingStroke && !this.isBeingMovedByFrame) {
|
|
350
|
-
this.updateFrameContainment();
|
|
351
|
-
}
|
|
352
|
-
|
|
353
|
-
this.updateAttachedArrows();
|
|
354
349
|
}
|
|
355
350
|
|
|
356
351
|
// Call after drag ends to bake the offset into actual point coordinates
|
|
@@ -366,37 +361,9 @@ class FreehandStroke {
|
|
|
366
361
|
}
|
|
367
362
|
|
|
368
363
|
updateAttachedArrows() {
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
updateFrameContainment() {
|
|
373
|
-
// Don't update if we're being moved by a frame
|
|
374
|
-
if (this.isBeingMovedByFrame) return;
|
|
375
|
-
|
|
376
|
-
let targetFrame = null;
|
|
377
|
-
|
|
378
|
-
// Find which frame this shape is over
|
|
379
|
-
shapes.forEach(shape => {
|
|
380
|
-
if (shape.shapeName === 'frame' && shape.isShapeInFrame(this)) {
|
|
381
|
-
targetFrame = shape;
|
|
382
|
-
}
|
|
383
|
-
});
|
|
384
|
-
|
|
385
|
-
// If we have a parent frame and we're being dragged, temporarily remove clipping
|
|
386
|
-
if (this.parentFrame && isDraggingStroke) {
|
|
387
|
-
this.parentFrame.temporarilyRemoveFromFrame(this);
|
|
388
|
-
}
|
|
389
|
-
|
|
390
|
-
// Update frame highlighting
|
|
391
|
-
if (hoveredFrameStroke && hoveredFrameStroke !== targetFrame) {
|
|
392
|
-
hoveredFrameStroke.removeHighlight();
|
|
364
|
+
if (typeof window.__updateArrowsForShape === 'function') {
|
|
365
|
+
window.__updateArrowsForShape(this);
|
|
393
366
|
}
|
|
394
|
-
|
|
395
|
-
if (targetFrame && targetFrame !== hoveredFrameStroke) {
|
|
396
|
-
targetFrame.highlightFrame();
|
|
397
|
-
}
|
|
398
|
-
|
|
399
|
-
hoveredFrameStroke = targetFrame;
|
|
400
367
|
}
|
|
401
368
|
|
|
402
369
|
selectStroke() {
|
|
@@ -425,11 +392,15 @@ class FreehandStroke {
|
|
|
425
392
|
if (this.rotationAnchor && this.rotationAnchor.parentNode === this.group) {
|
|
426
393
|
this.group.removeChild(this.rotationAnchor);
|
|
427
394
|
}
|
|
395
|
+
if (this.rotationLine && this.rotationLine.parentNode === this.group) {
|
|
396
|
+
this.group.removeChild(this.rotationLine);
|
|
397
|
+
}
|
|
428
398
|
if (this.selectionOutline && this.selectionOutline.parentNode === this.group) {
|
|
429
399
|
this.group.removeChild(this.selectionOutline);
|
|
430
400
|
}
|
|
431
401
|
this.anchors = [];
|
|
432
402
|
this.rotationAnchor = null;
|
|
403
|
+
this.rotationLine = null;
|
|
433
404
|
this.selectionOutline = null;
|
|
434
405
|
this.isSelected = false;
|
|
435
406
|
}
|
|
@@ -465,10 +436,14 @@ class FreehandStroke {
|
|
|
465
436
|
const buffer = 10;
|
|
466
437
|
const anchorSize = 10 / currentZoom;
|
|
467
438
|
|
|
439
|
+
// Account for pending move offset
|
|
440
|
+
const ox = this._moveOffsetX || 0;
|
|
441
|
+
const oy = this._moveOffsetY || 0;
|
|
442
|
+
|
|
468
443
|
// Transform the input coordinates to account for rotation
|
|
469
|
-
const centerX = this.boundingBox.x + this.boundingBox.width / 2;
|
|
470
|
-
const centerY = this.boundingBox.y + this.boundingBox.height / 2;
|
|
471
|
-
|
|
444
|
+
const centerX = this.boundingBox.x + ox + this.boundingBox.width / 2;
|
|
445
|
+
const centerY = this.boundingBox.y + oy + this.boundingBox.height / 2;
|
|
446
|
+
|
|
472
447
|
// Rotate the mouse coordinates to the shape's local coordinate system
|
|
473
448
|
const angleRad = -this.rotation * Math.PI / 180;
|
|
474
449
|
const dx = x - centerX;
|
|
@@ -477,8 +452,8 @@ class FreehandStroke {
|
|
|
477
452
|
const localY = dx * Math.sin(angleRad) + dy * Math.cos(angleRad) + centerY;
|
|
478
453
|
|
|
479
454
|
// Check resize anchors in local space
|
|
480
|
-
const expandedX = this.boundingBox.x - this.selectionPadding;
|
|
481
|
-
const expandedY = this.boundingBox.y - this.selectionPadding;
|
|
455
|
+
const expandedX = this.boundingBox.x + ox - this.selectionPadding;
|
|
456
|
+
const expandedY = this.boundingBox.y + oy - this.selectionPadding;
|
|
482
457
|
const expandedWidth = this.boundingBox.width + 2 * this.selectionPadding;
|
|
483
458
|
const expandedHeight = this.boundingBox.height + 2 * this.selectionPadding;
|
|
484
459
|
|
|
@@ -624,6 +599,7 @@ class FreehandStroke {
|
|
|
624
599
|
rotationLine.setAttribute('stroke-dasharray', '2 2');
|
|
625
600
|
rotationLine.setAttribute('style', 'pointer-events: none;');
|
|
626
601
|
this.group.appendChild(rotationLine);
|
|
602
|
+
this.rotationLine = rotationLine;
|
|
627
603
|
|
|
628
604
|
// Show sidebar when anchors are added (when shape is selected)
|
|
629
605
|
disableAllSideBars();
|
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
|
|
|
@@ -4,6 +4,9 @@ import { pushCreateAction, pushDeleteAction, pushOptionsChangeAction, pushTransf
|
|
|
4
4
|
import { updateAttachedArrows as updateArrowsForShape, cleanupAttachments } from './arrowTool.js';
|
|
5
5
|
import { calculateSnap, clearSnapGuides } from '../core/SnapGuides.js';
|
|
6
6
|
|
|
7
|
+
// Expose updateArrowsForShape globally so FreehandStroke.updateAttachedArrows() can call it
|
|
8
|
+
// (FreehandStroke.js is a separate module and cannot import freehandTool's local bindings)
|
|
9
|
+
window.__updateArrowsForShape = updateArrowsForShape;
|
|
7
10
|
|
|
8
11
|
const strokeColors = document.querySelectorAll(".strokeColors span");
|
|
9
12
|
const strokeThicknesses = document.querySelectorAll(".strokeThickness span");
|
|
@@ -80,6 +83,31 @@ function getSvgPathFromStroke(stroke) {
|
|
|
80
83
|
return pathData.join(' ');
|
|
81
84
|
}
|
|
82
85
|
|
|
86
|
+
// Frame containment check for a freehand stroke during drag.
|
|
87
|
+
// Lives here (not in FreehandStroke.js) because it needs isDraggingStroke & hoveredFrameStroke.
|
|
88
|
+
function updateFrameContainmentForStroke(shape) {
|
|
89
|
+
if (shape.isBeingMovedByFrame) return;
|
|
90
|
+
|
|
91
|
+
let targetFrame = null;
|
|
92
|
+
shapes.forEach(s => {
|
|
93
|
+
if (s.shapeName === 'frame' && s.isShapeInFrame(shape)) {
|
|
94
|
+
targetFrame = s;
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
if (shape.parentFrame && isDraggingStroke) {
|
|
99
|
+
shape.parentFrame.temporarilyRemoveFromFrame(shape);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (hoveredFrameStroke && hoveredFrameStroke !== targetFrame) {
|
|
103
|
+
hoveredFrameStroke.removeHighlight();
|
|
104
|
+
}
|
|
105
|
+
if (targetFrame && targetFrame !== hoveredFrameStroke) {
|
|
106
|
+
targetFrame.highlightFrame();
|
|
107
|
+
}
|
|
108
|
+
hoveredFrameStroke = targetFrame;
|
|
109
|
+
}
|
|
110
|
+
|
|
83
111
|
// Delete functionality
|
|
84
112
|
function deleteCurrentShape() {
|
|
85
113
|
if (currentShape && currentShape.shapeName === 'freehandStroke') {
|
|
@@ -304,6 +332,14 @@ function handleMouseMove(e) {
|
|
|
304
332
|
startX = x;
|
|
305
333
|
startY = y;
|
|
306
334
|
|
|
335
|
+
// Frame containment update (must live here where isDraggingStroke is in scope)
|
|
336
|
+
if (!currentShape.isBeingMovedByFrame) {
|
|
337
|
+
updateFrameContainmentForStroke(currentShape);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
// Update arrows attached to this shape
|
|
341
|
+
currentShape.updateAttachedArrows();
|
|
342
|
+
|
|
307
343
|
// Snap guides
|
|
308
344
|
if (window.__sketchStoreApi && window.__sketchStoreApi.getState().snapToObjects) {
|
|
309
345
|
const snap = calculateSnap(currentShape, e.shiftKey, e.clientX, e.clientY);
|
|
@@ -334,7 +370,7 @@ function handleMouseUp(e) {
|
|
|
334
370
|
if (currentStroke && currentStroke.points.length >= 2) {
|
|
335
371
|
currentStroke.draw(); // Redraw with final smoothing
|
|
336
372
|
pushCreateAction(currentStroke);
|
|
337
|
-
|
|
373
|
+
|
|
338
374
|
// Check for frame containment and track attachment
|
|
339
375
|
const finalFrame = hoveredFrameStroke;
|
|
340
376
|
if (finalFrame) {
|
|
@@ -342,6 +378,12 @@ function handleMouseUp(e) {
|
|
|
342
378
|
// Track the attachment for undo
|
|
343
379
|
pushFrameAttachmentAction(finalFrame, currentStroke, 'attach', null);
|
|
344
380
|
}
|
|
381
|
+
|
|
382
|
+
// Auto-select the drawn stroke and switch to selection tool
|
|
383
|
+
const drawnShape = currentStroke;
|
|
384
|
+
if (window.__sketchStoreApi) window.__sketchStoreApi.setActiveTool('select', { afterDraw: true });
|
|
385
|
+
currentShape = drawnShape;
|
|
386
|
+
currentShape.selectStroke();
|
|
345
387
|
} else if (currentStroke) {
|
|
346
388
|
// Remove strokes that are too small
|
|
347
389
|
shapes.pop();
|
|
@@ -349,13 +391,13 @@ function handleMouseUp(e) {
|
|
|
349
391
|
currentStroke.group.parentNode.removeChild(currentStroke.group);
|
|
350
392
|
}
|
|
351
393
|
}
|
|
352
|
-
|
|
394
|
+
|
|
353
395
|
// Clear frame highlighting
|
|
354
396
|
if (hoveredFrameStroke) {
|
|
355
397
|
hoveredFrameStroke.removeHighlight();
|
|
356
398
|
hoveredFrameStroke = null;
|
|
357
399
|
}
|
|
358
|
-
|
|
400
|
+
|
|
359
401
|
currentStroke = null;
|
|
360
402
|
}
|
|
361
403
|
|
package/src/tools/textTool.js
CHANGED
|
@@ -51,6 +51,7 @@ let rotationStartTransform = null;
|
|
|
51
51
|
let initialHandlePosRelGroup = null;
|
|
52
52
|
let initialGroupTx = 0;
|
|
53
53
|
let initialGroupTy = 0;
|
|
54
|
+
let initialInverseScreenCTM = null;
|
|
54
55
|
|
|
55
56
|
// Frame attachment variables
|
|
56
57
|
let draggedShapeInitialFrameText = null;
|
|
@@ -753,6 +754,10 @@ function startResize(event, anchor) {
|
|
|
753
754
|
|
|
754
755
|
startPoint = getSVGCoordinates(event, selectedElement);
|
|
755
756
|
|
|
757
|
+
// Freeze the group's screen CTM at resize start so mouse→local mapping stays stable
|
|
758
|
+
const groupScreenCTM = selectedElement.getScreenCTM();
|
|
759
|
+
initialInverseScreenCTM = groupScreenCTM ? groupScreenCTM.inverse() : null;
|
|
760
|
+
|
|
756
761
|
const currentTransform = selectedElement.transform.baseVal.consolidate();
|
|
757
762
|
initialGroupTx = currentTransform ? currentTransform.matrix.e : 0;
|
|
758
763
|
initialGroupTy = currentTransform ? currentTransform.matrix.f : 0;
|
|
@@ -828,7 +833,16 @@ const handleMouseMove = (event) => {
|
|
|
828
833
|
const textElement = selectedElement.querySelector('text');
|
|
829
834
|
if (!textElement || !startBBox || startFontSize === null || !startPoint || !initialHandlePosRelGroup) return;
|
|
830
835
|
|
|
831
|
-
|
|
836
|
+
// Use the frozen initial CTM so the mapping doesn't shift as we change the group transform
|
|
837
|
+
let currentPoint;
|
|
838
|
+
if (initialInverseScreenCTM) {
|
|
839
|
+
const pt = svg.createSVGPoint();
|
|
840
|
+
pt.x = event.clientX;
|
|
841
|
+
pt.y = event.clientY;
|
|
842
|
+
currentPoint = pt.matrixTransform(initialInverseScreenCTM);
|
|
843
|
+
} else {
|
|
844
|
+
currentPoint = getSVGCoordinates(event, selectedElement);
|
|
845
|
+
}
|
|
832
846
|
|
|
833
847
|
const startX = startBBox.x;
|
|
834
848
|
const startY = startBBox.y;
|
|
@@ -1820,4 +1834,4 @@ window.__setCodeLanguage = function(lang) {
|
|
|
1820
1834
|
}
|
|
1821
1835
|
};
|
|
1822
1836
|
|
|
1823
|
-
export { handleTextMouseDown, handleTextMouseMove, handleTextMouseUp, updateCodeToggleForShape, deselectElement as deselectTextElement };
|
|
1837
|
+
export { handleTextMouseDown, handleTextMouseMove, handleTextMouseUp, updateCodeToggleForShape, deselectElement as deselectTextElement, enterEditMode };
|