@snowcone-app/canvas 0.16.0 → 0.17.0

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 (70) hide show
  1. package/dist/{CanvasStateV1-BQD8fCBU.js → CanvasStateV1-C6i6vU6p.js} +63 -63
  2. package/dist/{CanvasStateV1-BQD8fCBU.js.map → CanvasStateV1-C6i6vU6p.js.map} +1 -1
  3. package/dist/EditorCanvasImpl-DkgE5-C2.js +68 -0
  4. package/dist/EditorCanvasImpl-DkgE5-C2.js.map +1 -0
  5. package/dist/EditorContext-CyanYzo2.js +1131 -0
  6. package/dist/EditorContext-CyanYzo2.js.map +1 -0
  7. package/dist/{ImportManager-eotl7r5d.js → ElementFactory-C05JJkRp.js} +1510 -5121
  8. package/dist/ElementFactory-C05JJkRp.js.map +1 -0
  9. package/dist/{HybridHistoryManager-NL3zyEl4.js → HybridHistoryManager-Bht98XRv.js} +98 -98
  10. package/dist/{HybridHistoryManager-NL3zyEl4.js.map → HybridHistoryManager-Bht98XRv.js.map} +1 -1
  11. package/dist/ImportManager-kRYfIX-D.js +3617 -0
  12. package/dist/ImportManager-kRYfIX-D.js.map +1 -0
  13. package/dist/SnowconeCanvas-C3eZ9PP8.js +1047 -0
  14. package/dist/SnowconeCanvas-C3eZ9PP8.js.map +1 -0
  15. package/dist/ThemeContext-Rjz25x9X.js +75 -0
  16. package/dist/ThemeContext-Rjz25x9X.js.map +1 -0
  17. package/dist/UnifiedFontService-BAtqqkU4.js +1478 -0
  18. package/dist/UnifiedFontService-BAtqqkU4.js.map +1 -0
  19. package/dist/advanced.mjs +210 -207
  20. package/dist/advanced.mjs.map +1 -1
  21. package/dist/api/embed.d.ts +8 -4
  22. package/dist/api/testing.d.ts +1 -0
  23. package/dist/components/editor/EditorBuy.d.ts +49 -0
  24. package/dist/components/editor/EditorCanvas.d.ts +30 -0
  25. package/dist/components/editor/EditorCanvasImpl.d.ts +33 -0
  26. package/dist/components/editor/EditorPreview.d.ts +39 -0
  27. package/dist/components/editor/EditorRoot.d.ts +86 -0
  28. package/dist/components/editor/EditorTextField.d.ts +22 -0
  29. package/dist/components/editor/RootInternals.d.ts +77 -0
  30. package/dist/components/editor/designStateStore.d.ts +45 -0
  31. package/dist/components/editor/index.d.ts +23 -0
  32. package/dist/components/embed-realtime/RealtimeCanvas.d.ts +1 -1
  33. package/dist/components/embed-realtime/RealtimeEmbed.d.ts +5 -78
  34. package/dist/components/embed-realtime/RealtimeMockup.d.ts +5 -34
  35. package/dist/components/embed-realtime/context.d.ts +12 -2
  36. package/dist/components/embed-realtime/validateEmbed.d.ts +17 -3
  37. package/dist/design/design-builder.d.ts +81 -6
  38. package/dist/editorError-DMENTZ-3.js +19 -0
  39. package/dist/editorError-DMENTZ-3.js.map +1 -0
  40. package/dist/editorErrors-DbIEtCTt.js +29 -0
  41. package/dist/editorErrors-DbIEtCTt.js.map +1 -0
  42. package/dist/embed-Bdj4PKUA.js +737 -0
  43. package/dist/embed-Bdj4PKUA.js.map +1 -0
  44. package/dist/embed.mjs +38 -230
  45. package/dist/embed.mjs.map +1 -1
  46. package/dist/errors/editorError.d.ts +57 -0
  47. package/dist/iconify-D6eVIEy4.js +1089 -0
  48. package/dist/iconify-D6eVIEy4.js.map +1 -0
  49. package/dist/index.mjs +48 -46
  50. package/dist/index.mjs.map +1 -1
  51. package/dist/internals.mjs +105 -104
  52. package/dist/internals.mjs.map +1 -1
  53. package/dist/template-fields-GOM4MNfq.js +643 -0
  54. package/dist/template-fields-GOM4MNfq.js.map +1 -0
  55. package/dist/testing/editorErrors.d.ts +30 -0
  56. package/dist/testing.mjs +28 -23
  57. package/dist/testing.mjs.map +1 -1
  58. package/dist/{usePieceGuideBundle-Cyl695jv.js → usePieceGuideBundle-CGERAQJl.js} +2 -2
  59. package/dist/{usePieceGuideBundle-Cyl695jv.js.map → usePieceGuideBundle-CGERAQJl.js.map} +1 -1
  60. package/dist/validation-DEd6EOCP.js +19947 -0
  61. package/dist/validation-DEd6EOCP.js.map +1 -0
  62. package/dist/workers/export-worker.bundle.string.d.ts +1 -1
  63. package/package.json +9 -4
  64. package/dist/ImportManager-eotl7r5d.js.map +0 -1
  65. package/dist/ThemeContext-D9AaUw9v.js +0 -1200
  66. package/dist/ThemeContext-D9AaUw9v.js.map +0 -1
  67. package/dist/template-fields-PcSPApl5.js +0 -1632
  68. package/dist/template-fields-PcSPApl5.js.map +0 -1
  69. package/dist/validation-BpEod0Rn.js +0 -22953
  70. package/dist/validation-BpEod0Rn.js.map +0 -1
@@ -0,0 +1,3617 @@
1
+ import { aC as tt, _ as ae, $ as re, a5 as Vt, aP as le, aN as he, aG as Ot, aM as ce, aF as ht, aW as de, aX as Gt, aY as fe, aZ as ge, c as zt, G as ue, I as Qt, P as pe, S as ye, T as be, a_ as me, a$ as Se, b0 as Te, b1 as we, b2 as te, ag as ee, az as ft, ah as ne, b3 as Bt, b4 as ve, V as xe, b5 as Ce, b6 as Yt, b7 as Me, b8 as Re, b9 as Ee, ac as ke, ba as Pe, bb as Ae, bc as Q, A as Xt } from "./HybridHistoryManager-Bht98XRv.js";
2
+ import { T as Ht, l as Ie, e as Et, m as Nt, n as Be, o as He, f as Kt } from "./ElementFactory-C05JJkRp.js";
3
+ const ot = {
4
+ // Canvas-specific
5
+ canvas: {
6
+ get selectionColor() {
7
+ return tt();
8
+ },
9
+ snapThreshold: 8,
10
+ // pixels
11
+ snapGuideDash: [5, 5],
12
+ snapGuideWidth: 1,
13
+ selectionPadding: 1,
14
+ // Red for spacing measurement lines
15
+ spacingIndicatorWidth: 1.5,
16
+ // Show spacing within 50px
17
+ spacingLabelFontSize: 11,
18
+ spacingLabelFontFamily: "-apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif"
19
+ }
20
+ }, Le = 2, Ct = 6, De = 16, We = 5, _e = 5, se = 0.5;
21
+ function $t(x) {
22
+ return Le / x;
23
+ }
24
+ function $e(x) {
25
+ return Ct / x;
26
+ }
27
+ function ze(x) {
28
+ return De / x;
29
+ }
30
+ function je(x) {
31
+ return [We / x, _e / x];
32
+ }
33
+ function Fe(x, t, e, o) {
34
+ x.save();
35
+ const n = se / o;
36
+ x.translate(t, e), x.scale(n, n), x.strokeStyle = "#000", x.lineWidth = 1.5, x.lineCap = "round", x.lineJoin = "round", x.translate(-12, -12), x.beginPath(), x.moveTo(21, 12), x.arc(12, 12, 9, 0, -Math.PI / 2, !0), x.bezierCurveTo(3.5, 4.5, 4, 6, 3, 8), x.stroke(), x.beginPath(), x.moveTo(3, 3), x.lineTo(3, 8), x.lineTo(8, 8), x.stroke(), x.beginPath(), x.moveTo(3, 12), x.arc(12, 12, 9, Math.PI, Math.PI / 2, !0), x.bezierCurveTo(20.5, 19.5, 20, 18, 21, 16), x.stroke(), x.beginPath(), x.moveTo(16, 16), x.lineTo(21, 16), x.lineTo(21, 21), x.stroke(), x.restore();
37
+ }
38
+ function Ue(x, t, e, o = 1, n = "#3b82f6", s = !1) {
39
+ const i = ze(o);
40
+ x.save(), x.fillStyle = "#fff", x.strokeStyle = n, x.lineWidth = s ? $t(o) * 1.25 : $t(o), x.beginPath(), x.arc(t, e, i, 0, Math.PI * 2), x.fill(), x.stroke(), Fe(x, t, e, o), x.restore();
41
+ }
42
+ const jt = Ct * 2;
43
+ class qe {
44
+ constructor() {
45
+ this.resizeHandles = [], this.rotationHandle = null, this.elementRotation = 0, this.rotationAnchor = { x: 0, y: 0 };
46
+ }
47
+ /**
48
+ * Update handle positions for an element
49
+ * @param element - The element to update handles for
50
+ * @param zoom - Current zoom level (used to maintain consistent rotation handle distance in screen space)
51
+ */
52
+ update(t, e = 1) {
53
+ const n = "isCropping" in t && t.isCropping ? t.getBoundingBox() : t.getVisualBoundingBox(), s = t.getRotationAnchor(), i = t.getEnabledAnchors();
54
+ this.elementRotation = t.rotation, this.rotationAnchor = s;
55
+ const a = ot.canvas.selectionPadding, r = {
56
+ x: n.x - a,
57
+ y: n.y - a,
58
+ width: n.width + a * 2,
59
+ height: n.height + a * 2
60
+ }, l = ae(r, t.rotation, s), h = Math.abs(t.rotation % 360) > 0.1;
61
+ this.resizeHandles = l.filter((c) => !(!i.includes(c.anchor) || h && c.anchor.includes("middle"))), this.rotationHandle = re(
62
+ r,
63
+ t.rotation,
64
+ s,
65
+ e
66
+ );
67
+ }
68
+ /**
69
+ * Render all handles
70
+ * @param renderRotationInCanvas - If false, skip canvas rendering of rotation handle (for React-based rendering)
71
+ */
72
+ render(t, e = !0, o = 0, n = !1, s = !1, i = null, a = !1, r = null, l = 1, h = !0) {
73
+ this.resizeHandles.forEach((c) => {
74
+ if (a && r && c.anchor !== r.anchor)
75
+ return;
76
+ const d = !!(i && i.anchor === c.anchor), u = !!(a && r && r.anchor === c.anchor);
77
+ this.renderResizeHandle(t, c, d, u, l);
78
+ }), h && e && this.rotationHandle && !n && !a && this.renderRotationHandle(t, this.rotationHandle, s, l), h && e && this.rotationHandle && n && this.renderRotationTooltip(t, this.rotationHandle, o, l);
79
+ }
80
+ /**
81
+ * Render a single resize handle
82
+ * Corner handles are circles, side handles are rounded rectangles
83
+ */
84
+ renderResizeHandle(t, e, o = !1, n = !1, s = 1) {
85
+ t.save();
86
+ const i = e.x * s, a = e.y * s;
87
+ if (e.anchor.includes("middle")) {
88
+ const l = e.anchor === "middle-top" || e.anchor === "middle-bottom";
89
+ t.translate(i, a), t.rotate(-this.elementRotation * Math.PI / 180), l && t.rotate(Math.PI / 2), t.translate(-i, -a);
90
+ const h = jt * 0.6, c = jt * 1.4, d = h / 2, u = i - h / 2, p = a - c / 2;
91
+ t.fillStyle = "#fff", t.strokeStyle = tt(t.canvas), t.lineWidth = o || n ? 2.5 : 2, t.beginPath(), t.roundRect(u, p, h, c, d), t.fill(), t.stroke();
92
+ } else
93
+ t.fillStyle = "#fff", t.strokeStyle = tt(t.canvas), t.lineWidth = o || n ? 2.5 : 2, t.beginPath(), t.arc(i, a, Ct, 0, Math.PI * 2), t.fill(), t.stroke();
94
+ t.restore();
95
+ }
96
+ /**
97
+ * Render the rotation handle (with refresh-ccw icon)
98
+ * Works in screen space (positions multiplied by zoom, fixed pixel sizes)
99
+ */
100
+ renderRotationHandle(t, e, o = !1, n = 1) {
101
+ t.save();
102
+ const s = e.x * n, i = e.y * n, a = Ct * 2.67;
103
+ t.fillStyle = "#fff", t.strokeStyle = tt(t.canvas), t.lineWidth = o ? 2.5 : 2, t.beginPath(), t.arc(s, i, a, 0, Math.PI * 2), t.fill(), t.stroke();
104
+ const r = se;
105
+ t.translate(s, i), t.scale(r, r), t.strokeStyle = "#000", t.lineWidth = 1.5, t.lineCap = "round", t.lineJoin = "round", t.translate(-12, -12), t.beginPath(), t.moveTo(21, 12), t.arc(12, 12, 9, 0, -Math.PI / 2, !0), t.bezierCurveTo(3.5, 4.5, 4, 6, 3, 8), t.stroke(), t.beginPath(), t.moveTo(3, 3), t.lineTo(3, 8), t.lineTo(8, 8), t.stroke(), t.beginPath(), t.moveTo(3, 12), t.arc(12, 12, 9, Math.PI, Math.PI / 2, !0), t.bezierCurveTo(20.5, 19.5, 20, 18, 21, 16), t.stroke(), t.beginPath(), t.moveTo(16, 16), t.lineTo(21, 16), t.lineTo(21, 21), t.stroke(), t.restore();
106
+ }
107
+ /**
108
+ * Render rotation tooltip (angle display while rotating)
109
+ */
110
+ renderRotationTooltip(t, e, o, n = 1) {
111
+ t.save();
112
+ const s = `${Math.round(o)}°`, i = s.length * 8 + 12, a = 22, r = e.x * n, l = e.y * n - 35;
113
+ t.shadowColor = "rgba(0,0,0,0.3)", t.shadowBlur = 3, t.shadowOffsetY = 1, t.fillStyle = "#333", t.beginPath(), t.roundRect(r - i / 2, l, i, a, 3), t.fill(), t.shadowColor = "transparent", t.shadowBlur = 0, t.shadowOffsetY = 0, t.fillStyle = "#fff", t.font = "12px Arial", t.textAlign = "center", t.textBaseline = "middle", t.fillText(s, r, l + a / 2), t.restore();
114
+ }
115
+ /**
116
+ * Test if a point hits any resize handle
117
+ * @param zoom - Current zoom level (radius scales inversely with zoom)
118
+ */
119
+ hitTestResize(t, e, o = 1) {
120
+ const n = he / o;
121
+ for (const s of this.resizeHandles)
122
+ if (Vt(t, e, s.x, s.y, n))
123
+ return s;
124
+ return null;
125
+ }
126
+ /**
127
+ * Test if a point hits the rotation handle
128
+ * @param zoom - Current zoom level (radius scales inversely with zoom)
129
+ */
130
+ hitTestRotation(t, e, o = 1) {
131
+ if (!this.rotationHandle) return !1;
132
+ const n = (Ct * 2.67 + 2) / o;
133
+ return Vt(t, e, this.rotationHandle.x, this.rotationHandle.y, n);
134
+ }
135
+ /**
136
+ * Get rotation handle position (for React-based rendering)
137
+ */
138
+ getRotationHandlePosition() {
139
+ return this.rotationHandle;
140
+ }
141
+ /**
142
+ * Get cursor style for a resize handle, adjusted for the element's current rotation.
143
+ * @param handle - The resize handle to get cursor for
144
+ * @param rotation - Element rotation in degrees
145
+ */
146
+ getCursor(t, e) {
147
+ const o = t.anchor.includes("middle") ? "edge" : "corner";
148
+ return le(
149
+ t.x,
150
+ t.y,
151
+ this.rotationAnchor.x,
152
+ this.rotationAnchor.y,
153
+ o,
154
+ e,
155
+ t.anchor
156
+ );
157
+ }
158
+ }
159
+ class Ve {
160
+ /**
161
+ * Calculate new dimensions and position for a resize operation
162
+ */
163
+ static calculateResize({
164
+ element: t,
165
+ handle: e,
166
+ dx: o,
167
+ dy: n,
168
+ startData: s,
169
+ snapSystem: i
170
+ }) {
171
+ var H, L, D, O, W;
172
+ const r = new Ot(t).worldDeltaToLocal(o, n), l = r.dx, h = r.dy, c = s.visualWidth ?? s.width ?? 0, d = s.visualHeight ?? s.height ?? 0, u = s.width ?? 0, p = s.height ?? 0;
173
+ let b = c, g = d;
174
+ e.anchor.includes("right") ? b = c + l : e.anchor.includes("left") && (b = c - l), e.anchor !== "middle-left" && e.anchor !== "middle-right" && (e.anchor.includes("bottom") ? g = d + h : e.anchor.includes("top") && (g = d - h));
175
+ let f, y;
176
+ if (e.anchor === "middle-left" || e.anchor === "middle-right") {
177
+ const V = b - c;
178
+ f = u + V, y = p;
179
+ } else if (e.anchor === "middle-top" || e.anchor === "middle-bottom") {
180
+ const V = g - d;
181
+ f = u, y = p + V;
182
+ } else {
183
+ const V = b / c, A = g / d;
184
+ f = u * V, y = p * A;
185
+ }
186
+ const S = b, T = g;
187
+ let w = !1;
188
+ if (i && t.transformType === "image") {
189
+ const V = e.anchor ?? "", A = V === "top-left" || V === "top-right" || V === "bottom-left" || V === "bottom-right", $ = V.startsWith("crop-");
190
+ if (A && !$) {
191
+ const j = (H = i.activeSnaps) == null ? void 0 : H.x, J = (L = i.activeSnaps) == null ? void 0 : L.y;
192
+ if (!!j || !!J) {
193
+ const E = [j, J].find(
194
+ (I) => typeof I == "object" && I !== null && "frozenWidth" in I
195
+ );
196
+ if (E) {
197
+ const I = t, _ = s.transformData, B = E.frozenWidth ?? ((D = I.transformData) == null ? void 0 : D.width) ?? u, F = I.imageAspectRatio || (_ != null && _.width && (_ != null && _.height) ? _.width / _.height : null);
198
+ let G = E.frozenHeight;
199
+ G == null && (F && F !== 0 ? G = B / F : (O = I.transformData) != null && O.height ? G = I.transformData.height : G = p);
200
+ const at = u !== 0 ? B / u : 1, gt = p !== 0 ? G / p : 1;
201
+ b = c * at, g = d * gt, f = B, y = G, w = !0;
202
+ }
203
+ }
204
+ }
205
+ }
206
+ f = Math.max(50, f), y = Math.max(40, y);
207
+ const m = t.clone(), C = m.resize(e.anchor, f, y, s);
208
+ if (!e.anchor.includes("middle") && ((W = m.stroke) != null && W.enabled) && s.strokeWidth !== void 0 && s.strokeWidth > 0) {
209
+ const V = s.width ?? 1, A = s.height ?? 1, $ = Math.sqrt(f / V * (y / A)), j = Math.max(1, Math.round(s.strokeWidth * $ * 10) / 10);
210
+ m.stroke = { ...m.stroke, width: j };
211
+ }
212
+ const M = w || C === !1, k = e.anchor.includes("top") || e.anchor.includes("bottom"), v = "isCropping" in m && m.isCropping, P = "isGroup" in m && m.isGroup;
213
+ if (k && !v && !P) {
214
+ const V = t.getRotationAnchor(), A = this._calculateFixedCornerOffset({
215
+ element: t,
216
+ updatedElement: m,
217
+ handle: e,
218
+ rotationAnchor: V
219
+ });
220
+ m.x += A.x, m.y += A.y;
221
+ }
222
+ return {
223
+ element: m,
224
+ isFrozen: M,
225
+ // For snap system to detect break-free: dimensions before freeze was applied
226
+ unfrozenDimensions: w ? { width: S, height: T } : void 0
227
+ };
228
+ }
229
+ /**
230
+ * Calculate position offset to keep opposite corner fixed
231
+ * Uses visual bounding box and rotation anchor for proper rotated resize
232
+ * @private
233
+ */
234
+ static _calculateFixedCornerOffset({
235
+ element: t,
236
+ updatedElement: e,
237
+ handle: o,
238
+ rotationAnchor: n
239
+ }) {
240
+ const s = new Ot(t), { cos: i, sin: a } = s.getRenderingCosSin(), r = t.getVisualBoundingBox(), l = e.getVisualBoundingBox(), h = n, c = e.getRotationAnchor(), d = this._getFixedLocalCoords(o.anchor, r.width, r.height), u = d.x, p = d.y, b = r.x - h.x, g = r.y - h.y, f = b + u, y = g + p, S = f * i - y * a, T = f * a + y * i, w = h.x + S, m = h.y + T, C = this._getFixedLocalCoords(o.anchor, l.width, l.height), R = C.x, M = C.y, k = l.x - c.x, v = l.y - c.y, P = k + R, H = v + M, L = P * i - H * a, D = P * a + H * i, O = c.x + L, W = c.y + D;
241
+ return {
242
+ x: w - O,
243
+ y: m - W
244
+ };
245
+ }
246
+ /**
247
+ * Get local coordinates of the fixed point for a given handle
248
+ * @private
249
+ */
250
+ static _getFixedLocalCoords(t, e, o) {
251
+ const n = { x: 0, y: 0 };
252
+ switch (t) {
253
+ case "top-left":
254
+ n.x = e, n.y = o;
255
+ break;
256
+ case "top-right":
257
+ n.x = 0, n.y = o;
258
+ break;
259
+ case "bottom-left":
260
+ n.x = e, n.y = 0;
261
+ break;
262
+ case "bottom-right":
263
+ n.x = 0, n.y = 0;
264
+ break;
265
+ case "middle-left":
266
+ n.x = e, n.y = o / 2;
267
+ break;
268
+ case "middle-right":
269
+ n.x = 0, n.y = o / 2;
270
+ break;
271
+ case "middle-top":
272
+ n.x = e / 2, n.y = o;
273
+ break;
274
+ case "middle-bottom":
275
+ n.x = e / 2, n.y = 0;
276
+ break;
277
+ }
278
+ return n;
279
+ }
280
+ }
281
+ class Oe {
282
+ constructor() {
283
+ this.hooks = {
284
+ beforeResize: [],
285
+ afterResize: [],
286
+ beforePositionUpdate: [],
287
+ afterPositionUpdate: []
288
+ };
289
+ }
290
+ /**
291
+ * Add a hook at a specific stage
292
+ * @param stage - 'beforeResize', 'afterResize', 'beforePositionUpdate', 'afterPositionUpdate'
293
+ * @param fn - Hook function
294
+ */
295
+ addHook(t, e) {
296
+ if (!this.hooks[t])
297
+ throw new Error(`Invalid hook stage: ${t}`);
298
+ this.hooks[t].push(e);
299
+ }
300
+ /**
301
+ * Remove a hook
302
+ */
303
+ removeHook(t, e) {
304
+ if (!this.hooks[t]) return;
305
+ const o = this.hooks[t].indexOf(e);
306
+ o !== -1 && this.hooks[t].splice(o, 1);
307
+ }
308
+ /**
309
+ * Clear all hooks at a stage
310
+ */
311
+ clearHooks(t) {
312
+ this.hooks[t] && (this.hooks[t] = []);
313
+ }
314
+ /**
315
+ * Clear all hooks
316
+ */
317
+ clearAllHooks() {
318
+ Object.keys(this.hooks).forEach((t) => {
319
+ this.hooks[t] = [];
320
+ });
321
+ }
322
+ /**
323
+ * Run hooks at a specific stage
324
+ */
325
+ runHooks(t, e) {
326
+ return this.hooks[t] ? this.hooks[t].reduce((o, n) => {
327
+ const s = n(o);
328
+ return s !== void 0 ? s : o;
329
+ }, e) : e;
330
+ }
331
+ /**
332
+ * Execute resize with hooks
333
+ */
334
+ executeResize(t) {
335
+ const e = this.runHooks("beforeResize", t);
336
+ let o = Ve.calculateResize({
337
+ ...e,
338
+ snapSystem: t.snapSystem
339
+ });
340
+ return o.handle = t.handle, o.startData = t.startData, o = this.runHooks("afterResize", o), o;
341
+ }
342
+ /**
343
+ * Execute position update with hooks
344
+ * Used for drag operations
345
+ */
346
+ executePositionUpdate(t, e) {
347
+ const o = this.runHooks("beforePositionUpdate", { position: t, element: e });
348
+ return this.runHooks("afterPositionUpdate", o).position;
349
+ }
350
+ }
351
+ const Ze = new Oe();
352
+ class Je {
353
+ constructor(t = null) {
354
+ this.state = "IDLE", this.context = {
355
+ mode: null,
356
+ startX: 0,
357
+ startY: 0,
358
+ startData: null,
359
+ activeHandle: null,
360
+ element: null
361
+ }, this.snapHandler = null, this.resizePipeline = t;
362
+ }
363
+ /**
364
+ * Set snap handler for position/dimension snapping
365
+ */
366
+ setSnapHandler(t) {
367
+ this.snapHandler = t;
368
+ }
369
+ /**
370
+ * Set resize pipeline for hooking into resize operations
371
+ */
372
+ setResizePipeline(t) {
373
+ this.resizePipeline = t;
374
+ }
375
+ /**
376
+ * Get current state
377
+ */
378
+ getState() {
379
+ return this.state;
380
+ }
381
+ /**
382
+ * Get current context
383
+ */
384
+ getContext() {
385
+ return this.context;
386
+ }
387
+ /**
388
+ * Check if currently interacting
389
+ */
390
+ isInteracting() {
391
+ return this.state !== "IDLE";
392
+ }
393
+ /**
394
+ * Start drag operation
395
+ */
396
+ startDrag(t, e, o, n = null) {
397
+ this.state = "DRAGGING";
398
+ const s = t.getTransformStartData();
399
+ let i = s;
400
+ if (n === "crop-image-drag" && t.transformData) {
401
+ const a = t.transformData;
402
+ i = {
403
+ ...s,
404
+ cropX: a.cropX,
405
+ cropY: a.cropY,
406
+ cropWidth: a.cropWidth,
407
+ cropHeight: a.cropHeight
408
+ };
409
+ }
410
+ this.context = {
411
+ mode: "drag",
412
+ dragMode: n,
413
+ // 'crop-image-drag' for moving image within crop, null for normal drag
414
+ startX: e,
415
+ startY: o,
416
+ startData: i,
417
+ activeHandle: null,
418
+ element: t
419
+ };
420
+ }
421
+ /**
422
+ * Get drag mode
423
+ */
424
+ getDragMode() {
425
+ return this.context.dragMode;
426
+ }
427
+ /**
428
+ * Reset drag start position (for incremental dragging)
429
+ */
430
+ resetDragStart(t, e) {
431
+ this.state === "DRAGGING" && (this.context.startX = t, this.context.startY = e);
432
+ }
433
+ /**
434
+ * Update drag operation
435
+ * Returns updated position { x, y }
436
+ */
437
+ updateDrag(t, e, o = null) {
438
+ if (this.state !== "DRAGGING")
439
+ throw new Error("Cannot update drag: not in DRAGGING state");
440
+ const n = t - this.context.startX, s = e - this.context.startY, i = this.context.startData;
441
+ let a = i.x + n, r = i.y + s;
442
+ if (this.snapHandler && this.snapHandler.snapPosition && this.context.element) {
443
+ const l = this.snapHandler.snapPosition({ x: a, y: r }, this.context.element, o);
444
+ a = l.x, r = l.y;
445
+ }
446
+ return { x: a, y: r };
447
+ }
448
+ /**
449
+ * Start resize operation
450
+ */
451
+ startResize(t, e, o, n, s, i) {
452
+ this.state = "RESIZING";
453
+ const a = t.getTransformStartData(), r = t.clone();
454
+ (e.anchor === "top-left" || e.anchor === "top-right" || e.anchor === "bottom-left" || e.anchor === "bottom-right") && a.lineCount !== void 0 && r.transformType === "custom" && (r._lockedLineCount = a.lineCount), this.context = {
455
+ mode: "resize",
456
+ startX: o,
457
+ startY: n,
458
+ startData: {
459
+ ...a,
460
+ visualX: i.x,
461
+ visualY: i.y,
462
+ visualWidth: i.width,
463
+ visualHeight: i.height,
464
+ bboxX: s.x,
465
+ bboxY: s.y
466
+ },
467
+ activeHandle: e,
468
+ element: r
469
+ // Use the cloned element with lock already set
470
+ };
471
+ }
472
+ /**
473
+ * Update resize operation
474
+ * Returns { dx, dy } deltas
475
+ */
476
+ updateResize(t, e) {
477
+ if (this.state !== "RESIZING")
478
+ throw new Error("Cannot update resize: not in RESIZING state");
479
+ const o = t - this.context.startX, n = e - this.context.startY;
480
+ return { dx: o, dy: n };
481
+ }
482
+ /**
483
+ * Adjust the resize start position to account for snap corrections
484
+ * This prevents dx/dy from accumulating when a snap is locked
485
+ */
486
+ adjustResizeStart(t, e) {
487
+ this.state === "RESIZING" && (this.context.startX += t, this.context.startY += e);
488
+ }
489
+ /**
490
+ * Get resize context for ResizeHandler
491
+ */
492
+ getResizeContext() {
493
+ if (this.state !== "RESIZING")
494
+ throw new Error("Cannot get resize context: not in RESIZING state");
495
+ return {
496
+ element: this.context.element,
497
+ handle: this.context.activeHandle,
498
+ startData: this.context.startData
499
+ };
500
+ }
501
+ /**
502
+ * Update the element in resize context (to preserve state between frames)
503
+ * This is critical for ImageElement crop mode where _lastValidCropState needs to persist
504
+ */
505
+ updateResizeElement(t) {
506
+ this.state === "RESIZING" && (this.context.element = t);
507
+ }
508
+ /**
509
+ * Reset resize start data to current element state (for unfreezing)
510
+ * This updates startData to use the current element dimensions, preventing jumps
511
+ */
512
+ resetResizeStartData(t, e, o) {
513
+ if (this.state !== "RESIZING")
514
+ return;
515
+ const n = t.getBoundingBox(), s = t.getVisualBoundingBox(), i = t.getTransformStartData();
516
+ this.context.startX = e, this.context.startY = o, this.context.startData = {
517
+ ...i,
518
+ visualX: s.x,
519
+ visualY: s.y,
520
+ visualWidth: s.width,
521
+ visualHeight: s.height,
522
+ bboxX: n.x,
523
+ bboxY: n.y
524
+ };
525
+ }
526
+ /**
527
+ * Start rotation operation
528
+ */
529
+ startRotate(t, e, o, n, s) {
530
+ this.state = "ROTATING", this.context = {
531
+ mode: "rotate",
532
+ startX: e,
533
+ startY: o,
534
+ startData: {
535
+ startAngle: n,
536
+ currentRotation: s,
537
+ elementData: t.getTransformStartData()
538
+ },
539
+ activeHandle: null,
540
+ element: t
541
+ };
542
+ }
543
+ /**
544
+ * Update rotation operation
545
+ * Returns updated rotation angle in degrees
546
+ */
547
+ updateRotate(t, e, o, n) {
548
+ if (this.state !== "ROTATING")
549
+ throw new Error("Cannot update rotate: not in ROTATING state");
550
+ const s = this.context.startData, a = (ce(o, n, t, e) - s.startAngle) * 180 / Math.PI;
551
+ let r = s.currentRotation - a;
552
+ return r = (r % 360 + 360) % 360, r > 180 && (r -= 360), this.snapHandler && this.snapHandler.snapRotation && this.context.element ? this.snapHandler.snapRotation(r, this.context.element) : r;
553
+ }
554
+ /**
555
+ * Get rotation context
556
+ */
557
+ getRotationContext() {
558
+ if (this.state !== "ROTATING")
559
+ throw new Error("Cannot get rotation context: not in ROTATING state");
560
+ const t = this.context.startData;
561
+ return {
562
+ startAngle: t.startAngle,
563
+ currentRotation: t.currentRotation
564
+ };
565
+ }
566
+ /**
567
+ * Start a two-finger pinch gesture on `element`. Captures the
568
+ * gesture's start centroid / distance / angle and the element's
569
+ * full transform snapshot — `updatePinch` then derives the
570
+ * gesture's scale + rotation delta against this snapshot every
571
+ * frame. PinchHandler consumes both via `getPinchContext()` to
572
+ * produce the next element.
573
+ */
574
+ startPinch(t, e, o) {
575
+ this.state = "PINCHING";
576
+ const n = {
577
+ x: (e.x + o.x) / 2,
578
+ y: (e.y + o.y) / 2
579
+ }, s = o.x - e.x, i = o.y - e.y, a = Math.hypot(s, i), r = Math.atan2(i, s), l = {
580
+ elementData: t.getTransformStartData(),
581
+ startCentroid: n,
582
+ startDistance: a,
583
+ startAngle: r
584
+ };
585
+ this.context = {
586
+ mode: "pinch",
587
+ startX: n.x,
588
+ startY: n.y,
589
+ startData: l,
590
+ // see InteractionContextData typing
591
+ activeHandle: null,
592
+ element: t.clone()
593
+ };
594
+ }
595
+ /**
596
+ * Update pinch gesture for the latest two-pointer positions.
597
+ * Returns the raw gesture state (scale ratio, rotation delta,
598
+ * centroid positions). The caller (PinchHandler) is responsible
599
+ * for applying this to the element — keeping the math out of the
600
+ * state machine mirrors how `updateResize` returns `{dx, dy}` and
601
+ * lets `ResizeHandler` do the geometry.
602
+ *
603
+ * Scale is clamped to a small floor so degenerate single-finger
604
+ * coalesces (e.g. fingers landing on the same pixel for one
605
+ * frame) don't produce zero / NaN dimensions.
606
+ */
607
+ updatePinch(t, e) {
608
+ if (this.state !== "PINCHING")
609
+ throw new Error("Cannot update pinch: not in PINCHING state");
610
+ const o = this.context.startData, n = {
611
+ x: (t.x + e.x) / 2,
612
+ y: (t.y + e.y) / 2
613
+ }, s = e.x - t.x, i = e.y - t.y, a = Math.hypot(s, i), r = Math.atan2(i, s), l = Math.max(o.startDistance, 1e-4), h = a / l, c = Math.max(h, 0.05), d = r - o.startAngle;
614
+ return {
615
+ scale: c,
616
+ rotationDelta: d,
617
+ startCentroid: o.startCentroid,
618
+ currentCentroid: n
619
+ };
620
+ }
621
+ /**
622
+ * Get the element snapshot + pinch start data (used by
623
+ * PinchHandler.calculatePinch). Mirrors `getResizeContext`.
624
+ */
625
+ getPinchContext() {
626
+ if (this.state !== "PINCHING")
627
+ throw new Error("Cannot get pinch context: not in PINCHING state");
628
+ const t = this.context.startData;
629
+ return {
630
+ element: this.context.element,
631
+ startData: t.elementData
632
+ };
633
+ }
634
+ /**
635
+ * End current interaction
636
+ */
637
+ end() {
638
+ this.state = "IDLE", this.context = {
639
+ mode: null,
640
+ startX: 0,
641
+ startY: 0,
642
+ startData: null,
643
+ activeHandle: null,
644
+ element: null
645
+ };
646
+ }
647
+ /**
648
+ * Cancel current interaction (no commit)
649
+ */
650
+ cancel() {
651
+ this.end();
652
+ }
653
+ /**
654
+ * Get active handle (for resize mode)
655
+ */
656
+ getActiveHandle() {
657
+ return this.context.activeHandle;
658
+ }
659
+ /**
660
+ * Get mode ('drag', 'resize', 'rotate', or null)
661
+ */
662
+ getMode() {
663
+ return this.context.mode;
664
+ }
665
+ }
666
+ class bt {
667
+ /**
668
+ * Resolve rotation anchor with clear precedence:
669
+ * 1. Transform's getRotationAnchor() if overridden
670
+ * 2. Center of visual bounding box (default)
671
+ */
672
+ static resolve(t) {
673
+ return t.getRotationAnchor();
674
+ }
675
+ /**
676
+ * Get all snap-able anchor points for an element
677
+ * Used for alignment snapping feature
678
+ *
679
+ * Returns 9 standard anchor points plus the rotation anchor:
680
+ * - 4 corners (top-left, top-right, bottom-left, bottom-right)
681
+ * - 4 edge midpoints (top, right, bottom, left)
682
+ * - 1 center
683
+ * - 1 rotation anchor (may be same as center)
684
+ */
685
+ static getSnapAnchors(t) {
686
+ const e = t.getVisualBoundingBox(), o = {
687
+ // Corners
688
+ topLeft: { x: e.x, y: e.y },
689
+ topRight: { x: e.x + e.width, y: e.y },
690
+ bottomLeft: { x: e.x, y: e.y + e.height },
691
+ bottomRight: { x: e.x + e.width, y: e.y + e.height },
692
+ // Edge midpoints
693
+ topCenter: { x: e.x + e.width / 2, y: e.y },
694
+ rightCenter: { x: e.x + e.width, y: e.y + e.height / 2 },
695
+ bottomCenter: { x: e.x + e.width / 2, y: e.y + e.height },
696
+ leftCenter: { x: e.x, y: e.y + e.height / 2 },
697
+ // Center
698
+ center: { x: e.x + e.width / 2, y: e.y + e.height / 2 },
699
+ // Rotation anchor (may differ from center for some transforms)
700
+ rotation: this.resolve(t)
701
+ };
702
+ if (t.rotation !== 0) {
703
+ const n = o.rotation, s = ht.toRadiansInverse(t.rotation);
704
+ Object.keys(o).forEach((i) => {
705
+ if (i === "rotation") return;
706
+ const a = o[i], r = a.x - n.x, l = a.y - n.y, h = r * Math.cos(s) - l * Math.sin(s), c = r * Math.sin(s) + l * Math.cos(s);
707
+ o[i] = {
708
+ x: n.x + h,
709
+ y: n.y + c
710
+ };
711
+ });
712
+ }
713
+ return o;
714
+ }
715
+ /**
716
+ * Get edge lines for an element (for edge-to-edge snapping)
717
+ *
718
+ * Returns 4 lines representing the element's edges:
719
+ * - top: left to right
720
+ * - right: top to bottom
721
+ * - bottom: right to left
722
+ * - left: bottom to top
723
+ */
724
+ static getEdgeLines(t) {
725
+ const e = this.getSnapAnchors(t);
726
+ return {
727
+ top: {
728
+ start: e.topLeft,
729
+ end: e.topRight
730
+ },
731
+ right: {
732
+ start: e.topRight,
733
+ end: e.bottomRight
734
+ },
735
+ bottom: {
736
+ start: e.bottomRight,
737
+ end: e.bottomLeft
738
+ },
739
+ left: {
740
+ start: e.bottomLeft,
741
+ end: e.topLeft
742
+ }
743
+ };
744
+ }
745
+ /**
746
+ * Get all anchor types as array (for iteration)
747
+ */
748
+ static getAnchorTypes() {
749
+ return [
750
+ "topLeft",
751
+ "topCenter",
752
+ "topRight",
753
+ "rightCenter",
754
+ "bottomRight",
755
+ "bottomCenter",
756
+ "bottomLeft",
757
+ "leftCenter",
758
+ "center",
759
+ "rotation"
760
+ ];
761
+ }
762
+ /**
763
+ * Check if an element has a custom rotation anchor
764
+ * (different from center of visual bbox)
765
+ */
766
+ static hasCustomRotationAnchor(t) {
767
+ const e = t.getVisualBoundingBox(), o = {
768
+ x: e.x + e.width / 2,
769
+ y: e.y + e.height / 2
770
+ }, n = this.resolve(t), s = 1e-3;
771
+ return Math.abs(n.x - o.x) > s || Math.abs(n.y - o.y) > s;
772
+ }
773
+ }
774
+ class Ut {
775
+ constructor(t) {
776
+ this.cellSize = t, this.cells = /* @__PURE__ */ new Map(), this.idIndex = /* @__PURE__ */ new Map();
777
+ }
778
+ /** Clear all entries */
779
+ clear() {
780
+ this.cells.clear(), this.idIndex.clear();
781
+ }
782
+ /** Insert a point into the grid */
783
+ insert(t) {
784
+ const e = this.getCellKey(t.x, t.y);
785
+ let o = this.cells.get(e);
786
+ o || (o = /* @__PURE__ */ new Set(), this.cells.set(e, o)), o.add(t);
787
+ let n = this.idIndex.get(t.id);
788
+ n || (n = /* @__PURE__ */ new Set(), this.idIndex.set(t.id, n)), n.add(e);
789
+ }
790
+ /** Remove all items with matching id */
791
+ removeById(t) {
792
+ const e = this.idIndex.get(t);
793
+ if (e) {
794
+ for (const o of e) {
795
+ const n = this.cells.get(o);
796
+ if (n) {
797
+ for (const s of n)
798
+ s.id === t && n.delete(s);
799
+ n.size === 0 && this.cells.delete(o);
800
+ }
801
+ }
802
+ this.idIndex.delete(t);
803
+ }
804
+ }
805
+ /**
806
+ * Query all items within `radius` of point (x, y), excluding given id.
807
+ *
808
+ * Checks all cells that overlap the axis-aligned bounding box
809
+ * [x - radius, x + radius] x [y - radius, y + radius].
810
+ * Each candidate is then distance-checked against the radius.
811
+ */
812
+ queryNear(t, e, o, n) {
813
+ const s = [], i = Math.floor((t - o) / this.cellSize), a = Math.floor((t + o) / this.cellSize), r = Math.floor((e - o) / this.cellSize), l = Math.floor((e + o) / this.cellSize);
814
+ for (let h = i; h <= a; h++)
815
+ for (let c = r; c <= l; c++) {
816
+ const d = `${h},${c}`, u = this.cells.get(d);
817
+ if (u)
818
+ for (const p of u) {
819
+ if (n !== void 0 && p.id === n) continue;
820
+ const b = p.x - t, g = p.y - e;
821
+ b * b + g * g <= o * o && s.push(p);
822
+ }
823
+ }
824
+ return s;
825
+ }
826
+ /**
827
+ * Query all items whose coordinate on `axis` is within `radius` of the
828
+ * given value, excluding a given id. Unlike `queryNear` (Euclidean),
829
+ * this checks only one axis -- essential for alignment snapping where
830
+ * two anchors may be far apart on one axis but perfectly aligned on
831
+ * the other.
832
+ *
833
+ * For axis='x': finds items where |item.x - value| <= radius
834
+ * For axis='y': finds items where |item.y - value| <= radius
835
+ *
836
+ * Scans all cells in the band along the target axis.
837
+ */
838
+ queryNearAxis(t, e, o, n) {
839
+ const s = [];
840
+ if (t === "x") {
841
+ const i = Math.floor((e - o) / this.cellSize), a = Math.floor((e + o) / this.cellSize);
842
+ for (const [r, l] of this.cells) {
843
+ const h = r.indexOf(","), c = parseInt(r.substring(0, h), 10);
844
+ if (!(c < i || c > a))
845
+ for (const d of l)
846
+ n !== void 0 && d.id === n || Math.abs(d.x - e) <= o && s.push(d);
847
+ }
848
+ } else {
849
+ const i = Math.floor((e - o) / this.cellSize), a = Math.floor((e + o) / this.cellSize);
850
+ for (const [r, l] of this.cells) {
851
+ const h = r.indexOf(","), c = parseInt(r.substring(h + 1), 10);
852
+ if (!(c < i || c > a))
853
+ for (const d of l)
854
+ n !== void 0 && d.id === n || Math.abs(d.y - e) <= o && s.push(d);
855
+ }
856
+ }
857
+ return s;
858
+ }
859
+ /** Rebuild from a full list of items (clears existing data first) */
860
+ rebuild(t) {
861
+ this.clear();
862
+ for (const e of t)
863
+ this.insert(e);
864
+ }
865
+ /** Get the cell key for a given world-space coordinate */
866
+ getCellKey(t, e) {
867
+ return `${Math.floor(t / this.cellSize)},${Math.floor(e / this.cellSize)}`;
868
+ }
869
+ }
870
+ function lt(x) {
871
+ const t = x.transformData;
872
+ if (t && "width" in t && typeof t.width == "number")
873
+ return t.width;
874
+ }
875
+ function mt(x) {
876
+ const t = x.transformData;
877
+ if (t && "height" in t && typeof t.height == "number")
878
+ return t.height;
879
+ }
880
+ function vt(x, t) {
881
+ const e = x.transformData;
882
+ return e && "width" in e ? (e.width = t, !0) : !1;
883
+ }
884
+ function kt(x, t) {
885
+ const e = x.transformData;
886
+ return e && "height" in e ? (e.height = t, !0) : !1;
887
+ }
888
+ class Qe {
889
+ constructor(t = {}) {
890
+ this._extraSnapRects = [], this.snapThreshold = t.snapThreshold || ot.canvas.snapThreshold, this.enabled = t.enabled !== !1, this.showGuides = t.showGuides !== !1, this.guides = [], this.activeSnaps = {
891
+ x: null,
892
+ // { snapTo: number, anchorKey: string, frozenFontSize: number, frozenWidth: number, frozenPosition: {x, y}, isPositionBasedSnap: boolean }
893
+ y: null
894
+ // { snapTo: number, anchorKey: string, frozenFontSize: number, frozenWidth: number, frozenPosition: {x, y}, isPositionBasedSnap: boolean }
895
+ }, this.stickyThreshold = t.stickyThreshold || 15, this._spatialGrid = new Ut(Math.max(this.snapThreshold * 2, 16)), this._anchorMetadata = /* @__PURE__ */ new Map();
896
+ }
897
+ /**
898
+ * Enable or disable snapping
899
+ */
900
+ setEnabled(t) {
901
+ this.enabled = t;
902
+ }
903
+ /**
904
+ * Set snap threshold in pixels
905
+ */
906
+ setSnapThreshold(t) {
907
+ this.snapThreshold = t, this._spatialGrid = new Ut(Math.max(t * 2, 16));
908
+ }
909
+ /**
910
+ * Get current snap guides for rendering
911
+ */
912
+ getGuides() {
913
+ return this.guides;
914
+ }
915
+ /**
916
+ * Clear all snap guides
917
+ */
918
+ clearGuides() {
919
+ this.guides = [];
920
+ }
921
+ /**
922
+ * Clear sticky snap state
923
+ */
924
+ clearSnapState() {
925
+ this.activeSnaps.x = null, this.activeSnaps.y = null;
926
+ }
927
+ /**
928
+ * ADR-0060: replace the set of extra snap rects (focused-piece bounds
929
+ * etc.). Pass `[]` to clear. Effective on the next snap operation.
930
+ */
931
+ setExtraSnapRects(t) {
932
+ this._extraSnapRects = t;
933
+ }
934
+ /**
935
+ * Populate the spatial grid with anchor points from the given elements.
936
+ * Each element produces ~9 anchor points (corners, edge midpoints, center).
937
+ * The grid enables O(1) amortized lookup of nearby anchors by axis.
938
+ *
939
+ * Also folds in `_extraSnapRects` (set via `setExtraSnapRects`) so
940
+ * a focused piece in spread mode contributes its corners / edges /
941
+ * center as snap targets even though there's no real BaseElement.
942
+ */
943
+ _populateSpatialGrid(t) {
944
+ this._spatialGrid.clear(), this._anchorMetadata.clear();
945
+ for (const e of t) {
946
+ const o = bt.getSnapAnchors(e);
947
+ for (const [n, s] of Object.entries(o)) {
948
+ const i = `${e.id}:${n}`, a = {
949
+ x: s.x,
950
+ y: s.y,
951
+ id: i
952
+ };
953
+ this._spatialGrid.insert(a), this._anchorMetadata.set(i, {
954
+ elementId: e.id,
955
+ anchorKey: n,
956
+ point: s
957
+ });
958
+ }
959
+ }
960
+ for (const e of this._extraSnapRects) {
961
+ const o = e.x + e.width / 2, n = e.y + e.height / 2, s = e.x + e.width, i = e.y + e.height, a = {
962
+ topLeft: { x: e.x, y: e.y },
963
+ topRight: { x: s, y: e.y },
964
+ bottomLeft: { x: e.x, y: i },
965
+ bottomRight: { x: s, y: i },
966
+ topCenter: { x: o, y: e.y },
967
+ rightCenter: { x: s, y: n },
968
+ bottomCenter: { x: o, y: i },
969
+ leftCenter: { x: e.x, y: n },
970
+ center: { x: o, y: n }
971
+ // No `rotation` anchor — extras are un-rotated by definition.
972
+ }, r = `__snap-rect:${e.id}`;
973
+ for (const [l, h] of Object.entries(a)) {
974
+ const c = `${r}:${l}`;
975
+ this._spatialGrid.insert({
976
+ x: h.x,
977
+ y: h.y,
978
+ id: c
979
+ }), this._anchorMetadata.set(c, {
980
+ elementId: r,
981
+ anchorKey: l,
982
+ point: h
983
+ });
984
+ }
985
+ }
986
+ }
987
+ /**
988
+ * Snap a position during drag operation
989
+ */
990
+ snapPosition(t, e, o) {
991
+ if (!this.enabled || !o)
992
+ return this.clearGuides(), t;
993
+ const n = e.clone();
994
+ n.x = t.x, n.y = t.y;
995
+ const s = bt.getSnapAnchors(n), i = o.filter((y) => y.id !== e.id);
996
+ this._populateSpatialGrid(i);
997
+ let a = null, r = null, l = 1 / 0, h = 1 / 0;
998
+ const c = [], d = [], u = [];
999
+ for (const [y, S] of Object.entries(s)) {
1000
+ const T = this._spatialGrid.queryNearAxis("x", S.x, this.snapThreshold);
1001
+ for (const m of T) {
1002
+ const C = this._anchorMetadata.get(m.id);
1003
+ if (!C) continue;
1004
+ const R = C.point, M = C.anchorKey, k = Math.abs(S.x - R.x);
1005
+ if (k < this.snapThreshold) {
1006
+ const v = {
1007
+ snapTo: R.x,
1008
+ offset: S.x - t.x,
1009
+ // How far anchor is from element position
1010
+ distance: k,
1011
+ guide: {
1012
+ type: "vertical",
1013
+ x: R.x,
1014
+ y1: Math.min(S.y, R.y) - 50,
1015
+ y2: Math.max(S.y, R.y) + 50,
1016
+ movingKey: y,
1017
+ otherKey: M
1018
+ }
1019
+ };
1020
+ c.push(v), k < l && (l = k, a = v);
1021
+ }
1022
+ }
1023
+ const w = this._spatialGrid.queryNearAxis("y", S.y, this.snapThreshold);
1024
+ for (const m of w) {
1025
+ const C = this._anchorMetadata.get(m.id);
1026
+ if (!C) continue;
1027
+ const R = C.point, M = C.anchorKey, k = Math.abs(S.y - R.y);
1028
+ if (k < this.snapThreshold) {
1029
+ const v = {
1030
+ snapTo: R.y,
1031
+ offset: S.y - t.y,
1032
+ distance: k,
1033
+ guide: {
1034
+ type: "horizontal",
1035
+ y: R.y,
1036
+ x1: Math.min(S.x, R.x) - 50,
1037
+ x2: Math.max(S.x, R.x) + 50,
1038
+ movingKey: y,
1039
+ otherKey: M
1040
+ }
1041
+ };
1042
+ d.push(v), k < h && (h = k, r = v);
1043
+ }
1044
+ }
1045
+ }
1046
+ let p = t.x, b = t.y;
1047
+ a && (p = a.snapTo - a.offset), r && (b = r.snapTo - r.offset);
1048
+ const g = /* @__PURE__ */ new Map(), f = /* @__PURE__ */ new Map();
1049
+ for (const y of c) {
1050
+ const S = g.get(y.snapTo);
1051
+ (!S || y.distance < S.distance) && g.set(y.snapTo, y);
1052
+ }
1053
+ for (const y of d) {
1054
+ const S = f.get(y.snapTo);
1055
+ (!S || y.distance < S.distance) && f.set(y.snapTo, y);
1056
+ }
1057
+ for (const y of g.values())
1058
+ u.push(y.guide);
1059
+ for (const y of f.values())
1060
+ u.push(y.guide);
1061
+ return this.guides = u, { x: p, y: b };
1062
+ }
1063
+ /**
1064
+ * Calculate what the frozen font size should be at the snap point
1065
+ * This ensures consistent font size regardless of which side of threshold you enter from
1066
+ */
1067
+ _calculateFrozenFontSizeAtSnap(t, e, o, n, s) {
1068
+ if (!s) return t.fontSize ?? 0;
1069
+ const i = n != null && n.anchor ? String(n.anchor) : "";
1070
+ if (!(n && (i.includes("top") || i.includes("bottom"))))
1071
+ return s.fontSize ?? 0;
1072
+ const r = lt(t) ?? t.width ?? s.width;
1073
+ let l = r;
1074
+ i.includes("right") && e !== 0 ? l = r + e : i.includes("left") && e !== 0 && (l = r - e);
1075
+ const h = l / s.width;
1076
+ return (s.fontSize ?? 0) * h;
1077
+ }
1078
+ /**
1079
+ * Check if this will be a position-based snap (vs dimension-based)
1080
+ * Position-based snaps adjust position to align edges when dimensions can't be modified
1081
+ * Used for text elements where height is derived, not settable
1082
+ */
1083
+ _isPositionBasedSnap(t, e) {
1084
+ return e === "x" ? !1 : e === "y" ? !(mt(t) !== void 0 || t.height !== void 0 && typeof t.height == "number") : !1;
1085
+ }
1086
+ /**
1087
+ * Determine which snap keys to use based on the resize handle
1088
+ */
1089
+ _getSnapKeysForHandle(t) {
1090
+ return !t || !t.anchor ? ["topLeft", "topRight", "bottomLeft", "bottomRight"] : t.anchor === "top-left" || t.anchor === "topLeft" ? ["topLeft"] : t.anchor === "top-right" || t.anchor === "topRight" ? ["topRight"] : t.anchor === "bottom-left" || t.anchor === "bottomLeft" ? ["bottomLeft"] : t.anchor === "bottom-right" || t.anchor === "bottomRight" ? ["bottomRight"] : t.anchor.includes("left") ? ["topLeft", "bottomLeft"] : t.anchor.includes("right") ? ["topRight", "bottomRight"] : t.anchor.includes("top") ? ["topLeft", "topRight"] : t.anchor.includes("bottom") ? ["bottomLeft", "bottomRight"] : [];
1091
+ }
1092
+ /**
1093
+ * Get all possible target snap keys (corners only, no center points)
1094
+ */
1095
+ _getTargetSnapKeys() {
1096
+ return ["topLeft", "topRight", "bottomLeft", "bottomRight"];
1097
+ }
1098
+ /**
1099
+ * Find all X-axis snaps within threshold.
1100
+ * Uses the spatial grid (pre-populated via _populateSpatialGrid) for
1101
+ * O(1) amortized lookup instead of scanning all other elements.
1102
+ */
1103
+ _findXSnaps(t, e, o, n) {
1104
+ const s = [], i = new Set(n);
1105
+ for (const a of o) {
1106
+ const r = t[a];
1107
+ if (!r) continue;
1108
+ const l = this._spatialGrid.queryNearAxis("x", r.x, this.snapThreshold);
1109
+ for (const h of l) {
1110
+ const c = this._anchorMetadata.get(h.id);
1111
+ if (!c || !i.has(c.anchorKey)) continue;
1112
+ const d = c.point, u = c.anchorKey, p = Math.abs(r.x - d.x);
1113
+ p < this.snapThreshold && s.push({
1114
+ distance: p,
1115
+ snapTo: d.x,
1116
+ currentValue: r.x,
1117
+ guide: {
1118
+ type: "vertical",
1119
+ x: d.x,
1120
+ y1: Math.min(r.y, d.y) - 50,
1121
+ y2: Math.max(r.y, d.y) + 50,
1122
+ movingKey: a,
1123
+ otherKey: u
1124
+ }
1125
+ });
1126
+ }
1127
+ }
1128
+ return s;
1129
+ }
1130
+ /**
1131
+ * Find all Y-axis snaps within threshold.
1132
+ * Uses the spatial grid (pre-populated via _populateSpatialGrid) for
1133
+ * O(1) amortized lookup instead of scanning all other elements.
1134
+ */
1135
+ _findYSnaps(t, e, o, n) {
1136
+ const s = [], i = new Set(n);
1137
+ for (const a of o) {
1138
+ const r = t[a];
1139
+ if (!r) continue;
1140
+ const l = this._spatialGrid.queryNearAxis("y", r.y, this.snapThreshold);
1141
+ for (const h of l) {
1142
+ const c = this._anchorMetadata.get(h.id);
1143
+ if (!c || !i.has(c.anchorKey)) continue;
1144
+ const d = c.point, u = c.anchorKey, p = Math.abs(r.y - d.y);
1145
+ p < this.snapThreshold && s.push({
1146
+ distance: p,
1147
+ snapTo: d.y,
1148
+ currentValue: r.y,
1149
+ guide: {
1150
+ type: "horizontal",
1151
+ y: d.y,
1152
+ x1: Math.min(r.x, d.x) - 50,
1153
+ x2: Math.max(r.x, d.x) + 50,
1154
+ movingKey: a,
1155
+ otherKey: u
1156
+ }
1157
+ });
1158
+ }
1159
+ }
1160
+ return s;
1161
+ }
1162
+ /**
1163
+ * Sort snaps by distance and prioritize based on handle anchor
1164
+ */
1165
+ _prioritizeSnaps(t, e, o) {
1166
+ if (!e || !e.anchor) {
1167
+ t.sort((n, s) => n.distance - s.distance);
1168
+ return;
1169
+ }
1170
+ o === "x" ? e.anchor.includes("left") ? t.sort((n, s) => {
1171
+ const i = n.guide.movingKey.includes("left") ? -1e3 : 0, a = s.guide.movingKey.includes("left") ? -1e3 : 0;
1172
+ return n.distance + i - (s.distance + a);
1173
+ }) : e.anchor.includes("right") ? t.sort((n, s) => {
1174
+ const i = n.guide.movingKey.includes("right") ? -1e3 : 0, a = s.guide.movingKey.includes("right") ? -1e3 : 0;
1175
+ return n.distance + i - (s.distance + a);
1176
+ }) : t.sort((n, s) => n.distance - s.distance) : o === "y" && (e.anchor.includes("top") ? t.sort((n, s) => {
1177
+ const i = n.guide.movingKey.includes("top") ? -1e3 : 0, a = s.guide.movingKey.includes("top") ? -1e3 : 0;
1178
+ return n.distance + i - (s.distance + a);
1179
+ }) : e.anchor.includes("bottom") ? t.sort((n, s) => {
1180
+ const i = n.guide.movingKey.includes("bottom") ? -1e3 : 0, a = s.guide.movingKey.includes("bottom") ? -1e3 : 0;
1181
+ return n.distance + i - (s.distance + a);
1182
+ }) : t.sort((n, s) => n.distance - s.distance));
1183
+ }
1184
+ /**
1185
+ * Create a guide visualization for an active snap
1186
+ */
1187
+ _createGuideForSnap(t, e, o, n, s) {
1188
+ const i = o[e];
1189
+ if (!i) return null;
1190
+ if (s === "x") {
1191
+ let a = 1 / 0, r = -1 / 0;
1192
+ for (const l of Object.values(o))
1193
+ Math.abs(l.x - t.snapTo) < 5 && (a = Math.min(a, l.y), r = Math.max(r, l.y));
1194
+ for (const l of n) {
1195
+ const h = bt.getSnapAnchors(l);
1196
+ for (const c of Object.values(h))
1197
+ Math.abs(c.x - t.snapTo) < 5 && (a = Math.min(a, c.y), r = Math.max(r, c.y));
1198
+ }
1199
+ return (!isFinite(a) || !isFinite(r)) && (a = i.y, r = i.y), {
1200
+ snapTo: t.snapTo,
1201
+ currentValue: i.x,
1202
+ distance: 0,
1203
+ guide: {
1204
+ type: "vertical",
1205
+ x: t.snapTo,
1206
+ y1: a - 50,
1207
+ y2: r + 50,
1208
+ movingKey: e,
1209
+ otherKey: "snap"
1210
+ }
1211
+ };
1212
+ } else if (s === "y") {
1213
+ let a = 1 / 0, r = -1 / 0;
1214
+ for (const l of Object.values(o))
1215
+ Math.abs(l.y - t.snapTo) < 5 && (a = Math.min(a, l.x), r = Math.max(r, l.x));
1216
+ for (const l of n) {
1217
+ const h = bt.getSnapAnchors(l);
1218
+ for (const c of Object.values(h))
1219
+ Math.abs(c.y - t.snapTo) < 5 && (a = Math.min(a, c.x), r = Math.max(r, c.x));
1220
+ }
1221
+ return (!isFinite(a) || !isFinite(r)) && (a = i.x, r = i.x), {
1222
+ snapTo: t.snapTo,
1223
+ currentValue: i.y,
1224
+ distance: 0,
1225
+ guide: {
1226
+ type: "horizontal",
1227
+ y: t.snapTo,
1228
+ x1: a - 50,
1229
+ x2: r + 50,
1230
+ movingKey: e,
1231
+ otherKey: "snap"
1232
+ }
1233
+ };
1234
+ }
1235
+ return null;
1236
+ }
1237
+ /**
1238
+ * Calculate aspect-ratio-aware sticky threshold for a given axis
1239
+ * Wide elements move faster horizontally, tall elements move faster vertically
1240
+ * Adjust threshold to compensate for this velocity difference
1241
+ */
1242
+ _getAdjustedStickyThreshold(t, e) {
1243
+ const o = e.getVisualBoundingBox(), n = o.width / o.height;
1244
+ let s = this.stickyThreshold;
1245
+ return t === "x" ? n > 1 && (s = this.stickyThreshold / Math.sqrt(n)) : t === "y" && n < 1 && (s = this.stickyThreshold * Math.sqrt(n)), s;
1246
+ }
1247
+ /**
1248
+ * Calculate what dimensions should be when edge reaches snap point
1249
+ * This ensures frozen dimensions match the geometry at the snap position
1250
+ *
1251
+ * The key insight: When a snap is first detected (edge within 5px of guideline),
1252
+ * we need to freeze dimensions at what they WILL BE when the edge reaches the snap point,
1253
+ * not what they ARE at the moment of detection. This prevents visual "jumps" during
1254
+ * corner resize because position compensation expects dimensions to match the snap point.
1255
+ *
1256
+ * @param element - The element being resized
1257
+ * @param axis - 'x' or 'y' axis of the snap
1258
+ * @param snapDelta - Distance to move to reach snap point (can be positive or negative)
1259
+ * @param anchorKey - Which anchor is snapping (e.g., 'topRight', 'bottomLeft', etc.)
1260
+ * @param handle - The resize handle being dragged
1261
+ * @returns {Object} { frozenWidth, frozenHeight } - Dimensions at snap point
1262
+ */
1263
+ _calculateFrozenDimensionsAtSnap(t, e, o, n, s) {
1264
+ const i = lt(t) ?? t.width ?? 0, a = mt(t) ?? t.height ?? 0;
1265
+ let r = i, l = a;
1266
+ const h = t.transformType === "image", c = h ? t.imageAspectRatio || (i && a ? i / a : null) : null, d = n.toLowerCase();
1267
+ return e === "x" ? d.includes("right") ? (r = i + o, h && c && (l = r / c)) : d.includes("left") && (r = i - o, h && c && (l = r / c)) : e === "y" && (d.includes("bottom") ? (l = a + o, h && c && (r = l * c)) : d.includes("top") && (l = a - o, h && c && (r = l * c))), { frozenWidth: r, frozenHeight: l };
1268
+ }
1269
+ /**
1270
+ * Process sticky snapping for a single axis
1271
+ * @returns {Object} { snapDelta, bestSnap }
1272
+ */
1273
+ _processStickySnap(t, e, o, n, s, i, a) {
1274
+ const r = this.activeSnaps[t];
1275
+ let l = 0, h = null;
1276
+ if (r) {
1277
+ const c = e[r.anchorKey];
1278
+ if (c) {
1279
+ const d = t === "x" ? c.x : c.y, u = Math.abs(d - r.snapTo), p = this._getAdjustedStickyThreshold(t, n);
1280
+ if (u < p) {
1281
+ if (l = r.snapTo - d, !(t === "y" && r.isPositionBasedSnap)) {
1282
+ const g = this._calculateFrozenFontSizeAtSnap(
1283
+ n,
1284
+ t === "x" ? l : 0,
1285
+ t === "y" ? l : 0,
1286
+ s,
1287
+ i
1288
+ );
1289
+ g !== r.frozenFontSize && (r.frozenFontSize = g);
1290
+ }
1291
+ h = this._createGuideForSnap(r, r.anchorKey, e, a, t);
1292
+ } else {
1293
+ const b = r;
1294
+ if (this.activeSnaps[t] = null, o.length > 0 && o[0].distance < this.snapThreshold) {
1295
+ h = o[0], l = h.snapTo - h.currentValue;
1296
+ const g = b && b.anchorKey === h.guide.movingKey && Math.abs(b.snapTo - h.snapTo) < 1, f = g ? b.frozenFontSize : this._calculateFrozenFontSizeAtSnap(
1297
+ n,
1298
+ t === "x" ? l : 0,
1299
+ t === "y" ? l : 0,
1300
+ s,
1301
+ i
1302
+ ), y = this._isPositionBasedSnap(n, t);
1303
+ let S;
1304
+ g ? S = b.frozenPosition : y ? S = {
1305
+ x: t === "x" ? n.x + l : n.x,
1306
+ y: t === "y" ? n.y + l : n.y
1307
+ } : S = { x: n.x, y: n.y };
1308
+ const T = this._calculateFrozenDimensionsAtSnap(
1309
+ n,
1310
+ t,
1311
+ l,
1312
+ h.guide.movingKey,
1313
+ s
1314
+ );
1315
+ this.activeSnaps[t] = {
1316
+ snapTo: h.snapTo,
1317
+ anchorKey: h.guide.movingKey,
1318
+ frozenFontSize: f,
1319
+ frozenWidth: T.frozenWidth,
1320
+ frozenHeight: T.frozenHeight,
1321
+ frozenPosition: S,
1322
+ isPositionBasedSnap: y
1323
+ };
1324
+ }
1325
+ }
1326
+ } else
1327
+ this.activeSnaps[t] = null;
1328
+ } else if (o.length > 0 && o[0].distance < this.snapThreshold) {
1329
+ h = o[0], l = h.snapTo - h.currentValue;
1330
+ const c = this.activeSnaps[t], d = c && c.anchorKey === h.guide.movingKey && Math.abs(c.snapTo - h.snapTo) < 1, u = d ? c.frozenFontSize : this._calculateFrozenFontSizeAtSnap(
1331
+ n,
1332
+ t === "x" ? l : 0,
1333
+ t === "y" ? l : 0,
1334
+ s,
1335
+ i
1336
+ ), p = this._isPositionBasedSnap(n, t);
1337
+ let b;
1338
+ d ? b = c.frozenPosition : p ? b = {
1339
+ x: t === "x" ? n.x + l : n.x,
1340
+ y: t === "y" ? n.y + l : n.y
1341
+ } : b = { x: n.x, y: n.y };
1342
+ const g = this._calculateFrozenDimensionsAtSnap(
1343
+ n,
1344
+ t,
1345
+ l,
1346
+ h.guide.movingKey,
1347
+ s
1348
+ );
1349
+ this.activeSnaps[t] = {
1350
+ snapTo: h.snapTo,
1351
+ anchorKey: h.guide.movingKey,
1352
+ frozenFontSize: u,
1353
+ frozenWidth: g.frozenWidth,
1354
+ frozenHeight: g.frozenHeight,
1355
+ frozenPosition: b,
1356
+ isPositionBasedSnap: p
1357
+ };
1358
+ }
1359
+ return { snapDelta: l, bestSnap: h };
1360
+ }
1361
+ /**
1362
+ * Adjust element for right edge resize
1363
+ */
1364
+ _adjustElementForRightResize(t, e, o) {
1365
+ const n = t.getVisualBoundingBox(), s = t.transformType === "image";
1366
+ if (!s) {
1367
+ const i = lt(t);
1368
+ i !== void 0 ? vt(t, i + e) : t.width !== void 0 && (t.width += e);
1369
+ }
1370
+ if (o.anchor === "middle-right") {
1371
+ const i = ht.toRadiansInverse(t.rotation), a = Math.cos(i), r = Math.sin(i);
1372
+ t.x += e / 2 * a, t.y += e / 2 * r;
1373
+ } else if (!s) {
1374
+ const a = t.getVisualBoundingBox().x - n.x;
1375
+ a !== 0 && (t.x -= a);
1376
+ }
1377
+ }
1378
+ /**
1379
+ * Adjust element for left edge resize
1380
+ */
1381
+ _adjustElementForLeftResize(t, e, o) {
1382
+ const n = t.getVisualBoundingBox(), s = t.transformType === "image";
1383
+ if (!s) {
1384
+ const i = lt(t);
1385
+ i !== void 0 ? vt(t, i - e) : t.width !== void 0 && (t.width -= e);
1386
+ }
1387
+ if (o.anchor === "middle-left") {
1388
+ const i = ht.toRadiansInverse(t.rotation), a = Math.cos(i), r = Math.sin(i);
1389
+ t.x += e / 2 * a, t.y += e / 2 * r;
1390
+ } else if (t.x += e, !s) {
1391
+ const i = t.getVisualBoundingBox(), a = i.x + i.width - (n.x + n.width);
1392
+ a !== 0 && (t.x -= a);
1393
+ }
1394
+ }
1395
+ /**
1396
+ * Adjust element for bottom edge resize
1397
+ */
1398
+ _adjustElementForBottomResize(t, e, o) {
1399
+ const n = t.getVisualBoundingBox(), s = t.transformType === "image";
1400
+ if (!s) {
1401
+ const r = mt(t);
1402
+ r !== void 0 ? kt(t, r + e) : t.height !== void 0 && (t.height += e);
1403
+ }
1404
+ const i = t.getVisualBoundingBox(), a = Math.abs(i.height - n.height) > 0.01;
1405
+ if (o.anchor === "middle-bottom") {
1406
+ const r = ht.toRadiansInverse(t.rotation), l = Math.cos(r), h = Math.sin(r);
1407
+ t.x -= e / 2 * h, t.y += e / 2 * l;
1408
+ } else if (!s && a) {
1409
+ const r = i.y - n.y;
1410
+ r !== 0 && (t.y -= r);
1411
+ } else s || (t.y += e);
1412
+ }
1413
+ /**
1414
+ * Adjust element for top edge resize
1415
+ */
1416
+ _adjustElementForTopResize(t, e, o) {
1417
+ const n = t.getVisualBoundingBox(), s = t.transformType === "image";
1418
+ if (!s) {
1419
+ const r = mt(t);
1420
+ r !== void 0 ? kt(t, r - e) : t.height !== void 0 && (t.height -= e);
1421
+ }
1422
+ const i = t.getVisualBoundingBox(), a = Math.abs(i.height - n.height) > 0.01;
1423
+ if (o.anchor === "middle-top") {
1424
+ const r = ht.toRadiansInverse(t.rotation), l = Math.cos(r), h = Math.sin(r);
1425
+ t.x -= e / 2 * h, t.y += e / 2 * l;
1426
+ } else {
1427
+ const r = !s && a;
1428
+ if (t.y += e, r) {
1429
+ const l = t.getVisualBoundingBox(), h = l.y + l.height - (n.y + n.height);
1430
+ h !== 0 && (t.y -= h);
1431
+ }
1432
+ }
1433
+ }
1434
+ /**
1435
+ * Snap dimensions during resize operation
1436
+ *
1437
+ * @param {object} result - Resize result AFTER ResizeHandler processes it
1438
+ * @param {array} allElements - All elements in the scene
1439
+ * @param {object} startData - Original data from when resize started (for font size preservation)
1440
+ * @returns {object} Modified resize result with adjusted element
1441
+ */
1442
+ snapDimensions(t, e, o = null) {
1443
+ var m, C, R, M, k;
1444
+ if (!this.enabled || !e)
1445
+ return this.clearGuides(), t;
1446
+ const { element: n } = t, s = t.handle;
1447
+ let i;
1448
+ const a = n.transformType === "image", r = this.activeSnaps.x !== null || this.activeSnaps.y !== null;
1449
+ if (a && r && t.isFrozen && t.unfrozenDimensions) {
1450
+ const v = n.clone();
1451
+ if (v.transformData) {
1452
+ const P = n.getVisualBoundingBox(), H = P.width, L = P.height, D = (o == null ? void 0 : o.width) ?? n.width ?? 0, O = (o == null ? void 0 : o.height) ?? n.height ?? 0, W = (o == null ? void 0 : o.visualWidth) ?? H, V = (o == null ? void 0 : o.visualHeight) ?? L, A = t.unfrozenDimensions.width / W, $ = t.unfrozenDimensions.height / V;
1453
+ vt(v, D * A), kt(v, O * $);
1454
+ }
1455
+ i = bt.getSnapAnchors(v);
1456
+ } else
1457
+ i = bt.getSnapAnchors(n);
1458
+ const l = e.filter((v) => v.id !== n.id);
1459
+ if (l.length === 0)
1460
+ return this.clearGuides(), t;
1461
+ this._populateSpatialGrid(l);
1462
+ const h = this._getSnapKeysForHandle(s), c = this._getTargetSnapKeys(), d = this._findXSnaps(i, l, h, c), u = this._findYSnaps(i, l, h, c);
1463
+ this._prioritizeSnaps(d, s, "x"), this._prioritizeSnaps(u, s, "y");
1464
+ const p = this._processStickySnap("x", i, d, n, s, o, l), b = this._processStickySnap("y", i, u, n, s, o, l), g = p.snapDelta, f = b.snapDelta, y = p.bestSnap, S = b.bestSnap, T = [];
1465
+ y && T.push(y.guide), S && T.push(S.guide), this.guides = T, T.length > 0;
1466
+ const w = s && (s.anchor === "top-left" || s.anchor === "top-right" || s.anchor === "bottom-left" || s.anchor === "bottom-right");
1467
+ if (a && w && (g !== 0 || f !== 0))
1468
+ return t;
1469
+ if (g !== 0 || f !== 0) {
1470
+ const v = n.clone(), P = this.activeSnaps.x || this.activeSnaps.y, H = (P == null ? void 0 : P.frozenFontSize) ?? n.fontSize ?? 0, L = (P == null ? void 0 : P.frozenWidth) ?? lt(n) ?? n.width ?? 0, D = (P == null ? void 0 : P.frozenHeight) ?? mt(n) ?? n.height ?? 0, O = (P == null ? void 0 : P.frozenPosition) ?? { x: n.x, y: n.y }, W = v.isCropping && !String(s.anchor).startsWith("crop-");
1471
+ if (!W)
1472
+ if (s && s.anchor) {
1473
+ if (g !== 0) {
1474
+ const E = (m = this.activeSnaps.x) == null ? void 0 : m.anchorKey, I = E == null ? void 0 : E.toLowerCase();
1475
+ I && I.includes("right") ? this._adjustElementForRightResize(v, g, s) : I && I.includes("left") ? this._adjustElementForLeftResize(v, g, s) : v.x += g;
1476
+ }
1477
+ if (f !== 0) {
1478
+ const E = (C = this.activeSnaps.y) == null ? void 0 : C.anchorKey, I = E == null ? void 0 : E.toLowerCase();
1479
+ I && I.includes("bottom") ? this._adjustElementForBottomResize(v, f, s) : I && I.includes("top") ? this._adjustElementForTopResize(v, f, s) : v.y += f;
1480
+ }
1481
+ } else
1482
+ v.x += g, v.y += f;
1483
+ v.fontSize !== void 0 && (v.fontSize = H);
1484
+ const V = v.transformType === "image", A = this.activeSnaps.x !== null || this.activeSnaps.y !== null;
1485
+ if (V && A) {
1486
+ if (lt(v) !== void 0) {
1487
+ const E = this.activeSnaps.x || this.activeSnaps.y;
1488
+ let I = L, _ = D;
1489
+ E && (I = E.frozenWidth ?? I, _ = E.frozenHeight ?? _);
1490
+ const B = lt(n), F = mt(n), G = v.imageAspectRatio || (B && F ? B / F : null);
1491
+ G && G !== 0 && (_ = I / G), vt(v, I), kt(v, _), E && (E.frozenWidth = I, E.frozenHeight = _);
1492
+ }
1493
+ } else
1494
+ (((R = this.activeSnaps.y) == null ? void 0 : R.isPositionBasedSnap) || !1) && (lt(v) !== void 0 ? vt(v, L) : v.width !== void 0 && (v.width = L));
1495
+ const $ = s && (s.anchor === "middle-left" || s.anchor === "middle-right"), j = s && (s.anchor === "middle-top" || s.anchor === "middle-bottom"), J = ((M = this.activeSnaps.x) == null ? void 0 : M.isPositionBasedSnap) || !1, X = ((k = this.activeSnaps.y) == null ? void 0 : k.isPositionBasedSnap) || !1;
1496
+ return (g === 0 || J) && !$ && !j && !W && (v.x = O.x), (f === 0 || X) && !j && !$ && !W && (v.y = O.y), {
1497
+ ...t,
1498
+ element: v,
1499
+ snapAdjustment: { dx: g, dy: f },
1500
+ // Flag to indicate we're in sticky snap mode (used to freeze font size)
1501
+ isInStickySnap: !0
1502
+ };
1503
+ }
1504
+ return t;
1505
+ }
1506
+ /**
1507
+ * Snap rotation (for future implementation)
1508
+ */
1509
+ snapRotation(t, e, o) {
1510
+ if (!this.enabled) return t;
1511
+ const n = [0, 45, 90, 135, 180, 225, 270, 315, 360], s = 5;
1512
+ for (const i of n)
1513
+ if (de(t, i, s))
1514
+ return i;
1515
+ return t;
1516
+ }
1517
+ }
1518
+ class tn {
1519
+ constructor(t = {}) {
1520
+ this.lastSnapTarget = null, this.detectionThreshold = t.detectionThreshold || 100, this.minDistance = t.minDistance || 10, this.enabled = t.enabled !== !1, this.showLabels = t.showLabels !== !1, this.indicators = [];
1521
+ }
1522
+ /**
1523
+ * Enable or disable spacing detection
1524
+ */
1525
+ setEnabled(t) {
1526
+ this.enabled = t;
1527
+ }
1528
+ /**
1529
+ * Set detection threshold in pixels
1530
+ */
1531
+ setDetectionThreshold(t) {
1532
+ this.detectionThreshold = t;
1533
+ }
1534
+ /**
1535
+ * Get current spacing indicators for rendering
1536
+ */
1537
+ getIndicators() {
1538
+ return this.indicators;
1539
+ }
1540
+ /**
1541
+ * Clear all spacing indicators
1542
+ */
1543
+ clearIndicators() {
1544
+ this.indicators = [], this.lastSnapTarget = null;
1545
+ }
1546
+ /**
1547
+ * Snap position to match consistent spacing patterns
1548
+ * Returns adjusted position if spacing snap is detected, otherwise returns original position
1549
+ */
1550
+ snapToSpacing(t, e, o) {
1551
+ if (!this.enabled || !o)
1552
+ return t;
1553
+ const n = Gt, s = o.filter((g) => g.id !== e.id);
1554
+ if (s.length === 0)
1555
+ return t;
1556
+ const i = e.clone();
1557
+ i.x = t.x, i.y = t.y;
1558
+ const a = i.getVisualBoundingBox(), r = a.x - t.x, l = a.y - t.y;
1559
+ let h = t.x, c = t.y, d = !1, u = !1;
1560
+ const p = s.filter((g) => {
1561
+ const f = g.getVisualBoundingBox();
1562
+ return !(a.x + a.width < f.x || a.x > f.x + f.width);
1563
+ });
1564
+ if (p.sort((g, f) => {
1565
+ const y = g.getVisualBoundingBox(), S = f.getVisualBoundingBox();
1566
+ return y.y - S.y;
1567
+ }), p.length >= 1) {
1568
+ let g = null, f = null;
1569
+ for (const T of p) {
1570
+ const w = T.getVisualBoundingBox();
1571
+ w.y + w.height <= a.y ? (!g || w.y > g.getVisualBoundingBox().y) && (g = T) : w.y >= a.y + a.height && (!f || w.y < f.getVisualBoundingBox().y) && (f = T);
1572
+ }
1573
+ const y = [];
1574
+ for (let T = 0; T < p.length - 1; T++) {
1575
+ const w = p[T].getVisualBoundingBox(), C = p[T + 1].getVisualBoundingBox().y - (w.y + w.height);
1576
+ y.push(C);
1577
+ }
1578
+ let S = null;
1579
+ if (g) {
1580
+ const T = g.getVisualBoundingBox(), w = a.y - (T.y + T.height);
1581
+ for (const m of y)
1582
+ if (Math.abs(w - m) <= n) {
1583
+ S = m;
1584
+ break;
1585
+ }
1586
+ }
1587
+ if (!S && f) {
1588
+ const w = f.getVisualBoundingBox().y - (a.y + a.height);
1589
+ for (const m of y)
1590
+ if (Math.abs(w - m) <= n) {
1591
+ S = m;
1592
+ break;
1593
+ }
1594
+ }
1595
+ if (S !== null && S > 0) {
1596
+ if (g) {
1597
+ const T = g.getVisualBoundingBox(), w = a.y - (T.y + T.height);
1598
+ Math.abs(w - S) <= n && (c = T.y + T.height + S - l, u = !0);
1599
+ }
1600
+ if (!u && f) {
1601
+ const T = f.getVisualBoundingBox(), w = T.y - (a.y + a.height);
1602
+ Math.abs(w - S) <= n && (c = T.y - a.height - S - l, u = !0);
1603
+ }
1604
+ }
1605
+ }
1606
+ const b = s.filter((g) => {
1607
+ const f = g.getVisualBoundingBox();
1608
+ return !(a.y + a.height < f.y || a.y > f.y + f.height);
1609
+ });
1610
+ if (b.sort((g, f) => {
1611
+ const y = g.getVisualBoundingBox(), S = f.getVisualBoundingBox();
1612
+ return y.x - S.x;
1613
+ }), b.length >= 1) {
1614
+ let g = null, f = null;
1615
+ for (const T of b) {
1616
+ const w = T.getVisualBoundingBox();
1617
+ w.x + w.width <= a.x ? (!g || w.x > g.getVisualBoundingBox().x) && (g = T) : w.x >= a.x + a.width && (!f || w.x < f.getVisualBoundingBox().x) && (f = T);
1618
+ }
1619
+ const y = [];
1620
+ for (let T = 0; T < b.length - 1; T++) {
1621
+ const w = b[T].getVisualBoundingBox(), C = b[T + 1].getVisualBoundingBox().x - (w.x + w.width);
1622
+ y.push(C);
1623
+ }
1624
+ let S = null;
1625
+ if (g) {
1626
+ const T = g.getVisualBoundingBox(), w = a.x - (T.x + T.width);
1627
+ for (const m of y)
1628
+ if (Math.abs(w - m) <= n) {
1629
+ S = m;
1630
+ break;
1631
+ }
1632
+ }
1633
+ if (!S && f) {
1634
+ const w = f.getVisualBoundingBox().x - (a.x + a.width);
1635
+ for (const m of y)
1636
+ if (Math.abs(w - m) <= n) {
1637
+ S = m;
1638
+ break;
1639
+ }
1640
+ }
1641
+ if (S !== null && S > 0) {
1642
+ if (g) {
1643
+ const T = g.getVisualBoundingBox(), w = a.x - (T.x + T.width);
1644
+ Math.abs(w - S) <= n && (h = T.x + T.width + S - r, d = !0);
1645
+ }
1646
+ if (!d && f) {
1647
+ const T = f.getVisualBoundingBox(), w = T.x - (a.x + a.width);
1648
+ Math.abs(w - S) <= n && (h = T.x - a.width - S - r, d = !0);
1649
+ }
1650
+ }
1651
+ }
1652
+ return this.lastSnapTarget = d || u ? { x: h, y: c } : null, { x: h, y: c };
1653
+ }
1654
+ /**
1655
+ * Detect spacing between a moving element and other elements
1656
+ * Returns spacing indicators for the closest elements in each direction
1657
+ * Also detects consistent spacing patterns (e.g., evenly spaced rows/columns)
1658
+ * @param showAllMeasurements - When true (Alt key held), shows all spacing measurements, not just to moving element
1659
+ */
1660
+ detectSpacing(t, e, o = !1) {
1661
+ if (!this.enabled || !e)
1662
+ return this.clearIndicators(), [];
1663
+ const n = e.filter((c) => c.id !== t.id);
1664
+ if (n.length === 0)
1665
+ return this.clearIndicators(), [];
1666
+ const s = t.getVisualBoundingBox(), i = [], a = [], r = [], l = [];
1667
+ for (const c of n) {
1668
+ const d = c.getVisualBoundingBox(), u = s.x - (d.x + d.width), p = d.x - (s.x + s.width), b = s.y - (d.y + d.height), g = d.y - (s.y + s.height), f = !(s.y + s.height < d.y || s.y > d.y + d.height), y = !(s.x + s.width < d.x || s.x > d.x + d.width);
1669
+ u >= this.minDistance && u < this.detectionThreshold && f && i.push({ element: c, distance: u }), p >= this.minDistance && p < this.detectionThreshold && f && a.push({ element: c, distance: p }), b >= this.minDistance && b < this.detectionThreshold && y && r.push({ element: c, distance: b }), g >= this.minDistance && g < this.detectionThreshold && y && l.push({ element: c, distance: g });
1670
+ }
1671
+ i.sort((c, d) => c.distance - d.distance), a.sort((c, d) => c.distance - d.distance), r.sort((c, d) => c.distance - d.distance), l.sort((c, d) => c.distance - d.distance);
1672
+ const h = [];
1673
+ if (o) {
1674
+ const c = this.detectAllMeasurements(t, e);
1675
+ h.push(...c);
1676
+ } else {
1677
+ if (i.length > 0) {
1678
+ const c = this.detectConsistentSpacing(t, s, i, "left", n);
1679
+ h.push(...c);
1680
+ }
1681
+ if (a.length > 0) {
1682
+ const c = this.detectConsistentSpacing(
1683
+ t,
1684
+ s,
1685
+ a,
1686
+ "right",
1687
+ n
1688
+ );
1689
+ h.push(...c);
1690
+ }
1691
+ if (r.length > 0) {
1692
+ const c = this.detectConsistentSpacing(t, s, r, "top", n);
1693
+ h.push(...c);
1694
+ }
1695
+ if (l.length > 0) {
1696
+ const c = this.detectConsistentSpacing(
1697
+ t,
1698
+ s,
1699
+ l,
1700
+ "bottom",
1701
+ n
1702
+ );
1703
+ h.push(...c);
1704
+ }
1705
+ }
1706
+ return this.indicators = h, h;
1707
+ }
1708
+ /**
1709
+ * Detect spacing from hovered element to ALL nearby elements (when Alt key is held)
1710
+ * Shows measurements from the hovered element to other elements within detection threshold
1711
+ */
1712
+ detectAllMeasurements(t, e) {
1713
+ const o = [], n = t.getVisualBoundingBox(), s = e.filter((l) => l.id !== t.id), i = this.detectionThreshold * 3;
1714
+ let a = 0, r = 0;
1715
+ for (const l of s) {
1716
+ const h = l.getVisualBoundingBox();
1717
+ if (Math.min(
1718
+ Math.abs(n.x - (h.x + h.width)),
1719
+ // left
1720
+ Math.abs(h.x - (n.x + n.width)),
1721
+ // right
1722
+ Math.abs(n.y - (h.y + h.height)),
1723
+ // top
1724
+ Math.abs(h.y - (n.y + n.height))
1725
+ // bottom
1726
+ ) > i)
1727
+ continue;
1728
+ let d = 0, u = !1, p = 0, b = 0, g = 0;
1729
+ const y = -30 * (a % 2 === 0 ? 1 : -1);
1730
+ h.x >= n.x + n.width ? (d = h.x - (n.x + n.width), d >= this.minDistance && (u = !0, p = n.x + n.width, b = h.x, g = (n.y + n.height / 2 + h.y + h.height / 2) / 2 + y)) : n.x >= h.x + h.width && (d = n.x - (h.x + h.width), d >= this.minDistance && (u = !0, p = h.x + h.width, b = n.x, g = (n.y + n.height / 2 + h.y + h.height / 2) / 2 + y)), u && (o.push({
1731
+ type: "horizontal",
1732
+ distance: Math.round(d),
1733
+ x1: p,
1734
+ x2: b,
1735
+ y: g,
1736
+ labelX: (p + b) / 2,
1737
+ labelY: g,
1738
+ isAltHover: !0
1739
+ }), a++);
1740
+ let S = 0, T = !1, w = 0, m = 0, C = 0;
1741
+ const M = 30 * (r % 2 === 0 ? 1 : -1);
1742
+ h.y >= n.y + n.height ? (S = h.y - (n.y + n.height), S >= this.minDistance && (T = !0, w = (n.x + n.width / 2 + h.x + h.width / 2) / 2 + M, m = n.y + n.height, C = h.y)) : n.y >= h.y + h.height && (S = n.y - (h.y + h.height), S >= this.minDistance && (T = !0, w = (n.x + n.width / 2 + h.x + h.width / 2) / 2 + M, m = h.y + h.height, C = n.y)), T && (o.push({
1743
+ type: "vertical",
1744
+ distance: Math.round(S),
1745
+ x: w,
1746
+ y1: m,
1747
+ y2: C,
1748
+ labelX: w,
1749
+ labelY: (m + C) / 2,
1750
+ isAltHover: !0
1751
+ }), r++);
1752
+ }
1753
+ return o;
1754
+ }
1755
+ /**
1756
+ * Detect consistent spacing patterns (e.g., evenly spaced rows/columns)
1757
+ * and create indicators for all matching spacings
1758
+ */
1759
+ detectConsistentSpacing(t, e, o, n, s) {
1760
+ const i = [], r = o[0], l = r.distance, h = r.element.getVisualBoundingBox(), c = this.checkIfWouldSnap(e, l, n, s);
1761
+ if (this.addIndicatorForDirection(i, e, h, l, n, c), n === "left" || n === "right") {
1762
+ const d = s.filter((u) => {
1763
+ if (u.id === t.id) return !1;
1764
+ const p = u.getVisualBoundingBox();
1765
+ return !(e.y + e.height < p.y || e.y > p.y + p.height);
1766
+ });
1767
+ if (d.sort((u, p) => {
1768
+ const b = u.getVisualBoundingBox(), g = p.getVisualBoundingBox();
1769
+ return b.x - g.x;
1770
+ }), n === "left")
1771
+ for (let u = 0; u < d.length - 1; u++) {
1772
+ const p = d[u], b = d[u + 1], g = p.getVisualBoundingBox(), f = b.getVisualBoundingBox(), y = f.x - (g.x + g.width);
1773
+ if (Math.abs(y - l) <= 16 && y >= this.minDistance) {
1774
+ const S = Math.max(g.y, f.y), T = Math.min(g.y + g.height, f.y + f.height), w = (S + T) / 2;
1775
+ i.push({
1776
+ type: "horizontal",
1777
+ distance: Math.round(y),
1778
+ x1: g.x + g.width,
1779
+ x2: f.x,
1780
+ y: w,
1781
+ labelX: (g.x + g.width + f.x) / 2,
1782
+ labelY: w
1783
+ });
1784
+ }
1785
+ }
1786
+ else
1787
+ for (let u = 0; u < d.length - 1; u++) {
1788
+ const p = d[u], b = d[u + 1], g = p.getVisualBoundingBox(), f = b.getVisualBoundingBox(), y = f.x - (g.x + g.width);
1789
+ if (Math.abs(y - l) <= 16 && y >= this.minDistance) {
1790
+ const S = Math.max(g.y, f.y), T = Math.min(g.y + g.height, f.y + f.height), w = (S + T) / 2;
1791
+ i.push({
1792
+ type: "horizontal",
1793
+ distance: Math.round(y),
1794
+ x1: g.x + g.width,
1795
+ x2: f.x,
1796
+ y: w,
1797
+ labelX: (g.x + g.width + f.x) / 2,
1798
+ labelY: w
1799
+ });
1800
+ }
1801
+ }
1802
+ } else {
1803
+ const d = s.filter((u) => {
1804
+ if (u.id === t.id) return !1;
1805
+ const p = u.getVisualBoundingBox();
1806
+ return !(e.x + e.width < p.x || e.x > p.x + p.width);
1807
+ });
1808
+ if (d.sort((u, p) => {
1809
+ const b = u.getVisualBoundingBox(), g = p.getVisualBoundingBox();
1810
+ return b.y - g.y;
1811
+ }), n === "top")
1812
+ for (let u = 0; u < d.length - 1; u++) {
1813
+ const p = d[u], b = d[u + 1], g = p.getVisualBoundingBox(), f = b.getVisualBoundingBox(), y = f.y - (g.y + g.height);
1814
+ if (Math.abs(y - l) <= 16 && y >= this.minDistance) {
1815
+ const S = Math.max(g.x, f.x), T = Math.min(g.x + g.width, f.x + f.width), w = (S + T) / 2;
1816
+ i.push({
1817
+ type: "vertical",
1818
+ distance: Math.round(y),
1819
+ x: w,
1820
+ y1: g.y + g.height,
1821
+ y2: f.y,
1822
+ labelX: w,
1823
+ labelY: (g.y + g.height + f.y) / 2
1824
+ });
1825
+ }
1826
+ }
1827
+ else
1828
+ for (let u = 0; u < d.length - 1; u++) {
1829
+ const p = d[u], b = d[u + 1], g = p.getVisualBoundingBox(), f = b.getVisualBoundingBox(), y = f.y - (g.y + g.height);
1830
+ if (Math.abs(y - l) <= 16 && y >= this.minDistance) {
1831
+ const S = Math.max(g.x, f.x), T = Math.min(g.x + g.width, f.x + f.width), w = (S + T) / 2;
1832
+ i.push({
1833
+ type: "vertical",
1834
+ distance: Math.round(y),
1835
+ x: w,
1836
+ y1: g.y + g.height,
1837
+ y2: f.y,
1838
+ labelX: w,
1839
+ labelY: (g.y + g.height + f.y) / 2
1840
+ });
1841
+ }
1842
+ }
1843
+ }
1844
+ return i;
1845
+ }
1846
+ /**
1847
+ * Check if spacing would snap to a pattern
1848
+ */
1849
+ checkIfWouldSnap(t, e, o, n) {
1850
+ const s = Gt;
1851
+ if (o === "top" || o === "bottom") {
1852
+ const i = n.filter((a) => {
1853
+ const r = a.getVisualBoundingBox();
1854
+ return !(t.x + t.width < r.x || t.x > r.x + r.width);
1855
+ });
1856
+ if (i.length >= 1) {
1857
+ i.sort((r, l) => {
1858
+ const h = r.getVisualBoundingBox(), c = l.getVisualBoundingBox();
1859
+ return h.y - c.y;
1860
+ });
1861
+ const a = [];
1862
+ for (let r = 0; r < i.length - 1; r++) {
1863
+ const l = i[r].getVisualBoundingBox(), c = i[r + 1].getVisualBoundingBox().y - (l.y + l.height);
1864
+ a.push(c);
1865
+ }
1866
+ if (a.length > 0) {
1867
+ const r = a.reduce((c, d) => c + d, 0) / a.length;
1868
+ return Math.abs(e - r) <= s;
1869
+ }
1870
+ }
1871
+ return !1;
1872
+ } else {
1873
+ const i = n.filter((a) => {
1874
+ const r = a.getVisualBoundingBox();
1875
+ return !(t.y + t.height < r.y || t.y > r.y + r.height);
1876
+ });
1877
+ if (i.length >= 1) {
1878
+ i.sort((r, l) => {
1879
+ const h = r.getVisualBoundingBox(), c = l.getVisualBoundingBox();
1880
+ return h.x - c.x;
1881
+ });
1882
+ const a = [];
1883
+ for (let r = 0; r < i.length - 1; r++) {
1884
+ const l = i[r].getVisualBoundingBox(), c = i[r + 1].getVisualBoundingBox().x - (l.x + l.width);
1885
+ a.push(c);
1886
+ }
1887
+ if (a.length > 0) {
1888
+ const r = a.reduce((c, d) => c + d, 0) / a.length;
1889
+ return Math.abs(e - r) <= s;
1890
+ }
1891
+ }
1892
+ return !1;
1893
+ }
1894
+ }
1895
+ /**
1896
+ * Helper method to add a spacing indicator in a specific direction
1897
+ */
1898
+ addIndicatorForDirection(t, e, o, n, s, i = !1) {
1899
+ if (s === "left") {
1900
+ const a = Math.max(e.y, o.y), r = Math.min(e.y + e.height, o.y + o.height), l = (a + r) / 2;
1901
+ t.push({
1902
+ type: "horizontal",
1903
+ distance: Math.round(n),
1904
+ x1: o.x + o.width,
1905
+ x2: e.x,
1906
+ y: l,
1907
+ labelX: (o.x + o.width + e.x) / 2,
1908
+ labelY: l,
1909
+ isSnapTarget: i
1910
+ });
1911
+ } else if (s === "right") {
1912
+ const a = Math.max(e.y, o.y), r = Math.min(e.y + e.height, o.y + o.height), l = (a + r) / 2;
1913
+ t.push({
1914
+ type: "horizontal",
1915
+ distance: Math.round(n),
1916
+ x1: e.x + e.width,
1917
+ x2: o.x,
1918
+ y: l,
1919
+ labelX: (e.x + e.width + o.x) / 2,
1920
+ labelY: l,
1921
+ isSnapTarget: i
1922
+ });
1923
+ } else if (s === "top") {
1924
+ const a = Math.max(e.x, o.x), r = Math.min(e.x + e.width, o.x + o.width), l = (a + r) / 2;
1925
+ t.push({
1926
+ type: "vertical",
1927
+ distance: Math.round(n),
1928
+ x: l,
1929
+ y1: o.y + o.height,
1930
+ y2: e.y,
1931
+ labelX: l,
1932
+ labelY: (o.y + o.height + e.y) / 2,
1933
+ isSnapTarget: i
1934
+ });
1935
+ } else if (s === "bottom") {
1936
+ const a = Math.max(e.x, o.x), r = Math.min(e.x + e.width, o.x + o.width), l = (a + r) / 2;
1937
+ t.push({
1938
+ type: "vertical",
1939
+ distance: Math.round(n),
1940
+ x: l,
1941
+ y1: e.y + e.height,
1942
+ y2: o.y,
1943
+ labelX: l,
1944
+ labelY: (e.y + e.height + o.y) / 2,
1945
+ isSnapTarget: i
1946
+ });
1947
+ }
1948
+ }
1949
+ }
1950
+ const Lt = zt("ArtboardRenderer"), Ge = {
1951
+ showLabel: !0,
1952
+ showBorder: !0,
1953
+ borderColor: "",
1954
+ // '' = use getThemeArtboardBorderColor() at render time
1955
+ borderWidth: 1,
1956
+ labelFontSize: 14,
1957
+ labelColor: "",
1958
+ // '' = use getThemeArtboardLabelColor() at render time
1959
+ labelPadding: 8,
1960
+ selectedBorderColor: "",
1961
+ // '' = use getThemeAccentColor() at render time
1962
+ selectedBorderWidth: 2,
1963
+ showPreviewBackground: !0,
1964
+ // Show preview by default in editor
1965
+ borderRadius: 0
1966
+ // Default to no border radius (sharp corners)
1967
+ };
1968
+ class oe {
1969
+ constructor(t = {}) {
1970
+ this.options = { ...Ge, ...t };
1971
+ }
1972
+ /**
1973
+ * Render an artboard
1974
+ */
1975
+ render(t, e, o = !1) {
1976
+ t.save(), this.renderBackground(t, e), this.options.showBorder && this.renderBorder(t, e, o), this.options.showLabel && this.renderLabel(t, e, o), t.restore();
1977
+ }
1978
+ /**
1979
+ * Render artboard background
1980
+ * Optionally renders preview background color for t-shirt design preview
1981
+ * Preview background is NEVER exported - only shown in editor
1982
+ * Background is clipped to the artboard's clipShape
1983
+ */
1984
+ renderBackground(t, e) {
1985
+ t.save(), this.applyClipPath(t, e), this.options.showPreviewBackground && e.previewBackgroundColor && e.previewBackgroundColor !== "transparent" && (t.fillStyle = e.previewBackgroundColor, t.fillRect(e.x, e.y, e.width, e.height)), e.backgroundType === "texture" && e.backgroundTexture ? this.renderTextureBackground(t, e) : e.backgroundType === "transparent" || e.backgroundColor === "transparent" ? this.renderCheckerboard(t, e.x, e.y, e.width, e.height) : (t.fillStyle = e.backgroundColor, t.fillRect(e.x, e.y, e.width, e.height)), t.restore();
1986
+ }
1987
+ /**
1988
+ * Render texture background
1989
+ * Renders preset color as base layer, then texture on top
1990
+ */
1991
+ renderTextureBackground(t, e) {
1992
+ const o = Ht.getTexture(e.backgroundTexture), n = Ht.getTexturePresetColor(e.backgroundTexture);
1993
+ if (t.fillStyle = n, t.fillRect(e.x, e.y, e.width, e.height), !o || !o.complete) {
1994
+ Ht.loadTexture(e.backgroundTexture).catch((s) => {
1995
+ Lt.error("Failed to load texture:", s);
1996
+ });
1997
+ return;
1998
+ }
1999
+ t.save(), t.beginPath(), t.rect(e.x, e.y, e.width, e.height), t.clip(), t.drawImage(o, e.x, e.y, e.width, e.height), t.restore();
2000
+ }
2001
+ /**
2002
+ * Render a checkerboard pattern for transparent backgrounds
2003
+ */
2004
+ renderCheckerboard(t, e, o, n, s) {
2005
+ const a = "#ffffff", r = "#d0d0d0";
2006
+ t.save(), t.beginPath(), t.rect(e, o, n, s), t.clip(), t.fillStyle = a, t.fillRect(e, o, n, s), t.fillStyle = r;
2007
+ for (let l = 0; l < Math.ceil(s / 16); l++)
2008
+ for (let h = 0; h < Math.ceil(n / 16); h++)
2009
+ (l + h) % 2 === 1 && t.fillRect(e + h * 16, o + l * 16, 16, 16);
2010
+ t.restore();
2011
+ }
2012
+ /**
2013
+ * Render artboard border (respects clipShape and borderRadius option)
2014
+ */
2015
+ renderBorder(t, e, o) {
2016
+ const { x: n, y: s, width: i, height: a, clipShape: r } = e, l = this.options.borderColor || fe(), h = this.options.selectedBorderColor || tt();
2017
+ if (t.strokeStyle = o ? h : l, t.lineWidth = o ? this.options.selectedBorderWidth : this.options.borderWidth, t.beginPath(), !r || r === "rectangle")
2018
+ if (this.options.borderRadius > 0) {
2019
+ const c = Math.min(this.options.borderRadius, Math.min(i, a) / 2);
2020
+ this.roundedRect(t, n, s, i, a, c);
2021
+ } else
2022
+ t.rect(n, s, i, a);
2023
+ else if (r === "circle") {
2024
+ const c = n + i / 2, d = s + a / 2, u = Math.min(i, a) / 2;
2025
+ t.arc(c, d, u, 0, Math.PI * 2);
2026
+ } else if (typeof r == "object" && r.type === "rounded") {
2027
+ const c = Math.min(r.radius, Math.min(i, a) / 2);
2028
+ this.roundedRect(t, n, s, i, a, c);
2029
+ } else if (typeof r == "object" && r.type === "path")
2030
+ try {
2031
+ const c = new Path2D(r.d);
2032
+ t.save(), t.translate(n, s), t.stroke(c), t.restore();
2033
+ return;
2034
+ } catch (c) {
2035
+ Lt.warn("Invalid SVG path for border, falling back to rectangle:", c), t.rect(n, s, i, a);
2036
+ }
2037
+ t.stroke();
2038
+ }
2039
+ /**
2040
+ * Render artboard label (name + dimensions)
2041
+ */
2042
+ renderLabel(t, e, o) {
2043
+ const n = this.options.labelFontSize, s = this.options.labelPadding, i = `${e.name} (${Math.round(e.width)} × ${Math.round(e.height)})`, a = e.x, r = e.y - s;
2044
+ t.font = `${n}px -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif`;
2045
+ const l = this.options.selectedBorderColor || tt(), h = this.options.labelColor || ge();
2046
+ t.fillStyle = o ? l : h, t.textAlign = "left", t.textBaseline = "bottom", t.fillText(i, a, r);
2047
+ }
2048
+ /**
2049
+ * Render multiple artboards
2050
+ */
2051
+ renderMultiple(t, e, o) {
2052
+ e.forEach((n) => {
2053
+ const s = n.id === o;
2054
+ this.render(t, n, s);
2055
+ });
2056
+ }
2057
+ /**
2058
+ * Clip rendering to artboard bounds (respects clipShape)
2059
+ * Use this to clip element rendering to stay within artboard
2060
+ */
2061
+ clipToArtboard(t, e) {
2062
+ t.save(), this.applyClipPath(t, e);
2063
+ }
2064
+ /**
2065
+ * Apply clip path based on artboard's clipShape
2066
+ * This creates the clip region for the given artboard
2067
+ * If borderRadius option is set and no special clipShape, uses rounded rectangle
2068
+ */
2069
+ applyClipPath(t, e) {
2070
+ const { x: o, y: n, width: s, height: i, clipShape: a } = e;
2071
+ if (this.options.borderRadius > 0, t.beginPath(), !a || a === "rectangle")
2072
+ if (this.options.borderRadius > 0) {
2073
+ const r = Math.min(this.options.borderRadius, Math.min(s, i) / 2);
2074
+ this.roundedRect(t, o, n, s, i, r);
2075
+ } else
2076
+ t.rect(o, n, s, i);
2077
+ else if (a === "circle") {
2078
+ const r = o + s / 2, l = n + i / 2, h = Math.min(s, i) / 2;
2079
+ t.arc(r, l, h, 0, Math.PI * 2);
2080
+ } else if (typeof a == "object" && a.type === "rounded") {
2081
+ const r = Math.min(a.radius, Math.min(s, i) / 2);
2082
+ this.roundedRect(t, o, n, s, i, r);
2083
+ } else if (typeof a == "object" && a.type === "path")
2084
+ try {
2085
+ const r = new Path2D(a.d);
2086
+ t.translate(o, n), t.clip(r), t.translate(-o, -n);
2087
+ return;
2088
+ } catch (r) {
2089
+ Lt.warn("Invalid SVG path, falling back to rectangle:", r), t.rect(o, n, s, i);
2090
+ }
2091
+ t.clip();
2092
+ }
2093
+ /**
2094
+ * Draw a rounded rectangle path
2095
+ * Helper method for rounded clip shapes
2096
+ */
2097
+ roundedRect(t, e, o, n, s, i) {
2098
+ typeof t.roundRect == "function" ? t.roundRect(e, o, n, s, i) : (t.moveTo(e + i, o), t.lineTo(e + n - i, o), t.arcTo(e + n, o, e + n, o + i, i), t.lineTo(e + n, o + s - i), t.arcTo(e + n, o + s, e + n - i, o + s, i), t.lineTo(e + i, o + s), t.arcTo(e, o + s, e, o + s - i, i), t.lineTo(e, o + i), t.arcTo(e, o, e + i, o, i));
2099
+ }
2100
+ /**
2101
+ * Restore after clipping
2102
+ */
2103
+ restoreClip(t) {
2104
+ t.restore();
2105
+ }
2106
+ /**
2107
+ * Render artboard outline (for dragging/resizing preview)
2108
+ */
2109
+ renderOutline(t, e, o, n, s, i = !1) {
2110
+ t.save(), t.setLineDash([5, 5]), t.strokeStyle = i ? "#0066cc" : "#999", t.lineWidth = 1, t.strokeRect(e, o, n, s), t.restore();
2111
+ }
2112
+ /**
2113
+ * Render resize handles for artboard
2114
+ */
2115
+ renderResizeHandles(t, e) {
2116
+ const { x: n, y: s, width: i, height: a } = e, r = [
2117
+ { x: n, y: s },
2118
+ // top-left
2119
+ { x: n + i, y: s },
2120
+ // top-right
2121
+ { x: n, y: s + a },
2122
+ // bottom-left
2123
+ { x: n + i, y: s + a }
2124
+ // bottom-right
2125
+ ];
2126
+ t.save(), r.forEach((l) => {
2127
+ t.fillStyle = "#fff", t.strokeStyle = "#0066cc", t.lineWidth = 2, t.fillRect(l.x - 8 / 2, l.y - 8 / 2, 8, 8), t.strokeRect(l.x - 8 / 2, l.y - 8 / 2, 8, 8);
2128
+ }), t.restore();
2129
+ }
2130
+ /**
2131
+ * Update options
2132
+ */
2133
+ setOptions(t) {
2134
+ this.options = { ...this.options, ...t };
2135
+ }
2136
+ /**
2137
+ * Get current options
2138
+ */
2139
+ getOptions() {
2140
+ return { ...this.options };
2141
+ }
2142
+ /**
2143
+ * Create export-safe renderer
2144
+ * Returns a new renderer instance with preview background disabled
2145
+ * Use this when exporting to ensure preview background is never included
2146
+ */
2147
+ static createForExport(t = {}) {
2148
+ return new oe({
2149
+ ...t,
2150
+ showPreviewBackground: !1
2151
+ // CRITICAL: Never show preview in exports
2152
+ });
2153
+ }
2154
+ /**
2155
+ * Check if preview background would be rendered for given artboard
2156
+ */
2157
+ willRenderPreviewBackground(t) {
2158
+ return !!(this.options.showPreviewBackground && t.previewBackgroundColor && t.previewBackgroundColor !== "transparent");
2159
+ }
2160
+ }
2161
+ function en(x) {
2162
+ return x instanceof be;
2163
+ }
2164
+ function Ye(x) {
2165
+ return x instanceof Qt;
2166
+ }
2167
+ function Dt(x) {
2168
+ return x instanceof ue;
2169
+ }
2170
+ function nn(x) {
2171
+ return x instanceof ye;
2172
+ }
2173
+ function sn(x) {
2174
+ return x instanceof pe;
2175
+ }
2176
+ class St {
2177
+ /**
2178
+ * Render selection box layer
2179
+ */
2180
+ static renderSelectionLayer(t, e, o, n = 1) {
2181
+ if (!e || e.visible === !1 || o && o.getMode() === "drag" || "isGroup" in e && e.isGroup) return;
2182
+ const i = e.getVisualBoundingBox(), a = e.getRotationAnchor();
2183
+ t.save(), t.strokeStyle = tt(t.canvas), t.lineWidth = 2, t.translate(a.x * n, a.y * n), t.rotate(ht.toRadians(e.rotation)), t.translate(-a.x * n, -a.y * n);
2184
+ const r = ot.canvas.selectionPadding;
2185
+ t.strokeRect(
2186
+ (i.x - r) * n,
2187
+ (i.y - r) * n,
2188
+ (i.width + r * 2) * n,
2189
+ (i.height + r * 2) * n
2190
+ ), t.restore();
2191
+ }
2192
+ /**
2193
+ * Render position and size info below the selected element
2194
+ * Only shows dimensions (width x height) while resizing
2195
+ */
2196
+ static renderPositionSizeInfo(t, e, o, n, s = 1) {
2197
+ if (!e || !(o != null && o.rotationHandle) || (n == null ? void 0 : n.getMode()) !== "resize") return;
2198
+ const i = e.getVisualBoundingBox(), a = Math.round(i.width), r = Math.round(i.height), l = `${a} × ${r}`, h = o.rotationHandle, c = h.x * s, d = h.y * s - 20;
2199
+ t.save(), t.font = "10px system-ui, -apple-system, sans-serif";
2200
+ const p = t.measureText(l).width + 12, b = 18;
2201
+ t.shadowColor = me(t.canvas), t.shadowBlur = 4, t.shadowOffsetY = 1, t.fillStyle = Se(t.canvas), t.beginPath(), t.roundRect(
2202
+ c - p / 2,
2203
+ d - b / 2,
2204
+ p,
2205
+ b,
2206
+ 3
2207
+ // Smaller border radius
2208
+ ), t.fill(), t.shadowColor = "transparent", t.shadowBlur = 0, t.shadowOffsetY = 0, t.fillStyle = Te(t.canvas), t.textAlign = "center", t.textBaseline = "middle", t.fillText(l, c, d + 0.5), t.restore();
2209
+ }
2210
+ /**
2211
+ * Render transform handles layer
2212
+ */
2213
+ static renderHandlesLayer(t, e, o, n, s, i, a, r = 1, l = !0) {
2214
+ if (!e || e.visible === !1 || a.getMode() === "drag") return;
2215
+ const c = i.type === "rotation-handle", d = i.type === "resize-handle" ? i.data : null, u = a.getMode() === "resize", p = u ? a.getActiveHandle() : null;
2216
+ o.render(
2217
+ t,
2218
+ l,
2219
+ n,
2220
+ s,
2221
+ c,
2222
+ d,
2223
+ u,
2224
+ p,
2225
+ r,
2226
+ !0
2227
+ // Render rotation handle on canvas (not React)
2228
+ );
2229
+ }
2230
+ }
2231
+ class _t {
2232
+ /**
2233
+ * Render hover highlights layer with fade-in animation
2234
+ * Outlines the actual element shape instead of a rectangular border
2235
+ */
2236
+ static renderHoverLayer(t, e, o, n = !1, s = 1, i) {
2237
+ if (e.type !== "element" || !e.data || e.data.id === o) return;
2238
+ const a = e.data;
2239
+ t.save();
2240
+ const r = 150, l = 0.5;
2241
+ let h = l;
2242
+ if (e.startTime) {
2243
+ const c = performance.now() - e.startTime;
2244
+ h = Math.min(c / r, 1) * l;
2245
+ }
2246
+ t.strokeStyle = we(t.canvas), t.globalAlpha = h, t.lineWidth = 3, this.renderElementOutline(t, a, s, i), t.restore();
2247
+ }
2248
+ /**
2249
+ * Render outline for a specific element type
2250
+ */
2251
+ static renderElementOutline(t, e, o = 1, n) {
2252
+ const s = e.getRotationAnchor();
2253
+ t.translate(s.x * o, s.y * o), t.rotate(ht.toRadians(e.rotation)), t.translate(-s.x * o, -s.y * o), e.transformType === "shape" ? this.renderShapeOutline(t, e, o) : e.transformType === "image" ? this.renderImageOutline(t, e, o) : e.transformType === "group" ? this.renderGroupOutline(t, e, o) : this.renderTextOutline(t, e, o, n);
2254
+ }
2255
+ /**
2256
+ * Render outline for shape elements (star, rectangle, circle, etc.)
2257
+ * Creates a ring effect with a gap between the element and the stroke
2258
+ */
2259
+ static renderShapeOutline(t, e, o) {
2260
+ const n = e.transformData, s = n.width * o, i = n.height * o, a = 2, l = a + a / 2;
2261
+ switch (t.save(), t.translate(e.x * o, e.y * o), t.beginPath(), n.shapeType) {
2262
+ case "rectangle": {
2263
+ const h = n.borderRadius || 0, c = -(s + l * 2) / 2, d = -(i + l * 2) / 2, u = s + l * 2, p = i + l * 2;
2264
+ if (h > 0) {
2265
+ const b = Math.min(h / 100 * Math.min(u, p), u / 2, p / 2);
2266
+ t.roundRect(c, d, u, p, b);
2267
+ } else
2268
+ t.rect(c, d, u, p);
2269
+ break;
2270
+ }
2271
+ case "circle": {
2272
+ const h = Math.min(s, i) / 2 + l;
2273
+ t.arc(0, 0, h, 0, Math.PI * 2);
2274
+ break;
2275
+ }
2276
+ case "ellipse": {
2277
+ const h = (n.radiusX ?? s / 2) + l, c = (n.radiusY ?? i / 2) + l;
2278
+ t.ellipse(0, 0, h, c, 0, 0, Math.PI * 2);
2279
+ break;
2280
+ }
2281
+ case "triangle": {
2282
+ const h = s / 2, c = i / 2, d = [
2283
+ { x: 0, y: -c },
2284
+ // top
2285
+ { x: h, y: c },
2286
+ // bottom-right
2287
+ { x: -h, y: c }
2288
+ // bottom-left
2289
+ ], u = d.map((b, g) => {
2290
+ const f = d[(g + 1) % 3], y = f.x - b.x, S = f.y - b.y, T = Math.sqrt(y * y + S * S);
2291
+ return { x: S / T, y: -y / T };
2292
+ }), p = d.map((b, g) => {
2293
+ const f = u[(g + 2) % 3], y = u[g], S = f.x + y.x, T = f.y + y.y, w = Math.sqrt(S * S + T * T), m = (S * f.x + T * f.y) / w, C = l / m;
2294
+ return {
2295
+ x: b.x + S / w * C,
2296
+ y: b.y + T / w * C
2297
+ };
2298
+ });
2299
+ t.moveTo(p[0].x, p[0].y), t.lineTo(p[1].x, p[1].y), t.lineTo(p[2].x, p[2].y), t.closePath();
2300
+ break;
2301
+ }
2302
+ case "star": {
2303
+ const h = n.points ?? 5, c = n.innerRadius ?? 0.4, d = Math.min(s, i) / 2 + l, u = d * c;
2304
+ for (let p = 0; p < h * 2; p++) {
2305
+ const b = p * Math.PI / h - Math.PI / 2, g = p % 2 === 0 ? d : u, f = g * Math.cos(b), y = g * Math.sin(b);
2306
+ p === 0 ? t.moveTo(f, y) : t.lineTo(f, y);
2307
+ }
2308
+ t.closePath();
2309
+ break;
2310
+ }
2311
+ case "polygon": {
2312
+ const h = n.sides ?? 5, c = Math.min(s, i) / 2 + l;
2313
+ for (let d = 0; d < h; d++) {
2314
+ const u = d * 2 * Math.PI / h - Math.PI / 2, p = c * Math.cos(u), b = c * Math.sin(u);
2315
+ d === 0 ? t.moveTo(p, b) : t.lineTo(p, b);
2316
+ }
2317
+ t.closePath();
2318
+ break;
2319
+ }
2320
+ case "line": {
2321
+ const h = s / 2 + l;
2322
+ t.moveTo(-h, 0), t.lineTo(h, 0);
2323
+ break;
2324
+ }
2325
+ }
2326
+ t.stroke(), t.restore();
2327
+ }
2328
+ /**
2329
+ * Render outline for image elements
2330
+ * Creates a ring effect with a gap between the element and the stroke
2331
+ */
2332
+ static renderImageOutline(t, e, o) {
2333
+ var b;
2334
+ const n = e.getVisualBoundingBox(), s = n.width * o, i = n.height * o, a = ((b = e.transformData) == null ? void 0 : b.borderRadius) || 0, r = 2, h = r + r / 2;
2335
+ t.save(), t.translate(e.x * o, e.y * o);
2336
+ const c = -(s + h * 2) / 2, d = -(i + h * 2) / 2, u = s + h * 2, p = i + h * 2;
2337
+ if (t.beginPath(), a > 0) {
2338
+ const g = Math.min(a / 100 * Math.min(u, p), u / 2, p / 2);
2339
+ t.roundRect(c, d, u, p, g);
2340
+ } else
2341
+ t.rect(c, d, u, p);
2342
+ t.stroke(), t.restore();
2343
+ }
2344
+ /**
2345
+ * Render outline for group elements
2346
+ * Creates a ring effect with a gap between the element and the stroke
2347
+ */
2348
+ static renderGroupOutline(t, e, o) {
2349
+ const n = e.getVisualBoundingBox(), s = 2, a = s + s / 2, r = n.x * o - a, l = n.y * o - a, h = n.width * o + a * 2, c = n.height * o + a * 2;
2350
+ t.save(), t.setLineDash([5, 5]), t.beginPath(), t.roundRect(r, l, h, c, 8), t.stroke(), t.restore();
2351
+ }
2352
+ /**
2353
+ * Render outline for text elements
2354
+ * Creates a ring effect with a gap between text and stroke (ring behind text fill)
2355
+ * Supports all transform types: custom, circle, wave, arch, etc.
2356
+ */
2357
+ static renderTextOutline(t, e, o, n) {
2358
+ const s = e.transformType;
2359
+ s === "circle" || s === "wave" || s === "arch" || s === "lean" || s === "ascend" || s === "flag" ? this.renderTransformHoverRingEffect(t, e, o) : this.renderHoverRingEffect(t, e, o);
2360
+ }
2361
+ /**
2362
+ * HOVER RING EFFECT for straight/custom text
2363
+ *
2364
+ * This is the colored ring that appears when hovering over text elements.
2365
+ * Uses offscreen canvas compositing to create a ring with a transparent gap.
2366
+ * Supports per-character fonts via rich text spans.
2367
+ *
2368
+ * NOTE: This is NOT the same as:
2369
+ * - Element stroke (StrokeRenderer.ts - only when stroke.enabled = true)
2370
+ * - Selection box (dashed rectangle in element.render())
2371
+ */
2372
+ static renderHoverRingEffect(t, e, o) {
2373
+ t.save(), t.translate(e.x * o, e.y * o), o !== 1 && t.scale(o, o);
2374
+ const n = e.fontSize, s = e.fontFamily, i = e.bold || !1, a = e.italic || !1, r = e.textAlign || "center", l = te(n, s, i, a), h = n * 1.2, c = 4 / o, d = 4 / o, u = document.createElement("canvas");
2375
+ u.width = t.canvas.width, u.height = t.canvas.height;
2376
+ const p = u.getContext("2d");
2377
+ if (!p) {
2378
+ t.restore();
2379
+ return;
2380
+ }
2381
+ p.setTransform(t.getTransform()), p.textBaseline = "alphabetic", p.lineJoin = "round";
2382
+ const b = e.getRichText ? e.getRichText() : null;
2383
+ if (b) {
2384
+ const g = ee(b), f = [], y = [];
2385
+ g.forEach((w) => {
2386
+ if (e.transformType === "custom" && e.transformData) {
2387
+ const m = e.transformData.width - ft * 2, C = ne(w, m, {
2388
+ fontSize: n,
2389
+ fontFamily: s,
2390
+ bold: i,
2391
+ italic: a
2392
+ });
2393
+ C.forEach((R, M) => {
2394
+ f.push(R), y.push({
2395
+ isParagraphStart: M === 0,
2396
+ isParagraphEnd: M === C.length - 1
2397
+ });
2398
+ });
2399
+ } else
2400
+ f.push(w), y.push({
2401
+ isParagraphStart: !0,
2402
+ isParagraphEnd: !0
2403
+ });
2404
+ });
2405
+ let S;
2406
+ f.length === 0 ? S = 0 : f.length === 1 ? S = l.height : S = (f.length - 1) * h + l.height;
2407
+ const T = -S / 2;
2408
+ f.forEach((w, m) => {
2409
+ var W, V;
2410
+ const C = y[m].isParagraphStart, R = y[m].isParagraphEnd;
2411
+ let M = 0;
2412
+ const k = w.map((A) => A.text).join(""), v = ((W = k.match(/^ +/)) == null ? void 0 : W[0].length) || 0, P = ((V = k.match(/ +$/)) == null ? void 0 : V[0].length) || 0;
2413
+ let H = 0;
2414
+ w.forEach((A) => {
2415
+ const $ = A.style.fontSize !== void 0 ? A.style.fontSize : n, j = A.style.fontFamily !== void 0 ? A.style.fontFamily : s, J = A.style.bold !== void 0 ? A.style.bold : i, X = A.style.italic !== void 0 ? A.style.italic : a;
2416
+ let E = A.text;
2417
+ const I = H, _ = H + A.text.length;
2418
+ if (!C && v > 0 && I < v) {
2419
+ const B = Math.min(v - I, A.text.length);
2420
+ E = E.substring(B);
2421
+ }
2422
+ if (!R && P > 0) {
2423
+ const B = k.length - P;
2424
+ if (_ > B) {
2425
+ const F = !C && I < v ? Math.min(v - I, A.text.length) : 0, G = Math.max(0, B - I - F);
2426
+ E = E.substring(0, G);
2427
+ }
2428
+ }
2429
+ E.length > 0 && (t.save(), t.font = Bt($, j, J, X), M += t.measureText(E).width, t.restore()), H += A.text.length;
2430
+ });
2431
+ const L = e.transformType === "custom" && e.transformData ? e.transformData.width - ft * 2 : void 0;
2432
+ let D;
2433
+ r === "center" ? D = -M / 2 : r === "right" ? D = L !== void 0 ? L / 2 - M : -M : D = L !== void 0 ? -L / 2 : 0;
2434
+ const O = T + l.ascent + m * h;
2435
+ H = 0, w.forEach((A) => {
2436
+ const $ = A.style.fontSize !== void 0 ? A.style.fontSize : n, j = A.style.fontFamily !== void 0 ? A.style.fontFamily : s;
2437
+ A.style.color !== void 0 ? A.style.color : e.color;
2438
+ const J = A.style.bold !== void 0 ? A.style.bold : i, X = A.style.italic !== void 0 ? A.style.italic : a;
2439
+ let E = A.text;
2440
+ const I = H, _ = H + A.text.length;
2441
+ if (!C && v > 0 && I < v) {
2442
+ const B = Math.min(v - I, A.text.length);
2443
+ E = E.substring(B);
2444
+ }
2445
+ if (!R && P > 0) {
2446
+ const B = k.length - P;
2447
+ if (_ > B) {
2448
+ const F = !C && I < v ? Math.min(v - I, A.text.length) : 0, G = Math.max(0, B - I - F);
2449
+ E = E.substring(0, G);
2450
+ }
2451
+ }
2452
+ H += A.text.length, E.length > 0 && (p.font = Bt($, j, J, X), p.textAlign = "left", p.globalCompositeOperation = "source-over", p.strokeStyle = t.strokeStyle, p.globalAlpha = Math.min(t.globalAlpha * 1.6, 0.8), p.lineWidth = (d + c) * 2, p.strokeText(E, D, O), p.globalCompositeOperation = "destination-out", p.globalAlpha = 1, p.lineWidth = d * 2, p.strokeText(E, D, O), p.fillText(E, D, O), D += p.measureText(E).width);
2453
+ });
2454
+ });
2455
+ } else {
2456
+ t.font = Bt(n, s, i, a), p.font = t.font, p.textAlign = r;
2457
+ let g, f;
2458
+ if (e.transformType === "custom" && e.transformData) {
2459
+ const C = e.transformData.width - ft * 2, { lines: R } = ve(
2460
+ e.text,
2461
+ C,
2462
+ n,
2463
+ s,
2464
+ i,
2465
+ a,
2466
+ void 0,
2467
+ !0
2468
+ );
2469
+ g = R;
2470
+ } else {
2471
+ g = e.text.split(`
2472
+ `);
2473
+ const C = e.text.includes(`
2474
+ `);
2475
+ g = xe(g, C);
2476
+ }
2477
+ g.length === 0 ? f = 0 : g.length === 1 ? f = l.height : f = (g.length - 1) * h + l.height;
2478
+ const y = -f / 2, S = e.text.includes(`
2479
+ `), T = e.transformType === "custom" && e.transformData ? e.transformData.width : 0, w = -T / 2;
2480
+ let m = w + ft;
2481
+ r === "center" ? m = w + T / 2 : r === "right" && (m = w + T - ft), g.forEach((C, R) => {
2482
+ const M = y + l.ascent + R * h, k = S || R === 0, v = S || R === g.length - 1;
2483
+ let P = C;
2484
+ k || (P = P.replace(/^ +/, "")), v || (P = P.replace(/ +$/, "")), p.globalCompositeOperation = "source-over", p.strokeStyle = t.strokeStyle, p.globalAlpha = Math.min(t.globalAlpha * 1.6, 0.8), p.lineWidth = (d + c) * 2, p.lineJoin = "round", p.strokeText(P, m, M), p.globalCompositeOperation = "destination-out", p.globalAlpha = 1, p.lineWidth = d * 2, p.strokeText(P, m, M), p.fillText(P, m, M);
2485
+ });
2486
+ }
2487
+ t.setTransform(1, 0, 0, 1, 0, 0), t.drawImage(u, 0, 0), t.restore();
2488
+ }
2489
+ /**
2490
+ * HOVER RING EFFECT for transformed text (circle, wave, arch, lean, ascend, flag)
2491
+ *
2492
+ * This is the colored ring that appears when hovering over curved/transformed text.
2493
+ * Renders text character-by-character along the transform path with ring effect.
2494
+ * Uses offscreen canvas compositing to create a ring with a transparent gap.
2495
+ *
2496
+ * NOTE: This is NOT the same as:
2497
+ * - Element stroke (StrokeRenderer.ts - only when stroke.enabled = true)
2498
+ * - Selection box (dashed outline shown by each transform's render() method)
2499
+ */
2500
+ static renderTransformHoverRingEffect(t, e, o) {
2501
+ t.save(), t.translate(e.x * o, e.y * o), o !== 1 && t.scale(o, o), t.rotate(-e.rotation * Math.PI / 180);
2502
+ const n = 4 / o, s = 4 / o, i = document.createElement("canvas");
2503
+ i.width = t.canvas.width, i.height = t.canvas.height;
2504
+ const a = i.getContext("2d");
2505
+ if (!a) {
2506
+ t.restore();
2507
+ return;
2508
+ }
2509
+ a.setTransform(t.getTransform()), this.renderTransformWithHoverEffect(a, e, n, s, t.strokeStyle), t.setTransform(1, 0, 0, 1, 0, 0), t.drawImage(i, 0, 0), t.restore();
2510
+ }
2511
+ /**
2512
+ * Helper to render text along a transform path with hover effect
2513
+ */
2514
+ static renderTransformWithHoverEffect(t, e, o, n, s) {
2515
+ const i = e, a = i.fontSize, r = i.fontFamily, l = i.bold || !1, h = i.italic || !1, c = l ? "bold" : "normal", d = h ? "italic" : "normal";
2516
+ if (t.font = `${d} ${c} ${a}px ${r}`, t.textAlign = "center", t.textBaseline = "middle", i.transformType === "lean") {
2517
+ const y = -i.transformData.leanAmount * Math.PI / 4;
2518
+ t.transform(1, 0, Math.tan(y), 1, 0, 0), t.globalCompositeOperation = "source-over", t.strokeStyle = s, t.globalAlpha = 0.8, t.lineWidth = (n + o) * 2, t.lineJoin = "round", t.strokeText(i.text, 0, 0), t.globalCompositeOperation = "destination-out", t.globalAlpha = 1, t.lineWidth = n * 2, t.strokeText(i.text, 0, 0), t.fillText(i.text, 0, 0);
2519
+ return;
2520
+ }
2521
+ const u = i.text.split(""), p = u.map((f) => t.measureText(f).width), b = this.calculateCharPositions(e, u, p), g = (f) => {
2522
+ t.translate(f.x, f.y), f.angle && t.rotate(f.angle), f.skewY && t.transform(1, f.skewY, 0, 1, 0, 0);
2523
+ };
2524
+ t.globalCompositeOperation = "source-over", t.strokeStyle = s, t.globalAlpha = 0.8, t.lineWidth = (n + o) * 2, t.lineJoin = "round", b.forEach((f, y) => {
2525
+ t.save(), g(f), t.strokeText(u[y], 0, 0), t.restore();
2526
+ }), t.globalCompositeOperation = "destination-out", t.globalAlpha = 1, t.lineWidth = n * 2, b.forEach((f, y) => {
2527
+ t.save(), g(f), t.strokeText(u[y], 0, 0), t.fillText(u[y], 0, 0), t.restore();
2528
+ });
2529
+ }
2530
+ /**
2531
+ * Calculate character positions for different transform types
2532
+ *
2533
+ * Also used by EditModeRenderer for cursor positioning on transformed text.
2534
+ */
2535
+ static calculateCharPositions(t, e, o) {
2536
+ const n = t.transformType, s = t.transformData;
2537
+ return n === "circle" ? this.calculateCirclePositions(s, e, o) : n === "wave" ? this.calculateWavePositions(t, e, o, s) : n === "arch" ? this.calculateArchPositions(t, e, o, s) : n === "lean" ? this.calculateLeanPositions(e, o) : n === "ascend" ? this.calculateAscendPositions(t, e, o, s) : n === "flag" ? this.calculateFlagPositions(t, e, o, s) : e.map(() => ({ x: 0, y: 0, angle: 0 }));
2538
+ }
2539
+ /**
2540
+ * Calculate positions for circle transform
2541
+ */
2542
+ static calculateCirclePositions(t, e, o) {
2543
+ const n = [], s = o.reduce((a, r) => a + r, 0), i = t.radius * t.scale;
2544
+ if (t.reverse) {
2545
+ let a = Math.PI / 2 + s / (2 * i);
2546
+ e.forEach((r, l) => {
2547
+ const c = o[l] / i, d = Math.cos(a - c / 2) * i, u = Math.sin(a - c / 2) * i, p = a - c / 2 - Math.PI / 2;
2548
+ n.push({ x: d, y: u, angle: p }), a -= c;
2549
+ });
2550
+ } else {
2551
+ let a = -Math.PI / 2 - s / (2 * i);
2552
+ e.forEach((r, l) => {
2553
+ const c = o[l] / i, d = Math.cos(a + c / 2) * i, u = Math.sin(a + c / 2) * i, p = a + c / 2 + Math.PI / 2;
2554
+ n.push({ x: d, y: u, angle: p }), a += c;
2555
+ });
2556
+ }
2557
+ return n;
2558
+ }
2559
+ /**
2560
+ * Calculate positions for wave transform
2561
+ */
2562
+ static calculateWavePositions(t, e, o, n) {
2563
+ const s = [];
2564
+ let a = -o.reduce((r, l) => r + l, 0) / 2;
2565
+ return e.forEach((r, l) => {
2566
+ const h = o[l], c = a + h / 2, d = c / (n.width / 2), u = n.amplitude * t.fontSize * Math.sin(n.frequency * Math.PI * d), p = n.amplitude * n.frequency * Math.PI * Math.cos(n.frequency * Math.PI * d);
2567
+ s.push({ x: c, y: u, angle: 0, skewY: p * Ce }), a += h;
2568
+ }), s;
2569
+ }
2570
+ /**
2571
+ * Calculate positions for arch transform
2572
+ */
2573
+ static calculateArchPositions(t, e, o, n) {
2574
+ const s = [], i = o.reduce((l, h) => l + h, 0), a = n.width / 2;
2575
+ let r = -i / 2;
2576
+ return e.forEach((l, h) => {
2577
+ const c = o[h], d = r + c / 2, u = d / a, p = (Math.pow(u, 2) - 1) * n.archHeight * t.fontSize, b = 2 * u * n.archHeight;
2578
+ s.push({ x: d, y: p, angle: 0, skewY: b * 0.3 }), r += c;
2579
+ }), s;
2580
+ }
2581
+ /**
2582
+ * Calculate positions for lean transform
2583
+ */
2584
+ static calculateLeanPositions(t, e) {
2585
+ const o = [];
2586
+ let s = -e.reduce((i, a) => i + a, 0) / 2;
2587
+ return t.forEach((i, a) => {
2588
+ const r = e[a], l = s + r / 2;
2589
+ o.push({ x: l, y: 0, angle: 0 }), s += r;
2590
+ }), o;
2591
+ }
2592
+ /**
2593
+ * Calculate positions for ascend transform
2594
+ */
2595
+ static calculateAscendPositions(t, e, o, n) {
2596
+ const s = [], i = o.reduce((h, c) => h + c, 0), a = n.ascendAngle * Math.PI / 180, r = Math.tan(a);
2597
+ let l = -i / 2;
2598
+ return e.forEach((h, c) => {
2599
+ const d = o[c], u = l + d / 2, p = u * r;
2600
+ s.push({ x: u, y: p, angle: 0, skewY: r * 1 }), l += d;
2601
+ }), s;
2602
+ }
2603
+ /**
2604
+ * Calculate positions for flag transform
2605
+ */
2606
+ static calculateFlagPositions(t, e, o, n) {
2607
+ const s = [];
2608
+ let a = -o.reduce((r, l) => r + l, 0) / 2;
2609
+ return e.forEach((r, l) => {
2610
+ const h = o[l], c = a + h / 2, d = c / (n.width / 2), u = Math.abs(d), p = n.amplitude * u, b = p * t.fontSize * Math.sin(n.frequency * Math.PI * d), g = p * n.frequency * Math.PI * Math.cos(n.frequency * Math.PI * d), f = n.amplitude * Math.sign(d) * Math.sin(n.frequency * Math.PI * d), y = g + f;
2611
+ s.push({ x: c, y: b, angle: 0, skewY: y * 0.3 }), a += h;
2612
+ }), s;
2613
+ }
2614
+ }
2615
+ class xt {
2616
+ /**
2617
+ * Render snap guides layer
2618
+ */
2619
+ static renderSnapGuidesLayer(t, e = []) {
2620
+ e.length !== 0 && (t.save(), t.strokeStyle = tt(t.canvas), t.lineWidth = ot.canvas.snapGuideWidth, t.setLineDash(ot.canvas.snapGuideDash), e.forEach((o) => {
2621
+ t.beginPath(), o.type === "vertical" ? (t.moveTo(o.x ?? 0, o.y1 ?? 0), t.lineTo(o.x ?? 0, o.y2 ?? 0)) : o.type === "horizontal" ? (t.moveTo(o.x1 ?? 0, o.y ?? 0), t.lineTo(o.x2 ?? 0, o.y ?? 0)) : (t.moveTo(o.x1 ?? 0, o.y1 ?? 0), t.lineTo(o.x2 ?? 0, o.y2 ?? 0)), t.stroke();
2622
+ }), t.restore());
2623
+ }
2624
+ /**
2625
+ * Render spacing indicators layer
2626
+ */
2627
+ static renderSpacingIndicatorsLayer(t, e = []) {
2628
+ e.length !== 0 && (t.save(), t.setLineDash([]), e.forEach((o) => {
2629
+ const n = Yt(t.canvas);
2630
+ if (t.strokeStyle = n, t.fillStyle = n, t.lineWidth = ot.canvas.spacingIndicatorWidth, t.beginPath(), o.type === "horizontal") {
2631
+ const s = o.x1 ?? 0, i = o.x2 ?? 0, a = o.y ?? 0;
2632
+ t.moveTo(s, a), t.lineTo(i, a), t.stroke();
2633
+ const r = 6;
2634
+ t.beginPath(), t.moveTo(s, a - r), t.lineTo(s, a + r), t.stroke(), t.beginPath(), t.moveTo(i, a - r), t.lineTo(i, a + r), t.stroke();
2635
+ const l = o.labelX, h = o.labelY;
2636
+ this.renderDistanceLabel(t, l, h, o.distance);
2637
+ } else if (o.type === "vertical") {
2638
+ const s = o.x ?? 0, i = o.y1 ?? 0, a = o.y2 ?? 0;
2639
+ t.moveTo(s, i), t.lineTo(s, a), t.stroke();
2640
+ const r = 6;
2641
+ t.beginPath(), t.moveTo(s - r, i), t.lineTo(s + r, i), t.stroke(), t.beginPath(), t.moveTo(s - r, a), t.lineTo(s + r, a), t.stroke();
2642
+ const l = o.labelX, h = o.labelY;
2643
+ this.renderDistanceLabel(t, l, h, o.distance);
2644
+ }
2645
+ }), t.restore());
2646
+ }
2647
+ /**
2648
+ * Render a distance label with background
2649
+ */
2650
+ static renderDistanceLabel(t, e, o, n) {
2651
+ const s = `${n}`;
2652
+ t.font = `600 ${ot.canvas.spacingLabelFontSize}px ${ot.canvas.spacingLabelFontFamily}`;
2653
+ const a = t.measureText(s).width, r = ot.canvas.spacingLabelFontSize, l = 4, h = a + l * 2, c = r + l * 2;
2654
+ t.fillStyle = Yt(t.canvas), t.beginPath(), t.roundRect(e - h / 2, o - c / 2, h, c, Me), t.fill(), t.fillStyle = Re, t.textAlign = "center", t.textBaseline = "middle", t.fillText(s, e, o);
2655
+ }
2656
+ }
2657
+ class Wt {
2658
+ /**
2659
+ * Render edit mode features: cursor and text selection highlight
2660
+ * This renders on the main canvas at world coordinates (same as the element)
2661
+ */
2662
+ static renderEditModeLayer(t, e, o, n) {
2663
+ if (!e || e.transformType === "image" || e.transformType === "group") return;
2664
+ const s = e, { cursorPosition: i, selectionStart: a, selectionEnd: r, editText: l, editRichText: h, cursorOpacity: c, isTouchDevice: d, selectionHandlePositionsRef: u } = o, p = n || 1, b = s.transformType;
2665
+ if (b === "circle" || b === "wave" || b === "arch" || b === "lean" || b === "ascend" || b === "flag") {
2666
+ this.renderTransformEditModeLayer(t, s, o, p);
2667
+ return;
2668
+ }
2669
+ const g = s.fontSize || 24, f = s.fontFamily || "Arial", y = s.bold || !1, S = s.italic || !1, T = s.underline || !1, w = s.strikethrough || !1, m = s.textAlign || "center";
2670
+ t.save(), t.translate(s.x, s.y), t.rotate(-s.rotation * Math.PI / 180);
2671
+ const C = te(g, f, y, S), R = C.height, M = g * 1.2;
2672
+ let k, v, P;
2673
+ if (h && s.transformType === "custom" && s.transformData.width) {
2674
+ const X = s.transformData.width - ft * 2;
2675
+ P = X;
2676
+ const E = ee(h);
2677
+ k = [], v = [], E.forEach((I) => {
2678
+ const _ = ne(I, X, {
2679
+ fontSize: s.fontSize,
2680
+ fontFamily: s.fontFamily,
2681
+ bold: s.bold,
2682
+ italic: s.italic
2683
+ });
2684
+ _.forEach((B, F) => {
2685
+ k.push(B), v.push({
2686
+ isParagraphStart: F === 0,
2687
+ isParagraphEnd: F === _.length - 1
2688
+ });
2689
+ });
2690
+ });
2691
+ } else
2692
+ k = l.split(`
2693
+ `).map((E) => [
2694
+ {
2695
+ text: E,
2696
+ style: { fontSize: g, fontFamily: f, bold: y, italic: S }
2697
+ }
2698
+ ]), v = k.map(() => ({
2699
+ isParagraphStart: !0,
2700
+ // Each line is its own paragraph
2701
+ isParagraphEnd: !0
2702
+ }));
2703
+ const H = k.length > 1 ? C.height + (k.length - 1) * M : C.height;
2704
+ let L = -H / 2, D = 0, O = L, W = R;
2705
+ const V = [];
2706
+ let A = 0;
2707
+ k.forEach((X, E) => {
2708
+ var F, G;
2709
+ const I = v[E].isParagraphStart, _ = v[E].isParagraphEnd, B = [];
2710
+ X.forEach((at) => {
2711
+ Array.from(at.text).forEach((ct) => {
2712
+ let Y = g, it = f, et = y, q = S;
2713
+ if (h && A < l.length) {
2714
+ const Z = h.getStyleAt(A);
2715
+ Y = Z.fontSize || g, it = Z.fontFamily || f, et = Z.bold !== void 0 ? Z.bold : y, q = Z.italic !== void 0 ? Z.italic : S;
2716
+ }
2717
+ t.save();
2718
+ const nt = et ? "bold" : "normal", z = q ? "italic" : "normal";
2719
+ t.font = `${z} ${nt} ${Y}px "${it}", Arial, sans-serif`;
2720
+ const U = t.measureText(ct).width;
2721
+ t.restore(), B.push({ char: ct, width: U, fontSize: Y, index: A }), A++;
2722
+ });
2723
+ });
2724
+ {
2725
+ const at = E === 0, gt = E === k.length - 1, ct = B.map((z) => z.char).join(""), Y = ((F = ct.match(/^ +/)) == null ? void 0 : F[0].length) || 0, it = ((G = ct.match(/ +$/)) == null ? void 0 : G[0].length) || 0;
2726
+ let et = 0;
2727
+ for (let z = 0; z < B.length; z++)
2728
+ !at && z < Y || !gt && z >= B.length - it || (et += B[z].width);
2729
+ let q;
2730
+ m === "center" ? q = -et / 2 : m === "right" ? q = P !== void 0 ? P / 2 - et : -et : q = P !== void 0 ? -P / 2 : 0;
2731
+ let nt = q;
2732
+ for (let z = 0; z < B.length; z++) {
2733
+ const U = B[z];
2734
+ V[U.index] = {
2735
+ x: nt,
2736
+ y: L,
2737
+ width: U.width,
2738
+ // All characters keep their width for cursor navigation
2739
+ height: U.fontSize
2740
+ }, nt += U.width;
2741
+ }
2742
+ if (B.length > 0) {
2743
+ const z = B[0].index, U = B[B.length - 1].index, dt = E < k.length - 1 && U + 1 === A ? U : U + 1;
2744
+ if (i >= z && i <= dt)
2745
+ if (i === z)
2746
+ D = q, O = L, W = R;
2747
+ else {
2748
+ let pt = 0;
2749
+ for (let st = 0; st < B.length && B[st].index < i; st++) {
2750
+ const rt = B[st], Mt = rt.char === " ";
2751
+ let yt = !0;
2752
+ if (Mt) {
2753
+ const Rt = st < Y, N = st >= B.length - it, Tt = !I && Y > 0, wt = !_ && it > 0;
2754
+ yt = !(Rt && Tt || N && wt);
2755
+ }
2756
+ yt && (pt += rt.width);
2757
+ }
2758
+ D = q + pt, O = L, W = R;
2759
+ }
2760
+ }
2761
+ E < k.length - 1 && (L += M);
2762
+ }
2763
+ });
2764
+ const $ = a !== r;
2765
+ let j = null, J = null;
2766
+ if ($) {
2767
+ const X = Math.min(a, r), E = Math.max(a, r);
2768
+ t.save(), t.fillStyle = Ee(t.canvas);
2769
+ let I = 0, _ = -H / 2;
2770
+ if (k.forEach((B, F) => {
2771
+ var it, et;
2772
+ const G = v[F].isParagraphStart, at = v[F].isParagraphEnd, gt = F === 0, ct = F === k.length - 1, Y = [];
2773
+ if (B.forEach((q) => {
2774
+ Array.from(q.text).forEach((nt) => {
2775
+ const z = q.style || {}, U = z.fontSize !== void 0 ? z.fontSize : g, Z = z.fontFamily !== void 0 ? z.fontFamily : f, ut = z.bold !== void 0 ? z.bold : y, dt = z.italic !== void 0 ? z.italic : S;
2776
+ t.save();
2777
+ const pt = ut ? "bold" : "normal", st = dt ? "italic" : "normal";
2778
+ t.font = `${st} ${pt} ${U}px "${Z}", Arial, sans-serif`;
2779
+ const rt = t.measureText(nt).width;
2780
+ t.restore(), Y.push({ char: nt, index: I, width: rt, style: z }), I++;
2781
+ });
2782
+ }), Y.length > 0) {
2783
+ const q = Y[0].index, nt = Y[Y.length - 1].index;
2784
+ if (E > q && X <= nt) {
2785
+ t.save();
2786
+ const z = Y.map((N) => N.char).join(""), U = ((it = z.match(/^ +/)) == null ? void 0 : it[0].length) || 0, Z = ((et = z.match(/ +$/)) == null ? void 0 : et[0].length) || 0;
2787
+ let ut = 0;
2788
+ for (let N = 0; N < Y.length; N++)
2789
+ !gt && N < U || !ct && N >= Y.length - Z || (ut += Y[N].width);
2790
+ let dt = 0;
2791
+ m === "center" ? dt = -ut / 2 : m === "right" && (dt = -ut);
2792
+ const pt = Math.max(X, q), st = Math.min(E, nt + 1);
2793
+ let rt = 0;
2794
+ for (let N = 0; N < Y.length && Y[N].index < pt; N++) {
2795
+ const Tt = Y[N], wt = N < U, Pt = N >= Y.length - Z, At = !G && U > 0, It = !at && Z > 0;
2796
+ !(wt && At || Pt && It) && (rt += Tt.width);
2797
+ }
2798
+ let Mt = 0;
2799
+ for (let N = 0; N < Y.length && Y[N].index < st; N++) {
2800
+ const Tt = Y[N], wt = N < U, Pt = N >= Y.length - Z, At = !G && U > 0, It = !at && Z > 0;
2801
+ !(wt && At || Pt && It) && (Mt += Tt.width);
2802
+ }
2803
+ const yt = dt + rt, Rt = Mt - rt;
2804
+ t.fillRect(yt, _, Rt, R), j === null && (j = { x: yt, y: _ }), J = { x: yt + Rt, y: _ + R }, t.restore();
2805
+ }
2806
+ }
2807
+ F < k.length - 1 && (_ += M);
2808
+ }), t.restore(), h) {
2809
+ t.save();
2810
+ const B = s.transformType === "custom" ? s.transformData : null, F = B != null && B.width ? B.width - ft * 2 : void 0;
2811
+ ke(t, h, {
2812
+ fontSize: g,
2813
+ fontFamily: f,
2814
+ color: s.color || "#000000",
2815
+ bold: y,
2816
+ italic: S,
2817
+ underline: T,
2818
+ strikethrough: w,
2819
+ textAlign: m
2820
+ }, F), t.restore();
2821
+ }
2822
+ }
2823
+ if ($ && d && j && J) {
2824
+ const X = j, E = J, I = 6 / p, _ = 2 / p;
2825
+ t.save();
2826
+ const B = tt();
2827
+ t.fillStyle = B, t.strokeStyle = B, t.lineWidth = _, t.lineCap = "round";
2828
+ const F = { x: X.x, y: X.y - I }, G = { x: E.x, y: E.y + I };
2829
+ t.beginPath(), t.moveTo(X.x, X.y), t.lineTo(X.x, X.y + R), t.stroke(), t.beginPath(), t.arc(F.x, F.y, I, 0, Math.PI * 2), t.fill(), t.strokeStyle = "white", t.lineWidth = 2 / p, t.stroke(), t.strokeStyle = B, t.lineWidth = _, t.beginPath(), t.moveTo(E.x, E.y - R), t.lineTo(E.x, E.y), t.stroke(), t.beginPath(), t.arc(G.x, G.y, I, 0, Math.PI * 2), t.fill(), t.strokeStyle = "white", t.lineWidth = 2 / p, t.stroke(), t.restore(), u && (u.current = {
2830
+ start: {
2831
+ center: F,
2832
+ textEdge: X
2833
+ // Original selection edge (top-left of selection on first line)
2834
+ },
2835
+ end: {
2836
+ center: G,
2837
+ textEdge: E
2838
+ // Original selection edge (bottom-right of selection on last line)
2839
+ }
2840
+ });
2841
+ } else u && (u.current = { start: null, end: null });
2842
+ !$ && c > 0 && (t.save(), t.globalAlpha = c, t.strokeStyle = tt(), t.lineWidth = 3 / p, t.lineCap = "round", t.beginPath(), t.moveTo(D, O), t.lineTo(D, O + W), t.stroke(), t.restore()), t.restore();
2843
+ }
2844
+ /**
2845
+ * Render edit mode features for transformed text (circle, wave, arch, etc.)
2846
+ * Renders cursor along the transform path
2847
+ */
2848
+ static renderTransformEditModeLayer(t, e, o, n = 1) {
2849
+ const { cursorPosition: s, cursorOpacity: i } = o, a = e.fontSize || 24, r = e.fontFamily || "Arial", l = e.bold || !1, h = e.italic || !1;
2850
+ t.save(), t.translate(e.x, e.y), t.rotate(-e.rotation * Math.PI / 180);
2851
+ const c = l ? "bold" : "normal", d = h ? "italic" : "normal";
2852
+ t.font = `${d} ${c} ${a}px ${r}`;
2853
+ const u = Array.from(e.text), p = u.map((g) => t.measureText(g).width), b = _t.calculateCharPositions(e, u, p);
2854
+ if (s >= 0 && s <= u.length && i > 0) {
2855
+ let g;
2856
+ if (s === 0)
2857
+ if (b.length > 0) {
2858
+ const y = b[0], S = p[0];
2859
+ g = {
2860
+ x: y.x - Math.cos(y.angle) * (S / 2),
2861
+ y: y.y - Math.sin(y.angle) * (S / 2),
2862
+ angle: y.angle
2863
+ };
2864
+ } else
2865
+ g = { x: 0, y: 0, angle: 0 };
2866
+ else if (s >= b.length) {
2867
+ const y = b[b.length - 1], S = p[p.length - 1];
2868
+ g = {
2869
+ x: y.x + Math.cos(y.angle) * (S / 2),
2870
+ y: y.y + Math.sin(y.angle) * (S / 2),
2871
+ angle: y.angle
2872
+ };
2873
+ } else {
2874
+ const y = b[s], S = p[s];
2875
+ g = {
2876
+ x: y.x - Math.cos(y.angle) * (S / 2),
2877
+ y: y.y - Math.sin(y.angle) * (S / 2),
2878
+ angle: y.angle
2879
+ };
2880
+ }
2881
+ t.save(), t.translate(g.x, g.y), t.rotate(g.angle), t.globalAlpha = i, t.strokeStyle = tt(), t.lineWidth = 2 / n;
2882
+ const f = a * 1.2;
2883
+ t.beginPath(), t.moveTo(0, -f / 2), t.lineTo(0, f / 2), t.stroke(), t.restore();
2884
+ }
2885
+ t.restore();
2886
+ }
2887
+ /**
2888
+ * Render crop box layer (for images in crop mode)
2889
+ * Shows both image resize handles and crop handles
2890
+ */
2891
+ static renderCropBoxLayer(t, e, o, n = 1) {
2892
+ if (!(e instanceof Qt) || !e.isCropping) return;
2893
+ const s = e, i = s.getCropBoxBounds();
2894
+ if (!i) return;
2895
+ const a = 4 / n, r = 2 / n, l = Pe / n, h = Ae / n, c = 1 / n;
2896
+ t.save(), t.translate(e.x * n, e.y * n), t.rotate(ht.toRadians(e.rotation));
2897
+ const d = s.transformData.flipHorizontal ? -1 : 1, u = s.transformData.flipVertical ? -1 : 1;
2898
+ t.scale(d * n, u * n);
2899
+ const p = s.transformData.width * s.transformData.cropWidth, b = s.transformData.height * s.transformData.cropHeight, g = s.transformData.width / 2 - (s.transformData.cropX * s.transformData.width + p / 2), f = s.transformData.height / 2 - (s.transformData.cropY * s.transformData.height + b / 2), y = s.transformData.width / 2, S = s.transformData.height / 2, T = {
2900
+ x: g - y,
2901
+ y: f - S,
2902
+ width: s.transformData.width,
2903
+ height: s.transformData.height
2904
+ };
2905
+ t.fillStyle = "rgba(0, 0, 0, 0.5)", i.y > T.y && t.fillRect(T.x, T.y, T.width, i.y - T.y);
2906
+ const w = i.y + i.height, m = T.y + T.height;
2907
+ w < m && t.fillRect(T.x, w, T.width, m - w), i.x > T.x && t.fillRect(T.x, i.y, i.x - T.x, i.height);
2908
+ const C = i.x + i.width, R = T.x + T.width;
2909
+ C < R && t.fillRect(C, i.y, R - C, i.height), t.strokeStyle = ot.canvas.selectionColor, t.lineWidth = r, t.strokeRect(i.x, i.y, i.width, i.height), t.strokeStyle = "rgba(255, 255, 255, 0.5)", t.lineWidth = c;
2910
+ const M = i.width / 3;
2911
+ t.beginPath(), t.moveTo(i.x + M, i.y), t.lineTo(i.x + M, i.y + i.height), t.moveTo(i.x + 2 * M, i.y), t.lineTo(i.x + 2 * M, i.y + i.height), t.stroke();
2912
+ const k = i.height / 3;
2913
+ if (t.beginPath(), t.moveTo(i.x, i.y + k), t.lineTo(i.x + i.width, i.y + k), t.moveTo(i.x, i.y + 2 * k), t.lineTo(i.x + i.width, i.y + 2 * k), t.stroke(), [
2914
+ { x: i.x, y: i.y },
2915
+ // top-left
2916
+ { x: i.x + i.width, y: i.y },
2917
+ // top-right
2918
+ { x: i.x + i.width, y: i.y + i.height },
2919
+ // bottom-right
2920
+ { x: i.x, y: i.y + i.height }
2921
+ // bottom-left
2922
+ ].forEach((H) => {
2923
+ t.beginPath(), t.arc(H.x, H.y, l + 1 / n, 0, Math.PI * 2), t.fillStyle = "#bbb", t.fill(), t.beginPath(), t.arc(H.x, H.y, l, 0, Math.PI * 2), t.fillStyle = "#fff", t.fill();
2924
+ }), [
2925
+ { x: i.x + i.width / 2, y: i.y, orientation: "horizontal" },
2926
+ // top
2927
+ { x: i.x + i.width / 2, y: i.y + i.height, orientation: "horizontal" },
2928
+ // bottom
2929
+ { x: i.x, y: i.y + i.height / 2, orientation: "vertical" },
2930
+ // left
2931
+ { x: i.x + i.width, y: i.y + i.height / 2, orientation: "vertical" }
2932
+ // right
2933
+ ].forEach((H) => {
2934
+ let L, D;
2935
+ H.orientation === "horizontal" ? (L = h, D = a) : (L = a, D = h);
2936
+ const O = H.x - L / 2, W = H.y - D / 2;
2937
+ t.fillStyle = "#fff", t.fillRect(O, W, L, D), t.strokeStyle = "#bbb", t.lineWidth = c, t.strokeRect(O, W, L, D);
2938
+ }), t.restore(), o && o.resizeHandles) {
2939
+ const H = o.resizeHandles.filter(
2940
+ (W) => W.anchor === "top-left" || W.anchor === "top-right" || W.anchor === "bottom-left" || W.anchor === "bottom-right"
2941
+ ), L = s.getCropBoxWorldCorners(), D = 6, O = 20;
2942
+ H.forEach((W) => {
2943
+ let V = !1;
2944
+ if (L)
2945
+ for (const A of L) {
2946
+ const $ = W.x - A.x, j = W.y - A.y;
2947
+ if (Math.sqrt($ * $ + j * j) < O) {
2948
+ V = !0;
2949
+ break;
2950
+ }
2951
+ }
2952
+ V || (t.save(), t.globalAlpha = 1, t.beginPath(), t.arc(W.x * n, W.y * n, D, 0, 2 * Math.PI), t.fillStyle = "#fff", t.fill(), t.strokeStyle = tt(t.canvas), t.lineWidth = 2, t.stroke(), t.restore());
2953
+ });
2954
+ }
2955
+ }
2956
+ }
2957
+ const Xe = zt("CanvasRenderer"), K = class K {
2958
+ /**
2959
+ * Get a pooled offscreen canvas for clipping content, only recreating
2960
+ * when dimensions change. Eliminates per-frame canvas allocation.
2961
+ */
2962
+ static _getContentCanvas(t, e) {
2963
+ return (!this._contentCanvas || this._contentCanvasSize.width !== t || this._contentCanvasSize.height !== e) && (this._contentCanvas = document.createElement("canvas"), this._contentCanvas.width = t, this._contentCanvas.height = e, this._contentCanvasSize = { width: t, height: e }), this._contentCanvas;
2964
+ }
2965
+ /**
2966
+ * Get a pooled offscreen canvas for clipping masks, only recreating
2967
+ * when dimensions change. Eliminates per-frame canvas allocation.
2968
+ */
2969
+ static _getMaskCanvas(t, e) {
2970
+ return (!this._maskCanvas || this._maskCanvasSize.width !== t || this._maskCanvasSize.height !== e) && (this._maskCanvas = document.createElement("canvas"), this._maskCanvas.width = t, this._maskCanvas.height = e, this._maskCanvasSize = { width: t, height: e }), this._maskCanvas;
2971
+ }
2972
+ /**
2973
+ * Safely render an element, catching errors so one element's failure
2974
+ * doesn't crash the entire canvas render loop.
2975
+ */
2976
+ static safeRender(t, e, o, n) {
2977
+ try {
2978
+ e.render(t, o, n);
2979
+ } catch (s) {
2980
+ const i = s instanceof Error ? s : new Error(String(s));
2981
+ Xe.error(`Failed to render element ${e.id}:`, i), K.onRenderError && K.onRenderError({
2982
+ category: "render",
2983
+ message: `Failed to render element: ${i.message}`,
2984
+ originalError: i,
2985
+ elementId: e.id,
2986
+ recoverable: !0
2987
+ });
2988
+ }
2989
+ }
2990
+ // =========================================================================
2991
+ // Element Layer Rendering (kept here - tightly coupled knockout/clipping)
2992
+ // =========================================================================
2993
+ /**
2994
+ * Helper function to render an element (using actual element instance, not config)
2995
+ * Used by knockout rendering system
2996
+ */
2997
+ static renderElement(t, e, o = !0) {
2998
+ o && (t.save(), t.globalAlpha = Q), e.render(t, !1, !1), o && t.restore();
2999
+ }
3000
+ /**
3001
+ * Apply artboard clip path to the context, respecting clipShape.
3002
+ * After calling this, subsequent drawing will be clipped to the artboard shape.
3003
+ * Caller must call ctx.beginPath() before this if needed.
3004
+ */
3005
+ static applyArtboardClipPath(t, e) {
3006
+ const { x: o, y: n, width: s, height: i, clipShape: a } = e, r = e.borderRadius || 0;
3007
+ if (t.beginPath(), !a || a === "rectangle")
3008
+ r > 0 ? t.roundRect(o, n, s, i, r) : t.rect(o, n, s, i);
3009
+ else if (a === "circle") {
3010
+ const l = o + s / 2, h = n + i / 2, c = Math.min(s, i) / 2;
3011
+ t.arc(l, h, c, 0, Math.PI * 2);
3012
+ } else if (typeof a == "object" && a.type === "rounded") {
3013
+ const l = Math.min(a.radius, Math.min(s, i) / 2);
3014
+ t.roundRect(o, n, s, i, l);
3015
+ } else if (typeof a == "object" && a.type === "path")
3016
+ try {
3017
+ const l = new Path2D(a.d);
3018
+ t.translate(o, n), t.clip(l), t.translate(-o, -n);
3019
+ return;
3020
+ } catch {
3021
+ t.rect(o, n, s, i);
3022
+ }
3023
+ else if (typeof a == "object" && a.type === "composite-path")
3024
+ try {
3025
+ const l = new Path2D();
3026
+ for (const h of a.pieces) {
3027
+ const c = new Path2D(h.d), d = h.rotation ?? 0, u = new DOMMatrix();
3028
+ if (u.translateSelf(h.x, h.y), d !== 0) {
3029
+ const p = (h.baseWidth ?? 0) / 2, b = (h.baseHeight ?? 0) / 2, g = d === 90 || d === 270, f = g ? h.baseHeight ?? 0 : h.baseWidth ?? 0, y = g ? h.baseWidth ?? 0 : h.baseHeight ?? 0;
3030
+ u.translateSelf(f / 2, y / 2), u.rotateSelf(d), u.translateSelf(-p, -b);
3031
+ }
3032
+ l.addPath(c, u);
3033
+ }
3034
+ t.translate(o, n), t.clip(l), t.translate(-o, -n);
3035
+ return;
3036
+ } catch {
3037
+ t.rect(o, n, s, i);
3038
+ }
3039
+ t.clip();
3040
+ }
3041
+ /**
3042
+ * Render an element with overflow dimming - parts outside the artboard render at 30% opacity
3043
+ * Uses two-pass rendering: first at 30% opacity (overflow), then clipped to artboard at 100% (inside)
3044
+ */
3045
+ static renderElementWithOverflowDimming(t, e, o, n, s, i) {
3046
+ const a = t.getTransform(), r = o.borderRadius || 0;
3047
+ t.save(), t.globalAlpha = 0.3 * (i ? Q : 1), t.filter = "blur(4px)", t.beginPath();
3048
+ const h = t.getTransform().inverse(), c = t.canvas.width, d = t.canvas.height, u = new DOMPoint(0, 0).matrixTransform(h), p = new DOMPoint(c, d).matrixTransform(h), b = Math.max(p.x - u.x, p.y - u.y), g = u.x - b, f = u.y - b, y = p.x + b, S = p.y + b, T = typeof o.clipShape == "object" && o.clipShape !== null && o.clipShape.type === "composite-path" ? o.clipShape : null;
3049
+ if (T) {
3050
+ const w = new Path2D();
3051
+ w.rect(g, f, y - g, S - f);
3052
+ for (const m of T.pieces) {
3053
+ const C = new Path2D(m.d), R = m.rotation ?? 0, M = new DOMMatrix();
3054
+ if (M.translateSelf(o.x + m.x, o.y + m.y), R !== 0) {
3055
+ const k = (m.baseWidth ?? 0) / 2, v = (m.baseHeight ?? 0) / 2, P = R === 90 || R === 270, H = P ? m.baseHeight ?? 0 : m.baseWidth ?? 0, L = P ? m.baseWidth ?? 0 : m.baseHeight ?? 0;
3056
+ M.translateSelf(H / 2, L / 2), M.rotateSelf(R), M.translateSelf(-k, -v);
3057
+ }
3058
+ w.addPath(C, M);
3059
+ }
3060
+ t.clip(w, "evenodd");
3061
+ } else
3062
+ t.rect(g, f, y - g, S - f), r > 0 ? t.roundRect(o.x, o.y, o.width, o.height, r) : t.rect(o.x, o.y, o.width, o.height), t.clip("evenodd");
3063
+ e.render(t, !1, !1), t.restore(), t.save(), this.applyArtboardClipPath(t, o), i && (t.globalAlpha = Q), e.render(t, n, s), t.restore(), t.setTransform(a);
3064
+ }
3065
+ /**
3066
+ * Render all elements layer
3067
+ * @param showOverflowDimmed When true, element parts outside the artboard render at 30% opacity
3068
+ * @param suppressOverflowDim When true, skip the 30% overflow pass even if `showOverflowDimmed`
3069
+ * is enabled. Used for piece-guide rendering (ADR-0054) where the boundary's custom path
3070
+ * clipShape is the visible design edge — overflow beyond the rectangular artboard reads as
3071
+ * a coloured halo rather than useful UX. Selected/hovered elements fall through to the
3072
+ * clipped-only branch which respects `clipShape` correctly.
3073
+ */
3074
+ static renderElementsLayer(t, e, o, n, s, i, a, r = !0, l = !1, h = !1) {
3075
+ const c = s != null && s.getMode() === "drag", d = s == null ? void 0 : s._activeChildContext;
3076
+ e !== K._cachedElementsRef && (K._cachedElementConfigs = e.map((f) => f.toJSON()), K._cachedElementMap = new Map(e.map((f) => [f.id, f])), K._cachedElementsRef = e);
3077
+ const u = K._cachedElementConfigs, p = K._cachedElementMap, b = K._knockoutRenderedIds;
3078
+ b.clear(), e.forEach((f, y) => {
3079
+ var S, T, w;
3080
+ if (f.blendMode === "knockout" || f.blendMode === "clip") {
3081
+ const m = u[y], C = f.blendMode === "knockout" ? "destination-out" : "destination-in", R = ((S = m.knockoutParts) == null ? void 0 : S.scope) || "group", M = u.findIndex((H) => H.id === m.id), k = ((T = m.knockoutParts) == null ? void 0 : T.fill) || ((w = m.knockoutParts) == null ? void 0 : w.stroke), v = R === "artboard" && M !== -1 ? u.slice(0, M) : [];
3082
+ k && (R === "artboard" || v.length > 0) && (v.forEach((H) => H.id && b.add(H.id)), Ie(
3083
+ t,
3084
+ m,
3085
+ u,
3086
+ (H, L) => {
3087
+ const D = L.id && p.get(L.id);
3088
+ D && this.renderElement(H, D, r);
3089
+ },
3090
+ i,
3091
+ a,
3092
+ C
3093
+ ), b.add(f.id));
3094
+ }
3095
+ }), e.some((f) => !b.has(f.id) && f.isClipping) ? this._renderWithClipping(
3096
+ t,
3097
+ e,
3098
+ b,
3099
+ o,
3100
+ n,
3101
+ c,
3102
+ d,
3103
+ u,
3104
+ r
3105
+ ) : e.forEach((f) => {
3106
+ var R, M;
3107
+ if (b.has(f.id) || f.visible === !1) return;
3108
+ const y = f.id === o, S = (n == null ? void 0 : n.type) === "element" && ((R = n == null ? void 0 : n.data) == null ? void 0 : R.id) === f.id, T = c && d && Dt(f) && f.id === o, w = y && !T, m = u[e.indexOf(f)], C = ((M = m == null ? void 0 : m.distressEffect) == null ? void 0 : M.enabled) && m.distressEffect.style === "custom" && m.distressEffect.textureUrl;
3109
+ if (m && m.masks && m.masks.length > 0)
3110
+ Et(t, m, (k) => {
3111
+ const v = (P) => {
3112
+ r && (P.save(), P.globalAlpha = Q), this.safeRender(P, f, w, S), r && P.restore();
3113
+ };
3114
+ if (C) {
3115
+ const P = f.getVisualBoundingBox();
3116
+ Nt(k, m.distressEffect, P, v);
3117
+ } else
3118
+ v(k);
3119
+ });
3120
+ else if (C) {
3121
+ const k = f.getVisualBoundingBox();
3122
+ Nt(t, m.distressEffect, k, (v) => {
3123
+ r && (v.save(), v.globalAlpha = Q), this.safeRender(v, f, w, S), r && v.restore();
3124
+ });
3125
+ } else l && !h && a && w ? this.renderElementWithOverflowDimming(t, f, a, w, S, r) : l && a ? (t.save(), this.applyArtboardClipPath(t, a), r && (t.globalAlpha = Q), this.safeRender(t, f, w, S), t.restore()) : (r && (t.save(), t.globalAlpha = Q), this.safeRender(t, f, w, S), r && t.restore());
3126
+ });
3127
+ }
3128
+ /**
3129
+ * Render elements with clipping mask support at artboard level
3130
+ * Elements with isClipping=true clip all elements below them
3131
+ */
3132
+ static _renderWithClipping(t, e, o, n, s, i, a, r, l = !0) {
3133
+ var f;
3134
+ const h = t.canvas, c = h.width, d = h.height, u = t.getTransform(), p = [];
3135
+ let b = [], g = [];
3136
+ for (let y = 0; y < e.length; y++) {
3137
+ const S = e[y], T = r[y];
3138
+ o.has(S.id) || S.visible !== !1 && (S.isClipping ? b.length > 0 ? (p.push({
3139
+ content: b,
3140
+ mask: S,
3141
+ contentConfigs: g,
3142
+ maskConfig: T
3143
+ }), b = [], g = []) : p.push({
3144
+ content: [S],
3145
+ contentConfigs: [T]
3146
+ }) : (b.push(S), g.push(T)));
3147
+ }
3148
+ b.length > 0 && p.push({
3149
+ content: b,
3150
+ contentConfigs: g
3151
+ });
3152
+ for (const y of p)
3153
+ if (!y.mask)
3154
+ y.content.forEach((S, T) => {
3155
+ var k;
3156
+ const w = S.id === n, m = (s == null ? void 0 : s.type) === "element" && ((k = s == null ? void 0 : s.data) == null ? void 0 : k.id) === S.id, C = i && a && Dt(S) && S.id === n, R = w && !C, M = y.contentConfigs[T];
3157
+ M && M.masks && M.masks.length > 0 ? Et(t, M, (v) => {
3158
+ l && (v.save(), v.globalAlpha = Q), S.render(v, R, m), l && v.restore();
3159
+ }) : (l && (t.save(), t.globalAlpha = Q), S.render(t, R, m), l && t.restore());
3160
+ });
3161
+ else {
3162
+ const S = this._getContentCanvas(c, d), T = S.getContext("2d");
3163
+ if (!T) continue;
3164
+ T.setTransform(1, 0, 0, 1, 0, 0), T.clearRect(0, 0, c, d), T.globalCompositeOperation = "source-over", T.setTransform(u), y.content.forEach((M, k) => {
3165
+ var O;
3166
+ const v = M.id === n, P = (s == null ? void 0 : s.type) === "element" && ((O = s == null ? void 0 : s.data) == null ? void 0 : O.id) === M.id, H = i && a && Dt(M) && M.id === n, L = v && !H, D = y.contentConfigs[k];
3167
+ D && D.masks && D.masks.length > 0 ? Et(T, D, (W) => {
3168
+ l && (W.save(), W.globalAlpha = Q), M.render(W, L, P), l && W.restore();
3169
+ }) : (l && (T.save(), T.globalAlpha = Q), M.render(T, L, P), l && T.restore());
3170
+ });
3171
+ const w = this._getMaskCanvas(c, d), m = w.getContext("2d");
3172
+ if (!m) continue;
3173
+ m.setTransform(1, 0, 0, 1, 0, 0), m.clearRect(0, 0, c, d), m.globalCompositeOperation = "source-over", m.setTransform(u);
3174
+ const C = y.mask.id === n, R = (s == null ? void 0 : s.type) === "element" && ((f = s == null ? void 0 : s.data) == null ? void 0 : f.id) === y.mask.id;
3175
+ y.maskConfig && y.maskConfig.masks && y.maskConfig.masks.length > 0 ? Et(m, y.maskConfig, (M) => {
3176
+ l && (M.save(), M.globalAlpha = Q), y.mask.render(M, C, R), l && M.restore();
3177
+ }) : (l && (m.save(), m.globalAlpha = Q), y.mask.render(m, C, R), l && m.restore()), T.globalCompositeOperation = "destination-in", T.setTransform(1, 0, 0, 1, 0, 0), T.drawImage(w, 0, 0), t.save(), t.setTransform(1, 0, 0, 1, 0, 0), t.drawImage(S, 0, 0), t.restore();
3178
+ }
3179
+ }
3180
+ // =========================================================================
3181
+ // Delegated Methods (preserve public API, delegate to focused renderers)
3182
+ // =========================================================================
3183
+ /**
3184
+ * Render snap guides layer
3185
+ * @see InteractionFeedbackRenderer.renderSnapGuidesLayer
3186
+ */
3187
+ static renderSnapGuidesLayer(t, e = []) {
3188
+ xt.renderSnapGuidesLayer(t, e);
3189
+ }
3190
+ /**
3191
+ * Render spacing indicators layer
3192
+ * @see InteractionFeedbackRenderer.renderSpacingIndicatorsLayer
3193
+ */
3194
+ static renderSpacingIndicatorsLayer(t, e = []) {
3195
+ xt.renderSpacingIndicatorsLayer(t, e);
3196
+ }
3197
+ /**
3198
+ * Render a distance label with background
3199
+ * @see InteractionFeedbackRenderer.renderDistanceLabel
3200
+ */
3201
+ static renderDistanceLabel(t, e, o, n) {
3202
+ xt.renderDistanceLabel(t, e, o, n);
3203
+ }
3204
+ /**
3205
+ * Render selection box layer
3206
+ * @see SelectionRenderer.renderSelectionLayer
3207
+ */
3208
+ static renderSelectionLayer(t, e, o, n = 1) {
3209
+ St.renderSelectionLayer(t, e, o, n);
3210
+ }
3211
+ /**
3212
+ * Render position and size info below the selected element
3213
+ * @see SelectionRenderer.renderPositionSizeInfo
3214
+ */
3215
+ static renderPositionSizeInfo(t, e, o, n, s = 1) {
3216
+ St.renderPositionSizeInfo(t, e, o, n, s);
3217
+ }
3218
+ /**
3219
+ * Render transform handles layer
3220
+ * @see SelectionRenderer.renderHandlesLayer
3221
+ */
3222
+ static renderHandlesLayer(t, e, o, n, s, i, a, r = 1, l = !0) {
3223
+ St.renderHandlesLayer(
3224
+ t,
3225
+ e,
3226
+ o,
3227
+ n,
3228
+ s,
3229
+ i,
3230
+ a,
3231
+ r,
3232
+ l
3233
+ );
3234
+ }
3235
+ /**
3236
+ * Render hover highlights layer with fade-in animation
3237
+ * @see HoverRenderer.renderHoverLayer
3238
+ */
3239
+ static renderHoverLayer(t, e, o, n = !1, s = 1, i) {
3240
+ _t.renderHoverLayer(t, e, o, n, s, i);
3241
+ }
3242
+ /**
3243
+ * Render crop box layer (for images in crop mode)
3244
+ * @see EditModeRenderer.renderCropBoxLayer
3245
+ */
3246
+ static renderCropBoxLayer(t, e, o, n = 1) {
3247
+ Wt.renderCropBoxLayer(t, e, o, n);
3248
+ }
3249
+ // =========================================================================
3250
+ // Main Render Orchestration
3251
+ // =========================================================================
3252
+ /**
3253
+ * Complete rendering pipeline
3254
+ * Renders all layers in correct z-order
3255
+ */
3256
+ static render({
3257
+ ctx: t,
3258
+ elements: e,
3259
+ selectedId: o,
3260
+ selectedElement: n,
3261
+ // Chrome-related fields (transformHandles, currentRotation,
3262
+ // isRotating, hideHandles, isAltKeyPressed, showRotationHandle)
3263
+ // are accepted for backwards compatibility with the public type
3264
+ // signature exposed via `api/internals.ts`, but are no longer
3265
+ // consumed here — single-selection chrome is now drawn by
3266
+ // `renderInteractionChromeLayer` after piece guides. See that
3267
+ // method for the actual chrome rendering.
3268
+ hoverState: s,
3269
+ isEditing: i,
3270
+ stateMachine: a,
3271
+ snapGuides: r = [],
3272
+ spacingIndicators: l = [],
3273
+ artboardBackgroundColor: h,
3274
+ artboard: c,
3275
+ isPreview: d = !0,
3276
+ zoom: u = 1,
3277
+ paddingOffset: p = { x: 0, y: 0 },
3278
+ showOverflowDimmed: b = !1,
3279
+ suppressOverflowDim: g = !1,
3280
+ distressTexture: f,
3281
+ imageMask: y,
3282
+ editModeData: S
3283
+ }) {
3284
+ if (this.renderElementsLayer(
3285
+ t,
3286
+ e,
3287
+ o,
3288
+ s,
3289
+ a ?? void 0,
3290
+ h,
3291
+ c,
3292
+ d,
3293
+ b,
3294
+ g
3295
+ ), f != null && f.enabled && c && Be(t, f, c, h), y != null && y.enabled && c && He(t, y, c, h), i && S && n && Wt.renderEditModeLayer(t, n, S, u), u !== 1) {
3296
+ t.save();
3297
+ const C = t.getTransform().a / u, R = p.x * u * C, M = p.y * u * C;
3298
+ t.setTransform(C, 0, 0, C, R, M);
3299
+ }
3300
+ const T = r.map((m) => ({
3301
+ ...m,
3302
+ x: m.x !== void 0 ? m.x * u : void 0,
3303
+ y: m.y !== void 0 ? m.y * u : void 0,
3304
+ x1: m.x1 !== void 0 ? m.x1 * u : void 0,
3305
+ y1: m.y1 !== void 0 ? m.y1 * u : void 0,
3306
+ x2: m.x2 !== void 0 ? m.x2 * u : void 0,
3307
+ y2: m.y2 !== void 0 ? m.y2 * u : void 0
3308
+ })), w = l.map((m) => ({
3309
+ ...m,
3310
+ x: m.x !== void 0 ? m.x * u : void 0,
3311
+ y: m.y !== void 0 ? m.y * u : void 0,
3312
+ x1: m.x1 !== void 0 ? m.x1 * u : void 0,
3313
+ y1: m.y1 !== void 0 ? m.y1 * u : void 0,
3314
+ x2: m.x2 !== void 0 ? m.x2 * u : void 0,
3315
+ y2: m.y2 !== void 0 ? m.y2 * u : void 0,
3316
+ labelX: m.labelX * u,
3317
+ labelY: m.labelY * u
3318
+ }));
3319
+ xt.renderSnapGuidesLayer(t, T), xt.renderSpacingIndicatorsLayer(t, w), u !== 1 && t.restore();
3320
+ }
3321
+ /**
3322
+ * Single-selection interaction chrome: bbox, handles, position/size
3323
+ * info, hover outline, and the crop-mode alt branch. Hoisted out of
3324
+ * `render()` so the master render loop can call it *after*
3325
+ * `renderPieceGuides`. Otherwise the piece-guide partial die-cut
3326
+ * knockout (`destination-out` at alpha 0.7) erases 70% of the
3327
+ * chrome's alpha along with the artwork it's intentionally dimming.
3328
+ *
3329
+ * Renders at native screen scale: identical transform manipulation
3330
+ * to the original Layer 2 / 3–6 block in `render()` so chrome stays
3331
+ * crisp regardless of canvas zoom.
3332
+ */
3333
+ static renderInteractionChromeLayer({
3334
+ ctx: t,
3335
+ selectedElement: e,
3336
+ transformHandles: o,
3337
+ stateMachine: n,
3338
+ currentRotation: s,
3339
+ isRotating: i,
3340
+ hoverState: a,
3341
+ selectedId: r,
3342
+ hideHandles: l = !1,
3343
+ isEditing: h,
3344
+ isAltKeyPressed: c = !1,
3345
+ artboardBackgroundColor: d,
3346
+ hoverClipArtboard: u,
3347
+ showRotationHandle: p = !0,
3348
+ zoom: b = 1,
3349
+ paddingOffset: g = { x: 0, y: 0 }
3350
+ }) {
3351
+ if (b !== 1) {
3352
+ t.save();
3353
+ const S = t.getTransform().a / b, T = g.x * b * S, w = g.y * b * S;
3354
+ t.setTransform(S, 0, 0, S, T, w);
3355
+ }
3356
+ if (e && Ye(e) && e.isCropping)
3357
+ o && Wt.renderCropBoxLayer(t, e, o, b);
3358
+ else if (!l && n && St.renderSelectionLayer(t, e, n, b), o && n && St.renderPositionSizeInfo(t, e, o, n, b), !l && o && n && St.renderHandlesLayer(
3359
+ t,
3360
+ e,
3361
+ o,
3362
+ s,
3363
+ i,
3364
+ a,
3365
+ n,
3366
+ b,
3367
+ p
3368
+ ), !h) {
3369
+ if (u) {
3370
+ t.save();
3371
+ const y = u, S = y.x * b, T = y.y * b, w = y.width * b, m = y.height * b, C = (y.borderRadius ?? 0) * b;
3372
+ t.beginPath(), C > 0 ? t.roundRect(S, T, w, m, C) : t.rect(S, T, w, m), t.clip();
3373
+ }
3374
+ _t.renderHoverLayer(t, a, r, c, b, d), u && t.restore();
3375
+ }
3376
+ b !== 1 && t.restore();
3377
+ }
3378
+ };
3379
+ K._contentCanvas = null, K._contentCanvasSize = { width: 0, height: 0 }, K._maskCanvas = null, K._maskCanvasSize = { width: 0, height: 0 }, K._cachedElementsRef = null, K._cachedElementConfigs = null, K._cachedElementMap = null, K._knockoutRenderedIds = /* @__PURE__ */ new Set(), K.onRenderError = null;
3380
+ let qt = K;
3381
+ const Zt = zt("ImportManager"), Ft = class Ft {
3382
+ /**
3383
+ * Import workspace from JSON file
3384
+ */
3385
+ static async importFromJSON(t) {
3386
+ try {
3387
+ const e = await this.readFileAsText(t), o = JSON.parse(e);
3388
+ if (this.validateDocument(o), !this.SUPPORTED_VERSIONS.includes(o.metadata.version))
3389
+ throw new Error(
3390
+ `Unsupported version: ${o.metadata.version}. Supported versions: ${this.SUPPORTED_VERSIONS.join(", ")}`
3391
+ );
3392
+ if (o.metadata.version === "1.0.0")
3393
+ return this.migrateV1Document(o);
3394
+ const n = [], s = [];
3395
+ return o.artboards.forEach((i) => {
3396
+ const a = Xt.fromJSON({
3397
+ id: i.id,
3398
+ name: i.name,
3399
+ x: i.x,
3400
+ y: i.y,
3401
+ width: i.width,
3402
+ height: i.height,
3403
+ backgroundColor: i.backgroundColor,
3404
+ elementIds: i.elements.map((l) => l.id || "")
3405
+ }), r = i.elements.map((l) => {
3406
+ try {
3407
+ return Kt.createFromJSON(l);
3408
+ } catch (h) {
3409
+ return Zt.error("Failed to create element:", l, h), null;
3410
+ }
3411
+ }).filter((l) => l !== null);
3412
+ n.push(a), s.push(...r);
3413
+ }), await this.preloadAllImages(s), {
3414
+ success: !0,
3415
+ artboards: n,
3416
+ elements: s,
3417
+ activeArtboardId: o.activeArtboardId
3418
+ };
3419
+ } catch (e) {
3420
+ return {
3421
+ success: !1,
3422
+ error: e instanceof Error ? e.message : "Unknown error"
3423
+ };
3424
+ }
3425
+ }
3426
+ /**
3427
+ * Migrate v1 document (no artboards) to v2 (with artboards)
3428
+ */
3429
+ static async migrateV1Document(t) {
3430
+ var e, o, n;
3431
+ try {
3432
+ const s = t.elements || [], i = new Xt({
3433
+ id: "artboard-migrated",
3434
+ name: "Artboard 1",
3435
+ x: 0,
3436
+ y: 0,
3437
+ width: ((e = t.canvas) == null ? void 0 : e.width) || 1920,
3438
+ height: ((o = t.canvas) == null ? void 0 : o.height) || 1080,
3439
+ backgroundColor: ((n = t.canvas) == null ? void 0 : n.backgroundColor) || "#ffffff"
3440
+ }), a = s.map((r) => {
3441
+ try {
3442
+ return Kt.createFromJSON(r);
3443
+ } catch (l) {
3444
+ return Zt.error("Failed to create element:", r, l), null;
3445
+ }
3446
+ }).filter((r) => r !== null);
3447
+ return a.forEach((r) => {
3448
+ i.addElementId(r.id);
3449
+ }), await this.preloadAllImages(a), {
3450
+ success: !0,
3451
+ artboards: [i],
3452
+ elements: a,
3453
+ activeArtboardId: i.id
3454
+ };
3455
+ } catch (s) {
3456
+ return {
3457
+ success: !1,
3458
+ error: s instanceof Error ? s.message : "Migration failed"
3459
+ };
3460
+ }
3461
+ }
3462
+ /**
3463
+ * Import image file
3464
+ */
3465
+ static async importImage(t) {
3466
+ try {
3467
+ if (!t.type.startsWith("image/"))
3468
+ throw new Error("File is not an image");
3469
+ const e = 10 * 1024 * 1024;
3470
+ if (t.size > e)
3471
+ throw new Error("Image file is too large (max 10MB)");
3472
+ const o = await this.readFileAsDataURL(t), { width: n, height: s } = await this.getImageDimensions(o);
3473
+ return {
3474
+ success: !0,
3475
+ dataUrl: o,
3476
+ filename: t.name,
3477
+ width: n,
3478
+ height: s
3479
+ };
3480
+ } catch (e) {
3481
+ return {
3482
+ success: !1,
3483
+ error: e instanceof Error ? e.message : "Unknown error"
3484
+ };
3485
+ }
3486
+ }
3487
+ /**
3488
+ * Validate document structure
3489
+ */
3490
+ static validateDocument(t) {
3491
+ if (!t || typeof t != "object")
3492
+ throw new Error("Invalid document");
3493
+ const e = t;
3494
+ if (!e.metadata)
3495
+ throw new Error("Missing metadata");
3496
+ if (!e.metadata.version)
3497
+ throw new Error("Missing version in metadata");
3498
+ if (!e.artboards && !e.elements)
3499
+ throw new Error("Missing artboards or elements array");
3500
+ if (e.artboards && !Array.isArray(e.artboards))
3501
+ throw new Error("Invalid artboards array");
3502
+ }
3503
+ /**
3504
+ * Read file as text
3505
+ */
3506
+ static readFileAsText(t) {
3507
+ return new Promise((e, o) => {
3508
+ const n = new FileReader();
3509
+ n.onload = (s) => {
3510
+ var i;
3511
+ return e((i = s.target) == null ? void 0 : i.result);
3512
+ }, n.onerror = () => o(new Error("Failed to read file")), n.readAsText(t);
3513
+ });
3514
+ }
3515
+ /**
3516
+ * Read file as data URL
3517
+ */
3518
+ static readFileAsDataURL(t) {
3519
+ return new Promise((e, o) => {
3520
+ const n = new FileReader();
3521
+ n.onload = (s) => {
3522
+ var i;
3523
+ return e((i = s.target) == null ? void 0 : i.result);
3524
+ }, n.onerror = () => o(new Error("Failed to read file")), n.readAsDataURL(t);
3525
+ });
3526
+ }
3527
+ /**
3528
+ * Get image dimensions from data URL
3529
+ */
3530
+ static getImageDimensions(t) {
3531
+ return new Promise((e, o) => {
3532
+ const n = new Image();
3533
+ n.onload = () => {
3534
+ e({ width: n.width, height: n.height });
3535
+ }, n.onerror = () => o(new Error("Failed to load image")), n.src = t;
3536
+ });
3537
+ }
3538
+ /**
3539
+ * Validate imported data before applying to state
3540
+ */
3541
+ static validateImportedData(t, e) {
3542
+ const o = [], n = /* @__PURE__ */ new Set();
3543
+ t.forEach((i) => {
3544
+ n.has(i.id) && o.push(`Duplicate artboard ID: ${i.id}`), n.add(i.id);
3545
+ });
3546
+ const s = /* @__PURE__ */ new Set();
3547
+ return e.forEach((i) => {
3548
+ s.has(i.id) && o.push(`Duplicate element ID: ${i.id}`), s.add(i.id);
3549
+ }), t.forEach((i) => {
3550
+ i.getElementIds().forEach((a) => {
3551
+ s.has(a) || o.push(`Artboard "${i.name}" references missing element: ${a}`);
3552
+ });
3553
+ }), {
3554
+ valid: o.length === 0,
3555
+ errors: o
3556
+ };
3557
+ }
3558
+ /**
3559
+ * Get import summary (for UI display before confirming)
3560
+ */
3561
+ static getImportSummary(t, e) {
3562
+ const o = this.validateImportedData(t, e);
3563
+ return {
3564
+ artboardCount: t.length,
3565
+ elementCount: e.length,
3566
+ artboardNames: t.map((n) => n.name),
3567
+ warnings: o.errors
3568
+ };
3569
+ }
3570
+ /**
3571
+ * Preload all images in parallel for fast artboard switching
3572
+ * Recursively finds all ImageElements (including those in groups)
3573
+ */
3574
+ static async preloadAllImages(t) {
3575
+ const e = [], o = (s) => {
3576
+ s.transformType === "image" ? e.push(s) : s.transformType === "group" && "children" in s && s.children.forEach((i) => o(i));
3577
+ };
3578
+ t.forEach(o);
3579
+ const n = e.map((s) => new Promise((i) => {
3580
+ if (s.imageLoaded)
3581
+ i();
3582
+ else if (s.imageElement) {
3583
+ const a = s.imageElement, r = () => {
3584
+ a.removeEventListener("load", r), a.removeEventListener("error", l), i();
3585
+ }, l = () => {
3586
+ a.removeEventListener("load", r), a.removeEventListener("error", l), i();
3587
+ };
3588
+ a.addEventListener("load", r), a.addEventListener("error", l);
3589
+ } else
3590
+ i();
3591
+ }));
3592
+ await Promise.all(n);
3593
+ }
3594
+ };
3595
+ Ft.SUPPORTED_VERSIONS = ["1.0.0", "2.0.0"];
3596
+ let Jt = Ft;
3597
+ export {
3598
+ Qe as A,
3599
+ qt as C,
3600
+ Jt as I,
3601
+ Oe as R,
3602
+ tn as S,
3603
+ qe as T,
3604
+ Ye as a,
3605
+ sn as b,
3606
+ nn as c,
3607
+ en as d,
3608
+ oe as e,
3609
+ Je as f,
3610
+ $t as g,
3611
+ je as h,
3612
+ Dt as i,
3613
+ $e as j,
3614
+ Ze as k,
3615
+ Ue as r
3616
+ };
3617
+ //# sourceMappingURL=ImportManager-kRYfIX-D.js.map