@elixpo/lixsketch 5.6.5 → 5.6.7

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.
Files changed (161) hide show
  1. package/dist/mcp/index.js +5 -1
  2. package/dist/mcp/index.js.map +2 -2
  3. package/dist/mcp/stdio.js +5 -1
  4. package/dist/mcp/stdio.js.map +2 -2
  5. package/dist/react/{AIRenderer-SRF7LU5K.js → AIRenderer-R5M2CIDO.js} +13 -6
  6. package/dist/react/AIRenderer-R5M2CIDO.js.map +7 -0
  7. package/dist/react/Circle-HMM5PKJ2.js +10 -0
  8. package/dist/react/{CopyPaste-YUDZ6DHY.js → CopyPaste-PPDS3AD6.js} +11 -10
  9. package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-PPDS3AD6.js.map} +1 -1
  10. package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-UV7LFSAS.js} +216 -63
  11. package/dist/react/EventDispatcher-UV7LFSAS.js.map +7 -0
  12. package/dist/react/Frame-DELCMYV6.js +11 -0
  13. package/dist/react/{FreehandStroke-U6GYZOM4.js → FreehandStroke-WXIJKD5I.js} +2 -2
  14. package/dist/react/{GraphEngine-MM6ALA2N.js → GraphEngine-XH3RPYO4.js} +9 -2
  15. package/dist/react/GraphEngine-XH3RPYO4.js.map +7 -0
  16. package/dist/react/IconShape-PZHMZSLR.js +10 -0
  17. package/dist/react/ImageShape-ZMBZARRI.js +10 -0
  18. package/dist/react/Line-VPVPL6ET.js +10 -0
  19. package/dist/react/{LixScriptParser-JYWJX375.js → LixScriptParser-YS3ZOTGS.js} +6 -2
  20. package/dist/react/LixScriptParser-YS3ZOTGS.js.map +7 -0
  21. package/dist/react/{MermaidFlowchartRenderer-PQEC4TMI.js → MermaidFlowchartRenderer-EL3Q2MNW.js} +6 -2
  22. package/dist/react/MermaidFlowchartRenderer-EL3Q2MNW.js.map +7 -0
  23. package/dist/react/MermaidSequenceParser-DBYXW23P.js +8 -0
  24. package/dist/react/{MermaidSequenceRenderer-KCUHO7UI.js → MermaidSequenceRenderer-K4KHHEVC.js} +10 -3
  25. package/dist/react/MermaidSequenceRenderer-K4KHHEVC.js.map +7 -0
  26. package/dist/react/{MermaidStructuredRenderer-W7UKN7BM.js → MermaidStructuredRenderer-6LN3WISP.js} +7 -2
  27. package/dist/react/MermaidStructuredRenderer-6LN3WISP.js.map +7 -0
  28. package/dist/react/Rectangle-4U4CD2ER.js +10 -0
  29. package/dist/react/{SceneSerializer-SG6SJN5X.js → SceneSerializer-H6KMQCO2.js} +120 -46
  30. package/dist/react/SceneSerializer-H6KMQCO2.js.map +7 -0
  31. package/dist/react/{Selection-2TSH3J6P.js → Selection-CFWRGVKH.js} +4 -4
  32. package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-UFLZJQXS.js} +26 -26
  33. package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-RXO7YHA2.js} +12 -2
  34. package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-DO327FKU.js} +3 -3
  35. package/dist/react/{chunk-EOURRJ55.js → chunk-2ALS5JGS.js} +2 -2
  36. package/dist/react/{chunk-LJR7EVW2.js → chunk-37PF7FG2.js} +3 -3
  37. package/dist/react/chunk-6AIFXYV6.js +31 -0
  38. package/dist/react/chunk-6AIFXYV6.js.map +7 -0
  39. package/dist/react/{chunk-XOWNCYE4.js → chunk-6LWUR7XB.js} +3 -3
  40. package/dist/react/{chunk-ASBC5G35.js → chunk-7DD2JRBC.js} +2 -2
  41. package/dist/react/{chunk-OP56XPWF.js → chunk-BOZEDUAF.js} +2 -2
  42. package/dist/react/{chunk-RGMDWLRJ.js → chunk-CGMUXYPJ.js} +3 -3
  43. package/dist/react/{chunk-M6TMN76K.js → chunk-DODPOWNX.js} +3 -3
  44. package/dist/react/{chunk-LSHZYN77.js → chunk-GQLXUZHQ.js} +2 -2
  45. package/dist/react/{chunk-7CM5XQDN.js → chunk-I7TVGJFF.js} +20 -36
  46. package/dist/react/chunk-I7TVGJFF.js.map +7 -0
  47. package/dist/react/{chunk-K5RY4EV7.js → chunk-KC26KJW4.js} +26 -17
  48. package/dist/react/chunk-KC26KJW4.js.map +7 -0
  49. package/dist/react/{chunk-XFL4R5ZF.js → chunk-KZBKYHZO.js} +16 -35
  50. package/dist/react/chunk-KZBKYHZO.js.map +7 -0
  51. package/dist/react/{chunk-VQVEPJSA.js → chunk-MHV5P6TS.js} +2 -1
  52. package/dist/react/chunk-MHV5P6TS.js.map +7 -0
  53. package/dist/react/{chunk-PXFPPO3R.js → chunk-NH3NVSWT.js} +18 -2
  54. package/dist/react/chunk-NH3NVSWT.js.map +7 -0
  55. package/dist/react/{chunk-APTOPAOY.js → chunk-O7KXLFZN.js} +3 -3
  56. package/dist/react/{chunk-GQUZPON4.js → chunk-RK3NVAER.js} +4 -4
  57. package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
  58. package/dist/react/chunk-VZPH7OAM.js.map +7 -0
  59. package/dist/react/{chunk-6GPEWDRB.js → chunk-VZX55VQL.js} +115 -6
  60. package/dist/react/chunk-VZX55VQL.js.map +7 -0
  61. package/dist/react/{chunk-TKXKJNTX.js → chunk-W7XUY4GR.js} +16 -19
  62. package/dist/react/chunk-W7XUY4GR.js.map +7 -0
  63. package/dist/react/{chunk-XYUFEIZR.js → chunk-X5ENBMGL.js} +2 -2
  64. package/dist/react/{chunk-K5BG5MHY.js → chunk-ZNZAICWY.js} +36 -60
  65. package/dist/react/chunk-ZNZAICWY.js.map +7 -0
  66. package/dist/react/{circleTool-LDKGZALF.js → circleTool-NK4RVDAR.js} +4 -4
  67. package/dist/react/{codeTool-4YZO5W7J.js → codeTool-3WF4HEOW.js} +4 -4
  68. package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-VCNV2NG2.js} +4 -4
  69. package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-TBAOD46K.js} +4 -4
  70. package/dist/react/{iconTool-46O5VEUX.js → iconTool-65JCAKYF.js} +4 -4
  71. package/dist/react/{imageTool-P2RWZRHO.js → imageTool-UQZUBOK2.js} +4 -4
  72. package/dist/react/index.js +148 -37
  73. package/dist/react/index.js.map +4 -4
  74. package/dist/react/{lineTool-ZMR27U5L.js → lineTool-762KBYOM.js} +4 -4
  75. package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-IQK3HZK6.js} +4 -4
  76. package/dist/react/{textTool-S4XXZHZH.js → textTool-ITG2SBJK.js} +5 -5
  77. package/package.json +5 -2
  78. package/src/core/AIRenderer.js +8 -2
  79. package/src/core/CanvasSpace.js +28 -0
  80. package/src/core/GraphRenderer.js +8 -3
  81. package/src/core/LixScriptParser.js +7 -1
  82. package/src/core/MermaidFlowchartRenderer.js +5 -1
  83. package/src/core/MermaidSequenceParser.js +1 -0
  84. package/src/core/MermaidSequenceRenderer.js +8 -3
  85. package/src/core/MermaidStructuredRenderer.js +6 -1
  86. package/src/core/SceneSerializer.js +121 -38
  87. package/src/core/Selection.js +29 -14
  88. package/src/core/UndoRedo.js +126 -5
  89. package/src/react/components/AppMenu.jsx +20 -19
  90. package/src/react/components/canvas/CanvasRulers.jsx +8 -13
  91. package/src/react/components/canvas/SVGCanvas.jsx +2 -1
  92. package/src/react/components/canvas/rulerUnits.js +28 -0
  93. package/src/react/components/sidebars/PaintBucketSidebar.jsx +1 -1
  94. package/src/react/store/useSketchStore.js +24 -3
  95. package/src/react/store/useUIStore.js +24 -1
  96. package/src/react/utils/canvasBackgrounds.js +29 -0
  97. package/src/shapes/Circle.js +12 -19
  98. package/src/shapes/Frame.js +38 -69
  99. package/src/shapes/FreehandStroke.js +24 -1
  100. package/src/shapes/Rectangle.js +10 -22
  101. package/src/tools/circleTool.js +3 -27
  102. package/src/tools/paintBucketTool.js +72 -15
  103. package/src/tools/rectangleTool.js +7 -23
  104. package/src/tools/shapeRecognition.js +270 -0
  105. package/src/tools/shapeRecognitionTool.js +36 -183
  106. package/dist/react/AIRenderer-SRF7LU5K.js.map +0 -7
  107. package/dist/react/Circle-6POZCUZD.js +0 -9
  108. package/dist/react/EventDispatcher-7LAK7GQ7.js.map +0 -7
  109. package/dist/react/Frame-7D2MXCFF.js +0 -11
  110. package/dist/react/GraphEngine-MM6ALA2N.js.map +0 -7
  111. package/dist/react/IconShape-ZADX7Y33.js +0 -10
  112. package/dist/react/ImageShape-UJQN2HI7.js +0 -10
  113. package/dist/react/Line-APEULMHE.js +0 -10
  114. package/dist/react/LixScriptParser-JYWJX375.js.map +0 -7
  115. package/dist/react/MermaidFlowchartRenderer-PQEC4TMI.js.map +0 -7
  116. package/dist/react/MermaidSequenceParser-UKG2NSZY.js +0 -8
  117. package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +0 -7
  118. package/dist/react/MermaidStructuredRenderer-W7UKN7BM.js.map +0 -7
  119. package/dist/react/Rectangle-S7XUKWYD.js +0 -9
  120. package/dist/react/SceneSerializer-SG6SJN5X.js.map +0 -7
  121. package/dist/react/chunk-6GPEWDRB.js.map +0 -7
  122. package/dist/react/chunk-7CM5XQDN.js.map +0 -7
  123. package/dist/react/chunk-FETTJLCV.js.map +0 -7
  124. package/dist/react/chunk-K5BG5MHY.js.map +0 -7
  125. package/dist/react/chunk-K5RY4EV7.js.map +0 -7
  126. package/dist/react/chunk-PXFPPO3R.js.map +0 -7
  127. package/dist/react/chunk-TKXKJNTX.js.map +0 -7
  128. package/dist/react/chunk-VQVEPJSA.js.map +0 -7
  129. package/dist/react/chunk-XFL4R5ZF.js.map +0 -7
  130. /package/dist/react/{Circle-6POZCUZD.js.map → Circle-HMM5PKJ2.js.map} +0 -0
  131. /package/dist/react/{Frame-7D2MXCFF.js.map → Frame-DELCMYV6.js.map} +0 -0
  132. /package/dist/react/{FreehandStroke-U6GYZOM4.js.map → FreehandStroke-WXIJKD5I.js.map} +0 -0
  133. /package/dist/react/{IconShape-ZADX7Y33.js.map → IconShape-PZHMZSLR.js.map} +0 -0
  134. /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-ZMBZARRI.js.map} +0 -0
  135. /package/dist/react/{Line-APEULMHE.js.map → Line-VPVPL6ET.js.map} +0 -0
  136. /package/dist/react/{MermaidSequenceParser-UKG2NSZY.js.map → MermaidSequenceParser-DBYXW23P.js.map} +0 -0
  137. /package/dist/react/{Rectangle-S7XUKWYD.js.map → Rectangle-4U4CD2ER.js.map} +0 -0
  138. /package/dist/react/{Selection-2TSH3J6P.js.map → Selection-CFWRGVKH.js.map} +0 -0
  139. /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-UFLZJQXS.js.map} +0 -0
  140. /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-RXO7YHA2.js.map} +0 -0
  141. /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-DO327FKU.js.map} +0 -0
  142. /package/dist/react/{chunk-EOURRJ55.js.map → chunk-2ALS5JGS.js.map} +0 -0
  143. /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-37PF7FG2.js.map} +0 -0
  144. /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-6LWUR7XB.js.map} +0 -0
  145. /package/dist/react/{chunk-ASBC5G35.js.map → chunk-7DD2JRBC.js.map} +0 -0
  146. /package/dist/react/{chunk-OP56XPWF.js.map → chunk-BOZEDUAF.js.map} +0 -0
  147. /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-CGMUXYPJ.js.map} +0 -0
  148. /package/dist/react/{chunk-M6TMN76K.js.map → chunk-DODPOWNX.js.map} +0 -0
  149. /package/dist/react/{chunk-LSHZYN77.js.map → chunk-GQLXUZHQ.js.map} +0 -0
  150. /package/dist/react/{chunk-APTOPAOY.js.map → chunk-O7KXLFZN.js.map} +0 -0
  151. /package/dist/react/{chunk-GQUZPON4.js.map → chunk-RK3NVAER.js.map} +0 -0
  152. /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-X5ENBMGL.js.map} +0 -0
  153. /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-NK4RVDAR.js.map} +0 -0
  154. /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-3WF4HEOW.js.map} +0 -0
  155. /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-VCNV2NG2.js.map} +0 -0
  156. /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-TBAOD46K.js.map} +0 -0
  157. /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-65JCAKYF.js.map} +0 -0
  158. /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-UQZUBOK2.js.map} +0 -0
  159. /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-762KBYOM.js.map} +0 -0
  160. /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-IQK3HZK6.js.map} +0 -0
  161. /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-ITG2SBJK.js.map} +0 -0
@@ -5,6 +5,22 @@
5
5
  const undoStack = [];
6
6
  const redoStack = [];
7
7
 
8
+ export function beginUndoBatch() {
9
+ return { startIndex: undoStack.length };
10
+ }
11
+
12
+ export function endUndoBatch(batch, label = 'batch') {
13
+ if (!batch || !Number.isInteger(batch.startIndex)) return;
14
+ const startIndex = Math.min(Math.max(batch.startIndex, 0), undoStack.length);
15
+ let actions = undoStack.splice(startIndex);
16
+ if (actions.length === 0) return;
17
+ const groupedAction = actions.length === 1 ? actions[0] : { type: 'batch', label, actions };
18
+ undoStack.push(groupedAction);
19
+ redoStack.length = 0;
20
+ notifyCollaboration();
21
+ return groupedAction;
22
+ }
23
+
8
24
  // Import references to text-related variables and functions
9
25
  let selectedElement = null;
10
26
  let updateSelectionFeedback = null;
@@ -58,6 +74,9 @@ export function pushDeleteAction(shape, meta = null) {
58
74
  // when the frame is restored, instead of leaving an empty box.
59
75
  const action = { type: 'delete', shape };
60
76
  if (meta && meta.childSnapshot) action.childSnapshot = meta.childSnapshot;
77
+ if (Number.isInteger(meta?.shapeIndex)) action.shapeIndex = meta.shapeIndex;
78
+ if (meta?.parentFrame) action.parentFrame = meta.parentFrame;
79
+ if (Number.isInteger(meta?.frameIndex)) action.frameIndex = meta.frameIndex;
61
80
  undoStack.push(action);
62
81
 
63
82
  // Clear redo stack when new action is performed
@@ -65,6 +84,30 @@ export function pushDeleteAction(shape, meta = null) {
65
84
  notifyCollaboration();
66
85
  }
67
86
 
87
+ export function pushCanvasResetAction(snapshot, restore, reset) {
88
+ if (!snapshot || typeof restore !== 'function' || typeof reset !== 'function') return;
89
+ // A reset is a new history boundary. Older per-shape actions reference
90
+ // objects that the scene loader will replace, so only the atomic reset
91
+ // snapshot remains undoable.
92
+ undoStack.length = 0;
93
+ redoStack.length = 0;
94
+ undoStack.push({ type: 'canvasReset', snapshot, restore, reset });
95
+ }
96
+
97
+ export function invalidateCanvasResetUndo() {
98
+ for (let index = undoStack.length - 1; index >= 0; index -= 1) {
99
+ if (undoStack[index]?.type === 'canvasReset') undoStack.splice(index, 1);
100
+ }
101
+ for (let index = redoStack.length - 1; index >= 0; index -= 1) {
102
+ if (redoStack[index]?.type === 'canvasReset') redoStack.splice(index, 1);
103
+ }
104
+ }
105
+
106
+ export function clearUndoHistory() {
107
+ undoStack.length = 0;
108
+ redoStack.length = 0;
109
+ }
110
+
68
111
  // Enhanced delete action to clean up attached arrows
69
112
  export function pushDeleteActionWithAttachments(shape) {
70
113
  let affectedArrows = [];
@@ -442,6 +485,15 @@ export function pushOptionsChangeAction(shape, oldOptions, newOptions = null) {
442
485
  notifyCollaboration();
443
486
  }
444
487
 
488
+ function applyOptionsSnapshot(shape, options) {
489
+ shape.options = { ...options };
490
+ if (shape.shapeName === 'frame') {
491
+ shape.fillColor = options.fillColor;
492
+ shape.fillStyle = options.fillStyle;
493
+ }
494
+ shape.draw();
495
+ }
496
+
445
497
  // Issue #34 bug #4: refresh the multi-selection outline + handles so it
446
498
  // follows the reverted geometry. Without this the selection rect stays
447
499
  // pinned to the pre-undo coordinates and the user sees a ghost rect
@@ -454,10 +506,54 @@ function refreshSelectionAfterAction() {
454
506
  try { ms.updateControls(); } catch (err) { console.warn('[UndoRedo] selection refresh failed:', err); }
455
507
  }
456
508
 
509
+ function restoreDeletedShapeFrame(action) {
510
+ const frame = action.parentFrame;
511
+ const shape = action.shape;
512
+ if (!frame || !shape || typeof frame.addShapeToFrame !== 'function') return;
513
+ frame.addShapeToFrame(shape);
514
+ if (Number.isInteger(action.frameIndex) && Array.isArray(frame.containedShapes)) {
515
+ const currentIndex = frame.containedShapes.indexOf(shape);
516
+ if (currentIndex >= 0) frame.containedShapes.splice(currentIndex, 1);
517
+ const targetIndex = Math.min(Math.max(action.frameIndex, 0), frame.containedShapes.length);
518
+ frame.containedShapes.splice(targetIndex, 0, shape);
519
+ const element = shape.group || shape.element;
520
+ const nextShape = frame.containedShapes[targetIndex + 1];
521
+ const nextElement = nextShape?.group || nextShape?.element;
522
+ if (element && frame.clipGroup && nextElement?.parentNode === frame.clipGroup) {
523
+ frame.clipGroup.insertBefore(element, nextElement);
524
+ }
525
+ }
526
+ }
527
+
528
+ function detachDeletedShapeFrame(action) {
529
+ const frame = action.parentFrame;
530
+ const shape = action.shape;
531
+ if (frame && shape && typeof frame.removeShapeFromFrame === 'function' && shape.parentFrame === frame) {
532
+ frame.removeShapeFromFrame(shape);
533
+ }
534
+ }
535
+
457
536
  export function undo() {
458
537
  if (undoStack.length === 0) return;
459
538
  const action = undoStack.pop();
460
539
  try {
540
+ if (action.type === 'batch') {
541
+ for (let index = action.actions.length - 1; index >= 0; index -= 1) {
542
+ undoStack.push(action.actions[index]);
543
+ undo();
544
+ redoStack.pop();
545
+ }
546
+ redoStack.push(action);
547
+ return;
548
+ }
549
+
550
+ if (action.type === 'canvasReset') {
551
+ action.restore(action.snapshot);
552
+ redoStack.push(action);
553
+ notifyCollaboration();
554
+ return;
555
+ }
556
+
461
557
  if (action.type === 'frameTransform') {
462
558
  // Handle frame transformation undo
463
559
  action.shape.x = action.oldPos.x;
@@ -652,7 +748,12 @@ export function undo() {
652
748
  action.shape.restore();
653
749
  } else {
654
750
  // Handle other shape deletion undo
655
- shapes.push(action.shape);
751
+ if (shapes.indexOf(action.shape) === -1) {
752
+ const index = Number.isInteger(action.shapeIndex)
753
+ ? Math.min(Math.max(action.shapeIndex, 0), shapes.length)
754
+ : shapes.length;
755
+ shapes.splice(index, 0, action.shape);
756
+ }
656
757
  if (svg) {
657
758
  svg.appendChild(action.shape.group);
658
759
  }
@@ -684,6 +785,7 @@ export function undo() {
684
785
  }
685
786
  }
686
787
  }
788
+ restoreDeletedShapeFrame(action);
687
789
  redoStack.push(action);
688
790
  return;
689
791
  }
@@ -959,8 +1061,7 @@ export function undo() {
959
1061
  action.shape.draw();
960
1062
  } else {
961
1063
  // Handle other shape options change undo
962
- action.shape.options = action.oldOptions;
963
- action.shape.draw();
1064
+ applyOptionsSnapshot(action.shape, action.oldOptions);
964
1065
  }
965
1066
  redoStack.push(action);
966
1067
  return;
@@ -975,6 +1076,23 @@ export function redo() {
975
1076
  if (redoStack.length === 0) return;
976
1077
  const action = redoStack.pop();
977
1078
  try {
1079
+ if (action.type === 'batch') {
1080
+ for (const childAction of action.actions) {
1081
+ redoStack.push(childAction);
1082
+ redo();
1083
+ undoStack.pop();
1084
+ }
1085
+ undoStack.push(action);
1086
+ return;
1087
+ }
1088
+
1089
+ if (action.type === 'canvasReset') {
1090
+ action.reset();
1091
+ undoStack.push(action);
1092
+ notifyCollaboration();
1093
+ return;
1094
+ }
1095
+
978
1096
  if (action.type === 'frameTransform') {
979
1097
  // Handle frame transformation redo
980
1098
  action.shape.x = action.newPos.x;
@@ -1137,6 +1255,7 @@ export function redo() {
1137
1255
  }
1138
1256
 
1139
1257
  if (action.type === 'delete') {
1258
+ detachDeletedShapeFrame(action);
1140
1259
  if (action.shape.type === 'text') {
1141
1260
  // Handle text deletion redo
1142
1261
  if ((action.shape.element || action.shape).parentNode) {
@@ -1453,8 +1572,7 @@ export function redo() {
1453
1572
  action.shape.arrowCurveAmount = action.newOptions.arrowCurveAmount;
1454
1573
  action.shape.draw();
1455
1574
  } else {
1456
- action.shape.options = { ...action.newOptions };
1457
- action.shape.draw();
1575
+ applyOptionsSnapshot(action.shape, action.newOptions);
1458
1576
  }
1459
1577
  undoStack.push(action);
1460
1578
  return;
@@ -1484,6 +1602,9 @@ document.getElementById('redo')?.addEventListener('click', redo);
1484
1602
  // Expose globally for plain scripts
1485
1603
  window.pushCreateAction = pushCreateAction;
1486
1604
  window.pushDeleteAction = pushDeleteAction;
1605
+ window.pushCanvasResetAction = pushCanvasResetAction;
1606
+ window.invalidateCanvasResetUndo = invalidateCanvasResetUndo;
1607
+ window.clearUndoHistory = clearUndoHistory;
1487
1608
  window.pushDeleteActionWithAttachments = pushDeleteActionWithAttachments;
1488
1609
  window.pushTransformAction = pushTransformAction;
1489
1610
  window.pushOptionsChangeAction = pushOptionsChangeAction;
@@ -7,23 +7,7 @@ import useSketchStore from '../store/useSketchStore'
7
7
  import { useAuthStore } from '../hooks/inertStores'
8
8
  import { triggerCloudSync } from '../hooks/inertStores'
9
9
  import { useTranslation } from '../hooks/useTranslation'
10
-
11
- // Issue #38 follow-up: theme-paired swatches. The menu picks the
12
- // matching list at render time based on the active theme.
13
- const CANVAS_BACKGROUNDS_LIGHT = [
14
- { color: '#ffffff', label: 'menu.canvasBg.white' },
15
- { color: '#fbf9fd', label: 'menu.canvasBg.cream' },
16
- { color: '#f5f3ed', label: 'menu.canvasBg.paper' },
17
- { color: '#f0f5fb', label: 'menu.canvasBg.skyTint' },
18
- { color: '#f0f5ef', label: 'menu.canvasBg.sageTint' },
19
- ]
20
- const CANVAS_BACKGROUNDS_DARK = [
21
- { color: '#000000', label: 'menu.canvasBg.black' },
22
- { color: '#161718', label: 'menu.canvasBg.darkGray' },
23
- { color: '#15111f', label: 'menu.canvasBg.blueBlack' },
24
- { color: '#181605', label: 'menu.canvasBg.darkYellow' },
25
- { color: '#1B1615', label: 'menu.canvasBg.darkBrown' },
26
- ]
10
+ import { CANVAS_BACKGROUNDS } from '../utils/canvasBackgrounds'
27
11
 
28
12
  function DangerWarningDialog({ action, workspaceName, onCancel, onConfirm }) {
29
13
  useEffect(() => {
@@ -130,6 +114,8 @@ export default function AppMenu() {
130
114
  const toggleGrid = useSketchStore((s) => s.toggleGrid)
131
115
  const rulersEnabled = useSketchStore((s) => s.rulersEnabled)
132
116
  const toggleRulers = useSketchStore((s) => s.toggleRulers)
117
+ const rulerUnit = useSketchStore((s) => s.rulerUnit)
118
+ const setRulerUnit = useSketchStore((s) => s.setRulerUnit)
133
119
 
134
120
  const viewMode = useSketchStore((s) => s.viewMode)
135
121
  const zenMode = useSketchStore((s) => s.zenMode)
@@ -300,6 +286,21 @@ export default function AppMenu() {
300
286
  <option value="en">English</option>
301
287
  <option value="bg">Български</option>
302
288
  <option value="de">Deutsch</option>
289
+ <option value="hi">हिन्दी</option>
290
+ </select>
291
+ </div>
292
+
293
+ <div className="w-full flex items-center justify-between px-3 py-2 border-b border-border-light text-text-secondary text-[11px]">
294
+ <span>Ruler unit</span>
295
+ <select
296
+ aria-label="Ruler unit"
297
+ className="cursor-pointer rounded border border-border-light bg-surface-hover px-1 text-[10px] uppercase text-text-primary outline-none"
298
+ value={rulerUnit}
299
+ onChange={(event) => setRulerUnit(event.target.value)}
300
+ >
301
+ <option value="px">px</option>
302
+ <option value="cm">cm</option>
303
+ <option value="in">in</option>
303
304
  </select>
304
305
  </div>
305
306
 
@@ -453,10 +454,10 @@ export default function AppMenu() {
453
454
  {t('menu.canvasBackground')}
454
455
  </p>
455
456
  <div className="flex items-center gap-1.5">
456
- {(theme === 'dark' ? CANVAS_BACKGROUNDS_DARK : CANVAS_BACKGROUNDS_LIGHT).map((bg) => (
457
+ {CANVAS_BACKGROUNDS[theme === 'light' ? 'light' : 'dark'].map((bg) => (
457
458
  <button
458
459
  key={bg.color}
459
- onClick={() => setCanvasBackground(bg.color)}
460
+ onClick={() => setCanvasBackground(bg.color, theme === 'light' ? 'light' : 'dark')}
460
461
  title={t(bg.label)}
461
462
  className={`w-7 h-7 rounded-full border-2 cursor-pointer transition-all duration-200 ${
462
463
  canvasBackground === bg.color
@@ -1,6 +1,7 @@
1
1
  "use client"
2
2
 
3
3
  import { useEffect, useMemo, useState } from 'react'
4
+ import { formatRulerValue, normalizeRulerUnit } from './rulerUnits.js'
4
5
 
5
6
  const GRID_SIZE = 20
6
7
  const MIN_LABEL_GAP = 52
@@ -16,14 +17,6 @@ function getRulerStep(zoom) {
16
17
  return GRID_SIZE
17
18
  }
18
19
 
19
- function formatDimension(value) {
20
- const rounded = Math.round(value)
21
- const absolute = Math.abs(rounded)
22
- if (absolute >= 1000000) return `${Number((rounded / 1000000).toFixed(1))}m`
23
- if (absolute >= 10000) return `${Number((rounded / 1000).toFixed(1))}k`
24
- return String(rounded)
25
- }
26
-
27
20
  function buildTicks(start, extent, zoom, step) {
28
21
  const first = Math.floor(start / step) * step
29
22
  const last = start + extent / zoom
@@ -34,7 +27,8 @@ function buildTicks(start, extent, zoom, step) {
34
27
  return ticks
35
28
  }
36
29
 
37
- export default function CanvasRulers({ enabled, svgRef }) {
30
+ export default function CanvasRulers({ enabled, svgRef, unit = 'px' }) {
31
+ const rulerUnit = normalizeRulerUnit(unit)
38
32
  const [viewport, setViewport] = useState({ x: 0, y: 0, zoom: 1, width: 0, height: 0, rulerLeft: 64, rulerTop: 48 })
39
33
  const [pointer, setPointer] = useState({ x: 0, y: 0, visible: false })
40
34
 
@@ -113,28 +107,29 @@ export default function CanvasRulers({ enabled, svgRef }) {
113
107
  <div className="absolute right-0 overflow-hidden border-b border-border-light bg-surface/95 shadow-sm backdrop-blur-sm" style={{ height: RULER_SIZE, left: horizontalStart, top: viewport.rulerTop }}>
114
108
  {horizontalTicks.map(({ value, position }) => (
115
109
  <div key={value} className="absolute bottom-0 h-2 border-l border-text-dim" style={{ left: position - horizontalStart }}>
116
- <span className="absolute bottom-2 left-1 font-mono text-[9px] leading-none tabular-nums">{formatDimension(value)}</span>
110
+ <span className="absolute bottom-2 left-1 font-mono text-[9px] leading-none tabular-nums">{formatRulerValue(value, rulerUnit)}</span>
117
111
  </div>
118
112
  ))}
119
113
  {pointerInDrawingArea && (
120
114
  <div className="absolute inset-y-0 border-l border-accent" style={{ left: pointer.x - horizontalStart }}>
121
- <span className="absolute left-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm">{formatDimension(pointerWorldX)}</span>
115
+ <span className="absolute left-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm">{formatRulerValue(pointerWorldX, rulerUnit, true)}</span>
122
116
  </div>
123
117
  )}
124
118
  </div>
125
119
  <div className="absolute bottom-0 overflow-hidden border-r border-border-light bg-surface/95 shadow-sm backdrop-blur-sm" style={{ width: RULER_SIZE, left: viewport.rulerLeft, top: verticalStart }}>
126
120
  {verticalTicks.map(({ value, position }) => (
127
121
  <div key={value} className="absolute right-0 w-2 border-t border-text-dim" style={{ top: position - verticalStart }}>
128
- <span className="absolute right-2 top-1 font-mono text-[9px] leading-none tabular-nums" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{formatDimension(value)}</span>
122
+ <span className="absolute right-2 top-1 font-mono text-[9px] leading-none tabular-nums" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{formatRulerValue(value, rulerUnit)}</span>
129
123
  </div>
130
124
  ))}
131
125
  {pointerInDrawingArea && (
132
126
  <div className="absolute inset-x-0 border-t border-accent" style={{ top: pointer.y - verticalStart }}>
133
- <span className="absolute right-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{formatDimension(pointerWorldY)}</span>
127
+ <span className="absolute right-1 top-1 rounded bg-accent px-1 font-mono text-[9px] leading-4 text-white shadow-sm" style={{ transform: 'rotate(-90deg)', transformOrigin: 'top right' }}>{formatRulerValue(pointerWorldY, rulerUnit, true)}</span>
134
128
  </div>
135
129
  )}
136
130
  </div>
137
131
  <div className="absolute border-b border-r border-border-light bg-accent/20" style={{ width: RULER_SIZE, height: RULER_SIZE, left: viewport.rulerLeft, top: viewport.rulerTop }}>
132
+ <span className="absolute bottom-0.5 left-1 font-mono text-[8px] uppercase text-accent">{rulerUnit}</span>
138
133
  <span className="absolute left-1/2 top-1/2 h-2.5 -translate-x-1/2 -translate-y-1/2 border-l border-accent" />
139
134
  <span className="absolute left-1/2 top-1/2 w-2.5 -translate-x-1/2 -translate-y-1/2 border-t border-accent" />
140
135
  </div>
@@ -14,6 +14,7 @@ export default function SVGCanvas() {
14
14
  const gridEnabled = useSketchStore((s) => s.gridEnabled)
15
15
  const hydrateGrid = useSketchStore((s) => s.hydrateGrid)
16
16
  const rulersEnabled = useSketchStore((s) => s.rulersEnabled)
17
+ const rulerUnit = useSketchStore((s) => s.rulerUnit)
17
18
  const hydrateRulers = useSketchStore((s) => s.hydrateRulers)
18
19
  const getCursor = useSketchStore((s) => s.getCursor)
19
20
  const cursor = getCursor()
@@ -152,7 +153,7 @@ export default function SVGCanvas() {
152
153
  />
153
154
  )}
154
155
  </svg>
155
- <CanvasRulers enabled={rulersEnabled} svgRef={svgRef} />
156
+ <CanvasRulers enabled={rulersEnabled} svgRef={svgRef} unit={rulerUnit} />
156
157
  </>
157
158
  )
158
159
  }
@@ -0,0 +1,28 @@
1
+ export const RULER_UNITS = ['px', 'cm', 'in']
2
+
3
+ const PIXELS_PER_UNIT = { px: 1, cm: 96 / 2.54, in: 96 }
4
+
5
+ export function normalizeRulerUnit(unit) {
6
+ return RULER_UNITS.includes(unit) ? unit : 'px'
7
+ }
8
+
9
+ export function convertRulerValue(pixelValue, unit = 'px') {
10
+ return pixelValue / PIXELS_PER_UNIT[normalizeRulerUnit(unit)]
11
+ }
12
+
13
+ export function formatRulerValue(pixelValue, unit = 'px', suffix = false) {
14
+ const normalized = normalizeRulerUnit(unit)
15
+ const converted = convertRulerValue(pixelValue, normalized)
16
+ let label
17
+ if (normalized === 'px') {
18
+ const rounded = Math.round(converted)
19
+ const absolute = Math.abs(rounded)
20
+ if (absolute >= 1000000) label = `${Number((rounded / 1000000).toFixed(1))}m`
21
+ else if (absolute >= 10000) label = `${Number((rounded / 1000).toFixed(1))}k`
22
+ else label = String(rounded)
23
+ } else {
24
+ const precision = Math.abs(converted) < 10 ? 2 : Math.abs(converted) < 100 ? 1 : 0
25
+ label = String(Number(converted.toFixed(precision)))
26
+ }
27
+ return suffix ? `${label} ${normalized}` : label
28
+ }
@@ -43,7 +43,7 @@ export default function PaintBucketSidebar() {
43
43
  {FILLS.map((fill) => <button key={fill.value} type="button" onClick={() => setFillStyle(fill.value)} className={`flex cursor-pointer items-center gap-2 rounded-lg px-2.5 py-1.5 text-xs transition ${fillStyle === fill.value ? 'bg-accent text-white' : 'text-text-secondary hover:bg-surface-hover'}`}><span className="h-1.5 w-1.5 rounded-full bg-current" />{fill.label}</button>)}
44
44
  </div>
45
45
  </ToolbarButton>
46
- <div className="max-w-[185px] px-2 text-[10px] leading-4 text-text-dim">Click a rectangle or circle to apply the selected fill.</div>
46
+ <div className="max-w-[185px] px-2 text-[10px] leading-4 text-text-dim">Fill rectangles, circles, frames, and closed freehand shapes. Lines and arrows are left unchanged.</div>
47
47
  </ShapeSidebar>
48
48
  )
49
49
  }
@@ -1,7 +1,9 @@
1
1
  import { create } from 'zustand'
2
+ import { DEFAULT_CANVAS_BACKGROUNDS, canvasBackgroundStorageKey, readCanvasBackground } from '../utils/canvasBackgrounds'
2
3
 
3
4
  const GRID_STORAGE_KEY = 'lixsketch-grid-enabled'
4
5
  const RULER_STORAGE_KEY = 'lixsketch-rulers-enabled'
6
+ const RULER_UNIT_STORAGE_KEY = 'lixsketch-ruler-unit'
5
7
 
6
8
  // Tool enum replaces 15 boolean flags
7
9
  export const TOOLS = {
@@ -193,13 +195,22 @@ const useSketchStore = create((set, get) => ({
193
195
  setPanStart: (p) => set({ panStart: p }),
194
196
 
195
197
  // --- Canvas background ---
196
- canvasBackground: 'var(--lixsketch-bg, #15111f)',
197
- setCanvasBackground: (color) => {
198
- set({ canvasBackground: color })
198
+ canvasBackground: DEFAULT_CANVAS_BACKGROUNDS.dark,
199
+ canvasBackgrounds: { ...DEFAULT_CANVAS_BACKGROUNDS },
200
+ setCanvasBackground: (color, theme = 'dark') => {
201
+ const resolved = theme === 'light' ? 'light' : 'dark'
202
+ set((state) => ({ canvasBackground: color, canvasBackgrounds: { ...state.canvasBackgrounds, [resolved]: color } }))
199
203
  if (typeof window !== 'undefined') {
204
+ try { localStorage.setItem(canvasBackgroundStorageKey(resolved), color) } catch {}
200
205
  requestAnimationFrame(() => window.__adaptCanvasContrast?.(color))
201
206
  }
202
207
  },
208
+ restoreCanvasBackground: (theme) => {
209
+ const resolved = theme === 'light' ? 'light' : 'dark'
210
+ const color = readCanvasBackground(resolved, typeof window === 'undefined' ? null : localStorage)
211
+ set((state) => ({ canvasBackground: color, canvasBackgrounds: { ...state.canvasBackgrounds, [resolved]: color } }))
212
+ if (typeof window !== 'undefined') requestAnimationFrame(() => window.__adaptCanvasContrast?.(color))
213
+ },
203
214
 
204
215
  // --- Grid ---
205
216
  gridEnabled: false,
@@ -222,6 +233,7 @@ const useSketchStore = create((set, get) => ({
222
233
 
223
234
  // --- Rulers ---
224
235
  rulersEnabled: false,
236
+ rulerUnit: 'px',
225
237
  toggleRulers: () => set((s) => {
226
238
  const rulersEnabled = !s.rulersEnabled
227
239
  if (typeof window !== 'undefined') {
@@ -229,6 +241,13 @@ const useSketchStore = create((set, get) => ({
229
241
  }
230
242
  return { rulersEnabled }
231
243
  }),
244
+ setRulerUnit: (unit) => {
245
+ const rulerUnit = ['px', 'cm', 'in'].includes(unit) ? unit : 'px'
246
+ if (typeof window !== 'undefined') {
247
+ try { localStorage.setItem(RULER_UNIT_STORAGE_KEY, rulerUnit) } catch {}
248
+ }
249
+ set({ rulerUnit })
250
+ },
232
251
  hydrateRulers: () => {
233
252
  if (typeof window === 'undefined') return
234
253
  try {
@@ -236,6 +255,8 @@ const useSketchStore = create((set, get) => ({
236
255
  if (saved === 'true' || saved === 'false') {
237
256
  set({ rulersEnabled: saved === 'true' })
238
257
  }
258
+ const savedUnit = localStorage.getItem(RULER_UNIT_STORAGE_KEY)
259
+ if (['px', 'cm', 'in'].includes(savedUnit)) set({ rulerUnit: savedUnit })
239
260
  } catch {}
240
261
  },
241
262
 
@@ -1,4 +1,5 @@
1
1
  import { create } from 'zustand'
2
+ import useSketchStore from './useSketchStore'
2
3
 
3
4
  export const MAX_WORKSPACE_NAME_LENGTH = 20
4
5
 
@@ -111,6 +112,20 @@ function invertShapeColors(prevResolved, nextResolved) {
111
112
  }
112
113
  }
113
114
 
115
+ function readStoredTheme() {
116
+ if (typeof window === 'undefined') return 'dark'
117
+ try {
118
+ const theme = localStorage.getItem('theme')
119
+ if (theme && ['dark', 'light', 'system'].includes(theme)) return theme
120
+ } catch {}
121
+ try {
122
+ const prefs = JSON.parse(localStorage.getItem('lix_ui_prefs') || '{}')
123
+ return ['dark', 'light', 'system'].includes(prefs.theme) ? prefs.theme : 'dark'
124
+ } catch {
125
+ return 'dark'
126
+ }
127
+ }
128
+
114
129
  function applyTheme(theme) {
115
130
  // Issue #38 bug #1: scope theme to body classes so light mode only
116
131
  // affects the canvas, not the landing/blog pages. Token values live
@@ -215,7 +230,7 @@ const useUIStore = create((set, get) => ({
215
230
  setCanvasLoading: (loading, message) => set({ canvasLoading: loading, canvasLoadingMessage: message || 'Loading canvas...' }),
216
231
 
217
232
  // --- Theme ---
218
- theme: 'dark',
233
+ theme: readStoredTheme(),
219
234
  setTheme: (newTheme) => {
220
235
  const prev = get().theme
221
236
  const resolve = (t) => t === 'system'
@@ -224,6 +239,14 @@ const useUIStore = create((set, get) => ({
224
239
  invertShapeColors(resolve(prev), resolve(newTheme))
225
240
  applyTheme(newTheme)
226
241
  set({ theme: newTheme })
242
+ useSketchStore.getState().restoreCanvasBackground(resolve(newTheme))
243
+ if (typeof window !== 'undefined') {
244
+ try {
245
+ localStorage.setItem('theme', newTheme)
246
+ const prefs = JSON.parse(localStorage.getItem('lix_ui_prefs') || '{}')
247
+ localStorage.setItem('lix_ui_prefs', JSON.stringify({ ...prefs, theme: newTheme }))
248
+ } catch {}
249
+ }
227
250
  },
228
251
 
229
252
  // --- Language / i18n ---
@@ -0,0 +1,29 @@
1
+ export const CANVAS_BACKGROUNDS = {
2
+ light: [
3
+ { color: '#FBE4DB', label: 'menu.canvasBg.white' },
4
+ { color: '#F6DBC0', label: 'menu.canvasBg.cream' },
5
+ { color: '#F5EED2', label: 'menu.canvasBg.paper' },
6
+ { color: '#F5F2ED', label: 'menu.canvasBg.skyTint' },
7
+ { color: '#F5DABF', label: 'menu.canvasBg.sageTint' },
8
+ ],
9
+ dark: [
10
+ { color: '#000000', label: 'menu.canvasBg.black' },
11
+ { color: '#161718', label: 'menu.canvasBg.darkGray' },
12
+ { color: '#15111f', label: 'menu.canvasBg.blueBlack' },
13
+ { color: '#181605', label: 'menu.canvasBg.darkYellow' },
14
+ { color: '#1B1615', label: 'menu.canvasBg.darkBrown' },
15
+ ],
16
+ }
17
+
18
+ export const DEFAULT_CANVAS_BACKGROUNDS = { dark: '#15111f', light: '#F5F2ED' }
19
+ export const canvasBackgroundStorageKey = (theme) => `lixsketch-canvas-background-${theme}`
20
+
21
+ export function readCanvasBackground(theme, storage) {
22
+ const resolved = theme === 'light' ? 'light' : 'dark'
23
+ try {
24
+ const stored = storage?.getItem(canvasBackgroundStorageKey(resolved))
25
+ const match = CANVAS_BACKGROUNDS[resolved].find(({ color }) => color.toLowerCase() === stored?.toLowerCase())
26
+ if (match) return match.color
27
+ } catch {}
28
+ return DEFAULT_CANVAS_BACKGROUNDS[resolved]
29
+ }
@@ -1,5 +1,6 @@
1
1
  /* eslint-disable */
2
2
  import { registerRotationAnchor } from '../core/ScreenSpaceControls.js';
3
+ import { canvasToLocal, localToCanvas } from '../core/CanvasSpace.js';
3
4
  // Circle shape class - extracted from drawCircle.js
4
5
  // Depends on globals: svg, shapes, rough, currentShape, currentZoom, rc
5
6
 
@@ -432,18 +433,17 @@ class Circle {
432
433
  }
433
434
  isNearAnchor(x, y) {
434
435
  if (!this.isSelected) return null;
435
- const buffer = 10 / currentZoom; // Scale buffer by zoom level
436
- const anchorSize = 10 / currentZoom; // Scale anchor size by zoom level
436
+ const zoom = window.currentZoom || 1;
437
+ const buffer = 10 / zoom; // Scale buffer by zoom level
438
+ const anchorSize = 10 / zoom; // Scale anchor size by zoom level
439
+ const transform = { x: this.x, y: this.y, rotation: this.rotation, centerX: 0, centerY: 0 };
437
440
 
438
441
  // Iterate through anchors
439
442
  for (let i = 0; i < this.anchors.length; i++) {
440
443
  const anchor = this.anchors[i];
441
444
  const anchorLocalX = parseFloat(anchor.getAttribute('x')) + anchorSize / 2;
442
445
  const anchorLocalY = parseFloat(anchor.getAttribute('y')) + anchorSize / 2;
443
- const svgPoint = svg.createSVGPoint();
444
- svgPoint.x = anchorLocalX;
445
- svgPoint.y = anchorLocalY;
446
- const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
446
+ const transformedPoint = localToCanvas({ x: anchorLocalX, y: anchorLocalY }, transform);
447
447
 
448
448
  const anchorLeft = transformedPoint.x - anchorSize/2 - buffer;
449
449
  const anchorRight = transformedPoint.x + anchorSize/2 + buffer;
@@ -459,12 +459,9 @@ class Circle {
459
459
  if (this.rotationAnchor) {
460
460
  const rotateAnchorLocalX = parseFloat(this.rotationAnchor.getAttribute('cx'));
461
461
  const rotateAnchorLocalY = parseFloat(this.rotationAnchor.getAttribute('cy'));
462
- const svgPoint = svg.createSVGPoint();
463
- svgPoint.x = rotateAnchorLocalX;
464
- svgPoint.y = rotateAnchorLocalY;
465
- const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
462
+ const transformedPoint = localToCanvas({ x: rotateAnchorLocalX, y: rotateAnchorLocalY }, transform);
466
463
 
467
- const rotateAnchorRadius = parseFloat(this.rotationAnchor.getAttribute('r')) / currentZoom;
464
+ const rotateAnchorRadius = parseFloat(this.rotationAnchor.getAttribute('r'));
468
465
  const distSq = (x - transformedPoint.x)**2 + (y - transformedPoint.y)**2;
469
466
  if (distSq <= (rotateAnchorRadius + buffer)**2) {
470
467
  return { type: 'rotate' };
@@ -475,14 +472,10 @@ class Circle {
475
472
  }
476
473
 
477
474
  updatePosition(anchorIndex, newMouseX, newMouseY) {
478
- const CTM = this.group.getCTM();
479
- if (!CTM) return;
480
-
481
- const inverseCTM = CTM.inverse();
482
- const svgPoint = svg.createSVGPoint();
483
- svgPoint.x = newMouseX;
484
- svgPoint.y = newMouseY;
485
- const transformedPoint = svgPoint.matrixTransform(inverseCTM);
475
+ const transformedPoint = canvasToLocal(
476
+ { x: newMouseX, y: newMouseY },
477
+ { x: this.x, y: this.y, rotation: this.rotation, centerX: 0, centerY: 0 },
478
+ );
486
479
 
487
480
  const dx = transformedPoint.x;
488
481
  const dy = transformedPoint.y;