@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.
- 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-PPDS3AD6.js} +11 -10
- package/dist/react/{CopyPaste-YUDZ6DHY.js.map → CopyPaste-PPDS3AD6.js.map} +1 -1
- package/dist/react/{EventDispatcher-7LAK7GQ7.js → EventDispatcher-UV7LFSAS.js} +216 -63
- package/dist/react/EventDispatcher-UV7LFSAS.js.map +7 -0
- package/dist/react/Frame-DELCMYV6.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-PZHMZSLR.js +10 -0
- package/dist/react/ImageShape-ZMBZARRI.js +10 -0
- package/dist/react/Line-VPVPL6ET.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-H6KMQCO2.js} +120 -46
- package/dist/react/SceneSerializer-H6KMQCO2.js.map +7 -0
- package/dist/react/{Selection-2TSH3J6P.js → Selection-CFWRGVKH.js} +4 -4
- package/dist/react/{SketchEngine-UD2XV2KT.js → SketchEngine-UFLZJQXS.js} +26 -26
- package/dist/react/{UndoRedo-IN4SXDG6.js → UndoRedo-RXO7YHA2.js} +12 -2
- package/dist/react/{arrowTool-OYDWJGQK.js → arrowTool-DO327FKU.js} +3 -3
- package/dist/react/{chunk-EOURRJ55.js → chunk-2ALS5JGS.js} +2 -2
- package/dist/react/{chunk-LJR7EVW2.js → chunk-37PF7FG2.js} +3 -3
- package/dist/react/chunk-6AIFXYV6.js +31 -0
- package/dist/react/chunk-6AIFXYV6.js.map +7 -0
- package/dist/react/{chunk-XOWNCYE4.js → chunk-6LWUR7XB.js} +3 -3
- package/dist/react/{chunk-ASBC5G35.js → chunk-7DD2JRBC.js} +2 -2
- package/dist/react/{chunk-OP56XPWF.js → chunk-BOZEDUAF.js} +2 -2
- package/dist/react/{chunk-RGMDWLRJ.js → chunk-CGMUXYPJ.js} +3 -3
- package/dist/react/{chunk-M6TMN76K.js → chunk-DODPOWNX.js} +3 -3
- package/dist/react/{chunk-LSHZYN77.js → chunk-GQLXUZHQ.js} +2 -2
- package/dist/react/{chunk-7CM5XQDN.js → chunk-I7TVGJFF.js} +20 -36
- package/dist/react/chunk-I7TVGJFF.js.map +7 -0
- package/dist/react/{chunk-K5RY4EV7.js → chunk-KC26KJW4.js} +26 -17
- package/dist/react/chunk-KC26KJW4.js.map +7 -0
- package/dist/react/{chunk-XFL4R5ZF.js → chunk-KZBKYHZO.js} +16 -35
- package/dist/react/chunk-KZBKYHZO.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-APTOPAOY.js → chunk-O7KXLFZN.js} +3 -3
- package/dist/react/{chunk-GQUZPON4.js → chunk-RK3NVAER.js} +4 -4
- package/dist/react/{chunk-FETTJLCV.js → chunk-VZPH7OAM.js} +14 -22
- package/dist/react/chunk-VZPH7OAM.js.map +7 -0
- package/dist/react/{chunk-6GPEWDRB.js → chunk-VZX55VQL.js} +115 -6
- package/dist/react/chunk-VZX55VQL.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-XYUFEIZR.js → chunk-X5ENBMGL.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-NK4RVDAR.js} +4 -4
- package/dist/react/{codeTool-4YZO5W7J.js → codeTool-3WF4HEOW.js} +4 -4
- package/dist/react/{frameTool-MXNJEXQQ.js → frameTool-VCNV2NG2.js} +4 -4
- package/dist/react/{freehandTool-26D3FOOX.js → freehandTool-TBAOD46K.js} +4 -4
- package/dist/react/{iconTool-46O5VEUX.js → iconTool-65JCAKYF.js} +4 -4
- package/dist/react/{imageTool-P2RWZRHO.js → imageTool-UQZUBOK2.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-762KBYOM.js} +4 -4
- package/dist/react/{rectangleTool-EJKK6UTX.js → rectangleTool-IQK3HZK6.js} +4 -4
- package/dist/react/{textTool-S4XXZHZH.js → textTool-ITG2SBJK.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 +29 -14
- package/src/core/UndoRedo.js +126 -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-K5RY4EV7.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-DELCMYV6.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-PZHMZSLR.js.map} +0 -0
- /package/dist/react/{ImageShape-UJQN2HI7.js.map → ImageShape-ZMBZARRI.js.map} +0 -0
- /package/dist/react/{Line-APEULMHE.js.map → Line-VPVPL6ET.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-CFWRGVKH.js.map} +0 -0
- /package/dist/react/{SketchEngine-UD2XV2KT.js.map → SketchEngine-UFLZJQXS.js.map} +0 -0
- /package/dist/react/{UndoRedo-IN4SXDG6.js.map → UndoRedo-RXO7YHA2.js.map} +0 -0
- /package/dist/react/{arrowTool-OYDWJGQK.js.map → arrowTool-DO327FKU.js.map} +0 -0
- /package/dist/react/{chunk-EOURRJ55.js.map → chunk-2ALS5JGS.js.map} +0 -0
- /package/dist/react/{chunk-LJR7EVW2.js.map → chunk-37PF7FG2.js.map} +0 -0
- /package/dist/react/{chunk-XOWNCYE4.js.map → chunk-6LWUR7XB.js.map} +0 -0
- /package/dist/react/{chunk-ASBC5G35.js.map → chunk-7DD2JRBC.js.map} +0 -0
- /package/dist/react/{chunk-OP56XPWF.js.map → chunk-BOZEDUAF.js.map} +0 -0
- /package/dist/react/{chunk-RGMDWLRJ.js.map → chunk-CGMUXYPJ.js.map} +0 -0
- /package/dist/react/{chunk-M6TMN76K.js.map → chunk-DODPOWNX.js.map} +0 -0
- /package/dist/react/{chunk-LSHZYN77.js.map → chunk-GQLXUZHQ.js.map} +0 -0
- /package/dist/react/{chunk-APTOPAOY.js.map → chunk-O7KXLFZN.js.map} +0 -0
- /package/dist/react/{chunk-GQUZPON4.js.map → chunk-RK3NVAER.js.map} +0 -0
- /package/dist/react/{chunk-XYUFEIZR.js.map → chunk-X5ENBMGL.js.map} +0 -0
- /package/dist/react/{circleTool-LDKGZALF.js.map → circleTool-NK4RVDAR.js.map} +0 -0
- /package/dist/react/{codeTool-4YZO5W7J.js.map → codeTool-3WF4HEOW.js.map} +0 -0
- /package/dist/react/{frameTool-MXNJEXQQ.js.map → frameTool-VCNV2NG2.js.map} +0 -0
- /package/dist/react/{freehandTool-26D3FOOX.js.map → freehandTool-TBAOD46K.js.map} +0 -0
- /package/dist/react/{iconTool-46O5VEUX.js.map → iconTool-65JCAKYF.js.map} +0 -0
- /package/dist/react/{imageTool-P2RWZRHO.js.map → imageTool-UQZUBOK2.js.map} +0 -0
- /package/dist/react/{lineTool-ZMR27U5L.js.map → lineTool-762KBYOM.js.map} +0 -0
- /package/dist/react/{rectangleTool-EJKK6UTX.js.map → rectangleTool-IQK3HZK6.js.map} +0 -0
- /package/dist/react/{textTool-S4XXZHZH.js.map → textTool-ITG2SBJK.js.map} +0 -0
|
@@ -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
|
+
}
|
|
@@ -1,10 +1,19 @@
|
|
|
1
1
|
/* eslint-disable */
|
|
2
2
|
import { pushCreateAction, pushFrameAttachmentAction } from '../core/UndoRedo.js';
|
|
3
|
+
import {
|
|
4
|
+
MAX_GESTURE_POINTS,
|
|
5
|
+
distance,
|
|
6
|
+
predictDrawnShape,
|
|
7
|
+
} from './shapeRecognition.js';
|
|
8
|
+
|
|
9
|
+
export {
|
|
10
|
+
MAX_GESTURE_POINTS,
|
|
11
|
+
capGestureSample,
|
|
12
|
+
predictDrawnShape,
|
|
13
|
+
} from './shapeRecognition.js';
|
|
3
14
|
|
|
4
15
|
const SVG_NS = 'http://www.w3.org/2000/svg';
|
|
5
|
-
const MAX_POINTS = 96;
|
|
6
16
|
const MIN_SCREEN_SAMPLE_PX = 4;
|
|
7
|
-
const MIN_SHAPE_SIZE = 8;
|
|
8
17
|
|
|
9
18
|
let drawing = false;
|
|
10
19
|
let points = [];
|
|
@@ -26,185 +35,6 @@ function getSVGPoint(event) {
|
|
|
26
35
|
};
|
|
27
36
|
}
|
|
28
37
|
|
|
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
38
|
function rotatedPoint(center, x, y, angle) {
|
|
209
39
|
const cos = Math.cos(angle);
|
|
210
40
|
const sin = Math.sin(angle);
|
|
@@ -232,6 +62,12 @@ function pathForPrediction(prediction) {
|
|
|
232
62
|
const rotation = prediction.angle * 180 / Math.PI;
|
|
233
63
|
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
64
|
}
|
|
65
|
+
if (prediction.type === 'triangle' || prediction.type === 'diamond') {
|
|
66
|
+
return prediction.vertices.reduce(
|
|
67
|
+
(path, point, index) => `${path}${index === 0 ? 'M' : ' L'} ${point.x} ${point.y}`,
|
|
68
|
+
'',
|
|
69
|
+
) + ' Z';
|
|
70
|
+
}
|
|
235
71
|
if (prediction.type === 'freehand') {
|
|
236
72
|
if (!prediction.points.length) return '';
|
|
237
73
|
return prediction.points.reduce((path, point, index) => `${path}${index === 0 ? 'M' : ' L'} ${point.x} ${point.y}`, '');
|
|
@@ -269,7 +105,7 @@ function renderPreview() {
|
|
|
269
105
|
latestPrediction = predictDrawnShape(points);
|
|
270
106
|
ensurePreview();
|
|
271
107
|
previewPath.setAttribute('d', pathForPrediction(latestPrediction));
|
|
272
|
-
previewPath.setAttribute('fill',
|
|
108
|
+
previewPath.setAttribute('fill', ['rectangle', 'circle', 'triangle', 'diamond'].includes(latestPrediction?.type)
|
|
273
109
|
? 'rgba(148, 148, 158, 0.06)'
|
|
274
110
|
: 'none');
|
|
275
111
|
}
|
|
@@ -333,6 +169,23 @@ function createPredictedShape(prediction) {
|
|
|
333
169
|
);
|
|
334
170
|
shape.rotation = prediction.angle * 180 / Math.PI;
|
|
335
171
|
shape.draw();
|
|
172
|
+
} else if (prediction.type === 'triangle' || prediction.type === 'diamond') {
|
|
173
|
+
const polygonPoints = [...prediction.vertices, prediction.vertices[0]];
|
|
174
|
+
shape = new window.FreehandStroke(
|
|
175
|
+
polygonPoints.map((point) => [point.x, point.y, 0.5]),
|
|
176
|
+
{
|
|
177
|
+
stroke: 'transparent',
|
|
178
|
+
fill: 'transparent',
|
|
179
|
+
fillStyle: 'none',
|
|
180
|
+
outlineStroke: style.stroke,
|
|
181
|
+
outlineWidth: style.strokeWidth,
|
|
182
|
+
closedFill: true,
|
|
183
|
+
strokeWidth: style.strokeWidth,
|
|
184
|
+
strokeStyle: style.outline,
|
|
185
|
+
roughness: 0,
|
|
186
|
+
},
|
|
187
|
+
);
|
|
188
|
+
shape.recognizedShapeType = prediction.type;
|
|
336
189
|
} else if (prediction.type === 'freehand') {
|
|
337
190
|
const brush = window.freehandToolSettings || {};
|
|
338
191
|
shape = new window.FreehandStroke(
|
|
@@ -383,7 +236,7 @@ export function handleShapeRecognitionMove(event) {
|
|
|
383
236
|
const last = points[points.length - 1];
|
|
384
237
|
const threshold = MIN_SCREEN_SAMPLE_PX / (window.currentZoom || 1);
|
|
385
238
|
if (distance(last, point) < threshold) return;
|
|
386
|
-
if (points.length <
|
|
239
|
+
if (points.length < MAX_GESTURE_POINTS) {
|
|
387
240
|
points.push(point);
|
|
388
241
|
} else {
|
|
389
242
|
// Preserve the first point and evenly discard older detail. Memory and
|