@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elixpo/lixsketch",
3
- "version": "5.4.0",
3
+ "version": "5.4.2",
4
4
  "description": "Open-source SVG whiteboard engine with hand-drawn aesthetics — the core of LixSketch",
5
5
  "type": "module",
6
6
  "main": "src/index.js",
@@ -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
- // Also forward the move to the main handler so the tool keeps updating
79
- // (the SVG won't receive mousemove while cursor is outside it)
80
- handleMainMouseMove(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);
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
  }
@@ -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.boundingBox.x,
155
- y: shape.boundingBox.y,
156
- width: shape.boundingBox.width,
157
- height: shape.boundingBox.height
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.boundingBox.x,
393
- y: shape.boundingBox.y,
394
- width: shape.boundingBox.width,
395
- height: shape.boundingBox.height
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 = shape.boundingBox || this.getShapeBounds(shape);
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 (multiSelection.isPointInBounds(x, y)) {
1424
- // Only start drag if clicking on a shape that's part of the selection
1425
- // This allows clicking through empty areas of the selection rectangle
1426
- let clickedOnSelectedShape = null;
1427
- for (const shape of multiSelection.selectedShapes) {
1428
- if (shape.contains && shape.contains(x, y)) {
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
- if (clickedOnSelectedShape) {
1434
- // Ctrl+Click: toggle shape out of multi-selection
1435
- if (e.ctrlKey || e.metaKey) {
1436
- multiSelection.removeShape(clickedOnSelectedShape);
1437
- return true;
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
- // Click was in bounds but not on a selected shape — fall through
1443
- // to allow clicking on other shapes or starting a new selection
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
@@ -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
@@ -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
- const bbox = codeElement.getBBox();
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
- const dy = 0;
86
- this.points = this.points.map(point => [point[0] + dx, point[1] + dy, point[2] || 0.5]);
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
- updateArrowsForShape(this);
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();
@@ -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
- const bbox = textElement.getBBox();
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
 
@@ -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
- const currentPoint = getSVGCoordinates(event, selectedElement);
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 };