@elixpo/lixsketch 4.6.3 → 5.4.1
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 +129 -0
- package/src/core/Selection.js +36 -22
- package/src/shapes/Arrow.js +32 -0
- package/src/shapes/FreehandStroke.js +48 -66
- package/src/tools/freehandTool.js +45 -3
package/package.json
CHANGED
|
@@ -14,6 +14,94 @@ import { handleMultiSelectionMouseDown, handleMultiSelectionMouseMove, handleMul
|
|
|
14
14
|
import { handleMouseDownIcon, handleMouseMoveIcon, handleMouseUpIcon } from '../tools/iconTool.js';
|
|
15
15
|
import { handleCodeMouseDown, handleCodeMouseMove, handleCodeMouseUp } from '../tools/codeTool.js';
|
|
16
16
|
|
|
17
|
+
// === Auto-scroll when dragging near viewport edges ===
|
|
18
|
+
const EDGE_THRESHOLD = 40; // px from edge to start scrolling
|
|
19
|
+
const SCROLL_SPEED = 8; // base px per frame (scaled by zoom)
|
|
20
|
+
let _autoScrollRAF = null;
|
|
21
|
+
|
|
22
|
+
function _autoScroll(e) {
|
|
23
|
+
if (!(e.buttons & 1)) { _stopAutoScroll(); return; } // no primary button
|
|
24
|
+
if (typeof currentViewBox === 'undefined' || typeof currentZoom === 'undefined') return;
|
|
25
|
+
if (typeof isPanning !== 'undefined' && isPanning) return; // don't fight pan tool
|
|
26
|
+
if (typeof isPanningToolActive !== 'undefined' && isPanningToolActive) return;
|
|
27
|
+
|
|
28
|
+
const rect = svg.getBoundingClientRect();
|
|
29
|
+
const mx = e.clientX - rect.left;
|
|
30
|
+
const my = e.clientY - rect.top;
|
|
31
|
+
|
|
32
|
+
let dx = 0, dy = 0;
|
|
33
|
+
if (mx < EDGE_THRESHOLD) dx = -SCROLL_SPEED * (1 - mx / EDGE_THRESHOLD);
|
|
34
|
+
else if (mx > rect.width - EDGE_THRESHOLD) dx = SCROLL_SPEED * (1 - (rect.width - mx) / EDGE_THRESHOLD);
|
|
35
|
+
if (my < EDGE_THRESHOLD) dy = -SCROLL_SPEED * (1 - my / EDGE_THRESHOLD);
|
|
36
|
+
else if (my > rect.height - EDGE_THRESHOLD) dy = SCROLL_SPEED * (1 - (rect.height - my) / EDGE_THRESHOLD);
|
|
37
|
+
|
|
38
|
+
// Also scroll when cursor is outside the canvas entirely
|
|
39
|
+
if (e.clientX < rect.left) dx = -SCROLL_SPEED;
|
|
40
|
+
else if (e.clientX > rect.right) dx = SCROLL_SPEED;
|
|
41
|
+
if (e.clientY < rect.top) dy = -SCROLL_SPEED;
|
|
42
|
+
else if (e.clientY > rect.bottom) dy = SCROLL_SPEED;
|
|
43
|
+
|
|
44
|
+
if (dx === 0 && dy === 0) { _stopAutoScroll(); return; }
|
|
45
|
+
|
|
46
|
+
// Scale speed inversely with zoom so scrolling feels consistent
|
|
47
|
+
const scale = 1 / currentZoom;
|
|
48
|
+
currentViewBox.x += dx * scale;
|
|
49
|
+
currentViewBox.y += dy * scale;
|
|
50
|
+
svg.setAttribute('viewBox', `${currentViewBox.x} ${currentViewBox.y} ${currentViewBox.width} ${currentViewBox.height}`);
|
|
51
|
+
if (typeof freehandCanvas !== 'undefined' && freehandCanvas !== svg) {
|
|
52
|
+
freehandCanvas.setAttribute('viewBox', `${currentViewBox.x} ${currentViewBox.y} ${currentViewBox.width} ${currentViewBox.height}`);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function _startAutoScroll() {
|
|
57
|
+
if (_autoScrollRAF) return;
|
|
58
|
+
const tick = () => {
|
|
59
|
+
if (_lastDragEvent) _autoScroll(_lastDragEvent);
|
|
60
|
+
_autoScrollRAF = requestAnimationFrame(tick);
|
|
61
|
+
};
|
|
62
|
+
_autoScrollRAF = requestAnimationFrame(tick);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function _stopAutoScroll() {
|
|
66
|
+
if (_autoScrollRAF) {
|
|
67
|
+
cancelAnimationFrame(_autoScrollRAF);
|
|
68
|
+
_autoScrollRAF = null;
|
|
69
|
+
}
|
|
70
|
+
_lastDragEvent = null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
let _lastDragEvent = null;
|
|
74
|
+
let _documentDragActive = false;
|
|
75
|
+
|
|
76
|
+
function _onDocumentDragMove(e) {
|
|
77
|
+
_lastDragEvent = e;
|
|
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);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
function _onDocumentDragUp(e) {
|
|
97
|
+
_stopAutoScroll();
|
|
98
|
+
document.removeEventListener('mousemove', _onDocumentDragMove);
|
|
99
|
+
document.removeEventListener('mouseup', _onDocumentDragUp);
|
|
100
|
+
_documentDragActive = false;
|
|
101
|
+
// Finalize the operation
|
|
102
|
+
handleMainMouseUp(e);
|
|
103
|
+
}
|
|
104
|
+
|
|
17
105
|
const handleMainMouseDown = (e) => {
|
|
18
106
|
// Safety: remove any stray selection rectangle from a previous interrupted drag
|
|
19
107
|
removeMultiSelectionRect();
|
|
@@ -156,6 +244,14 @@ const handleMainMouseDown = (e) => {
|
|
|
156
244
|
};
|
|
157
245
|
|
|
158
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
|
+
|
|
159
255
|
if (isSquareToolActive) {
|
|
160
256
|
handleMouseMoveRect(e);
|
|
161
257
|
} else if (isArrowToolActive) {
|
|
@@ -239,6 +335,7 @@ const handleMainMouseMove = (e) => {
|
|
|
239
335
|
};
|
|
240
336
|
|
|
241
337
|
const handleMainMouseUp = (e) => {
|
|
338
|
+
_stopAutoScroll();
|
|
242
339
|
if (isSquareToolActive) {
|
|
243
340
|
handleMouseUpRect(e);
|
|
244
341
|
} else if (isArrowToolActive) {
|
|
@@ -318,6 +415,21 @@ const handleMainMouseUp = (e) => {
|
|
|
318
415
|
};
|
|
319
416
|
|
|
320
417
|
const handleMainMouseLeave = (e) => {
|
|
418
|
+
// If the user is still holding the primary button (dragging outside the canvas),
|
|
419
|
+
// keep auto-scrolling and don't finalize the operation yet.
|
|
420
|
+
if (e.buttons & 1) {
|
|
421
|
+
_lastDragEvent = e;
|
|
422
|
+
_startAutoScroll();
|
|
423
|
+
|
|
424
|
+
// Listen on document to continue receiving move events outside the SVG
|
|
425
|
+
if (!_documentDragActive) {
|
|
426
|
+
_documentDragActive = true;
|
|
427
|
+
document.addEventListener('mousemove', _onDocumentDragMove);
|
|
428
|
+
document.addEventListener('mouseup', _onDocumentDragUp);
|
|
429
|
+
}
|
|
430
|
+
return;
|
|
431
|
+
}
|
|
432
|
+
|
|
321
433
|
// Stop all active drawing tools when pointer leaves the canvas
|
|
322
434
|
|
|
323
435
|
// Fire mouseUp for whichever tool is active to finalize/cancel the operation
|
|
@@ -343,6 +455,15 @@ const handleMainMouseLeave = (e) => {
|
|
|
343
455
|
|
|
344
456
|
let _boundSvg = null;
|
|
345
457
|
|
|
458
|
+
function _onMouseEnter(e) {
|
|
459
|
+
// Cursor re-entered the SVG — stop document-level tracking, SVG handles events again
|
|
460
|
+
if (_documentDragActive) {
|
|
461
|
+
document.removeEventListener('mousemove', _onDocumentDragMove);
|
|
462
|
+
document.removeEventListener('mouseup', _onDocumentDragUp);
|
|
463
|
+
_documentDragActive = false;
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
|
|
346
467
|
function initEventDispatcher(svgEl) {
|
|
347
468
|
if (_boundSvg) cleanupEventDispatcher();
|
|
348
469
|
const target = svgEl || svg;
|
|
@@ -350,15 +471,23 @@ function initEventDispatcher(svgEl) {
|
|
|
350
471
|
target.addEventListener('mousemove', handleMainMouseMove);
|
|
351
472
|
target.addEventListener('mouseup', handleMainMouseUp);
|
|
352
473
|
target.addEventListener('mouseleave', handleMainMouseLeave);
|
|
474
|
+
target.addEventListener('mouseenter', _onMouseEnter);
|
|
353
475
|
_boundSvg = target;
|
|
354
476
|
}
|
|
355
477
|
|
|
356
478
|
function cleanupEventDispatcher() {
|
|
479
|
+
_stopAutoScroll();
|
|
480
|
+
if (_documentDragActive) {
|
|
481
|
+
document.removeEventListener('mousemove', _onDocumentDragMove);
|
|
482
|
+
document.removeEventListener('mouseup', _onDocumentDragUp);
|
|
483
|
+
_documentDragActive = false;
|
|
484
|
+
}
|
|
357
485
|
if (_boundSvg) {
|
|
358
486
|
_boundSvg.removeEventListener('mousedown', handleMainMouseDown);
|
|
359
487
|
_boundSvg.removeEventListener('mousemove', handleMainMouseMove);
|
|
360
488
|
_boundSvg.removeEventListener('mouseup', handleMainMouseUp);
|
|
361
489
|
_boundSvg.removeEventListener('mouseleave', handleMainMouseLeave);
|
|
490
|
+
_boundSvg.removeEventListener('mouseenter', _onMouseEnter);
|
|
362
491
|
_boundSvg = null;
|
|
363
492
|
}
|
|
364
493
|
}
|
package/src/core/Selection.js
CHANGED
|
@@ -151,10 +151,10 @@ function isShapeInSelectionRect(shape, selectionBounds) {
|
|
|
151
151
|
break;
|
|
152
152
|
case 'freehandStroke':
|
|
153
153
|
shapeBounds = {
|
|
154
|
-
x: shape.
|
|
155
|
-
y: shape.
|
|
156
|
-
width: shape.
|
|
157
|
-
height: shape.
|
|
154
|
+
x: shape.x,
|
|
155
|
+
y: shape.y,
|
|
156
|
+
width: shape.width,
|
|
157
|
+
height: shape.height
|
|
158
158
|
};
|
|
159
159
|
break;
|
|
160
160
|
case 'text':
|
|
@@ -389,10 +389,10 @@ class MultiSelection {
|
|
|
389
389
|
};
|
|
390
390
|
case 'freehandStroke':
|
|
391
391
|
return {
|
|
392
|
-
x: shape.
|
|
393
|
-
y: shape.
|
|
394
|
-
width: shape.
|
|
395
|
-
height: shape.
|
|
392
|
+
x: shape.x,
|
|
393
|
+
y: shape.y,
|
|
394
|
+
width: shape.width,
|
|
395
|
+
height: shape.height
|
|
396
396
|
};
|
|
397
397
|
case 'text':
|
|
398
398
|
const textElement = shape.group ? shape.group.querySelector('text') : null;
|
|
@@ -728,7 +728,7 @@ class MultiSelection {
|
|
|
728
728
|
shape.updateBoundingBox();
|
|
729
729
|
}
|
|
730
730
|
|
|
731
|
-
const boundingBox =
|
|
731
|
+
const boundingBox = this.getShapeBounds(shape);
|
|
732
732
|
|
|
733
733
|
shapeData = {
|
|
734
734
|
x: boundingBox.x || 0,
|
|
@@ -1360,6 +1360,8 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1360
1360
|
if (typeof shape.finalizeMove === 'function') {
|
|
1361
1361
|
shape.finalizeMove();
|
|
1362
1362
|
}
|
|
1363
|
+
// Restore isSelected flag (startDrag's removeSelection clears it)
|
|
1364
|
+
shape.isSelected = true;
|
|
1363
1365
|
});
|
|
1364
1366
|
|
|
1365
1367
|
this.initialPositions.clear();
|
|
@@ -1367,6 +1369,9 @@ createRotatedControls(angleDiff = 0) {
|
|
|
1367
1369
|
// Push undo for all moved shapes
|
|
1368
1370
|
this._pushUndoForAll();
|
|
1369
1371
|
|
|
1372
|
+
// Refresh bounds and controls after finalizeMove may have changed positions
|
|
1373
|
+
this.updateControls();
|
|
1374
|
+
|
|
1370
1375
|
if (typeof svg !== 'undefined') {
|
|
1371
1376
|
svg.style.cursor = 'default';
|
|
1372
1377
|
}
|
|
@@ -1420,23 +1425,26 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1420
1425
|
return true;
|
|
1421
1426
|
}
|
|
1422
1427
|
|
|
1423
|
-
if
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
clickedOnSelectedShape = true;
|
|
1430
|
-
break;
|
|
1431
|
-
}
|
|
1428
|
+
// Check if clicking on any shape that's part of the selection
|
|
1429
|
+
let clickedOnSelectedShape = null;
|
|
1430
|
+
for (const shape of multiSelection.selectedShapes) {
|
|
1431
|
+
if (shape.contains && shape.contains(x, y)) {
|
|
1432
|
+
clickedOnSelectedShape = shape;
|
|
1433
|
+
break;
|
|
1432
1434
|
}
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
+
}
|
|
1436
|
+
if (clickedOnSelectedShape) {
|
|
1437
|
+
// Ctrl+Click: toggle shape out of multi-selection
|
|
1438
|
+
if (e.ctrlKey || e.metaKey) {
|
|
1439
|
+
multiSelection.removeShape(clickedOnSelectedShape);
|
|
1435
1440
|
return true;
|
|
1436
1441
|
}
|
|
1437
|
-
|
|
1438
|
-
|
|
1442
|
+
multiSelection.startDrag(e);
|
|
1443
|
+
return true;
|
|
1439
1444
|
}
|
|
1445
|
+
|
|
1446
|
+
// If click is within the selection bounds but not on a shape,
|
|
1447
|
+
// fall through to allow clicking on other shapes or starting a new selection
|
|
1440
1448
|
}
|
|
1441
1449
|
|
|
1442
1450
|
// Check if clicking on individual shape anchors - let them handle it
|
|
@@ -1496,6 +1504,12 @@ function handleMultiSelectionMouseDown(e) {
|
|
|
1496
1504
|
return true;
|
|
1497
1505
|
}
|
|
1498
1506
|
|
|
1507
|
+
// If the clicked shape is part of the multi-selection, start dragging instead of clearing
|
|
1508
|
+
if (multiSelection.selectedShapes.size > 1 && multiSelection.selectedShapes.has(clickedShape)) {
|
|
1509
|
+
multiSelection.startDrag(e);
|
|
1510
|
+
return true;
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1499
1513
|
// If it's the same shape that's already selected, let individual handlers manage it
|
|
1500
1514
|
if (currentShape === clickedShape) {
|
|
1501
1515
|
// 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
|
|
@@ -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() {
|
|
@@ -339,16 +342,10 @@ class FreehandStroke {
|
|
|
339
342
|
// Accumulate offset for transform-based movement (avoids full path rebuild)
|
|
340
343
|
this._moveOffsetX = (this._moveOffsetX || 0) + dx;
|
|
341
344
|
this._moveOffsetY = (this._moveOffsetY || 0) + dy;
|
|
342
|
-
const
|
|
345
|
+
const centerX = this.boundingBox.x + this.boundingBox.width / 2;
|
|
346
|
+
const centerY = this.boundingBox.y + this.boundingBox.height / 2;
|
|
347
|
+
const rot = this.rotation ? `rotate(${this.rotation} ${centerX} ${centerY})` : '';
|
|
343
348
|
this.group.setAttribute('transform', `translate(${this._moveOffsetX}, ${this._moveOffsetY}) ${rot}`);
|
|
344
|
-
|
|
345
|
-
// Only update frame containment if we're actively dragging the shape itself
|
|
346
|
-
// and not being moved by a parent frame
|
|
347
|
-
if (isDraggingStroke && !this.isBeingMovedByFrame) {
|
|
348
|
-
this.updateFrameContainment();
|
|
349
|
-
}
|
|
350
|
-
|
|
351
|
-
this.updateAttachedArrows();
|
|
352
349
|
}
|
|
353
350
|
|
|
354
351
|
// Call after drag ends to bake the offset into actual point coordinates
|
|
@@ -364,37 +361,9 @@ class FreehandStroke {
|
|
|
364
361
|
}
|
|
365
362
|
|
|
366
363
|
updateAttachedArrows() {
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
updateFrameContainment() {
|
|
371
|
-
// Don't update if we're being moved by a frame
|
|
372
|
-
if (this.isBeingMovedByFrame) return;
|
|
373
|
-
|
|
374
|
-
let targetFrame = null;
|
|
375
|
-
|
|
376
|
-
// Find which frame this shape is over
|
|
377
|
-
shapes.forEach(shape => {
|
|
378
|
-
if (shape.shapeName === 'frame' && shape.isShapeInFrame(this)) {
|
|
379
|
-
targetFrame = shape;
|
|
380
|
-
}
|
|
381
|
-
});
|
|
382
|
-
|
|
383
|
-
// If we have a parent frame and we're being dragged, temporarily remove clipping
|
|
384
|
-
if (this.parentFrame && isDraggingStroke) {
|
|
385
|
-
this.parentFrame.temporarilyRemoveFromFrame(this);
|
|
364
|
+
if (typeof window.__updateArrowsForShape === 'function') {
|
|
365
|
+
window.__updateArrowsForShape(this);
|
|
386
366
|
}
|
|
387
|
-
|
|
388
|
-
// Update frame highlighting
|
|
389
|
-
if (hoveredFrameStroke && hoveredFrameStroke !== targetFrame) {
|
|
390
|
-
hoveredFrameStroke.removeHighlight();
|
|
391
|
-
}
|
|
392
|
-
|
|
393
|
-
if (targetFrame && targetFrame !== hoveredFrameStroke) {
|
|
394
|
-
targetFrame.highlightFrame();
|
|
395
|
-
}
|
|
396
|
-
|
|
397
|
-
hoveredFrameStroke = targetFrame;
|
|
398
367
|
}
|
|
399
368
|
|
|
400
369
|
selectStroke() {
|
|
@@ -423,11 +392,15 @@ class FreehandStroke {
|
|
|
423
392
|
if (this.rotationAnchor && this.rotationAnchor.parentNode === this.group) {
|
|
424
393
|
this.group.removeChild(this.rotationAnchor);
|
|
425
394
|
}
|
|
395
|
+
if (this.rotationLine && this.rotationLine.parentNode === this.group) {
|
|
396
|
+
this.group.removeChild(this.rotationLine);
|
|
397
|
+
}
|
|
426
398
|
if (this.selectionOutline && this.selectionOutline.parentNode === this.group) {
|
|
427
399
|
this.group.removeChild(this.selectionOutline);
|
|
428
400
|
}
|
|
429
401
|
this.anchors = [];
|
|
430
402
|
this.rotationAnchor = null;
|
|
403
|
+
this.rotationLine = null;
|
|
431
404
|
this.selectionOutline = null;
|
|
432
405
|
this.isSelected = false;
|
|
433
406
|
}
|
|
@@ -436,22 +409,26 @@ class FreehandStroke {
|
|
|
436
409
|
updateSidebar() {}
|
|
437
410
|
|
|
438
411
|
contains(x, y) {
|
|
439
|
-
//
|
|
440
|
-
const
|
|
441
|
-
const
|
|
442
|
-
|
|
412
|
+
// Account for pending move offset (during drag, points aren't updated yet)
|
|
413
|
+
const ox = this._moveOffsetX || 0;
|
|
414
|
+
const oy = this._moveOffsetY || 0;
|
|
415
|
+
const bbX = this.boundingBox.x + ox;
|
|
416
|
+
const bbY = this.boundingBox.y + oy;
|
|
417
|
+
const centerX = bbX + this.boundingBox.width / 2;
|
|
418
|
+
const centerY = bbY + this.boundingBox.height / 2;
|
|
419
|
+
|
|
443
420
|
// Adjust for rotation
|
|
444
421
|
const dx = x - centerX;
|
|
445
422
|
const dy = y - centerY;
|
|
446
|
-
|
|
423
|
+
|
|
447
424
|
const angleRad = -this.rotation * Math.PI / 180;
|
|
448
425
|
const rotatedX = dx * Math.cos(angleRad) - dy * Math.sin(angleRad) + centerX;
|
|
449
426
|
const rotatedY = dx * Math.sin(angleRad) + dy * Math.cos(angleRad) + centerY;
|
|
450
|
-
|
|
451
|
-
return rotatedX >=
|
|
452
|
-
rotatedX <=
|
|
453
|
-
rotatedY >=
|
|
454
|
-
rotatedY <=
|
|
427
|
+
|
|
428
|
+
return rotatedX >= bbX &&
|
|
429
|
+
rotatedX <= bbX + this.boundingBox.width &&
|
|
430
|
+
rotatedY >= bbY &&
|
|
431
|
+
rotatedY <= bbY + this.boundingBox.height;
|
|
455
432
|
}
|
|
456
433
|
|
|
457
434
|
isNearAnchor(x, y) {
|
|
@@ -459,10 +436,14 @@ class FreehandStroke {
|
|
|
459
436
|
const buffer = 10;
|
|
460
437
|
const anchorSize = 10 / currentZoom;
|
|
461
438
|
|
|
439
|
+
// Account for pending move offset
|
|
440
|
+
const ox = this._moveOffsetX || 0;
|
|
441
|
+
const oy = this._moveOffsetY || 0;
|
|
442
|
+
|
|
462
443
|
// Transform the input coordinates to account for rotation
|
|
463
|
-
const centerX = this.boundingBox.x + this.boundingBox.width / 2;
|
|
464
|
-
const centerY = this.boundingBox.y + this.boundingBox.height / 2;
|
|
465
|
-
|
|
444
|
+
const centerX = this.boundingBox.x + ox + this.boundingBox.width / 2;
|
|
445
|
+
const centerY = this.boundingBox.y + oy + this.boundingBox.height / 2;
|
|
446
|
+
|
|
466
447
|
// Rotate the mouse coordinates to the shape's local coordinate system
|
|
467
448
|
const angleRad = -this.rotation * Math.PI / 180;
|
|
468
449
|
const dx = x - centerX;
|
|
@@ -471,8 +452,8 @@ class FreehandStroke {
|
|
|
471
452
|
const localY = dx * Math.sin(angleRad) + dy * Math.cos(angleRad) + centerY;
|
|
472
453
|
|
|
473
454
|
// Check resize anchors in local space
|
|
474
|
-
const expandedX = this.boundingBox.x - this.selectionPadding;
|
|
475
|
-
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;
|
|
476
457
|
const expandedWidth = this.boundingBox.width + 2 * this.selectionPadding;
|
|
477
458
|
const expandedHeight = this.boundingBox.height + 2 * this.selectionPadding;
|
|
478
459
|
|
|
@@ -618,6 +599,7 @@ class FreehandStroke {
|
|
|
618
599
|
rotationLine.setAttribute('stroke-dasharray', '2 2');
|
|
619
600
|
rotationLine.setAttribute('style', 'pointer-events: none;');
|
|
620
601
|
this.group.appendChild(rotationLine);
|
|
602
|
+
this.rotationLine = rotationLine;
|
|
621
603
|
|
|
622
604
|
// Show sidebar when anchors are added (when shape is selected)
|
|
623
605
|
disableAllSideBars();
|
|
@@ -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
|
|