@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.
- package/dist/{CanvasStateV1-BQD8fCBU.js → CanvasStateV1-C6i6vU6p.js} +63 -63
- package/dist/{CanvasStateV1-BQD8fCBU.js.map → CanvasStateV1-C6i6vU6p.js.map} +1 -1
- package/dist/EditorCanvasImpl-DkgE5-C2.js +68 -0
- package/dist/EditorCanvasImpl-DkgE5-C2.js.map +1 -0
- package/dist/EditorContext-CyanYzo2.js +1131 -0
- package/dist/EditorContext-CyanYzo2.js.map +1 -0
- package/dist/{ImportManager-eotl7r5d.js → ElementFactory-C05JJkRp.js} +1510 -5121
- package/dist/ElementFactory-C05JJkRp.js.map +1 -0
- package/dist/{HybridHistoryManager-NL3zyEl4.js → HybridHistoryManager-Bht98XRv.js} +98 -98
- package/dist/{HybridHistoryManager-NL3zyEl4.js.map → HybridHistoryManager-Bht98XRv.js.map} +1 -1
- package/dist/ImportManager-kRYfIX-D.js +3617 -0
- package/dist/ImportManager-kRYfIX-D.js.map +1 -0
- package/dist/SnowconeCanvas-C3eZ9PP8.js +1047 -0
- package/dist/SnowconeCanvas-C3eZ9PP8.js.map +1 -0
- package/dist/ThemeContext-Rjz25x9X.js +75 -0
- package/dist/ThemeContext-Rjz25x9X.js.map +1 -0
- package/dist/UnifiedFontService-BAtqqkU4.js +1478 -0
- package/dist/UnifiedFontService-BAtqqkU4.js.map +1 -0
- package/dist/advanced.mjs +210 -207
- package/dist/advanced.mjs.map +1 -1
- package/dist/api/embed.d.ts +8 -4
- package/dist/api/testing.d.ts +1 -0
- package/dist/components/editor/EditorBuy.d.ts +49 -0
- package/dist/components/editor/EditorCanvas.d.ts +30 -0
- package/dist/components/editor/EditorCanvasImpl.d.ts +33 -0
- package/dist/components/editor/EditorPreview.d.ts +39 -0
- package/dist/components/editor/EditorRoot.d.ts +86 -0
- package/dist/components/editor/EditorTextField.d.ts +22 -0
- package/dist/components/editor/RootInternals.d.ts +77 -0
- package/dist/components/editor/designStateStore.d.ts +45 -0
- package/dist/components/editor/index.d.ts +23 -0
- package/dist/components/embed-realtime/RealtimeCanvas.d.ts +1 -1
- package/dist/components/embed-realtime/RealtimeEmbed.d.ts +5 -78
- package/dist/components/embed-realtime/RealtimeMockup.d.ts +5 -34
- package/dist/components/embed-realtime/context.d.ts +12 -2
- package/dist/components/embed-realtime/validateEmbed.d.ts +17 -3
- package/dist/design/design-builder.d.ts +81 -6
- package/dist/editorError-DMENTZ-3.js +19 -0
- package/dist/editorError-DMENTZ-3.js.map +1 -0
- package/dist/editorErrors-DbIEtCTt.js +29 -0
- package/dist/editorErrors-DbIEtCTt.js.map +1 -0
- package/dist/embed-Bdj4PKUA.js +737 -0
- package/dist/embed-Bdj4PKUA.js.map +1 -0
- package/dist/embed.mjs +38 -230
- package/dist/embed.mjs.map +1 -1
- package/dist/errors/editorError.d.ts +57 -0
- package/dist/iconify-D6eVIEy4.js +1089 -0
- package/dist/iconify-D6eVIEy4.js.map +1 -0
- package/dist/index.mjs +48 -46
- package/dist/index.mjs.map +1 -1
- package/dist/internals.mjs +105 -104
- package/dist/internals.mjs.map +1 -1
- package/dist/template-fields-GOM4MNfq.js +643 -0
- package/dist/template-fields-GOM4MNfq.js.map +1 -0
- package/dist/testing/editorErrors.d.ts +30 -0
- package/dist/testing.mjs +28 -23
- package/dist/testing.mjs.map +1 -1
- package/dist/{usePieceGuideBundle-Cyl695jv.js → usePieceGuideBundle-CGERAQJl.js} +2 -2
- package/dist/{usePieceGuideBundle-Cyl695jv.js.map → usePieceGuideBundle-CGERAQJl.js.map} +1 -1
- package/dist/validation-DEd6EOCP.js +19947 -0
- package/dist/validation-DEd6EOCP.js.map +1 -0
- package/dist/workers/export-worker.bundle.string.d.ts +1 -1
- package/package.json +9 -4
- package/dist/ImportManager-eotl7r5d.js.map +0 -1
- package/dist/ThemeContext-D9AaUw9v.js +0 -1200
- package/dist/ThemeContext-D9AaUw9v.js.map +0 -1
- package/dist/template-fields-PcSPApl5.js +0 -1632
- package/dist/template-fields-PcSPApl5.js.map +0 -1
- package/dist/validation-BpEod0Rn.js +0 -22953
- 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
|