@elixpo/lixsketch 5.5.23 → 5.6.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/README.md +85 -6
  2. package/dist/mcp/index.js +585 -0
  3. package/dist/mcp/index.js.map +7 -0
  4. package/dist/mcp/node.js +184 -0
  5. package/dist/mcp/node.js.map +7 -0
  6. package/dist/mcp/stdio.js +723 -0
  7. package/dist/mcp/stdio.js.map +7 -0
  8. package/dist/react/{CopyPaste-5OWPTCQ5.js → CopyPaste-YUDZ6DHY.js} +7 -7
  9. package/dist/react/EventDispatcher-7LAK7GQ7.js +1097 -0
  10. package/dist/react/EventDispatcher-7LAK7GQ7.js.map +7 -0
  11. package/dist/react/Frame-7D2MXCFF.js +11 -0
  12. package/dist/react/IconShape-ZADX7Y33.js +10 -0
  13. package/dist/react/ImageShape-UJQN2HI7.js +10 -0
  14. package/dist/react/Line-APEULMHE.js +10 -0
  15. package/dist/react/{SceneSerializer-RDISX4X7.js → SceneSerializer-SG6SJN5X.js} +17 -7
  16. package/dist/react/SceneSerializer-SG6SJN5X.js.map +7 -0
  17. package/dist/react/{Selection-ZL3G6IAO.js → Selection-2TSH3J6P.js} +6 -4
  18. package/dist/react/{SketchEngine-OB6ZW3B6.js → SketchEngine-UD2XV2KT.js} +50 -23
  19. package/dist/react/{SketchEngine-OB6ZW3B6.js.map → SketchEngine-UD2XV2KT.js.map} +2 -2
  20. package/dist/react/{UndoRedo-NL7KLR5B.js → UndoRedo-IN4SXDG6.js} +2 -2
  21. package/dist/react/{ZoomPan-FAWA5OPS.js → ZoomPan-RR7KUYZT.js} +34 -1
  22. package/dist/react/{ZoomPan-FAWA5OPS.js.map → ZoomPan-RR7KUYZT.js.map} +2 -2
  23. package/dist/react/{arrowTool-CNVGM6RX.js → arrowTool-OYDWJGQK.js} +3 -3
  24. package/dist/react/{chunk-U3TB4KRS.js → chunk-6GPEWDRB.js} +19 -12
  25. package/dist/react/chunk-6GPEWDRB.js.map +7 -0
  26. package/dist/react/{chunk-4BLMNGTR.js → chunk-7CM5XQDN.js} +3 -3
  27. package/dist/react/{chunk-XFQECTTF.js → chunk-APTOPAOY.js} +3 -3
  28. package/dist/react/{chunk-EDSKEM6E.js → chunk-ASBC5G35.js} +47 -8
  29. package/dist/react/chunk-ASBC5G35.js.map +7 -0
  30. package/dist/react/{chunk-4TAS67SL.js → chunk-EOURRJ55.js} +2 -2
  31. package/dist/react/{chunk-ETAAQUUV.js → chunk-GQUZPON4.js} +4 -4
  32. package/dist/react/{chunk-KVAQPCUH.js → chunk-K5BG5MHY.js} +49 -5
  33. package/dist/react/chunk-K5BG5MHY.js.map +7 -0
  34. package/dist/react/{chunk-AH4PWNEB.js → chunk-K5RY4EV7.js} +23 -3
  35. package/dist/react/chunk-K5RY4EV7.js.map +7 -0
  36. package/dist/react/{chunk-GC3KTQDA.js → chunk-LJR7EVW2.js} +3 -3
  37. package/dist/react/{chunk-SZD7DKYJ.js → chunk-LSHZYN77.js} +2 -2
  38. package/dist/react/{chunk-HFQZYCHJ.js → chunk-M6TMN76K.js} +3 -3
  39. package/dist/react/{chunk-V2QTYSBK.js → chunk-OP56XPWF.js} +2 -2
  40. package/dist/react/chunk-QZI4UIE5.js +48 -0
  41. package/dist/react/chunk-QZI4UIE5.js.map +7 -0
  42. package/dist/react/{chunk-ILGAFR56.js → chunk-RGMDWLRJ.js} +3 -3
  43. package/dist/react/{chunk-OFGJJKPH.js → chunk-XFL4R5ZF.js} +3 -3
  44. package/dist/react/{chunk-GPIYXREE.js → chunk-XOWNCYE4.js} +3 -3
  45. package/dist/react/{chunk-YF3TK7SC.js → chunk-XYUFEIZR.js} +2 -2
  46. package/dist/react/{circleTool-KICNRQCI.js → circleTool-LDKGZALF.js} +4 -4
  47. package/dist/react/{codeTool-NCQ73R6M.js → codeTool-4YZO5W7J.js} +4 -4
  48. package/dist/react/{frameTool-Y6KB67DO.js → frameTool-MXNJEXQQ.js} +4 -4
  49. package/dist/react/{freehandTool-MMULHCQ7.js → freehandTool-26D3FOOX.js} +4 -4
  50. package/dist/react/{iconTool-LSW2DYGB.js → iconTool-46O5VEUX.js} +4 -4
  51. package/dist/react/{imageTool-2DSR32RX.js → imageTool-P2RWZRHO.js} +4 -4
  52. package/dist/react/index.js +848 -614
  53. package/dist/react/index.js.map +4 -4
  54. package/dist/react/{lineTool-N5I7IGT7.js → lineTool-ZMR27U5L.js} +4 -4
  55. package/dist/react/{rectangleTool-YUT7ZK2E.js → rectangleTool-EJKK6UTX.js} +4 -4
  56. package/dist/react/{textTool-JMTX7OVT.js → textTool-S4XXZHZH.js} +5 -5
  57. package/package.json +7 -1
  58. package/src/EngineShortcuts.js +22 -0
  59. package/src/SketchEngine.js +27 -0
  60. package/src/core/EventDispatcher.js +35 -4
  61. package/src/core/SceneSerializer.js +5 -0
  62. package/src/core/Selection.js +26 -0
  63. package/src/core/UndoRedo.js +20 -16
  64. package/src/core/WebEmbedPolicy.js +41 -0
  65. package/src/core/ZoomPan.js +36 -0
  66. package/src/index.js +4 -0
  67. package/src/mcp/fileStore.js +70 -0
  68. package/src/mcp/index.js +5 -0
  69. package/src/mcp/node.js +2 -0
  70. package/src/mcp/preview.js +39 -0
  71. package/src/mcp/scene.js +275 -0
  72. package/src/mcp/server.js +195 -0
  73. package/src/mcp/stdio.js +41 -0
  74. package/src/mcp/stdioTransport.js +46 -0
  75. package/src/mcp/store.js +23 -0
  76. package/src/mcp/templates.js +51 -0
  77. package/src/react/LixSketchCanvas.jsx +4 -0
  78. package/src/react/components/Toolbar.jsx +84 -7
  79. package/src/react/components/modals/CommandPalette.jsx +8 -0
  80. package/src/react/components/modals/WebEmbedModal.jsx +40 -0
  81. package/src/react/components/sidebars/PaintBucketSidebar.jsx +49 -0
  82. package/src/react/components/sidebars/PaintbrushSidebar.jsx +1 -1
  83. package/src/react/store/useSketchStore.js +13 -0
  84. package/src/react/toolsEnum.js +4 -0
  85. package/src/shapes/Frame.js +47 -4
  86. package/src/shapes/ImageShape.js +49 -6
  87. package/src/tools/lassoTool.js +84 -0
  88. package/src/tools/paintBucketTool.js +50 -0
  89. package/src/tools/shapeRecognitionTool.js +431 -0
  90. package/src/tools/webEmbedTool.js +61 -0
  91. package/dist/react/EventDispatcher-GKNO2KSL.js +0 -464
  92. package/dist/react/EventDispatcher-GKNO2KSL.js.map +0 -7
  93. package/dist/react/Frame-RLSMJJSK.js +0 -11
  94. package/dist/react/IconShape-7OPXAKLK.js +0 -10
  95. package/dist/react/ImageShape-T7UEHP4A.js +0 -10
  96. package/dist/react/Line-WWQFBNX5.js +0 -10
  97. package/dist/react/SceneSerializer-RDISX4X7.js.map +0 -7
  98. package/dist/react/chunk-AH4PWNEB.js.map +0 -7
  99. package/dist/react/chunk-EDSKEM6E.js.map +0 -7
  100. package/dist/react/chunk-KVAQPCUH.js.map +0 -7
  101. package/dist/react/chunk-U3TB4KRS.js.map +0 -7
  102. /package/dist/react/{CopyPaste-5OWPTCQ5.js.map → CopyPaste-YUDZ6DHY.js.map} +0 -0
  103. /package/dist/react/{Frame-RLSMJJSK.js.map → Frame-7D2MXCFF.js.map} +0 -0
  104. /package/dist/react/{IconShape-7OPXAKLK.js.map → IconShape-ZADX7Y33.js.map} +0 -0
  105. /package/dist/react/{ImageShape-T7UEHP4A.js.map → ImageShape-UJQN2HI7.js.map} +0 -0
  106. /package/dist/react/{Line-WWQFBNX5.js.map → Line-APEULMHE.js.map} +0 -0
  107. /package/dist/react/{Selection-ZL3G6IAO.js.map → Selection-2TSH3J6P.js.map} +0 -0
  108. /package/dist/react/{UndoRedo-NL7KLR5B.js.map → UndoRedo-IN4SXDG6.js.map} +0 -0
  109. /package/dist/react/{arrowTool-CNVGM6RX.js.map → arrowTool-OYDWJGQK.js.map} +0 -0
  110. /package/dist/react/{chunk-4BLMNGTR.js.map → chunk-7CM5XQDN.js.map} +0 -0
  111. /package/dist/react/{chunk-XFQECTTF.js.map → chunk-APTOPAOY.js.map} +0 -0
  112. /package/dist/react/{chunk-4TAS67SL.js.map → chunk-EOURRJ55.js.map} +0 -0
  113. /package/dist/react/{chunk-ETAAQUUV.js.map → chunk-GQUZPON4.js.map} +0 -0
  114. /package/dist/react/{chunk-GC3KTQDA.js.map → chunk-LJR7EVW2.js.map} +0 -0
  115. /package/dist/react/{chunk-SZD7DKYJ.js.map → chunk-LSHZYN77.js.map} +0 -0
  116. /package/dist/react/{chunk-HFQZYCHJ.js.map → chunk-M6TMN76K.js.map} +0 -0
  117. /package/dist/react/{chunk-V2QTYSBK.js.map → chunk-OP56XPWF.js.map} +0 -0
  118. /package/dist/react/{chunk-ILGAFR56.js.map → chunk-RGMDWLRJ.js.map} +0 -0
  119. /package/dist/react/{chunk-OFGJJKPH.js.map → chunk-XFL4R5ZF.js.map} +0 -0
  120. /package/dist/react/{chunk-GPIYXREE.js.map → chunk-XOWNCYE4.js.map} +0 -0
  121. /package/dist/react/{chunk-YF3TK7SC.js.map → chunk-XYUFEIZR.js.map} +0 -0
  122. /package/dist/react/{circleTool-KICNRQCI.js.map → circleTool-LDKGZALF.js.map} +0 -0
  123. /package/dist/react/{codeTool-NCQ73R6M.js.map → codeTool-4YZO5W7J.js.map} +0 -0
  124. /package/dist/react/{frameTool-Y6KB67DO.js.map → frameTool-MXNJEXQQ.js.map} +0 -0
  125. /package/dist/react/{freehandTool-MMULHCQ7.js.map → freehandTool-26D3FOOX.js.map} +0 -0
  126. /package/dist/react/{iconTool-LSW2DYGB.js.map → iconTool-46O5VEUX.js.map} +0 -0
  127. /package/dist/react/{imageTool-2DSR32RX.js.map → imageTool-P2RWZRHO.js.map} +0 -0
  128. /package/dist/react/{lineTool-N5I7IGT7.js.map → lineTool-ZMR27U5L.js.map} +0 -0
  129. /package/dist/react/{rectangleTool-YUT7ZK2E.js.map → rectangleTool-EJKK6UTX.js.map} +0 -0
  130. /package/dist/react/{textTool-JMTX7OVT.js.map → textTool-S4XXZHZH.js.map} +0 -0
@@ -0,0 +1,84 @@
1
+ /* eslint-disable */
2
+
3
+ import { multiSelection } from '../core/Selection.js';
4
+
5
+ const MAX_POINTS = 192;
6
+ const MIN_SAMPLE_PX = 4;
7
+ let points = [];
8
+ let preview = null;
9
+ let drawing = false;
10
+
11
+ function coords(e) {
12
+ const vb = svg.viewBox.baseVal;
13
+ const rect = svg.getBoundingClientRect();
14
+ return { x: vb.x + ((e.clientX - rect.left) / rect.width) * vb.width, y: vb.y + ((e.clientY - rect.top) / rect.height) * vb.height };
15
+ }
16
+
17
+ function pointInPolygon(point, polygon) {
18
+ let inside = false;
19
+ for (let i = 0, j = polygon.length - 1; i < polygon.length; j = i++) {
20
+ const a = polygon[i], b = polygon[j];
21
+ if (((a.y > point.y) !== (b.y > point.y)) && point.x < ((b.x - a.x) * (point.y - a.y)) / ((b.y - a.y) || Number.EPSILON) + a.x) inside = !inside;
22
+ }
23
+ return inside;
24
+ }
25
+
26
+ function boundsPoints(shape) {
27
+ if ((shape.shapeName === 'line' || shape.shapeName === 'arrow') && shape.startPoint && shape.endPoint) return [shape.startPoint, shape.endPoint];
28
+ if (shape.shapeName === 'freehandStroke' && Array.isArray(shape.points)) {
29
+ const step = Math.max(1, Math.ceil(shape.points.length / 16));
30
+ return shape.points.filter((_, i) => i % step === 0).map((p) => ({ x: p[0], y: p[1] }));
31
+ }
32
+ let x = shape.x || 0, y = shape.y || 0, width = shape.width || 0, height = shape.height || 0;
33
+ if (shape.shapeName === 'circle') { x = shape.x - shape.rx; y = shape.y - shape.ry; width = shape.rx * 2; height = shape.ry * 2; }
34
+ if ((!width || !height) && shape.group?.getBBox) {
35
+ try { const box = shape.group.getBBox(); x = box.x; y = box.y; width = box.width; height = box.height; } catch {}
36
+ }
37
+ const raw = [{ x, y }, { x: x + width, y }, { x: x + width, y: y + height }, { x, y: y + height }, { x: x + width / 2, y: y + height / 2 }];
38
+ const rotation = Number(shape.rotation) || 0;
39
+ if (!rotation) return raw;
40
+ const cx = x + width / 2, cy = y + height / 2, angle = rotation * Math.PI / 180;
41
+ return raw.map((p) => ({ x: cx + (p.x - cx) * Math.cos(angle) - (p.y - cy) * Math.sin(angle), y: cy + (p.x - cx) * Math.sin(angle) + (p.y - cy) * Math.cos(angle) }));
42
+ }
43
+
44
+ function cleanup() {
45
+ preview?.remove(); preview = null; points = []; drawing = false;
46
+ }
47
+
48
+ export function handleLassoDown(e) {
49
+ if (!window.isLassoToolActive) return;
50
+ cleanup(); multiSelection.clearSelection(); drawing = true; points = [coords(e)];
51
+ preview = document.createElementNS('http://www.w3.org/2000/svg', 'path');
52
+ preview.setAttribute('fill', 'rgba(139, 118, 214, .10)'); preview.setAttribute('stroke', '#a78bfa');
53
+ preview.setAttribute('stroke-dasharray', '5 4'); preview.setAttribute('vector-effect', 'non-scaling-stroke');
54
+ preview.style.pointerEvents = 'none'; svg.appendChild(preview);
55
+ }
56
+
57
+ export function handleLassoMove(e) {
58
+ if (!drawing) return;
59
+ const p = coords(e), last = points[points.length - 1];
60
+ const min = MIN_SAMPLE_PX / (window.currentZoom || 1);
61
+ if (Math.hypot(p.x - last.x, p.y - last.y) < min) return;
62
+ if (points.length < MAX_POINTS) points.push(p); else points[points.length - 1] = p;
63
+ preview?.setAttribute('d', `M ${points.map((v) => `${v.x} ${v.y}`).join(' L ')} Z`);
64
+ }
65
+
66
+ export function handleLassoUp() {
67
+ if (!drawing) return;
68
+ const polygon = points.slice(); cleanup();
69
+ // Switching tools clears selection state. Do that before applying the
70
+ // lasso result so the newly selected shapes survive the tool transition.
71
+ window.__sketchStoreApi?.setActiveTool('select', { afterDraw: true });
72
+ if (polygon.length >= 3 && Array.isArray(window.shapes)) {
73
+ const selected = window.shapes.filter((shape) => boundsPoints(shape).every((p) => pointInPolygon(p, polygon)));
74
+ const filtered = selected.filter((shape) => !shape.parentFrame || !selected.includes(shape.parentFrame));
75
+ // Apply the batch once; addShape() rebuilds controls per item and becomes
76
+ // unnecessarily quadratic for a large lasso selection.
77
+ filtered.forEach((shape) => { multiSelection.selectedShapes.add(shape); shape.isSelected = true; });
78
+ if (filtered.length) multiSelection.updateControls();
79
+ window.currentShape = filtered.length === 1 ? filtered[0] : null;
80
+ }
81
+ }
82
+
83
+ export function cancelLasso() { cleanup(); }
84
+ window.__cancelLasso = cancelLasso;
@@ -0,0 +1,50 @@
1
+ /* eslint-disable */
2
+ import { pushOptionsChangeAction } from '../core/UndoRedo.js';
3
+
4
+ const CLOSED_SHAPES = new Set(['rectangle', 'circle']);
5
+
6
+ window.paintBucketSettings = window.paintBucketSettings || {
7
+ fillColor: '#A98DEB',
8
+ fillStyle: 'solid',
9
+ };
10
+
11
+ function getSVGPoint(event) {
12
+ const viewBox = svg.viewBox.baseVal;
13
+ const rect = svg.getBoundingClientRect();
14
+ return {
15
+ x: viewBox.x + ((event.clientX - rect.left) / rect.width) * viewBox.width,
16
+ y: viewBox.y + ((event.clientY - rect.top) / rect.height) * viewBox.height,
17
+ };
18
+ }
19
+
20
+ function findClosedShapeAt(x, y) {
21
+ for (let index = shapes.length - 1; index >= 0; index -= 1) {
22
+ const shape = shapes[index];
23
+ if (!CLOSED_SHAPES.has(shape?.shapeName) || typeof shape.contains !== 'function') continue;
24
+ if (shape.contains(x, y)) return shape;
25
+ }
26
+ return null;
27
+ }
28
+
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;
39
+ const transparent = settings.fillStyle === 'none' || settings.fillStyle === 'transparent';
40
+ const nextFill = transparent ? 'transparent' : settings.fillColor;
41
+ const nextStyle = transparent ? 'none' : settings.fillStyle;
42
+ if (shape.options.fill === nextFill && shape.options.fillStyle === nextStyle) return;
43
+
44
+ const oldOptions = { ...shape.options };
45
+ const newOptions = { ...shape.options, fill: nextFill, fillStyle: nextStyle };
46
+ pushOptionsChangeAction(shape, oldOptions, newOptions);
47
+ shape.options = newOptions;
48
+ shape.draw();
49
+ window.dispatchEvent(new CustomEvent('lixsketch:bucket-filled', { detail: { shapeID: shape.shapeID } }));
50
+ }
@@ -0,0 +1,431 @@
1
+ /* eslint-disable */
2
+ import { pushCreateAction, pushFrameAttachmentAction } from '../core/UndoRedo.js';
3
+
4
+ const SVG_NS = 'http://www.w3.org/2000/svg';
5
+ const MAX_POINTS = 96;
6
+ const MIN_SCREEN_SAMPLE_PX = 4;
7
+ const MIN_SHAPE_SIZE = 8;
8
+
9
+ let drawing = false;
10
+ let points = [];
11
+ let previewPath = null;
12
+ let previewFrame = 0;
13
+ let latestPrediction = null;
14
+
15
+ function getThemeStroke() {
16
+ return document.body?.classList.contains('theme-dark') ? '#f7f4ff' : '#211a33';
17
+ }
18
+
19
+ function getSVGPoint(event) {
20
+ const viewBox = svg.viewBox.baseVal;
21
+ const rect = svg.getBoundingClientRect();
22
+ return {
23
+ x: viewBox.x + ((event.clientX - rect.left) / rect.width) * viewBox.width,
24
+ y: viewBox.y + ((event.clientY - rect.top) / rect.height) * viewBox.height,
25
+ pressure: event.pressure || 0.5,
26
+ };
27
+ }
28
+
29
+ function distance(a, b) {
30
+ return Math.hypot(b.x - a.x, b.y - a.y);
31
+ }
32
+
33
+ function orientedBounds(sample, angle) {
34
+ const cos = Math.cos(angle);
35
+ const sin = Math.sin(angle);
36
+ let minU = Infinity;
37
+ let maxU = -Infinity;
38
+ let minV = Infinity;
39
+ let maxV = -Infinity;
40
+ for (const point of sample) {
41
+ const u = point.x * cos + point.y * sin;
42
+ const v = -point.x * sin + point.y * cos;
43
+ minU = Math.min(minU, u);
44
+ maxU = Math.max(maxU, u);
45
+ minV = Math.min(minV, v);
46
+ maxV = Math.max(maxV, v);
47
+ }
48
+ const centerU = (minU + maxU) / 2;
49
+ const centerV = (minV + maxV) / 2;
50
+ return {
51
+ center: {
52
+ x: centerU * cos - centerV * sin,
53
+ y: centerU * sin + centerV * cos,
54
+ },
55
+ width: Math.max(MIN_SHAPE_SIZE, maxU - minU),
56
+ height: Math.max(MIN_SHAPE_SIZE, maxV - minV),
57
+ angle,
58
+ minU,
59
+ maxU,
60
+ minV,
61
+ maxV,
62
+ };
63
+ }
64
+
65
+ function principalAngle(sample) {
66
+ let meanX = 0;
67
+ let meanY = 0;
68
+ for (const point of sample) {
69
+ meanX += point.x;
70
+ meanY += point.y;
71
+ }
72
+ meanX /= sample.length;
73
+ meanY /= sample.length;
74
+ let xx = 0;
75
+ let yy = 0;
76
+ let xy = 0;
77
+ for (const point of sample) {
78
+ const dx = point.x - meanX;
79
+ const dy = point.y - meanY;
80
+ xx += dx * dx;
81
+ yy += dy * dy;
82
+ xy += dx * dy;
83
+ }
84
+ if (Math.abs(xx - yy) + Math.abs(xy) < 0.001) return 0;
85
+ return Math.atan2(2 * xy, xx - yy) / 2;
86
+ }
87
+
88
+ function rectangleAngle(sample) {
89
+ let x = 0;
90
+ let y = 0;
91
+ for (let index = 1; index < sample.length; index += 1) {
92
+ const dx = sample[index].x - sample[index - 1].x;
93
+ const dy = sample[index].y - sample[index - 1].y;
94
+ const length = Math.hypot(dx, dy);
95
+ if (length === 0) continue;
96
+ const angle = Math.atan2(dy, dx) * 4;
97
+ x += Math.cos(angle) * length;
98
+ y += Math.sin(angle) * length;
99
+ }
100
+ return Math.atan2(y, x) / 4;
101
+ }
102
+
103
+ function closedShapePrediction(sample, pathLength) {
104
+ const rectBounds = orientedBounds(sample, rectangleAngle(sample));
105
+ const ellipseBounds = orientedBounds(sample, principalAngle(sample));
106
+ let rectangleError = 0;
107
+ let ellipseError = 0;
108
+
109
+ const rectCos = Math.cos(rectBounds.angle);
110
+ const rectSin = Math.sin(rectBounds.angle);
111
+ const ellipseCos = Math.cos(ellipseBounds.angle);
112
+ const ellipseSin = Math.sin(ellipseBounds.angle);
113
+ const rectHalfW = rectBounds.width / 2;
114
+ const rectHalfH = rectBounds.height / 2;
115
+ const ellipseHalfW = ellipseBounds.width / 2;
116
+ const ellipseHalfH = ellipseBounds.height / 2;
117
+
118
+ for (const point of sample) {
119
+ const rectDx = point.x - rectBounds.center.x;
120
+ const rectDy = point.y - rectBounds.center.y;
121
+ const rectU = Math.abs(rectDx * rectCos + rectDy * rectSin) / rectHalfW;
122
+ const rectV = Math.abs(-rectDx * rectSin + rectDy * rectCos) / rectHalfH;
123
+ rectangleError += Math.min(Math.abs(1 - rectU), Math.abs(1 - rectV));
124
+
125
+ const ellipseDx = point.x - ellipseBounds.center.x;
126
+ const ellipseDy = point.y - ellipseBounds.center.y;
127
+ const ellipseU = (ellipseDx * ellipseCos + ellipseDy * ellipseSin) / ellipseHalfW;
128
+ const ellipseV = (-ellipseDx * ellipseSin + ellipseDy * ellipseCos) / ellipseHalfH;
129
+ ellipseError += Math.abs(1 - Math.hypot(ellipseU, ellipseV));
130
+ }
131
+
132
+ rectangleError /= sample.length;
133
+ ellipseError /= sample.length;
134
+ const rectangle = rectangleError <= ellipseError * 1.08;
135
+ const bestError = rectangle ? rectangleError : ellipseError;
136
+ if (bestError > 0.24) {
137
+ return { type: 'freehand', points: sample.map((point) => ({ ...point })), pathLength };
138
+ }
139
+ const bounds = rectangle ? rectBounds : ellipseBounds;
140
+ return {
141
+ type: rectangle ? 'rectangle' : 'circle',
142
+ ...bounds,
143
+ pathLength,
144
+ };
145
+ }
146
+
147
+ export function predictDrawnShape(sample) {
148
+ if (!Array.isArray(sample) || sample.length < 2) return null;
149
+ const start = sample[0];
150
+ let pathLength = 0;
151
+ let farthestIndex = 1;
152
+ let farthestDistance = 0;
153
+ let minX = start.x;
154
+ let maxX = start.x;
155
+ let minY = start.y;
156
+ let maxY = start.y;
157
+
158
+ for (let index = 1; index < sample.length; index += 1) {
159
+ pathLength += distance(sample[index - 1], sample[index]);
160
+ const fromStart = distance(start, sample[index]);
161
+ if (fromStart > farthestDistance) {
162
+ farthestDistance = fromStart;
163
+ farthestIndex = index;
164
+ }
165
+ minX = Math.min(minX, sample[index].x);
166
+ maxX = Math.max(maxX, sample[index].x);
167
+ minY = Math.min(minY, sample[index].y);
168
+ maxY = Math.max(maxY, sample[index].y);
169
+ }
170
+
171
+ const diagonal = Math.max(MIN_SHAPE_SIZE, Math.hypot(maxX - minX, maxY - minY));
172
+ const end = sample[sample.length - 1];
173
+ const closed = sample.length >= 8 && distance(start, end) <= diagonal * 0.28 && pathLength >= diagonal * 2.05;
174
+ if (closed) return closedShapePrediction(sample, pathLength);
175
+
176
+ const tip = sample[farthestIndex];
177
+ let afterTipLength = 0;
178
+ for (let index = farthestIndex + 1; index < sample.length; index += 1) {
179
+ afterTipLength += distance(sample[index - 1], sample[index]);
180
+ }
181
+ let shaftLength = 0;
182
+ for (let index = 1; index <= farthestIndex; index += 1) {
183
+ shaftLength += distance(sample[index - 1], sample[index]);
184
+ }
185
+ const arrow = farthestIndex >= Math.floor(sample.length * 0.45)
186
+ && farthestIndex < sample.length - 2
187
+ && afterTipLength >= diagonal * 0.18
188
+ && distance(end, tip) <= diagonal * 0.48
189
+ && farthestDistance / Math.max(shaftLength, 1) >= 0.86;
190
+
191
+ const directDistance = distance(start, end);
192
+ const line = !arrow && directDistance / Math.max(pathLength, 1) >= 0.92;
193
+
194
+ if (!arrow && !line) {
195
+ return { type: 'freehand', points: sample.map((point) => ({ ...point })), pathLength };
196
+ }
197
+
198
+ return {
199
+ type: arrow ? 'arrow' : 'line',
200
+ start: { ...start },
201
+ end: arrow ? { ...tip } : { ...end },
202
+ width: Math.max(MIN_SHAPE_SIZE, maxX - minX),
203
+ height: Math.max(MIN_SHAPE_SIZE, maxY - minY),
204
+ pathLength,
205
+ };
206
+ }
207
+
208
+ function rotatedPoint(center, x, y, angle) {
209
+ const cos = Math.cos(angle);
210
+ const sin = Math.sin(angle);
211
+ return { x: center.x + x * cos - y * sin, y: center.y + x * sin + y * cos };
212
+ }
213
+
214
+ function pathForPrediction(prediction) {
215
+ if (!prediction) return '';
216
+ if (prediction.type === 'rectangle') {
217
+ const halfW = prediction.width / 2;
218
+ const halfH = prediction.height / 2;
219
+ const corners = [
220
+ rotatedPoint(prediction.center, -halfW, -halfH, prediction.angle),
221
+ rotatedPoint(prediction.center, halfW, -halfH, prediction.angle),
222
+ rotatedPoint(prediction.center, halfW, halfH, prediction.angle),
223
+ rotatedPoint(prediction.center, -halfW, halfH, prediction.angle),
224
+ ];
225
+ return `M ${corners[0].x} ${corners[0].y} L ${corners[1].x} ${corners[1].y} L ${corners[2].x} ${corners[2].y} L ${corners[3].x} ${corners[3].y} Z`;
226
+ }
227
+ if (prediction.type === 'circle') {
228
+ const rx = prediction.width / 2;
229
+ const ry = prediction.height / 2;
230
+ const start = rotatedPoint(prediction.center, rx, 0, prediction.angle);
231
+ const opposite = rotatedPoint(prediction.center, -rx, 0, prediction.angle);
232
+ const rotation = prediction.angle * 180 / Math.PI;
233
+ return `M ${start.x} ${start.y} A ${rx} ${ry} ${rotation} 1 0 ${opposite.x} ${opposite.y} A ${rx} ${ry} ${rotation} 1 0 ${start.x} ${start.y}`;
234
+ }
235
+ if (prediction.type === 'freehand') {
236
+ if (!prediction.points.length) return '';
237
+ return prediction.points.reduce((path, point, index) => `${path}${index === 0 ? 'M' : ' L'} ${point.x} ${point.y}`, '');
238
+ }
239
+ const { start, end } = prediction;
240
+ let path = `M ${start.x} ${start.y} L ${end.x} ${end.y}`;
241
+ if (prediction.type === 'arrow') {
242
+ const angle = Math.atan2(end.y - start.y, end.x - start.x);
243
+ const head = Math.min(24 / (window.currentZoom || 1), distance(start, end) * 0.25);
244
+ const left = rotatedPoint(end, -head, head * 0.55, angle);
245
+ const right = rotatedPoint(end, -head, -head * 0.55, angle);
246
+ path += ` M ${left.x} ${left.y} L ${end.x} ${end.y} L ${right.x} ${right.y}`;
247
+ }
248
+ return path;
249
+ }
250
+
251
+ function ensurePreview() {
252
+ if (previewPath?.isConnected) return;
253
+ previewPath = document.createElementNS(SVG_NS, 'path');
254
+ previewPath.dataset.recognitionPreview = 'true';
255
+ previewPath.setAttribute('fill', 'rgba(148, 148, 158, 0.06)');
256
+ previewPath.setAttribute('stroke', '#9696a0');
257
+ previewPath.setAttribute('stroke-width', '2');
258
+ previewPath.setAttribute('stroke-dasharray', '5 7');
259
+ previewPath.setAttribute('stroke-linecap', 'round');
260
+ previewPath.setAttribute('stroke-linejoin', 'round');
261
+ previewPath.setAttribute('vector-effect', 'non-scaling-stroke');
262
+ previewPath.setAttribute('pointer-events', 'none');
263
+ svg.appendChild(previewPath);
264
+ }
265
+
266
+ function renderPreview() {
267
+ previewFrame = 0;
268
+ if (!drawing) return;
269
+ latestPrediction = predictDrawnShape(points);
270
+ ensurePreview();
271
+ previewPath.setAttribute('d', pathForPrediction(latestPrediction));
272
+ previewPath.setAttribute('fill', latestPrediction?.type === 'rectangle' || latestPrediction?.type === 'circle'
273
+ ? 'rgba(148, 148, 158, 0.06)'
274
+ : 'none');
275
+ }
276
+
277
+ function schedulePreview() {
278
+ if (!previewFrame) previewFrame = requestAnimationFrame(renderPreview);
279
+ }
280
+
281
+ function cleanupPreview() {
282
+ if (previewFrame) cancelAnimationFrame(previewFrame);
283
+ previewFrame = 0;
284
+ previewPath?.remove();
285
+ previewPath = null;
286
+ }
287
+
288
+ function currentStyle() {
289
+ const settings = window.freehandToolSettings || {};
290
+ const outline = settings.strokeStyle || 'solid';
291
+ return {
292
+ stroke: settings.strokeColor || getThemeStroke(),
293
+ fill: 'transparent',
294
+ fillStyle: 'none',
295
+ strokeWidth: settings.strokeWidth || 2,
296
+ strokeDasharray: outline === 'dashed' ? '10,10' : (outline === 'dotted' ? '2,8' : ''),
297
+ outline,
298
+ };
299
+ }
300
+
301
+ function attachToContainingFrame(shape) {
302
+ for (let index = shapes.length - 1; index >= 0; index -= 1) {
303
+ const frame = shapes[index];
304
+ if (frame.shapeName !== 'frame' || typeof frame.isShapeInFrame !== 'function') continue;
305
+ if (!frame.isShapeInFrame(shape)) continue;
306
+ frame.addShapeToFrame(shape);
307
+ pushFrameAttachmentAction(frame, shape, 'attach', null);
308
+ break;
309
+ }
310
+ }
311
+
312
+ function createPredictedShape(prediction) {
313
+ if (!prediction) return null;
314
+ const style = currentStyle();
315
+ let shape = null;
316
+ if (prediction.type === 'rectangle') {
317
+ shape = new window.Rectangle(
318
+ prediction.center.x - prediction.width / 2,
319
+ prediction.center.y - prediction.height / 2,
320
+ prediction.width,
321
+ prediction.height,
322
+ style,
323
+ );
324
+ shape.rotation = prediction.angle * 180 / Math.PI;
325
+ shape.draw();
326
+ } else if (prediction.type === 'circle') {
327
+ shape = new window.Circle(
328
+ prediction.center.x,
329
+ prediction.center.y,
330
+ prediction.width / 2,
331
+ prediction.height / 2,
332
+ style,
333
+ );
334
+ shape.rotation = prediction.angle * 180 / Math.PI;
335
+ shape.draw();
336
+ } else if (prediction.type === 'freehand') {
337
+ const brush = window.freehandToolSettings || {};
338
+ shape = new window.FreehandStroke(
339
+ prediction.points.map((point) => [point.x, point.y, point.pressure || 0.5]),
340
+ {
341
+ stroke: style.stroke,
342
+ strokeWidth: style.strokeWidth,
343
+ strokeStyle: style.outline,
344
+ thinning: brush.thinning ?? 0.5,
345
+ roughness: brush.roughness || 'smooth',
346
+ strokeOpacity: brush.opacity ?? 1,
347
+ },
348
+ );
349
+ } else if (prediction.type === 'arrow') {
350
+ shape = new window.Arrow(prediction.start, prediction.end, {
351
+ stroke: style.stroke,
352
+ strokeWidth: style.strokeWidth,
353
+ arrowOutlineStyle: style.outline,
354
+ arrowCurved: 'straight',
355
+ });
356
+ } else {
357
+ shape = new window.Line(prediction.start, prediction.end, {
358
+ stroke: style.stroke,
359
+ strokeWidth: style.strokeWidth,
360
+ strokeDasharray: style.strokeDasharray,
361
+ roughness: 1.5,
362
+ bowing: 1,
363
+ });
364
+ }
365
+ shapes.push(shape);
366
+ pushCreateAction(shape);
367
+ attachToContainingFrame(shape);
368
+ return shape;
369
+ }
370
+
371
+ export function handleShapeRecognitionDown(event) {
372
+ if (!window.isShapeRecognitionToolActive || event.button !== 0) return;
373
+ drawing = true;
374
+ points = [getSVGPoint(event)];
375
+ latestPrediction = null;
376
+ ensurePreview();
377
+ schedulePreview();
378
+ }
379
+
380
+ export function handleShapeRecognitionMove(event) {
381
+ if (!drawing || !window.isShapeRecognitionToolActive) return;
382
+ const point = getSVGPoint(event);
383
+ const last = points[points.length - 1];
384
+ const threshold = MIN_SCREEN_SAMPLE_PX / (window.currentZoom || 1);
385
+ if (distance(last, point) < threshold) return;
386
+ if (points.length < MAX_POINTS) {
387
+ points.push(point);
388
+ } else {
389
+ // Preserve the first point and evenly discard older detail. Memory and
390
+ // recognition time stay constant even during very long gestures.
391
+ points = [points[0], ...points.slice(2).filter((_, index) => index % 2 === 0), point];
392
+ }
393
+ schedulePreview();
394
+ }
395
+
396
+ export function handleShapeRecognitionUp(event) {
397
+ if (!drawing) return;
398
+ drawing = false;
399
+ const rect = svg.getBoundingClientRect();
400
+ if (event.clientX >= rect.left && event.clientX <= rect.right && event.clientY >= rect.top && event.clientY <= rect.bottom) {
401
+ const point = getSVGPoint(event);
402
+ if (distance(points[points.length - 1], point) > 0) points.push(point);
403
+ }
404
+ latestPrediction = predictDrawnShape(points);
405
+ cleanupPreview();
406
+
407
+ const prediction = latestPrediction;
408
+ points = [];
409
+ latestPrediction = null;
410
+ if (!prediction || prediction.pathLength < MIN_SHAPE_SIZE) return;
411
+
412
+ const shape = createPredictedShape(prediction);
413
+ if (!shape) return;
414
+ if (window.__sketchStoreApi) window.__sketchStoreApi.setActiveTool('select', { afterDraw: true });
415
+ currentShape = shape;
416
+ if (shape.shapeName === 'freehandStroke' && typeof shape.selectStroke === 'function') {
417
+ shape.selectStroke();
418
+ } else {
419
+ shape.isSelected = true;
420
+ if (typeof shape.addAnchors === 'function') shape.addAnchors();
421
+ }
422
+ }
423
+
424
+ export function cancelShapeRecognition() {
425
+ drawing = false;
426
+ points = [];
427
+ latestPrediction = null;
428
+ cleanupPreview();
429
+ }
430
+
431
+ window.__cancelShapeRecognition = cancelShapeRecognition;
@@ -0,0 +1,61 @@
1
+ /* eslint-disable */
2
+
3
+ import { Frame } from '../shapes/Frame.js';
4
+ import { pushCreateAction } from '../core/UndoRedo.js';
5
+ import { normalizeWebEmbedUrl } from '../core/WebEmbedPolicy.js';
6
+
7
+ let frame = null, start = null;
8
+ function coords(e) {
9
+ const vb = svg.viewBox.baseVal, rect = svg.getBoundingClientRect();
10
+ return { x: vb.x + ((e.clientX - rect.left) / rect.width) * vb.width, y: vb.y + ((e.clientY - rect.top) / rect.height) * vb.height };
11
+ }
12
+
13
+ export function placeWebEmbed(urlValue) {
14
+ const url = normalizeWebEmbedUrl(urlValue);
15
+ if (!url || typeof svg === 'undefined') return null;
16
+
17
+ // Complete the tool transition first because setActiveTool clears the
18
+ // previous selection. The newly inserted frame should remain selected.
19
+ window.__sketchStoreApi?.setActiveTool('select');
20
+ const viewBox = svg.viewBox.baseVal;
21
+ const width = Math.min(640, Math.max(320, viewBox.width * 0.56));
22
+ const height = width * 9 / 16;
23
+ const x = viewBox.x + (viewBox.width - width) / 2;
24
+ const y = viewBox.y + (viewBox.height - height) / 2;
25
+ const placedFrame = new Frame(x, y, width, height, {
26
+ frameName: 'Web embed', frameType: 'web-embed', webEmbedURL: url,
27
+ stroke: '#8b76d6', strokeWidth: 2,
28
+ });
29
+ shapes.push(placedFrame);
30
+ pushCreateAction(placedFrame);
31
+ currentShape = placedFrame;
32
+ placedFrame.selectFrame();
33
+ window.__pendingWebEmbedURL = null;
34
+ return placedFrame;
35
+ }
36
+
37
+ export function handleWebEmbedDown(e) {
38
+ const url = normalizeWebEmbedUrl(window.__pendingWebEmbedURL);
39
+ if (!window.isWebEmbedToolActive || !url) { window.__showWebEmbedModal?.(); return; }
40
+ start = coords(e);
41
+ frame = new Frame(start.x, start.y, 0, 0, { frameName: 'Web embed', frameType: 'web-embed', webEmbedURL: url, stroke: '#8b76d6', strokeWidth: 2 });
42
+ shapes.push(frame); currentShape = frame;
43
+ }
44
+
45
+ export function handleWebEmbedMove(e) {
46
+ if (!frame || !start) return;
47
+ const p = coords(e); frame.x = Math.min(start.x, p.x); frame.y = Math.min(start.y, p.y);
48
+ frame.width = Math.abs(p.x - start.x); frame.height = Math.abs(p.y - start.y); frame.draw();
49
+ }
50
+
51
+ export function handleWebEmbedUp() {
52
+ if (!frame) return;
53
+ if (frame.width < 40 || frame.height < 30) { frame.width = 560; frame.height = 315; }
54
+ frame.draw(); pushCreateAction(frame);
55
+ const placedFrame = frame;
56
+ window.__pendingWebEmbedURL = null; frame = null; start = null;
57
+ window.__sketchStoreApi?.setActiveTool('select', { afterDraw: true });
58
+ currentShape = placedFrame; placedFrame.selectFrame();
59
+ }
60
+
61
+ window.__placeWebEmbed = placeWebEmbed;