@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
@@ -101,6 +101,36 @@ class Frame {
101
101
  let fillValue = "transparent";
102
102
  if (this.fillStyle === 'solid') {
103
103
  fillValue = this.fillColor;
104
+ } else if (['hachure', 'cross-hatch', 'dots'].includes(this.fillStyle)) {
105
+ const patternId = `frame-fill-${this.shapeID}`;
106
+ let pattern = defs.querySelector(`#${patternId}`);
107
+ if (!pattern) {
108
+ pattern = document.createElementNS('http://www.w3.org/2000/svg', 'pattern');
109
+ pattern.setAttribute('id', patternId);
110
+ pattern.setAttribute('patternUnits', 'userSpaceOnUse');
111
+ defs.appendChild(pattern);
112
+ }
113
+ pattern.setAttribute('width', 10);
114
+ pattern.setAttribute('height', 10);
115
+ while (pattern.firstChild) pattern.removeChild(pattern.firstChild);
116
+ if (this.fillStyle === 'dots') {
117
+ const dot = document.createElementNS('http://www.w3.org/2000/svg', 'circle');
118
+ dot.setAttribute('cx', 5);
119
+ dot.setAttribute('cy', 5);
120
+ dot.setAttribute('r', 1.5);
121
+ dot.setAttribute('fill', this.fillColor);
122
+ pattern.appendChild(dot);
123
+ } else {
124
+ const hatch = document.createElementNS('http://www.w3.org/2000/svg', 'path');
125
+ hatch.setAttribute('d', this.fillStyle === 'cross-hatch'
126
+ ? 'M-2 2L2-2M0 10L10 0M8 12L12 8M-2 8L2 12M0 0L10 10M8-2L12 2'
127
+ : 'M-2 2L2-2M0 10L10 0M8 12L12 8');
128
+ hatch.setAttribute('fill', 'none');
129
+ hatch.setAttribute('stroke', this.fillColor);
130
+ hatch.setAttribute('stroke-width', 1.5);
131
+ pattern.appendChild(hatch);
132
+ }
133
+ fillValue = `url(#${patternId})`;
104
134
  } else if (this.fillStyle === 'grid') {
105
135
  // Create or update a grid pattern unique to this frame
106
136
  const patternId = `frame-grid-${this.shapeID}`;
@@ -426,6 +456,14 @@ move(dx, dy) {
426
456
  this.updateClipPath();
427
457
  }
428
458
 
459
+ finalizeMove() {
460
+ this.containedShapes.forEach(shape => {
461
+ if (shape && typeof shape.finalizeMove === 'function') {
462
+ shape.finalizeMove();
463
+ }
464
+ });
465
+ }
466
+
429
467
  handleResizeLegacy(anchorIndex, currentPos, startPos, initialFrame) {
430
468
  // Legacy version - superseded by the handleResize below with contained shapes scaling
431
469
  const dx = currentPos.x - startPos.x;
@@ -1409,75 +1447,6 @@ restoreToFrame(shape) {
1409
1447
  this.draw();
1410
1448
  }
1411
1449
 
1412
- destroy() {
1413
- const isDiagramFrame = !!this._diagramType;
1414
-
1415
- [...this.containedShapes].forEach(shape => {
1416
- if (isDiagramFrame) {
1417
- // Diagram/graph frame: destroy contained shapes with the frame
1418
- if (typeof window.cleanupAttachments === 'function') {
1419
- window.cleanupAttachments(shape);
1420
- }
1421
- const el = shape.group || shape.element;
1422
- if (el && el.parentNode) {
1423
- el.parentNode.removeChild(el);
1424
- }
1425
- if (shape.shapeName === 'frame') {
1426
- if (shape.clipGroup && shape.clipGroup.parentNode) {
1427
- shape.clipGroup.parentNode.removeChild(shape.clipGroup);
1428
- }
1429
- if (shape.clipPath && shape.clipPath.parentNode) {
1430
- shape.clipPath.parentNode.removeChild(shape.clipPath);
1431
- }
1432
- }
1433
- const idx = shapes.indexOf(shape);
1434
- if (idx > -1) {
1435
- shapes.splice(idx, 1);
1436
- }
1437
- } else {
1438
- // Regular frame: release contained shapes back to canvas
1439
- const el = shape.group || shape.element;
1440
- if (el) {
1441
- if (el.parentNode === this.clipGroup) {
1442
- this.clipGroup.removeChild(el);
1443
- }
1444
- svg.appendChild(el);
1445
- }
1446
- if (shape.shapeName === 'frame' && shape.clipGroup) {
1447
- if (shape.clipGroup.parentNode === this.clipGroup) {
1448
- this.clipGroup.removeChild(shape.clipGroup);
1449
- }
1450
- svg.appendChild(shape.clipGroup);
1451
- }
1452
- }
1453
- shape.parentFrame = null;
1454
- delete shape.isBeingMovedByFrame;
1455
- });
1456
- this.containedShapes = [];
1457
-
1458
- // Remove clip path
1459
- if (this.clipPath && this.clipPath.parentNode) {
1460
- this.clipPath.parentNode.removeChild(this.clipPath);
1461
- }
1462
-
1463
- // Remove groups (clipGroup should be empty now)
1464
- if (this.clipGroup && this.clipGroup.parentNode) {
1465
- this.clipGroup.parentNode.removeChild(this.clipGroup);
1466
- }
1467
-
1468
- if (this.group && this.group.parentNode) {
1469
- this.group.parentNode.removeChild(this.group);
1470
- }
1471
-
1472
- const index = shapes.indexOf(this);
1473
- if (index > -1) {
1474
- shapes.splice(index, 1);
1475
- }
1476
- if (currentShape === this) {
1477
- currentShape = null;
1478
- }
1479
- }
1480
-
1481
1450
  /**
1482
1451
  * Set a background image for this frame from a URL.
1483
1452
  * The image will fit/cover the frame dimensions.
@@ -69,6 +69,8 @@ class FreehandStroke {
69
69
  this.selectionOutline = null;
70
70
  this.boundingBox = { x: 0, y: 0, width: 0, height: 0 };
71
71
  this.shapeName = "freehandStroke";
72
+ this.shapeID = `freehand-${String(Date.now()).slice(0, 8)}-${Math.floor(Math.random() * 10000)}`;
73
+ this.group.setAttribute('id', this.shapeID);
72
74
  this._moveOffsetX = 0;
73
75
  this._moveOffsetY = 0;
74
76
 
@@ -304,7 +306,16 @@ class FreehandStroke {
304
306
  const path = document.createElementNS('http://www.w3.org/2000/svg', 'path');
305
307
  const pathData = this.getPathData();
306
308
  path.setAttribute('d', pathData);
307
- path.setAttribute('fill', this.options.stroke);
309
+ const closedFillStyle = this.options.fillStyle || 'solid';
310
+ const closedFill = closedFillStyle === 'none'
311
+ ? 'transparent'
312
+ : (this.options.fill || this.options.stroke);
313
+ const patternedFill = this.options.closedFill
314
+ && !['none', 'solid'].includes(closedFillStyle)
315
+ && typeof rough !== 'undefined';
316
+ path.setAttribute('fill', this.options.closedFill
317
+ ? (patternedFill ? 'transparent' : closedFill)
318
+ : this.options.stroke);
308
319
  path.setAttribute('fill-opacity', this.options.strokeOpacity);
309
320
  path.setAttribute('stroke', this.options.closedFill ? (this.options.outlineStroke || 'none') : 'none');
310
321
  if (this.options.closedFill) {
@@ -330,6 +341,18 @@ class FreehandStroke {
330
341
  this.group.appendChild(overlay);
331
342
  }
332
343
 
344
+ if (patternedFill) {
345
+ const fillGroup = rough.svg(svg).path(pathData, {
346
+ stroke: 'none',
347
+ fill: closedFill,
348
+ fillStyle: closedFillStyle,
349
+ fillWeight: Math.max(1, (this.options.strokeWidth || 2) / 2),
350
+ roughness: 0.8,
351
+ });
352
+ fillGroup.setAttribute('data-freehand-fill-pattern', closedFillStyle);
353
+ this.group.appendChild(fillGroup);
354
+ }
355
+
333
356
  this.element = path;
334
357
  this.group.appendChild(path);
335
358
 
@@ -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
  // Rectangle shape class - extracted from drawSquare.js
4
5
  // Depends on globals: svg, shapes, rough, currentShape, currentZoom, rc
5
6
 
@@ -640,18 +641,17 @@ class Rectangle {
640
641
  // Helper to check if a point is near an anchor
641
642
  isNearAnchor(x, y) {
642
643
  if (!this.isSelected) return null;
643
- const buffer = 10;
644
- const anchorSize = 10;
644
+ const zoom = window.currentZoom || 1;
645
+ const buffer = 10 / zoom;
646
+ const anchorSize = 10 / zoom;
647
+ const transform = { x: this.x, y: this.y, rotation: this.rotation, centerX: this.width / 2, centerY: this.height / 2 };
645
648
 
646
649
  // Iterate through anchors
647
650
  for (let i = 0; i < this.anchors.length; i++) {
648
651
  const anchor = this.anchors[i];
649
652
  const anchorLocalX = parseFloat(anchor.getAttribute('x')) + anchorSize / 2;
650
653
  const anchorLocalY = parseFloat(anchor.getAttribute('y')) + anchorSize / 2;
651
- const svgPoint = svg.createSVGPoint();
652
- svgPoint.x = anchorLocalX;
653
- svgPoint.y = anchorLocalY;
654
- const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
654
+ const transformedPoint = localToCanvas({ x: anchorLocalX, y: anchorLocalY }, transform);
655
655
  const anchorLeft = transformedPoint.x - anchorSize/2 - buffer;
656
656
  const anchorRight = transformedPoint.x + anchorSize/2 + buffer;
657
657
  const anchorTop = transformedPoint.y - anchorSize/2 - buffer;
@@ -665,10 +665,7 @@ class Rectangle {
665
665
  if (this.rotationAnchor) {
666
666
  const rotateAnchorLocalX = parseFloat(this.rotationAnchor.getAttribute('cx'));
667
667
  const rotateAnchorLocalY = parseFloat(this.rotationAnchor.getAttribute('cy'));
668
- const svgPoint = svg.createSVGPoint();
669
- svgPoint.x = rotateAnchorLocalX;
670
- svgPoint.y = rotateAnchorLocalY;
671
- const transformedPoint = svgPoint.matrixTransform(this.group.getCTM());
668
+ const transformedPoint = localToCanvas({ x: rotateAnchorLocalX, y: rotateAnchorLocalY }, transform);
672
669
  const rotateAnchorRadius = parseFloat(this.rotationAnchor.getAttribute('r'));
673
670
  const distSq = (x - transformedPoint.x)**2 + (y - transformedPoint.y)**2;
674
671
  if (distSq <= (rotateAnchorRadius + buffer)**2) {
@@ -729,16 +726,10 @@ updateFrameContainment() {
729
726
  }
730
727
 
731
728
  updatePosition(anchorIndex, newMouseX, newMouseY) {
732
- const CTM = this.group.getCTM();
733
- if (!CTM) return;
734
- const inverseCTM = CTM.inverse();
735
- const svgPoint = svg.createSVGPoint();
736
- svgPoint.x = newMouseX;
737
- svgPoint.y = newMouseY;
738
- const localMouse = svgPoint.matrixTransform(inverseCTM);
739
-
740
729
  const oldW = this.width;
741
730
  const oldH = this.height;
731
+ const transform = { x: this.x, y: this.y, rotation: this.rotation, centerX: oldW / 2, centerY: oldH / 2 };
732
+ const localMouse = canvasToLocal({ x: newMouseX, y: newMouseY }, transform);
742
733
 
743
734
  // Determine fixed point in old local coords and compute raw new dimensions
744
735
  let fixedOldX, fixedOldY, rawW, rawH;
@@ -754,10 +745,7 @@ updateFrameContainment() {
754
745
  }
755
746
 
756
747
  // Get world position of the fixed point using current CTM
757
- const fp = svg.createSVGPoint();
758
- fp.x = fixedOldX;
759
- fp.y = fixedOldY;
760
- const fixedWorld = fp.matrixTransform(CTM);
748
+ const fixedWorld = localToCanvas({ x: fixedOldX, y: fixedOldY }, transform);
761
749
 
762
750
  // Handle negative dimensions (dragged past opposite edge)
763
751
  const newW = Math.abs(rawW);
@@ -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) {