@widgetic/canvas 0.5.4

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 (51) hide show
  1. package/README.md +45 -0
  2. package/dist/canvas/Canvas.svelte +10678 -0
  3. package/dist/canvas/Canvas.svelte.d.ts +147 -0
  4. package/dist/canvas/CanvasToolbar.svelte +1422 -0
  5. package/dist/canvas/CanvasToolbar.svelte.d.ts +54 -0
  6. package/dist/canvas/ContextMenu.svelte +279 -0
  7. package/dist/canvas/ContextMenu.svelte.d.ts +36 -0
  8. package/dist/canvas/PanZoomPanel.svelte +315 -0
  9. package/dist/canvas/PanZoomPanel.svelte.d.ts +32 -0
  10. package/dist/canvas/canvasLogger.d.ts +5 -0
  11. package/dist/canvas/canvasLogger.js +19 -0
  12. package/dist/canvas/index.d.ts +13 -0
  13. package/dist/canvas/index.js +12 -0
  14. package/dist/canvas/props-panel/PropsPanel.svelte +1902 -0
  15. package/dist/canvas/props-panel/PropsPanel.svelte.d.ts +73 -0
  16. package/dist/canvas/props-panel/TextPropsICSection.svelte +238 -0
  17. package/dist/canvas/props-panel/TextPropsICSection.svelte.d.ts +36 -0
  18. package/dist/canvas/shapes/FrameShape.d.ts +97 -0
  19. package/dist/canvas/shapes/FrameShape.js +951 -0
  20. package/dist/canvas/shapes/ImageShape.d.ts +38 -0
  21. package/dist/canvas/shapes/ImageShape.js +245 -0
  22. package/dist/canvas/shapes/ShapeLibrary.d.ts +64 -0
  23. package/dist/canvas/shapes/ShapeLibrary.js +526 -0
  24. package/dist/canvas/shapes/WidgetShape.d.ts +21 -0
  25. package/dist/canvas/shapes/WidgetShape.js +132 -0
  26. package/dist/canvas/types.d.ts +26 -0
  27. package/dist/canvas/types.js +5 -0
  28. package/dist/components/Tooltip.svelte +179 -0
  29. package/dist/components/Tooltip.svelte.d.ts +21 -0
  30. package/dist/components/index.d.ts +11 -0
  31. package/dist/components/index.js +13 -0
  32. package/dist/components/input-controls/ColorIC.svelte +388 -0
  33. package/dist/components/input-controls/ColorIC.svelte.d.ts +22 -0
  34. package/dist/components/input-controls/FontSelectorIC.svelte +69 -0
  35. package/dist/components/input-controls/FontSelectorIC.svelte.d.ts +17 -0
  36. package/dist/components/input-controls/SliderUnitIC.svelte +217 -0
  37. package/dist/components/input-controls/SliderUnitIC.svelte.d.ts +24 -0
  38. package/dist/components/input-controls/TextAlignIC.svelte +84 -0
  39. package/dist/components/input-controls/TextAlignIC.svelte.d.ts +17 -0
  40. package/dist/components/input-controls/TextStyleIC.svelte +85 -0
  41. package/dist/components/input-controls/TextStyleIC.svelte.d.ts +21 -0
  42. package/dist/icons/arrow.svg +3 -0
  43. package/dist/icons/checkmark.svg +3 -0
  44. package/dist/icons/draw.svg +22 -0
  45. package/dist/icons/eraser.svg +23 -0
  46. package/dist/icons/hand.svg +6 -0
  47. package/dist/icons/select.svg +3 -0
  48. package/dist/icons/text.svg +5 -0
  49. package/dist/index.d.ts +11 -0
  50. package/dist/index.js +12 -0
  51. package/package.json +101 -0
@@ -0,0 +1,526 @@
1
+ /**
2
+ * ShapeLibrary.ts
3
+ *
4
+ * Pre-defined SVG path data for special shapes (media controls, UI icons, etc.).
5
+ * All paths are normalized to a 100×100 viewBox so Fabric.js scales them uniformly.
6
+ *
7
+ * cornerRadiusMode per shape:
8
+ * 'none' – shape already curved (circle, heart) → hide Corner Radius slider
9
+ * 'clipPath' – clip bounding-box rect to rounded rect (arrows, …)
10
+ * 'strokeRound' – apply round linecap / linejoin (line-only: check, X, +, -)
11
+ * 'pathRegen' – regenerate sub-shape path data with rounded corners (rects & polygons)
12
+ */
13
+ // ── Path-regen helpers ────────────────────────────────────────────────────────
14
+ /** Generate SVG path data for a rounded rectangle. */
15
+ export function roundedRectPath(x, y, w, h, r) {
16
+ r = Math.min(r, w / 2, h / 2);
17
+ const x2 = x + w, y2 = y + h;
18
+ return [
19
+ `M ${x + r} ${y}`,
20
+ `L ${x2 - r} ${y}`, `Q ${x2} ${y} ${x2} ${y + r}`,
21
+ `L ${x2} ${y2 - r}`, `Q ${x2} ${y2} ${x2 - r} ${y2}`,
22
+ `L ${x + r} ${y2}`, `Q ${x} ${y2} ${x} ${y2 - r}`,
23
+ `L ${x} ${y + r}`, `Q ${x} ${y} ${x + r} ${y}`,
24
+ 'Z',
25
+ ].join(' ');
26
+ }
27
+ /**
28
+ * Generate SVG path data for a convex polygon with rounded corners.
29
+ * @param points – vertices in order (any winding)
30
+ * @param r – corner radius in path units
31
+ */
32
+ export function roundedPolygonPath(points, r) {
33
+ if (r <= 0) {
34
+ return (points.map((p, i) => `${i === 0 ? 'M' : 'L'} ${p[0]} ${p[1]}`).join(' ') + ' Z');
35
+ }
36
+ const n = points.length;
37
+ // Cap r to 45% of the shortest edge to stay inside the polygon
38
+ let minEdge = Infinity;
39
+ for (let i = 0; i < n; i++) {
40
+ const a = points[i], b = points[(i + 1) % n];
41
+ const d = Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2);
42
+ if (d < minEdge)
43
+ minEdge = d;
44
+ }
45
+ const rClamped = Math.min(r, minEdge * 0.45);
46
+ let d = '';
47
+ for (let i = 0; i < n; i++) {
48
+ const prev = points[(i - 1 + n) % n];
49
+ const curr = points[i];
50
+ const next = points[(i + 1) % n];
51
+ // Unit vectors from corner toward its neighbours
52
+ const pLen = Math.sqrt((prev[0] - curr[0]) ** 2 + (prev[1] - curr[1]) ** 2) || 1;
53
+ const nLen = Math.sqrt((next[0] - curr[0]) ** 2 + (next[1] - curr[1]) ** 2) || 1;
54
+ const pn = [(prev[0] - curr[0]) / pLen, (prev[1] - curr[1]) / pLen];
55
+ const nn = [(next[0] - curr[0]) / nLen, (next[1] - curr[1]) / nLen];
56
+ // Points along each edge, distance rClamped from the corner vertex
57
+ const p1 = [curr[0] + pn[0] * rClamped, curr[1] + pn[1] * rClamped];
58
+ const p2 = [curr[0] + nn[0] * rClamped, curr[1] + nn[1] * rClamped];
59
+ d += i === 0
60
+ ? `M ${p1[0].toFixed(2)} ${p1[1].toFixed(2)} `
61
+ : `L ${p1[0].toFixed(2)} ${p1[1].toFixed(2)} `;
62
+ d += `Q ${curr[0]} ${curr[1]} ${p2[0].toFixed(2)} ${p2[1].toFixed(2)} `;
63
+ }
64
+ return d + 'Z';
65
+ }
66
+ /**
67
+ * Regenerate the full SVG path string for a library shape at the given roundness (0-100 %).
68
+ * Returns null for shapes that don't use pathRegen.
69
+ */
70
+ export function generatePathForShape(shape, roundnessPercent) {
71
+ // Special case: curved-arrow has a Bezier-curve body that cannot be expressed as polygon
72
+ // regenData. Each L-command corner and the two C-to-L junctions are rounded individually
73
+ // using Quadratic Bezier arcs so the entire shape looks smooth at high roundness values.
74
+ if (shape.key === 'curved-arrow') {
75
+ const t = (roundnessPercent / 100) * 8; // max 8-unit radius at 100%
76
+ if (t < 0.1)
77
+ return null;
78
+ // Round a corner at (cx,cy): px/py is the INCOMING reference point, nx/ny is OUTGOING.
79
+ // For C commands use the adjacent control point as the reference direction.
80
+ // Returns { s: arcStart, e: arcEnd } strings for "…s Q cx cy e…" insertion.
81
+ const rc = (px, py, cx, cy, nx, ny) => {
82
+ const dx1 = px - cx, dy1 = py - cy;
83
+ const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1) || 1;
84
+ const dx2 = nx - cx, dy2 = ny - cy;
85
+ const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
86
+ const r = Math.min(t, l1 * 0.45, l2 * 0.45);
87
+ return {
88
+ s: `${(cx + dx1 / l1 * r).toFixed(1)} ${(cy + dy1 / l1 * r).toFixed(1)}`,
89
+ e: `${(cx + dx2 / l2 * r).toFixed(1)} ${(cy + dy2 / l2 * r).toFixed(1)}`,
90
+ };
91
+ };
92
+ // Corners with reference points for tangent direction:
93
+ // C0: M/Z corner at (80,20) — see detailed note below
94
+ // C1: arrowhead TIP (95,35) prev=(80,20) next=(80,50)
95
+ // C2: outer lower wing (80,50) prev=(95,35) next=(80,40)
96
+ // C3: inner shoulder (80,40) prev=(80,50) next=C cp1=(50,40)
97
+ // C4: C-to-L junction (30,80) prev=C cp2=(30,55) next=(15,80) [modifies C endpoint]
98
+ // C5: tail end (15,80) prev=(30,80) next=C cp1=(15,45)
99
+ //
100
+ // C0 corner at M(80,20) — ASYMMETRIC arc approach:
101
+ //
102
+ // Constraint: the Z-line (from C endpoint (80,25) to M(80,20)) is only 5px, so a symmetric
103
+ // rc() would cap the radius to 2.25px. Moving the C endpoint to get more room (previous
104
+ // attempts) either deformed the shape (moved horizontally) or changed the 0% base shape
105
+ // (moved vertically by changing pathData).
106
+ //
107
+ // Solution: use a fixed incoming side (C endpoint NEVER changes, always stays at (80,25))
108
+ // and a scaling outgoing side (M moves along the first-L edge toward (95,35) with t).
109
+ // — c0_s is always (80,25): the Z-line start, no modification to the C command at all.
110
+ // — c0_e scales with t: at t=0 → M=(80,20) = exact original path; at t=8 → M=(85.7,25.7).
111
+ // The Q arc from (80,25) Q(80,20) to c0_e smoothly rounds the top corner with up to 8px
112
+ // effective radius on the outgoing side, while the shape stays correct at any % level.
113
+ const r0 = Math.min(t, 21.2 * 0.45); // cap only by first-L edge length (21.2px × 0.45)
114
+ const c0_s = '80.0 25.0'; // original C endpoint — Z-line start, never moves
115
+ const c0_dirX = 15 / 21.2, c0_dirY = 15 / 21.2; // unit direction (80,20)→(95,35)
116
+ const c0_e = `${(80 + c0_dirX * r0).toFixed(1)} ${(20 + c0_dirY * r0).toFixed(1)}`;
117
+ const c1 = rc(80, 20, 95, 35, 80, 50); // arrowhead tip
118
+ const c2 = rc(95, 35, 80, 50, 80, 40); // outer lower arrowhead wing
119
+ const c3 = rc(80, 50, 80, 40, 50, 40); // inner shoulder (L→C, use C cp1 as direction)
120
+ const c4 = rc(30, 55, 30, 80, 15, 80); // C-to-L junction (C cp2 as incoming direction)
121
+ const c5 = rc(30, 80, 15, 80, 15, 45); // tail end (L→C, use C cp1 as direction)
122
+ return [
123
+ `M ${c0_e}`, // start at c0 arc end (Z closes back here)
124
+ `L ${c1.s} Q 95 35 ${c1.e}`, // arrowhead tip
125
+ `L ${c2.s} Q 80 50 ${c2.e}`, // outer lower arrowhead wing
126
+ `L ${c3.s} Q 80 40 ${c3.e}`, // inner shoulder
127
+ `C 50 40 30 55 ${c4.s} Q 30 80 ${c4.e}`, // curve body + C→L junction
128
+ `L ${c5.s} Q 15 80 ${c5.e}`, // tail end
129
+ `C 15 45 40 25 ${c0_s} Q 80 20 ${c0_e} Z`, // return C (endpoint unchanged) + M arc
130
+ ].join(' ');
131
+ }
132
+ // ── Shield: quadratic-arc rounding of 5 angular corners, bezier bottom preserved ──
133
+ // Path: M 50 5 L 90 20 L 90 55 C 90 75 70 90 50 95 C 30 90 10 75 10 55 L 10 20 Z
134
+ // Corners: P0=(50,5) P1=(90,20) P2=(90,55) P4=(10,55) P5=(10,20)
135
+ // At 0%, returns null so the original pathData is used exactly (no shape change).
136
+ if (shape.key === 'shield') {
137
+ const maxT = 10; // max arc radius in path units at 100% roundness
138
+ const t = (roundnessPercent / 100) * maxT;
139
+ if (t < 0.1)
140
+ return null;
141
+ // rc: quadratic bezier arc at (cx,cy), optional tMax clamps radius for corners
142
+ // adjacent to bezier curves to limit distortion of the bezier endpoints.
143
+ const rc2 = (px, py, cx, cy, nx, ny, tMax = t) => {
144
+ const tEff = Math.min(t, tMax);
145
+ const dx1 = px - cx, dy1 = py - cy;
146
+ const l1 = Math.sqrt(dx1 * dx1 + dy1 * dy1) || 1;
147
+ const dx2 = nx - cx, dy2 = ny - cy;
148
+ const l2 = Math.sqrt(dx2 * dx2 + dy2 * dy2) || 1;
149
+ const r = Math.min(tEff, l1 * 0.45, l2 * 0.45);
150
+ return {
151
+ s: `${(cx + dx1 / l1 * r).toFixed(1)} ${(cy + dy1 / l1 * r).toFixed(1)}`,
152
+ e: `${(cx + dx2 / l2 * r).toFixed(1)} ${(cy + dy2 / l2 * r).toFixed(1)}`,
153
+ };
154
+ };
155
+ // c0: top point (50,5) — prev is (10,20) via Z-line, next is (90,20)
156
+ // c1: top-right shoulder (90,20) — prev (50,5), next (90,55)
157
+ // c2: right side base (90,55) — limited tMax=4 to minimize bezier bottom distortion
158
+ // c3: bottom point (50,95) — junction of two bezier curves; reference points are
159
+ // the last cp of C1 (70,90) and the first cp of C2 (30,90)
160
+ // c4: left side base (10,55) — limited tMax=4 to minimize bezier bottom distortion
161
+ // c5: top-left shoulder (10,20) — prev (10,55), next (50,5)
162
+ const c0 = rc2(10, 20, 50, 5, 90, 20);
163
+ const c1 = rc2(50, 5, 90, 20, 90, 55);
164
+ const c2 = rc2(90, 20, 90, 55, 90, 75, 4); // cap arc size near bezier
165
+ const c3 = rc2(70, 90, 50, 95, 30, 90, 6); // bottom point (bezier junction, cap=6)
166
+ const c4 = rc2(10, 75, 10, 55, 10, 20, 4); // cap arc size near bezier
167
+ const c5 = rc2(10, 55, 10, 20, 50, 5);
168
+ // The top corner arc (c0) is placed LAST, just before Z, so the path visits:
169
+ // M(c0.e) → c1 arc → c2 arc → bezier1 (shortened to c3.s) → c3 Q arc →
170
+ // bezier2 (from c3.e, shortened) → c4 arc → c5 arc → c0 arc → Z
171
+ return [
172
+ `M ${c0.e}`, // start past top-point toward P1
173
+ `L ${c1.s} Q 90 20 ${c1.e}`, // arc at top-right shoulder
174
+ `L ${c2.s} Q 90 55 ${c2.e}`, // arc at right base (limited radius)
175
+ `C 90 75 70 90 ${c3.s}`, // bottom-right bezier, shortened to c3 arc entry
176
+ `Q 50 95 ${c3.e}`, // arc at bottom point
177
+ `C 30 90 10 75 ${c4.s}`, // bottom-left bezier, shortened to c4 arc entry
178
+ `Q 10 55 ${c4.e}`, // arc at left base (limited radius)
179
+ `L ${c5.s} Q 10 20 ${c5.e}`, // arc at top-left shoulder
180
+ `L ${c0.s} Q 50 5 ${c0.e}`, // arc at top point
181
+ `Z`, // zero-distance close back to M (c0.e)
182
+ ].join(' ');
183
+ }
184
+ if (shape.cornerRadiusMode !== 'pathRegen' || !shape.regenData)
185
+ return null;
186
+ return shape.regenData.map(sub => {
187
+ if (sub.type === 'fixed')
188
+ return sub.path;
189
+ if (sub.type === 'rect') {
190
+ const maxR = Math.min(sub.w, sub.h) / 2;
191
+ const r = (roundnessPercent / 100) * maxR;
192
+ return roundedRectPath(sub.x, sub.y, sub.w, sub.h, r);
193
+ }
194
+ // polygon
195
+ const pts = sub.points;
196
+ const n = pts.length;
197
+ let minEdge = Infinity;
198
+ for (let i = 0; i < n; i++) {
199
+ const a = pts[i], b = pts[(i + 1) % n];
200
+ minEdge = Math.min(minEdge, Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2));
201
+ }
202
+ const maxR = minEdge * 0.45;
203
+ const r = (roundnessPercent / 100) * maxR;
204
+ return roundedPolygonPath(pts, r);
205
+ }).join(' ');
206
+ }
207
+ // ── Geometric Shapes (library) ─────────────────────────────────────────────
208
+ const GEOMETRIC_SHAPES = [
209
+ {
210
+ key: 'hexagon',
211
+ label: 'Hexagon',
212
+ category: 'Geometric Shapes',
213
+ pathData: 'M 50 3 L 93 27 L 93 73 L 50 97 L 7 73 L 7 27 Z',
214
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><polygon points="12,2 21.5,7 21.5,17 12,22 2.5,17 2.5,7" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
215
+ cornerRadiusMode: 'pathRegen',
216
+ regenData: [
217
+ { type: 'polygon', points: [[50, 3], [93, 27], [93, 73], [50, 97], [7, 73], [7, 27]] },
218
+ ],
219
+ lockAspectRatio: true,
220
+ },
221
+ {
222
+ key: 'oval',
223
+ label: 'Oval',
224
+ category: 'Geometric Shapes',
225
+ // Capsule/stadium shape: wide rounded rect with max corner radius (half the height).
226
+ // 160×90 content area, corner radius = 45 (half of 90). Produces semicircle caps like a pill.
227
+ pathData: 'M 50 5 L 110 5 Q 155 5 155 50 L 155 50 Q 155 95 110 95 L 50 95 Q 5 95 5 50 L 5 50 Q 5 5 50 5 Z',
228
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><rect x="3" y="5" width="18" height="14" rx="7" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>`,
229
+ cornerRadiusMode: 'none',
230
+ lockAspectRatio: false,
231
+ },
232
+ ];
233
+ // ── Media Icons ──────────────────────────────────────────────────────────────
234
+ const MEDIA_SHAPES = [
235
+ {
236
+ key: 'play',
237
+ label: 'Play',
238
+ category: 'Media Icons',
239
+ pathData: 'M 10 5 L 10 95 L 90 50 Z',
240
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><polygon points="5,3 5,21 20,12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
241
+ cornerRadiusMode: 'pathRegen',
242
+ regenData: [
243
+ { type: 'polygon', points: [[10, 5], [10, 95], [90, 50]] },
244
+ ],
245
+ },
246
+ {
247
+ key: 'pause',
248
+ label: 'Pause',
249
+ category: 'Media Icons',
250
+ pathData: 'M 15 5 L 15 95 L 35 95 L 35 5 Z M 65 5 L 65 95 L 85 95 L 85 5 Z',
251
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><rect x="6" y="4" width="4" height="16" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/><rect x="14" y="4" width="4" height="16" rx="1" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>`,
252
+ cornerRadiusMode: 'pathRegen',
253
+ regenData: [
254
+ { type: 'rect', x: 15, y: 5, w: 20, h: 90 },
255
+ { type: 'rect', x: 65, y: 5, w: 20, h: 90 },
256
+ ],
257
+ },
258
+ {
259
+ key: 'stop',
260
+ label: 'Stop',
261
+ category: 'Media Icons',
262
+ pathData: 'M 10 10 L 10 90 L 90 90 L 90 10 Z',
263
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><rect x="5" y="5" width="14" height="14" rx="1.5" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>`,
264
+ cornerRadiusMode: 'pathRegen',
265
+ regenData: [
266
+ { type: 'rect', x: 10, y: 10, w: 80, h: 80 },
267
+ ],
268
+ },
269
+ {
270
+ key: 'rewind',
271
+ label: 'Rewind',
272
+ category: 'Media Icons',
273
+ pathData: 'M 50 5 L 50 95 L 5 50 Z M 95 5 L 95 95 L 50 50 Z',
274
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><polygon points="12,3 12,21 3,12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><polygon points="21,3 21,21 12,12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
275
+ cornerRadiusMode: 'pathRegen',
276
+ regenData: [
277
+ { type: 'polygon', points: [[50, 5], [50, 95], [5, 50]] },
278
+ { type: 'polygon', points: [[95, 5], [95, 95], [50, 50]] },
279
+ ],
280
+ },
281
+ {
282
+ key: 'fast-forward',
283
+ label: 'Fast Forward',
284
+ category: 'Media Icons',
285
+ pathData: 'M 5 5 L 5 95 L 50 50 Z M 50 5 L 50 95 L 95 50 Z',
286
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><polygon points="3,3 3,21 12,12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><polygon points="12,3 12,21 21,12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
287
+ cornerRadiusMode: 'pathRegen',
288
+ regenData: [
289
+ { type: 'polygon', points: [[5, 5], [5, 95], [50, 50]] },
290
+ { type: 'polygon', points: [[50, 5], [50, 95], [95, 50]] },
291
+ ],
292
+ },
293
+ {
294
+ key: 'record',
295
+ label: 'Record',
296
+ category: 'Media Icons',
297
+ // Already a circle — no corner rounding needed
298
+ pathData: 'M 50 5 A 45 45 0 1 1 49.999 5 Z',
299
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="12" r="7" fill="none" stroke="currentColor" stroke-width="1.5"/></svg>`,
300
+ cornerRadiusMode: 'none',
301
+ },
302
+ ];
303
+ // ── Arrow Shapes ─────────────────────────────────────────────────────────────
304
+ const ARROW_SHAPES = [
305
+ {
306
+ key: 'arrow-right',
307
+ label: 'Arrow Right',
308
+ category: 'Arrow Shapes',
309
+ pathData: 'M 5 40 L 65 40 L 65 20 L 95 50 L 65 80 L 65 60 L 5 60 Z',
310
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M5 12 H19 M13 6 L19 12 L13 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
311
+ cornerRadiusMode: 'pathRegen',
312
+ regenData: [{ type: 'polygon', points: [[5, 40], [65, 40], [65, 20], [95, 50], [65, 80], [65, 60], [5, 60]] }],
313
+ },
314
+ {
315
+ key: 'arrow-left',
316
+ label: 'Arrow Left',
317
+ category: 'Arrow Shapes',
318
+ pathData: 'M 95 40 L 35 40 L 35 20 L 5 50 L 35 80 L 35 60 L 95 60 Z',
319
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M19 12 H5 M11 6 L5 12 L11 18" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
320
+ cornerRadiusMode: 'pathRegen',
321
+ regenData: [{ type: 'polygon', points: [[95, 40], [35, 40], [35, 20], [5, 50], [35, 80], [35, 60], [95, 60]] }],
322
+ },
323
+ {
324
+ key: 'arrow-up',
325
+ label: 'Arrow Up',
326
+ category: 'Arrow Shapes',
327
+ pathData: 'M 40 95 L 40 35 L 20 35 L 50 5 L 80 35 L 60 35 L 60 95 Z',
328
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 19 V5 M6 11 L12 5 L18 11" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
329
+ cornerRadiusMode: 'pathRegen',
330
+ regenData: [{ type: 'polygon', points: [[40, 95], [40, 35], [20, 35], [50, 5], [80, 35], [60, 35], [60, 95]] }],
331
+ },
332
+ {
333
+ key: 'arrow-down',
334
+ label: 'Arrow Down',
335
+ category: 'Arrow Shapes',
336
+ pathData: 'M 60 5 L 60 65 L 80 65 L 50 95 L 20 65 L 40 65 L 40 5 Z',
337
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 5 V19 M6 13 L12 19 L18 13" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
338
+ cornerRadiusMode: 'pathRegen',
339
+ regenData: [{ type: 'polygon', points: [[60, 5], [60, 65], [80, 65], [50, 95], [20, 65], [40, 65], [40, 5]] }],
340
+ },
341
+ {
342
+ key: 'double-arrow',
343
+ label: 'Double Arrow',
344
+ category: 'Arrow Shapes',
345
+ pathData: 'M 30 20 L 5 50 L 30 80 L 30 62 L 70 62 L 70 80 L 95 50 L 70 20 L 70 38 L 30 38 Z',
346
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M7 12 H17 M5 8 L1 12 L5 16 M19 8 L23 12 L19 16" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
347
+ cornerRadiusMode: 'pathRegen',
348
+ regenData: [{ type: 'polygon', points: [[30, 20], [5, 50], [30, 80], [30, 62], [70, 62], [70, 80], [95, 50], [70, 20], [70, 38], [30, 38]] }],
349
+ },
350
+ {
351
+ key: 'curved-arrow',
352
+ label: 'Curved Arrow',
353
+ category: 'Arrow Shapes',
354
+ // Uses pathRegen mode: generatePathForShape() has a special case for 'curved-arrow'
355
+ // that rounds only the arrowhead tip via a Quadratic Bezier. clipPath was avoided
356
+ // because it clips the bounding box, cutting off arrowhead tips at high roundness.
357
+ // strokeRound was avoided because it only affects the stroke outline, not the fill corners.
358
+ pathData: 'M 80 20 L 95 35 L 80 50 L 80 40 C 50 40 30 55 30 80 L 15 80 C 15 45 40 25 80 25 Z',
359
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M17 4 L21 8 L17 12 M21 8 C15 8 9 11 9 20" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
360
+ cornerRadiusMode: 'pathRegen',
361
+ // regenData is intentionally omitted — generatePathForShape handles this shape directly.
362
+ },
363
+ ];
364
+ // ── UI Icons ─────────────────────────────────────────────────────────────────
365
+ const UI_ICON_SHAPES = [
366
+ {
367
+ key: 'check',
368
+ label: 'Check Mark',
369
+ category: 'UI Icons',
370
+ // Open path — round by setting strokeLineCap/strokeLineJoin
371
+ pathData: 'M 5 55 L 35 85 L 95 15',
372
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M4 13 L9 18 L20 6" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/></svg>`,
373
+ cornerRadiusMode: 'strokeRound',
374
+ },
375
+ {
376
+ key: 'cross',
377
+ label: 'Cross / X',
378
+ category: 'UI Icons',
379
+ pathData: 'M 10 10 L 90 90 M 90 10 L 10 90',
380
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M5 5 L19 19 M19 5 L5 19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
381
+ cornerRadiusMode: 'strokeRound',
382
+ },
383
+ {
384
+ key: 'plus',
385
+ label: 'Plus',
386
+ category: 'UI Icons',
387
+ pathData: 'M 50 10 L 50 90 M 10 50 L 90 50',
388
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 5 V19 M5 12 H19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
389
+ cornerRadiusMode: 'strokeRound',
390
+ },
391
+ {
392
+ key: 'minus',
393
+ label: 'Minus',
394
+ category: 'UI Icons',
395
+ pathData: 'M 10 50 L 90 50',
396
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M5 12 H19" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
397
+ cornerRadiusMode: 'strokeRound',
398
+ },
399
+ {
400
+ key: 'info',
401
+ label: 'Info',
402
+ category: 'UI Icons',
403
+ // Outer circle is already curved — no corner rounding
404
+ pathData: 'M 50 5 A 45 45 0 1 1 49.999 5 Z M 50 30 L 50 32 M 50 42 L 50 72',
405
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><circle cx="12" cy="12" r="9" fill="none" stroke="currentColor" stroke-width="1.5"/><path d="M12 8 V8.5 M12 11 V16" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
406
+ cornerRadiusMode: 'none',
407
+ },
408
+ {
409
+ key: 'warning',
410
+ label: 'Warning',
411
+ category: 'UI Icons',
412
+ // Outer triangle + inner ! lines (inner lines kept fixed)
413
+ pathData: 'M 50 5 L 95 90 L 5 90 Z M 50 40 L 50 60 M 50 70 L 50 72',
414
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 3 L22 20 H2 Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/><path d="M12 10 V14 M12 17 V17.5" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round"/></svg>`,
415
+ cornerRadiusMode: 'pathRegen',
416
+ regenData: [
417
+ { type: 'polygon', points: [[50, 5], [95, 90], [5, 90]] },
418
+ { type: 'fixed', path: 'M 50 40 L 50 60 M 50 70 L 50 72' },
419
+ ],
420
+ },
421
+ ];
422
+ // ── Symbol Shapes ─────────────────────────────────────────────────────────────
423
+ const SYMBOL_SHAPES = [
424
+ {
425
+ key: 'star',
426
+ label: 'Star',
427
+ category: 'Symbol Shapes',
428
+ pathData: 'M 50 5 L 61 35 L 95 35 L 68 57 L 79 91 L 50 70 L 21 91 L 32 57 L 5 35 L 39 35 Z',
429
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><polygon points="12,2 15.09,8.26 22,9.27 17,14.14 18.18,21.02 12,17.77 5.82,21.02 7,14.14 2,9.27 8.91,8.26" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
430
+ // pathRegen rounds each polygon corner individually (outer tips + inner concave vertices)
431
+ cornerRadiusMode: 'pathRegen',
432
+ regenData: [
433
+ { type: 'polygon', points: [[50, 5], [61, 35], [95, 35], [68, 57], [79, 91], [50, 70], [21, 91], [32, 57], [5, 35], [39, 35]] },
434
+ ],
435
+ },
436
+ {
437
+ key: 'heart',
438
+ label: 'Heart',
439
+ category: 'Symbol Shapes',
440
+ // Already curved — no corner rounding
441
+ pathData: 'M 50 85 C 5 55 5 20 25 15 C 35 12 45 17 50 25 C 55 17 65 12 75 15 C 95 20 95 55 50 85 Z',
442
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 21C12 21 3 14 3 8.5A4.5 4.5 0 0 1 12 6.5 4.5 4.5 0 0 1 21 8.5C21 14 12 21 12 21Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
443
+ cornerRadiusMode: 'none',
444
+ },
445
+ {
446
+ key: 'speech-bubble',
447
+ label: 'Speech Bubble',
448
+ category: 'Symbol Shapes',
449
+ pathData: 'M 10 10 L 10 65 L 35 65 L 50 90 L 65 65 L 90 65 L 90 10 Z',
450
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M4 4 H20 A2 2 0 0 1 22 6 V15 A2 2 0 0 1 20 17 H9 L4 21 V17 A2 2 0 0 1 2 15 V6 A2 2 0 0 1 4 4 Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
451
+ // pathRegen rounds box corners; the tail corner also softens slightly
452
+ cornerRadiusMode: 'pathRegen',
453
+ regenData: [
454
+ { type: 'polygon', points: [[10, 10], [10, 65], [35, 65], [50, 90], [65, 65], [90, 65], [90, 10]] },
455
+ ],
456
+ // Speech bubbles are intentionally rectangular — allow free resize to set width/height
457
+ lockAspectRatio: false,
458
+ },
459
+ {
460
+ key: 'diamond',
461
+ label: 'Rhombus',
462
+ category: 'Symbol Shapes',
463
+ pathData: 'M 50 5 L 95 50 L 50 95 L 5 50 Z',
464
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><polygon points="12,2 22,12 12,22 2,12" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
465
+ cornerRadiusMode: 'pathRegen',
466
+ regenData: [
467
+ { type: 'polygon', points: [[50, 5], [95, 50], [50, 95], [5, 50]] },
468
+ ],
469
+ lockAspectRatio: false,
470
+ },
471
+ {
472
+ key: 'lightning',
473
+ label: 'Lightning',
474
+ category: 'Symbol Shapes',
475
+ pathData: 'M 55 5 L 20 52 L 45 52 L 38 95 L 80 42 L 55 42 Z',
476
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M13 2 L5 13 H11 L9 22 L19 9 H13 Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
477
+ cornerRadiusMode: 'pathRegen',
478
+ regenData: [
479
+ { type: 'polygon', points: [[55, 5], [20, 52], [45, 52], [38, 95], [80, 42], [55, 42]] },
480
+ ],
481
+ lockAspectRatio: false,
482
+ },
483
+ {
484
+ key: 'shield',
485
+ label: 'Shield',
486
+ category: 'Symbol Shapes',
487
+ pathData: 'M 50 5 L 90 20 L 90 55 C 90 75 70 90 50 95 C 30 90 10 75 10 55 L 10 20 Z',
488
+ iconSvg: `<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 24 24"><path d="M12 3 L20 7 V13 C20 17.5 16.5 21 12 22 C7.5 21 4 17.5 4 13 V7 Z" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round"/></svg>`,
489
+ cornerRadiusMode: 'pathRegen',
490
+ lockAspectRatio: false,
491
+ },
492
+ ];
493
+ /** All library shapes, grouped by category (Geometric first, then Arrows) */
494
+ export const SHAPE_LIBRARY = [
495
+ ...GEOMETRIC_SHAPES,
496
+ ...ARROW_SHAPES,
497
+ ...MEDIA_SHAPES,
498
+ ...UI_ICON_SHAPES,
499
+ ...SYMBOL_SHAPES,
500
+ ];
501
+ /**
502
+ * Keyed shape index: single canonical id→shape lookup, built once at module
503
+ * load. Replaces the repeated `SHAPE_LIBRARY.find(s => s.key === ...)`
504
+ * linear scans across Canvas.svelte. Keys are unique by construction
505
+ * (categories partition the array); a duplicate would be an authoring bug.
506
+ */
507
+ const SHAPE_LIBRARY_BY_KEY = new Map(SHAPE_LIBRARY.map(shape => [shape.key, shape]));
508
+ /**
509
+ * Look up a library shape definition by its stable key.
510
+ * Returns undefined for unknown keys (dynamic/legacy shape keys).
511
+ */
512
+ export function getShapeByKey(key) {
513
+ if (!key)
514
+ return undefined;
515
+ return SHAPE_LIBRARY_BY_KEY.get(key);
516
+ }
517
+ /** Shapes grouped by category for rendering in the UI */
518
+ export function getShapesByCategory() {
519
+ const map = new Map();
520
+ for (const shape of SHAPE_LIBRARY) {
521
+ const existing = map.get(shape.category) ?? [];
522
+ existing.push(shape);
523
+ map.set(shape.category, existing);
524
+ }
525
+ return map;
526
+ }
@@ -0,0 +1,21 @@
1
+ import { ImageShape } from './ImageShape';
2
+ export declare const WIDGET_SHAPE_EXTRA_PROPS: string[];
3
+ export declare const WIDGET_SHAPE_CUSTOM_PROPS: string[];
4
+ export declare class WidgetShape extends ImageShape {
5
+ static type: string;
6
+ _isWidgetImage: boolean;
7
+ _widgetId: string | null;
8
+ _widgetName: string | null;
9
+ /** True when _widgetId no longer exists in the host widget list (deleted or moved). */
10
+ _widgetOrphaned: boolean;
11
+ /** Custom placeholder overlay: replace generic "Drop image" text with the widget name. */
12
+ _render(ctx: CanvasRenderingContext2D): void;
13
+ private _renderWidgetPlaceholder;
14
+ toObject(propertiesToInclude?: any[]): any;
15
+ static fromObject(object: Record<string, unknown>, options?: Record<string, unknown>): Promise<WidgetShape>;
16
+ }
17
+ /**
18
+ * Upgrades an ImageShape to a WidgetShape by swapping the prototype chain.
19
+ * Called when user clicks "Convert to Widget".
20
+ */
21
+ export declare function upgradeToWidgetShape(obj: any): WidgetShape | null;