@pixerate/editor 0.41.0 → 0.43.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.
@@ -3,6 +3,7 @@ import {
3
3
  } from "../chunk-QVZZGT4X.js";
4
4
  import {
5
5
  PlanarMap,
6
+ angleToSvgLinearCoords,
6
7
  bezierPathBounds,
7
8
  bezierPathToSvg,
8
9
  closeContour,
@@ -14,10 +15,10 @@ import {
14
15
  fitCubics,
15
16
  flattenChain,
16
17
  formatNumber,
17
- gradientToSvgDef,
18
18
  hatchPath,
19
19
  isStraightCubic,
20
20
  normalizeBezierPath,
21
+ offsetPath,
21
22
  outlineStroke,
22
23
  pathBoolean,
23
24
  pathToContours,
@@ -40,17 +41,25 @@ import {
40
41
  windingNumber
41
42
  } from "../chunk-3LHWTGHW.js";
42
43
  import {
43
- mixColorsOklab
44
+ mixColorsOklab,
45
+ oklabToRgb,
46
+ rgbToOklab
44
47
  } from "../chunk-ZRJMJIQU.js";
45
48
  import {
46
49
  evaluateEasing
47
50
  } from "../chunk-UB2OY7FD.js";
48
51
  import {
49
- hexToRgba
52
+ hexToRgba,
53
+ normalizeTokenColor,
54
+ rgbaToHex
50
55
  } from "../chunk-TGD52FEZ.js";
51
56
 
52
57
  // src/vector/types.ts
53
58
  var VECTOR_CORNER_KINDS = ["round", "inverted", "chamfer"];
59
+ var VECTOR_GRADIENT_TYPES = ["linear", "radial", "conic", "reflected", "diamond"];
60
+ var VECTOR_GRADIENT_SPREADS = ["pad", "reflect", "repeat"];
61
+ var VECTOR_GRADIENT_INTERPOLATIONS = ["srgb", "linear", "oklab"];
62
+ var VECTOR_STROKE_GRADIENT_MODES = ["within", "along", "across"];
54
63
  var VECTOR_ARROWHEADS = ["triangle", "arrow", "open", "circle", "square", "bar"];
55
64
  var VECTOR_BLEND_SPACINGS = ["steps", "distance", "smoothColor"];
56
65
  var VECTOR_TOOLS = [
@@ -69,18 +78,824 @@ var VECTOR_TOOLS = [
69
78
  "width",
70
79
  "shapeBuilder",
71
80
  "paintBucket",
72
- "blend"
81
+ "blend",
82
+ "gradient"
73
83
  ];
74
84
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
75
85
  var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
86
+ var VECTOR_EFFECT_TYPES = ["trim", "roundCorners", "puckerBloat", "twist", "zigzag", "wiggle", "offset", "merge", "repeater"];
87
+ var VECTOR_MERGE_MODES = ["merge", "add", "subtract", "intersect", "exclude"];
76
88
 
77
- // src/vector/corners.ts
78
- var EPS = 1e-9;
79
- var MIN_SINE = 1e-6;
89
+ // src/vector/gradient.ts
90
+ var clamp = (v2, lo, hi) => Math.min(hi, Math.max(lo, v2));
91
+ var IDENTITY = [1, 0, 0, 1, 0, 0];
92
+ var mul = (a, b) => [
93
+ b[0] * a[0] + b[2] * a[1],
94
+ b[1] * a[0] + b[3] * a[1],
95
+ b[0] * a[2] + b[2] * a[3],
96
+ b[1] * a[2] + b[3] * a[3],
97
+ b[0] * a[4] + b[2] * a[5] + b[4],
98
+ b[1] * a[4] + b[3] * a[5] + b[5]
99
+ ];
100
+ var apply = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
101
+ var invert = (m) => {
102
+ const [a, b, c, d, e, f] = m;
103
+ const det = a * d - b * c;
104
+ if (!Number.isFinite(det) || Math.abs(det) < 1e-18) return null;
105
+ return [d / det, -b / det, -c / det, a / det, (c * f - d * e) / det, (b * e - a * f) / det];
106
+ };
107
+ var isIdentity = (m) => m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
80
108
  var sub = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
81
109
  var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
82
- var mul = (a, k) => ({ x: a.x * k, y: a.y * k });
110
+ var scale = (a, k) => ({ x: a.x * k, y: a.y * k });
83
111
  var dot = (a, b) => a.x * b.x + a.y * b.y;
112
+ var perp = (v2) => ({ x: -v2.y, y: v2.x });
113
+ var finitePoint = (p) => !!p && typeof p === "object" && Number.isFinite(p.x) && Number.isFinite(p.y);
114
+ var fmt = (n, digits = 4) => {
115
+ const k = 10 ** digits;
116
+ const v2 = Math.round(n * k) / k;
117
+ return Object.is(v2, -0) ? "0" : String(v2);
118
+ };
119
+ var fmtMatrix = (m) => `matrix(${m.map((n) => fmt(n, 6)).join(" ")})`;
120
+ function isVectorGradient(paint) {
121
+ return !!paint && typeof paint === "object" && VECTOR_GRADIENT_TYPES.includes(paint.type) && Array.isArray(paint.stops);
122
+ }
123
+ function parseVectorColor(color) {
124
+ const hex = normalizeTokenColor(color);
125
+ if (hex === null) return null;
126
+ const c = hexToRgba(hex);
127
+ return [c.r, c.g, c.b, c.a];
128
+ }
129
+ var toLinear = (c) => c <= 0.04045 ? c / 12.92 : ((c + 0.055) / 1.055) ** 2.4;
130
+ var toGamma = (c) => {
131
+ const v2 = clamp(c, 0, 1);
132
+ return v2 <= 31308e-7 ? 12.92 * v2 : 1.055 * v2 ** (1 / 2.4) - 0.055;
133
+ };
134
+ function intoSpace(space, [r, g, b]) {
135
+ if (space === "linear") return [toLinear(r), toLinear(g), toLinear(b)];
136
+ if (space === "oklab") {
137
+ const c = rgbToOklab(r, g, b);
138
+ return [c.l, c.a, c.b];
139
+ }
140
+ return [r, g, b];
141
+ }
142
+ function outOfSpace(space, [x, y, z]) {
143
+ if (space === "linear") return [toGamma(x), toGamma(y), toGamma(z)];
144
+ if (space === "oklab") {
145
+ const c = oklabToRgb({ l: x, a: y, b: z, alpha: 1 });
146
+ return [c.r, c.g, c.b];
147
+ }
148
+ return [clamp(x, 0, 1), clamp(y, 0, 1), clamp(z, 0, 1)];
149
+ }
150
+ var MID_MIN = 0.01;
151
+ var MID_MAX = 0.99;
152
+ var VECTOR_GRADIENT_MIN_STOPS = 2;
153
+ function gradientMidpointRemap(u, m) {
154
+ const mid = clamp(m, MID_MIN, MID_MAX);
155
+ if (Math.abs(mid - 0.5) < 1e-6) return u;
156
+ return u <= mid ? 0.5 * u / mid : 0.5 + 0.5 * (u - mid) / (1 - mid);
157
+ }
158
+ var hasMidpoint = (s) => s.midpoint !== void 0 && Number.isFinite(s.midpoint) && Math.abs(s.midpoint - 0.5) > 1e-3;
159
+ function sortedVectorGradientStops(g) {
160
+ return (g.stops ?? []).filter((s) => s && Number.isFinite(s.offset) && typeof s.color === "string").map((s, i) => ({ s: { ...s, offset: clamp(s.offset, 0, 1) }, i })).sort((a, b) => a.s.offset - b.s.offset || a.i - b.i).map((e) => e.s);
161
+ }
162
+ function vectorGradientNeedsExpansion(g) {
163
+ return (g.interpolation ?? "srgb") !== "srgb" || (g.smoothness ?? 0) > 0 || (g.stops ?? []).some(hasMidpoint);
164
+ }
165
+ function vectorGradientRamp(g) {
166
+ const space = g.interpolation && VECTOR_GRADIENT_INTERPOLATIONS.includes(g.interpolation) ? g.interpolation : "srgb";
167
+ const smooth = clamp(Number.isFinite(g.smoothness) ? g.smoothness : 0, 0, 1);
168
+ const pts = [];
169
+ for (const s of sortedVectorGradientStops(g)) {
170
+ const c = parseVectorColor(s.color);
171
+ if (!c) return null;
172
+ pts.push({ t: s.offset, v: intoSpace(space, c), a: c[3] * clamp(s.opacity ?? 1, 0, 1), mid: s.midpoint ?? 0.5 });
173
+ }
174
+ return {
175
+ sample(t) {
176
+ const n = pts.length;
177
+ if (!n) return [0, 0, 0, 0];
178
+ const first = pts[0];
179
+ const last = pts[n - 1];
180
+ const at2 = (p) => [...outOfSpace(space, p.v), p.a];
181
+ if (!(t > first.t) || n < 2) return at2(first);
182
+ if (t >= last.t) return at2(last);
183
+ let i = 0;
184
+ while (i + 2 < n && !(t >= pts[i].t && t <= pts[i + 1].t)) i++;
185
+ const p0 = pts[i];
186
+ const p1 = pts[i + 1];
187
+ const h = Math.max(p1.t - p0.t, 1e-6);
188
+ const u = gradientMidpointRemap((t - p0.t) / h, p0.mid);
189
+ const alpha = p0.a + (p1.a - p0.a) * u;
190
+ if (smooth <= 0) return [...outOfSpace(space, [0, 1, 2].map((c) => p0.v[c] + (p1.v[c] - p0.v[c]) * u)), alpha];
191
+ const tangent2 = (k) => {
192
+ const [a, b] = k === 0 ? [0, 1] : k === n - 1 ? [n - 2, n - 1] : [k - 1, k + 1];
193
+ const d = Math.max(pts[b].t - pts[a].t, 1e-6);
194
+ const half = k === 0 || k === n - 1 ? 0.5 : 1;
195
+ return [0, 1, 2].map((c) => (pts[b].v[c] - pts[a].v[c]) / d * half);
196
+ };
197
+ const m0 = tangent2(i);
198
+ const m1 = tangent2(i + 1);
199
+ const u2 = u * u;
200
+ const u3 = u2 * u;
201
+ const h00 = 2 * u3 - 3 * u2 + 1;
202
+ const h10 = u3 - 2 * u2 + u;
203
+ const h01 = -2 * u3 + 3 * u2;
204
+ const h11 = u3 - u2;
205
+ const v2 = [0, 1, 2].map((c) => {
206
+ const linear = p0.v[c] + (p1.v[c] - p0.v[c]) * u;
207
+ const spline = h00 * p0.v[c] + h10 * h * m0[c] + h01 * p1.v[c] + h11 * h * m1[c];
208
+ const value = linear + (spline - linear) * smooth;
209
+ return clamp(value, Math.min(p0.v[c], p1.v[c]), Math.max(p0.v[c], p1.v[c]));
210
+ });
211
+ return [...outOfSpace(space, v2), alpha];
212
+ }
213
+ };
214
+ }
215
+ function sampleVectorGradient(g, t) {
216
+ const ramp = vectorGradientRamp(g);
217
+ if (ramp) {
218
+ const [r, gg, b, a] = ramp.sample(t);
219
+ return { color: rgbaToHex(r, gg, b), opacity: a };
220
+ }
221
+ const stops = sortedVectorGradientStops(g);
222
+ const s = [...stops].reverse().find((x) => x.offset <= t) ?? stops[0];
223
+ return s ? { color: s.color, opacity: s.opacity ?? 1 } : { color: "#000000", opacity: 0 };
224
+ }
225
+ var stopOf = (offset, c) => {
226
+ const color = rgbaToHex(c[0], c[1], c[2]);
227
+ const opacity = Math.round(c[3] * 1e4) / 1e4;
228
+ return opacity < 1 ? { offset, color, opacity } : { offset, color };
229
+ };
230
+ var maxDiff = (a, b) => Math.max(Math.abs(a[0] - b[0]), Math.abs(a[1] - b[1]), Math.abs(a[2] - b[2]), Math.abs(a[3] - b[3]));
231
+ var lerpRgba = (a, b, u) => [0, 1, 2, 3].map((c) => a[c] + (b[c] - a[c]) * u);
232
+ function expandVectorGradientStops(g, options = {}) {
233
+ const stops = sortedVectorGradientStops(g);
234
+ const plain3 = () => stops.map(({ offset, color, opacity }) => opacity !== void 0 && opacity !== 1 ? { offset, color, opacity } : { offset, color });
235
+ if (!vectorGradientNeedsExpansion(g)) return plain3();
236
+ const ramp = vectorGradientRamp(g);
237
+ if (!ramp || stops.length < 2) return plain3();
238
+ const tol = options.tolerance ?? 0.5 / 255;
239
+ const sRgbOnly = (g.interpolation ?? "srgb") === "srgb" && !((g.smoothness ?? 0) > 0);
240
+ const own = (s) => {
241
+ const c = parseVectorColor(s.color);
242
+ return [c[0], c[1], c[2], c[3] * clamp(s.opacity ?? 1, 0, 1)];
243
+ };
244
+ const out = [];
245
+ const push = (offset, c) => out.push(stopOf(offset, c));
246
+ for (let i = 0; i < stops.length; i++) {
247
+ const a = stops[i];
248
+ const b = stops[i + 1];
249
+ push(a.offset, own(a));
250
+ if (!b || b.offset - a.offset < 1e-9) continue;
251
+ const knots = [a.offset];
252
+ if (hasMidpoint(a)) knots.push(a.offset + (b.offset - a.offset) * clamp(a.midpoint, MID_MIN, MID_MAX));
253
+ knots.push(b.offset);
254
+ const inside = (t) => ramp.sample(clamp(t, a.offset + 1e-9, b.offset - 1e-9));
255
+ for (let k = 0; k + 1 < knots.length; k++) {
256
+ const t0 = knots[k];
257
+ const t1 = knots[k + 1];
258
+ if (k > 0) push(t0, inside(t0));
259
+ if (sRgbOnly) continue;
260
+ const refine = (x0, c0, x1, c1, depth) => {
261
+ const xm = (x0 + x1) / 2;
262
+ const cm = inside(xm);
263
+ const ok = depth >= 8 || [0.03, 0.1, 0.25, 0.5, 0.75, 0.9, 0.97].every((f) => maxDiff(inside(x0 + (x1 - x0) * f), lerpRgba(c0, c1, f)) <= tol);
264
+ if (ok) return;
265
+ refine(x0, c0, xm, cm, depth + 1);
266
+ push(xm, cm);
267
+ refine(xm, cm, x1, c1, depth + 1);
268
+ };
269
+ refine(t0, inside(t0), t1, inside(t1), 0);
270
+ }
271
+ }
272
+ return out;
273
+ }
274
+ function samplePlainStops(stops, t) {
275
+ const parsed = stops.map((s) => {
276
+ const c = parseVectorColor(s.color) ?? [0, 0, 0, 1];
277
+ return { t: s.offset, c: [c[0], c[1], c[2], c[3] * (s.opacity ?? 1)] };
278
+ });
279
+ if (!parsed.length) return [0, 0, 0, 0];
280
+ if (t <= parsed[0].t) return parsed[0].c;
281
+ for (let i = 0; i + 1 < parsed.length; i++) {
282
+ const a = parsed[i];
283
+ const b = parsed[i + 1];
284
+ if (t <= b.t) return b.t - a.t < 1e-12 ? b.c : lerpRgba(a.c, b.c, (t - a.t) / (b.t - a.t));
285
+ }
286
+ return parsed[parsed.length - 1].c;
287
+ }
288
+ function reverseVectorGradient(g) {
289
+ const stops = sortedVectorGradientStops(g).reverse();
290
+ const next = stops.map((s, i) => {
291
+ const following = stops[i + 1];
292
+ const mid = following ? 1 - (following.midpoint ?? 0.5) : 0.5;
293
+ const { midpoint: _m, ...rest } = s;
294
+ return { ...rest, offset: 1 - s.offset, ...Math.abs(mid - 0.5) > 1e-6 ? { midpoint: mid } : {} };
295
+ });
296
+ return { ...g, stops: next };
297
+ }
298
+ function placeStop(stops, stop) {
299
+ let i = stops.findIndex((o) => o.offset > stop.offset);
300
+ if (i < 0) i = stops.length;
301
+ stops.splice(i, 0, stop);
302
+ return i;
303
+ }
304
+ function insertVectorGradientStop(g, offset) {
305
+ const at2 = clamp(offset, 0, 1);
306
+ const { color, opacity } = sampleVectorGradient(g, at2);
307
+ const stops = sortedVectorGradientStops(g);
308
+ const index = placeStop(stops, opacity < 1 ? { offset: at2, color, opacity } : { offset: at2, color });
309
+ return { stops, index };
310
+ }
311
+ function moveVectorGradientStop(stops, index, offset) {
312
+ const next = stops.map((s2) => ({ ...s2 }));
313
+ if (index < 0 || index >= next.length) return { stops: next, index };
314
+ const [s] = next.splice(index, 1);
315
+ return { stops: next, index: placeStop(next, { ...s, offset: clamp(offset, 0, 1) }) };
316
+ }
317
+ function removeVectorGradientStop(stops, index) {
318
+ if (stops.length <= VECTOR_GRADIENT_MIN_STOPS || index < 0 || index >= stops.length) return null;
319
+ return stops.filter((_, i) => i !== index);
320
+ }
321
+ function vectorGradientMidpointPosition(stops, index) {
322
+ const a = stops[index];
323
+ const b = stops[index + 1];
324
+ if (!a || !b) return null;
325
+ return a.offset + (b.offset - a.offset) * (a.midpoint ?? 0.5);
326
+ }
327
+ function vectorGradientMidpointFromPosition(stops, index, pos) {
328
+ const a = stops[index];
329
+ const b = stops[index + 1];
330
+ if (!a || !b) return null;
331
+ const span = Math.max(b.offset - a.offset, 1e-6);
332
+ return clamp((pos - a.offset) / span, 0.05, 0.95);
333
+ }
334
+ function spanVectorGradientStops(g, from, to, expanded = expandVectorGradientStops(g)) {
335
+ if (to < from) {
336
+ const s = spanVectorGradientStops(g, to, from, expanded);
337
+ return s.map((x) => ({ ...x, offset: 1 - x.offset })).reverse();
338
+ }
339
+ const d = to - from;
340
+ const at2 = (offset, t) => stopOf(offset, samplePlainStops(expanded, t));
341
+ const out = [at2(0, from)];
342
+ if (d > 1e-9) {
343
+ for (const s of expanded) if (s.offset > from && s.offset < to) out.push({ ...s, offset: (s.offset - from) / d });
344
+ }
345
+ out.push(at2(1, to));
346
+ return out;
347
+ }
348
+ var hasExplicitGradientGeometry = (g) => finitePoint(g.start) && finitePoint(g.end);
349
+ function shorthandPoints(g) {
350
+ if (g.type === "linear" || g.type === "reflected") {
351
+ const c = angleToSvgLinearCoords(Number.isFinite(g.angle) ? g.angle : 180);
352
+ const start2 = g.type === "linear" ? { x: c.x1, y: c.y1 } : { x: 0.5, y: 0.5 };
353
+ const end = { x: c.x2, y: c.y2 };
354
+ return { start: start2, end, width: add(start2, perp(sub(end, start2))) };
355
+ }
356
+ const start = { x: Number.isFinite(g.cx) ? g.cx : 0.5, y: Number.isFinite(g.cy) ? g.cy : 0.5 };
357
+ const r = Number.isFinite(g.r) ? g.r : 0.5;
358
+ if (g.type === "radial") return { start, end: { x: start.x + r, y: start.y }, width: { x: start.x, y: start.y + r } };
359
+ const a = (Number.isFinite(g.angle) ? g.angle : 90) * Math.PI / 180;
360
+ const dir = { x: Math.sin(a), y: -Math.cos(a) };
361
+ return { start, end: add(start, scale(dir, r)), width: add(start, scale(perp(dir), r)) };
362
+ }
363
+ function vectorGradientPoints(g) {
364
+ const explicit = hasExplicitGradientGeometry(g);
365
+ const base = explicit ? null : shorthandPoints(g);
366
+ const start = explicit ? { x: g.start.x, y: g.start.y } : base.start;
367
+ const end = explicit ? { x: g.end.x, y: g.end.y } : base.end;
368
+ const width = explicit ? finitePoint(g.width) ? { x: g.width.x, y: g.width.y } : add(start, perp(sub(end, start))) : base.width;
369
+ return {
370
+ start,
371
+ end,
372
+ width,
373
+ focal: g.type === "radial" && finitePoint(g.focal) ? { x: g.focal.x, y: g.focal.y } : null,
374
+ focalRadius: g.type === "radial" ? clamp(Number.isFinite(g.focalRadius) ? g.focalRadius : 0, 0, 0.99) : 0,
375
+ units: explicit && g.units === "userSpaceOnUse" ? "userSpaceOnUse" : "objectBoundingBox"
376
+ };
377
+ }
378
+ function vectorBoxMatrix(bounds) {
379
+ if (!bounds) return IDENTITY;
380
+ let { x, y, width: w, height: h } = bounds;
381
+ const fallback = Math.max(w, h, 1e-9) > 1e-9 ? Math.max(w, h) : 1;
382
+ if (!(w > 1e-9)) {
383
+ x -= fallback / 2 - w / 2;
384
+ w = fallback;
385
+ }
386
+ if (!(h > 1e-9)) {
387
+ y -= fallback / 2 - h / 2;
388
+ h = fallback;
389
+ }
390
+ return [w, 0, 0, h, x, y];
391
+ }
392
+ var framePointsMatrix = (p) => [p.end.x - p.start.x, p.end.y - p.start.y, p.width.x - p.start.x, p.width.y - p.start.y, p.start.x, p.start.y];
393
+ function vectorGradientUnitsMatrix(g, bounds) {
394
+ return vectorGradientPoints(g).units === "objectBoundingBox" ? vectorBoxMatrix(bounds) : IDENTITY;
395
+ }
396
+ function vectorGradientMatrix(g, bounds) {
397
+ return mul(framePointsMatrix(vectorGradientPoints(g)), vectorGradientUnitsMatrix(g, bounds));
398
+ }
399
+ var FOCAL_MAX = 0.99;
400
+ function unitFocal(g) {
401
+ const p = vectorGradientPoints(g);
402
+ if (!p.focal && !(p.focalRadius > 0)) return null;
403
+ const inv = invert(framePointsMatrix(p));
404
+ if (!inv) return null;
405
+ let f = p.focal ? apply(inv, p.focal) : { x: 0, y: 0 };
406
+ const reach = FOCAL_MAX * (1 - p.focalRadius);
407
+ const r = Math.hypot(f.x, f.y);
408
+ if (r > reach) f = scale(f, reach / r);
409
+ return { f, fr: p.focalRadius };
410
+ }
411
+ function vectorGradientFocalParam(q, f = { x: 0, y: 0 }, fr = 0) {
412
+ const d = sub(q, f);
413
+ const dr = 1 - fr;
414
+ const a = dot(f, f) - dr * dr;
415
+ const b = dot(d, f) - fr * dr;
416
+ const c = dot(d, d) - fr * fr;
417
+ if (Math.abs(a) < 1e-12) return Math.abs(b) < 1e-12 ? 0 : -c / (2 * b);
418
+ return (-b - Math.sqrt(Math.max(0, b * b - a * c))) / a;
419
+ }
420
+ function vectorGradientParam(type, q, focal) {
421
+ switch (type) {
422
+ case "linear":
423
+ return q.x;
424
+ case "reflected":
425
+ return Math.abs(q.x);
426
+ case "diamond":
427
+ return Math.abs(q.x) + Math.abs(q.y);
428
+ case "conic": {
429
+ const turn = Math.atan2(q.y, q.x) / (2 * Math.PI);
430
+ return turn - Math.floor(turn);
431
+ }
432
+ case "radial":
433
+ return focal ? vectorGradientFocalParam(q, focal.f, focal.fr) : Math.hypot(q.x, q.y);
434
+ }
435
+ }
436
+ function applyVectorGradientSpread(t, spread = "pad") {
437
+ if (!Number.isFinite(t)) return 0;
438
+ if (spread === "repeat") return t - Math.floor(t);
439
+ if (spread === "reflect") {
440
+ const m = (t % 2 + 2) % 2;
441
+ return m <= 1 ? m : 2 - m;
442
+ }
443
+ return clamp(t, 0, 1);
444
+ }
445
+ function vectorGradientTAt(g, bounds, p) {
446
+ const inv = invert(vectorGradientMatrix(g, bounds));
447
+ if (!inv) return 0;
448
+ const raw = vectorGradientParam(g.type, apply(inv, p), g.type === "radial" ? unitFocal(g) : null);
449
+ return g.type === "conic" ? raw : applyVectorGradientSpread(raw, g.spread ?? "pad");
450
+ }
451
+ function sampleVectorGradientAt(g, bounds, p) {
452
+ const t = vectorGradientTAt(g, bounds, p);
453
+ const ramp = vectorGradientRamp(g);
454
+ return ramp ? ramp.sample(t) : samplePlainStops(sortedVectorGradientStops(g), t);
455
+ }
456
+ function vectorGradientAspect(g) {
457
+ const p = vectorGradientPoints(g);
458
+ const u = sub(p.end, p.start);
459
+ const l2 = dot(u, u);
460
+ return l2 > 1e-18 ? dot(sub(p.width, p.start), perp(u)) / l2 : 1;
461
+ }
462
+ function explicitVectorGradient(g) {
463
+ const p = vectorGradientPoints(g);
464
+ const { angle: _a, cx: _cx, cy: _cy, r: _r, ...rest } = g;
465
+ return {
466
+ ...rest,
467
+ start: p.start,
468
+ end: p.end,
469
+ width: p.width,
470
+ ...p.focal ? { focal: p.focal } : {},
471
+ units: p.units
472
+ };
473
+ }
474
+ function pinVectorGradient(g, bounds) {
475
+ const e = explicitVectorGradient(g);
476
+ if (e.units === "userSpaceOnUse") return e;
477
+ const m = vectorBoxMatrix(bounds);
478
+ return { ...e, start: apply(m, e.start), end: apply(m, e.end), width: apply(m, e.width), ...e.focal ? { focal: apply(m, e.focal) } : {}, units: "userSpaceOnUse" };
479
+ }
480
+ function transformVectorGradient(g, m) {
481
+ if (!hasExplicitGradientGeometry(g) || g.units !== "userSpaceOnUse" || isIdentity(m)) return g;
482
+ const p = vectorGradientPoints(g);
483
+ return { ...g, start: apply(m, p.start), end: apply(m, p.end), width: apply(m, p.width), ...p.focal ? { focal: apply(m, p.focal) } : {} };
484
+ }
485
+ function transformVectorPaint(paint, m) {
486
+ return isVectorGradient(paint) ? transformVectorGradient(paint, m) : paint;
487
+ }
488
+ var isCentredVectorGradient = (type) => type === "radial" || type === "conic" || type === "diamond";
489
+ function vectorGradientHandles(g, bounds, toWorld = IDENTITY) {
490
+ const p = vectorGradientPoints(g);
491
+ const m = mul(vectorGradientUnitsMatrix(g, bounds), toWorld);
492
+ const f = g.type === "radial" ? unitFocal(g) : null;
493
+ const frame2 = mul(framePointsMatrix(p), m);
494
+ return {
495
+ start: apply(m, p.start),
496
+ end: apply(m, p.end),
497
+ aspect: isCentredVectorGradient(g.type) ? apply(m, p.width) : null,
498
+ focal: g.type === "radial" ? apply(frame2, f?.f ?? { x: 0, y: 0 }) : null
499
+ };
500
+ }
501
+ function withMainAxis(p, start, end) {
502
+ const before = invert(framePointsMatrix(p));
503
+ const u = sub(p.end, p.start);
504
+ const l2 = dot(u, u);
505
+ const w = sub(p.width, p.start);
506
+ const rel = l2 > 1e-18 ? { a: dot(w, u) / l2, b: dot(w, perp(u)) / l2 } : { a: 0, b: 1 };
507
+ const u2 = sub(end, start);
508
+ const width = add(start, add(scale(u2, rel.a), scale(perp(u2), rel.b)));
509
+ const next = { start, end, width };
510
+ const focal = p.focal && before ? apply(framePointsMatrix(next), apply(before, p.focal)) : p.focal;
511
+ return { ...next, focal };
512
+ }
513
+ function moveVectorGradientHandle(g, bounds, handle, to, from = to) {
514
+ const pinned = pinVectorGradient(g, bounds);
515
+ const p = vectorGradientPoints(pinned);
516
+ const set = (q) => {
517
+ const next = { ...pinned, ...q };
518
+ if (next.focal === null || next.focal === void 0) delete next.focal;
519
+ return next;
520
+ };
521
+ switch (handle) {
522
+ case "start": {
523
+ const d = sub(to, from);
524
+ return set({ start: add(p.start, d), end: add(p.end, d), width: add(p.width, d), focal: p.focal ? add(p.focal, d) : null });
525
+ }
526
+ case "end":
527
+ case "radius": {
528
+ if (Math.hypot(to.x - p.start.x, to.y - p.start.y) < 1e-6) return pinned;
529
+ const n = withMainAxis(p, p.start, to);
530
+ return set({ end: n.end, width: n.width, focal: n.focal });
531
+ }
532
+ case "aspect": {
533
+ const u = sub(p.end, p.start);
534
+ const l2 = dot(u, u);
535
+ if (l2 < 1e-18) return pinned;
536
+ let aspect = dot(sub(to, p.start), perp(u)) / l2;
537
+ if (Math.abs(aspect) < 0.01) aspect = aspect < 0 ? -0.01 : 0.01;
538
+ const before = invert(framePointsMatrix(p));
539
+ const width = add(p.start, scale(perp(u), aspect));
540
+ const focal = p.focal && before ? apply(framePointsMatrix({ start: p.start, end: p.end, width }), apply(before, p.focal)) : null;
541
+ return set({ width, focal });
542
+ }
543
+ case "focal": {
544
+ const frame2 = framePointsMatrix(p);
545
+ const inv = invert(frame2);
546
+ if (!inv) return pinned;
547
+ let q = apply(inv, to);
548
+ const r = Math.hypot(q.x, q.y);
549
+ if (r < 1e-6) return set({ focal: null });
550
+ const reach = FOCAL_MAX * (1 - p.focalRadius);
551
+ if (r > reach) q = scale(q, reach / r);
552
+ return set({ focal: apply(frame2, q) });
553
+ }
554
+ }
555
+ }
556
+ function setVectorGradientVector(g, bounds, start, end) {
557
+ const pinned = pinVectorGradient(g, bounds);
558
+ const p = vectorGradientPoints(pinned);
559
+ const aspect = Math.abs(vectorGradientAspect(pinned)) > 1e-6 ? vectorGradientAspect(pinned) : 1;
560
+ const u = sub(end, start);
561
+ const width = add(start, scale(perp(u), aspect));
562
+ const before = invert(framePointsMatrix(p));
563
+ const focal = p.focal && before ? apply(framePointsMatrix({ start, end, width }), apply(before, p.focal)) : null;
564
+ const next = { ...pinned, start, end, width };
565
+ if (focal) next.focal = focal;
566
+ else delete next.focal;
567
+ return next;
568
+ }
569
+ var VECTOR_GRADIENT_MAX_PERIODS = 64;
570
+ function wholePeriod(stops) {
571
+ const out = stops.map((s) => ({ ...s }));
572
+ if (out.length && out[0].offset > 0) out.unshift({ ...out[0], offset: 0 });
573
+ const last = out[out.length - 1];
574
+ if (last && last.offset < 1) out.push({ ...last, offset: 1 });
575
+ return out;
576
+ }
577
+ function periodicVectorGradientStops(stops, lo, hi, mirrored) {
578
+ const period = wholePeriod(stops);
579
+ const back = reverseVectorGradient({ stops: period }).stops;
580
+ const n = Math.max(1, hi - lo);
581
+ const out = [];
582
+ for (let j = 0; j < n; j++) {
583
+ const one = mirrored(lo + j) ? back : period;
584
+ for (const s of one) out.push({ ...s, offset: (j + s.offset) / n });
585
+ }
586
+ return out;
587
+ }
588
+ function expandVectorGradientSpread(g, bounds, area) {
589
+ const m = vectorGradientMatrix(g, bounds);
590
+ const inv = invert(m);
591
+ const spread = g.spread ?? "pad";
592
+ const reflected = g.type === "reflected";
593
+ if (!inv || !reflected && spread === "pad") return null;
594
+ const odd = (k) => (k % 2 + 2) % 2 === 1;
595
+ const stops = expandVectorGradientStops(g);
596
+ const along2 = (a, b, mirrored2, capped2 = false) => ({
597
+ start: apply(m, { x: a, y: 0 }),
598
+ end: apply(m, { x: b, y: 0 }),
599
+ from: a,
600
+ to: b,
601
+ matrix: m,
602
+ stops: periodicVectorGradientStops(stops, a, b, mirrored2),
603
+ capped: capped2
604
+ });
605
+ if (reflected && spread === "pad") return along2(-1, 1, (k) => k < 0);
606
+ const us = boxCorners(area).map((c) => apply(inv, c).x);
607
+ let lo = Math.floor(Math.min(...us));
608
+ let hi = Math.ceil(Math.max(...us));
609
+ if (reflected) {
610
+ lo = Math.min(lo, -1);
611
+ hi = Math.max(hi, 1);
612
+ }
613
+ hi = Math.max(hi, lo + 1);
614
+ if (!reflected && lo >= 0 && hi <= 1) return null;
615
+ const capped = hi - lo > VECTOR_GRADIENT_MAX_PERIODS;
616
+ if (capped) {
617
+ lo = Math.max(lo, -VECTOR_GRADIENT_MAX_PERIODS / 2);
618
+ hi = lo + VECTOR_GRADIENT_MAX_PERIODS;
619
+ }
620
+ const mirrored = reflected ? (k) => {
621
+ const j = k >= 0 ? k : -k - 1;
622
+ const own = spread === "reflect" && odd(j);
623
+ return k < 0 ? !own : own;
624
+ } : (k) => spread === "reflect" && odd(k);
625
+ return along2(lo, hi, mirrored, capped);
626
+ }
627
+ var boxCorners = (b) => [
628
+ { x: b.x, y: b.y },
629
+ { x: b.x + b.width, y: b.y },
630
+ { x: b.x + b.width, y: b.y + b.height },
631
+ { x: b.x, y: b.y + b.height }
632
+ ];
633
+ var toSvgStops = (stops) => stops.map((s) => ({ offset: `${fmt(s.offset * 100, 3)}%`, stopColor: s.color, ...s.opacity !== void 0 && s.opacity !== 1 ? { stopOpacity: Math.round(s.opacity * 1e4) / 1e4 } : {} }));
634
+ var polygonD = (pts, precision) => `M${pts.map((p) => `${fmt(p.x, precision)} ${fmt(p.y, precision)}`).join("L")}Z`;
635
+ var padBounds = (b, pad) => ({ x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad });
636
+ var vectorGradientArea = (bounds) => padBounds(bounds ?? { x: 0, y: 0, width: 1, height: 1 }, 1);
637
+ var transformAttr = (m) => isIdentity(m) ? {} : { gradientTransform: fmtMatrix(m) };
638
+ function minScale(m) {
639
+ const [a, b, c, d] = m;
640
+ const p = a * a + b * b;
641
+ const q = c * c + d * d;
642
+ const r = a * c + b * d;
643
+ return Math.sqrt(Math.max(0, (p + q - Math.sqrt((p - q) ** 2 + 4 * r * r)) / 2));
644
+ }
645
+ function patternAttrs(area) {
646
+ const [x, y, w, h] = [area.x, area.y, Math.max(area.width, 1e-3), Math.max(area.height, 1e-3)].map((n) => fmt(n, 3));
647
+ return { patternUnits: "userSpaceOnUse", x, y, width: w, height: h, viewBox: `${x} ${y} ${w} ${h}` };
648
+ }
649
+ function patternDef(id, area, gradients, tiles) {
650
+ return {
651
+ id,
652
+ type: "pattern",
653
+ attrs: patternAttrs(area),
654
+ gradients,
655
+ tiles
656
+ };
657
+ }
658
+ function vectorGradientSvgDefs(g, id, options = {}) {
659
+ const precision = options.precision ?? 3;
660
+ const bounds = options.bounds ?? null;
661
+ const area = options.area ?? vectorGradientArea(bounds);
662
+ const spread = g.spread && VECTOR_GRADIENT_SPREADS.includes(g.spread) ? g.spread : "pad";
663
+ const stops = expandVectorGradientStops(g);
664
+ const p = vectorGradientPoints(g);
665
+ const unitsM = vectorGradientUnitsMatrix(g, bounds);
666
+ const frameM = mul(framePointsMatrix(p), unitsM);
667
+ const spreadAttr = (s) => s === "pad" ? {} : { spreadMethod: s };
668
+ const base = { gradientUnits: "userSpaceOnUse" };
669
+ switch (g.type) {
670
+ case "linear":
671
+ case "reflected": {
672
+ if (g.type === "reflected" && spread !== "reflect") {
673
+ const ex = expandVectorGradientSpread(g, bounds, area);
674
+ const [a, b] = ex ? [ex.from, ex.to] : [-1, 1];
675
+ const st = ex?.stops ?? periodicVectorGradientStops(stops, -1, 1, (k) => k < 0);
676
+ return [{ id, type: "linear", attrs: { ...base, x1: a, y1: 0, x2: b, y2: 0, gradientTransform: fmtMatrix(frameM) }, stops: toSvgStops(st) }];
677
+ }
678
+ const u = sub(p.end, p.start);
679
+ const w = sub(p.width, p.start);
680
+ const square = Math.abs(dot(u, w)) <= 1e-9 * Math.max(dot(u, u), dot(w, w), 1e-18);
681
+ const coords = square ? { x1: fmt(p.start.x, 6), y1: fmt(p.start.y, 6), x2: fmt(p.end.x, 6), y2: fmt(p.end.y, 6), ...transformAttr(unitsM) } : { x1: 0, y1: 0, x2: 1, y2: 0, gradientTransform: fmtMatrix(frameM) };
682
+ return [{ id, type: "linear", attrs: { ...base, ...coords, ...spreadAttr(spread) }, stops: toSvgStops(stops) }];
683
+ }
684
+ case "radial": {
685
+ const f = unitFocal(g);
686
+ const attrs = { ...base, cx: 0, cy: 0, r: 1, gradientTransform: fmtMatrix(frameM), ...spreadAttr(spread) };
687
+ if (f && (Math.abs(f.f.x) > 1e-9 || Math.abs(f.f.y) > 1e-9)) {
688
+ attrs.fx = fmt(f.f.x, 6);
689
+ attrs.fy = fmt(f.f.y, 6);
690
+ }
691
+ if (f && f.fr > 0) attrs.fr = fmt(f.fr, 6);
692
+ return [{ id, type: "radial", attrs, stops: toSvgStops(stops) }];
693
+ }
694
+ case "diamond": {
695
+ const inv = invert(frameM);
696
+ if (!inv) return [patternDef(id, area, [], [{ d: polygonD(boxCorners(area), precision), fill: stops[stops.length - 1]?.color ?? "none" }])];
697
+ const reach = 2 * Math.max(...boxCorners(area).map((c) => {
698
+ const q = apply(inv, c);
699
+ return Math.abs(q.x) + Math.abs(q.y);
700
+ })) + 1;
701
+ const delta = Math.min(0.05 * reach, 0.75 / Math.max(minScale(frameM), 1e-9));
702
+ const gradients = [];
703
+ const tiles = [];
704
+ [[1, 1], [-1, 1], [-1, -1], [1, -1]].forEach(([sx, sy], k) => {
705
+ const gid = `${id}-q${k}`;
706
+ gradients.push({ id: gid, type: "linear", attrs: { ...base, x1: 0, y1: 0, x2: sx / 2, y2: sy / 2, gradientTransform: fmtMatrix(frameM), ...spreadAttr(spread) }, stops: toSvgStops(stops) });
707
+ const quad = [{ x: -delta * sx, y: -delta * sy }, { x: reach * sx, y: -delta * sy }, { x: -delta * sx, y: reach * sy }].map((q) => apply(frameM, q));
708
+ tiles.push({ d: polygonD(quad, precision), fill: `url(#${gid})` });
709
+ });
710
+ return [patternDef(id, area, gradients, tiles)];
711
+ }
712
+ case "conic":
713
+ return [conicPattern(id, frameM, stops, area, options.wedges ?? 72, precision)];
714
+ }
715
+ }
716
+ function conicPattern(id, frameM, stops, area, wedges, precision) {
717
+ const inv = invert(frameM);
718
+ const n = Math.max(8, Math.min(720, Math.round(wedges)));
719
+ const half = Math.PI / n;
720
+ if (!inv || !stops.length) return patternDef(id, area, [], [{ d: polygonD(boxCorners(area), precision), fill: stops[0]?.color ?? "none" }]);
721
+ const reach = (Math.max(...boxCorners(area).map((c) => Math.hypot(apply(inv, c).x, apply(inv, c).y))) + 1) / Math.cos(half);
722
+ const overlap = Math.min(half, 2e-3);
723
+ const gradients = [];
724
+ const tiles = [];
725
+ for (let k = 0; k < n; k++) {
726
+ const a0 = 2 * Math.PI * k / n;
727
+ const a1 = 2 * Math.PI * (k + 1) / n;
728
+ const mid = (a0 + a1) / 2;
729
+ const tau = { x: -Math.sin(mid), y: Math.cos(mid) };
730
+ const s = Math.sin(half);
731
+ const at2 = (angle) => (Math.sin(angle - mid) / s + 1) / 2;
732
+ const t0 = k / n;
733
+ const t1 = (k + 1) / n;
734
+ const own = [stopOf(0, samplePlainStops(stops, t0))];
735
+ for (const st of stops) if (st.offset > t0 && st.offset < t1) own.push({ ...st, offset: at2(st.offset * 2 * Math.PI) });
736
+ own.push(stopOf(1, samplePlainStops(stops, t1)));
737
+ const gid = `${id}-w${k}`;
738
+ gradients.push({
739
+ id: gid,
740
+ type: "linear",
741
+ attrs: { gradientUnits: "userSpaceOnUse", x1: fmt(-s * tau.x, 6), y1: fmt(-s * tau.y, 6), x2: fmt(s * tau.x, 6), y2: fmt(s * tau.y, 6), gradientTransform: fmtMatrix(frameM) },
742
+ stops: toSvgStops(own)
743
+ });
744
+ const from = a0 - (k === 0 ? 0 : overlap);
745
+ const pts = [{ x: 0, y: 0 }, { x: reach * Math.cos(from), y: reach * Math.sin(from) }, { x: reach * Math.cos(a1), y: reach * Math.sin(a1) }].map((q) => apply(frameM, q));
746
+ tiles.push({ d: polygonD(pts, precision), fill: `url(#${gid})` });
747
+ }
748
+ return patternDef(id, area, gradients, tiles);
749
+ }
750
+ var num = (v2, fallback = 0) => {
751
+ if (v2 === void 0) return fallback;
752
+ const n = typeof v2 === "number" ? v2 : parseFloat(v2);
753
+ return Number.isFinite(n) ? n : fallback;
754
+ };
755
+ var parseMatrixAttr = (v2) => {
756
+ const m = typeof v2 === "string" ? /^matrix\(([^)]*)\)$/.exec(v2.trim()) : null;
757
+ if (!m) return IDENTITY;
758
+ const n = m[1].split(/[\s,]+/).map(Number);
759
+ return n.length === 6 && n.every(Number.isFinite) ? n : IDENTITY;
760
+ };
761
+ var plainOf = (stops) => stops.map((s) => ({ offset: parseFloat(s.offset) / (s.offset.endsWith("%") ? 100 : 1), color: s.stopColor, ...s.stopOpacity !== void 0 ? { opacity: s.stopOpacity } : {} }));
762
+ function inTile(d, p) {
763
+ const n = d.match(/-?[\d.]+(?:e-?\d+)?/gi)?.map(Number) ?? [];
764
+ let inside = false;
765
+ for (let i = 0, j = n.length - 2; i + 1 < n.length; j = i, i += 2) {
766
+ const [xi, yi, xj, yj] = [n[i], n[i + 1], n[j], n[j + 1]];
767
+ if (yi > p.y !== yj > p.y && p.x < (xj - xi) * (p.y - yi) / (yj - yi) + xi) inside = !inside;
768
+ }
769
+ return inside;
770
+ }
771
+ function sampleVectorPaintDefAt(defs, id, p) {
772
+ const all = /* @__PURE__ */ new Map();
773
+ for (const d of defs) {
774
+ all.set(d.id, d);
775
+ if (d.type === "pattern") for (const gd of d.gradients) all.set(gd.id, gd);
776
+ }
777
+ const paint = (ref, q) => {
778
+ const url = /^url\(#(.+)\)$/.exec(ref);
779
+ if (!url) return parseVectorColor(ref);
780
+ const def = all.get(url[1]);
781
+ if (!def) return null;
782
+ if (def.type === "pattern") {
783
+ for (let i = def.tiles.length - 1; i >= 0; i--) if (inTile(def.tiles[i].d, q)) return paint(def.tiles[i].fill, q);
784
+ return null;
785
+ }
786
+ const inv = invert(parseMatrixAttr(def.attrs.gradientTransform)) ?? IDENTITY;
787
+ const local = apply(inv, q);
788
+ const spread = def.attrs.spreadMethod ?? "pad";
789
+ let t;
790
+ if (def.type === "radial") {
791
+ const r = num(def.attrs.r, 0.5);
792
+ const c = { x: num(def.attrs.cx), y: num(def.attrs.cy) };
793
+ const f = { x: num(def.attrs.fx, c.x), y: num(def.attrs.fy, c.y) };
794
+ const u = { x: (local.x - c.x) / r, y: (local.y - c.y) / r };
795
+ t = vectorGradientFocalParam(u, { x: (f.x - c.x) / r, y: (f.y - c.y) / r }, num(def.attrs.fr) / r);
796
+ } else {
797
+ const a = { x: num(def.attrs.x1), y: num(def.attrs.y1) };
798
+ const b = { x: num(def.attrs.x2, 1), y: num(def.attrs.y2) };
799
+ const v2 = sub(b, a);
800
+ const l2 = dot(v2, v2);
801
+ t = l2 > 1e-18 ? dot(sub(local, a), v2) / l2 : 0;
802
+ }
803
+ return samplePlainStops(plainOf(def.stops), applyVectorGradientSpread(t, spread));
804
+ };
805
+ return paint(`url(#${id})`, p);
806
+ }
807
+ var readPoint = (v2) => finitePoint(v2) ? { x: v2.x, y: v2.y } : void 0;
808
+ function sanitizeVectorGradient(input) {
809
+ if (!input || typeof input !== "object") return null;
810
+ const r = input;
811
+ if (!VECTOR_GRADIENT_TYPES.includes(r.type) || !Array.isArray(r.stops)) return null;
812
+ const stops = [];
813
+ for (const s of r.stops) {
814
+ if (!s || typeof s !== "object") continue;
815
+ const x = s;
816
+ if (typeof x.color !== "string" || typeof x.offset !== "number" || !Number.isFinite(x.offset)) continue;
817
+ const stop = { offset: clamp(x.offset, 0, 1), color: x.color.slice(0, 100) };
818
+ if (typeof x.opacity === "number" && Number.isFinite(x.opacity) && x.opacity !== 1) stop.opacity = clamp(x.opacity, 0, 1);
819
+ if (typeof x.midpoint === "number" && Number.isFinite(x.midpoint) && Math.abs(x.midpoint - 0.5) > 1e-6) stop.midpoint = clamp(x.midpoint, 0.05, 0.95);
820
+ stops.push(stop);
821
+ }
822
+ if (!stops.length) return null;
823
+ const g = { type: r.type, stops };
824
+ for (const k of ["angle", "cx", "cy", "r"]) if (typeof r[k] === "number" && Number.isFinite(r[k])) g[k] = r[k];
825
+ const start = readPoint(r.start);
826
+ const end = readPoint(r.end);
827
+ if (start && end) {
828
+ g.start = start;
829
+ g.end = end;
830
+ const width = readPoint(r.width);
831
+ if (width) g.width = width;
832
+ if (r.units === "userSpaceOnUse" || r.units === "objectBoundingBox") g.units = r.units;
833
+ }
834
+ const focal = readPoint(r.focal);
835
+ if (focal && g.type === "radial") g.focal = focal;
836
+ if (typeof r.focalRadius === "number" && r.focalRadius > 0 && g.type === "radial") g.focalRadius = clamp(r.focalRadius, 0, 0.99);
837
+ if (VECTOR_GRADIENT_SPREADS.includes(r.spread) && r.spread !== "pad") g.spread = r.spread;
838
+ if (VECTOR_GRADIENT_INTERPOLATIONS.includes(r.interpolation) && r.interpolation !== "srgb") g.interpolation = r.interpolation;
839
+ if (typeof r.smoothness === "number" && r.smoothness > 0) g.smoothness = clamp(r.smoothness, 0, 1);
840
+ if (VECTOR_STROKE_GRADIENT_MODES.includes(r.strokeMode) && r.strokeMode !== "within") g.strokeMode = r.strokeMode;
841
+ return g;
842
+ }
843
+ function convertVectorGradient(g, type) {
844
+ if (g.type === type) return g;
845
+ const next = { ...g, type };
846
+ if (type !== "radial") {
847
+ delete next.focal;
848
+ delete next.focalRadius;
849
+ }
850
+ if (!hasExplicitGradientGeometry(g)) {
851
+ if (isCentredVectorGradient(type) !== isCentredVectorGradient(g.type)) {
852
+ delete next.angle;
853
+ delete next.cx;
854
+ delete next.cy;
855
+ delete next.r;
856
+ }
857
+ }
858
+ return next;
859
+ }
860
+ function defaultVectorGradient(type = "linear", from) {
861
+ if (isVectorGradient(from)) return convertVectorGradient(from, type);
862
+ const colour = typeof from === "string" && parseVectorColor(from) ? from : "#000000";
863
+ return { type, stops: [{ offset: 0, color: colour }, { offset: 1, color: "#ffffff" }] };
864
+ }
865
+ function applyVectorGradientPatch(g, patch) {
866
+ const { aspect, reverse: reverse2, type, ...fields } = patch;
867
+ let next = type && type !== g.type ? convertVectorGradient(g, type) : { ...g };
868
+ const record = next;
869
+ for (const [k, v2] of Object.entries(fields)) {
870
+ if (v2 === void 0) continue;
871
+ if (v2 === null) delete record[k];
872
+ else record[k] = v2;
873
+ }
874
+ if ((fields.start || fields.end) && !hasExplicitGradientGeometry(next)) {
875
+ const p = vectorGradientPoints({ ...g, start: void 0, end: void 0 });
876
+ next.start = next.start ?? p.start;
877
+ next.end = next.end ?? p.end;
878
+ }
879
+ if (aspect !== void 0 && Number.isFinite(aspect)) {
880
+ const e = explicitVectorGradient(next);
881
+ const u = sub(e.end, e.start);
882
+ const a = Math.abs(aspect) < 0.01 ? aspect < 0 ? -0.01 : 0.01 : aspect;
883
+ next = { ...e, width: add(e.start, scale(perp(u), a)) };
884
+ }
885
+ if (reverse2) next = reverseVectorGradient(next);
886
+ const clean2 = sanitizeVectorGradient(next);
887
+ if (!clean2) return null;
888
+ clean2.stops = sortedVectorGradientStops(clean2);
889
+ return clean2;
890
+ }
891
+
892
+ // src/vector/corners.ts
893
+ var EPS = 1e-9;
894
+ var MIN_SINE = 1e-6;
895
+ var sub2 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
896
+ var add2 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
897
+ var mul2 = (a, k) => ({ x: a.x * k, y: a.y * k });
898
+ var dot2 = (a, b) => a.x * b.x + a.y * b.y;
84
899
  var cross = (a, b) => a.x * b.y - a.y * b.x;
85
900
  var len = (a) => Math.hypot(a.x, a.y);
86
901
  var pt = (p) => [p.x, p.y];
@@ -99,9 +914,9 @@ var Side = class {
99
914
  direction() {
100
915
  const p0 = vec(this.curve[0]);
101
916
  for (const q of this.curve.slice(1)) {
102
- const d = sub(vec(q), p0);
917
+ const d = sub2(vec(q), p0);
103
918
  const l = len(d);
104
- if (l > EPS) return mul(d, 1 / l);
919
+ if (l > EPS) return mul2(d, 1 / l);
105
920
  }
106
921
  return null;
107
922
  }
@@ -109,12 +924,12 @@ var Side = class {
109
924
  at(s, u) {
110
925
  if (this.straight) {
111
926
  const t2 = this.length > EPS ? Math.min(1, Math.max(0, s / this.length)) : 0;
112
- return { t: t2, p: add(vec(this.curve[0]), mul(u, s)), dir: u };
927
+ return { t: t2, p: add2(vec(this.curve[0]), mul2(u, s)), dir: u };
113
928
  }
114
929
  const t = segmentParamAtLength(this.curve, s, this.length);
115
930
  const d = vec(segmentDerivative(this.curve, t));
116
931
  const l = len(d);
117
- return { t, p: vec(segmentPoint(this.curve, t)), dir: l > EPS ? mul(d, 1 / l) : u };
932
+ return { t, p: vec(segmentPoint(this.curve, t)), dir: l > EPS ? mul2(d, 1 / l) : u };
118
933
  }
119
934
  };
120
935
  function cornerOf(sp, s, i) {
@@ -133,7 +948,7 @@ function cornerOf(sp, s, i) {
133
948
  const lu = sideU.length;
134
949
  const lv = sideV.length;
135
950
  if (!(lu > EPS && lv > EPS) || !Number.isFinite(lu) || !Number.isFinite(lv)) return null;
136
- const cos = Math.max(-1, Math.min(1, dot(u, v2)));
951
+ const cos = Math.max(-1, Math.min(1, dot2(u, v2)));
137
952
  const sin = Math.abs(cross(u, v2));
138
953
  if (sin <= MIN_SINE) return null;
139
954
  return { subpath: s, index: i, at: { x: a.x, y: a.y }, u, v: v2, lu, lv, cos, sin, angle: Math.atan2(sin, cos) * 180 / Math.PI, maxRadius: Math.min(lu, lv) / 2 * sin / (1 + cos), sideU, sideV };
@@ -155,8 +970,8 @@ function vectorCorners(subpaths) {
155
970
  }
156
971
  var cornerSetback = (c, r) => r * (1 + c.cos) / c.sin;
157
972
  var fitCornerRadius = (c, radius) => Number.isFinite(radius) ? Math.min(Math.max(0, radius), c.maxRadius) : 0;
158
- var cornerBisectorPoint = (c, k) => add(c.at, mul(add(c.u, c.v), k));
159
- var cornerRadiusChange = (c, d) => dot(d, add(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
973
+ var cornerBisectorPoint = (c, k) => add2(c.at, mul2(add2(c.u, c.v), k));
974
+ var cornerRadiusChange = (c, d) => dot2(d, add2(c.u, c.v)) * c.sin / (2 + 2 * c.cos);
160
975
  function arcHandle(cos) {
161
976
  const c = Math.max(-1, Math.min(1, cos));
162
977
  const sinHalf = Math.sqrt(Math.max(0, (1 - c) / 2));
@@ -164,9 +979,9 @@ function arcHandle(cos) {
164
979
  return 4 / 3 * sinHalf / (1 + cosHalf);
165
980
  }
166
981
  function squareTo(a, b) {
167
- const w = sub(b, mul(a, dot(a, b)));
982
+ const w = sub2(b, mul2(a, dot2(a, b)));
168
983
  const l = len(w);
169
- return l > EPS ? mul(w, 1 / l) : { x: 0, y: 0 };
984
+ return l > EPS ? mul2(w, 1 / l) : { x: 0, y: 0 };
170
985
  }
171
986
  function cutCorner(c, radius, kind) {
172
987
  const s = Math.min(cornerSetback(c, radius), Math.min(c.lu, c.lv) / 2);
@@ -175,16 +990,16 @@ function cutCorner(c, radius, kind) {
175
990
  const b = c.sideV.at(s, c.v);
176
991
  if (kind === "chamfer") return { tu: a.t, tv: b.t, start: a.p, out: null, in: null, end: b.p };
177
992
  if (kind === "inverted") {
178
- const h = s * arcHandle(dot(a.dir, b.dir));
179
- return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(squareTo(a.dir, b.dir), h)), in: add(b.p, mul(squareTo(b.dir, a.dir), h)), end: b.p };
993
+ const h = s * arcHandle(dot2(a.dir, b.dir));
994
+ return { tu: a.t, tv: b.t, start: a.p, out: add2(a.p, mul2(squareTo(a.dir, b.dir), h)), in: add2(b.p, mul2(squareTo(b.dir, a.dir), h)), end: b.p };
180
995
  }
181
- const d1 = mul(a.dir, -1);
182
- const d2 = mul(b.dir, -1);
183
- const turn = Math.acos(Math.max(-1, Math.min(1, -dot(d1, d2))));
184
- const chord = len(sub(b.p, a.p));
996
+ const d1 = mul2(a.dir, -1);
997
+ const d2 = mul2(b.dir, -1);
998
+ const turn = Math.acos(Math.max(-1, Math.min(1, -dot2(d1, d2))));
999
+ const chord = len(sub2(b.p, a.p));
185
1000
  const k = turn < 1e-9 ? chord / 3 : 4 / 3 * Math.tan(turn / 4) * chord / (2 * Math.sin(turn / 2));
186
1001
  if (!Number.isFinite(k)) return null;
187
- return { tu: a.t, tv: b.t, start: a.p, out: add(a.p, mul(d1, k)), in: add(b.p, mul(d2, k)), end: b.p };
1002
+ return { tu: a.t, tv: b.t, start: a.p, out: add2(a.p, mul2(d1, k)), in: add2(b.p, mul2(d2, k)), end: b.p };
188
1003
  }
189
1004
  var radiusOf = (a) => {
190
1005
  const r = a.corner?.radius;
@@ -496,7 +1311,7 @@ function pressureWidthProfile(samples, options = {}) {
496
1311
  // src/vector/varstroke.ts
497
1312
  var SAMPLES_PER_SUBPATH = 128;
498
1313
  var MAX_DASHES = 5e3;
499
- var sub2 = (a, b) => [a[0] - b[0], a[1] - b[1]];
1314
+ var sub3 = (a, b) => [a[0] - b[0], a[1] - b[1]];
500
1315
  var along = (p, v2, k) => [p[0] + v2[0] * k, p[1] + v2[1] * k];
501
1316
  var lerp = (a, b, u) => [a[0] + (b[0] - a[0]) * u, a[1] + (b[1] - a[1]) * u];
502
1317
  var dist = (a, b) => Math.hypot(a[0] - b[0], a[1] - b[1]);
@@ -587,11 +1402,11 @@ function arcPoints(c, a0, sweep, r0, r1, tol) {
587
1402
  return out;
588
1403
  }
589
1404
  function crossing(a0, a1, b0, b1) {
590
- const r = sub2(a1, a0);
591
- const q = sub2(b1, b0);
1405
+ const r = sub3(a1, a0);
1406
+ const q = sub3(b1, b0);
592
1407
  const den = r[0] * q[1] - r[1] * q[0];
593
1408
  if (Math.abs(den) < 1e-12) return null;
594
- const w = sub2(b0, a0);
1409
+ const w = sub3(b0, a0);
595
1410
  const t = (w[0] * q[1] - w[1] * q[0]) / den;
596
1411
  const u = (w[0] * r[1] - w[1] * r[0]) / den;
597
1412
  return t > 1e-9 && t <= 1 && u >= 0 && u < 1 - 1e-9 ? [a0[0] + r[0] * t, a0[1] + r[1] * t] : null;
@@ -635,7 +1450,7 @@ function variableStrokeRings(path, options) {
635
1450
  const closed = whole && poly.closed;
636
1451
  const m = run.length - 1;
637
1452
  const dirs = [];
638
- for (let i = 0; i < m; i++) dirs.push(unitOf(sub2(run[i + 1].p, run[i].p)));
1453
+ for (let i = 0; i < m; i++) dirs.push(unitOf(sub3(run[i + 1].p, run[i].p)));
639
1454
  const normals = dirs.map(leftNormal);
640
1455
  const w = run.map((q) => sides2(q.s));
641
1456
  const side = (sigma) => {
@@ -654,12 +1469,12 @@ function variableStrokeRings(path, options) {
654
1469
  const d0 = dirs[i];
655
1470
  const d1 = dirs[j];
656
1471
  const cross2 = d0[0] * d1[1] - d0[1] * d1[0];
657
- const dot2 = d0[0] * d1[0] + d0[1] * d1[1];
1472
+ const dot3 = d0[0] * d1[0] + d0[1] * d1[1];
658
1473
  const r0 = w[i + 1][k];
659
1474
  const r1 = w[j][k];
660
1475
  const e = E(i);
661
1476
  const st = S(j);
662
- if (Math.abs(cross2) < 1e-12 && dot2 > 0) {
1477
+ if (Math.abs(cross2) < 1e-12 && dot3 > 0) {
663
1478
  push(out, e);
664
1479
  if (!seam) push(out, st);
665
1480
  continue;
@@ -677,7 +1492,7 @@ function variableStrokeRings(path, options) {
677
1492
  continue;
678
1493
  }
679
1494
  push(out, e);
680
- const theta = uturn ? Math.PI : Math.atan2(cross2, dot2);
1495
+ const theta = uturn ? Math.PI : Math.atan2(cross2, dot3);
681
1496
  const r = (r0 + r1) / 2;
682
1497
  const kind = run[i + 1].corner || seam && poly.corner[0] ? join : "round";
683
1498
  if (r > 1e-9) {
@@ -697,10 +1512,10 @@ function variableStrokeRings(path, options) {
697
1512
  if (closed && out.length > 1 && dist(out[0], out[out.length - 1]) <= 1e-12) out.pop();
698
1513
  return out;
699
1514
  };
700
- const left = side(1);
1515
+ const left2 = side(1);
701
1516
  const right = side(-1);
702
1517
  if (closed) {
703
- rings.push(left, right.reverse());
1518
+ rings.push(left2, right.reverse());
704
1519
  continue;
705
1520
  }
706
1521
  const capPoints = (atStart) => {
@@ -722,7 +1537,7 @@ function variableStrokeRings(path, options) {
722
1537
  return atStart ? [along(R, o, ext), along(L, o, ext)] : [along(L, o, ext), along(R, o, ext)];
723
1538
  };
724
1539
  const ring = [];
725
- for (const q of [...left, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
1540
+ for (const q of [...left2, ...capPoints(false), ...right.reverse(), ...capPoints(true)]) push(ring, q);
726
1541
  if (ring.length > 2) rings.push(ring);
727
1542
  }
728
1543
  }
@@ -838,8 +1653,8 @@ function vectorStrokeLineArea(parts, style, tolerance) {
838
1653
  }
839
1654
  var capExtent = (style, width) => style.strokeLinecap === "round" || style.strokeLinecap === "square" ? width / 2 : 0;
840
1655
  function arrowheadWeight(style, end) {
841
- const scale = end === "start" ? style.startArrowScale : style.endArrowScale;
842
- const s = typeof scale === "number" && scale > 0 && Number.isFinite(scale) ? scale : 1;
1656
+ const scale3 = end === "start" ? style.startArrowScale : style.endArrowScale;
1657
+ const s = typeof scale3 === "number" && scale3 > 0 && Number.isFinite(scale3) ? scale3 : 1;
843
1658
  const width = style.strokeWidth ?? 1;
844
1659
  const profile = strokeWidthProfileOf(style);
845
1660
  const at2 = profile ? widthProfileAt(profile, end === "start" ? 0 : 1) : null;
@@ -1042,8 +1857,872 @@ function vectorStrokeReach(style) {
1042
1857
  return reach;
1043
1858
  }
1044
1859
 
1045
- // src/vector/geometry.ts
1860
+ // src/vector/pathEffects.ts
1046
1861
  var EPS2 = 1e-9;
1862
+ var degenerate = (s) => s.every((p) => Math.abs(p[0] - s[0][0]) < EPS2 && Math.abs(p[1] - s[0][1]) < EPS2);
1863
+ function raise(seg) {
1864
+ if (seg.length !== 3) return seg.map((p) => [p[0], p[1]]);
1865
+ const [p0, p1, p2] = seg;
1866
+ return [p0, [p0[0] + 2 / 3 * (p1[0] - p0[0]), p0[1] + 2 / 3 * (p1[1] - p0[1])], [p2[0] + 2 / 3 * (p1[0] - p2[0]), p2[1] + 2 / 3 * (p1[1] - p2[1])], p2];
1867
+ }
1868
+ function subsOf(path) {
1869
+ return pathToContours(path).map((c) => ({ segs: closeContour(c).filter((s) => !degenerate(s)).map(raise), closed: c.closed })).filter((s) => s.segs.length > 0);
1870
+ }
1871
+ var subsToPath = (subs) => contoursToPath(subs.map((s) => ({ segments: s.segs, closed: s.closed })));
1872
+ var unit2 = (x, y) => {
1873
+ const l = Math.hypot(x, y);
1874
+ return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
1875
+ };
1876
+ function startTangent(s) {
1877
+ for (let i = 1; i < s.length; i++) {
1878
+ const u = unit2(s[i][0] - s[0][0], s[i][1] - s[0][1]);
1879
+ if (u) return u;
1880
+ }
1881
+ return [1, 0];
1882
+ }
1883
+ function endTangent(s) {
1884
+ const end = s[s.length - 1];
1885
+ for (let i = s.length - 2; i >= 0; i--) {
1886
+ const u = unit2(end[0] - s[i][0], end[1] - s[i][1]);
1887
+ if (u) return u;
1888
+ }
1889
+ return [1, 0];
1890
+ }
1891
+ function tangentAt(s, t) {
1892
+ if (t <= 1e-9) return startTangent(s);
1893
+ if (t >= 1 - 1e-9) return endTangent(s);
1894
+ const d = segmentDerivative(s, t);
1895
+ return unit2(d[0], d[1]) ?? startTangent(s);
1896
+ }
1897
+ var perp2 = (v2) => [-v2[1], v2[0]];
1898
+ function effectPathLength(path) {
1899
+ let total = 0;
1900
+ for (const s of subsOf(path)) for (const seg of s.segs) total += segmentLength(seg);
1901
+ return total;
1902
+ }
1903
+ function effectPathVertexCount(path) {
1904
+ return subsOf(path).reduce((n, s) => n + s.segs.length + (s.closed ? 0 : 1), 0);
1905
+ }
1906
+ var Measured = class {
1907
+ segs;
1908
+ lens;
1909
+ total;
1910
+ constructor(path) {
1911
+ this.segs = subsOf(path).flatMap((s) => s.segs);
1912
+ this.lens = this.segs.map((s) => segmentLength(s));
1913
+ this.total = this.lens.reduce((a, b) => a + b, 0);
1914
+ }
1915
+ /**
1916
+ * Appends the part between lengths `sa` and `sb`, continuing the last piece when this one
1917
+ * starts where it ends (across the start of a closed path).
1918
+ */
1919
+ append(sa, sb, out) {
1920
+ for (const piece of trimSegments(this.segs, sa, sb, this.lens)) {
1921
+ if (degenerate(piece)) continue;
1922
+ const last = out[out.length - 1]?.segs;
1923
+ const end = last?.[last.length - 1]?.at(-1);
1924
+ if (!end || Math.hypot(end[0] - piece[0][0], end[1] - piece[0][1]) > 1e-6) out.push({ segs: [piece], closed: false });
1925
+ else last.push(piece);
1926
+ }
1927
+ }
1928
+ };
1929
+ function trimIntervals(start, end, offsetDegrees) {
1930
+ let s = start / 100;
1931
+ let e = end / 100;
1932
+ if (!Number.isFinite(s) || !Number.isFinite(e)) return null;
1933
+ if (s > e) [s, e] = [e, s];
1934
+ s = Math.min(1, Math.max(0, s));
1935
+ e = Math.min(1, Math.max(0, e));
1936
+ if (e - s >= 1 - 1e-9) return null;
1937
+ if (e - s <= 1e-9) return [];
1938
+ const off = Number.isFinite(offsetDegrees) ? (offsetDegrees / 360 % 1 + 1) % 1 : 0;
1939
+ s += off;
1940
+ e += off;
1941
+ if (e <= 1) return [[s, e]];
1942
+ if (s >= 1) return [[s - 1, e - 1]];
1943
+ return [
1944
+ [s, 1],
1945
+ [0, e - 1]
1946
+ ];
1947
+ }
1948
+ function trimPaths(paths, start, end, offsetDegrees = 0) {
1949
+ const iv = trimIntervals(start, end, offsetDegrees);
1950
+ if (!iv) return paths.slice();
1951
+ return paths.map((p) => {
1952
+ const m = new Measured(p);
1953
+ const out = [];
1954
+ if (m.total > 0) for (const [a, b] of iv) m.append(a * m.total, b * m.total, out);
1955
+ return subsToPath(out);
1956
+ });
1957
+ }
1958
+ function trimPathsIndividually(paths, start, end, offsetDegrees = 0) {
1959
+ const iv = trimIntervals(start, end, offsetDegrees);
1960
+ if (!iv) return paths.slice();
1961
+ const ms = paths.map((p) => new Measured(p));
1962
+ const total = ms.reduce((n, m) => n + m.total, 0);
1963
+ let base = 0;
1964
+ return ms.map((m) => {
1965
+ const out = [];
1966
+ for (const [a, b] of iv) {
1967
+ const la = Math.max(0, a * total - base);
1968
+ const lb = Math.min(m.total, b * total - base);
1969
+ if (lb > la) m.append(la, lb, out);
1970
+ }
1971
+ base += m.total;
1972
+ return subsToPath(out);
1973
+ });
1974
+ }
1975
+ function puckerBloatPaths(paths, amount) {
1976
+ const a = amount / 100;
1977
+ if (!a || !Number.isFinite(a)) return paths.slice();
1978
+ return paths.map((p) => {
1979
+ const verts = p.filter((c) => c.type !== "Z");
1980
+ if (!verts.length) return p;
1981
+ const cx = verts.reduce((s, c) => s + c.x, 0) / verts.length;
1982
+ const cy = verts.reduce((s, c) => s + c.y, 0) / verts.length;
1983
+ const mv = (x, y) => ({ x: x + (cx - x) * a, y: y + (cy - y) * a });
1984
+ const mvh = (x, y) => ({ x: x + (x - cx) * a, y: y + (y - cy) * a });
1985
+ const out = [];
1986
+ let prev = { x: 0, y: 0 };
1987
+ let start = { x: 0, y: 0 };
1988
+ const curve = (c1, c2, q) => {
1989
+ const h1 = mvh(c1.x, c1.y);
1990
+ const h2 = mvh(c2.x, c2.y);
1991
+ const e = mv(q.x, q.y);
1992
+ out.push({ type: "C", x1: h1.x, y1: h1.y, x2: h2.x, y2: h2.y, x: e.x, y: e.y });
1993
+ };
1994
+ for (const c of p) {
1995
+ switch (c.type) {
1996
+ case "M": {
1997
+ const e = mv(c.x, c.y);
1998
+ out.push({ type: "M", x: e.x, y: e.y });
1999
+ prev = start = { x: c.x, y: c.y };
2000
+ break;
2001
+ }
2002
+ case "L":
2003
+ curve(prev, c, c);
2004
+ prev = { x: c.x, y: c.y };
2005
+ break;
2006
+ case "Q": {
2007
+ const c1 = { x: prev.x + 2 / 3 * (c.x1 - prev.x), y: prev.y + 2 / 3 * (c.y1 - prev.y) };
2008
+ const c2 = { x: c.x + 2 / 3 * (c.x1 - c.x), y: c.y + 2 / 3 * (c.y1 - c.y) };
2009
+ curve(c1, c2, c);
2010
+ prev = { x: c.x, y: c.y };
2011
+ break;
2012
+ }
2013
+ case "C":
2014
+ curve({ x: c.x1, y: c.y1 }, { x: c.x2, y: c.y2 }, c);
2015
+ prev = { x: c.x, y: c.y };
2016
+ break;
2017
+ case "Z":
2018
+ if (Math.hypot(prev.x - start.x, prev.y - start.y) > EPS2) curve(prev, start, start);
2019
+ out.push({ type: "Z" });
2020
+ prev = start;
2021
+ break;
2022
+ }
2023
+ }
2024
+ return out;
2025
+ });
2026
+ }
2027
+ function roundCornerPaths(paths, radius) {
2028
+ if (!(radius > 0) || !Number.isFinite(radius)) return paths.slice();
2029
+ return paths.map((p) => subpathsToBezier(bezierToSubpaths(p).map((sp) => setSubpathCorners(sp, radius))));
2030
+ }
2031
+ function offsetPaths(paths, amount, join = "miter", miterLimit = 4, copies = 1, copyOffset = 0) {
2032
+ const n = Math.min(1e3, Math.max(1, Math.round(Number.isFinite(copies) ? copies : 1)));
2033
+ return paths.map((p) => {
2034
+ const out = [];
2035
+ for (let i = 0; i < n; i++) out.push(...offsetPath(p, amount * (i + 1 + copyOffset), { join, miterLimit }));
2036
+ return out;
2037
+ });
2038
+ }
2039
+ function twistPaths(paths, angle, center) {
2040
+ let rmax = 0;
2041
+ for (const p of paths) for (const c of p) if (c.type !== "Z") rmax = Math.max(rmax, Math.hypot(c.x - center.x, c.y - center.y));
2042
+ const a = angle * Math.PI / 180;
2043
+ if (!a || rmax <= 1e-9 || !Number.isFinite(a)) return paths.slice();
2044
+ const k = a / rmax;
2045
+ const rot = (v2, th) => {
2046
+ const s = Math.sin(th);
2047
+ const co = Math.cos(th);
2048
+ return [v2[0] * co - v2[1] * s, v2[0] * s + v2[1] * co];
2049
+ };
2050
+ const map = (p) => {
2051
+ const v2 = [p[0] - center.x, p[1] - center.y];
2052
+ const r = rot(v2, k * Math.hypot(v2[0], v2[1]));
2053
+ return [center.x + r[0], center.y + r[1]];
2054
+ };
2055
+ const jac = (p, dv) => {
2056
+ const v2 = [p[0] - center.x, p[1] - center.y];
2057
+ const r = Math.hypot(v2[0], v2[1]);
2058
+ let w = dv;
2059
+ if (r > 1e-12) {
2060
+ const f = k * (v2[0] * dv[0] + v2[1] * dv[1]) / r;
2061
+ w = [dv[0] - v2[1] * f, dv[1] + v2[0] * f];
2062
+ }
2063
+ return rot(w, k * r);
2064
+ };
2065
+ return paths.map((p) => {
2066
+ const out = [];
2067
+ for (const sub5 of subsOf(p)) {
2068
+ const segs = [];
2069
+ for (const seg of sub5.segs) {
2070
+ const cubic = seg.length === 2 ? [seg[0], seg[0], seg[1], seg[1]] : seg;
2071
+ const len2 = segmentLength(seg);
2072
+ const m = Math.min(64, Math.max(1, Math.ceil(Math.abs(a) * len2 / rmax / 0.15 + len2 / 50)));
2073
+ for (let i = 0; i < m; i++) {
2074
+ const q = subSegment(cubic, i / m, (i + 1) / m);
2075
+ const p0 = map(q[0]);
2076
+ const p3 = map(q[3]);
2077
+ const h1 = jac(q[0], [q[1][0] - q[0][0], q[1][1] - q[0][1]]);
2078
+ const h2 = jac(q[3], [q[2][0] - q[3][0], q[2][1] - q[3][1]]);
2079
+ segs.push([p0, [p0[0] + h1[0], p0[1] + h1[1]], [p3[0] + h2[0], p3[1] + h2[1]], p3]);
2080
+ }
2081
+ }
2082
+ out.push({ segs, closed: sub5.closed });
2083
+ }
2084
+ return subsToPath(out);
2085
+ });
2086
+ }
2087
+ function resample(sub5, m) {
2088
+ const segs = sub5.segs;
2089
+ const n = segs.length;
2090
+ const lens = segs.map((s) => segmentLength(s));
2091
+ const out = [];
2092
+ for (let j = 0; j < n; j++) {
2093
+ const piece = lens[j] / m;
2094
+ for (let k = 0; k < m; k++) {
2095
+ if (k === 0) {
2096
+ const tout = startTangent(segs[j]);
2097
+ const prev = sub5.closed || j > 0 ? (j + n - 1) % n : null;
2098
+ let tan = tout;
2099
+ if (prev !== null) {
2100
+ const tin = endTangent(segs[prev]);
2101
+ tan = unit2(tin[0] + tout[0], tin[1] + tout[1]) ?? tout;
2102
+ }
2103
+ out.push({ p: segs[j][0], tan, lenPrev: prev === null ? 0 : lens[prev] / m, lenNext: piece });
2104
+ } else {
2105
+ const t = segmentParamAtLength(segs[j], piece * k, lens[j]);
2106
+ out.push({ p: segmentPoint(segs[j], t), tan: tangentAt(segs[j], t), lenPrev: piece, lenNext: piece });
2107
+ }
2108
+ }
2109
+ }
2110
+ if (!sub5.closed) {
2111
+ const last = segs[n - 1];
2112
+ out.push({ p: last[last.length - 1], tan: endTangent(last), lenPrev: lens[n - 1] / m, lenNext: 0 });
2113
+ }
2114
+ return out;
2115
+ }
2116
+ function emit(samples, disp, closed, smooth, handle) {
2117
+ const n = samples.length;
2118
+ if (!n) return null;
2119
+ const pt2 = (i) => [samples[i].p[0] + disp[i][0], samples[i].p[1] + disp[i][1]];
2120
+ const segs = [];
2121
+ const count = closed ? n : n - 1;
2122
+ for (let i = 0; i < count; i++) {
2123
+ const j = (i + 1) % n;
2124
+ const a = samples[i];
2125
+ const b = samples[j];
2126
+ const p = pt2(i);
2127
+ const q = pt2(j);
2128
+ if (smooth) segs.push([p, [p[0] + a.tan[0] * a.lenNext * handle, p[1] + a.tan[1] * a.lenNext * handle], [q[0] - b.tan[0] * b.lenPrev * handle, q[1] - b.tan[1] * b.lenPrev * handle], q]);
2129
+ else segs.push([p, q]);
2130
+ }
2131
+ if (!segs.length) return null;
2132
+ return { segs, closed };
2133
+ }
2134
+ function zigzagPaths(paths, size, ridges, smooth = false) {
2135
+ const m = Math.min(1e3, Math.max(0, Math.round(Number.isFinite(ridges) ? ridges : 0))) + 1;
2136
+ return paths.map((p) => {
2137
+ const out = [];
2138
+ for (const sub5 of subsOf(p)) {
2139
+ const s = resample(sub5, m);
2140
+ const disp = s.map((x, i) => {
2141
+ const n = perp2(x.tan);
2142
+ const d = i % 2 === 0 ? size : -size;
2143
+ return [n[0] * d, n[1] * d];
2144
+ });
2145
+ const e = emit(s, disp, sub5.closed, smooth, 0.36);
2146
+ if (e) out.push(e);
2147
+ }
2148
+ return subsToPath(out);
2149
+ });
2150
+ }
2151
+ var MASK64 = (1n << 64n) - 1n;
2152
+ function splitmix(x) {
2153
+ x = x + 0x9e3779b97f4a7c15n & MASK64;
2154
+ let z = x;
2155
+ z = (z ^ z >> 30n) * 0xbf58476d1ce4e5b9n & MASK64;
2156
+ z = (z ^ z >> 27n) * 0x94d049bb133111ebn & MASK64;
2157
+ return z ^ z >> 31n;
2158
+ }
2159
+ function hash(seed, idx, t) {
2160
+ const h = splitmix(splitmix(splitmix(seed) ^ idx) ^ BigInt.asUintN(64, BigInt(t)));
2161
+ return Number(h >> 11n) / 2 ** 53 * 2 - 1;
2162
+ }
2163
+ function noise(seed, idx, t) {
2164
+ const i = Math.floor(t);
2165
+ const f = t - i;
2166
+ const p0 = hash(seed, idx, i - 1);
2167
+ const p1 = hash(seed, idx, i);
2168
+ const p2 = hash(seed, idx, i + 1);
2169
+ const p3 = hash(seed, idx, i + 2);
2170
+ const v2 = 0.5 * (2 * p1 + (-p0 + p2) * f + (2 * p0 - 5 * p1 + 4 * p2 - p3) * f * f + (-p0 + 3 * p1 - 3 * p2 + p3) * f * f * f);
2171
+ return Math.min(1, Math.max(-1, v2));
2172
+ }
2173
+ function wigglePaths(paths, w) {
2174
+ if (!w.size || !Number.isFinite(w.size)) return paths.slice();
2175
+ const m = Math.min(1e3, Math.max(0, Math.round(Number.isFinite(w.detail) ? w.detail : 0))) + 1;
2176
+ const seed = BigInt.asUintN(64, BigInt(Math.round(Number.isFinite(w.seed) ? w.seed : 0)));
2177
+ const t = (Number.isFinite(w.time) ? w.time : 0) * (Number.isFinite(w.speed) ? w.speed : 0) + (Number.isFinite(w.phase) ? w.phase : 0) / 360;
2178
+ const corr = Math.min(1, Math.max(0, (Number.isFinite(w.correlation) ? w.correlation : 0) / 100));
2179
+ const sx = noise(seed, MASK64, t);
2180
+ const sy = noise(seed, MASK64 - 1n, t);
2181
+ let idx = 0;
2182
+ return paths.map((p) => {
2183
+ const out = [];
2184
+ for (const sub5 of subsOf(p)) {
2185
+ const s = resample(sub5, m);
2186
+ const disp = s.map(() => {
2187
+ const i = BigInt(idx);
2188
+ const ox = noise(seed, i * 2n, t + idx * 0.37);
2189
+ const oy = noise(seed, i * 2n + 1n, t + idx * 0.61);
2190
+ idx++;
2191
+ return [(sx * corr + ox * (1 - corr)) * w.size, (sy * corr + oy * (1 - corr)) * w.size];
2192
+ });
2193
+ const e = emit(s, disp, sub5.closed, w.smooth, 1 / 3);
2194
+ if (e) out.push(e);
2195
+ }
2196
+ return subsToPath(out);
2197
+ });
2198
+ }
2199
+ var MERGE_OPS = { add: "union", subtract: "difference", intersect: "intersection", exclude: "xor" };
2200
+ function mergePaths(paths, mode = "merge") {
2201
+ if (mode === "merge" || !(mode in MERGE_OPS)) return paths.flat();
2202
+ const inputs = paths.filter((p) => p.length > 0).map((path) => ({ path, fillRule: "nonzero" }));
2203
+ if (!inputs.length) return [];
2204
+ return pathBoolean(MERGE_OPS[mode], inputs).path;
2205
+ }
2206
+ function vectorRepeaterCopies(r, pivot) {
2207
+ const copies = Math.max(0, Number.isFinite(r.copies) ? Math.min(r.copies, 1e3) : 0);
2208
+ const n = Math.ceil(copies);
2209
+ const offset = Number.isFinite(r.offset) ? r.offset : 0;
2210
+ const so = (r.startOpacity ?? 100) / 100;
2211
+ const eo = (r.endOpacity ?? 100) / 100;
2212
+ const fract = copies - Math.floor(copies);
2213
+ const out = [];
2214
+ for (let i = 0; i < n; i++) {
2215
+ const k = i + offset;
2216
+ let m = translateMatrix(-pivot.x, -pivot.y);
2217
+ m = multiplyMatrices(m, scaleMatrix(Math.pow((r.scale?.x ?? 100) / 100, k), Math.pow((r.scale?.y ?? 100) / 100, k)));
2218
+ m = multiplyMatrices(m, rotateMatrix((r.rotation ?? 0) * k));
2219
+ m = multiplyMatrices(m, translateMatrix(pivot.x + (r.position?.x ?? 0) * k, pivot.y + (r.position?.y ?? 0) * k));
2220
+ const t = n > 1 ? i / (n - 1) : 0;
2221
+ const partial = i + 1 === n && fract > 0 ? fract : 1;
2222
+ out.push({ index: i, matrix: m, opacity: Math.min(1, Math.max(0, (so + (eo - so) * t) * partial)) });
2223
+ }
2224
+ return out;
2225
+ }
2226
+ function repeatPaths(paths, r, pivot) {
2227
+ return vectorRepeaterCopies(r, pivot).map((copy) => ({ copy, paths: paths.map((p) => transformBezierPath(p, copy.matrix)) }));
2228
+ }
2229
+ function pathsCenter(paths) {
2230
+ const b = bezierPathBounds(paths.flat());
2231
+ return b ? { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 } : { x: 0, y: 0 };
2232
+ }
2233
+
2234
+ // src/vector/appearance.ts
2235
+ var VECTOR_EFFECT_DEFAULTS = {
2236
+ trim: { start: 0, end: 50, offset: 0, mode: "simultaneous" },
2237
+ roundCorners: { radius: 10 },
2238
+ puckerBloat: { amount: 30 },
2239
+ twist: { angle: 45, center: { x: 0, y: 0 } },
2240
+ zigzag: { size: 5, ridges: 4, smooth: false },
2241
+ wiggle: { size: 5, detail: 6, smooth: true, speed: 2, correlation: 50, phase: 0, seed: 1, time: 0 },
2242
+ offset: { amount: 5, join: "miter", miterLimit: 4, copies: 1, copyOffset: 0 },
2243
+ merge: { mode: "add" },
2244
+ repeater: {
2245
+ copies: 3,
2246
+ offset: 0,
2247
+ position: { x: 100, y: 0 },
2248
+ anchor: { x: 0, y: 0 },
2249
+ rotation: 0,
2250
+ scale: { x: 100, y: 100 },
2251
+ startOpacity: 100,
2252
+ endOpacity: 100,
2253
+ composite: "below"
2254
+ }
2255
+ };
2256
+ var VECTOR_EFFECT_LABELS = {
2257
+ trim: "Trim Paths",
2258
+ roundCorners: "Round Corners",
2259
+ puckerBloat: "Pucker & Bloat",
2260
+ twist: "Twist",
2261
+ zigzag: "Zig Zag",
2262
+ wiggle: "Wiggle Paths",
2263
+ offset: "Offset Paths",
2264
+ merge: "Merge Paths",
2265
+ repeater: "Repeater"
2266
+ };
2267
+ var VECTOR_EFFECT_PARAM_SPECS = {
2268
+ trim: [
2269
+ { key: "start", label: "Start %", kind: "number", min: 0, max: 100, step: 1, slider: true },
2270
+ { key: "end", label: "End %", kind: "number", min: 0, max: 100, step: 1, slider: true },
2271
+ { key: "offset", label: "Offset \xB0", kind: "number", min: -360, max: 360, step: 1, slider: true },
2272
+ {
2273
+ key: "mode",
2274
+ label: "Paths",
2275
+ kind: "select",
2276
+ options: [
2277
+ { value: "simultaneous", label: "Simultaneously" },
2278
+ { value: "individual", label: "Individually" }
2279
+ ]
2280
+ }
2281
+ ],
2282
+ roundCorners: [{ key: "radius", label: "Radius", kind: "number", min: 0, max: 200, step: 1, slider: true }],
2283
+ puckerBloat: [{ key: "amount", label: "Amount %", kind: "number", min: -100, max: 100, step: 1, slider: true }],
2284
+ twist: [
2285
+ { key: "angle", label: "Angle \xB0", kind: "number", min: -720, max: 720, step: 1, slider: true },
2286
+ { key: "center", label: "Centre", kind: "vec2", step: 1 }
2287
+ ],
2288
+ zigzag: [
2289
+ { key: "size", label: "Size", kind: "number", min: 0, max: 100, step: 0.5, slider: true },
2290
+ { key: "ridges", label: "Ridges", kind: "number", min: 0, max: 50, step: 1, slider: true },
2291
+ { key: "smooth", label: "Smooth", kind: "boolean" }
2292
+ ],
2293
+ wiggle: [
2294
+ { key: "size", label: "Size", kind: "number", min: 0, max: 100, step: 0.5, slider: true },
2295
+ { key: "detail", label: "Detail", kind: "number", min: 0, max: 50, step: 1, slider: true },
2296
+ { key: "smooth", label: "Smooth", kind: "boolean" },
2297
+ { key: "correlation", label: "Correlation %", kind: "number", min: 0, max: 100, step: 1, slider: true },
2298
+ { key: "speed", label: "Speed", kind: "number", min: 0, step: 0.1 },
2299
+ { key: "phase", label: "Phase \xB0", kind: "number", step: 1 },
2300
+ { key: "time", label: "Time (s)", kind: "number", min: 0, max: 10, step: 0.05, slider: true },
2301
+ { key: "seed", label: "Seed", kind: "number", step: 1 }
2302
+ ],
2303
+ offset: [
2304
+ { key: "amount", label: "Amount", kind: "number", min: -100, max: 100, step: 0.5, slider: true },
2305
+ {
2306
+ key: "join",
2307
+ label: "Join",
2308
+ kind: "select",
2309
+ options: [
2310
+ { value: "miter", label: "Miter" },
2311
+ { value: "round", label: "Round" },
2312
+ { value: "bevel", label: "Bevel" }
2313
+ ]
2314
+ },
2315
+ { key: "miterLimit", label: "Miter limit", kind: "number", min: 1, step: 0.5 },
2316
+ { key: "copies", label: "Copies", kind: "number", min: 1, max: 20, step: 1 },
2317
+ { key: "copyOffset", label: "Copy offset", kind: "number", step: 0.5 }
2318
+ ],
2319
+ merge: [
2320
+ {
2321
+ key: "mode",
2322
+ label: "Mode",
2323
+ kind: "select",
2324
+ options: [
2325
+ { value: "merge", label: "Merge" },
2326
+ { value: "add", label: "Add" },
2327
+ { value: "subtract", label: "Subtract" },
2328
+ { value: "intersect", label: "Intersect" },
2329
+ { value: "exclude", label: "Exclude" }
2330
+ ]
2331
+ }
2332
+ ],
2333
+ repeater: [
2334
+ { key: "copies", label: "Copies", kind: "number", min: 0, max: 50, step: 0.1, slider: true },
2335
+ { key: "offset", label: "Offset", kind: "number", min: -10, max: 10, step: 0.1, slider: true },
2336
+ { key: "position", label: "Position", kind: "vec2", step: 1 },
2337
+ { key: "anchor", label: "Anchor", kind: "vec2", step: 1 },
2338
+ { key: "rotation", label: "Rotation \xB0", kind: "number", min: -360, max: 360, step: 1, slider: true },
2339
+ { key: "scale", label: "Scale %", kind: "vec2", step: 1 },
2340
+ { key: "startOpacity", label: "Start opacity %", kind: "number", min: 0, max: 100, step: 1, slider: true },
2341
+ { key: "endOpacity", label: "End opacity %", kind: "number", min: 0, max: 100, step: 1, slider: true },
2342
+ {
2343
+ key: "composite",
2344
+ label: "Copies",
2345
+ kind: "select",
2346
+ options: [
2347
+ { value: "below", label: "Below" },
2348
+ { value: "above", label: "Above" }
2349
+ ]
2350
+ }
2351
+ ]
2352
+ };
2353
+ var isVec2 = (v2) => !!v2 && typeof v2 === "object" && Number.isFinite(v2.x) && Number.isFinite(v2.y);
2354
+ function normalizeVectorEffectParams(type, params, base) {
2355
+ const defaults = VECTOR_EFFECT_DEFAULTS[type];
2356
+ if (!defaults) return null;
2357
+ const out = { ...defaults, ...base ?? {} };
2358
+ for (const spec of VECTOR_EFFECT_PARAM_SPECS[type]) {
2359
+ const v2 = params?.[spec.key];
2360
+ if (v2 === void 0) continue;
2361
+ if (spec.kind === "number" && typeof v2 === "number" && Number.isFinite(v2)) out[spec.key] = Math.min(spec.max ?? Infinity, Math.max(spec.min ?? -Infinity, v2));
2362
+ else if (spec.kind === "boolean" && typeof v2 === "boolean") out[spec.key] = v2;
2363
+ else if (spec.kind === "vec2" && isVec2(v2)) out[spec.key] = { x: v2.x, y: v2.y };
2364
+ else if (spec.kind === "select" && spec.options.some((o) => o.value === v2)) out[spec.key] = v2;
2365
+ }
2366
+ return out;
2367
+ }
2368
+ function createVectorEffect(type, id, params) {
2369
+ const p = normalizeVectorEffectParams(type, params);
2370
+ return p ? { id, type, params: p } : null;
2371
+ }
2372
+ var isEnabled = (e) => e.enabled !== false && !!VECTOR_EFFECT_DEFAULTS[e.type];
2373
+ function activeVectorEffects(node) {
2374
+ return node.effects?.filter(isEnabled) ?? [];
2375
+ }
2376
+ function hasOwnVectorAppearance(node) {
2377
+ return node.type === "group" && !!node.liveBoolean || !!node.effects?.some(isEnabled);
2378
+ }
2379
+ var liveCache = /* @__PURE__ */ new WeakMap();
2380
+ function hasVectorAppearance(node) {
2381
+ let v2 = liveCache.get(node);
2382
+ if (v2 === void 0) {
2383
+ v2 = hasOwnVectorAppearance(node) || node.type === "group" && node.children.some(hasVectorAppearance);
2384
+ liveCache.set(node, v2);
2385
+ }
2386
+ return v2;
2387
+ }
2388
+ function isVectorLiveBoolean(node) {
2389
+ return node?.type === "group" && !!node.liveBoolean;
2390
+ }
2391
+ function vectorOperandOperation(group, child) {
2392
+ return child.booleanOperation ?? group.liveBoolean ?? "union";
2393
+ }
2394
+ var PATH_OPS = { union: "union", subtract: "difference", intersect: "intersection", exclude: "xor" };
2395
+ var FoldCache = class {
2396
+ constructor(cap) {
2397
+ this.cap = cap;
2398
+ }
2399
+ cap;
2400
+ map = /* @__PURE__ */ new Map();
2401
+ get(key) {
2402
+ const hit = this.map.get(key);
2403
+ if (hit) {
2404
+ this.map.delete(key);
2405
+ this.map.set(key, hit);
2406
+ }
2407
+ return hit;
2408
+ }
2409
+ set(key, path) {
2410
+ if (this.map.size >= this.cap) {
2411
+ let drop = Math.floor(this.map.size / 2);
2412
+ for (const k of this.map.keys()) {
2413
+ if (drop-- <= 0) break;
2414
+ this.map.delete(k);
2415
+ }
2416
+ }
2417
+ this.map.set(key, path);
2418
+ }
2419
+ clear() {
2420
+ this.map.clear();
2421
+ }
2422
+ };
2423
+ var foldCache = new FoldCache(4096);
2424
+ var foldSteps = 0;
2425
+ function vectorLiveBooleanSteps() {
2426
+ return foldSteps;
2427
+ }
2428
+ function clearVectorLiveBooleanCache() {
2429
+ foldCache.clear();
2430
+ }
2431
+ var RollingHash = class _RollingHash {
2432
+ a = 2166136261;
2433
+ b = 16777619 ^ 1540483477;
2434
+ static view = new DataView(new ArrayBuffer(8));
2435
+ byte(v2) {
2436
+ this.a = Math.imul(this.a ^ v2 & 255, 16777619);
2437
+ this.b = Math.imul(this.b ^ v2 & 255, 1540483477) ^ this.b >>> 15;
2438
+ }
2439
+ num(n) {
2440
+ _RollingHash.view.setFloat64(0, n);
2441
+ for (let i = 0; i < 8; i++) this.byte(_RollingHash.view.getUint8(i));
2442
+ }
2443
+ path(p) {
2444
+ for (const c of p) {
2445
+ this.byte(c.type.charCodeAt(0));
2446
+ if (c.type === "Z") continue;
2447
+ if (c.type === "C") {
2448
+ this.num(c.x1);
2449
+ this.num(c.y1);
2450
+ this.num(c.x2);
2451
+ this.num(c.y2);
2452
+ } else if (c.type === "Q") {
2453
+ this.num(c.x1);
2454
+ this.num(c.y1);
2455
+ }
2456
+ this.num(c.x);
2457
+ this.num(c.y);
2458
+ }
2459
+ }
2460
+ key() {
2461
+ return `${(this.a >>> 0).toString(36)}.${(this.b >>> 0).toString(36)}`;
2462
+ }
2463
+ };
2464
+ function foldVectorBooleanOperands(operands) {
2465
+ if (!operands.length) return [];
2466
+ if (operands.length === 1) return operands[0].path;
2467
+ const h = new RollingHash();
2468
+ const keys = operands.map((o, i) => {
2469
+ if (i > 0) h.byte(o.operation.charCodeAt(0) + o.operation.length);
2470
+ h.byte(o.fillRule === "evenodd" ? 1 : 0);
2471
+ h.path(o.path);
2472
+ h.byte(9);
2473
+ return h.key();
2474
+ });
2475
+ let start = 0;
2476
+ let acc = operands[0].path;
2477
+ for (let i = keys.length - 1; i >= 1; i--) {
2478
+ const hit = foldCache.get(keys[i]);
2479
+ if (hit) {
2480
+ if (i === keys.length - 1) return hit;
2481
+ start = i;
2482
+ acc = hit;
2483
+ break;
2484
+ }
2485
+ }
2486
+ let accRule = start === 0 ? operands[0].fillRule : "nonzero";
2487
+ for (let i = start + 1; i < operands.length; i++) {
2488
+ const o = operands[i];
2489
+ foldSteps++;
2490
+ const r = pathBoolean(PATH_OPS[o.operation] ?? "union", [
2491
+ { path: acc, fillRule: accRule },
2492
+ { path: o.path, fillRule: o.fillRule }
2493
+ ]);
2494
+ if (r.error) return [];
2495
+ acc = r.path;
2496
+ accRule = "nonzero";
2497
+ foldCache.set(keys[i], acc);
2498
+ }
2499
+ return acc;
2500
+ }
2501
+ var compareOrder = (a, b) => {
2502
+ const n = Math.min(a.length, b.length);
2503
+ for (let i = 0; i < n; i++) if (a[i] !== b[i]) return a[i] - b[i];
2504
+ return a.length - b.length;
2505
+ };
2506
+ var add3 = (a, b) => ({ x: a.x + (b?.x ?? 0), y: a.y + (b?.y ?? 0) });
2507
+ var mapPaths = (shapes, fn) => {
2508
+ const out = fn(shapes.map((s) => s.path));
2509
+ return shapes.map((s, i) => ({ ...s, path: out[i] ?? [] }));
2510
+ };
2511
+ function applyVectorEffects(shapes, effects) {
2512
+ let list = shapes.slice();
2513
+ for (const e of effects ?? []) {
2514
+ if (!isEnabled(e) || !list.length) continue;
2515
+ switch (e.type) {
2516
+ case "trim": {
2517
+ const { start, end, offset, mode } = e.params;
2518
+ list = mapPaths(list, (p) => mode === "individual" ? trimPathsIndividually(p, start, end, offset) : trimPaths(p, start, end, offset));
2519
+ break;
2520
+ }
2521
+ case "roundCorners":
2522
+ list = mapPaths(list, (p) => roundCornerPaths(p, e.params.radius));
2523
+ break;
2524
+ case "puckerBloat":
2525
+ list = mapPaths(list, (p) => puckerBloatPaths(p, e.params.amount));
2526
+ break;
2527
+ case "twist":
2528
+ list = mapPaths(list, (p) => twistPaths(p, e.params.angle, add3(pathsCenter(p), e.params.center)));
2529
+ break;
2530
+ case "zigzag":
2531
+ list = mapPaths(list, (p) => zigzagPaths(p, e.params.size, e.params.ridges, e.params.smooth));
2532
+ break;
2533
+ case "wiggle":
2534
+ list = mapPaths(list, (p) => wigglePaths(p, e.params));
2535
+ break;
2536
+ case "offset": {
2537
+ const o = e.params;
2538
+ list = mapPaths(list, (p) => offsetPaths(p, o.amount, o.join, o.miterLimit, o.copies, o.copyOffset));
2539
+ break;
2540
+ }
2541
+ case "merge": {
2542
+ const first = list[0];
2543
+ list = [{ ...first, path: mergePaths(list.map((s) => s.path), e.params.mode) }];
2544
+ break;
2545
+ }
2546
+ case "repeater": {
2547
+ const r = e.params;
2548
+ const copies = repeatPaths(
2549
+ list.map((s) => s.path),
2550
+ r,
2551
+ add3(pathsCenter(list.map((s) => s.path)), r.anchor)
2552
+ );
2553
+ const n = copies.length;
2554
+ const next = [];
2555
+ for (const { copy, paths } of copies) {
2556
+ const rank = r.composite === "above" ? copy.index : n - 1 - copy.index;
2557
+ list.forEach((s, i) => next.push({ source: s.source, path: paths[i], opacity: s.opacity * copy.opacity, order: [rank, ...s.order] }));
2558
+ }
2559
+ list = next;
2560
+ break;
2561
+ }
2562
+ }
2563
+ }
2564
+ return list;
2565
+ }
2566
+ function childShapes(child, index) {
2567
+ if (child.visible === false) return [];
2568
+ const m = child.transform;
2569
+ const k = child.opacity ?? 1;
2570
+ return vectorAppearanceShapes(child).map((s) => ({
2571
+ source: s.source,
2572
+ path: m && !isIdentityMatrix(m) ? transformBezierPath(s.path, m) : s.path,
2573
+ opacity: s.opacity * k,
2574
+ order: [index, ...s.order]
2575
+ }));
2576
+ }
2577
+ function liveBooleanShapes(group) {
2578
+ const operands = [];
2579
+ let source = null;
2580
+ group.children.forEach((child, i) => {
2581
+ const shapes = childShapes(child, i);
2582
+ if (!shapes.length) return;
2583
+ source ??= shapes[0].source;
2584
+ const path = shapes.length === 1 ? shapes[0].path : pathBoolean("union", shapes.map((s) => ({ path: s.path, fillRule: s.source.fillRule ?? "nonzero" }))).path;
2585
+ const fillRule = shapes.length === 1 ? shapes[0].source.fillRule ?? "nonzero" : "nonzero";
2586
+ operands.push({ path, fillRule, operation: vectorOperandOperation(group, child) });
2587
+ });
2588
+ if (!source) return [];
2589
+ return [{ source, path: foldVectorBooleanOperands(operands), opacity: 1, order: [0] }];
2590
+ }
2591
+ var shapeCache = /* @__PURE__ */ new WeakMap();
2592
+ function vectorAppearanceShapes(node) {
2593
+ const hit = shapeCache.get(node);
2594
+ if (hit) return hit;
2595
+ let base;
2596
+ if (node.type === "path") base = [{ source: node, path: subpathsToBezier(node.subpaths), opacity: 1, order: [] }];
2597
+ else if (node.liveBoolean) base = liveBooleanShapes(node);
2598
+ else base = node.children.flatMap((c, i) => childShapes(c, i));
2599
+ const out = applyVectorEffects(base, node.effects).sort((a, b) => compareOrder(a.order, b.order));
2600
+ shapeCache.set(node, out);
2601
+ return out;
2602
+ }
2603
+ var outlineCache = /* @__PURE__ */ new WeakMap();
2604
+ function vectorAppearanceOutline(node) {
2605
+ let hit = outlineCache.get(node);
2606
+ if (!hit) {
2607
+ hit = node.type === "path" && !hasOwnVectorAppearance(node) ? subpathsToBezier(node.subpaths) : vectorAppearanceShapes(node).flatMap((s) => s.path);
2608
+ outlineCache.set(node, hit);
2609
+ }
2610
+ return hit;
2611
+ }
2612
+ function shapeNode(shape, id) {
2613
+ const {
2614
+ id: _id,
2615
+ type: _type,
2616
+ subpaths: _s,
2617
+ transform: _t,
2618
+ effects: _e,
2619
+ booleanOperation: _b,
2620
+ blendStep: _bs,
2621
+ locked: _l,
2622
+ visible: _v,
2623
+ opacity: _o,
2624
+ ...style
2625
+ } = shape.source;
2626
+ return { ...style, id, type: "path", subpaths: bezierToSubpaths(shape.path), ...shape.opacity < 1 ? { opacity: shape.opacity } : {} };
2627
+ }
2628
+ function placement(node) {
2629
+ return {
2630
+ ...node.name !== void 0 ? { name: node.name } : {},
2631
+ ...node.transform ? { transform: node.transform } : {},
2632
+ ...node.visible === false ? { visible: false } : {},
2633
+ ...node.locked ? { locked: true } : {},
2634
+ ...node.blendStep ? { blendStep: true } : {},
2635
+ ...node.booleanOperation ? { booleanOperation: node.booleanOperation } : {}
2636
+ };
2637
+ }
2638
+ function vectorAppearanceNodes(node) {
2639
+ return vectorAppearanceShapes(node).map((s, k) => shapeNode(s, `${node.id}~fx${k}`));
2640
+ }
2641
+ var expandCache = /* @__PURE__ */ new WeakMap();
2642
+ function expandVectorAppearance(node) {
2643
+ if (!hasVectorAppearance(node)) return node;
2644
+ const hit = expandCache.get(node);
2645
+ if (hit) return hit;
2646
+ let out;
2647
+ if (!hasOwnVectorAppearance(node)) {
2648
+ const group = node;
2649
+ out = { ...group, children: group.children.map(expandVectorAppearance) };
2650
+ } else {
2651
+ const shapes = vectorAppearanceShapes(node);
2652
+ if (node.type === "path" && shapes.length === 1) {
2653
+ const one = shapes[0];
2654
+ const { effects: _e, opacity: _o, ...rest } = node;
2655
+ const opacity = (node.opacity ?? 1) * one.opacity;
2656
+ out = { ...rest, subpaths: bezierToSubpaths(one.path), ...opacity < 1 ? { opacity } : {} };
2657
+ } else {
2658
+ out = {
2659
+ id: node.id,
2660
+ type: "group",
2661
+ ...placement(node),
2662
+ ...node.opacity !== void 0 ? { opacity: node.opacity } : {},
2663
+ children: vectorAppearanceNodes(node)
2664
+ };
2665
+ }
2666
+ }
2667
+ expandCache.set(node, out);
2668
+ return out;
2669
+ }
2670
+ function expandVectorAppearances(nodes) {
2671
+ let changed = false;
2672
+ const out = nodes.map((n) => {
2673
+ const e = expandVectorAppearance(n);
2674
+ if (e !== n) changed = true;
2675
+ return e;
2676
+ });
2677
+ return changed ? out : nodes;
2678
+ }
2679
+ function sanitizeVectorEffects(value) {
2680
+ if (!Array.isArray(value)) return [];
2681
+ const out = [];
2682
+ const used = /* @__PURE__ */ new Set();
2683
+ for (const raw of value) {
2684
+ if (!raw || typeof raw !== "object") continue;
2685
+ const r = raw;
2686
+ const type = r.type;
2687
+ if (typeof type !== "string" || !VECTOR_EFFECT_DEFAULTS[type]) continue;
2688
+ let id = typeof r.id === "string" && r.id && !used.has(r.id) ? r.id : "";
2689
+ for (let k = out.length + 1; !id; k++) if (!used.has(`fx${k}`)) id = `fx${k}`;
2690
+ used.add(id);
2691
+ const effect = createVectorEffect(type, id, r.params ?? {});
2692
+ out.push(r.enabled === false ? { ...effect, enabled: false } : effect);
2693
+ }
2694
+ return out;
2695
+ }
2696
+ function transformVectorEffects(effects, m) {
2697
+ const lin = (v2) => ({ x: m[0] * v2.x + m[2] * v2.y, y: m[1] * v2.x + m[3] * v2.y });
2698
+ const mirror = m[0] * m[3] - m[1] * m[2] < 0 ? -1 : 1;
2699
+ return effects.map((e) => {
2700
+ if (e.type === "twist") return { ...e, params: { ...e.params, center: lin(e.params.center), angle: e.params.angle * mirror } };
2701
+ if (e.type === "repeater")
2702
+ return { ...e, params: { ...e.params, position: lin(e.params.position), anchor: lin(e.params.anchor), rotation: e.params.rotation * mirror } };
2703
+ return e;
2704
+ });
2705
+ }
2706
+ function vectorEnteredGroups(nodes, ids) {
2707
+ const out = /* @__PURE__ */ new Set();
2708
+ if (!ids.length) return out;
2709
+ const want = new Set(ids);
2710
+ const visit = (list, live) => {
2711
+ for (const n of list) {
2712
+ const own = n.type === "group" && hasOwnVectorAppearance(n);
2713
+ if (want.has(n.id)) {
2714
+ for (const id of live) out.add(id);
2715
+ if (own) out.add(n.id);
2716
+ }
2717
+ if (n.type === "group") visit(n.children, own ? [...live, n.id] : live);
2718
+ }
2719
+ };
2720
+ visit(nodes, []);
2721
+ return out;
2722
+ }
2723
+
2724
+ // src/vector/geometry.ts
2725
+ var EPS3 = 1e-9;
1047
2726
  var cloneAnchor = (a) => ({
1048
2727
  x: a.x,
1049
2728
  y: a.y,
@@ -1068,7 +2747,7 @@ function hasPaint(paint) {
1068
2747
  return true;
1069
2748
  }
1070
2749
  var handleOf = (a, side) => a[side] ?? { x: a.x, y: a.y };
1071
- var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS2 && Math.abs(h.y - a.y) < EPS2;
2750
+ var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
1072
2751
  function subpathSegmentCount(sp) {
1073
2752
  const n = sp.anchors.length;
1074
2753
  if (n < 2) return 0;
@@ -1100,7 +2779,7 @@ function subpathsToBezier(subpaths, options = {}) {
1100
2779
  return out;
1101
2780
  }
1102
2781
  function vectorNodeToBezier(node) {
1103
- return node.type === "path" ? subpathsToBezier(node.subpaths) : [];
2782
+ return node.type === "path" ? vectorAppearanceOutline(node) : [];
1104
2783
  }
1105
2784
  function vectorPathData(input, precision = 3) {
1106
2785
  const subpaths = Array.isArray(input) ? input : input.subpaths;
@@ -1115,8 +2794,8 @@ function inferAnchorType(a) {
1115
2794
  const li = Math.hypot(ix, iy);
1116
2795
  const lo = Math.hypot(ox, oy);
1117
2796
  const cross2 = (ix * oy - iy * ox) / (li * lo);
1118
- const dot2 = (ix * ox + iy * oy) / (li * lo);
1119
- if (Math.abs(cross2) > 1e-3 || dot2 > 0) return "corner";
2797
+ const dot3 = (ix * ox + iy * oy) / (li * lo);
2798
+ if (Math.abs(cross2) > 1e-3 || dot3 > 0) return "corner";
1120
2799
  return Math.abs(li - lo) <= 1e-3 * Math.max(li, lo) ? "symmetric" : "smooth";
1121
2800
  }
1122
2801
  function bezierToSubpaths(input) {
@@ -1214,12 +2893,13 @@ function collectPathNodes(node) {
1214
2893
  function nodeMatrix(node) {
1215
2894
  return node.transform ?? IDENTITY_MATRIX;
1216
2895
  }
1217
- function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
2896
+ function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX, options = {}) {
1218
2897
  const m = multiplyMatrices(nodeMatrix(node), parent);
1219
- const path = subpathsToBezier(node.subpaths);
2898
+ const path = options.effects === false || !hasOwnVectorAppearance(node) ? subpathsToBezier(node.subpaths) : vectorAppearanceOutline(node);
1220
2899
  return isIdentityMatrix(m) ? path : transformBezierPath(path, m);
1221
2900
  }
1222
2901
  function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
2902
+ node = expandVectorAppearance(node);
1223
2903
  if (node.type === "group") {
1224
2904
  const m = multiplyMatrices(nodeMatrix(node), parent);
1225
2905
  return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
@@ -1249,11 +2929,12 @@ function boundsIntersect(a, b) {
1249
2929
  function boundsFromPoints(a, b) {
1250
2930
  return { x: Math.min(a.x, b.x), y: Math.min(a.y, b.y), width: Math.abs(b.x - a.x), height: Math.abs(b.y - a.y) };
1251
2931
  }
1252
- function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
2932
+ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX, options = {}) {
1253
2933
  if (node.visible === false) return false;
2934
+ if (!(node.type === "group" && (!hasOwnVectorAppearance(node) || options.entered?.has(node.id)))) node = expandVectorAppearance(node);
1254
2935
  if (node.type === "group") {
1255
2936
  const m = multiplyMatrices(nodeMatrix(node), parent);
1256
- return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m));
2937
+ return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m, options));
1257
2938
  }
1258
2939
  const path = vectorPathWorldBezier(node, parent);
1259
2940
  if (path.length === 0) return false;
@@ -1266,11 +2947,11 @@ function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
1266
2947
  if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return [];
1267
2948
  return vectorStrokeOutlinePath(vectorPathWorldBezier(node, parent), node);
1268
2949
  }
1269
- function hitTestVectorNodes(nodes, p, tolerance = 0) {
2950
+ function hitTestVectorNodes(nodes, p, tolerance = 0, options = {}) {
1270
2951
  for (let i = nodes.length - 1; i >= 0; i--) {
1271
2952
  const node = nodes[i];
1272
2953
  if (node.visible === false || node.locked) continue;
1273
- if (hitTestVectorNode(node, p, tolerance)) return node;
2954
+ if (hitTestVectorNode(node, p, tolerance, IDENTITY_MATRIX, options)) return node;
1274
2955
  }
1275
2956
  return null;
1276
2957
  }
@@ -1349,6 +3030,7 @@ function mapAnchor(a, m) {
1349
3030
  }
1350
3031
  function bakeIntoGeometry(node, m) {
1351
3032
  if (isIdentityMatrix(m)) return node;
3033
+ if (node.effects?.length) node = { ...node, effects: transformVectorEffects(node.effects, m) };
1352
3034
  if (node.type === "group") {
1353
3035
  const group = node;
1354
3036
  const children = group.children.map((c) => transformVectorNode(c, m));
@@ -1362,6 +3044,9 @@ function bakeIntoGeometry(node, m) {
1362
3044
  return {
1363
3045
  ...path,
1364
3046
  ...mirrored ? { strokeWidthProfile: mirrorWidthProfile(path.strokeWidthProfile) } : {},
3047
+ // Gradients in the node's own space move with its geometry.
3048
+ ...isVectorGradient(path.fill) ? { fill: transformVectorPaint(path.fill, m) } : {},
3049
+ ...isVectorGradient(path.stroke) ? { stroke: transformVectorPaint(path.stroke, m) } : {},
1365
3050
  subpaths: path.subpaths.map((sp) => ({
1366
3051
  closed: sp.closed,
1367
3052
  anchors: sp.anchors.map((a) => mapAnchor(a, m))
@@ -1416,7 +3101,7 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
1416
3101
  const dy = pos.y - a.y;
1417
3102
  const len2 = Math.hypot(dx, dy);
1418
3103
  if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
1419
- else if (type === "smooth" && len2 > EPS2) {
3104
+ else if (type === "smooth" && len2 > EPS3) {
1420
3105
  const o = a[other];
1421
3106
  const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len2;
1422
3107
  next[other] = { x: a.x - dx / len2 * olen, y: a.y - dy / len2 * olen };
@@ -1446,15 +3131,15 @@ function convertAnchor(sp, index, type) {
1446
3131
  else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
1447
3132
  else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
1448
3133
  else if (prev) dir = { x: a.x - prev.x, y: a.y - prev.y };
1449
- if (!dir || Math.hypot(dir.x, dir.y) < EPS2) return { anchors, closed: sp.closed };
3134
+ if (!dir || Math.hypot(dir.x, dir.y) < EPS3) return { anchors, closed: sp.closed };
1450
3135
  const dl = Math.hypot(dir.x, dir.y);
1451
3136
  const ux = dir.x / dl;
1452
3137
  const uy = dir.y / dl;
1453
3138
  lenIn = a.in && !isOnAnchor(a, a.in) ? Math.hypot(a.in.x - a.x, a.in.y - a.y) : prev ? Math.hypot(prev.x - a.x, prev.y - a.y) / 3 : 0;
1454
3139
  lenOut = a.out && !isOnAnchor(a, a.out) ? Math.hypot(a.out.x - a.x, a.out.y - a.y) : next ? Math.hypot(next.x - a.x, next.y - a.y) / 3 : 0;
1455
3140
  if (type === "symmetric") lenIn = lenOut = (lenIn + lenOut) / 2 || Math.max(lenIn, lenOut);
1456
- if (lenIn > EPS2) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
1457
- if (lenOut > EPS2) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
3141
+ if (lenIn > EPS3) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
3142
+ if (lenOut > EPS3) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
1458
3143
  a.type = type;
1459
3144
  return { anchors, closed: sp.closed };
1460
3145
  }
@@ -1464,12 +3149,12 @@ function insertAnchor(sp, segment, t) {
1464
3149
  const ia = segment;
1465
3150
  const ib = (segment + 1) % n;
1466
3151
  const seg = subpathSegment(sp, segment);
1467
- const [left, right] = splitSegment(seg, t);
1468
- const mid = left[left.length - 1];
3152
+ const [left2, right] = splitSegment(seg, t);
3153
+ const mid = left2[left2.length - 1];
1469
3154
  const anchor = { x: mid[0], y: mid[1] };
1470
3155
  if (seg.length === 4) {
1471
- anchors[ia].out = { x: left[1][0], y: left[1][1] };
1472
- anchor.in = { x: left[2][0], y: left[2][1] };
3156
+ anchors[ia].out = { x: left2[1][0], y: left2[1][1] };
3157
+ anchor.in = { x: left2[2][0], y: left2[2][1] };
1473
3158
  anchor.out = { x: right[1][0], y: right[1][1] };
1474
3159
  anchor.type = "smooth";
1475
3160
  anchors[ib].in = { x: right[2][0], y: right[2][1] };
@@ -1554,6 +3239,166 @@ function roundSubpaths(subpaths, precision = 3) {
1554
3239
  }));
1555
3240
  }
1556
3241
 
3242
+ // src/vector/gradientStroke.ts
3243
+ var SHARP = 1.25;
3244
+ var TOLERANCE = 0.2;
3245
+ var OVERLAP = 0.5;
3246
+ var fmt2 = (n, digits = 3) => {
3247
+ const k = 10 ** digits;
3248
+ const v2 = Math.round(n * k) / k;
3249
+ return Object.is(v2, -0) ? "0" : String(v2);
3250
+ };
3251
+ var toSvgStops2 = (stops) => stops.map((s) => ({ offset: `${fmt2(s.offset * 100)}%`, stopColor: s.color, ...s.opacity !== void 0 && s.opacity !== 1 ? { stopOpacity: Math.round(s.opacity * 1e4) / 1e4 } : {} }));
3252
+ var polygonD2 = (pts, precision) => `M${pts.map((p) => `${fmt2(p.x, precision)} ${fmt2(p.y, precision)}`).join("L")}Z`;
3253
+ var add4 = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
3254
+ var sub4 = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
3255
+ var scale2 = (a, k) => ({ x: a.x * k, y: a.y * k });
3256
+ var left = (d) => ({ x: d.y, y: -d.x });
3257
+ var unit3 = (v2) => {
3258
+ const l = Math.hypot(v2.x, v2.y);
3259
+ return l > 1e-12 ? { x: v2.x / l, y: v2.y / l } : { x: 1, y: 0 };
3260
+ };
3261
+ function flattenedSubpaths(node) {
3262
+ const out = [];
3263
+ for (const contour of pathToContours(subpathsToBezier(node.subpaths))) {
3264
+ const segs = closeContour(contour);
3265
+ if (!segs.length) continue;
3266
+ const ring = flattenChain(segs, TOLERANCE);
3267
+ const pts = ring.map(([x, y]) => ({ x, y })).filter((p, i, a) => i === 0 || Math.hypot(p.x - a[i - 1].x, p.y - a[i - 1].y) > 1e-9);
3268
+ if (contour.closed && pts.length > 2 && Math.hypot(pts[0].x - pts[pts.length - 1].x, pts[0].y - pts[pts.length - 1].y) < 1e-9) pts.pop();
3269
+ if (pts.length >= 2) out.push({ pts, closed: contour.closed && pts.length > 2 });
3270
+ }
3271
+ return out;
3272
+ }
3273
+ var signedArea = (pts) => {
3274
+ let a = 0;
3275
+ for (let i = 0; i < pts.length; i++) {
3276
+ const p = pts[i];
3277
+ const q = pts[(i + 1) % pts.length];
3278
+ a += p.x * q.y - q.x * p.y;
3279
+ }
3280
+ return a / 2;
3281
+ };
3282
+ function vectorStrokeGradientDefs(g, id, node, options = {}) {
3283
+ const precision = options.precision ?? 3;
3284
+ const mode = g.strokeMode === "across" ? "across" : "along";
3285
+ const expanded = expandVectorGradientStops(g);
3286
+ const reach = vectorStrokeReach(node) + 1;
3287
+ const area = options.area ?? vectorGradientArea(options.bounds && { x: options.bounds.x - reach, y: options.bounds.y - reach, width: options.bounds.width + 2 * reach, height: options.bounds.height + 2 * reach });
3288
+ const width = node.strokeWidth ?? 1;
3289
+ const align = node.strokeAlign ?? "center";
3290
+ const profile = strokeWidthProfileOf(node);
3291
+ const gradients = [];
3292
+ const patches = [];
3293
+ const slices = [];
3294
+ const colourAt = (t) => {
3295
+ const s = spanVectorGradientStops(g, t, t, expanded)[0];
3296
+ return s;
3297
+ };
3298
+ let counter = 0;
3299
+ const solid = (pts, t) => {
3300
+ const s = colourAt(t);
3301
+ const gid = `${id}-p${counter++}`;
3302
+ gradients.push({ id: gid, type: "linear", attrs: { gradientUnits: "userSpaceOnUse", x1: 0, y1: 0, x2: 1, y2: 0 }, stops: toSvgStops2([{ ...s, offset: 0 }]) });
3303
+ patches.push({ d: polygonD2(pts, precision), fill: `url(#${gid})` });
3304
+ };
3305
+ const disc = (c, r) => Array.from({ length: 16 }, (_, k) => add4(c, scale2({ x: Math.cos(k * Math.PI / 8), y: Math.sin(k * Math.PI / 8) }, r)));
3306
+ for (const { pts, closed } of flattenedSubpaths(node)) {
3307
+ const n = pts.length;
3308
+ const segCount = closed ? n : n - 1;
3309
+ const dirs = [];
3310
+ const lengths = [0];
3311
+ for (let i = 0; i < segCount; i++) {
3312
+ const a = pts[i];
3313
+ const b = pts[(i + 1) % n];
3314
+ dirs.push(unit3(sub4(b, a)));
3315
+ lengths.push(lengths[i] + Math.hypot(b.x - a.x, b.y - a.y));
3316
+ }
3317
+ const total = lengths[segCount];
3318
+ if (!(total > 1e-9)) continue;
3319
+ const tAt = (i) => lengths[i] / total;
3320
+ const clockwise = closed && signedArea(pts) > 0;
3321
+ const visibleLeft = align === "center" || !closed ? null : align === "inside" ? !clockwise : clockwise;
3322
+ const ribs = [];
3323
+ for (let i = 0; i < n; i++) {
3324
+ const p = pts[i];
3325
+ const din = i > 0 || closed ? dirs[(i - 1 + segCount) % segCount] : null;
3326
+ const dout = i < segCount ? dirs[i] : null;
3327
+ const square = (d) => ({ l: add4(p, scale2(left(d), reach)), r: sub4(p, scale2(left(d), reach)) });
3328
+ if (!din || !dout) {
3329
+ const rib = square(din ?? dout);
3330
+ ribs.push({ before: rib, after: rib });
3331
+ continue;
3332
+ }
3333
+ const nin = left(din);
3334
+ const nout = left(dout);
3335
+ const den = 1 + nin.x * nout.x + nin.y * nout.y;
3336
+ const stretch = den > 1e-12 ? Math.sqrt(2 / den) : Infinity;
3337
+ if (stretch <= SHARP) {
3338
+ const m = scale2(add4(nin, nout), reach / den);
3339
+ const rib = { l: add4(p, m), r: sub4(p, m) };
3340
+ ribs.push({ before: rib, after: rib });
3341
+ } else {
3342
+ ribs.push({ before: square(din), after: square(dout) });
3343
+ solid(disc(p, reach * 1.5), tAt(i));
3344
+ }
3345
+ }
3346
+ if (!closed) {
3347
+ const d0 = dirs[0];
3348
+ const d1 = dirs[segCount - 1];
3349
+ const a = pts[0];
3350
+ const b = pts[n - 1];
3351
+ const cap = (p, d) => [add4(p, scale2(left(d), reach)), add4(add4(p, scale2(d, reach)), scale2(left(d), reach)), sub4(add4(p, scale2(d, reach)), scale2(left(d), reach)), sub4(p, scale2(left(d), reach))];
3352
+ solid(cap(a, scale2(d0, -1)), 0);
3353
+ solid(cap(b, d1), 1);
3354
+ }
3355
+ for (let i = 0; i < segCount; i++) {
3356
+ const j = (i + 1) % n;
3357
+ const a = pts[i];
3358
+ const b = pts[j];
3359
+ const d = dirs[i];
3360
+ const from = ribs[i].after;
3361
+ const to = ribs[j].before;
3362
+ const ext = scale2(d, OVERLAP);
3363
+ const quad = [from.l, add4(to.l, ext), add4(to.r, ext), from.r];
3364
+ const gid = `${id}-s${counter++}`;
3365
+ const t0 = tAt(i);
3366
+ const t1 = closed && i === segCount - 1 ? 1 : tAt(i + 1);
3367
+ if (mode === "along") {
3368
+ gradients.push({
3369
+ id: gid,
3370
+ type: "linear",
3371
+ attrs: { gradientUnits: "userSpaceOnUse", x1: fmt2(a.x, 4), y1: fmt2(a.y, 4), x2: fmt2(b.x, 4), y2: fmt2(b.y, 4) },
3372
+ stops: toSvgStops2(spanVectorGradientStops(g, t0, t1, expanded))
3373
+ });
3374
+ } else {
3375
+ const mid = scale2(add4(a, b), 0.5);
3376
+ const side = widthProfileAt(profile, (t0 + t1) / 2);
3377
+ const nrm = left(d);
3378
+ const reachL = visibleLeft === null ? width / 2 * side.left : visibleLeft ? width * side.left : 0;
3379
+ const reachR = visibleLeft === null ? width / 2 * side.right : visibleLeft ? 0 : width * side.right;
3380
+ const p0 = add4(mid, scale2(nrm, reachL));
3381
+ const p1 = sub4(mid, scale2(nrm, reachR));
3382
+ gradients.push({
3383
+ id: gid,
3384
+ type: "linear",
3385
+ attrs: { gradientUnits: "userSpaceOnUse", x1: fmt2(p0.x, 4), y1: fmt2(p0.y, 4), x2: fmt2(p1.x, 4), y2: fmt2(p1.y, 4) },
3386
+ stops: toSvgStops2(expanded)
3387
+ });
3388
+ }
3389
+ slices.push({ d: polygonD2(quad, precision), fill: `url(#${gid})` });
3390
+ }
3391
+ }
3392
+ const def = {
3393
+ id,
3394
+ type: "pattern",
3395
+ attrs: patternAttrs(area),
3396
+ gradients,
3397
+ tiles: [...patches, ...slices]
3398
+ };
3399
+ return [def];
3400
+ }
3401
+
1557
3402
  // src/vector/svg.ts
1558
3403
  function vectorCornersAttribute(subpaths, precision = 3) {
1559
3404
  const out = [];
@@ -1578,10 +3423,30 @@ function applyVectorCornersAttribute(subpaths, value) {
1578
3423
  return subpaths;
1579
3424
  }
1580
3425
  var escapeAttr = (value) => value.replace(/&/g, "&amp;").replace(/"/g, "&quot;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
1581
- function vectorPaintToSvg(paint, gradientId) {
1582
- if (!hasPaint(paint)) return { value: "none", gradient: null };
1583
- if (typeof paint === "string") return { value: paint, gradient: null };
1584
- return { value: `url(#${gradientId})`, gradient: gradientToSvgDef(paint, gradientId) };
3426
+ function vectorPathLocalBounds(node) {
3427
+ const b = bezierPathBounds(subpathsToBezier(node.subpaths));
3428
+ return b ? { x: b.minX, y: b.minY, width: b.maxX - b.minX, height: b.maxY - b.minY } : null;
3429
+ }
3430
+ var paintCache = /* @__PURE__ */ new WeakMap();
3431
+ function vectorPaintToSvg(paint, gradientId, options = {}) {
3432
+ if (!hasPaint(paint)) return { value: "none", defs: [] };
3433
+ if (typeof paint === "string") return { value: paint, defs: [] };
3434
+ if (!isVectorGradient(paint)) return { value: "none", defs: [] };
3435
+ const role = options.role ?? "fill";
3436
+ const precision = options.precision ?? 3;
3437
+ const key = `${gradientId}|${role}|${precision}`;
3438
+ const owner = options.node ?? paint;
3439
+ const cached = paintCache.get(owner)?.get(key);
3440
+ if (cached && (!options.node || (role === "fill" ? options.node.fill : options.node.stroke) === paint)) return { value: `url(#${gradientId})`, defs: cached };
3441
+ const node = options.node;
3442
+ const bounds = node ? vectorPathLocalBounds(node) : null;
3443
+ const reach = node && role === "stroke" ? vectorStrokeReach(node) + 1 : 1;
3444
+ const area = bounds ? { x: bounds.x - reach, y: bounds.y - reach, width: bounds.width + 2 * reach, height: bounds.height + 2 * reach } : null;
3445
+ const defs = node && role === "stroke" && (paint.strokeMode === "along" || paint.strokeMode === "across") ? vectorStrokeGradientDefs(paint, gradientId, node, { bounds, area, precision }) : vectorGradientSvgDefs(paint, gradientId, { bounds, area, precision });
3446
+ let byKey = paintCache.get(owner);
3447
+ if (!byKey) paintCache.set(owner, byKey = /* @__PURE__ */ new Map());
3448
+ byKey.set(key, defs);
3449
+ return { value: `url(#${gradientId})`, defs };
1585
3450
  }
1586
3451
  var partsCache = /* @__PURE__ */ new WeakMap();
1587
3452
  function vectorStrokeSvgParts(node, clipId, precision = 3) {
@@ -1620,18 +3485,26 @@ function widthProfileFromAttribute(value) {
1620
3485
  if (!value) return [];
1621
3486
  return sanitizeWidthProfile(
1622
3487
  value.split(";").map((part) => {
1623
- const [t, left, right] = part.split(",").map((n) => parseFloat(n));
1624
- return { t, left, right: Number.isFinite(right) ? right : left };
3488
+ const [t, left2, right] = part.split(",").map((n) => parseFloat(n));
3489
+ return { t, left: left2, right: Number.isFinite(right) ? right : left2 };
1625
3490
  })
1626
3491
  );
1627
3492
  }
1628
3493
  var round2 = (n) => Math.round(n * 100) / 100;
1629
- function gradientMarkup(def) {
3494
+ function gradientMarkup(def, extra = "") {
1630
3495
  const tag = def.type === "radial" ? "radialGradient" : "linearGradient";
1631
3496
  const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
1632
3497
  const stops = def.stops.map((s) => `<stop offset="${s.offset}" stop-color="${escapeAttr(s.stopColor)}"${s.stopOpacity !== void 0 ? ` stop-opacity="${s.stopOpacity}"` : ""}/>`).join("");
1633
- return `<${tag} id="${escapeAttr(def.id)}"${attrs}>${stops}</${tag}>`;
3498
+ return `<${tag} id="${escapeAttr(def.id)}"${attrs}${extra}>${stops}</${tag}>`;
3499
+ }
3500
+ function paintDefMarkup(def, gradient) {
3501
+ const extra = gradient ? ` data-vector-gradient="${escapeAttr(JSON.stringify(gradient))}"` : "";
3502
+ if (def.type !== "pattern") return gradientMarkup(def, extra);
3503
+ const attrs = Object.entries(def.attrs).map(([k, v2]) => ` ${k}="${escapeAttr(String(v2))}"`).join("");
3504
+ const inner = def.gradients.map((g) => gradientMarkup(g)).join("") + def.tiles.map((t) => `<path d="${t.d}" fill="${escapeAttr(t.fill)}"/>`).join("");
3505
+ return `<pattern id="${escapeAttr(def.id)}"${attrs}${extra}>${inner}</pattern>`;
1634
3506
  }
3507
+ var paintMarkup = (paint, defs) => defs.map((d, i) => paintDefMarkup(d, i === 0 && isVectorGradient(paint) ? paint : void 0));
1635
3508
  function blendFromAttribute(value, m) {
1636
3509
  if (!value) return null;
1637
3510
  let raw;
@@ -1665,38 +3538,57 @@ function vectorDocumentToSvg(doc, options = {}) {
1665
3538
  const pretty = options.pretty !== false;
1666
3539
  const defs = [];
1667
3540
  const lines = [];
1668
- const num = (n) => formatNumber(n, precision);
3541
+ const num2 = (n) => formatNumber(n, precision);
1669
3542
  const indent = (depth) => pretty ? " ".repeat(depth) : "";
1670
3543
  const common = (node) => {
1671
3544
  let out2 = ` id="${escapeAttr(node.id)}"`;
1672
3545
  if (node.transform) out2 += ` transform="matrix(${node.transform.map((n) => formatNumber(n, 6)).join(" ")})"`;
1673
- if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num(node.opacity)}"`;
3546
+ if (node.opacity !== void 0 && node.opacity < 1) out2 += ` opacity="${num2(node.opacity)}"`;
1674
3547
  if (node.visible === false) out2 += ` display="none"`;
1675
3548
  if (node.blendStep) out2 += ` data-vector-blend-step=""`;
1676
3549
  if (node.type === "group" && node.blend) out2 += ` data-vector-blend="${escapeAttr(JSON.stringify(node.blend))}"`;
3550
+ if (node.booleanOperation) out2 += ` data-vector-boolean-operation="${node.booleanOperation}"`;
1677
3551
  return out2;
1678
3552
  };
1679
- const emit = (node, depth) => {
3553
+ const emit2 = (node, depth) => {
1680
3554
  if (node.visible === false && !options.includeHidden) return;
3555
+ if (hasOwnVectorAppearance(node)) {
3556
+ const live2 = {
3557
+ kind: node.type,
3558
+ ...node.effects?.length ? { effects: node.effects } : {},
3559
+ ...node.type === "group" && node.liveBoolean ? { liveBoolean: node.liveBoolean } : {}
3560
+ };
3561
+ lines.push(`${indent(depth)}<g${common(node)} data-vector-live="${escapeAttr(JSON.stringify(live2))}">`);
3562
+ lines.push(`${indent(depth + 1)}<g data-vector-part="appearance">`);
3563
+ for (const shape of vectorAppearanceNodes(node)) emit2(shape, depth + 2);
3564
+ lines.push(`${indent(depth + 1)}</g>`);
3565
+ lines.push(`${indent(depth + 1)}<g data-vector-part="source" display="none">`);
3566
+ if (node.type === "path") {
3567
+ const { effects: _e, transform: _t, opacity: _o, visible: _v, booleanOperation: _b, blendStep: _s, ...source } = node;
3568
+ emit2({ ...source, id: `${node.id}~src` }, depth + 2);
3569
+ } else for (const child of node.children) emit2(child, depth + 2);
3570
+ lines.push(`${indent(depth + 1)}</g>`);
3571
+ lines.push(`${indent(depth)}</g>`);
3572
+ return;
3573
+ }
1681
3574
  if (node.type === "group") {
1682
3575
  lines.push(`${indent(depth)}<g${common(node)}>`);
1683
- for (const child of node.children) emit(child, depth + 1);
3576
+ for (const child of node.children) emit2(child, depth + 1);
1684
3577
  lines.push(`${indent(depth)}</g>`);
1685
3578
  return;
1686
3579
  }
1687
- const fill = vectorPaintToSvg(node.fill, `${prefix}fill-${node.id}`);
1688
- const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`);
1689
- if (fill.gradient) defs.push(gradientMarkup(fill.gradient));
1690
- if (stroke.gradient) defs.push(gradientMarkup(stroke.gradient));
3580
+ const fill = vectorPaintToSvg(node.fill, `${prefix}fill-${node.id}`, { node, precision });
3581
+ const stroke = vectorPaintToSvg(node.stroke, `${prefix}stroke-${node.id}`, { node, role: "stroke", precision });
3582
+ defs.push(...paintMarkup(node.fill, fill.defs), ...paintMarkup(node.stroke, stroke.defs));
1691
3583
  const d = vectorPathData(node, precision);
1692
3584
  const corners2 = vectorCornersAttribute(node.subpaths, precision);
1693
3585
  const live = corners2 ? ` data-vector-d="${bezierPathToSvg(subpathsToBezier(node.subpaths, { corners: false }), { precision })}" data-vector-corners="${corners2}"` : "";
1694
3586
  const fillAttrs = ` fill="${escapeAttr(fill.value)}"${node.fillRule === "evenodd" ? ` fill-rule="evenodd"` : ""}`;
1695
3587
  const lineAttrs = (width) => {
1696
- let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num(width)}"`;
3588
+ let out2 = ` stroke="${escapeAttr(stroke.value)}" stroke-width="${num2(width)}"`;
1697
3589
  if (node.strokeLinecap && node.strokeLinecap !== "butt") out2 += ` stroke-linecap="${node.strokeLinecap}"`;
1698
3590
  if (node.strokeLinejoin && node.strokeLinejoin !== "miter") out2 += ` stroke-linejoin="${node.strokeLinejoin}"`;
1699
- if (node.strokeDasharray?.length) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
3591
+ if (node.strokeDasharray?.length) out2 += ` stroke-dasharray="${node.strokeDasharray.map(num2).join(" ")}"`;
1700
3592
  return out2;
1701
3593
  };
1702
3594
  const parts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${prefix}clip-${node.id}`, precision) : void 0;
@@ -1704,19 +3596,19 @@ function vectorDocumentToSvg(doc, options = {}) {
1704
3596
  lines.push(`${indent(depth)}<path${common(node)} d="${d}"${fillAttrs}${stroke.value !== "none" ? lineAttrs(node.strokeWidth ?? 1) : ""}${live}/>`);
1705
3597
  return;
1706
3598
  }
1707
- let data = ` data-vector-path="" data-vector-stroke-width="${num(node.strokeWidth ?? 1)}"`;
3599
+ let data = ` data-vector-path="" data-vector-stroke-width="${num2(node.strokeWidth ?? 1)}"`;
1708
3600
  if (parts.align !== "center") data += ` data-vector-stroke-align="${parts.align}"`;
1709
3601
  if (node.startArrow) data += ` data-vector-start-arrow="${node.startArrow}"`;
1710
3602
  if (node.endArrow) data += ` data-vector-end-arrow="${node.endArrow}"`;
1711
3603
  if (node.arrowAlign && node.arrowAlign !== "extend") data += ` data-vector-arrow-align="${node.arrowAlign}"`;
1712
- if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num(node.startArrowScale)}"`;
1713
- if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num(node.endArrowScale)}"`;
3604
+ if (node.startArrowScale !== void 0 && node.startArrowScale !== 1) data += ` data-vector-start-arrow-scale="${num2(node.startArrowScale)}"`;
3605
+ if (node.endArrowScale !== void 0 && node.endArrowScale !== 1) data += ` data-vector-end-arrow-scale="${num2(node.endArrowScale)}"`;
1714
3606
  const profile = strokeWidthProfileOf(node);
1715
3607
  if (profile) {
1716
3608
  data += ` data-vector-width-profile="${widthProfileToAttribute(profile)}"`;
1717
3609
  if (node.strokeLinecap && node.strokeLinecap !== "butt") data += ` data-vector-stroke-linecap="${node.strokeLinecap}"`;
1718
3610
  if (node.strokeLinejoin && node.strokeLinejoin !== "miter") data += ` data-vector-stroke-linejoin="${node.strokeLinejoin}"`;
1719
- if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num).join(" ")}"`;
3611
+ if (node.strokeDasharray?.length) data += ` data-vector-stroke-dasharray="${node.strokeDasharray.map(num2).join(" ")}"`;
1720
3612
  }
1721
3613
  lines.push(`${indent(depth)}<g${common(node)}${data}>`);
1722
3614
  lines.push(`${indent(depth + 1)}<path data-vector-part="fill" d="${d}"${fillAttrs}${live}/>`);
@@ -1733,7 +3625,7 @@ function vectorDocumentToSvg(doc, options = {}) {
1733
3625
  clip = ` clip-path="url(#${id})"`;
1734
3626
  } else {
1735
3627
  const b = parts.clip.bounds;
1736
- const box = `x="${num(b.x)}" y="${num(b.y)}" width="${num(b.width)}" height="${num(b.height)}"`;
3628
+ const box = `x="${num2(b.x)}" y="${num2(b.y)}" width="${num2(b.width)}" height="${num2(b.height)}"`;
1737
3629
  defs.push(`<mask id="${id}" maskUnits="userSpaceOnUse" ${box}><rect ${box} fill="#fff"/><path d="${d}" fill="#000" fill-rule="${rule}"/></mask>`);
1738
3630
  clip = ` mask="url(#${id})"`;
1739
3631
  }
@@ -1744,9 +3636,9 @@ function vectorDocumentToSvg(doc, options = {}) {
1744
3636
  }
1745
3637
  lines.push(`${indent(depth)}</g>`);
1746
3638
  };
1747
- for (const node of doc.nodes) emit(node, 1);
1748
- const w = num(doc.width);
1749
- const h = num(doc.height);
3639
+ for (const node of doc.nodes) emit2(node, 1);
3640
+ const w = num2(doc.width);
3641
+ const h = num2(doc.height);
1750
3642
  const nl = pretty ? "\n" : "";
1751
3643
  const out = [`<svg xmlns="http://www.w3.org/2000/svg" width="${w}" height="${h}" viewBox="0 0 ${w} ${h}">`];
1752
3644
  if (defs.length) out.push(`${indent(1)}<defs>${defs.join("")}</defs>`);
@@ -1756,6 +3648,7 @@ function vectorDocumentToSvg(doc, options = {}) {
1756
3648
  out.push(...lines, "</svg>");
1757
3649
  return out.join(nl);
1758
3650
  }
3651
+ var BOOLEAN_OPERATIONS = ["union", "subtract", "intersect", "exclude"];
1759
3652
  function parseXml(source) {
1760
3653
  const text = source.replace(/<!--[\s\S]*?-->/g, "").replace(/<\?[\s\S]*?\?>/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<!DOCTYPE[^>[]*(\[[\s\S]*?\])?\s*>/gi, "");
1761
3654
  const root = { name: "#root", attrs: {}, children: [] };
@@ -1875,28 +3768,89 @@ var fraction = (s, fallback) => {
1875
3768
  if (!Number.isFinite(n)) return fallback;
1876
3769
  return s.trim().endsWith("%") ? n / 100 : n;
1877
3770
  };
3771
+ var PAINT_SERVERS = /* @__PURE__ */ new Set(["linearGradient", "radialGradient", "pattern"]);
1878
3772
  function collectGradients(el, raw) {
1879
- if ((el.name === "linearGradient" || el.name === "radialGradient") && el.attrs.id) raw.set(el.attrs.id, el);
3773
+ if (PAINT_SERVERS.has(el.name) && el.attrs.id) raw.set(el.attrs.id, el);
1880
3774
  for (const c of el.children) collectGradients(c, raw);
1881
3775
  }
1882
- function resolveGradient(id, raw, seen = /* @__PURE__ */ new Set()) {
1883
- const el = raw.get(id);
1884
- if (!el || seen.has(id)) return null;
1885
- seen.add(id);
1886
- let stops = readStops(el);
1887
- const href = el.attrs.href ?? el.attrs["xlink:href"];
1888
- if (!stops.length && href?.startsWith("#")) stops = resolveGradient(href.slice(1), raw, seen)?.stops ?? [];
1889
- if (!stops.length) return null;
1890
- if (el.name === "radialGradient") {
1891
- return { type: "radial", cx: fraction(el.attrs.cx, 0.5), cy: fraction(el.attrs.cy, 0.5), r: fraction(el.attrs.r, 0.5), stops };
3776
+ function gradientFromAttribute(value) {
3777
+ if (!value) return null;
3778
+ try {
3779
+ return sanitizeVectorGradient(JSON.parse(value));
3780
+ } catch {
3781
+ return null;
1892
3782
  }
1893
- const x1 = fraction(el.attrs.x1, 0);
1894
- const y1 = fraction(el.attrs.y1, 0);
1895
- const x2 = fraction(el.attrs.x2, 1);
1896
- const y2 = fraction(el.attrs.y2, 0);
1897
- const angle = Math.round((Math.atan2(x2 - x1, -(y2 - y1)) * 180 / Math.PI + 360) % 360);
1898
- return { type: "linear", angle, stops };
1899
3783
  }
3784
+ function resolveGradient(id, raw) {
3785
+ const first = raw.get(id);
3786
+ if (!first) return null;
3787
+ const own = gradientFromAttribute(first.attrs["data-vector-gradient"]);
3788
+ if (own) return own;
3789
+ if (first.name === "pattern") return null;
3790
+ const chain = [];
3791
+ for (let el = first; el && !chain.includes(el) && chain.length < 32; ) {
3792
+ chain.push(el);
3793
+ const href = el.attrs.href ?? el.attrs["xlink:href"];
3794
+ el = href?.startsWith("#") ? raw.get(href.slice(1)) : void 0;
3795
+ }
3796
+ const attr = (name) => chain.find((el) => el.attrs[name] !== void 0)?.attrs[name];
3797
+ const stops = chain.map(readStops).find((s) => s.length) ?? [];
3798
+ if (!stops.length) return null;
3799
+ const user = attr("gradientUnits") === "userSpaceOnUse";
3800
+ const t = parseSvgTransform(attr("gradientTransform"));
3801
+ const plain3 = t === IDENTITY_MATRIX || t[0] === 1 && t[1] === 0 && t[2] === 0 && t[3] === 1 && t[4] === 0 && t[5] === 0;
3802
+ const at2 = (name, fallback) => user ? length(attr(name), fallback) : fraction(attr(name), fallback);
3803
+ const spreadAttr = attr("spreadMethod");
3804
+ const spread = spreadAttr === "reflect" || spreadAttr === "repeat" ? { spread: spreadAttr } : {};
3805
+ const map = (p) => applySvgMatrix(t, p);
3806
+ if (first.name === "radialGradient") {
3807
+ const cx = at2("cx", user ? 0 : 0.5);
3808
+ const cy = at2("cy", user ? 0 : 0.5);
3809
+ const r = at2("r", user ? 0 : 0.5);
3810
+ const fx = attr("fx") !== void 0 ? at2("fx", cx) : cx;
3811
+ const fy = attr("fy") !== void 0 ? at2("fy", cy) : cy;
3812
+ const fr = attr("fr") !== void 0 ? at2("fr", 0) : 0;
3813
+ const focal = Math.abs(fx - cx) > 1e-9 || Math.abs(fy - cy) > 1e-9;
3814
+ if (!user && plain3 && !focal && !(fr > 0)) return { type: "radial", cx, cy, r, stops, ...spread };
3815
+ const g = {
3816
+ type: "radial",
3817
+ stops,
3818
+ start: map({ x: cx, y: cy }),
3819
+ end: map({ x: cx + r, y: cy }),
3820
+ width: map({ x: cx, y: cy + r }),
3821
+ units: user ? "userSpaceOnUse" : "objectBoundingBox",
3822
+ ...spread
3823
+ };
3824
+ if (focal) g.focal = map({ x: fx, y: fy });
3825
+ if (fr > 0 && r > 0) g.focalRadius = Math.min(0.99, fr / r);
3826
+ return g;
3827
+ }
3828
+ const x1 = at2("x1", 0);
3829
+ const y1 = at2("y1", 0);
3830
+ const x2 = attr("x2") !== void 0 ? at2("x2", 1) : user ? 0 : 1;
3831
+ const y2 = at2("y2", 0);
3832
+ if (!user && plain3) {
3833
+ const angle = Math.round((Math.atan2(x2 - x1, -(y2 - y1)) * 180 / Math.PI + 360) % 360);
3834
+ const c = angleToSvgLinearCoords(angle);
3835
+ const len2 = Math.hypot(x2 - x1, y2 - y1);
3836
+ const d = len2 > 0 ? { x: (x2 - x1) / len2, y: (y2 - y1) / len2 } : { x: 0, y: 1 };
3837
+ const along2 = (x, y) => x * d.x + y * d.y;
3838
+ const same = len2 > 0 && Math.abs((c.x2 - c.x1) * d.y - (c.y2 - c.y1) * d.x) < 1e-3 && Math.abs(along2(c.x1, c.y1) - along2(x1, y1)) < 1e-3 && Math.abs(along2(c.x2, c.y2) - along2(x2, y2)) < 1e-3;
3839
+ if (same) return { type: "linear", angle, stops, ...spread };
3840
+ }
3841
+ const start = { x: x1, y: y1 };
3842
+ const end = { x: x2, y: y2 };
3843
+ return {
3844
+ type: "linear",
3845
+ stops,
3846
+ start: map(start),
3847
+ end: map(end),
3848
+ width: map({ x: x1 - (y2 - y1), y: y1 + (x2 - x1) }),
3849
+ units: user ? "userSpaceOnUse" : "objectBoundingBox",
3850
+ ...spread
3851
+ };
3852
+ }
3853
+ var applySvgMatrix = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
1900
3854
  function parseVectorSvg(svg, options = {}) {
1901
3855
  const root = parseXml(svg);
1902
3856
  if (!root) throw new Error("The markup has no <svg> element.");
@@ -1997,7 +3951,7 @@ function parseVectorSvg(svg, options = {}) {
1997
3951
  const ss = strokeEl ? readStyle(strokeEl, style) : null;
1998
3952
  const width2 = length(a["data-vector-stroke-width"], ss ? length(ss.strokeWidth, 1) : 1);
1999
3953
  const arrow = (v2) => VECTOR_ARROWHEADS.includes(v2) ? v2 : void 0;
2000
- const scale = (v2) => {
3954
+ const scale3 = (v2) => {
2001
3955
  const n = v2 === void 0 ? NaN : parseFloat(v2);
2002
3956
  return n > 0 ? n : void 0;
2003
3957
  };
@@ -2021,8 +3975,8 @@ function parseVectorSvg(svg, options = {}) {
2021
3975
  ...arrow(a["data-vector-start-arrow"]) ? { startArrow: arrow(a["data-vector-start-arrow"]) } : {},
2022
3976
  ...arrow(a["data-vector-end-arrow"]) ? { endArrow: arrow(a["data-vector-end-arrow"]) } : {},
2023
3977
  ...a["data-vector-arrow-align"] === "tip" ? { arrowAlign: "tip" } : {},
2024
- ...scale(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale(a["data-vector-start-arrow-scale"]) } : {},
2025
- ...scale(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale(a["data-vector-end-arrow-scale"]) } : {},
3978
+ ...scale3(a["data-vector-start-arrow-scale"]) ? { startArrowScale: scale3(a["data-vector-start-arrow-scale"]) } : {},
3979
+ ...scale3(a["data-vector-end-arrow-scale"]) ? { endArrowScale: scale3(a["data-vector-end-arrow-scale"]) } : {},
2026
3980
  ...profile.length ? { strokeWidthProfile: profile } : {},
2027
3981
  ...keptCap ? { strokeLinecap: keptCap } : {},
2028
3982
  ...keptJoin ? { strokeLinejoin: keptJoin } : {},
@@ -2030,6 +3984,34 @@ function parseVectorSvg(svg, options = {}) {
2030
3984
  };
2031
3985
  return placed(node, m);
2032
3986
  };
3987
+ const liveNode = (g, style, m, meta) => {
3988
+ let raw = {};
3989
+ try {
3990
+ raw = JSON.parse(g.attrs["data-vector-live"]) ?? {};
3991
+ } catch {
3992
+ raw = {};
3993
+ }
3994
+ const sourceEl = g.children.find((c) => c.name === "g" && c.attrs["data-vector-part"] === "source");
3995
+ if (!sourceEl) return null;
3996
+ const inner = visit(sourceEl, style, m);
3997
+ const effects = sanitizeVectorEffects(raw.effects);
3998
+ const liveBoolean = BOOLEAN_OPERATIONS.includes(raw.liveBoolean) ? raw.liveBoolean : null;
3999
+ const id = idFor(g);
4000
+ if (raw.kind === "path") {
4001
+ const path = inner.find((n) => n.type === "path");
4002
+ if (!path) return null;
4003
+ const { residual } = decomposeRigid(m);
4004
+ const { visible: _v, ...rest } = path;
4005
+ return { ...rest, id, ...meta, ...effects.length ? { effects: transformVectorEffects(effects, residual) } : {} };
4006
+ }
4007
+ if (!inner.length) return null;
4008
+ const group = { id, type: "group", ...meta, children: inner };
4009
+ if (effects.length) group.effects = transformVectorEffects(effects, m);
4010
+ if (liveBoolean) group.liveBoolean = liveBoolean;
4011
+ const blend = blendFromAttribute(g.attrs["data-vector-blend"], m);
4012
+ if (blend) group.blend = blend;
4013
+ return group;
4014
+ };
2033
4015
  const visit = (el, style, matrix) => {
2034
4016
  const out = [];
2035
4017
  for (const child of el.children) {
@@ -2044,6 +4026,13 @@ function parseVectorSvg(svg, options = {}) {
2044
4026
  ...hidden ? { visible: false } : {}
2045
4027
  };
2046
4028
  if (child.attrs["data-vector-blend-step"] !== void 0) continue;
4029
+ const operation = child.attrs["data-vector-boolean-operation"];
4030
+ if (BOOLEAN_OPERATIONS.includes(operation)) meta.booleanOperation = operation;
4031
+ if (child.name === "g" && child.attrs["data-vector-live"] !== void 0) {
4032
+ const node2 = liveNode(child, s, m, meta);
4033
+ if (node2) out.push(node2);
4034
+ continue;
4035
+ }
2047
4036
  if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
2048
4037
  const node2 = mergedPath(child, s, m, meta);
2049
4038
  if (node2) out.push(node2);
@@ -2080,6 +4069,72 @@ function parseVectorSvg(svg, options = {}) {
2080
4069
  return { width, height, nodes };
2081
4070
  }
2082
4071
 
4072
+ // src/vector/gradientTool.ts
4073
+ var VECTOR_GRADIENT_STOP_GAP = 12;
4074
+ var VECTOR_GRADIENT_MIDPOINT_GAP = 9;
4075
+ var VECTOR_GRADIENT_OFF_BAR = 24;
4076
+ var STOP_HIT = 6;
4077
+ var MIDPOINT_HIT = 5;
4078
+ var HANDLE_HIT = 6;
4079
+ var BAR_HIT = 5;
4080
+ var fmt3 = (n) => Math.round(n * 100) / 100;
4081
+ var apply2 = (m, p) => ({ x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] });
4082
+ function vectorGradientTargetPath(doc, selection, target) {
4083
+ for (const id of selection) {
4084
+ const node = findVectorNode(doc.nodes, id);
4085
+ if (!node) continue;
4086
+ const path = collectPathNodes(node).find((p) => isVectorGradient(p[target]));
4087
+ if (path) return path;
4088
+ }
4089
+ return null;
4090
+ }
4091
+ function getVectorGradientAnnotator(doc, selection, viewport, target) {
4092
+ const path = vectorGradientTargetPath(doc, selection, target);
4093
+ if (!path) return null;
4094
+ const gradient = path[target];
4095
+ const world = vectorWorldMatrix(doc.nodes, path.id) ?? IDENTITY_MATRIX;
4096
+ const screen = multiplyMatrices(world, [viewport.zoom, 0, 0, viewport.zoom, viewport.x, viewport.y]);
4097
+ const bounds = vectorPathLocalBounds(path);
4098
+ const h = vectorGradientHandles(gradient, bounds, screen);
4099
+ const v2 = { x: h.end.x - h.start.x, y: h.end.y - h.start.y };
4100
+ const len2 = Math.hypot(v2.x, v2.y);
4101
+ const normal = len2 > 1e-9 ? { x: -v2.y / len2, y: v2.x / len2 } : { x: 0, y: 1 };
4102
+ const at2 = (t, gap) => ({ x: h.start.x + v2.x * t + normal.x * gap, y: h.start.y + v2.y * t + normal.y * gap });
4103
+ const sorted = sortedVectorGradientStops(gradient);
4104
+ const stops = sorted.map((s, index) => ({ ...s, index, ...at2(s.offset, VECTOR_GRADIENT_STOP_GAP) }));
4105
+ const midpoints = sorted.slice(0, -1).flatMap((s, index) => {
4106
+ const pos = vectorGradientMidpointPosition(sorted, index);
4107
+ return pos === null || sorted[index + 1].offset - s.offset < 1e-6 ? [] : [{ index, midpoint: s.midpoint ?? 0.5, ...at2(pos, -VECTOR_GRADIENT_MIDPOINT_GAP) }];
4108
+ });
4109
+ let extent = null;
4110
+ if (isCentredVectorGradient(gradient.type)) {
4111
+ const frame2 = multiplyMatrices(vectorGradientMatrix(gradient, bounds), screen);
4112
+ const pts = gradient.type === "diamond" ? [{ x: 1, y: 0 }, { x: 0, y: 1 }, { x: -1, y: 0 }, { x: 0, y: -1 }] : Array.from({ length: 48 }, (_, k) => ({ x: Math.cos(k * Math.PI / 24), y: Math.sin(k * Math.PI / 24) }));
4113
+ extent = `M${pts.map((p) => apply2(frame2, p)).map((p) => `${fmt3(p.x)} ${fmt3(p.y)}`).join("L")}Z`;
4114
+ }
4115
+ return { nodeId: path.id, target, gradient, bounds, world, start: h.start, end: h.end, aspect: h.aspect, focal: h.focal, normal, stops, midpoints, extent };
4116
+ }
4117
+ function vectorGradientBarPosition(a, p) {
4118
+ const v2 = { x: a.end.x - a.start.x, y: a.end.y - a.start.y };
4119
+ const l2 = v2.x * v2.x + v2.y * v2.y;
4120
+ const d = { x: p.x - a.start.x, y: p.y - a.start.y };
4121
+ if (l2 < 1e-12) return { offset: 0, across: Math.hypot(d.x, d.y) };
4122
+ return { offset: (d.x * v2.x + d.y * v2.y) / l2, across: d.x * a.normal.x + d.y * a.normal.y };
4123
+ }
4124
+ function hitVectorGradientAnnotator(a, p) {
4125
+ const near2 = (q, r) => !!q && Math.hypot(q.x - p.x, q.y - p.y) <= r;
4126
+ for (let i = a.stops.length - 1; i >= 0; i--) if (near2(a.stops[i], STOP_HIT)) return { kind: "stop", index: a.stops[i].index };
4127
+ for (const m of a.midpoints) if (near2(m, MIDPOINT_HIT)) return { kind: "midpoint", index: m.index };
4128
+ const centred = isCentredVectorGradient(a.gradient.type);
4129
+ if (near2(a.end, HANDLE_HIT)) return { kind: "handle", handle: centred ? "radius" : "end" };
4130
+ if (a.focal && near2(a.focal, HANDLE_HIT) && !near2(a.start, 1)) return { kind: "handle", handle: "focal" };
4131
+ if (near2(a.start, HANDLE_HIT)) return { kind: "handle", handle: "start" };
4132
+ if (near2(a.aspect, HANDLE_HIT)) return { kind: "handle", handle: "aspect" };
4133
+ const { offset, across } = vectorGradientBarPosition(a, p);
4134
+ if (offset >= 0 && offset <= 1 && across >= -BAR_HIT && across <= VECTOR_GRADIENT_STOP_GAP + STOP_HIT) return { kind: "bar", offset };
4135
+ return null;
4136
+ }
4137
+
2083
4138
  // src/vector/blend.ts
2084
4139
  var lerpN = (a, b, t) => a + (b - a) * t;
2085
4140
  var lerpP = (a, b, t) => [a[0] + (b[0] - a[0]) * t, a[1] + (b[1] - a[1]) * t];
@@ -2171,10 +4226,10 @@ function refineMorphContour(c, segments) {
2171
4226
  const extra = segments - have;
2172
4227
  const want = lens.map((l) => sum > 1e-12 ? l / sum * extra : extra / have);
2173
4228
  const parts = want.map((w) => 1 + Math.floor(w));
2174
- let left = segments - parts.reduce((a, b) => a + b, 0);
4229
+ let left2 = segments - parts.reduce((a, b) => a + b, 0);
2175
4230
  const frac = want.map((w) => w - Math.floor(w));
2176
4231
  const order = parts.map((_, i) => i).sort((a, b) => Math.abs(frac[b] - frac[a]) > 1e-9 ? frac[b] - frac[a] : a - b);
2177
- for (let i = 0; left > 0; i = (i + 1) % order.length, left--) parts[order[i]]++;
4232
+ for (let i = 0; left2 > 0; i = (i + 1) % order.length, left2--) parts[order[i]]++;
2178
4233
  const pts = [c.pts[0]];
2179
4234
  const anchors = [0];
2180
4235
  let at2 = 0;
@@ -2283,19 +4338,19 @@ function bestStart(fixed, free, useShapeContext) {
2283
4338
  const gsc = useShapeContext ? shapeContexts(ga, free.closed) : null;
2284
4339
  const m = Math.max(1, fn.length);
2285
4340
  const cost = (s) => {
2286
- let dot2 = 0;
4341
+ let dot3 = 0;
2287
4342
  let cross2 = 0;
2288
4343
  let sc = 0;
2289
4344
  for (let i = 0; i < fn.length; i++) {
2290
4345
  const j = free.closed ? (i + s) % n : i;
2291
4346
  const p = fn[i];
2292
4347
  const q = gn[j];
2293
- dot2 += p[0] * q[0] + p[1] * q[1];
4348
+ dot3 += p[0] * q[0] + p[1] * q[1];
2294
4349
  cross2 += p[0] * q[1] - p[1] * q[0];
2295
4350
  if (fsc && gsc) sc += chi2(fsc[i], gsc[j]);
2296
4351
  }
2297
- const residual = Math.max(0, 1 - (dot2 * dot2 + cross2 * cross2) / (m * m));
2298
- const turn = 1 - Math.cos(Math.atan2(cross2, dot2));
4352
+ const residual = Math.max(0, 1 - (dot3 * dot3 + cross2 * cross2) / (m * m));
4353
+ const turn = 1 - Math.cos(Math.atan2(cross2, dot3));
2299
4354
  return residual + TURN_WEIGHT * turn + sc / m;
2300
4355
  };
2301
4356
  if (!free.closed) return { start: 0, cost: cost(0) };
@@ -2440,16 +4495,16 @@ function procrustes(a, b) {
2440
4495
  const ca = frame(pa).c;
2441
4496
  const cb = frame(pb).c;
2442
4497
  let ss = 0;
2443
- let dot2 = 0;
4498
+ let dot3 = 0;
2444
4499
  let cross2 = 0;
2445
4500
  for (let i = 0; i < Math.min(pa.length, pb.length); i++) {
2446
4501
  const p = [pa[i][0] - ca[0], pa[i][1] - ca[1]];
2447
4502
  const q = [pb[i][0] - cb[0], pb[i][1] - cb[1]];
2448
4503
  ss += p[0] * p[0] + p[1] * p[1];
2449
- dot2 += p[0] * q[0] + p[1] * q[1];
4504
+ dot3 += p[0] * q[0] + p[1] * q[1];
2450
4505
  cross2 += p[0] * q[1] - p[1] * q[0];
2451
4506
  }
2452
- return { ca, cb, theta: Math.atan2(cross2, dot2), s: ss > 1e-12 ? Math.hypot(dot2, cross2) / ss : 1 };
4507
+ return { ca, cb, theta: Math.atan2(cross2, dot3), s: ss > 1e-12 ? Math.hypot(dot3, cross2) / ss : 1 };
2453
4508
  }
2454
4509
  function interpolateMorphContours(a, b, u, options = {}) {
2455
4510
  const w = options.linear ? 0 : Math.min(1, Math.max(0, options.rigidity ?? 1));
@@ -2503,39 +4558,24 @@ function interpolateVectorColor(a, b, t) {
2503
4558
  if (t >= 1) return b;
2504
4559
  return mixColorsOklab(a, b, t) ?? (t < 0.5 ? a : b);
2505
4560
  }
2506
- function sortedStops(g) {
2507
- return g.stops.slice().sort((x, y) => x.offset - y.offset);
2508
- }
2509
- function sampleGradient(stops, offset) {
2510
- if (!stops.length) return { color: "transparent", opacity: 1 };
2511
- const first = stops[0];
2512
- const last = stops[stops.length - 1];
2513
- if (offset <= first.offset) return { color: first.color, opacity: first.opacity ?? 1 };
2514
- if (offset >= last.offset) return { color: last.color, opacity: last.opacity ?? 1 };
2515
- let i = 0;
2516
- while (i + 1 < stops.length && stops[i + 1].offset < offset) i++;
2517
- const x = stops[i];
2518
- const y = stops[i + 1];
2519
- const u = y.offset - x.offset > 1e-12 ? (offset - x.offset) / (y.offset - x.offset) : 1;
2520
- return { color: interpolateVectorColor(x.color, y.color, u), opacity: lerpN(x.opacity ?? 1, y.opacity ?? 1, u) };
2521
- }
2522
4561
  function resampleGradientStops(g, offsets) {
2523
- const stops = sortedStops(g);
2524
4562
  return offsets.map((offset) => {
2525
- const s = sampleGradient(stops, offset);
2526
- return s.opacity < 1 ? { offset, color: s.color, opacity: s.opacity } : { offset, color: s.color };
4563
+ const s = sampleVectorGradient(g, offset);
4564
+ return s.opacity < 1 ? { offset, color: s.color, opacity: Math.round(s.opacity * 1e4) / 1e4 } : { offset, color: s.color };
2527
4565
  });
2528
4566
  }
2529
4567
  function lerpAngle(a, b, t) {
2530
4568
  const d = ((b - a + 180) % 360 + 360) % 360 - 180;
2531
4569
  return a + d * t;
2532
4570
  }
2533
- function lerpGradient(a, b, t) {
2534
- if (a.type !== b.type) return null;
2535
- let stopsA = sortedStops(a);
2536
- let stopsB = sortedStops(b);
2537
- if (stopsA.length !== stopsB.length) {
2538
- const offs = [...stopsA, ...stopsB].map((s) => s.offset).filter(Number.isFinite).sort((x, y) => x - y);
4571
+ var lerpPoint = (a, b, t) => ({ x: lerpN(a.x, b.x, t), y: lerpN(a.y, b.y, t) });
4572
+ var sameColourModel = (a, b) => (a.interpolation ?? "srgb") === (b.interpolation ?? "srgb") && (a.smoothness ?? 0) === (b.smoothness ?? 0);
4573
+ function lerpStops(a, b, t) {
4574
+ let stopsA = sortedVectorGradientStops(a);
4575
+ let stopsB = sortedVectorGradientStops(b);
4576
+ const resampled = stopsA.length !== stopsB.length || !sameColourModel(a, b);
4577
+ if (resampled) {
4578
+ const offs = [...expandVectorGradientStops(a), ...expandVectorGradientStops(b), ...stopsA, ...stopsB].map((s) => s.offset).filter(Number.isFinite).sort((x, y) => x - y);
2539
4579
  const common = offs.filter((o, i) => i === 0 || o - offs[i - 1] > 1e-4);
2540
4580
  stopsA = resampleGradientStops(a, common);
2541
4581
  stopsB = resampleGradientStops(b, common);
@@ -2545,12 +4585,53 @@ function lerpGradient(a, b, t) {
2545
4585
  const opacity = lerpN(x.opacity ?? 1, y.opacity ?? 1, t);
2546
4586
  const stop = { offset: lerpN(x.offset, y.offset, t), color: interpolateVectorColor(x.color, y.color, t) };
2547
4587
  if (opacity < 1) stop.opacity = opacity;
4588
+ const mid = lerpN(x.midpoint ?? 0.5, y.midpoint ?? 0.5, t);
4589
+ if (Math.abs(mid - 0.5) > 1e-6) stop.midpoint = mid;
2548
4590
  return stop;
2549
4591
  });
2550
- if (a.type === "linear" && b.type === "linear") return { type: "linear", angle: lerpAngle(a.angle ?? 90, b.angle ?? 90, t), stops };
2551
- const ra = a;
2552
- const rb = b;
2553
- return { type: "radial", cx: lerpN(ra.cx ?? 0.5, rb.cx ?? 0.5, t), cy: lerpN(ra.cy ?? 0.5, rb.cy ?? 0.5, t), r: lerpN(ra.r ?? 0.5, rb.r ?? 0.5, t), stops };
4592
+ return { stops, resampled };
4593
+ }
4594
+ function lerpGradient(a, b, t) {
4595
+ if (a.type !== b.type) return null;
4596
+ const { stops, resampled } = lerpStops(a, b, t);
4597
+ const near2 = t < 0.5 ? a : b;
4598
+ const out = { type: a.type, stops };
4599
+ if (!resampled) {
4600
+ if (a.interpolation && a.interpolation !== "srgb") out.interpolation = a.interpolation;
4601
+ if (a.smoothness) out.smoothness = a.smoothness;
4602
+ }
4603
+ if (near2.spread && near2.spread !== "pad") out.spread = near2.spread;
4604
+ if (near2.strokeMode && near2.strokeMode !== "within") out.strokeMode = near2.strokeMode;
4605
+ const ea = hasExplicitGradientGeometry(a);
4606
+ const eb = hasExplicitGradientGeometry(b);
4607
+ if (!ea && !eb) {
4608
+ if (a.angle !== void 0 || b.angle !== void 0 || a.type === "linear" || a.type === "reflected") {
4609
+ const fallback = a.type === "linear" || a.type === "reflected" ? 180 : 90;
4610
+ out.angle = lerpAngle(a.angle ?? fallback, b.angle ?? fallback, t);
4611
+ }
4612
+ if (a.type !== "linear" && a.type !== "reflected") {
4613
+ out.cx = lerpN(a.cx ?? 0.5, b.cx ?? 0.5, t);
4614
+ out.cy = lerpN(a.cy ?? 0.5, b.cy ?? 0.5, t);
4615
+ out.r = lerpN(a.r ?? 0.5, b.r ?? 0.5, t);
4616
+ }
4617
+ return out;
4618
+ }
4619
+ const pa = vectorGradientPoints(a);
4620
+ const pb = vectorGradientPoints(b);
4621
+ if (pa.units !== pb.units) {
4622
+ const src = explicitVectorGradient(near2);
4623
+ return { ...src, stops: out.stops, ...out.interpolation ? { interpolation: out.interpolation } : {}, ...out.smoothness ? { smoothness: out.smoothness } : {} };
4624
+ }
4625
+ out.start = lerpPoint(pa.start, pb.start, t);
4626
+ out.end = lerpPoint(pa.end, pb.end, t);
4627
+ out.width = lerpPoint(pa.width, pb.width, t);
4628
+ out.units = pa.units;
4629
+ if (a.type === "radial") {
4630
+ if (pa.focal || pb.focal) out.focal = lerpPoint(pa.focal ?? pa.start, pb.focal ?? pb.start, t);
4631
+ const fr = lerpN(pa.focalRadius, pb.focalRadius, t);
4632
+ if (fr > 0) out.focalRadius = fr;
4633
+ }
4634
+ return out;
2554
4635
  }
2555
4636
  function solidLike(color, like) {
2556
4637
  return { ...like, stops: like.stops.map((s) => color === null ? { offset: s.offset, color: s.color, opacity: 0 } : { offset: s.offset, color }) };
@@ -2564,8 +4645,8 @@ function interpolateVectorPaint(a, b, t) {
2564
4645
  if (typeof a === "string" && pa && typeof b === "string" && pb) return interpolateVectorColor(a, b, t);
2565
4646
  if (!pa && typeof b === "string") return interpolateVectorColor("transparent", b, t);
2566
4647
  if (!pb && typeof a === "string") return interpolateVectorColor(a, "transparent", t);
2567
- const ga = pa && typeof a === "object" && a ? a : null;
2568
- const gb = pb && typeof b === "object" && b ? b : null;
4648
+ const ga = pa && isVectorGradient(a) ? a : null;
4649
+ const gb = pb && isVectorGradient(b) ? b : null;
2569
4650
  if (ga && gb) return lerpGradient(ga, gb, t) ?? (t < 0.5 ? a : b);
2570
4651
  if (gb) return lerpGradient(solidLike(pa ? a : null, gb), gb, t) ?? b;
2571
4652
  if (ga) return lerpGradient(ga, solidLike(pb ? b : null, ga), t) ?? a;
@@ -2702,15 +4783,15 @@ function createVectorMorph(a, b, options = {}) {
2702
4783
  for (let i = 0; i < Math.max(pa.length, pb.length); i++) {
2703
4784
  const x = pa[i];
2704
4785
  const y = pb[i];
2705
- const sub3 = { ...options, id: `${id}-${i}`, starts: void 0 };
4786
+ const sub5 = { ...options, id: `${id}-${i}`, starts: void 0 };
2706
4787
  if (x && y) {
2707
- parts.push(preparePathMorph(x.path, y.path, sub3, x.m, y.m));
4788
+ parts.push(preparePathMorph(x.path, y.path, sub5, x.m, y.m));
2708
4789
  opacities.push([x.opacity, y.opacity]);
2709
4790
  } else if (x) {
2710
- parts.push(preparePathMorph(x.path, collapsedPath(x.path, x.m, ba, bb), sub3, x.m, IDENTITY_MATRIX));
4791
+ parts.push(preparePathMorph(x.path, collapsedPath(x.path, x.m, ba, bb), sub5, x.m, IDENTITY_MATRIX));
2711
4792
  opacities.push([x.opacity, x.opacity]);
2712
4793
  } else if (y) {
2713
- parts.push(preparePathMorph(collapsedPath(y.path, y.m, bb, ba), y.path, sub3, IDENTITY_MATRIX, y.m));
4794
+ parts.push(preparePathMorph(collapsedPath(y.path, y.m, bb, ba), y.path, sub5, IDENTITY_MATRIX, y.m));
2714
4795
  opacities.push([y.opacity, y.opacity]);
2715
4796
  }
2716
4797
  }
@@ -3197,8 +5278,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
3197
5278
  let anchors = BUDGET;
3198
5279
  let segments = BUDGET;
3199
5280
  const visit = (list, parent) => {
3200
- for (const n of list) {
3201
- if (exclude.has(n.id) || n.visible === false) continue;
5281
+ for (const stored of list) {
5282
+ if (exclude.has(stored.id) || stored.visible === false) continue;
5283
+ const n = contains(stored, exclude) ? stored : expandVectorAppearance(stored);
3202
5284
  const m = multiplyMatrices(nodeMatrix(n), parent);
3203
5285
  if (n.type === "group") {
3204
5286
  visit(n.children, m);
@@ -3694,16 +5776,16 @@ function solveOnSegment(seg, base, cond) {
3694
5776
  return cond([p[0] - b[0], p[1] - b[1]], segmentDerivative(seg, t));
3695
5777
  };
3696
5778
  const vals = [];
3697
- let scale = 0;
5779
+ let scale3 = 0;
3698
5780
  for (let i = 0; i <= CURVE_SAMPLES; i++) {
3699
5781
  const t = i / CURVE_SAMPLES;
3700
5782
  const p = segmentPoint(seg, t);
3701
5783
  const d = segmentDerivative(seg, t);
3702
5784
  const r = [p[0] - b[0], p[1] - b[1]];
3703
5785
  vals.push(cond(r, d));
3704
- scale = Math.max(scale, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
5786
+ scale3 = Math.max(scale3, Math.hypot(r[0], r[1]) * Math.hypot(d[0], d[1]));
3705
5787
  }
3706
- if (!(scale > 0) || vals.every((v2) => Math.abs(v2) <= scale * 1e-12)) return [];
5788
+ if (!(scale3 > 0) || vals.every((v2) => Math.abs(v2) <= scale3 * 1e-12)) return [];
3707
5789
  const out = [];
3708
5790
  for (let i = 0; i < CURVE_SAMPLES; i++) {
3709
5791
  const ga = vals[i];
@@ -3736,8 +5818,8 @@ function solveOnSegment(seg, base, cond) {
3736
5818
  }
3737
5819
 
3738
5820
  // src/vector/pathEdit.ts
3739
- var EPS3 = 1e-9;
3740
- var unit2 = (x, y) => {
5821
+ var EPS4 = 1e-9;
5822
+ var unit4 = (x, y) => {
3741
5823
  const l = Math.hypot(x, y);
3742
5824
  return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
3743
5825
  };
@@ -3748,16 +5830,16 @@ function cubicOf(sp, i) {
3748
5830
  return [a, [a[0] + (b[0] - a[0]) / 3, a[1] + (b[1] - a[1]) / 3], [a[0] + 2 * (b[0] - a[0]) / 3, a[1] + 2 * (b[1] - a[1]) / 3], b];
3749
5831
  }
3750
5832
  var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
3751
- function startTangent(c) {
5833
+ function startTangent2(c) {
3752
5834
  for (let k = 1; k < 4; k++) {
3753
- const u = unit2(c[k][0] - c[0][0], c[k][1] - c[0][1]);
5835
+ const u = unit4(c[k][0] - c[0][0], c[k][1] - c[0][1]);
3754
5836
  if (u) return u;
3755
5837
  }
3756
5838
  return [1, 0];
3757
5839
  }
3758
- function endTangent(c) {
5840
+ function endTangent2(c) {
3759
5841
  for (let k = 2; k >= 0; k--) {
3760
- const u = unit2(c[3][0] - c[k][0], c[3][1] - c[k][1]);
5842
+ const u = unit4(c[3][0] - c[k][0], c[3][1] - c[k][1]);
3761
5843
  if (u) return u;
3762
5844
  }
3763
5845
  return [1, 0];
@@ -3765,7 +5847,7 @@ function endTangent(c) {
3765
5847
  function sample(c, n, out, withStart) {
3766
5848
  for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
3767
5849
  }
3768
- var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
5850
+ var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS4 && Math.abs(h.y - a.y) < EPS4;
3769
5851
  function tidy(a, keepType = true) {
3770
5852
  const next = cloneAnchor(a);
3771
5853
  if (onAnchor2(next, next.in)) delete next.in;
@@ -3821,14 +5903,14 @@ function removeAnchorKeepingShape(sp, index) {
3821
5903
  const pi = (index + n - 1) % n;
3822
5904
  const ni = (index + 1) % n;
3823
5905
  if (!(isLine(sp, pi) && isLine(sp, index))) {
3824
- const left = cubicOf(sp, pi);
5906
+ const left2 = cubicOf(sp, pi);
3825
5907
  const right = cubicOf(sp, index);
3826
5908
  const N = 16;
3827
5909
  const pts = [];
3828
- sample(left, N, pts, true);
5910
+ sample(left2, N, pts, true);
3829
5911
  sample(right, N, pts, false);
3830
- const t0 = startTangent(left);
3831
- const t1 = endTangent(right);
5912
+ const t0 = startTangent2(left2);
5913
+ const t1 = endTangent2(right);
3832
5914
  const { curve } = fitAcross(pts, N, t0, t1);
3833
5915
  if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
3834
5916
  anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
@@ -4029,7 +6111,7 @@ function smoothSubpath(sp, amount = 1, only) {
4029
6111
  const prev = orig[(i + n - 1) % n];
4030
6112
  const next = orig[(i + 1) % n];
4031
6113
  const p = orig[i];
4032
- const dir = unit2(next.x - prev.x, next.y - prev.y);
6114
+ const dir = unit4(next.x - prev.x, next.y - prev.y);
4033
6115
  if (!dir) continue;
4034
6116
  const li = dist2(p, prev) / 3;
4035
6117
  const lo = dist2(p, next) / 3;
@@ -4095,13 +6177,13 @@ function mergePair(a, b, tol) {
4095
6177
  }
4096
6178
  const ca = a.line ? [a0, a0, a3, a3] : a.c;
4097
6179
  const cb = b.line ? [a3, a3, b3, b3] : b.c;
4098
- const ta = endTangent(ca);
4099
- const tb = startTangent(cb);
6180
+ const ta = endTangent2(ca);
6181
+ const tb = startTangent2(cb);
4100
6182
  if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
4101
6183
  const pts = [];
4102
6184
  sample(ca, 16, pts, true);
4103
6185
  sample(cb, 16, pts, false);
4104
- const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
6186
+ const { curve, error } = fitAcross(pts, 16, startTangent2(ca), endTangent2(cb));
4105
6187
  return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
4106
6188
  }
4107
6189
  function removeRedundantPoints(sp, tolerance) {
@@ -4164,9 +6246,9 @@ function facesOf(path, fillRule) {
4164
6246
  function splitIntoFaces(path, fillRule = "nonzero") {
4165
6247
  return facesOf(path, fillRule).map(faceToPath);
4166
6248
  }
4167
- function rootsInUnit(a, b, c, d, scale) {
6249
+ function rootsInUnit(a, b, c, d, scale3) {
4168
6250
  const f = (t) => ((a * t + b) * t + c) * t + d;
4169
- const tol = 1e-12 * Math.max(1, scale);
6251
+ const tol = 1e-12 * Math.max(1, scale3);
4170
6252
  const cuts = [0];
4171
6253
  const qa = 3 * a;
4172
6254
  const qb = 2 * b;
@@ -4223,9 +6305,9 @@ function lineHits(p, q, seg) {
4223
6305
  coeffs = [-h0 + 3 * h1 - 3 * h2 + h3, 3 * h0 - 6 * h1 + 3 * h2, -3 * h0 + 3 * h1, h0];
4224
6306
  }
4225
6307
  if (coeffs.every((c) => Math.abs(c) < 1e-12 * Math.max(1, len2))) return [];
4226
- const scale = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
6308
+ const scale3 = Math.sqrt(len2) * Math.max(1, ...seg.map((pt2) => Math.abs(pt2[0]) + Math.abs(pt2[1])));
4227
6309
  const out = [];
4228
- for (const t of rootsInUnit(...coeffs, scale)) {
6310
+ for (const t of rootsInUnit(...coeffs, scale3)) {
4229
6311
  const point = segmentPoint(seg, t);
4230
6312
  const s = ((point[0] - p[0]) * dx + (point[1] - p[1]) * dy) / len2;
4231
6313
  if (s >= -1e-9 && s <= 1 + 1e-9) out.push({ s: Math.min(1, Math.max(0, s)), t, point });
@@ -4233,14 +6315,14 @@ function lineHits(p, q, seg) {
4233
6315
  return out;
4234
6316
  }
4235
6317
  var near = (a, b, eps) => Math.abs(a[0] - b[0]) <= eps && Math.abs(a[1] - b[1]) <= eps;
4236
- var degenerate = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
6318
+ var degenerate2 = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
4237
6319
  function arc(l, a, b, eps) {
4238
6320
  const n = l.segs.length;
4239
6321
  const segs = [];
4240
6322
  const chord = [];
4241
6323
  const push = (k, t0, t1) => {
4242
6324
  const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : subSegment(l.segs[k], t0, t1);
4243
- if (!degenerate(piece, eps)) {
6325
+ if (!degenerate2(piece, eps)) {
4244
6326
  segs.push(piece);
4245
6327
  chord.push(l.chord[k]);
4246
6328
  }
@@ -4698,8 +6780,9 @@ function recognizeInkShape(points, options = {}) {
4698
6780
  // src/vector/builder.ts
4699
6781
  function vectorPathEntries(doc, roots, options = {}) {
4700
6782
  const out = [];
4701
- const visit = (node, parent, inside) => {
4702
- if (node.visible === false || node.locked && !options.locked) return;
6783
+ const visit = (stored, parent, inside) => {
6784
+ if (stored.visible === false || stored.locked && !options.locked) return;
6785
+ const node = options.expand === false ? stored : expandVectorAppearance(stored);
4703
6786
  const picked = inside || roots === null || roots.includes(node.id);
4704
6787
  if (node.type === "group") {
4705
6788
  const m = multiplyMatrices(nodeMatrix(node), parent);
@@ -4753,7 +6836,7 @@ var STROKE_KEYS = [
4753
6836
  "strokeWidthProfile"
4754
6837
  ];
4755
6838
  function vectorResultStyle(node, paint, edge = false) {
4756
- const { id: _id, subpaths: _s, transform: _t, ...rest } = node;
6839
+ const { id: _id, subpaths: _s, transform: _t, effects: _e, booleanOperation: _b, ...rest } = node;
4757
6840
  const base = { ...rest, fillRule: "nonzero" };
4758
6841
  if (edge) {
4759
6842
  for (const k of STROKE_KEYS) delete base[k];
@@ -5170,7 +7253,8 @@ var TOOL_LABELS = {
5170
7253
  width: "Width",
5171
7254
  shapeBuilder: "Shape Builder",
5172
7255
  paintBucket: "Paint Bucket",
5173
- blend: "Blend"
7256
+ blend: "Blend",
7257
+ gradient: "Gradient"
5174
7258
  };
5175
7259
  var TOOL_KEYS = {
5176
7260
  v: "select",
@@ -5188,7 +7272,8 @@ var TOOL_KEYS = {
5188
7272
  w: "width",
5189
7273
  m: "shapeBuilder",
5190
7274
  b: "paintBucket",
5191
- j: "blend"
7275
+ j: "blend",
7276
+ g: "gradient"
5192
7277
  };
5193
7278
  function vectorToolLabel(tool) {
5194
7279
  return TOOL_LABELS[tool];
@@ -5209,7 +7294,8 @@ var VECTOR_TOOL_SHORTCUTS = {
5209
7294
  width: "W",
5210
7295
  shapeBuilder: "M",
5211
7296
  paintBucket: "B",
5212
- blend: "J"
7297
+ blend: "J",
7298
+ gradient: "G"
5213
7299
  };
5214
7300
  var VECTOR_TOOL_GLYPHS = {
5215
7301
  select: "\u2196",
@@ -5227,7 +7313,8 @@ var VECTOR_TOOL_GLYPHS = {
5227
7313
  width: "\u21F9",
5228
7314
  shapeBuilder: "\u2295",
5229
7315
  paintBucket: "\u25CD",
5230
- blend: "\u29D3"
7316
+ blend: "\u29D3",
7317
+ gradient: "\u25D0"
5231
7318
  };
5232
7319
  var SHAPE_TOOLS = /* @__PURE__ */ new Set(["rect", "ellipse", "polygon", "star", "line"]);
5233
7320
  var BOOLEAN_OPS = {
@@ -5281,6 +7368,12 @@ function spliceTree(nodes, id, replacements) {
5281
7368
  return changed ? out : nodes;
5282
7369
  }
5283
7370
  var toPoint2 = (p) => [p.x, p.y];
7371
+ var placementOf = (n) => ({
7372
+ ...n.transform ? { transform: n.transform } : {},
7373
+ ...n.visible === false ? { visible: false } : {},
7374
+ ...n.locked ? { locked: true } : {},
7375
+ ...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}
7376
+ });
5284
7377
  var isRoot = (m) => m === IDENTITY_MATRIX || m[0] === 1 && m[1] === 0 && m[2] === 0 && m[3] === 1 && m[4] === 0 && m[5] === 0;
5285
7378
  var idCounter = 0;
5286
7379
  var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
@@ -5318,6 +7411,7 @@ var VectorEditorController = class {
5318
7411
  blendStart = null;
5319
7412
  /** Options new blends get (the toolbars edit them along with the selected blend's). */
5320
7413
  blendDefaults = {};
7414
+ gradientEdit = { target: "fill", stop: null };
5321
7415
  /** What the highlight shows, to skip notifying when the pointer stays over the same face. */
5322
7416
  builderHoverKey = null;
5323
7417
  createId;
@@ -5342,15 +7436,26 @@ var VectorEditorController = class {
5342
7436
  this.createId = options.createId ?? defaultCreateId;
5343
7437
  this.onChange = options.onChange;
5344
7438
  this.state = this.computeState();
7439
+ this.connect();
7440
+ }
7441
+ /** Listens to the history (once; again after `destroy`). */
7442
+ connect() {
7443
+ if (this.cleanups.length) return;
5345
7444
  this.cleanups.push(
5346
7445
  this.history.subscribe((h) => {
5347
- const s = this.state;
5348
- if (this.applying || s.canUndo === h.canUndo && s.canRedo === h.canRedo && s.undoLabel === h.undoName && s.redoLabel === h.redoName) return;
5349
- this.notify();
7446
+ if (!this.applying && this.historyDiffers(h)) this.notify();
5350
7447
  })
5351
7448
  );
7449
+ if (this.historyDiffers(this.history.getState())) this.state = this.computeState();
5352
7450
  }
5353
- /** Stops listening to the history. */
7451
+ historyDiffers(h) {
7452
+ const s = this.state;
7453
+ return s.canUndo !== h.canUndo || s.canRedo !== h.canRedo || s.undoLabel !== h.undoName || s.redoLabel !== h.redoName;
7454
+ }
7455
+ /**
7456
+ * Stops listening to the history and drops all listeners. A later `subscribe` reconnects, so a
7457
+ * controller torn down by React StrictMode's double effect keeps working when it is reused.
7458
+ */
5354
7459
  destroy() {
5355
7460
  this.cleanups.splice(0).forEach((fn) => fn());
5356
7461
  this.listeners.clear();
@@ -5362,6 +7467,7 @@ var VectorEditorController = class {
5362
7467
  getState = () => this.state;
5363
7468
  /** Calls `listener` after every change. Returns an unsubscribe function. */
5364
7469
  subscribe = (listener) => {
7470
+ this.connect();
5365
7471
  this.listeners.add(listener);
5366
7472
  return () => {
5367
7473
  this.listeners.delete(listener);
@@ -5399,6 +7505,7 @@ var VectorEditorController = class {
5399
7505
  builder: this.builderPreview,
5400
7506
  blendStart: this.blendStart,
5401
7507
  blendDefaults: this.blendDefaults,
7508
+ gradientEdit: this.gradientEdit,
5402
7509
  canUndo: h.canUndo,
5403
7510
  canRedo: h.canRedo,
5404
7511
  undoLabel: h.undoName,
@@ -5472,6 +7579,13 @@ var VectorEditorController = class {
5472
7579
  return this.builderPreview ? "cell" : "crosshair";
5473
7580
  case "blend":
5474
7581
  return h?.nodeId ? this.blendStart && this.blendStart.nodeId !== h.nodeId ? "copy" : "pointer" : "crosshair";
7582
+ case "gradient": {
7583
+ const part = this.drag?.kind === "gradient" ? this.drag.part : h?.gradient;
7584
+ if (!part) return "crosshair";
7585
+ if (part.kind === "bar") return this.drag ? "grabbing" : "copy";
7586
+ if (part.kind === "handle" && part.handle === "start") return this.drag ? "grabbing" : "move";
7587
+ return "pointer";
7588
+ }
5475
7589
  default:
5476
7590
  return "crosshair";
5477
7591
  }
@@ -5505,8 +7619,11 @@ var VectorEditorController = class {
5505
7619
  }
5506
7620
  this.notify(docChanged);
5507
7621
  }
5508
- /** Records the change from `before` to the current state as one undo step. */
5509
- commit(name, before) {
7622
+ /**
7623
+ * Records the change from `before` to the current state as one undo step. Consecutive
7624
+ * changes with the same `merge` key (a slider drag) fold into one step.
7625
+ */
7626
+ commit(name, before, merge) {
5510
7627
  this.doc = this.refreshed(this.doc);
5511
7628
  const after = this.snapshot();
5512
7629
  if (before.doc === after.doc) {
@@ -5517,17 +7634,23 @@ var VectorEditorController = class {
5517
7634
  name,
5518
7635
  execute: () => this.restore(after),
5519
7636
  undo: () => this.restore(before),
5520
- redo: () => this.restore(after)
7637
+ redo: () => this.restore(after),
7638
+ ...merge ? { tag: merge } : {}
5521
7639
  };
5522
7640
  this.committing = true;
5523
- this.history.execute(command);
7641
+ if (merge) this.history.executeMerged(command, { key: merge });
7642
+ else this.history.execute(command);
5524
7643
  this.committing = false;
5525
7644
  }
5526
- /** Runs `fn` (which updates fields) and records it as one undo step. */
5527
- change(name, fn) {
7645
+ /** Runs `fn` (which updates fields) and records it as one undo step (merged by `merge` key). */
7646
+ change(name, fn, merge) {
5528
7647
  const before = this.snapshot();
5529
7648
  fn();
5530
- this.commit(name, before);
7649
+ this.commit(name, before, merge);
7650
+ }
7651
+ /** Ends a merged edit (a slider released): the next change starts a new undo step. */
7652
+ endMergedEdit() {
7653
+ this.history.endGesture();
5531
7654
  }
5532
7655
  setDoc(nodes) {
5533
7656
  if (nodes !== this.doc.nodes) this.doc = { ...this.doc, nodes };
@@ -5888,6 +8011,233 @@ var VectorEditorController = class {
5888
8011
  this.widthEdit = null;
5889
8012
  return this.setStyle({ strokeWidthProfile: points.length ? points : null }, options.ids ?? this.selection, { label: points.length ? "Width Profile" : "Uniform Width" });
5890
8013
  }
8014
+ // -------------------------------------------------------------------------
8015
+ // Gradients
8016
+ // -------------------------------------------------------------------------
8017
+ /** Which paint (fill or stroke) the gradient tool and editor work on. */
8018
+ setGradientTarget(target) {
8019
+ if (target === this.gradientEdit.target) return;
8020
+ this.gradientEdit = { target, stop: null };
8021
+ this.notify();
8022
+ }
8023
+ /** Selects a stop of the gradient being edited (index into its sorted stops), or none. */
8024
+ selectGradientStop(index) {
8025
+ if (index === this.gradientEdit.stop) return;
8026
+ this.gradientEdit = { ...this.gradientEdit, stop: index };
8027
+ this.notify();
8028
+ }
8029
+ /** The gradient tool's annotator for the selection (screen pixels), or null when its paint is not a gradient. */
8030
+ getGradientAnnotator() {
8031
+ return getVectorGradientAnnotator(this.doc, this.selection, this.viewport, this.gradientEdit.target);
8032
+ }
8033
+ /** Unlocked paths of `ids` (default: the selection). */
8034
+ gradientPaths(ids = this.selection) {
8035
+ const out = /* @__PURE__ */ new Map();
8036
+ for (const id of ids) {
8037
+ const node = this.getNode(id);
8038
+ if (node && !node.locked) {
8039
+ for (const p of collectPathNodes(node)) if (!p.locked) out.set(p.id, p);
8040
+ }
8041
+ }
8042
+ return [...out.values()];
8043
+ }
8044
+ /**
8045
+ * Edits the gradient of the `target` paint (default: the editor's) of the selected (or given)
8046
+ * paths with `patch`: a colour or no paint becomes a gradient (`patch.type`, default linear,
8047
+ * from the colour to white). Consecutive edits with the same `merge` key are one undo step.
8048
+ */
8049
+ setGradient(patch, options = {}) {
8050
+ const target = options.target ?? this.gradientEdit.target;
8051
+ const paths = this.gradientPaths(options.ids ?? this.selection);
8052
+ if (!paths.length) return { ok: false, message: "Nothing is selected." };
8053
+ const next = /* @__PURE__ */ new Map();
8054
+ for (const path of paths) {
8055
+ const paint = path[target];
8056
+ const base = isVectorGradient(paint) ? paint : defaultVectorGradient(patch.type ?? "linear", paint);
8057
+ const g = applyVectorGradientPatch(base, patch);
8058
+ if (!g) return { ok: false, message: "That is not a valid gradient: it needs at least one stop with an offset and a colour." };
8059
+ next.set(path.id, { ...path, [target]: g });
8060
+ }
8061
+ this.change(options.label ?? "Gradient", () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), options.merge);
8062
+ const stops = next.values().next().value?.[target]?.stops.length ?? 0;
8063
+ if (this.gradientEdit.stop !== null && this.gradientEdit.stop >= stops) this.gradientEdit = { ...this.gradientEdit, stop: null };
8064
+ return { ok: true };
8065
+ }
8066
+ /** Applies `fn` to every target gradient of the selection (paths whose paint is not a gradient are skipped). */
8067
+ editGradients(label, fn, merge) {
8068
+ const target = this.gradientEdit.target;
8069
+ const paths = this.gradientPaths().filter((p) => isVectorGradient(p[target]));
8070
+ if (!paths.length) return { ok: false, message: "The selection has no gradient." };
8071
+ const next = /* @__PURE__ */ new Map();
8072
+ for (const path of paths) {
8073
+ const g = path[target];
8074
+ const stops = fn(sortedVectorGradientStops(g));
8075
+ if (!stops) return { ok: false, message: "A gradient keeps at least two stops." };
8076
+ next.set(path.id, { ...path, [target]: { ...g, stops } });
8077
+ }
8078
+ this.change(label, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)), merge);
8079
+ return { ok: true };
8080
+ }
8081
+ /** Changes stop `index` (colour, opacity, offset or midpoint) of the selection's gradients; selects it. */
8082
+ setGradientStop(index, patch, options = {}) {
8083
+ let moved = index;
8084
+ const result = this.editGradients(
8085
+ "Gradient Stop",
8086
+ (stops) => {
8087
+ if (!stops[index]) return stops;
8088
+ let next = stops.map((s, i) => {
8089
+ if (i !== index) return s;
8090
+ const out = { ...s };
8091
+ if (patch.color !== void 0) out.color = patch.color;
8092
+ if (patch.opacity !== void 0 && Number.isFinite(patch.opacity)) {
8093
+ const o = Math.min(1, Math.max(0, patch.opacity));
8094
+ if (o < 1) out.opacity = o;
8095
+ else delete out.opacity;
8096
+ }
8097
+ if (patch.midpoint !== void 0 && Number.isFinite(patch.midpoint)) {
8098
+ const m = Math.min(0.95, Math.max(0.05, patch.midpoint));
8099
+ if (Math.abs(m - 0.5) > 1e-3) out.midpoint = m;
8100
+ else delete out.midpoint;
8101
+ }
8102
+ return out;
8103
+ });
8104
+ if (patch.offset !== void 0 && Number.isFinite(patch.offset)) ({ stops: next, index: moved } = moveVectorGradientStop(next, index, patch.offset));
8105
+ return next;
8106
+ },
8107
+ options.merge
8108
+ );
8109
+ if (result.ok) this.selectGradientStop(moved);
8110
+ return result;
8111
+ }
8112
+ /** Adds a stop at `offset` (painting what the gradient paints there) to the selection's gradients and selects it. */
8113
+ addGradientStop(offset) {
8114
+ let index = 0;
8115
+ const target = this.gradientEdit.target;
8116
+ const result = this.editGradients("Add Gradient Stop", (stops) => {
8117
+ const path = this.gradientPaths().find((p) => isVectorGradient(p[target]));
8118
+ const r = insertVectorGradientStop({ ...path[target], stops }, offset);
8119
+ index = r.index;
8120
+ return r.stops;
8121
+ });
8122
+ if (result.ok) this.selectGradientStop(index);
8123
+ return result;
8124
+ }
8125
+ /** Removes stop `index` of the selection's gradients (never below two stops). */
8126
+ removeGradientStop(index) {
8127
+ const result = this.editGradients("Delete Gradient Stop", (stops) => removeVectorGradientStop(stops, index));
8128
+ if (result.ok) this.selectGradientStop(null);
8129
+ return result;
8130
+ }
8131
+ /** Reverses the selection's gradients (stops and midpoints). */
8132
+ reverseGradient() {
8133
+ return this.setGradient({ reverse: true }, { label: "Reverse Gradient" });
8134
+ }
8135
+ /** `doc` with path `id`'s `target` paint replaced. */
8136
+ withGradient(doc, id, target, g) {
8137
+ return { ...doc, nodes: mapTree(doc.nodes, (n) => n.id === id ? { ...n, [target]: g } : n) };
8138
+ }
8139
+ downGradient(screen, p, input) {
8140
+ const before = this.snapshot();
8141
+ const a = this.getGradientAnnotator();
8142
+ let part = a ? hitVectorGradientAnnotator(a, screen) : null;
8143
+ if (a && part) {
8144
+ if (part.kind === "handle" && part.handle === "start" && input.alt && a.gradient.type === "radial") part = { kind: "handle", handle: "focal" };
8145
+ if (part.kind === "stop") this.gradientEdit = { ...this.gradientEdit, stop: part.index };
8146
+ this.drag = { kind: "gradient", part, nodeId: a.nodeId, targets: [a.nodeId], annotator: a, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: part.kind === "stop" ? part.index : null };
8147
+ this.interaction = { type: "gradient", part: part.kind, ...part.kind === "handle" ? { handle: part.handle } : {} };
8148
+ this.notify();
8149
+ return;
8150
+ }
8151
+ const hit = this.hitPath(p);
8152
+ if (hit && !this.gradientPaths().some((n) => n.id === hit.id)) {
8153
+ this.selection = [hit.id];
8154
+ this.gradientEdit = { ...this.gradientEdit, stop: null };
8155
+ }
8156
+ const targets = this.gradientPaths().map((n) => n.id);
8157
+ if (!targets.length) {
8158
+ if (this.selection.length) this.clearSelection();
8159
+ return;
8160
+ }
8161
+ this.drag = { kind: "gradient", part: null, nodeId: null, targets, annotator: null, start: screen, startDoc: p, before, base: this.doc, moved: false, stop: null };
8162
+ this.interaction = { type: "gradient", part: "create" };
8163
+ this.notify();
8164
+ }
8165
+ moveGradient(drag, screen, p, input) {
8166
+ if (!drag.moved && Math.hypot(screen.x - drag.start.x, screen.y - drag.start.y) < 3) return;
8167
+ drag.moved = true;
8168
+ const target = this.gradientEdit.target;
8169
+ const base = drag.base;
8170
+ const constrained = (from, to) => {
8171
+ if (!input.shift) return to;
8172
+ const ang = Math.round(Math.atan2(to.y - from.y, to.x - from.x) / (Math.PI / 4)) * (Math.PI / 4);
8173
+ const len2 = Math.hypot(to.x - from.x, to.y - from.y);
8174
+ return { x: from.x + Math.cos(ang) * len2, y: from.y + Math.sin(ang) * len2 };
8175
+ };
8176
+ if (!drag.part) {
8177
+ const end = constrained(drag.startDoc, p);
8178
+ let doc = base;
8179
+ for (const id2 of drag.targets) {
8180
+ const node2 = findVectorNode(base.nodes, id2);
8181
+ if (!node2) continue;
8182
+ const paint = node2[target];
8183
+ const g2 = isVectorGradient(paint) ? paint : defaultVectorGradient("linear", paint);
8184
+ doc = this.withGradient(doc, id2, target, setVectorGradientVector(g2, vectorPathLocalBounds(node2), this.toLocal(id2, drag.startDoc, base), this.toLocal(id2, end, base)));
8185
+ }
8186
+ this.doc = doc;
8187
+ this.notify(true);
8188
+ return;
8189
+ }
8190
+ const id = drag.nodeId;
8191
+ const node = findVectorNode(base.nodes, id);
8192
+ const g = node[target];
8193
+ const bounds = vectorPathLocalBounds(node);
8194
+ const local = (q) => this.toLocal(id, q, base);
8195
+ const part = drag.part;
8196
+ let next = g;
8197
+ if (part.kind === "handle") {
8198
+ let to = p;
8199
+ if (part.handle === "end" || part.handle === "radius") to = constrained(this.screenToDocument(drag.annotator.start), p);
8200
+ next = moveVectorGradientHandle(g, bounds, part.handle, local(to), local(drag.startDoc));
8201
+ } else if (part.kind === "bar") {
8202
+ next = moveVectorGradientHandle(g, bounds, "start", local(p), local(drag.startDoc));
8203
+ } else {
8204
+ const a = drag.annotator;
8205
+ const pos = vectorGradientBarPosition(a, screen);
8206
+ const stops = sortedVectorGradientStops(g);
8207
+ if (part.kind === "stop") {
8208
+ const off = Math.abs(pos.across - VECTOR_GRADIENT_STOP_GAP) > VECTOR_GRADIENT_OFF_BAR;
8209
+ const removed = off ? removeVectorGradientStop(stops, part.index) : null;
8210
+ if (removed) {
8211
+ next = { ...g, stops: removed };
8212
+ drag.stop = null;
8213
+ } else {
8214
+ const r = moveVectorGradientStop(stops, part.index, Math.min(1, Math.max(0, pos.offset)));
8215
+ next = { ...g, stops: r.stops };
8216
+ drag.stop = r.index;
8217
+ }
8218
+ this.interaction = { type: "gradient", part: "stop", ...removed ? { removing: true } : {} };
8219
+ this.gradientEdit = { ...this.gradientEdit, stop: drag.stop };
8220
+ } else {
8221
+ const m = vectorGradientMidpointFromPosition(stops, part.index, pos.offset);
8222
+ if (m !== null) next = { ...g, stops: stops.map((s, i) => i !== part.index ? s : Math.abs(m - 0.5) < 1e-3 ? (({ midpoint: _m, ...rest }) => rest)(s) : { ...s, midpoint: m }) };
8223
+ }
8224
+ }
8225
+ this.doc = this.withGradient(base, id, target, next);
8226
+ this.notify(true);
8227
+ }
8228
+ upGradient(drag) {
8229
+ const part = drag.part;
8230
+ if (!drag.moved) {
8231
+ if (part?.kind === "bar") {
8232
+ this.addGradientStop(part.offset);
8233
+ return;
8234
+ }
8235
+ this.notify();
8236
+ return;
8237
+ }
8238
+ const label = !part ? "Gradient" : part.kind === "stop" ? drag.stop === null ? "Delete Gradient Stop" : "Move Gradient Stop" : part.kind === "midpoint" ? "Gradient Midpoint" : part.kind === "bar" || part.kind === "handle" && part.handle === "start" ? "Move Gradient" : "Gradient Handle";
8239
+ return this.commit(label, drag.before);
8240
+ }
5891
8241
  /** Removes one width point of a path's profile. */
5892
8242
  removeWidthPoint(ref) {
5893
8243
  return this.removeWidthPoints([ref]);
@@ -5938,11 +8288,11 @@ var VectorEditorController = class {
5938
8288
  if (node?.type !== "path") return { ok: false, message: "There is no such path." };
5939
8289
  const g = vectorStrokeWidthGeometry(node);
5940
8290
  if (!g || !(g.unit > 0)) return { ok: false, message: "The path has no visible stroke." };
5941
- const num = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
5942
- const t = num(values.t);
5943
- const left = num(values.left);
5944
- const right = num(values.right);
5945
- const scale = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
8291
+ const num2 = (v2) => typeof v2 === "number" && Number.isFinite(v2) ? v2 : void 0;
8292
+ const t = num2(values.t);
8293
+ const left2 = num2(values.left);
8294
+ const right = num2(values.right);
8295
+ const scale3 = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
5946
8296
  const profile = sanitizeWidthProfile(node.strokeWidthProfile);
5947
8297
  let points;
5948
8298
  let index;
@@ -5950,11 +8300,11 @@ var VectorEditorController = class {
5950
8300
  if (existing === null) {
5951
8301
  if (t === void 0) return { ok: false, message: "A new width point needs a position t." };
5952
8302
  if (profile.length >= VECTOR_WIDTH_PROFILE_MAX_POINTS) return { ok: false, message: "The profile has too many width points." };
5953
- ({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale(left), right: scale(right) }, { step: true }));
8303
+ ({ points, index } = insertWidthPoint(profile.length ? profile : null, t, { left: scale3(left2), right: scale3(right) }, { step: true }));
5954
8304
  } else {
5955
8305
  const p = profile[existing];
5956
8306
  if (!p) return { ok: false, message: "There is no such width point." };
5957
- const sides2 = { left: scale(left) ?? p.left, right: scale(right) ?? p.right };
8307
+ const sides2 = { left: scale3(left2) ?? p.left, right: scale3(right) ?? p.right };
5958
8308
  const tt = t === void 0 ? p.t : Math.min(Math.max(t, 0), 1);
5959
8309
  if (Math.abs(tt - p.t) <= VECTOR_WIDTH_SAME_T) {
5960
8310
  points = setWidthPoint(profile, existing, sides2);
@@ -6077,7 +8427,7 @@ var VectorEditorController = class {
6077
8427
  const created = [];
6078
8428
  const replace = /* @__PURE__ */ new Map();
6079
8429
  for (const p of paths) {
6080
- const outline = bezierToSubpaths(vectorStrokeOutlinePath(subpathsToBezier(p.subpaths), p));
8430
+ const outline = bezierToSubpaths(vectorStrokeOutlinePath(vectorAppearanceOutline(p), p));
6081
8431
  if (!outline.length) continue;
6082
8432
  const {
6083
8433
  stroke,
@@ -6092,11 +8442,12 @@ var VectorEditorController = class {
6092
8442
  startArrowScale: _ss,
6093
8443
  endArrowScale: _es,
6094
8444
  strokeWidthProfile: _wp,
8445
+ effects,
6095
8446
  ...rest
6096
8447
  } = cloneVectorNode(p);
6097
8448
  const id = this.newId();
6098
8449
  const strokeNode = { ...rest, id, name: `${p.name ?? "Path"} Stroke`, subpaths: outline, fill: stroke ?? null, stroke: null, fillRule: "nonzero" };
6099
- const list = hasPaint(p.fill) ? [{ ...rest, stroke: null }, strokeNode] : [strokeNode];
8450
+ const list = hasPaint(p.fill) ? [{ ...rest, ...effects ? { effects } : {}, stroke: null }, strokeNode] : [strokeNode];
6100
8451
  created.push(id);
6101
8452
  replace.set(p.id, list);
6102
8453
  }
@@ -6111,6 +8462,121 @@ var VectorEditorController = class {
6111
8462
  });
6112
8463
  return created;
6113
8464
  }
8465
+ // -------------------------------------------------------------------------
8466
+ // Path effects
8467
+ // -------------------------------------------------------------------------
8468
+ /** Selected (or given) nodes that can carry effects: the roots of the selection. */
8469
+ effectTargets(ids) {
8470
+ return (ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n) => !!n && !n.blendStep);
8471
+ }
8472
+ /**
8473
+ * Adds an effect of `type` to the selected (or given) nodes, at the bottom of each stack (or
8474
+ * at `index`), with default parameters merged with `params`. A repeater's copies start one
8475
+ * shape width apart. One undo step; returns the new effect ids (one per node, in order).
8476
+ */
8477
+ addEffect(type, options = {}) {
8478
+ const targets = this.effectTargets(options.ids);
8479
+ if (!targets.length || !createVectorEffect(type, "")) return [];
8480
+ const created = [];
8481
+ const next = /* @__PURE__ */ new Map();
8482
+ for (const node of targets) {
8483
+ const list = node.effects ?? [];
8484
+ let k = list.length + 1;
8485
+ while (list.some((e) => e.id === `fx${k}`)) k++;
8486
+ const params = { ...options.params ?? {} };
8487
+ if (type === "repeater" && params.position === void 0) {
8488
+ const b = bezierPathBounds(vectorAppearanceOutline(node));
8489
+ if (b && b.maxX - b.minX > 0) params.position = { x: Math.round((b.maxX - b.minX) * 1.25 * 100) / 100, y: 0 };
8490
+ }
8491
+ const effect = createVectorEffect(type, `fx${k}`, params);
8492
+ const at2 = options.index === void 0 ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));
8493
+ next.set(node.id, { ...node, effects: [...list.slice(0, at2), effect, ...list.slice(at2)] });
8494
+ created.push(effect.id);
8495
+ }
8496
+ this.change(`Add ${VECTOR_EFFECT_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));
8497
+ return created;
8498
+ }
8499
+ editEffects(nodeId, label, fn, merge) {
8500
+ const node = this.getNode(nodeId);
8501
+ if (!node) return { ok: false, message: `There is no node "${nodeId}".` };
8502
+ const result = fn(node.effects ?? []);
8503
+ if (typeof result === "string") return { ok: false, message: result };
8504
+ this.change(label, () => this.replaceNode(nodeId, result.length ? { ...node, effects: result } : (({ effects: _e, ...rest }) => rest)(node)), merge);
8505
+ return { ok: true };
8506
+ }
8507
+ /**
8508
+ * Turns an effect on or off and changes its parameters (unknown or invalid values are
8509
+ * ignored). With `merge` (true, or a key of your own), consecutive updates of the same effect
8510
+ * fold into one undo step, as a slider drag should; call `endMergedEdit` when it ends.
8511
+ */
8512
+ updateEffect(nodeId, effectId, patch, options = {}) {
8513
+ const merge = options.merge === true ? `effect:${nodeId}:${effectId}` : options.merge || void 0;
8514
+ const node = this.getNode(nodeId);
8515
+ const current = node?.effects?.find((e) => e.id === effectId);
8516
+ const label = current ? VECTOR_EFFECT_LABELS[current.type] ?? "Effect" : "Effect";
8517
+ return this.editEffects(
8518
+ nodeId,
8519
+ `Edit ${label}`,
8520
+ (list) => {
8521
+ const i = list.findIndex((e2) => e2.id === effectId);
8522
+ if (i < 0) return `Node "${nodeId}" has no effect "${effectId}".`;
8523
+ const e = list[i];
8524
+ const params = patch.params ? normalizeVectorEffectParams(e.type, patch.params, e.params) : e.params;
8525
+ const next = { ...e, params, ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {} };
8526
+ if (next.enabled !== false) delete next.enabled;
8527
+ return list.map((x, j) => j === i ? next : x);
8528
+ },
8529
+ merge
8530
+ );
8531
+ }
8532
+ /** Removes an effect from a node's stack. */
8533
+ removeEffect(nodeId, effectId) {
8534
+ return this.editEffects(nodeId, "Remove Effect", (list) => list.some((e) => e.id === effectId) ? list.filter((e) => e.id !== effectId) : `Node "${nodeId}" has no effect "${effectId}".`);
8535
+ }
8536
+ /** Moves an effect to position `index` of its stack (0 is applied first). */
8537
+ moveEffect(nodeId, effectId, index) {
8538
+ return this.editEffects(nodeId, "Reorder Effects", (list) => {
8539
+ const i = list.findIndex((e2) => e2.id === effectId);
8540
+ if (i < 0) return `Node "${nodeId}" has no effect "${effectId}".`;
8541
+ const to = Math.min(list.length - 1, Math.max(0, Math.round(index)));
8542
+ const out = list.slice();
8543
+ const [e] = out.splice(i, 1);
8544
+ out.splice(to, 0, e);
8545
+ return out;
8546
+ });
8547
+ }
8548
+ /**
8549
+ * Expand Appearance: bakes the path effects and live booleans of the selected (or given)
8550
+ * nodes, and of everything inside them, into plain paths. A node keeps its id (a group when
8551
+ * it draws as several shapes); the shapes it made get new ids. Returns the ids of the expanded
8552
+ * nodes (empty when none was live).
8553
+ */
8554
+ expandAppearance(options = {}) {
8555
+ const roots = (options.ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n) => !!n && hasVectorAppearance(n));
8556
+ if (!roots.length) return [];
8557
+ const fresh = (n) => {
8558
+ const id = n.id.includes("~fx") ? this.newId() : n.id;
8559
+ return n.type === "group" ? { ...n, id, children: n.children.map(fresh) } : { ...n, id };
8560
+ };
8561
+ const bake = (n) => {
8562
+ if (!hasVectorAppearance(n)) return n;
8563
+ if (!hasOwnVectorAppearance(n)) return { ...n, children: n.children.map(bake) };
8564
+ let e = expandVectorAppearance(n);
8565
+ if (e.type === "group" && e.children.length === 1 && e.children[0].type === "path") {
8566
+ const { id: _id, opacity, name, ...child } = e.children[0];
8567
+ const k = (e.opacity ?? 1) * (opacity ?? 1);
8568
+ e = { ...child, id: e.id, name: e.name ?? name, ...placementOf(e), ...k < 1 ? { opacity: k } : {} };
8569
+ }
8570
+ return fresh(e);
8571
+ };
8572
+ const next = new Map(roots.map((n) => [n.id, bake(n)]));
8573
+ this.change("Expand Appearance", () => {
8574
+ this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n));
8575
+ this.anchors = [];
8576
+ this.editingId = null;
8577
+ });
8578
+ return roots.map((n) => n.id);
8579
+ }
6114
8580
  rename(id, name) {
6115
8581
  return this.updateNode(id, { name }, { label: "Rename" });
6116
8582
  }
@@ -6402,7 +8868,7 @@ var VectorEditorController = class {
6402
8868
  const opacity = n.opacity;
6403
8869
  const m = nodeMatrix(n);
6404
8870
  out.push(
6405
- ...n.children.map((c) => c.blendStep ? (({ blendStep: _s, locked: _l, ...rest }) => rest)(c) : c).map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
8871
+ ...n.children.map((c) => c.blendStep ? (({ blendStep: _s, locked: _l, ...rest }) => rest)(c) : c).map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c).map((c) => n.transform ? transformVectorNode(c, m) : c).map((c) => opacity !== void 0 && opacity < 1 ? { ...c, opacity: (c.opacity ?? 1) * opacity } : c)
6406
8872
  );
6407
8873
  } else if (n.type === "group") {
6408
8874
  const children = flatten2(n.children);
@@ -6439,34 +8905,33 @@ var VectorEditorController = class {
6439
8905
  /**
6440
8906
  * Combines the selected paths (two or more) into one with a boolean operation. `subtract`
6441
8907
  * removes the upper shapes from the lowest one. The result takes the lowest shape's style
6442
- * and place.
8908
+ * and place. Shapes take part as drawn (path effects and live booleans applied).
8909
+ *
8910
+ * With `live`, the operands stay editable instead: a lone live boolean group gets the
8911
+ * operation (its operands follow it again), operands of live boolean groups get it as their
8912
+ * own, and anything else (two or more nodes with one parent) is wrapped in a new live boolean
8913
+ * group. Returns the result's (or the live group's) id, or null.
6443
8914
  */
6444
8915
  booleanOp(op, options = {}) {
6445
8916
  const roots = options.ids ?? this.selectedRoots();
6446
- const order = [];
6447
- walkVectorNodes(this.doc.nodes, (n) => {
6448
- if (roots.includes(n.id)) for (const p of collectPathNodes(n)) order.push(p.id);
6449
- });
6450
- const paths = [...new Set(order)].map((id2) => this.getNode(id2));
6451
- if (paths.length < 2) return null;
8917
+ if (options.live) return this.liveBooleanOp(op, roots);
8918
+ const entries = vectorPathEntries(this.doc, roots, { locked: true });
8919
+ if (entries.length < 2) return null;
6452
8920
  const result = pathBoolean(
6453
8921
  BOOLEAN_OPS[op],
6454
- paths.map((p) => ({
6455
- path: vectorPathWorldBezier(
6456
- { ...p, subpaths: p.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) },
6457
- vectorParentMatrix(this.doc.nodes, p.id) ?? IDENTITY_MATRIX
6458
- ),
6459
- fillRule: p.fillRule ?? "nonzero"
8922
+ entries.map((e) => ({
8923
+ path: vectorPathWorldBezier({ ...e.node, subpaths: e.node.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) }, e.parent),
8924
+ fillRule: e.node.fillRule ?? "nonzero"
6460
8925
  }))
6461
8926
  );
6462
- const base = paths[0];
6463
- const anchorRoot = roots.find((r) => findVectorNode([this.getNode(r)], base.id)) ?? base.id;
8927
+ const base = entries[0].node;
8928
+ const anchorRoot = roots.find((r) => this.holds(r, base.id)) ?? base.id;
6464
8929
  const subpaths = bezierToSubpaths(this.intoParentSpace(anchorRoot, result.path));
6465
8930
  if (result.error || !subpaths.length) return null;
6466
8931
  const id = this.newId();
6467
- const { transform: _t, ...baseRest } = cloneVectorNode(base);
8932
+ const { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(base);
6468
8933
  const node = { ...baseRest, id, name: BOOLEAN_LABELS[op], subpaths };
6469
- const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
8934
+ const drop = /* @__PURE__ */ new Set([...entries.map((e) => e.node.id), ...roots]);
6470
8935
  this.change(BOOLEAN_LABELS[op], () => {
6471
8936
  this.setDoc(mapTree(this.doc.nodes, (n) => n.id === anchorRoot ? node : drop.has(n.id) ? null : n));
6472
8937
  this.selection = [id];
@@ -6475,18 +8940,75 @@ var VectorEditorController = class {
6475
8940
  });
6476
8941
  return id;
6477
8942
  }
8943
+ /** Whether stored node `rootId` holds `id` as drawn (`id` may be an expanded path inside it). */
8944
+ holds(rootId, id) {
8945
+ const n = this.getNode(rootId);
8946
+ return !!n && (!!findVectorNode([n], id) || !!findVectorNode([expandVectorAppearance(n)], id));
8947
+ }
8948
+ liveBooleanOp(op, roots) {
8949
+ const nodes = roots.map((id2) => this.getNode(id2));
8950
+ if (!nodes.length || nodes.some((n) => !n)) return null;
8951
+ const label = `Live ${BOOLEAN_LABELS[op]}`;
8952
+ const lone = nodes.length === 1 ? nodes[0] : null;
8953
+ if (isVectorLiveBoolean(lone)) {
8954
+ const children = lone.children.map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c);
8955
+ this.change(label, () => this.replaceNode(lone.id, { ...lone, liveBoolean: op, children }));
8956
+ return lone.id;
8957
+ }
8958
+ const parents = roots.map((id2) => locateVectorNode(this.doc.nodes, id2)?.parent ?? null);
8959
+ if (parents.every((g) => isVectorLiveBoolean(g))) {
8960
+ const set = new Set(roots);
8961
+ this.change(
8962
+ label,
8963
+ () => this.setDoc(
8964
+ mapTree(this.doc.nodes, (n) => {
8965
+ if (n.type !== "group" || !n.liveBoolean || !n.children.some((c) => set.has(c.id))) return n;
8966
+ const group = n;
8967
+ const children = group.children.map((c) => {
8968
+ if (!set.has(c.id)) return c;
8969
+ const { booleanOperation: _b, ...rest } = c;
8970
+ return op === group.liveBoolean ? rest : { ...rest, booleanOperation: op };
8971
+ });
8972
+ return { ...group, children };
8973
+ })
8974
+ )
8975
+ );
8976
+ return parents[0].id;
8977
+ }
8978
+ if (nodes.length < 2) return null;
8979
+ const parent = parents[0]?.id ?? null;
8980
+ if (parents.some((g) => (g?.id ?? null) !== parent)) return null;
8981
+ const id = this.newId();
8982
+ this.change(label, () => {
8983
+ const set = new Set(roots);
8984
+ const wrap = (list) => {
8985
+ const members = list.filter((n) => set.has(n.id)).map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c);
8986
+ const top = Math.max(...list.map((n, i) => set.has(n.id) ? i : -1));
8987
+ const out = [];
8988
+ list.forEach((n, i) => {
8989
+ if (i === top) out.push({ id, type: "group", name: BOOLEAN_LABELS[op], liveBoolean: op, children: members });
8990
+ if (!set.has(n.id)) out.push(n);
8991
+ });
8992
+ return out;
8993
+ };
8994
+ if (parent === null) this.setDoc(wrap(this.doc.nodes));
8995
+ else this.setDoc(mapTree(this.doc.nodes, (n) => n.id === parent && n.type === "group" ? { ...n, children: wrap(n.children) } : n));
8996
+ this.selection = [id];
8997
+ this.anchors = [];
8998
+ this.editingId = null;
8999
+ });
9000
+ return id;
9001
+ }
6478
9002
  /** Joins the selected paths into one compound path (subpaths kept, lowest style). */
6479
9003
  combine(options = {}) {
6480
9004
  const roots = options.ids ?? this.selectedRoots();
6481
- const paths = [];
6482
- walkVectorNodes(this.doc.nodes, (n) => {
6483
- if (roots.includes(n.id)) paths.push(...collectPathNodes(n));
6484
- });
6485
- if (paths.length < 2) return null;
9005
+ const entries = vectorPathEntries(this.doc, roots, { locked: true });
9006
+ if (entries.length < 2) return null;
9007
+ const paths = entries.map((e) => e.node);
6486
9008
  const id = this.newId();
6487
- const first = roots.find((r) => findVectorNode([this.getNode(r)], paths[0].id)) ?? paths[0].id;
6488
- const subpaths = paths.flatMap((p) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(p, vectorParentMatrix(this.doc.nodes, p.id) ?? IDENTITY_MATRIX))));
6489
- const { transform: _t, ...baseRest } = cloneVectorNode(paths[0]);
9009
+ const first = roots.find((r) => this.holds(r, paths[0].id)) ?? paths[0].id;
9010
+ const subpaths = entries.flatMap((e) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(e.node, e.parent))));
9011
+ const { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(paths[0]);
6490
9012
  const node = { ...baseRest, id, name: "Compound Path", subpaths };
6491
9013
  const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
6492
9014
  this.change("Combine", () => {
@@ -6541,10 +9063,7 @@ var VectorEditorController = class {
6541
9063
  const result = pathfinder(op, shapes);
6542
9064
  if (result.error || !result.pieces.length) return [];
6543
9065
  const last = entries[entries.length - 1].node.id;
6544
- const topRoot = [...roots].reverse().find((r) => {
6545
- const n = this.getNode(r);
6546
- return !!n && !!findVectorNode([n], last);
6547
- }) ?? last;
9066
+ const topRoot = [...roots].reverse().find((r) => this.holds(r, last)) ?? last;
6548
9067
  const nodes = [];
6549
9068
  for (const piece of result.pieces) {
6550
9069
  const subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => piece.open ? { ...sp, closed: false } : sp);
@@ -6737,7 +9256,7 @@ var VectorEditorController = class {
6737
9256
  const roots = ids ?? this.selectedRoots();
6738
9257
  const key = roots.join("\0");
6739
9258
  if (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;
6740
- const entries = roots.length ? vectorPathEntries(this.doc, roots) : [];
9259
+ const entries = roots.length ? vectorPathEntries(this.doc, roots, { expand: false }) : [];
6741
9260
  const map = entries.length ? vectorShapeBuilderMap(entries) : null;
6742
9261
  this.builderCache = { doc: this.doc, key, map };
6743
9262
  return map;
@@ -7521,16 +10040,22 @@ var VectorEditorController = class {
7521
10040
  return best;
7522
10041
  }
7523
10042
  /** Deepest unlocked visible path under `p`. */
10043
+ /** Live groups being edited (selected, or holding the selection or the edited path): their children are hit as stored. */
10044
+ enteredGroups() {
10045
+ return vectorEnteredGroups(this.doc.nodes, this.editingId ? [...this.selection, this.editingId] : this.selection);
10046
+ }
7524
10047
  hitPath(p) {
7525
10048
  const tol = this.tolerance();
10049
+ const entered = this.enteredGroups();
7526
10050
  const search = (list, parent) => {
7527
10051
  for (let i = list.length - 1; i >= 0; i--) {
7528
10052
  const n = list[i];
7529
10053
  if (n.visible === false || n.locked) continue;
7530
10054
  if (n.type === "group") {
10055
+ if (hasOwnVectorAppearance(n) && !entered.has(n.id) && !hitTestVectorNode(n, p, tol, parent, { entered })) continue;
7531
10056
  const found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));
7532
10057
  if (found) return found;
7533
- } else if (hitTestVectorNode(n, p, tol, parent)) return n;
10058
+ } else if (hitTestVectorNode(n, p, tol, parent, { entered })) return n;
7534
10059
  }
7535
10060
  return null;
7536
10061
  };
@@ -7555,7 +10080,7 @@ var VectorEditorController = class {
7555
10080
  case "select": {
7556
10081
  const handle = this.hitTransformHandle(screen);
7557
10082
  if (handle) return { nodeId: null, transformHandle: handle };
7558
- const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
10083
+ const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });
7559
10084
  return node ? { nodeId: node.id } : null;
7560
10085
  }
7561
10086
  case "direct": {
@@ -7596,6 +10121,13 @@ var VectorEditorController = class {
7596
10121
  const spot = this.strokeSpotAt(p);
7597
10122
  return spot ? { nodeId: spot.nodeId, strokeSpot: spot } : null;
7598
10123
  }
10124
+ case "gradient": {
10125
+ const a = this.getGradientAnnotator();
10126
+ const part = a ? hitVectorGradientAnnotator(a, screen) : null;
10127
+ if (a && part) return { nodeId: a.nodeId, gradient: part };
10128
+ const path = this.hitPath(p);
10129
+ return path ? { nodeId: path.id } : null;
10130
+ }
7599
10131
  default:
7600
10132
  return null;
7601
10133
  }
@@ -7818,6 +10350,8 @@ var VectorEditorController = class {
7818
10350
  return this.downBuilder(p, input);
7819
10351
  case "blend":
7820
10352
  return this.downBlend(screen, p);
10353
+ case "gradient":
10354
+ return this.downGradient(screen, p, input);
7821
10355
  case "scissors": {
7822
10356
  const seg = this.nearestOutline(p);
7823
10357
  if (seg) this.cutPathAt(seg.nodeId, seg.point);
@@ -7884,7 +10418,7 @@ var VectorEditorController = class {
7884
10418
  this.notify();
7885
10419
  return;
7886
10420
  }
7887
- const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
10421
+ const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });
7888
10422
  if (node) {
7889
10423
  const selected = this.selection.includes(node.id);
7890
10424
  if (input.shift) {
@@ -8109,6 +10643,8 @@ var VectorEditorController = class {
8109
10643
  this.viewport = { ...drag.viewport, x: drag.viewport.x + screen.x - drag.start.x, y: drag.viewport.y + screen.y - drag.start.y };
8110
10644
  this.notify();
8111
10645
  return;
10646
+ case "gradient":
10647
+ return this.moveGradient(drag, screen, p, input);
8112
10648
  case "move": {
8113
10649
  let dx = p.x - drag.start.x;
8114
10650
  let dy = p.y - drag.start.y;
@@ -8484,6 +11020,8 @@ var VectorEditorController = class {
8484
11020
  }
8485
11021
  case "builder":
8486
11022
  return this.upBuilder(drag);
11023
+ case "gradient":
11024
+ return this.upGradient(drag);
8487
11025
  case "stroke": {
8488
11026
  this.notify();
8489
11027
  if (drag.tool === "knife") this.knife(drag.points);
@@ -8698,6 +11236,7 @@ var VectorEditorController = class {
8698
11236
  case "Delete":
8699
11237
  case "Backspace":
8700
11238
  if (this.tool === "width" && this.widthPoints.length) return this.removeWidthPoints().ok;
11239
+ if (this.tool === "gradient" && this.gradientEdit.stop !== null) return this.removeGradientStop(this.gradientEdit.stop).ok;
8701
11240
  if (this.pen) {
8702
11241
  const node = this.getNode(this.pen.nodeId);
8703
11242
  const sp = node.subpaths[this.pen.subpath];
@@ -8806,24 +11345,24 @@ function pencilPressureProfile(points, pressures) {
8806
11345
  }
8807
11346
 
8808
11347
  // src/vector/render.ts
8809
- var fmt = (n) => Math.round(n * 100) / 100;
8810
- var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
11348
+ var fmt4 = (n) => Math.round(n * 100) / 100;
11349
+ var fmtMatrix2 = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
8811
11350
  var VECTOR_BUILDER_HATCH_GAP = 6;
8812
- function renderNode(node, defs, idPrefix) {
8813
- if (node.visible === false) return null;
11351
+ function renderNode(stored, defs, idPrefix) {
11352
+ if (stored.visible === false) return null;
11353
+ const node = expandVectorAppearance(stored);
8814
11354
  if (node.type === "group") {
8815
11355
  const children = node.children.map((c) => renderNode(c, defs, idPrefix)).filter((c) => !!c);
8816
- return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: node.opacity ?? 1, children };
11356
+ return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix2(node.transform) } : {}, opacity: node.opacity ?? 1, children };
8817
11357
  }
8818
- const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`);
8819
- const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`);
8820
- if (fill.gradient) defs.push(fill.gradient);
8821
- if (stroke.gradient) defs.push(stroke.gradient);
11358
+ const fill = vectorPaintToSvg(node.fill, `${idPrefix}fill-${node.id}`, { node });
11359
+ const stroke = vectorPaintToSvg(node.stroke, `${idPrefix}stroke-${node.id}`, { node, role: "stroke" });
11360
+ defs.push(...fill.defs, ...stroke.defs);
8822
11361
  const strokeParts = stroke.value !== "none" ? vectorStrokeSvgParts(node, `${idPrefix}clip-${node.id}`) : void 0;
8823
11362
  return {
8824
11363
  kind: "path",
8825
11364
  id: node.id,
8826
- ...node.transform ? { transform: fmtMatrix(node.transform) } : {},
11365
+ ...node.transform ? { transform: fmtMatrix2(node.transform) } : {},
8827
11366
  ...strokeParts ? { strokeParts } : {},
8828
11367
  d: vectorPathData(node),
8829
11368
  fill: fill.value,
@@ -8842,7 +11381,7 @@ function getVectorSceneRenderData(state, options = {}) {
8842
11381
  const nodes = state.document.nodes.map((n) => renderNode(n, defs, prefix)).filter((n) => !!n);
8843
11382
  const v2 = state.viewport;
8844
11383
  return {
8845
- transform: `translate(${fmt(v2.x)} ${fmt(v2.y)}) scale(${v2.zoom})`,
11384
+ transform: `translate(${fmt4(v2.x)} ${fmt4(v2.y)}) scale(${v2.zoom})`,
8846
11385
  width: state.document.width,
8847
11386
  height: state.document.height,
8848
11387
  ...state.document.background ? { background: state.document.background } : {},
@@ -8851,16 +11390,20 @@ function getVectorSceneRenderData(state, options = {}) {
8851
11390
  };
8852
11391
  }
8853
11392
  var toScreen = (state, p) => ({ x: p.x * state.viewport.zoom + state.viewport.x, y: p.y * state.viewport.zoom + state.viewport.y });
8854
- function screenOutline(state, node) {
11393
+ function screenOutline(state, node, entered) {
8855
11394
  const v2 = state.viewport;
8856
11395
  const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
8857
11396
  const parent = vectorParentMatrix(state.document.nodes, node.id) ?? IDENTITY_MATRIX;
8858
- return collectWithMatrix(node, parent).map(({ path, parent: m }) => bezierPathToSvg(transformBezierPath(vectorPathWorldBezier(path, m), view), { precision: 2 })).join(" ");
11397
+ return outlinePaths(node, parent, entered).map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 })).join(" ");
8859
11398
  }
8860
- function collectWithMatrix(node, parent) {
8861
- if (node.type === "path") return [{ path: node, parent }];
11399
+ function outlinePaths(node, parent, entered) {
11400
+ if (node.type === "path") return [vectorPathWorldBezier(node, parent, { effects: false })];
8862
11401
  const m = multiplyMatrices(nodeMatrix(node), parent);
8863
- return node.children.flatMap((c) => collectWithMatrix(c, m));
11402
+ if (hasOwnVectorAppearance(node) && !entered.has(node.id)) return [transformBezierPath(vectorAppearanceOutline(node), m)];
11403
+ return node.children.filter((c) => c.visible !== false).flatMap((c) => outlinePaths(c, m, entered));
11404
+ }
11405
+ function getVectorEnteredGroups(state) {
11406
+ return vectorEnteredGroups(state.document.nodes, state.editingId ? [...state.selection, state.editingId] : state.selection);
8864
11407
  }
8865
11408
  function collect(node) {
8866
11409
  if (node.type === "path") return [node];
@@ -8870,13 +11413,14 @@ function getVectorOverlayRenderData(state) {
8870
11413
  const doc = state.document;
8871
11414
  const z = state.viewport.zoom;
8872
11415
  const outlines = [];
11416
+ const entered = getVectorEnteredGroups(state);
8873
11417
  const seen = /* @__PURE__ */ new Set();
8874
11418
  const outline = (id, selected2) => {
8875
11419
  if (seen.has(id)) return;
8876
11420
  const node = findVectorNode(doc.nodes, id);
8877
11421
  if (!node || node.visible === false) return;
8878
11422
  seen.add(id);
8879
- outlines.push({ id, d: screenOutline(state, node), selected: selected2 });
11423
+ outlines.push({ id, d: screenOutline(state, node, entered), selected: selected2 });
8880
11424
  };
8881
11425
  for (const id of state.selection) outline(id, true);
8882
11426
  if (state.editingId) outline(state.editingId, true);
@@ -8925,7 +11469,7 @@ function getVectorOverlayRenderData(state) {
8925
11469
  const a = toScreen(state, last);
8926
11470
  const p = toScreen(state, state.pointer);
8927
11471
  const c = last.out ? toScreen(state, last.out) : null;
8928
- penPreview = c ? `M${fmt(a.x)} ${fmt(a.y)}Q${fmt(c.x)} ${fmt(c.y)} ${fmt(p.x)} ${fmt(p.y)}` : `M${fmt(a.x)} ${fmt(a.y)}L${fmt(p.x)} ${fmt(p.y)}`;
11472
+ penPreview = c ? `M${fmt4(a.x)} ${fmt4(a.y)}Q${fmt4(c.x)} ${fmt4(c.y)} ${fmt4(p.x)} ${fmt4(p.y)}` : `M${fmt4(a.x)} ${fmt4(a.y)}L${fmt4(p.x)} ${fmt4(p.y)}`;
8929
11473
  }
8930
11474
  }
8931
11475
  const origin = toScreen(state, { x: 0, y: 0 });
@@ -8933,45 +11477,45 @@ function getVectorOverlayRenderData(state) {
8933
11477
  let status = null;
8934
11478
  const it = state.interaction;
8935
11479
  if (it?.type === "rotate") status = `${Math.round(it.angle)}\xB0`;
8936
- else if (it?.type === "move" || it?.type === "anchors") status = `\u0394 ${fmt(it.dx)}, ${fmt(it.dy)}`;
11480
+ else if (it?.type === "move" || it?.type === "anchors") status = `\u0394 ${fmt4(it.dx)}, ${fmt4(it.dy)}`;
8937
11481
  else if (it?.type === "scale" || it?.type === "draw") {
8938
11482
  const b = it.type === "draw" ? findVectorNode(doc.nodes, it.nodeId) ? vectorNodeBounds(findVectorNode(doc.nodes, it.nodeId)) : null : state.selectionFrame;
8939
- if (b) status = `W ${fmt(b.width)} \xD7 H ${fmt(b.height)}`;
11483
+ if (b) status = `W ${fmt4(b.width)} \xD7 H ${fmt4(b.height)}`;
8940
11484
  }
8941
11485
  const guides = { lines: [], labels: [] };
8942
11486
  if (state.guides) {
8943
11487
  for (const l of state.guides.lines) {
8944
11488
  const a = toScreen(state, { x: l.x1, y: l.y1 });
8945
11489
  const b = toScreen(state, { x: l.x2, y: l.y2 });
8946
- guides.lines.push({ kind: l.kind, x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) });
11490
+ guides.lines.push({ kind: l.kind, x1: fmt4(a.x), y1: fmt4(a.y), x2: fmt4(b.x), y2: fmt4(b.y) });
8947
11491
  }
8948
11492
  for (const l of state.guides.labels) {
8949
11493
  const p = toScreen(state, l);
8950
- guides.labels.push({ kind: l.kind, x: fmt(p.x), y: fmt(p.y), text: l.text });
11494
+ guides.labels.push({ kind: l.kind, x: fmt4(p.x), y: fmt4(p.y), text: l.text });
8951
11495
  }
8952
11496
  }
8953
11497
  let toolStroke = null;
8954
11498
  if (it?.type === "stroke") {
8955
11499
  const pts = it.points.map((q) => toScreen(state, q));
8956
- const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt(q.x)} ${fmt(q.y)}`).join("") + (pts.length === 1 ? `L${fmt(pts[0].x)} ${fmt(pts[0].y)}` : "");
8957
- toolStroke = { tool: it.tool, d, width: fmt(it.width * z) };
11500
+ const d = pts.map((q, i) => `${i ? "L" : "M"}${fmt4(q.x)} ${fmt4(q.y)}`).join("") + (pts.length === 1 ? `L${fmt4(pts[0].x)} ${fmt4(pts[0].y)}` : "");
11501
+ toolStroke = { tool: it.tool, d, width: fmt4(it.width * z) };
8958
11502
  }
8959
11503
  let pencilString = null;
8960
11504
  if (it?.type === "draw" && it.string) {
8961
11505
  const a = toScreen(state, it.string[0]);
8962
11506
  const b = toScreen(state, it.string[1]);
8963
- pencilString = { x1: fmt(a.x), y1: fmt(a.y), x2: fmt(b.x), y2: fmt(b.y) };
11507
+ pencilString = { x1: fmt4(a.x), y1: fmt4(a.y), x2: fmt4(b.x), y2: fmt4(b.y) };
8964
11508
  }
8965
11509
  const dragged = it?.type === "corner" ? it.ref : null;
8966
11510
  const cornerWidgets = getVectorCornerWidgets(state).map((w) => {
8967
11511
  const s = toScreen(state, w);
8968
- return { ref: w.ref, x: fmt(s.x), y: fmt(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(state.hover?.corner, w.ref), active: !!dragged };
11512
+ return { ref: w.ref, x: fmt4(s.x), y: fmt4(s.y), radius: w.radius, kind: w.kind, hovered: sameAnchor(state.hover?.corner, w.ref), active: !!dragged };
8969
11513
  });
8970
- if (it?.type === "corner") status = `R ${fmt(it.radius)}`;
8971
- const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt(x), y: fmt(y) }))(toScreen(state, state.hover.segment.point)) : null;
11514
+ if (it?.type === "corner") status = `R ${fmt4(it.radius)}`;
11515
+ const cutPoint = state.tool === "scissors" && state.hover?.segment && !it ? (({ x, y }) => ({ x: fmt4(x), y: fmt4(y) }))(toScreen(state, state.hover.segment.point)) : null;
8972
11516
  const sc = (p) => {
8973
11517
  const q = toScreen(state, p);
8974
- return { x: fmt(q.x), y: fmt(q.y) };
11518
+ return { x: fmt4(q.x), y: fmt4(q.y) };
8975
11519
  };
8976
11520
  const sameWidth = (a, b) => !!a && a.nodeId === b.nodeId && a.index === b.index;
8977
11521
  const widthPoints = getVectorWidthHandles(state).map((h) => {
@@ -8982,7 +11526,7 @@ function getVectorOverlayRenderData(state) {
8982
11526
  });
8983
11527
  const spot = state.tool === "width" && !it ? state.hover?.strokeSpot : null;
8984
11528
  const widthSpot = spot ? { ...sc(spot.point), left: sc(spot.left), right: sc(spot.right) } : null;
8985
- if (it?.type === "width") status = it.mode === "move" ? "Move width point" : it.mode === "copy" ? "Copy width point" : `W ${fmt(it.width)}`;
11529
+ if (it?.type === "width") status = it.mode === "move" ? "Move width point" : it.mode === "copy" ? "Copy width point" : `W ${fmt4(it.width)}`;
8986
11530
  let builder = null;
8987
11531
  if (state.builder) {
8988
11532
  const v2 = state.viewport;
@@ -9005,7 +11549,43 @@ function getVectorOverlayRenderData(state) {
9005
11549
  }
9006
11550
  const blend = blendOverlay(state);
9007
11551
  if (state.tool === "blend" && !it) status = state.blendStart ? "Click another shape to blend into" : "Click a shape to blend from";
9008
- return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend };
11552
+ let gradient = null;
11553
+ if (state.tool === "gradient") {
11554
+ gradient = gradientOverlay(state);
11555
+ if (!gradient && !it) status = "Drag across a shape to set a gradient";
11556
+ else if (it?.type === "gradient" && it.removing) status = "Release to delete the stop";
11557
+ }
11558
+ return { outlines, selectionBox, anchors, handles, marquee, penPreview, artboard, guides, status, toolStroke, pencilString, cutPoint, cornerWidgets, widthPoints, widthSpot, builder, blend, gradient };
11559
+ }
11560
+ function gradientOverlay(state) {
11561
+ const a = getVectorGradientAnnotator(state.document, state.selection, state.viewport, state.gradientEdit.target);
11562
+ if (!a) return null;
11563
+ const hover = state.interaction ? null : state.hover?.gradient;
11564
+ const handleHovered = (h) => hover?.kind === "handle" && hover.handle === h;
11565
+ const pt2 = (p) => ({ x: fmt4(p.x), y: fmt4(p.y) });
11566
+ const centred = isCentredVectorGradient(a.gradient.type);
11567
+ const removing = state.interaction?.type === "gradient" && !!state.interaction.removing;
11568
+ return {
11569
+ nodeId: a.nodeId,
11570
+ type: a.gradient.type,
11571
+ start: { ...pt2(a.start), hovered: handleHovered("start") },
11572
+ end: { ...pt2(a.end), hovered: handleHovered(centred ? "radius" : "end"), handle: centred ? "radius" : "end" },
11573
+ aspect: a.aspect ? { ...pt2(a.aspect), hovered: handleHovered("aspect") } : null,
11574
+ focal: a.focal ? { ...pt2(a.focal), hovered: handleHovered("focal") } : null,
11575
+ stops: a.stops.map((s) => ({
11576
+ index: s.index,
11577
+ ...pt2(s),
11578
+ barX: fmt4(s.x - a.normal.x * VECTOR_GRADIENT_STOP_GAP),
11579
+ barY: fmt4(s.y - a.normal.y * VECTOR_GRADIENT_STOP_GAP),
11580
+ color: s.color,
11581
+ opacity: s.opacity ?? 1,
11582
+ selected: state.gradientEdit.stop === s.index,
11583
+ hovered: hover?.kind === "stop" && hover.index === s.index
11584
+ })),
11585
+ midpoints: a.midpoints.map((m) => ({ index: m.index, ...pt2(m), hovered: hover?.kind === "midpoint" && hover.index === m.index })),
11586
+ extent: a.extent,
11587
+ removing
11588
+ };
9009
11589
  }
9010
11590
  function selectedBlends(state) {
9011
11591
  const out = /* @__PURE__ */ new Map();
@@ -9022,7 +11602,7 @@ function blendOverlay(state) {
9022
11602
  const doc = state.document;
9023
11603
  const sc = (p, m) => {
9024
11604
  const q = { x: m[0] * p.x + m[2] * p.y + m[4], y: m[1] * p.x + m[3] * p.y + m[5] };
9025
- return { x: fmt(q.x), y: fmt(q.y) };
11605
+ return { x: fmt4(q.x), y: fmt4(q.y) };
9026
11606
  };
9027
11607
  const spines = selectedBlends(state).flatMap((g) => {
9028
11608
  const s = vectorBlendSpine(g);
@@ -9041,7 +11621,7 @@ function blendOverlay(state) {
9041
11621
  const from = state.blendStart ? findVectorNode(doc.nodes, state.blendStart.nodeId) : void 0;
9042
11622
  if (from && state.blendStart) {
9043
11623
  const a = state.blendStart.anchor === null ? void 0 : getAnchorWorld(doc, { nodeId: from.id, subpath: 0, index: state.blendStart.anchor });
9044
- start = { d: screenOutline(state, from), anchor: a ? sc(a, view) : null };
11624
+ start = { d: screenOutline(state, from, /* @__PURE__ */ new Set()), anchor: a ? sc(a, view) : null };
9045
11625
  }
9046
11626
  const hovered = state.hover?.nodeId ? findVectorNode(doc.nodes, state.hover.nodeId) : void 0;
9047
11627
  if (hovered?.type === "path" && hovered.subpaths[0]) {
@@ -9100,7 +11680,7 @@ var VECTOR_OVERLAY_THEME = {
9100
11680
  builderHatchOpacity: 0.6
9101
11681
  };
9102
11682
  function vectorDiamondPath(x, y, r = VECTOR_OVERLAY_THEME.widthPointSize) {
9103
- return `M${fmt(x)} ${fmt(y - r)}L${fmt(x + r)} ${fmt(y)}L${fmt(x)} ${fmt(y + r)}L${fmt(x - r)} ${fmt(y)}Z`;
11683
+ return `M${fmt4(x)} ${fmt4(y - r)}L${fmt4(x + r)} ${fmt4(y)}L${fmt4(x)} ${fmt4(y + r)}L${fmt4(x - r)} ${fmt4(y)}Z`;
9104
11684
  }
9105
11685
  function toVectorModifiers(event) {
9106
11686
  return { shift: !!event.shiftKey, alt: !!event.altKey, mod: !!(event.metaKey || event.ctrlKey) };
@@ -9205,6 +11785,45 @@ function vectorPaintToHex(paint) {
9205
11785
  const hex = m[1].length === 3 ? m[1].replace(/./g, (c) => c + c) : m[1];
9206
11786
  return `#${hex.toLowerCase()}`;
9207
11787
  }
11788
+ var VECTOR_GRADIENT_TYPE_LABELS = { linear: "Linear", radial: "Radial", conic: "Conic", reflected: "Reflected", diamond: "Diamond" };
11789
+ var VECTOR_GRADIENT_SPREAD_LABELS = { pad: "Pad", reflect: "Reflect", repeat: "Repeat" };
11790
+ var VECTOR_GRADIENT_INTERPOLATION_LABELS = { srgb: "sRGB", linear: "Linear light", oklab: "Oklab" };
11791
+ var VECTOR_STROKE_GRADIENT_MODE_LABELS = { within: "Within stroke", along: "Along stroke", across: "Across stroke" };
11792
+ function getVectorGradientEditor(state) {
11793
+ const paths = [];
11794
+ for (const id of state.selection) {
11795
+ const node = findVectorNode(state.document.nodes, id);
11796
+ if (node) paths.push(...collect(node));
11797
+ }
11798
+ if (!paths.length) return null;
11799
+ const target = state.gradientEdit.target;
11800
+ const paint = paths[0][target];
11801
+ const gradient = isVectorGradient(paint) ? paint : null;
11802
+ const sorted = gradient ? sortedVectorGradientStops(gradient) : [];
11803
+ const stopIndex = state.gradientEdit.stop;
11804
+ const stops = sorted.map((s, index) => ({ ...s, index, selected: index === stopIndex }));
11805
+ const sel = stopIndex !== null && sorted[stopIndex] ? { ...sorted[stopIndex], index: stopIndex, last: stopIndex === sorted.length - 1 } : null;
11806
+ const preview = gradient ? `linear-gradient(to right, ${expandVectorGradientStops(gradient).map((s) => {
11807
+ const c = parseVectorColor(s.color);
11808
+ const a = (c?.[3] ?? 1) * (s.opacity ?? 1);
11809
+ const color = c ? `rgba(${Math.round(c[0] * 255)}, ${Math.round(c[1] * 255)}, ${Math.round(c[2] * 255)}, ${Math.round(a * 1e3) / 1e3})` : s.color;
11810
+ return `${color} ${Math.round(s.offset * 1e4) / 100}%`;
11811
+ }).join(", ")})` : null;
11812
+ return {
11813
+ target,
11814
+ gradient,
11815
+ color: vectorPaintToHex(paint),
11816
+ stops,
11817
+ selected: sel,
11818
+ preview,
11819
+ type: gradient?.type ?? null,
11820
+ spread: gradient?.spread ?? "pad",
11821
+ interpolation: gradient?.interpolation ?? "srgb",
11822
+ smoothness: gradient?.smoothness ?? 0,
11823
+ strokeMode: gradient?.strokeMode ?? "within",
11824
+ mixed: paths.some((p) => JSON.stringify(p[target] ?? null) !== JSON.stringify(paint ?? null))
11825
+ };
11826
+ }
9208
11827
  var VECTOR_PATH_ACTIONS = [
9209
11828
  { id: "join", label: "\u22C8", title: "Join", shortcut: "J", enabled: (s, p) => s + p > 0, run: (c) => c.joinPaths() },
9210
11829
  { id: "average", label: "\u22B9", title: "Average Points", shortcut: "Alt+J", enabled: (_s, p) => p > 1, run: (c) => c.averageAnchors("both") },
@@ -9226,8 +11845,28 @@ var VECTOR_EDIT_TOGGLES = [
9226
11845
  { key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
9227
11846
  { key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
9228
11847
  ];
11848
+ var VECTOR_EFFECT_MENU = VECTOR_EFFECT_TYPES.map((type) => ({ type, label: VECTOR_EFFECT_LABELS[type] }));
11849
+ function getVectorEffectsPanel(state) {
11850
+ const nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n) => !!n && !n.blendStep);
11851
+ if (!nodes.length) return null;
11852
+ const one = nodes.length === 1 ? nodes[0] : null;
11853
+ return {
11854
+ nodeId: one?.id ?? null,
11855
+ count: nodes.length,
11856
+ effects: (one?.effects ?? []).map((e) => ({
11857
+ id: e.id,
11858
+ type: e.type,
11859
+ label: VECTOR_EFFECT_LABELS[e.type] ?? e.type,
11860
+ enabled: e.enabled !== false,
11861
+ params: e.params,
11862
+ specs: VECTOR_EFFECT_PARAM_SPECS[e.type] ?? []
11863
+ })),
11864
+ expandable: nodes.some(hasVectorAppearance)
11865
+ };
11866
+ }
9229
11867
 
9230
11868
  // src/vector/commands.ts
11869
+ var EFFECT_PARAMS_DESCRIPTION = "Effect parameters (missing ones keep their value or default). trim: start, end (percent), offset (degrees), mode simultaneous|individual. roundCorners: radius. puckerBloat: amount (\u2212100\u2026100). twist: angle, center {x, y} (offset from the middle). zigzag: size, ridges, smooth. wiggle: size, detail, smooth, speed, correlation, phase, seed, time (seconds). offset: amount, join miter|round|bevel, miterLimit, copies, copyOffset. merge: mode merge|add|subtract|intersect|exclude. repeater: copies (fractions fade the last), offset, position {x, y}, anchor {x, y} (offset from the middle), rotation, scale {x, y} (percent), startOpacity, endOpacity (percent), composite below|above.";
9231
11870
  var fail = (message) => {
9232
11871
  throw new Error(message);
9233
11872
  };
@@ -9272,8 +11911,41 @@ var checkBlendOptions = (params) => {
9272
11911
  if (params?.steps !== void 0 && !(Number.isFinite(params.steps) && params.steps >= 1)) fail("steps must be at least 1.");
9273
11912
  if (params?.distance !== void 0 && !(Number.isFinite(params.distance) && params.distance > 0)) fail("distance must be positive.");
9274
11913
  };
11914
+ var POINT = { type: "object", properties: { x: { type: "number" }, y: { type: "number" } }, required: ["x", "y"] };
11915
+ var GRADIENT_PROPS = {
11916
+ type: { type: "string", enum: VECTOR_GRADIENT_TYPES, description: "linear, radial, conic (an angle sweep), reflected (linear mirrored about the start) or diamond." },
11917
+ stops: {
11918
+ type: "array",
11919
+ minItems: 1,
11920
+ items: {
11921
+ type: "object",
11922
+ properties: {
11923
+ offset: { type: "number", minimum: 0, maximum: 1 },
11924
+ color: { type: "string", description: "A CSS colour." },
11925
+ opacity: { type: "number", minimum: 0, maximum: 1 },
11926
+ midpoint: { type: "number", minimum: 0.05, maximum: 0.95, description: "Where between this stop and the next the colours are half-mixed (default 0.5)." }
11927
+ },
11928
+ required: ["offset", "color"]
11929
+ }
11930
+ },
11931
+ angle: { type: "number", description: "Shorthand direction in degrees (0 up, clockwise): linear / reflected direction across the box, conic start angle, diamond turn." },
11932
+ cx: { type: "number", description: "Shorthand centre x of radial / conic / diamond gradients, a fraction of the box (default 0.5)." },
11933
+ cy: { type: "number" },
11934
+ r: { type: "number", description: "Shorthand radius, a fraction of the box (default 0.5)." },
11935
+ start: { ...POINT, description: "Explicit start (centre of radial / conic / diamond), in `units`. Wins over the shorthand." },
11936
+ end: { ...POINT, description: "Explicit end of the main axis (the radius of radial / conic / diamond)." },
11937
+ width: { ...POINT, description: "End of the secondary axis (aspect and turn of radial / conic / diamond); default square to the main axis, as long." },
11938
+ focal: { ...POINT, description: "Radial focal point (where t = 0), in `units`." },
11939
+ focalRadius: { type: "number", minimum: 0, maximum: 0.99, description: "Radial focal circle radius as a fraction of the radius (SVG fr)." },
11940
+ units: { type: "string", enum: ["objectBoundingBox", "userSpaceOnUse"], description: "Space of start / end / width / focal: fractions of the shape box (default) or the node's own coordinates." },
11941
+ spread: { type: "string", enum: VECTOR_GRADIENT_SPREADS, description: "What paints past the ends: pad (default), reflect or repeat." },
11942
+ interpolation: { type: "string", enum: VECTOR_GRADIENT_INTERPOLATIONS, description: "Colour mixing space: srgb (default), linear (light) or oklab." },
11943
+ smoothness: { type: "number", minimum: 0, maximum: 1, description: "Bends the ramp along a smooth spline through the stops (0 = straight)." },
11944
+ strokeMode: { type: "string", enum: VECTOR_STROKE_GRADIENT_MODES, description: "On strokes: within (default, like a fill), along the path or across it." }
11945
+ };
9275
11946
  var PAINT = {
9276
- description: 'A CSS colour, `null` for none, or a gradient `{ type: "linear", angle, stops: [{ offset, color }] }`.'
11947
+ description: 'A CSS colour, `null` for none, or a gradient object: `{ type: "linear" | "radial" | "conic" | "reflected" | "diamond", stops: [{ offset, color, opacity?, midpoint? }] }` plus the shorthand `angle` / `cx, cy, r` or explicit `start`, `end`, `width`, `focal`, `focalRadius`, `units`, and `spread`, `interpolation`, `smoothness`, `strokeMode` (see vector.setGradient).',
11948
+ anyOf: [{ type: "string" }, { type: "null" }, { type: "object", properties: GRADIENT_PROPS, required: ["type", "stops"] }]
9277
11949
  };
9278
11950
  var STYLE_PROPS = {
9279
11951
  fill: PAINT,
@@ -9294,6 +11966,10 @@ var STYLE_PROPS = {
9294
11966
  var pickStyle = (params) => {
9295
11967
  const out = {};
9296
11968
  for (const key of Object.keys(STYLE_PROPS)) if (params[key] !== void 0) out[key] = params[key];
11969
+ for (const key of ["fill", "stroke"]) {
11970
+ const paint = out[key];
11971
+ if (paint && typeof paint === "object") out[key] = sanitizeVectorGradient(paint) ?? fail(`${key} is not a valid gradient: give a type and at least one stop { offset, color }.`);
11972
+ }
9297
11973
  return out;
9298
11974
  };
9299
11975
  var check = (result) => {
@@ -9338,7 +12014,10 @@ function createVectorCommands(controller, options = {}) {
9338
12014
  selected: state.selection.includes(n.id),
9339
12015
  ...n.type === "path" ? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) } : { children: n.children.length },
9340
12016
  ...n.type === "group" && n.blend ? { blend: { ...resolveVectorBlendOptions(n.blend), spine: !!n.blend.spine, keys: vectorBlendKeys(n).map((k) => k.id) } } : {},
9341
- ...n.blendStep ? { blendStep: true } : {}
12017
+ ...n.blendStep ? { blendStep: true } : {},
12018
+ ...n.effects?.length ? { effects: n.effects } : {},
12019
+ ...n.type === "group" && n.liveBoolean ? { liveBoolean: n.liveBoolean } : {},
12020
+ ...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}
9342
12021
  });
9343
12022
  if (n.type === "group") visit(n.children, depth + 1);
9344
12023
  }
@@ -9431,6 +12110,26 @@ function createVectorCommands(controller, options = {}) {
9431
12110
  return { ok: true };
9432
12111
  }
9433
12112
  },
12113
+ {
12114
+ ...base,
12115
+ id: `${prefix}setGradient`,
12116
+ label: "Gradient\u2026",
12117
+ keywords: ["gradient", "linear", "radial", "conic", "angle", "reflected", "diamond", "stops", "midpoint", "oklab", "spread", "focal", "along stroke", "across stroke", "reverse"],
12118
+ description: "Edits the gradient of the fill (default) or stroke of paths (groups apply to every path inside): any gradient field (missing ones keep their value; a colour becomes a gradient from it to white), `aspect` (secondary axis over main axis), `reverse` (flip the stops and midpoints), `focal` / `width` null to remove them. Midpoints, smoothness and oklab / linear interpolation export to SVG as extra stops; conic and diamond gradients as patterns.",
12119
+ params: { type: "object", properties: { ids: IDS, target: { type: "string", enum: ["fill", "stroke"] }, ...GRADIENT_PROPS, aspect: { type: "number" }, reverse: { type: "boolean" } } },
12120
+ enabled: enabledFor,
12121
+ undoable: true,
12122
+ run: (_ctx, params) => {
12123
+ const { ids: _i, target, ...patch } = params ?? {};
12124
+ if (patch.type !== void 0 && !VECTOR_GRADIENT_TYPES.includes(patch.type)) fail(`Unknown gradient type "${patch.type}".`);
12125
+ if (patch.stops !== void 0 && !(Array.isArray(patch.stops) && patch.stops.length)) fail("stops needs at least one { offset, color }.");
12126
+ check(controller.setGradient(patch, { ids: ids(params), target: target ?? "fill" }));
12127
+ const id = ids(params)[0];
12128
+ const node = id ? controller.getNode(id) : void 0;
12129
+ const path = node?.type === "path" ? node : void 0;
12130
+ return { ok: true, ...path ? { gradient: path[target ?? "fill"] } : {} };
12131
+ }
12132
+ },
9434
12133
  {
9435
12134
  ...base,
9436
12135
  id: `${prefix}setCorners`,
@@ -9574,15 +12273,101 @@ function createVectorCommands(controller, options = {}) {
9574
12273
  id: `${prefix}boolean`,
9575
12274
  label: "Boolean Operation\u2026",
9576
12275
  keywords: ["union", "unite", "subtract", "intersect", "exclude", "combine", "pathfinder"],
9577
- description: "Combines two or more paths with union, subtract (upper shapes from the lowest), intersect or exclude.",
9578
- params: { type: "object", properties: { operation: { type: "string", enum: ["union", "subtract", "intersect", "exclude"] }, ids: IDS }, required: ["operation"] },
12276
+ description: "Combines two or more paths with union, subtract (upper shapes from the lowest), intersect or exclude. With `live`, the shapes stay editable operands of a live boolean group instead (a lone live group gets the operation; its operands get their own).",
12277
+ params: {
12278
+ type: "object",
12279
+ properties: {
12280
+ operation: { type: "string", enum: ["union", "subtract", "intersect", "exclude"] },
12281
+ ids: IDS,
12282
+ live: { type: "boolean", description: "Keep the operands editable in a live boolean group (default false: one new path)." }
12283
+ },
12284
+ required: ["operation"]
12285
+ },
9579
12286
  undoable: true,
9580
12287
  run: (_ctx, params) => {
9581
- const id = controller.booleanOp(params.operation, { ids: ids(params) });
9582
- if (!id) fail("Select at least two overlapping paths (the result was empty).");
12288
+ const id = controller.booleanOp(params.operation, { ids: ids(params), live: params.live });
12289
+ if (!id) fail(params.live ? "Select two or more nodes with one parent, a live boolean group or its operands." : "Select at least two overlapping paths (the result was empty).");
9583
12290
  return { id };
9584
12291
  }
9585
12292
  },
12293
+ {
12294
+ ...base,
12295
+ id: `${prefix}addEffect`,
12296
+ label: "Add Path Effect\u2026",
12297
+ keywords: ["effect", "trim", "round corners", "pucker", "bloat", "twist", "zig zag", "wiggle", "offset", "merge", "repeater", "appearance", "live"],
12298
+ description: "Adds a live path effect to the bottom of the effect stack of nodes (paths or groups). Effects apply top to bottom; effects after a repeater act on its copies. Returns the new effect ids.",
12299
+ params: {
12300
+ type: "object",
12301
+ properties: {
12302
+ type: { type: "string", enum: VECTOR_EFFECT_TYPES },
12303
+ params: { type: "object", description: EFFECT_PARAMS_DESCRIPTION },
12304
+ index: { type: "integer", minimum: 0, description: "Position in the stack (default: last)." },
12305
+ ids: IDS
12306
+ },
12307
+ required: ["type"]
12308
+ },
12309
+ undoable: true,
12310
+ enabled: enabledFor,
12311
+ run: (_ctx, params) => {
12312
+ if (!VECTOR_EFFECT_TYPES.includes(params.type)) fail(`Unknown effect type "${params.type}".`);
12313
+ const targets = ids(params);
12314
+ const created = controller.addEffect(params.type, { ids: targets, params: params.params, index: params.index });
12315
+ if (!created.length) fail("Nothing to add the effect to.");
12316
+ return { effects: targets.map((id, i) => ({ nodeId: id, effectId: created[i] })) };
12317
+ }
12318
+ },
12319
+ {
12320
+ ...base,
12321
+ id: `${prefix}updateEffect`,
12322
+ label: "Edit Path Effect\u2026",
12323
+ keywords: ["effect", "parameters", "enable", "disable", "reorder"],
12324
+ description: "Changes a path effect: its parameters, whether it is enabled, and its position in the stack.",
12325
+ params: {
12326
+ type: "object",
12327
+ properties: {
12328
+ nodeId: { type: "string" },
12329
+ effectId: { type: "string" },
12330
+ params: { type: "object", description: EFFECT_PARAMS_DESCRIPTION },
12331
+ enabled: { type: "boolean" },
12332
+ index: { type: "integer", minimum: 0, description: "Move it to this position in the stack (0 applies first)." }
12333
+ },
12334
+ required: ["nodeId", "effectId"]
12335
+ },
12336
+ undoable: true,
12337
+ run: (_ctx, params) => {
12338
+ if (params.params !== void 0 || params.enabled !== void 0) check(controller.updateEffect(params.nodeId, params.effectId, { params: params.params, enabled: params.enabled }));
12339
+ if (params.index !== void 0) check(controller.moveEffect(params.nodeId, params.effectId, params.index));
12340
+ return { effects: controller.getNode(params.nodeId)?.effects ?? [] };
12341
+ }
12342
+ },
12343
+ {
12344
+ ...base,
12345
+ id: `${prefix}removeEffect`,
12346
+ label: "Remove Path Effect",
12347
+ keywords: ["effect", "delete"],
12348
+ description: "Removes a path effect from a node.",
12349
+ params: { type: "object", properties: { nodeId: { type: "string" }, effectId: { type: "string" } }, required: ["nodeId", "effectId"] },
12350
+ undoable: true,
12351
+ run: (_ctx, params) => {
12352
+ check(controller.removeEffect(params.nodeId, params.effectId));
12353
+ return { ok: true };
12354
+ }
12355
+ },
12356
+ {
12357
+ ...base,
12358
+ id: `${prefix}expandAppearance`,
12359
+ label: "Expand Appearance",
12360
+ keywords: ["effects", "bake", "flatten", "live boolean", "expand"],
12361
+ description: "Bakes the path effects and live booleans of nodes (and everything inside them) into plain paths.",
12362
+ params: { type: "object", properties: { ids: IDS } },
12363
+ undoable: true,
12364
+ enabled: enabledFor,
12365
+ run: (_ctx, params) => {
12366
+ const expanded = controller.expandAppearance({ ids: ids(params) });
12367
+ if (!expanded.length) fail("Nothing selected has path effects or a live boolean.");
12368
+ return { ids: expanded };
12369
+ }
12370
+ },
9586
12371
  {
9587
12372
  ...base,
9588
12373
  id: `${prefix}blend`,
@@ -10030,8 +12815,25 @@ export {
10030
12815
  VECTOR_CORNER_KIND_LABELS,
10031
12816
  VECTOR_CORNER_WIDGET_INSET,
10032
12817
  VECTOR_EDIT_TOGGLES,
12818
+ VECTOR_EFFECT_DEFAULTS,
12819
+ VECTOR_EFFECT_LABELS,
12820
+ VECTOR_EFFECT_MENU,
12821
+ VECTOR_EFFECT_PARAM_SPECS,
12822
+ VECTOR_EFFECT_TYPES,
10033
12823
  VECTOR_FILL_GAP,
12824
+ VECTOR_GRADIENT_INTERPOLATIONS,
12825
+ VECTOR_GRADIENT_INTERPOLATION_LABELS,
12826
+ VECTOR_GRADIENT_MAX_PERIODS,
12827
+ VECTOR_GRADIENT_MIDPOINT_GAP,
12828
+ VECTOR_GRADIENT_MIN_STOPS,
12829
+ VECTOR_GRADIENT_OFF_BAR,
12830
+ VECTOR_GRADIENT_SPREADS,
12831
+ VECTOR_GRADIENT_SPREAD_LABELS,
12832
+ VECTOR_GRADIENT_STOP_GAP,
12833
+ VECTOR_GRADIENT_TYPES,
12834
+ VECTOR_GRADIENT_TYPE_LABELS,
10034
12835
  VECTOR_INK_MAX_CORNERS,
12836
+ VECTOR_MERGE_MODES,
10035
12837
  VECTOR_OVERLAY_THEME,
10036
12838
  VECTOR_PATHFINDER_ACTIONS,
10037
12839
  VECTOR_PATHFINDER_LABELS,
@@ -10041,6 +12843,8 @@ export {
10041
12843
  VECTOR_PRESSURE_NOMINAL,
10042
12844
  VECTOR_ROTATE_HANDLE_OFFSET,
10043
12845
  VECTOR_STROKE_ALIGN_LABELS,
12846
+ VECTOR_STROKE_GRADIENT_MODES,
12847
+ VECTOR_STROKE_GRADIENT_MODE_LABELS,
10044
12848
  VECTOR_STYLE_KEYS,
10045
12849
  VECTOR_TOOLS,
10046
12850
  VECTOR_TOOL_GLYPHS,
@@ -10053,11 +12857,15 @@ export {
10053
12857
  VECTOR_WIDTH_SAME_T,
10054
12858
  VectorEditorController,
10055
12859
  VectorSnapTargets,
12860
+ activeVectorEffects,
10056
12861
  addBuilderHit,
10057
12862
  applyMatrix,
10058
12863
  applyMatrixLinear,
10059
12864
  applyVectorBlendOptions,
10060
12865
  applyVectorCornersAttribute,
12866
+ applyVectorEffects,
12867
+ applyVectorGradientPatch,
12868
+ applyVectorGradientSpread,
10061
12869
  arrowheadInset,
10062
12870
  arrowheadPath,
10063
12871
  arrowheadWeight,
@@ -10065,6 +12873,7 @@ export {
10065
12873
  bezierToSubpaths,
10066
12874
  boundsFromPoints,
10067
12875
  boundsIntersect,
12876
+ clearVectorLiveBooleanCache,
10068
12877
  cloneAnchor,
10069
12878
  cloneSubpath,
10070
12879
  cloneVectorDocument,
@@ -10072,27 +12881,38 @@ export {
10072
12881
  collectPathNodes,
10073
12882
  constrainToLeave,
10074
12883
  convertAnchor,
12884
+ convertVectorGradient,
10075
12885
  cornerBisectorPoint,
10076
12886
  cornerRadiusChange,
10077
12887
  cornerSetback,
10078
12888
  correspondMorphContours,
10079
12889
  createVectorCommands,
12890
+ createVectorEffect,
10080
12891
  createVectorMorph,
10081
12892
  curvatureMoveAnchor,
10082
12893
  cutSubpathAt,
10083
12894
  dashIntervals,
10084
12895
  decomposeRigid,
12896
+ defaultVectorGradient,
12897
+ effectPathLength,
12898
+ effectPathVertexCount,
10085
12899
  effectiveStrokeAlign,
10086
12900
  ellipseSubpath,
10087
12901
  eraseFromFill,
10088
12902
  eraseFromOutline,
10089
12903
  eraserRegion,
10090
12904
  evaluateVectorPathKeyframes,
12905
+ expandVectorAppearance,
12906
+ expandVectorAppearances,
10091
12907
  expandVectorBlend,
10092
12908
  expandVectorCorners,
10093
12909
  expandVectorCornersOf,
12910
+ expandVectorGradientSpread,
12911
+ expandVectorGradientStops,
12912
+ explicitVectorGradient,
10094
12913
  findVectorNode,
10095
12914
  fitCornerRadius,
12915
+ foldVectorBooleanOperands,
10096
12916
  generateVectorBlendChildren,
10097
12917
  generateWidthProfile,
10098
12918
  getAnchor,
@@ -10101,6 +12921,10 @@ export {
10101
12921
  getTransformHandlePositions,
10102
12922
  getVectorBlendControls,
10103
12923
  getVectorCornerWidgets,
12924
+ getVectorEffectsPanel,
12925
+ getVectorEnteredGroups,
12926
+ getVectorGradientAnnotator,
12927
+ getVectorGradientEditor,
10104
12928
  getVectorOverlayRenderData,
10105
12929
  getVectorSceneRenderData,
10106
12930
  getVectorSelectionBoxGeometry,
@@ -10109,14 +12933,20 @@ export {
10109
12933
  getVectorWidthHandles,
10110
12934
  getVisibleAnchors,
10111
12935
  getVisibleHandles,
12936
+ gradientMidpointRemap,
12937
+ hasExplicitGradientGeometry,
12938
+ hasOwnVectorAppearance,
10112
12939
  hasPaint,
10113
12940
  hasStrokeExtras,
12941
+ hasVectorAppearance,
10114
12942
  hasVectorCorners,
10115
12943
  hitTestVectorNode,
10116
12944
  hitTestVectorNodes,
10117
12945
  hitTestVectorStroke,
12946
+ hitVectorGradientAnnotator,
10118
12947
  inferAnchorType,
10119
12948
  insertAnchor,
12949
+ insertVectorGradientStop,
10120
12950
  insertWidthPoint,
10121
12951
  interpolateMorphContours,
10122
12952
  interpolateRigidMatrix,
@@ -10126,31 +12956,45 @@ export {
10126
12956
  interpolateVectorStyle,
10127
12957
  interpolateWidthProfiles,
10128
12958
  invertMatrix,
12959
+ isCentredVectorGradient,
10129
12960
  isClosedBezierShape,
10130
12961
  isIdentityMatrix,
10131
12962
  isUniformWidthProfile,
10132
12963
  isVectorBlendGroup,
12964
+ isVectorGradient,
12965
+ isVectorLiveBoolean,
10133
12966
  isVectorTextTarget,
10134
12967
  joinSubpaths,
10135
12968
  knifePath,
10136
12969
  lineSubpath,
10137
12970
  locateVectorNode,
10138
12971
  matrixRotation,
12972
+ mergePaths,
10139
12973
  mirrorWidthProfile,
10140
12974
  morphContourToSubpath,
10141
12975
  morphPathsCross,
10142
12976
  moveAnchorHandle,
12977
+ moveVectorGradientHandle,
12978
+ moveVectorGradientStop,
10143
12979
  multiplyMatrices,
10144
12980
  nearestSegment,
10145
12981
  nearestStrokePosition,
10146
12982
  nextVectorCornerKind,
10147
12983
  nodeMatrix,
12984
+ normalizeVectorEffectParams,
12985
+ offsetPaths,
10148
12986
  parseSvgTransform,
12987
+ parseVectorColor,
10149
12988
  parseVectorSvg,
12989
+ pathsCenter,
12990
+ patternAttrs,
12991
+ periodicVectorGradientStops,
12992
+ pinVectorGradient,
10150
12993
  planShapeBuilder,
10151
12994
  polygonSubpath,
10152
12995
  polylineBounds,
10153
12996
  pressureWidthProfile,
12997
+ puckerBloatPaths,
10154
12998
  recognizeInkShape,
10155
12999
  rectSubpath,
10156
13000
  refineMorphContour,
@@ -10160,23 +13004,36 @@ export {
10160
13004
  removeAnchors,
10161
13005
  removeAnchorsKeepingShape,
10162
13006
  removeRedundantPoints,
13007
+ removeVectorGradientStop,
10163
13008
  removeWidthPoints,
13009
+ repeatPaths,
10164
13010
  resampleGradientStops,
10165
13011
  resolveVectorBlendOptions,
10166
13012
  reverseMorphContour,
10167
13013
  reverseSubpath,
13014
+ reverseVectorGradient,
10168
13015
  reverseWidthProfile,
10169
13016
  rotateMatrix,
13017
+ roundCornerPaths,
10170
13018
  roundSubpaths,
13019
+ samplePlainStops,
10171
13020
  samplePolyline,
13021
+ sampleVectorGradient,
13022
+ sampleVectorGradientAt,
13023
+ sampleVectorPaintDefAt,
13024
+ sanitizeVectorEffects,
13025
+ sanitizeVectorGradient,
10172
13026
  sanitizeWidthProfile,
10173
13027
  scaleFor,
10174
13028
  scaleMatrix,
10175
13029
  setAnchorCorner,
10176
13030
  setSubpathCorners,
13031
+ setVectorGradientVector,
10177
13032
  setWidthPoint,
10178
13033
  simplifyPolyline,
10179
13034
  smoothSubpath,
13035
+ sortedVectorGradientStops,
13036
+ spanVectorGradientStops,
10180
13037
  splitIntoFaces,
10181
13038
  splitSubpathAt,
10182
13039
  starSubpath,
@@ -10192,13 +13049,23 @@ export {
10192
13049
  toVectorKeyInput,
10193
13050
  toVectorModifiers,
10194
13051
  toVectorPointerInput,
13052
+ transformVectorEffects,
13053
+ transformVectorGradient,
10195
13054
  transformVectorNode,
13055
+ transformVectorPaint,
10196
13056
  translateAnchor,
10197
13057
  translateMatrix,
13058
+ trimIntervals,
13059
+ trimPaths,
13060
+ trimPathsIndividually,
10198
13061
  turnMorphContour,
13062
+ twistPaths,
10199
13063
  unionBounds,
10200
13064
  variableStrokeOutline,
10201
13065
  variableStrokeRings,
13066
+ vectorAppearanceNodes,
13067
+ vectorAppearanceOutline,
13068
+ vectorAppearanceShapes,
10202
13069
  vectorBlendColorDistance,
10203
13070
  vectorBlendKeyTargets,
10204
13071
  vectorBlendKeys,
@@ -10206,28 +13073,51 @@ export {
10206
13073
  vectorBlendRailAt,
10207
13074
  vectorBlendSpine,
10208
13075
  vectorBlendStepCount,
13076
+ vectorBoxMatrix,
10209
13077
  vectorBuilderErasedEdges,
10210
13078
  vectorCornerAt,
10211
13079
  vectorCorners,
10212
13080
  vectorCornersAttribute,
10213
13081
  vectorDiamondPath,
10214
13082
  vectorDocumentToSvg,
13083
+ vectorEnteredGroups,
10215
13084
  vectorFillMap,
13085
+ vectorGradientArea,
13086
+ vectorGradientAspect,
13087
+ vectorGradientBarPosition,
13088
+ vectorGradientFocalParam,
13089
+ vectorGradientHandles,
13090
+ vectorGradientMatrix,
13091
+ vectorGradientMidpointFromPosition,
13092
+ vectorGradientMidpointPosition,
13093
+ vectorGradientNeedsExpansion,
13094
+ vectorGradientParam,
13095
+ vectorGradientPoints,
13096
+ vectorGradientRamp,
13097
+ vectorGradientSvgDefs,
13098
+ vectorGradientTAt,
13099
+ vectorGradientTargetPath,
13100
+ vectorGradientUnitsMatrix,
10216
13101
  vectorInkShapeName,
13102
+ vectorLiveBooleanSteps,
10217
13103
  vectorNodeBounds,
10218
13104
  vectorNodeRotation,
10219
13105
  vectorNodeToBezier,
13106
+ vectorOperandOperation,
10220
13107
  vectorPaintToHex,
10221
13108
  vectorPaintToSvg,
10222
13109
  vectorParentMatrix,
10223
13110
  vectorPathData,
10224
13111
  vectorPathEntries,
13112
+ vectorPathLocalBounds,
10225
13113
  vectorPathWorldBezier,
10226
13114
  vectorPathfinderShape,
13115
+ vectorRepeaterCopies,
10227
13116
  vectorResizeCursor,
10228
13117
  vectorResultStyle,
10229
13118
  vectorShapeBuilderHit,
10230
13119
  vectorShapeBuilderMap,
13120
+ vectorStrokeGradientDefs,
10231
13121
  vectorStrokeLineArea,
10232
13122
  vectorStrokeOutline,
10233
13123
  vectorStrokeOutlinePath,
@@ -10243,6 +13133,8 @@ export {
10243
13133
  widthProfileMaxScale,
10244
13134
  widthProfilePreset,
10245
13135
  widthProfilePresetId,
10246
- widthProfileToAttribute
13136
+ widthProfileToAttribute,
13137
+ wigglePaths,
13138
+ zigzagPaths
10247
13139
  };
10248
13140
  //# sourceMappingURL=index.js.map