@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.
- package/dist/mcp/index.js +5 -1
- package/dist/mcp/index.js.map +2 -2
- package/dist/mcp/stdio.js +5 -1
- package/dist/mcp/stdio.js.map +2 -2
- package/dist/react/{AIRenderer-SRF7LU5K.js → AIRenderer-R5M2CIDO.js} +13 -6
- package/dist/react/AIRenderer-R5M2CIDO.js.map +7 -0
- package/dist/react/Circle-HMM5PKJ2.js +10 -0
- package/dist/react/{CopyPaste-YUDZ6DHY.js → CopyPaste-P6G35CXO.js} +11 -10
- package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-P6G35CXO.js.map} +1 -1
- package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-M5B4EWHJ.js} +216 -63
- package/dist/react/EventDispatcher-M5B4EWHJ.js.map +7 -0
- package/dist/react/Frame-QEUEYITT.js +11 -0
- package/dist/react/{FreehandStroke-U6GYZOM4.js → FreehandStroke-WXIJKD5I.js} +2 -2
- package/dist/react/{GraphEngine-MM6ALA2N.js → GraphEngine-XH3RPYO4.js} +9 -2
- package/dist/react/GraphEngine-XH3RPYO4.js.map +7 -0
- package/dist/react/IconShape-NHJE4YBY.js +10 -0
- package/dist/react/ImageShape-33VCMSDP.js +10 -0
- package/dist/react/Line-A6FE232Q.js +10 -0
- package/dist/react/{LixScriptParser-JYWJX375.js → LixScriptParser-YS3ZOTGS.js} +6 -2
- package/dist/react/LixScriptParser-YS3ZOTGS.js.map +7 -0
- package/dist/react/{MermaidFlowchartRenderer-PQEC4TMI.js → MermaidFlowchartRenderer-EL3Q2MNW.js} +6 -2
- package/dist/react/MermaidFlowchartRenderer-EL3Q2MNW.js.map +7 -0
- package/dist/react/MermaidSequenceParser-DBYXW23P.js +8 -0
- package/dist/react/{MermaidSequenceRenderer-KCUHO7UI.js → MermaidSequenceRenderer-K4KHHEVC.js} +10 -3
- package/dist/react/MermaidSequenceRenderer-K4KHHEVC.js.map +7 -0
- package/dist/react/{MermaidStructuredRenderer-W7UKN7BM.js → MermaidStructuredRenderer-6LN3WISP.js} +7 -2
- package/dist/react/MermaidStructuredRenderer-6LN3WISP.js.map +7 -0
- package/dist/react/Rectangle-4U4CD2ER.js +10 -0
- package/dist/react/{SceneSerializer-SG6SJN5X.js → SceneSerializer-JEYTYXZQ.js} +120 -46
- package/dist/react/SceneSerializer-JEYTYXZQ.js.map +7 -0
- package/dist/react/{Selection-2TSH3J6P.js → Selection-23MOLHEI.js} +4 -4
- package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-OPJON7TD.js} +26 -26
- package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-QVNRI5OB.js} +8 -2
- package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-VH3PJPDM.js} +3 -3
- package/dist/react/{chunk-LSHZYN77.js → chunk-2DZHO67H.js} +2 -2
- package/dist/react/{chunk-XOWNCYE4.js → chunk-5NHJJJBZ.js} +3 -3
- package/dist/react/chunk-6AIFXYV6.js +31 -0
- package/dist/react/chunk-6AIFXYV6.js.map +7 -0
- package/dist/react/{chunk-K5RY4EV7.js → chunk-6GPWSPJF.js} +9 -5
- package/dist/react/{chunk-K5RY4EV7.js.map → chunk-6GPWSPJF.js.map} +2 -2
- package/dist/react/{chunk-ASBC5G35.js → chunk-B6CYUZC4.js} +2 -2
- package/dist/react/{chunk-OP56XPWF.js → chunk-DNFPTVIP.js} +2 -2
- package/dist/react/{chunk-XYUFEIZR.js → chunk-DWEFPKDO.js} +2 -2
- package/dist/react/{chunk-GQUZPON4.js → chunk-F56XUE5S.js} +4 -4
- package/dist/react/{chunk-7CM5XQDN.js → chunk-G3DJQY65.js} +20 -36
- package/dist/react/chunk-G3DJQY65.js.map +7 -0
- package/dist/react/{chunk-LJR7EVW2.js → chunk-JEGLUNBC.js} +3 -3
- package/dist/react/{chunk-6GPEWDRB.js → chunk-M7GAB24F.js} +52 -6
- package/dist/react/chunk-M7GAB24F.js.map +7 -0
- package/dist/react/{chunk-VQVEPJSA.js → chunk-MHV5P6TS.js} +2 -1
- package/dist/react/chunk-MHV5P6TS.js.map +7 -0
- package/dist/react/{chunk-PXFPPO3R.js → chunk-NH3NVSWT.js} +18 -2
- package/dist/react/chunk-NH3NVSWT.js.map +7 -0
- package/dist/react/{chunk-XFL4R5ZF.js → chunk-OK4ROGMM.js} +16 -35
- package/dist/react/chunk-OK4ROGMM.js.map +7 -0
- package/dist/react/{chunk-APTOPAOY.js → chunk-SSFEN5CY.js} +3 -3
- package/dist/react/{chunk-RGMDWLRJ.js → chunk-UEL2G6D2.js} +3 -3
- package/dist/react/{chunk-M6TMN76K.js → chunk-ULPIHUVC.js} +3 -3
- package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
- package/dist/react/chunk-VZPH7OAM.js.map +7 -0
- package/dist/react/{chunk-TKXKJNTX.js → chunk-W7XUY4GR.js} +16 -19
- package/dist/react/chunk-W7XUY4GR.js.map +7 -0
- package/dist/react/{chunk-EOURRJ55.js → chunk-XSE4HQ2D.js} +2 -2
- package/dist/react/{chunk-K5BG5MHY.js → chunk-ZNZAICWY.js} +36 -60
- package/dist/react/chunk-ZNZAICWY.js.map +7 -0
- package/dist/react/{circleTool-LDKGZALF.js → circleTool-2UODOBCV.js} +4 -4
- package/dist/react/{codeTool-4YZO5W7J.js → codeTool-F4RV7SAI.js} +4 -4
- package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-NTGA5BI6.js} +4 -4
- package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-L55EUB5C.js} +4 -4
- package/dist/react/{iconTool-46O5VEUX.js → iconTool-KQQBLGR3.js} +4 -4
- package/dist/react/{imageTool-P2RWZRHO.js → imageTool-LDJ45AD7.js} +4 -4
- package/dist/react/index.js +148 -37
- package/dist/react/index.js.map +4 -4
- package/dist/react/{lineTool-ZMR27U5L.js → lineTool-CSAFD7PW.js} +4 -4
- package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-YPD2SIIZ.js} +4 -4
- package/dist/react/{textTool-S4XXZHZH.js → textTool-VQHMWEFJ.js} +5 -5
- package/package.json +5 -2
- package/src/core/AIRenderer.js +8 -2
- package/src/core/CanvasSpace.js +28 -0
- package/src/core/GraphRenderer.js +8 -3
- package/src/core/LixScriptParser.js +7 -1
- package/src/core/MermaidFlowchartRenderer.js +5 -1
- package/src/core/MermaidSequenceParser.js +1 -0
- package/src/core/MermaidSequenceRenderer.js +8 -3
- package/src/core/MermaidStructuredRenderer.js +6 -1
- package/src/core/SceneSerializer.js +121 -38
- package/src/core/Selection.js +12 -2
- package/src/core/UndoRedo.js +59 -5
- package/src/react/components/AppMenu.jsx +20 -19
- package/src/react/components/canvas/CanvasRulers.jsx +8 -13
- package/src/react/components/canvas/SVGCanvas.jsx +2 -1
- package/src/react/components/canvas/rulerUnits.js +28 -0
- package/src/react/components/sidebars/PaintBucketSidebar.jsx +1 -1
- package/src/react/store/useSketchStore.js +24 -3
- package/src/react/store/useUIStore.js +24 -1
- package/src/react/utils/canvasBackgrounds.js +29 -0
- package/src/shapes/Circle.js +12 -19
- package/src/shapes/Frame.js +38 -69
- package/src/shapes/FreehandStroke.js +24 -1
- package/src/shapes/Rectangle.js +10 -22
- package/src/tools/circleTool.js +3 -27
- package/src/tools/paintBucketTool.js +72 -15
- package/src/tools/rectangleTool.js +7 -23
- package/src/tools/shapeRecognition.js +270 -0
- package/src/tools/shapeRecognitionTool.js +36 -183
- package/dist/react/AIRenderer-SRF7LU5K.js.map +0 -7
- package/dist/react/Circle-6POZCUZD.js +0 -9
- package/dist/react/EventDispatcher-7LAK7GQ7.js.map +0 -7
- package/dist/react/Frame-7D2MXCFF.js +0 -11
- package/dist/react/GraphEngine-MM6ALA2N.js.map +0 -7
- package/dist/react/IconShape-ZADX7Y33.js +0 -10
- package/dist/react/ImageShape-UJQN2HI7.js +0 -10
- package/dist/react/Line-APEULMHE.js +0 -10
- package/dist/react/LixScriptParser-JYWJX375.js.map +0 -7
- package/dist/react/MermaidFlowchartRenderer-PQEC4TMI.js.map +0 -7
- package/dist/react/MermaidSequenceParser-UKG2NSZY.js +0 -8
- package/dist/react/MermaidSequenceRenderer-KCUHO7UI.js.map +0 -7
- package/dist/react/MermaidStructuredRenderer-W7UKN7BM.js.map +0 -7
- package/dist/react/Rectangle-S7XUKWYD.js +0 -9
- package/dist/react/SceneSerializer-SG6SJN5X.js.map +0 -7
- package/dist/react/chunk-6GPEWDRB.js.map +0 -7
- package/dist/react/chunk-7CM5XQDN.js.map +0 -7
- package/dist/react/chunk-FETTJLCV.js.map +0 -7
- package/dist/react/chunk-K5BG5MHY.js.map +0 -7
- package/dist/react/chunk-PXFPPO3R.js.map +0 -7
- package/dist/react/chunk-TKXKJNTX.js.map +0 -7
- package/dist/react/chunk-VQVEPJSA.js.map +0 -7
- package/dist/react/chunk-XFL4R5ZF.js.map +0 -7
- /package/dist/react/{Circle-6POZCUZD.js.map → Circle-HMM5PKJ2.js.map} +0 -0
- /package/dist/react/{Frame-7D2MXCFF.js.map → Frame-QEUEYITT.js.map} +0 -0
- /package/dist/react/{FreehandStroke-U6GYZOM4.js.map → FreehandStroke-WXIJKD5I.js.map} +0 -0
- /package/dist/react/{IconShape-ZADX7Y33.js.map → IconShape-NHJE4YBY.js.map} +0 -0
- /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-33VCMSDP.js.map} +0 -0
- /package/dist/react/{Line-APEULMHE.js.map → Line-A6FE232Q.js.map} +0 -0
- /package/dist/react/{MermaidSequenceParser-UKG2NSZY.js.map → MermaidSequenceParser-DBYXW23P.js.map} +0 -0
- /package/dist/react/{Rectangle-S7XUKWYD.js.map → Rectangle-4U4CD2ER.js.map} +0 -0
- /package/dist/react/{Selection-2TSH3J6P.js.map → Selection-23MOLHEI.js.map} +0 -0
- /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-OPJON7TD.js.map} +0 -0
- /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-QVNRI5OB.js.map} +0 -0
- /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-VH3PJPDM.js.map} +0 -0
- /package/dist/react/{chunk-LSHZYN77.js.map → chunk-2DZHO67H.js.map} +0 -0
- /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-5NHJJJBZ.js.map} +0 -0
- /package/dist/react/{chunk-ASBC5G35.js.map → chunk-B6CYUZC4.js.map} +0 -0
- /package/dist/react/{chunk-OP56XPWF.js.map → chunk-DNFPTVIP.js.map} +0 -0
- /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-DWEFPKDO.js.map} +0 -0
- /package/dist/react/{chunk-GQUZPON4.js.map → chunk-F56XUE5S.js.map} +0 -0
- /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-JEGLUNBC.js.map} +0 -0
- /package/dist/react/{chunk-APTOPAOY.js.map → chunk-SSFEN5CY.js.map} +0 -0
- /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-UEL2G6D2.js.map} +0 -0
- /package/dist/react/{chunk-M6TMN76K.js.map → chunk-ULPIHUVC.js.map} +0 -0
- /package/dist/react/{chunk-EOURRJ55.js.map → chunk-XSE4HQ2D.js.map} +0 -0
- /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-2UODOBCV.js.map} +0 -0
- /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-F4RV7SAI.js.map} +0 -0
- /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-NTGA5BI6.js.map} +0 -0
- /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-L55EUB5C.js.map} +0 -0
- /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-KQQBLGR3.js.map} +0 -0
- /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-LDJ45AD7.js.map} +0 -0
- /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-CSAFD7PW.js.map} +0 -0
- /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-YPD2SIIZ.js.map} +0 -0
- /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-VQHMWEFJ.js.map} +0 -0
package/src/tools/circleTool.js
CHANGED
|
@@ -124,20 +124,8 @@ const handleMouseDown = (e) => {
|
|
|
124
124
|
else if(anchorInfo.type === 'rotate')
|
|
125
125
|
{
|
|
126
126
|
isRotatingShapeCircle = true;
|
|
127
|
-
|
|
128
|
-
|
|
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
|
|
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
|
|
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
|
|
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 (
|
|
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
|
|
30
|
-
if (!
|
|
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
|
-
|
|
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
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
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
|
|
282
|
-
|
|
283
|
-
|
|
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
|
+
}
|