@elixpo/lixsketch 5.6.5 → 5.6.6

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 (160) 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-P6G35CXO.js} +11 -10
  9. package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-P6G35CXO.js.map} +1 -1
  10. package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-M5B4EWHJ.js} +216 -63
  11. package/dist/react/EventDispatcher-M5B4EWHJ.js.map +7 -0
  12. package/dist/react/Frame-QEUEYITT.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-NHJE4YBY.js +10 -0
  17. package/dist/react/ImageShape-33VCMSDP.js +10 -0
  18. package/dist/react/Line-A6FE232Q.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-JEYTYXZQ.js} +120 -46
  30. package/dist/react/SceneSerializer-JEYTYXZQ.js.map +7 -0
  31. package/dist/react/{Selection-2TSH3J6P.js → Selection-23MOLHEI.js} +4 -4
  32. package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-OPJON7TD.js} +26 -26
  33. package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-QVNRI5OB.js} +8 -2
  34. package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-VH3PJPDM.js} +3 -3
  35. package/dist/react/{chunk-LSHZYN77.js → chunk-2DZHO67H.js} +2 -2
  36. package/dist/react/{chunk-XOWNCYE4.js → chunk-5NHJJJBZ.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-K5RY4EV7.js → chunk-6GPWSPJF.js} +9 -5
  40. package/dist/react/{chunk-K5RY4EV7.js.map → chunk-6GPWSPJF.js.map} +2 -2
  41. package/dist/react/{chunk-ASBC5G35.js → chunk-B6CYUZC4.js} +2 -2
  42. package/dist/react/{chunk-OP56XPWF.js → chunk-DNFPTVIP.js} +2 -2
  43. package/dist/react/{chunk-XYUFEIZR.js → chunk-DWEFPKDO.js} +2 -2
  44. package/dist/react/{chunk-GQUZPON4.js → chunk-F56XUE5S.js} +4 -4
  45. package/dist/react/{chunk-7CM5XQDN.js → chunk-G3DJQY65.js} +20 -36
  46. package/dist/react/chunk-G3DJQY65.js.map +7 -0
  47. package/dist/react/{chunk-LJR7EVW2.js → chunk-JEGLUNBC.js} +3 -3
  48. package/dist/react/{chunk-6GPEWDRB.js → chunk-M7GAB24F.js} +52 -6
  49. package/dist/react/chunk-M7GAB24F.js.map +7 -0
  50. package/dist/react/{chunk-VQVEPJSA.js → chunk-MHV5P6TS.js} +2 -1
  51. package/dist/react/chunk-MHV5P6TS.js.map +7 -0
  52. package/dist/react/{chunk-PXFPPO3R.js → chunk-NH3NVSWT.js} +18 -2
  53. package/dist/react/chunk-NH3NVSWT.js.map +7 -0
  54. package/dist/react/{chunk-XFL4R5ZF.js → chunk-OK4ROGMM.js} +16 -35
  55. package/dist/react/chunk-OK4ROGMM.js.map +7 -0
  56. package/dist/react/{chunk-APTOPAOY.js → chunk-SSFEN5CY.js} +3 -3
  57. package/dist/react/{chunk-RGMDWLRJ.js → chunk-UEL2G6D2.js} +3 -3
  58. package/dist/react/{chunk-M6TMN76K.js → chunk-ULPIHUVC.js} +3 -3
  59. package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
  60. package/dist/react/chunk-VZPH7OAM.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-EOURRJ55.js → chunk-XSE4HQ2D.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-2UODOBCV.js} +4 -4
  67. package/dist/react/{codeTool-4YZO5W7J.js → codeTool-F4RV7SAI.js} +4 -4
  68. package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-NTGA5BI6.js} +4 -4
  69. package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-L55EUB5C.js} +4 -4
  70. package/dist/react/{iconTool-46O5VEUX.js → iconTool-KQQBLGR3.js} +4 -4
  71. package/dist/react/{imageTool-P2RWZRHO.js → imageTool-LDJ45AD7.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-CSAFD7PW.js} +4 -4
  75. package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-YPD2SIIZ.js} +4 -4
  76. package/dist/react/{textTool-S4XXZHZH.js → textTool-VQHMWEFJ.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 +12 -2
  88. package/src/core/UndoRedo.js +59 -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-PXFPPO3R.js.map +0 -7
  126. package/dist/react/chunk-TKXKJNTX.js.map +0 -7
  127. package/dist/react/chunk-VQVEPJSA.js.map +0 -7
  128. package/dist/react/chunk-XFL4R5ZF.js.map +0 -7
  129. /package/dist/react/{Circle-6POZCUZD.js.map → Circle-HMM5PKJ2.js.map} +0 -0
  130. /package/dist/react/{Frame-7D2MXCFF.js.map → Frame-QEUEYITT.js.map} +0 -0
  131. /package/dist/react/{FreehandStroke-U6GYZOM4.js.map → FreehandStroke-WXIJKD5I.js.map} +0 -0
  132. /package/dist/react/{IconShape-ZADX7Y33.js.map → IconShape-NHJE4YBY.js.map} +0 -0
  133. /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-33VCMSDP.js.map} +0 -0
  134. /package/dist/react/{Line-APEULMHE.js.map → Line-A6FE232Q.js.map} +0 -0
  135. /package/dist/react/{MermaidSequenceParser-UKG2NSZY.js.map → MermaidSequenceParser-DBYXW23P.js.map} +0 -0
  136. /package/dist/react/{Rectangle-S7XUKWYD.js.map → Rectangle-4U4CD2ER.js.map} +0 -0
  137. /package/dist/react/{Selection-2TSH3J6P.js.map → Selection-23MOLHEI.js.map} +0 -0
  138. /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-OPJON7TD.js.map} +0 -0
  139. /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-QVNRI5OB.js.map} +0 -0
  140. /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-VH3PJPDM.js.map} +0 -0
  141. /package/dist/react/{chunk-LSHZYN77.js.map → chunk-2DZHO67H.js.map} +0 -0
  142. /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-5NHJJJBZ.js.map} +0 -0
  143. /package/dist/react/{chunk-ASBC5G35.js.map → chunk-B6CYUZC4.js.map} +0 -0
  144. /package/dist/react/{chunk-OP56XPWF.js.map → chunk-DNFPTVIP.js.map} +0 -0
  145. /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-DWEFPKDO.js.map} +0 -0
  146. /package/dist/react/{chunk-GQUZPON4.js.map → chunk-F56XUE5S.js.map} +0 -0
  147. /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-JEGLUNBC.js.map} +0 -0
  148. /package/dist/react/{chunk-APTOPAOY.js.map → chunk-SSFEN5CY.js.map} +0 -0
  149. /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-UEL2G6D2.js.map} +0 -0
  150. /package/dist/react/{chunk-M6TMN76K.js.map → chunk-ULPIHUVC.js.map} +0 -0
  151. /package/dist/react/{chunk-EOURRJ55.js.map → chunk-XSE4HQ2D.js.map} +0 -0
  152. /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-2UODOBCV.js.map} +0 -0
  153. /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-F4RV7SAI.js.map} +0 -0
  154. /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-NTGA5BI6.js.map} +0 -0
  155. /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-L55EUB5C.js.map} +0 -0
  156. /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-KQQBLGR3.js.map} +0 -0
  157. /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-LDJ45AD7.js.map} +0 -0
  158. /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-CSAFD7PW.js.map} +0 -0
  159. /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-YPD2SIIZ.js.map} +0 -0
  160. /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-VQHMWEFJ.js.map} +0 -0
@@ -124,20 +124,8 @@ const handleMouseDown = (e) => {
124
124
  else if(anchorInfo.type === 'rotate')
125
125
  {
126
126
  isRotatingShapeCircle = true;
127
- const CTM = currentShape.group.getCTM();
128
- if(CTM)
129
- {
130
- const svgPoint = svg.createSVGPoint();
131
- svgPoint.x = currentShape.x;
132
- svgPoint.y = currentShape.y;
133
- const centerSVGPoint = svgPoint.matrixTransform(CTM);
134
- startRotationMouseAngleCircle = Math.atan2(svgMouseY - centerSVGPoint.y, svgMouseX - centerSVGPoint.x) * (180 / Math.PI);
135
- startShapeRotationCircle = currentShape.rotation;
136
- }
137
- else
138
- {
139
- isRotatingShapeCircle = false;
140
- }
127
+ startRotationMouseAngleCircle = Math.atan2(svgMouseY - currentShape.y, svgMouseX - currentShape.x) * (180 / Math.PI);
128
+ startShapeRotationCircle = currentShape.rotation;
141
129
  }
142
130
  clickedOnShape = true;
143
131
  }
@@ -293,13 +281,7 @@ const handleMouseMove = (e) => {
293
281
  }
294
282
  else if (isRotatingShapeCircle && currentShape && currentShape.isSelected)
295
283
  {
296
- const CTM = currentShape.group.getCTM();
297
- if(CTM) {
298
- const svgPoint = svg.createSVGPoint();
299
- svgPoint.x = currentShape.x;
300
- svgPoint.y = currentShape.y;
301
- const centerSVGPoint = svgPoint.matrixTransform(CTM);
302
- const currentMouseAngle = Math.atan2(svgMouseY - centerSVGPoint.y, svgMouseX - centerSVGPoint.x) * (180 / Math.PI);
284
+ const currentMouseAngle = Math.atan2(svgMouseY - currentShape.y, svgMouseX - currentShape.x) * (180 / Math.PI);
303
285
  const angleDiff = currentMouseAngle - startRotationMouseAngleCircle;
304
286
  let newRotation = startShapeRotationCircle + angleDiff;
305
287
  const snapAngle = 15;
@@ -311,12 +293,6 @@ const handleMouseMove = (e) => {
311
293
  currentShape.draw();
312
294
  currentShape._skipAnchors = false;
313
295
  svg.style.cursor = 'grabbing';
314
- }
315
- else
316
- {
317
- isRotatingShapeCircle = false;
318
- svg.style.cursor = 'default';
319
- }
320
296
  }
321
297
  else if (isSelectionToolActive && !isDrawingCircle && currentShape && currentShape.isSelected)
322
298
  {
@@ -1,7 +1,7 @@
1
1
  /* eslint-disable */
2
2
  import { pushOptionsChangeAction } from '../core/UndoRedo.js';
3
3
 
4
- const CLOSED_SHAPES = new Set(['rectangle', 'circle']);
4
+ const OPTION_FILL_SHAPES = new Set(['rectangle', 'circle']);
5
5
 
6
6
  window.paintBucketSettings = window.paintBucketSettings || {
7
7
  fillColor: '#A98DEB',
@@ -17,34 +17,91 @@ function getSVGPoint(event) {
17
17
  };
18
18
  }
19
19
 
20
- function findClosedShapeAt(x, y) {
20
+ function isClosedFreehand(shape) {
21
+ if (shape?.shapeName !== 'freehandStroke' || !Array.isArray(shape.points) || shape.points.length < 3) return false;
22
+ if (shape.options?.closedFill) return true;
23
+ const first = shape.points[0];
24
+ const last = shape.points[shape.points.length - 1];
25
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
26
+ for (const point of shape.points) {
27
+ const x = Number(point[0]) || 0;
28
+ const y = Number(point[1]) || 0;
29
+ minX = Math.min(minX, x);
30
+ minY = Math.min(minY, y);
31
+ maxX = Math.max(maxX, x);
32
+ maxY = Math.max(maxY, y);
33
+ }
34
+ const diagonal = Math.hypot(maxX - minX, maxY - minY);
35
+ const closeDistance = Math.hypot(last[0] - first[0], last[1] - first[1]);
36
+ const tolerance = Math.max((shape.options?.strokeWidth || 2) * 2, Math.min(16, diagonal * 0.08));
37
+ return diagonal > 0 && closeDistance <= tolerance;
38
+ }
39
+
40
+ export function isPaintBucketFillable(shape) {
41
+ return OPTION_FILL_SHAPES.has(shape?.shapeName)
42
+ || shape?.shapeName === 'frame'
43
+ || isClosedFreehand(shape);
44
+ }
45
+
46
+ function findShapeAt(x, y) {
21
47
  for (let index = shapes.length - 1; index >= 0; index -= 1) {
22
48
  const shape = shapes[index];
23
- if (!CLOSED_SHAPES.has(shape?.shapeName) || typeof shape.contains !== 'function') continue;
49
+ if (typeof shape?.contains !== 'function') continue;
24
50
  if (shape.contains(x, y)) return shape;
25
51
  }
26
52
  return null;
27
53
  }
28
54
 
29
- export function handlePaintBucketDown(event) {
30
- if (!window.isPaintBucketToolActive || event.button !== 0) return;
31
- const { x, y } = getSVGPoint(event);
32
- const shape = findClosedShapeAt(x, y);
33
- if (!shape) {
34
- window.dispatchEvent(new CustomEvent('lixsketch:bucket-miss'));
35
- return;
36
- }
37
-
38
- const settings = window.paintBucketSettings;
55
+ export function fillShape(shape, settings = window.paintBucketSettings) {
56
+ if (!isPaintBucketFillable(shape)) return false;
39
57
  const transparent = settings.fillStyle === 'none' || settings.fillStyle === 'transparent';
40
58
  const nextFill = transparent ? 'transparent' : settings.fillColor;
41
59
  const nextStyle = transparent ? 'none' : settings.fillStyle;
42
- if (shape.options.fill === nextFill && shape.options.fillStyle === nextStyle) return;
60
+
61
+ if (shape.shapeName === 'frame') {
62
+ if (shape.fillColor === nextFill && shape.fillStyle === nextStyle) return false;
63
+ const oldOptions = { ...shape.options, fillColor: shape.fillColor, fillStyle: shape.fillStyle };
64
+ const newOptions = { ...shape.options, fillColor: nextFill, fillStyle: nextStyle };
65
+ pushOptionsChangeAction(shape, oldOptions, newOptions);
66
+ shape.fillColor = nextFill;
67
+ shape.fillStyle = nextStyle;
68
+ shape.options = { ...shape.options, fillColor: nextFill, fillStyle: nextStyle };
69
+ shape.draw();
70
+ return true;
71
+ }
72
+
73
+ const freehandOptions = shape.shapeName === 'freehandStroke'
74
+ ? {
75
+ closedFill: true,
76
+ outlineStroke: shape.options.outlineStroke || shape.options.stroke,
77
+ }
78
+ : {};
79
+ if (shape.options.fill === nextFill
80
+ && shape.options.fillStyle === nextStyle
81
+ && Object.entries(freehandOptions).every(([key, value]) => shape.options[key] === value)) return false;
43
82
 
44
83
  const oldOptions = { ...shape.options };
45
- const newOptions = { ...shape.options, fill: nextFill, fillStyle: nextStyle };
84
+ const newOptions = { ...shape.options, ...freehandOptions, fill: nextFill, fillStyle: nextStyle };
46
85
  pushOptionsChangeAction(shape, oldOptions, newOptions);
47
86
  shape.options = newOptions;
48
87
  shape.draw();
88
+ return true;
89
+ }
90
+
91
+ export function handlePaintBucketDown(event) {
92
+ if (!window.isPaintBucketToolActive || event.button !== 0) return;
93
+ const { x, y } = getSVGPoint(event);
94
+ const shape = findShapeAt(x, y);
95
+ if (!shape) {
96
+ window.dispatchEvent(new CustomEvent('lixsketch:bucket-miss'));
97
+ return;
98
+ }
99
+ if (!isPaintBucketFillable(shape)) {
100
+ window.dispatchEvent(new CustomEvent('lixsketch:bucket-not-fillable', {
101
+ detail: { shapeID: shape.shapeID, shapeType: shape.shapeName },
102
+ }));
103
+ return;
104
+ }
105
+ if (!fillShape(shape)) return;
49
106
  window.dispatchEvent(new CustomEvent('lixsketch:bucket-filled', { detail: { shapeID: shape.shapeID } }));
50
107
  }
@@ -113,18 +113,10 @@ const handleMouseDownRect = (e) => {
113
113
  } else if (anchorInfo.type === 'rotate') {
114
114
  isRotatingShapeSquare = true;
115
115
 
116
- const CTM = currentShape.group.getCTM();
117
- if (CTM) {
118
- const svgPoint = svg.createSVGPoint();
119
- svgPoint.x = currentShape.width / 2;
120
- svgPoint.y = currentShape.height / 2;
121
- const centerSVG = svgPoint.matrixTransform(CTM);
122
- startRotationMouseAngleSquare = Math.atan2(mouseY - centerSVG.y, mouseX - centerSVG.x) * 180 / Math.PI;
123
- startShapeRotationSquare = currentShape.rotation;
124
- } else {
125
- isRotatingShapeSquare = false;
126
- console.warn("Could not get CTM for rotation.");
127
- }
116
+ const centerX = currentShape.x + currentShape.width / 2;
117
+ const centerY = currentShape.y + currentShape.height / 2;
118
+ startRotationMouseAngleSquare = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI;
119
+ startShapeRotationSquare = currentShape.rotation;
128
120
  }
129
121
  clickedOnShape = true;
130
122
  } else if (currentShape.contains(mouseX, mouseY)) {
@@ -278,13 +270,9 @@ const handleMouseMoveRect = (e) => {
278
270
  currentShape.draw();
279
271
  currentShape._skipAnchors = false;
280
272
  } else if (isRotatingShapeSquare && currentShape && currentShape.isSelected) {
281
- const CTM = currentShape.group.getCTM();
282
- if (CTM) {
283
- const svgPoint = svg.createSVGPoint();
284
- svgPoint.x = currentShape.width / 2;
285
- svgPoint.y = currentShape.height / 2;
286
- const centerSVG = svgPoint.matrixTransform(CTM);
287
- const currentRotationMouseAngle = Math.atan2(mouseY - centerSVG.y, mouseX - centerSVG.x) * 180 / Math.PI;
273
+ const centerX = currentShape.x + currentShape.width / 2;
274
+ const centerY = currentShape.y + currentShape.height / 2;
275
+ const currentRotationMouseAngle = Math.atan2(mouseY - centerY, mouseX - centerX) * 180 / Math.PI;
288
276
  const angleDiff = currentRotationMouseAngle - startRotationMouseAngleSquare;
289
277
  let newRotation = startShapeRotationSquare + angleDiff;
290
278
  const snapAngle = 15;
@@ -296,10 +284,6 @@ const handleMouseMoveRect = (e) => {
296
284
  currentShape.draw();
297
285
  currentShape._skipAnchors = false;
298
286
  svg.style.cursor = 'grabbing';
299
- } else {
300
- isRotatingShapeSquare = false;
301
- svg.style.cursor = 'default';
302
- }
303
287
  } else if (isSelectionToolActive && !isDrawingSquare && currentShape && currentShape.isSelected) {
304
288
  const anchorInfo = currentShape.isNearAnchor(mouseX, mouseY);
305
289
  if (anchorInfo) {
@@ -0,0 +1,270 @@
1
+ const MIN_SHAPE_SIZE = 8;
2
+
3
+ export const MAX_GESTURE_POINTS = 96;
4
+
5
+ export function distance(a, b) {
6
+ return Math.hypot(b.x - a.x, b.y - a.y);
7
+ }
8
+
9
+ function orientedBounds(sample, angle) {
10
+ const cos = Math.cos(angle);
11
+ const sin = Math.sin(angle);
12
+ let minU = Infinity;
13
+ let maxU = -Infinity;
14
+ let minV = Infinity;
15
+ let maxV = -Infinity;
16
+ for (const point of sample) {
17
+ const u = point.x * cos + point.y * sin;
18
+ const v = -point.x * sin + point.y * cos;
19
+ minU = Math.min(minU, u);
20
+ maxU = Math.max(maxU, u);
21
+ minV = Math.min(minV, v);
22
+ maxV = Math.max(maxV, v);
23
+ }
24
+ const centerU = (minU + maxU) / 2;
25
+ const centerV = (minV + maxV) / 2;
26
+ return {
27
+ center: {
28
+ x: centerU * cos - centerV * sin,
29
+ y: centerU * sin + centerV * cos,
30
+ },
31
+ width: Math.max(MIN_SHAPE_SIZE, maxU - minU),
32
+ height: Math.max(MIN_SHAPE_SIZE, maxV - minV),
33
+ angle,
34
+ minU,
35
+ maxU,
36
+ minV,
37
+ maxV,
38
+ };
39
+ }
40
+
41
+ function principalAngle(sample) {
42
+ let meanX = 0;
43
+ let meanY = 0;
44
+ for (const point of sample) {
45
+ meanX += point.x;
46
+ meanY += point.y;
47
+ }
48
+ meanX /= sample.length;
49
+ meanY /= sample.length;
50
+ let xx = 0;
51
+ let yy = 0;
52
+ let xy = 0;
53
+ for (const point of sample) {
54
+ const dx = point.x - meanX;
55
+ const dy = point.y - meanY;
56
+ xx += dx * dx;
57
+ yy += dy * dy;
58
+ xy += dx * dy;
59
+ }
60
+ if (Math.abs(xx - yy) + Math.abs(xy) < 0.001) return 0;
61
+ return Math.atan2(2 * xy, xx - yy) / 2;
62
+ }
63
+
64
+ function rectangleAngle(sample) {
65
+ let x = 0;
66
+ let y = 0;
67
+ for (let index = 1; index < sample.length; index += 1) {
68
+ const dx = sample[index].x - sample[index - 1].x;
69
+ const dy = sample[index].y - sample[index - 1].y;
70
+ const length = Math.hypot(dx, dy);
71
+ if (length === 0) continue;
72
+ const angle = Math.atan2(dy, dx) * 4;
73
+ x += Math.cos(angle) * length;
74
+ y += Math.sin(angle) * length;
75
+ }
76
+ return Math.atan2(y, x) / 4;
77
+ }
78
+
79
+ function pointToSegmentDistance(point, start, end) {
80
+ const dx = end.x - start.x;
81
+ const dy = end.y - start.y;
82
+ const lengthSquared = dx * dx + dy * dy;
83
+ if (lengthSquared === 0) return distance(point, start);
84
+ const amount = Math.max(0, Math.min(1,
85
+ ((point.x - start.x) * dx + (point.y - start.y) * dy) / lengthSquared));
86
+ return Math.hypot(point.x - (start.x + dx * amount), point.y - (start.y + dy * amount));
87
+ }
88
+
89
+ function polygonError(sample, vertices, scale) {
90
+ let total = 0;
91
+ for (const point of sample) {
92
+ let nearest = Infinity;
93
+ for (let index = 0; index < vertices.length; index += 1) {
94
+ nearest = Math.min(nearest, pointToSegmentDistance(
95
+ point,
96
+ vertices[index],
97
+ vertices[(index + 1) % vertices.length],
98
+ ));
99
+ }
100
+ total += nearest;
101
+ }
102
+ return total / sample.length / Math.max(scale, MIN_SHAPE_SIZE);
103
+ }
104
+
105
+ function polygonVertices(bounds, kind) {
106
+ const cos = Math.cos(bounds.angle);
107
+ const sin = Math.sin(bounds.angle);
108
+ const toWorld = (u, v) => ({
109
+ x: u * cos - v * sin,
110
+ y: u * sin + v * cos,
111
+ });
112
+ const centerU = (bounds.minU + bounds.maxU) / 2;
113
+ const centerV = (bounds.minV + bounds.maxV) / 2;
114
+ const local = kind === 'triangle'
115
+ ? [[centerU, bounds.minV], [bounds.maxU, bounds.maxV], [bounds.minU, bounds.maxV]]
116
+ : kind === 'diamond'
117
+ ? [[centerU, bounds.minV], [bounds.maxU, centerV], [centerU, bounds.maxV], [bounds.minU, centerV]]
118
+ : [[bounds.minU, bounds.minV], [bounds.maxU, bounds.minV], [bounds.maxU, bounds.maxV], [bounds.minU, bounds.maxV]];
119
+ return local.map(([u, v]) => toWorld(u, v));
120
+ }
121
+
122
+ function trianglePrediction(sample, pathLength) {
123
+ const baseAngle = principalAngle(sample);
124
+ let best = null;
125
+ // Principal axes do not encode direction. Testing four quarter turns keeps
126
+ // the fit rotation-independent while retaining a constant number of O(N)
127
+ // passes over the capped gesture sample.
128
+ for (let turn = 0; turn < 4; turn += 1) {
129
+ const bounds = orientedBounds(sample, baseAngle + turn * Math.PI / 2);
130
+ const vertices = polygonVertices(bounds, 'triangle');
131
+ const error = polygonError(sample, vertices, Math.hypot(bounds.width, bounds.height));
132
+ if (!best || error < best.error) best = { bounds, vertices, error };
133
+ }
134
+ if (!best || best.error > 0.045) return null;
135
+ return {
136
+ type: 'triangle',
137
+ ...best.bounds,
138
+ vertices: best.vertices,
139
+ pathLength,
140
+ };
141
+ }
142
+
143
+ function diamondPrediction(sample, pathLength) {
144
+ const bounds = orientedBounds(sample, principalAngle(sample));
145
+ const vertices = polygonVertices(bounds, 'diamond');
146
+ const error = polygonError(sample, vertices, Math.hypot(bounds.width, bounds.height));
147
+ if (error > 0.035) return null;
148
+ return { type: 'diamond', ...bounds, vertices, pathLength };
149
+ }
150
+
151
+ function closedShapePrediction(sample, pathLength) {
152
+ const triangle = trianglePrediction(sample, pathLength);
153
+ if (triangle) return triangle;
154
+ const fittedDiamond = diamondPrediction(sample, pathLength);
155
+ if (fittedDiamond) return fittedDiamond;
156
+ const rectBounds = orientedBounds(sample, rectangleAngle(sample));
157
+ const ellipseBounds = orientedBounds(sample, principalAngle(sample));
158
+ let rectangleError = 0;
159
+ let ellipseError = 0;
160
+ const rectCos = Math.cos(rectBounds.angle);
161
+ const rectSin = Math.sin(rectBounds.angle);
162
+ const ellipseCos = Math.cos(ellipseBounds.angle);
163
+ const ellipseSin = Math.sin(ellipseBounds.angle);
164
+ const rectHalfW = rectBounds.width / 2;
165
+ const rectHalfH = rectBounds.height / 2;
166
+ const ellipseHalfW = ellipseBounds.width / 2;
167
+ const ellipseHalfH = ellipseBounds.height / 2;
168
+
169
+ for (const point of sample) {
170
+ const rectDx = point.x - rectBounds.center.x;
171
+ const rectDy = point.y - rectBounds.center.y;
172
+ const rectU = Math.abs(rectDx * rectCos + rectDy * rectSin) / rectHalfW;
173
+ const rectV = Math.abs(-rectDx * rectSin + rectDy * rectCos) / rectHalfH;
174
+ rectangleError += Math.min(Math.abs(1 - rectU), Math.abs(1 - rectV));
175
+ const ellipseDx = point.x - ellipseBounds.center.x;
176
+ const ellipseDy = point.y - ellipseBounds.center.y;
177
+ const ellipseU = (ellipseDx * ellipseCos + ellipseDy * ellipseSin) / ellipseHalfW;
178
+ const ellipseV = (-ellipseDx * ellipseSin + ellipseDy * ellipseCos) / ellipseHalfH;
179
+ ellipseError += Math.abs(1 - Math.hypot(ellipseU, ellipseV));
180
+ }
181
+
182
+ rectangleError /= sample.length;
183
+ ellipseError /= sample.length;
184
+ const rectangle = rectangleError <= ellipseError * 1.08;
185
+ const bestError = rectangle ? rectangleError : ellipseError;
186
+ if (bestError > 0.24) {
187
+ return { type: 'freehand', points: sample.map((point) => ({ ...point })), pathLength };
188
+ }
189
+ const bounds = rectangle ? rectBounds : ellipseBounds;
190
+ const normalizedAngle = Math.abs(Math.atan2(Math.sin(rectBounds.angle * 2), Math.cos(rectBounds.angle * 2)) / 2);
191
+ const diamond = rectangle && normalizedAngle >= Math.PI / 7;
192
+ return {
193
+ type: diamond ? 'diamond' : (rectangle ? 'rectangle' : 'circle'),
194
+ ...bounds,
195
+ ...(diamond ? { vertices: polygonVertices(rectBounds, 'rectangle') } : {}),
196
+ pathLength,
197
+ };
198
+ }
199
+
200
+ export function capGestureSample(sample, maxPoints = MAX_GESTURE_POINTS) {
201
+ if (!Array.isArray(sample) || sample.length <= maxPoints) return Array.isArray(sample) ? sample : [];
202
+ if (maxPoints <= 1) return sample.slice(0, Math.max(0, maxPoints));
203
+ const capped = [sample[0]];
204
+ const step = (sample.length - 1) / (maxPoints - 1);
205
+ for (let index = 1; index < maxPoints - 1; index += 1) {
206
+ capped.push(sample[Math.round(index * step)]);
207
+ }
208
+ capped.push(sample[sample.length - 1]);
209
+ return capped;
210
+ }
211
+
212
+ export function predictDrawnShape(input) {
213
+ const sample = capGestureSample(input);
214
+ if (sample.length < 2) return null;
215
+ const start = sample[0];
216
+ let pathLength = 0;
217
+ let farthestIndex = 1;
218
+ let farthestDistance = 0;
219
+ let minX = start.x;
220
+ let maxX = start.x;
221
+ let minY = start.y;
222
+ let maxY = start.y;
223
+
224
+ for (let index = 1; index < sample.length; index += 1) {
225
+ pathLength += distance(sample[index - 1], sample[index]);
226
+ const fromStart = distance(start, sample[index]);
227
+ if (fromStart > farthestDistance) {
228
+ farthestDistance = fromStart;
229
+ farthestIndex = index;
230
+ }
231
+ minX = Math.min(minX, sample[index].x);
232
+ maxX = Math.max(maxX, sample[index].x);
233
+ minY = Math.min(minY, sample[index].y);
234
+ maxY = Math.max(maxY, sample[index].y);
235
+ }
236
+
237
+ const diagonal = Math.max(MIN_SHAPE_SIZE, Math.hypot(maxX - minX, maxY - minY));
238
+ const end = sample[sample.length - 1];
239
+ const closed = sample.length >= 8 && distance(start, end) <= diagonal * 0.28 && pathLength >= diagonal * 1.85;
240
+ if (closed) return closedShapePrediction(sample, pathLength);
241
+
242
+ const tip = sample[farthestIndex];
243
+ let afterTipLength = 0;
244
+ for (let index = farthestIndex + 1; index < sample.length; index += 1) {
245
+ afterTipLength += distance(sample[index - 1], sample[index]);
246
+ }
247
+ let shaftLength = 0;
248
+ for (let index = 1; index <= farthestIndex; index += 1) {
249
+ shaftLength += distance(sample[index - 1], sample[index]);
250
+ }
251
+ const arrow = farthestIndex >= Math.floor(sample.length * 0.45)
252
+ && farthestIndex < sample.length - 2
253
+ && afterTipLength >= diagonal * 0.18
254
+ && distance(end, tip) <= diagonal * 0.48
255
+ && farthestDistance / Math.max(shaftLength, 1) >= 0.86;
256
+ const directDistance = distance(start, end);
257
+ const line = !arrow && directDistance / Math.max(pathLength, 1) >= 0.92;
258
+
259
+ if (!arrow && !line) {
260
+ return { type: 'freehand', points: sample.map((point) => ({ ...point })), pathLength };
261
+ }
262
+ return {
263
+ type: arrow ? 'arrow' : 'line',
264
+ start: { ...start },
265
+ end: arrow ? { ...tip } : { ...end },
266
+ width: Math.max(MIN_SHAPE_SIZE, maxX - minX),
267
+ height: Math.max(MIN_SHAPE_SIZE, maxY - minY),
268
+ pathLength,
269
+ };
270
+ }