@pixerate/editor 0.41.0 → 0.42.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.
@@ -18,6 +18,7 @@ import {
18
18
  hatchPath,
19
19
  isStraightCubic,
20
20
  normalizeBezierPath,
21
+ offsetPath,
21
22
  outlineStroke,
22
23
  pathBoolean,
23
24
  pathToContours,
@@ -73,6 +74,8 @@ var VECTOR_TOOLS = [
73
74
  ];
74
75
  var VECTOR_TRANSFORM_HANDLES = ["nw", "n", "ne", "e", "se", "s", "sw", "w", "rotate"];
75
76
  var VECTOR_PATHFINDER_OPERATIONS = ["divide", "trim", "merge", "crop", "outline", "minusBack"];
77
+ var VECTOR_EFFECT_TYPES = ["trim", "roundCorners", "puckerBloat", "twist", "zigzag", "wiggle", "offset", "merge", "repeater"];
78
+ var VECTOR_MERGE_MODES = ["merge", "add", "subtract", "intersect", "exclude"];
76
79
 
77
80
  // src/vector/corners.ts
78
81
  var EPS = 1e-9;
@@ -1042,8 +1045,872 @@ function vectorStrokeReach(style) {
1042
1045
  return reach;
1043
1046
  }
1044
1047
 
1045
- // src/vector/geometry.ts
1048
+ // src/vector/pathEffects.ts
1046
1049
  var EPS2 = 1e-9;
1050
+ var degenerate = (s) => s.every((p) => Math.abs(p[0] - s[0][0]) < EPS2 && Math.abs(p[1] - s[0][1]) < EPS2);
1051
+ function raise(seg) {
1052
+ if (seg.length !== 3) return seg.map((p) => [p[0], p[1]]);
1053
+ const [p0, p1, p2] = seg;
1054
+ 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];
1055
+ }
1056
+ function subsOf(path) {
1057
+ return pathToContours(path).map((c) => ({ segs: closeContour(c).filter((s) => !degenerate(s)).map(raise), closed: c.closed })).filter((s) => s.segs.length > 0);
1058
+ }
1059
+ var subsToPath = (subs) => contoursToPath(subs.map((s) => ({ segments: s.segs, closed: s.closed })));
1060
+ var unit2 = (x, y) => {
1061
+ const l = Math.hypot(x, y);
1062
+ return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
1063
+ };
1064
+ function startTangent(s) {
1065
+ for (let i = 1; i < s.length; i++) {
1066
+ const u = unit2(s[i][0] - s[0][0], s[i][1] - s[0][1]);
1067
+ if (u) return u;
1068
+ }
1069
+ return [1, 0];
1070
+ }
1071
+ function endTangent(s) {
1072
+ const end = s[s.length - 1];
1073
+ for (let i = s.length - 2; i >= 0; i--) {
1074
+ const u = unit2(end[0] - s[i][0], end[1] - s[i][1]);
1075
+ if (u) return u;
1076
+ }
1077
+ return [1, 0];
1078
+ }
1079
+ function tangentAt(s, t) {
1080
+ if (t <= 1e-9) return startTangent(s);
1081
+ if (t >= 1 - 1e-9) return endTangent(s);
1082
+ const d = segmentDerivative(s, t);
1083
+ return unit2(d[0], d[1]) ?? startTangent(s);
1084
+ }
1085
+ var perp = (v2) => [-v2[1], v2[0]];
1086
+ function effectPathLength(path) {
1087
+ let total = 0;
1088
+ for (const s of subsOf(path)) for (const seg of s.segs) total += segmentLength(seg);
1089
+ return total;
1090
+ }
1091
+ function effectPathVertexCount(path) {
1092
+ return subsOf(path).reduce((n, s) => n + s.segs.length + (s.closed ? 0 : 1), 0);
1093
+ }
1094
+ var Measured = class {
1095
+ segs;
1096
+ lens;
1097
+ total;
1098
+ constructor(path) {
1099
+ this.segs = subsOf(path).flatMap((s) => s.segs);
1100
+ this.lens = this.segs.map((s) => segmentLength(s));
1101
+ this.total = this.lens.reduce((a, b) => a + b, 0);
1102
+ }
1103
+ /**
1104
+ * Appends the part between lengths `sa` and `sb`, continuing the last piece when this one
1105
+ * starts where it ends (across the start of a closed path).
1106
+ */
1107
+ append(sa, sb, out) {
1108
+ for (const piece of trimSegments(this.segs, sa, sb, this.lens)) {
1109
+ if (degenerate(piece)) continue;
1110
+ const last = out[out.length - 1]?.segs;
1111
+ const end = last?.[last.length - 1]?.at(-1);
1112
+ if (!end || Math.hypot(end[0] - piece[0][0], end[1] - piece[0][1]) > 1e-6) out.push({ segs: [piece], closed: false });
1113
+ else last.push(piece);
1114
+ }
1115
+ }
1116
+ };
1117
+ function trimIntervals(start, end, offsetDegrees) {
1118
+ let s = start / 100;
1119
+ let e = end / 100;
1120
+ if (!Number.isFinite(s) || !Number.isFinite(e)) return null;
1121
+ if (s > e) [s, e] = [e, s];
1122
+ s = Math.min(1, Math.max(0, s));
1123
+ e = Math.min(1, Math.max(0, e));
1124
+ if (e - s >= 1 - 1e-9) return null;
1125
+ if (e - s <= 1e-9) return [];
1126
+ const off = Number.isFinite(offsetDegrees) ? (offsetDegrees / 360 % 1 + 1) % 1 : 0;
1127
+ s += off;
1128
+ e += off;
1129
+ if (e <= 1) return [[s, e]];
1130
+ if (s >= 1) return [[s - 1, e - 1]];
1131
+ return [
1132
+ [s, 1],
1133
+ [0, e - 1]
1134
+ ];
1135
+ }
1136
+ function trimPaths(paths, start, end, offsetDegrees = 0) {
1137
+ const iv = trimIntervals(start, end, offsetDegrees);
1138
+ if (!iv) return paths.slice();
1139
+ return paths.map((p) => {
1140
+ const m = new Measured(p);
1141
+ const out = [];
1142
+ if (m.total > 0) for (const [a, b] of iv) m.append(a * m.total, b * m.total, out);
1143
+ return subsToPath(out);
1144
+ });
1145
+ }
1146
+ function trimPathsIndividually(paths, start, end, offsetDegrees = 0) {
1147
+ const iv = trimIntervals(start, end, offsetDegrees);
1148
+ if (!iv) return paths.slice();
1149
+ const ms = paths.map((p) => new Measured(p));
1150
+ const total = ms.reduce((n, m) => n + m.total, 0);
1151
+ let base = 0;
1152
+ return ms.map((m) => {
1153
+ const out = [];
1154
+ for (const [a, b] of iv) {
1155
+ const la = Math.max(0, a * total - base);
1156
+ const lb = Math.min(m.total, b * total - base);
1157
+ if (lb > la) m.append(la, lb, out);
1158
+ }
1159
+ base += m.total;
1160
+ return subsToPath(out);
1161
+ });
1162
+ }
1163
+ function puckerBloatPaths(paths, amount) {
1164
+ const a = amount / 100;
1165
+ if (!a || !Number.isFinite(a)) return paths.slice();
1166
+ return paths.map((p) => {
1167
+ const verts = p.filter((c) => c.type !== "Z");
1168
+ if (!verts.length) return p;
1169
+ const cx = verts.reduce((s, c) => s + c.x, 0) / verts.length;
1170
+ const cy = verts.reduce((s, c) => s + c.y, 0) / verts.length;
1171
+ const mv = (x, y) => ({ x: x + (cx - x) * a, y: y + (cy - y) * a });
1172
+ const mvh = (x, y) => ({ x: x + (x - cx) * a, y: y + (y - cy) * a });
1173
+ const out = [];
1174
+ let prev = { x: 0, y: 0 };
1175
+ let start = { x: 0, y: 0 };
1176
+ const curve = (c1, c2, q) => {
1177
+ const h1 = mvh(c1.x, c1.y);
1178
+ const h2 = mvh(c2.x, c2.y);
1179
+ const e = mv(q.x, q.y);
1180
+ out.push({ type: "C", x1: h1.x, y1: h1.y, x2: h2.x, y2: h2.y, x: e.x, y: e.y });
1181
+ };
1182
+ for (const c of p) {
1183
+ switch (c.type) {
1184
+ case "M": {
1185
+ const e = mv(c.x, c.y);
1186
+ out.push({ type: "M", x: e.x, y: e.y });
1187
+ prev = start = { x: c.x, y: c.y };
1188
+ break;
1189
+ }
1190
+ case "L":
1191
+ curve(prev, c, c);
1192
+ prev = { x: c.x, y: c.y };
1193
+ break;
1194
+ case "Q": {
1195
+ const c1 = { x: prev.x + 2 / 3 * (c.x1 - prev.x), y: prev.y + 2 / 3 * (c.y1 - prev.y) };
1196
+ const c2 = { x: c.x + 2 / 3 * (c.x1 - c.x), y: c.y + 2 / 3 * (c.y1 - c.y) };
1197
+ curve(c1, c2, c);
1198
+ prev = { x: c.x, y: c.y };
1199
+ break;
1200
+ }
1201
+ case "C":
1202
+ curve({ x: c.x1, y: c.y1 }, { x: c.x2, y: c.y2 }, c);
1203
+ prev = { x: c.x, y: c.y };
1204
+ break;
1205
+ case "Z":
1206
+ if (Math.hypot(prev.x - start.x, prev.y - start.y) > EPS2) curve(prev, start, start);
1207
+ out.push({ type: "Z" });
1208
+ prev = start;
1209
+ break;
1210
+ }
1211
+ }
1212
+ return out;
1213
+ });
1214
+ }
1215
+ function roundCornerPaths(paths, radius) {
1216
+ if (!(radius > 0) || !Number.isFinite(radius)) return paths.slice();
1217
+ return paths.map((p) => subpathsToBezier(bezierToSubpaths(p).map((sp) => setSubpathCorners(sp, radius))));
1218
+ }
1219
+ function offsetPaths(paths, amount, join = "miter", miterLimit = 4, copies = 1, copyOffset = 0) {
1220
+ const n = Math.min(1e3, Math.max(1, Math.round(Number.isFinite(copies) ? copies : 1)));
1221
+ return paths.map((p) => {
1222
+ const out = [];
1223
+ for (let i = 0; i < n; i++) out.push(...offsetPath(p, amount * (i + 1 + copyOffset), { join, miterLimit }));
1224
+ return out;
1225
+ });
1226
+ }
1227
+ function twistPaths(paths, angle, center) {
1228
+ let rmax = 0;
1229
+ 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));
1230
+ const a = angle * Math.PI / 180;
1231
+ if (!a || rmax <= 1e-9 || !Number.isFinite(a)) return paths.slice();
1232
+ const k = a / rmax;
1233
+ const rot = (v2, th) => {
1234
+ const s = Math.sin(th);
1235
+ const co = Math.cos(th);
1236
+ return [v2[0] * co - v2[1] * s, v2[0] * s + v2[1] * co];
1237
+ };
1238
+ const map = (p) => {
1239
+ const v2 = [p[0] - center.x, p[1] - center.y];
1240
+ const r = rot(v2, k * Math.hypot(v2[0], v2[1]));
1241
+ return [center.x + r[0], center.y + r[1]];
1242
+ };
1243
+ const jac = (p, dv) => {
1244
+ const v2 = [p[0] - center.x, p[1] - center.y];
1245
+ const r = Math.hypot(v2[0], v2[1]);
1246
+ let w = dv;
1247
+ if (r > 1e-12) {
1248
+ const f = k * (v2[0] * dv[0] + v2[1] * dv[1]) / r;
1249
+ w = [dv[0] - v2[1] * f, dv[1] + v2[0] * f];
1250
+ }
1251
+ return rot(w, k * r);
1252
+ };
1253
+ return paths.map((p) => {
1254
+ const out = [];
1255
+ for (const sub3 of subsOf(p)) {
1256
+ const segs = [];
1257
+ for (const seg of sub3.segs) {
1258
+ const cubic = seg.length === 2 ? [seg[0], seg[0], seg[1], seg[1]] : seg;
1259
+ const len2 = segmentLength(seg);
1260
+ const m = Math.min(64, Math.max(1, Math.ceil(Math.abs(a) * len2 / rmax / 0.15 + len2 / 50)));
1261
+ for (let i = 0; i < m; i++) {
1262
+ const q = subSegment(cubic, i / m, (i + 1) / m);
1263
+ const p0 = map(q[0]);
1264
+ const p3 = map(q[3]);
1265
+ const h1 = jac(q[0], [q[1][0] - q[0][0], q[1][1] - q[0][1]]);
1266
+ const h2 = jac(q[3], [q[2][0] - q[3][0], q[2][1] - q[3][1]]);
1267
+ segs.push([p0, [p0[0] + h1[0], p0[1] + h1[1]], [p3[0] + h2[0], p3[1] + h2[1]], p3]);
1268
+ }
1269
+ }
1270
+ out.push({ segs, closed: sub3.closed });
1271
+ }
1272
+ return subsToPath(out);
1273
+ });
1274
+ }
1275
+ function resample(sub3, m) {
1276
+ const segs = sub3.segs;
1277
+ const n = segs.length;
1278
+ const lens = segs.map((s) => segmentLength(s));
1279
+ const out = [];
1280
+ for (let j = 0; j < n; j++) {
1281
+ const piece = lens[j] / m;
1282
+ for (let k = 0; k < m; k++) {
1283
+ if (k === 0) {
1284
+ const tout = startTangent(segs[j]);
1285
+ const prev = sub3.closed || j > 0 ? (j + n - 1) % n : null;
1286
+ let tan = tout;
1287
+ if (prev !== null) {
1288
+ const tin = endTangent(segs[prev]);
1289
+ tan = unit2(tin[0] + tout[0], tin[1] + tout[1]) ?? tout;
1290
+ }
1291
+ out.push({ p: segs[j][0], tan, lenPrev: prev === null ? 0 : lens[prev] / m, lenNext: piece });
1292
+ } else {
1293
+ const t = segmentParamAtLength(segs[j], piece * k, lens[j]);
1294
+ out.push({ p: segmentPoint(segs[j], t), tan: tangentAt(segs[j], t), lenPrev: piece, lenNext: piece });
1295
+ }
1296
+ }
1297
+ }
1298
+ if (!sub3.closed) {
1299
+ const last = segs[n - 1];
1300
+ out.push({ p: last[last.length - 1], tan: endTangent(last), lenPrev: lens[n - 1] / m, lenNext: 0 });
1301
+ }
1302
+ return out;
1303
+ }
1304
+ function emit(samples, disp, closed, smooth, handle) {
1305
+ const n = samples.length;
1306
+ if (!n) return null;
1307
+ const pt2 = (i) => [samples[i].p[0] + disp[i][0], samples[i].p[1] + disp[i][1]];
1308
+ const segs = [];
1309
+ const count = closed ? n : n - 1;
1310
+ for (let i = 0; i < count; i++) {
1311
+ const j = (i + 1) % n;
1312
+ const a = samples[i];
1313
+ const b = samples[j];
1314
+ const p = pt2(i);
1315
+ const q = pt2(j);
1316
+ 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]);
1317
+ else segs.push([p, q]);
1318
+ }
1319
+ if (!segs.length) return null;
1320
+ return { segs, closed };
1321
+ }
1322
+ function zigzagPaths(paths, size, ridges, smooth = false) {
1323
+ const m = Math.min(1e3, Math.max(0, Math.round(Number.isFinite(ridges) ? ridges : 0))) + 1;
1324
+ return paths.map((p) => {
1325
+ const out = [];
1326
+ for (const sub3 of subsOf(p)) {
1327
+ const s = resample(sub3, m);
1328
+ const disp = s.map((x, i) => {
1329
+ const n = perp(x.tan);
1330
+ const d = i % 2 === 0 ? size : -size;
1331
+ return [n[0] * d, n[1] * d];
1332
+ });
1333
+ const e = emit(s, disp, sub3.closed, smooth, 0.36);
1334
+ if (e) out.push(e);
1335
+ }
1336
+ return subsToPath(out);
1337
+ });
1338
+ }
1339
+ var MASK64 = (1n << 64n) - 1n;
1340
+ function splitmix(x) {
1341
+ x = x + 0x9e3779b97f4a7c15n & MASK64;
1342
+ let z = x;
1343
+ z = (z ^ z >> 30n) * 0xbf58476d1ce4e5b9n & MASK64;
1344
+ z = (z ^ z >> 27n) * 0x94d049bb133111ebn & MASK64;
1345
+ return z ^ z >> 31n;
1346
+ }
1347
+ function hash(seed, idx, t) {
1348
+ const h = splitmix(splitmix(splitmix(seed) ^ idx) ^ BigInt.asUintN(64, BigInt(t)));
1349
+ return Number(h >> 11n) / 2 ** 53 * 2 - 1;
1350
+ }
1351
+ function noise(seed, idx, t) {
1352
+ const i = Math.floor(t);
1353
+ const f = t - i;
1354
+ const p0 = hash(seed, idx, i - 1);
1355
+ const p1 = hash(seed, idx, i);
1356
+ const p2 = hash(seed, idx, i + 1);
1357
+ const p3 = hash(seed, idx, i + 2);
1358
+ 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);
1359
+ return Math.min(1, Math.max(-1, v2));
1360
+ }
1361
+ function wigglePaths(paths, w) {
1362
+ if (!w.size || !Number.isFinite(w.size)) return paths.slice();
1363
+ const m = Math.min(1e3, Math.max(0, Math.round(Number.isFinite(w.detail) ? w.detail : 0))) + 1;
1364
+ const seed = BigInt.asUintN(64, BigInt(Math.round(Number.isFinite(w.seed) ? w.seed : 0)));
1365
+ const t = (Number.isFinite(w.time) ? w.time : 0) * (Number.isFinite(w.speed) ? w.speed : 0) + (Number.isFinite(w.phase) ? w.phase : 0) / 360;
1366
+ const corr = Math.min(1, Math.max(0, (Number.isFinite(w.correlation) ? w.correlation : 0) / 100));
1367
+ const sx = noise(seed, MASK64, t);
1368
+ const sy = noise(seed, MASK64 - 1n, t);
1369
+ let idx = 0;
1370
+ return paths.map((p) => {
1371
+ const out = [];
1372
+ for (const sub3 of subsOf(p)) {
1373
+ const s = resample(sub3, m);
1374
+ const disp = s.map(() => {
1375
+ const i = BigInt(idx);
1376
+ const ox = noise(seed, i * 2n, t + idx * 0.37);
1377
+ const oy = noise(seed, i * 2n + 1n, t + idx * 0.61);
1378
+ idx++;
1379
+ return [(sx * corr + ox * (1 - corr)) * w.size, (sy * corr + oy * (1 - corr)) * w.size];
1380
+ });
1381
+ const e = emit(s, disp, sub3.closed, w.smooth, 1 / 3);
1382
+ if (e) out.push(e);
1383
+ }
1384
+ return subsToPath(out);
1385
+ });
1386
+ }
1387
+ var MERGE_OPS = { add: "union", subtract: "difference", intersect: "intersection", exclude: "xor" };
1388
+ function mergePaths(paths, mode = "merge") {
1389
+ if (mode === "merge" || !(mode in MERGE_OPS)) return paths.flat();
1390
+ const inputs = paths.filter((p) => p.length > 0).map((path) => ({ path, fillRule: "nonzero" }));
1391
+ if (!inputs.length) return [];
1392
+ return pathBoolean(MERGE_OPS[mode], inputs).path;
1393
+ }
1394
+ function vectorRepeaterCopies(r, pivot) {
1395
+ const copies = Math.max(0, Number.isFinite(r.copies) ? Math.min(r.copies, 1e3) : 0);
1396
+ const n = Math.ceil(copies);
1397
+ const offset = Number.isFinite(r.offset) ? r.offset : 0;
1398
+ const so = (r.startOpacity ?? 100) / 100;
1399
+ const eo = (r.endOpacity ?? 100) / 100;
1400
+ const fract = copies - Math.floor(copies);
1401
+ const out = [];
1402
+ for (let i = 0; i < n; i++) {
1403
+ const k = i + offset;
1404
+ let m = translateMatrix(-pivot.x, -pivot.y);
1405
+ m = multiplyMatrices(m, scaleMatrix(Math.pow((r.scale?.x ?? 100) / 100, k), Math.pow((r.scale?.y ?? 100) / 100, k)));
1406
+ m = multiplyMatrices(m, rotateMatrix((r.rotation ?? 0) * k));
1407
+ m = multiplyMatrices(m, translateMatrix(pivot.x + (r.position?.x ?? 0) * k, pivot.y + (r.position?.y ?? 0) * k));
1408
+ const t = n > 1 ? i / (n - 1) : 0;
1409
+ const partial = i + 1 === n && fract > 0 ? fract : 1;
1410
+ out.push({ index: i, matrix: m, opacity: Math.min(1, Math.max(0, (so + (eo - so) * t) * partial)) });
1411
+ }
1412
+ return out;
1413
+ }
1414
+ function repeatPaths(paths, r, pivot) {
1415
+ return vectorRepeaterCopies(r, pivot).map((copy) => ({ copy, paths: paths.map((p) => transformBezierPath(p, copy.matrix)) }));
1416
+ }
1417
+ function pathsCenter(paths) {
1418
+ const b = bezierPathBounds(paths.flat());
1419
+ return b ? { x: (b.minX + b.maxX) / 2, y: (b.minY + b.maxY) / 2 } : { x: 0, y: 0 };
1420
+ }
1421
+
1422
+ // src/vector/appearance.ts
1423
+ var VECTOR_EFFECT_DEFAULTS = {
1424
+ trim: { start: 0, end: 50, offset: 0, mode: "simultaneous" },
1425
+ roundCorners: { radius: 10 },
1426
+ puckerBloat: { amount: 30 },
1427
+ twist: { angle: 45, center: { x: 0, y: 0 } },
1428
+ zigzag: { size: 5, ridges: 4, smooth: false },
1429
+ wiggle: { size: 5, detail: 6, smooth: true, speed: 2, correlation: 50, phase: 0, seed: 1, time: 0 },
1430
+ offset: { amount: 5, join: "miter", miterLimit: 4, copies: 1, copyOffset: 0 },
1431
+ merge: { mode: "add" },
1432
+ repeater: {
1433
+ copies: 3,
1434
+ offset: 0,
1435
+ position: { x: 100, y: 0 },
1436
+ anchor: { x: 0, y: 0 },
1437
+ rotation: 0,
1438
+ scale: { x: 100, y: 100 },
1439
+ startOpacity: 100,
1440
+ endOpacity: 100,
1441
+ composite: "below"
1442
+ }
1443
+ };
1444
+ var VECTOR_EFFECT_LABELS = {
1445
+ trim: "Trim Paths",
1446
+ roundCorners: "Round Corners",
1447
+ puckerBloat: "Pucker & Bloat",
1448
+ twist: "Twist",
1449
+ zigzag: "Zig Zag",
1450
+ wiggle: "Wiggle Paths",
1451
+ offset: "Offset Paths",
1452
+ merge: "Merge Paths",
1453
+ repeater: "Repeater"
1454
+ };
1455
+ var VECTOR_EFFECT_PARAM_SPECS = {
1456
+ trim: [
1457
+ { key: "start", label: "Start %", kind: "number", min: 0, max: 100, step: 1, slider: true },
1458
+ { key: "end", label: "End %", kind: "number", min: 0, max: 100, step: 1, slider: true },
1459
+ { key: "offset", label: "Offset \xB0", kind: "number", min: -360, max: 360, step: 1, slider: true },
1460
+ {
1461
+ key: "mode",
1462
+ label: "Paths",
1463
+ kind: "select",
1464
+ options: [
1465
+ { value: "simultaneous", label: "Simultaneously" },
1466
+ { value: "individual", label: "Individually" }
1467
+ ]
1468
+ }
1469
+ ],
1470
+ roundCorners: [{ key: "radius", label: "Radius", kind: "number", min: 0, max: 200, step: 1, slider: true }],
1471
+ puckerBloat: [{ key: "amount", label: "Amount %", kind: "number", min: -100, max: 100, step: 1, slider: true }],
1472
+ twist: [
1473
+ { key: "angle", label: "Angle \xB0", kind: "number", min: -720, max: 720, step: 1, slider: true },
1474
+ { key: "center", label: "Centre", kind: "vec2", step: 1 }
1475
+ ],
1476
+ zigzag: [
1477
+ { key: "size", label: "Size", kind: "number", min: 0, max: 100, step: 0.5, slider: true },
1478
+ { key: "ridges", label: "Ridges", kind: "number", min: 0, max: 50, step: 1, slider: true },
1479
+ { key: "smooth", label: "Smooth", kind: "boolean" }
1480
+ ],
1481
+ wiggle: [
1482
+ { key: "size", label: "Size", kind: "number", min: 0, max: 100, step: 0.5, slider: true },
1483
+ { key: "detail", label: "Detail", kind: "number", min: 0, max: 50, step: 1, slider: true },
1484
+ { key: "smooth", label: "Smooth", kind: "boolean" },
1485
+ { key: "correlation", label: "Correlation %", kind: "number", min: 0, max: 100, step: 1, slider: true },
1486
+ { key: "speed", label: "Speed", kind: "number", min: 0, step: 0.1 },
1487
+ { key: "phase", label: "Phase \xB0", kind: "number", step: 1 },
1488
+ { key: "time", label: "Time (s)", kind: "number", min: 0, max: 10, step: 0.05, slider: true },
1489
+ { key: "seed", label: "Seed", kind: "number", step: 1 }
1490
+ ],
1491
+ offset: [
1492
+ { key: "amount", label: "Amount", kind: "number", min: -100, max: 100, step: 0.5, slider: true },
1493
+ {
1494
+ key: "join",
1495
+ label: "Join",
1496
+ kind: "select",
1497
+ options: [
1498
+ { value: "miter", label: "Miter" },
1499
+ { value: "round", label: "Round" },
1500
+ { value: "bevel", label: "Bevel" }
1501
+ ]
1502
+ },
1503
+ { key: "miterLimit", label: "Miter limit", kind: "number", min: 1, step: 0.5 },
1504
+ { key: "copies", label: "Copies", kind: "number", min: 1, max: 20, step: 1 },
1505
+ { key: "copyOffset", label: "Copy offset", kind: "number", step: 0.5 }
1506
+ ],
1507
+ merge: [
1508
+ {
1509
+ key: "mode",
1510
+ label: "Mode",
1511
+ kind: "select",
1512
+ options: [
1513
+ { value: "merge", label: "Merge" },
1514
+ { value: "add", label: "Add" },
1515
+ { value: "subtract", label: "Subtract" },
1516
+ { value: "intersect", label: "Intersect" },
1517
+ { value: "exclude", label: "Exclude" }
1518
+ ]
1519
+ }
1520
+ ],
1521
+ repeater: [
1522
+ { key: "copies", label: "Copies", kind: "number", min: 0, max: 50, step: 0.1, slider: true },
1523
+ { key: "offset", label: "Offset", kind: "number", min: -10, max: 10, step: 0.1, slider: true },
1524
+ { key: "position", label: "Position", kind: "vec2", step: 1 },
1525
+ { key: "anchor", label: "Anchor", kind: "vec2", step: 1 },
1526
+ { key: "rotation", label: "Rotation \xB0", kind: "number", min: -360, max: 360, step: 1, slider: true },
1527
+ { key: "scale", label: "Scale %", kind: "vec2", step: 1 },
1528
+ { key: "startOpacity", label: "Start opacity %", kind: "number", min: 0, max: 100, step: 1, slider: true },
1529
+ { key: "endOpacity", label: "End opacity %", kind: "number", min: 0, max: 100, step: 1, slider: true },
1530
+ {
1531
+ key: "composite",
1532
+ label: "Copies",
1533
+ kind: "select",
1534
+ options: [
1535
+ { value: "below", label: "Below" },
1536
+ { value: "above", label: "Above" }
1537
+ ]
1538
+ }
1539
+ ]
1540
+ };
1541
+ var isVec2 = (v2) => !!v2 && typeof v2 === "object" && Number.isFinite(v2.x) && Number.isFinite(v2.y);
1542
+ function normalizeVectorEffectParams(type, params, base) {
1543
+ const defaults = VECTOR_EFFECT_DEFAULTS[type];
1544
+ if (!defaults) return null;
1545
+ const out = { ...defaults, ...base ?? {} };
1546
+ for (const spec of VECTOR_EFFECT_PARAM_SPECS[type]) {
1547
+ const v2 = params?.[spec.key];
1548
+ if (v2 === void 0) continue;
1549
+ if (spec.kind === "number" && typeof v2 === "number" && Number.isFinite(v2)) out[spec.key] = Math.min(spec.max ?? Infinity, Math.max(spec.min ?? -Infinity, v2));
1550
+ else if (spec.kind === "boolean" && typeof v2 === "boolean") out[spec.key] = v2;
1551
+ else if (spec.kind === "vec2" && isVec2(v2)) out[spec.key] = { x: v2.x, y: v2.y };
1552
+ else if (spec.kind === "select" && spec.options.some((o) => o.value === v2)) out[spec.key] = v2;
1553
+ }
1554
+ return out;
1555
+ }
1556
+ function createVectorEffect(type, id, params) {
1557
+ const p = normalizeVectorEffectParams(type, params);
1558
+ return p ? { id, type, params: p } : null;
1559
+ }
1560
+ var isEnabled = (e) => e.enabled !== false && !!VECTOR_EFFECT_DEFAULTS[e.type];
1561
+ function activeVectorEffects(node) {
1562
+ return node.effects?.filter(isEnabled) ?? [];
1563
+ }
1564
+ function hasOwnVectorAppearance(node) {
1565
+ return node.type === "group" && !!node.liveBoolean || !!node.effects?.some(isEnabled);
1566
+ }
1567
+ var liveCache = /* @__PURE__ */ new WeakMap();
1568
+ function hasVectorAppearance(node) {
1569
+ let v2 = liveCache.get(node);
1570
+ if (v2 === void 0) {
1571
+ v2 = hasOwnVectorAppearance(node) || node.type === "group" && node.children.some(hasVectorAppearance);
1572
+ liveCache.set(node, v2);
1573
+ }
1574
+ return v2;
1575
+ }
1576
+ function isVectorLiveBoolean(node) {
1577
+ return node?.type === "group" && !!node.liveBoolean;
1578
+ }
1579
+ function vectorOperandOperation(group, child) {
1580
+ return child.booleanOperation ?? group.liveBoolean ?? "union";
1581
+ }
1582
+ var PATH_OPS = { union: "union", subtract: "difference", intersect: "intersection", exclude: "xor" };
1583
+ var FoldCache = class {
1584
+ constructor(cap) {
1585
+ this.cap = cap;
1586
+ }
1587
+ cap;
1588
+ map = /* @__PURE__ */ new Map();
1589
+ get(key) {
1590
+ const hit = this.map.get(key);
1591
+ if (hit) {
1592
+ this.map.delete(key);
1593
+ this.map.set(key, hit);
1594
+ }
1595
+ return hit;
1596
+ }
1597
+ set(key, path) {
1598
+ if (this.map.size >= this.cap) {
1599
+ let drop = Math.floor(this.map.size / 2);
1600
+ for (const k of this.map.keys()) {
1601
+ if (drop-- <= 0) break;
1602
+ this.map.delete(k);
1603
+ }
1604
+ }
1605
+ this.map.set(key, path);
1606
+ }
1607
+ clear() {
1608
+ this.map.clear();
1609
+ }
1610
+ };
1611
+ var foldCache = new FoldCache(4096);
1612
+ var foldSteps = 0;
1613
+ function vectorLiveBooleanSteps() {
1614
+ return foldSteps;
1615
+ }
1616
+ function clearVectorLiveBooleanCache() {
1617
+ foldCache.clear();
1618
+ }
1619
+ var RollingHash = class _RollingHash {
1620
+ a = 2166136261;
1621
+ b = 16777619 ^ 1540483477;
1622
+ static view = new DataView(new ArrayBuffer(8));
1623
+ byte(v2) {
1624
+ this.a = Math.imul(this.a ^ v2 & 255, 16777619);
1625
+ this.b = Math.imul(this.b ^ v2 & 255, 1540483477) ^ this.b >>> 15;
1626
+ }
1627
+ num(n) {
1628
+ _RollingHash.view.setFloat64(0, n);
1629
+ for (let i = 0; i < 8; i++) this.byte(_RollingHash.view.getUint8(i));
1630
+ }
1631
+ path(p) {
1632
+ for (const c of p) {
1633
+ this.byte(c.type.charCodeAt(0));
1634
+ if (c.type === "Z") continue;
1635
+ if (c.type === "C") {
1636
+ this.num(c.x1);
1637
+ this.num(c.y1);
1638
+ this.num(c.x2);
1639
+ this.num(c.y2);
1640
+ } else if (c.type === "Q") {
1641
+ this.num(c.x1);
1642
+ this.num(c.y1);
1643
+ }
1644
+ this.num(c.x);
1645
+ this.num(c.y);
1646
+ }
1647
+ }
1648
+ key() {
1649
+ return `${(this.a >>> 0).toString(36)}.${(this.b >>> 0).toString(36)}`;
1650
+ }
1651
+ };
1652
+ function foldVectorBooleanOperands(operands) {
1653
+ if (!operands.length) return [];
1654
+ if (operands.length === 1) return operands[0].path;
1655
+ const h = new RollingHash();
1656
+ const keys = operands.map((o, i) => {
1657
+ if (i > 0) h.byte(o.operation.charCodeAt(0) + o.operation.length);
1658
+ h.byte(o.fillRule === "evenodd" ? 1 : 0);
1659
+ h.path(o.path);
1660
+ h.byte(9);
1661
+ return h.key();
1662
+ });
1663
+ let start = 0;
1664
+ let acc = operands[0].path;
1665
+ for (let i = keys.length - 1; i >= 1; i--) {
1666
+ const hit = foldCache.get(keys[i]);
1667
+ if (hit) {
1668
+ if (i === keys.length - 1) return hit;
1669
+ start = i;
1670
+ acc = hit;
1671
+ break;
1672
+ }
1673
+ }
1674
+ let accRule = start === 0 ? operands[0].fillRule : "nonzero";
1675
+ for (let i = start + 1; i < operands.length; i++) {
1676
+ const o = operands[i];
1677
+ foldSteps++;
1678
+ const r = pathBoolean(PATH_OPS[o.operation] ?? "union", [
1679
+ { path: acc, fillRule: accRule },
1680
+ { path: o.path, fillRule: o.fillRule }
1681
+ ]);
1682
+ if (r.error) return [];
1683
+ acc = r.path;
1684
+ accRule = "nonzero";
1685
+ foldCache.set(keys[i], acc);
1686
+ }
1687
+ return acc;
1688
+ }
1689
+ var compareOrder = (a, b) => {
1690
+ const n = Math.min(a.length, b.length);
1691
+ for (let i = 0; i < n; i++) if (a[i] !== b[i]) return a[i] - b[i];
1692
+ return a.length - b.length;
1693
+ };
1694
+ var add2 = (a, b) => ({ x: a.x + (b?.x ?? 0), y: a.y + (b?.y ?? 0) });
1695
+ var mapPaths = (shapes, fn) => {
1696
+ const out = fn(shapes.map((s) => s.path));
1697
+ return shapes.map((s, i) => ({ ...s, path: out[i] ?? [] }));
1698
+ };
1699
+ function applyVectorEffects(shapes, effects) {
1700
+ let list = shapes.slice();
1701
+ for (const e of effects ?? []) {
1702
+ if (!isEnabled(e) || !list.length) continue;
1703
+ switch (e.type) {
1704
+ case "trim": {
1705
+ const { start, end, offset, mode } = e.params;
1706
+ list = mapPaths(list, (p) => mode === "individual" ? trimPathsIndividually(p, start, end, offset) : trimPaths(p, start, end, offset));
1707
+ break;
1708
+ }
1709
+ case "roundCorners":
1710
+ list = mapPaths(list, (p) => roundCornerPaths(p, e.params.radius));
1711
+ break;
1712
+ case "puckerBloat":
1713
+ list = mapPaths(list, (p) => puckerBloatPaths(p, e.params.amount));
1714
+ break;
1715
+ case "twist":
1716
+ list = mapPaths(list, (p) => twistPaths(p, e.params.angle, add2(pathsCenter(p), e.params.center)));
1717
+ break;
1718
+ case "zigzag":
1719
+ list = mapPaths(list, (p) => zigzagPaths(p, e.params.size, e.params.ridges, e.params.smooth));
1720
+ break;
1721
+ case "wiggle":
1722
+ list = mapPaths(list, (p) => wigglePaths(p, e.params));
1723
+ break;
1724
+ case "offset": {
1725
+ const o = e.params;
1726
+ list = mapPaths(list, (p) => offsetPaths(p, o.amount, o.join, o.miterLimit, o.copies, o.copyOffset));
1727
+ break;
1728
+ }
1729
+ case "merge": {
1730
+ const first = list[0];
1731
+ list = [{ ...first, path: mergePaths(list.map((s) => s.path), e.params.mode) }];
1732
+ break;
1733
+ }
1734
+ case "repeater": {
1735
+ const r = e.params;
1736
+ const copies = repeatPaths(
1737
+ list.map((s) => s.path),
1738
+ r,
1739
+ add2(pathsCenter(list.map((s) => s.path)), r.anchor)
1740
+ );
1741
+ const n = copies.length;
1742
+ const next = [];
1743
+ for (const { copy, paths } of copies) {
1744
+ const rank = r.composite === "above" ? copy.index : n - 1 - copy.index;
1745
+ list.forEach((s, i) => next.push({ source: s.source, path: paths[i], opacity: s.opacity * copy.opacity, order: [rank, ...s.order] }));
1746
+ }
1747
+ list = next;
1748
+ break;
1749
+ }
1750
+ }
1751
+ }
1752
+ return list;
1753
+ }
1754
+ function childShapes(child, index) {
1755
+ if (child.visible === false) return [];
1756
+ const m = child.transform;
1757
+ const k = child.opacity ?? 1;
1758
+ return vectorAppearanceShapes(child).map((s) => ({
1759
+ source: s.source,
1760
+ path: m && !isIdentityMatrix(m) ? transformBezierPath(s.path, m) : s.path,
1761
+ opacity: s.opacity * k,
1762
+ order: [index, ...s.order]
1763
+ }));
1764
+ }
1765
+ function liveBooleanShapes(group) {
1766
+ const operands = [];
1767
+ let source = null;
1768
+ group.children.forEach((child, i) => {
1769
+ const shapes = childShapes(child, i);
1770
+ if (!shapes.length) return;
1771
+ source ??= shapes[0].source;
1772
+ const path = shapes.length === 1 ? shapes[0].path : pathBoolean("union", shapes.map((s) => ({ path: s.path, fillRule: s.source.fillRule ?? "nonzero" }))).path;
1773
+ const fillRule = shapes.length === 1 ? shapes[0].source.fillRule ?? "nonzero" : "nonzero";
1774
+ operands.push({ path, fillRule, operation: vectorOperandOperation(group, child) });
1775
+ });
1776
+ if (!source) return [];
1777
+ return [{ source, path: foldVectorBooleanOperands(operands), opacity: 1, order: [0] }];
1778
+ }
1779
+ var shapeCache = /* @__PURE__ */ new WeakMap();
1780
+ function vectorAppearanceShapes(node) {
1781
+ const hit = shapeCache.get(node);
1782
+ if (hit) return hit;
1783
+ let base;
1784
+ if (node.type === "path") base = [{ source: node, path: subpathsToBezier(node.subpaths), opacity: 1, order: [] }];
1785
+ else if (node.liveBoolean) base = liveBooleanShapes(node);
1786
+ else base = node.children.flatMap((c, i) => childShapes(c, i));
1787
+ const out = applyVectorEffects(base, node.effects).sort((a, b) => compareOrder(a.order, b.order));
1788
+ shapeCache.set(node, out);
1789
+ return out;
1790
+ }
1791
+ var outlineCache = /* @__PURE__ */ new WeakMap();
1792
+ function vectorAppearanceOutline(node) {
1793
+ let hit = outlineCache.get(node);
1794
+ if (!hit) {
1795
+ hit = node.type === "path" && !hasOwnVectorAppearance(node) ? subpathsToBezier(node.subpaths) : vectorAppearanceShapes(node).flatMap((s) => s.path);
1796
+ outlineCache.set(node, hit);
1797
+ }
1798
+ return hit;
1799
+ }
1800
+ function shapeNode(shape, id) {
1801
+ const {
1802
+ id: _id,
1803
+ type: _type,
1804
+ subpaths: _s,
1805
+ transform: _t,
1806
+ effects: _e,
1807
+ booleanOperation: _b,
1808
+ blendStep: _bs,
1809
+ locked: _l,
1810
+ visible: _v,
1811
+ opacity: _o,
1812
+ ...style
1813
+ } = shape.source;
1814
+ return { ...style, id, type: "path", subpaths: bezierToSubpaths(shape.path), ...shape.opacity < 1 ? { opacity: shape.opacity } : {} };
1815
+ }
1816
+ function placement(node) {
1817
+ return {
1818
+ ...node.name !== void 0 ? { name: node.name } : {},
1819
+ ...node.transform ? { transform: node.transform } : {},
1820
+ ...node.visible === false ? { visible: false } : {},
1821
+ ...node.locked ? { locked: true } : {},
1822
+ ...node.blendStep ? { blendStep: true } : {},
1823
+ ...node.booleanOperation ? { booleanOperation: node.booleanOperation } : {}
1824
+ };
1825
+ }
1826
+ function vectorAppearanceNodes(node) {
1827
+ return vectorAppearanceShapes(node).map((s, k) => shapeNode(s, `${node.id}~fx${k}`));
1828
+ }
1829
+ var expandCache = /* @__PURE__ */ new WeakMap();
1830
+ function expandVectorAppearance(node) {
1831
+ if (!hasVectorAppearance(node)) return node;
1832
+ const hit = expandCache.get(node);
1833
+ if (hit) return hit;
1834
+ let out;
1835
+ if (!hasOwnVectorAppearance(node)) {
1836
+ const group = node;
1837
+ out = { ...group, children: group.children.map(expandVectorAppearance) };
1838
+ } else {
1839
+ const shapes = vectorAppearanceShapes(node);
1840
+ if (node.type === "path" && shapes.length === 1) {
1841
+ const one = shapes[0];
1842
+ const { effects: _e, opacity: _o, ...rest } = node;
1843
+ const opacity = (node.opacity ?? 1) * one.opacity;
1844
+ out = { ...rest, subpaths: bezierToSubpaths(one.path), ...opacity < 1 ? { opacity } : {} };
1845
+ } else {
1846
+ out = {
1847
+ id: node.id,
1848
+ type: "group",
1849
+ ...placement(node),
1850
+ ...node.opacity !== void 0 ? { opacity: node.opacity } : {},
1851
+ children: vectorAppearanceNodes(node)
1852
+ };
1853
+ }
1854
+ }
1855
+ expandCache.set(node, out);
1856
+ return out;
1857
+ }
1858
+ function expandVectorAppearances(nodes) {
1859
+ let changed = false;
1860
+ const out = nodes.map((n) => {
1861
+ const e = expandVectorAppearance(n);
1862
+ if (e !== n) changed = true;
1863
+ return e;
1864
+ });
1865
+ return changed ? out : nodes;
1866
+ }
1867
+ function sanitizeVectorEffects(value) {
1868
+ if (!Array.isArray(value)) return [];
1869
+ const out = [];
1870
+ const used = /* @__PURE__ */ new Set();
1871
+ for (const raw of value) {
1872
+ if (!raw || typeof raw !== "object") continue;
1873
+ const r = raw;
1874
+ const type = r.type;
1875
+ if (typeof type !== "string" || !VECTOR_EFFECT_DEFAULTS[type]) continue;
1876
+ let id = typeof r.id === "string" && r.id && !used.has(r.id) ? r.id : "";
1877
+ for (let k = out.length + 1; !id; k++) if (!used.has(`fx${k}`)) id = `fx${k}`;
1878
+ used.add(id);
1879
+ const effect = createVectorEffect(type, id, r.params ?? {});
1880
+ out.push(r.enabled === false ? { ...effect, enabled: false } : effect);
1881
+ }
1882
+ return out;
1883
+ }
1884
+ function transformVectorEffects(effects, m) {
1885
+ const lin = (v2) => ({ x: m[0] * v2.x + m[2] * v2.y, y: m[1] * v2.x + m[3] * v2.y });
1886
+ const mirror = m[0] * m[3] - m[1] * m[2] < 0 ? -1 : 1;
1887
+ return effects.map((e) => {
1888
+ if (e.type === "twist") return { ...e, params: { ...e.params, center: lin(e.params.center), angle: e.params.angle * mirror } };
1889
+ if (e.type === "repeater")
1890
+ return { ...e, params: { ...e.params, position: lin(e.params.position), anchor: lin(e.params.anchor), rotation: e.params.rotation * mirror } };
1891
+ return e;
1892
+ });
1893
+ }
1894
+ function vectorEnteredGroups(nodes, ids) {
1895
+ const out = /* @__PURE__ */ new Set();
1896
+ if (!ids.length) return out;
1897
+ const want = new Set(ids);
1898
+ const visit = (list, live) => {
1899
+ for (const n of list) {
1900
+ const own = n.type === "group" && hasOwnVectorAppearance(n);
1901
+ if (want.has(n.id)) {
1902
+ for (const id of live) out.add(id);
1903
+ if (own) out.add(n.id);
1904
+ }
1905
+ if (n.type === "group") visit(n.children, own ? [...live, n.id] : live);
1906
+ }
1907
+ };
1908
+ visit(nodes, []);
1909
+ return out;
1910
+ }
1911
+
1912
+ // src/vector/geometry.ts
1913
+ var EPS3 = 1e-9;
1047
1914
  var cloneAnchor = (a) => ({
1048
1915
  x: a.x,
1049
1916
  y: a.y,
@@ -1068,7 +1935,7 @@ function hasPaint(paint) {
1068
1935
  return true;
1069
1936
  }
1070
1937
  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;
1938
+ var isOnAnchor = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
1072
1939
  function subpathSegmentCount(sp) {
1073
1940
  const n = sp.anchors.length;
1074
1941
  if (n < 2) return 0;
@@ -1100,7 +1967,7 @@ function subpathsToBezier(subpaths, options = {}) {
1100
1967
  return out;
1101
1968
  }
1102
1969
  function vectorNodeToBezier(node) {
1103
- return node.type === "path" ? subpathsToBezier(node.subpaths) : [];
1970
+ return node.type === "path" ? vectorAppearanceOutline(node) : [];
1104
1971
  }
1105
1972
  function vectorPathData(input, precision = 3) {
1106
1973
  const subpaths = Array.isArray(input) ? input : input.subpaths;
@@ -1214,12 +2081,13 @@ function collectPathNodes(node) {
1214
2081
  function nodeMatrix(node) {
1215
2082
  return node.transform ?? IDENTITY_MATRIX;
1216
2083
  }
1217
- function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX) {
2084
+ function vectorPathWorldBezier(node, parent = IDENTITY_MATRIX, options = {}) {
1218
2085
  const m = multiplyMatrices(nodeMatrix(node), parent);
1219
- const path = subpathsToBezier(node.subpaths);
2086
+ const path = options.effects === false || !hasOwnVectorAppearance(node) ? subpathsToBezier(node.subpaths) : vectorAppearanceOutline(node);
1220
2087
  return isIdentityMatrix(m) ? path : transformBezierPath(path, m);
1221
2088
  }
1222
2089
  function vectorNodeBounds(node, parent = IDENTITY_MATRIX) {
2090
+ node = expandVectorAppearance(node);
1223
2091
  if (node.type === "group") {
1224
2092
  const m = multiplyMatrices(nodeMatrix(node), parent);
1225
2093
  return unionBounds(node.children.map((c) => vectorNodeBounds(c, m)));
@@ -1249,11 +2117,12 @@ function boundsIntersect(a, b) {
1249
2117
  function boundsFromPoints(a, b) {
1250
2118
  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
2119
  }
1252
- function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX) {
2120
+ function hitTestVectorNode(node, p, tolerance = 0, parent = IDENTITY_MATRIX, options = {}) {
1253
2121
  if (node.visible === false) return false;
2122
+ if (!(node.type === "group" && (!hasOwnVectorAppearance(node) || options.entered?.has(node.id)))) node = expandVectorAppearance(node);
1254
2123
  if (node.type === "group") {
1255
2124
  const m = multiplyMatrices(nodeMatrix(node), parent);
1256
- return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m));
2125
+ return node.children.some((c) => hitTestVectorNode(c, p, tolerance, m, options));
1257
2126
  }
1258
2127
  const path = vectorPathWorldBezier(node, parent);
1259
2128
  if (path.length === 0) return false;
@@ -1266,11 +2135,11 @@ function vectorStrokeOutline(node, parent = IDENTITY_MATRIX) {
1266
2135
  if (!hasPaint(node.stroke) || !((node.strokeWidth ?? 1) > 0)) return [];
1267
2136
  return vectorStrokeOutlinePath(vectorPathWorldBezier(node, parent), node);
1268
2137
  }
1269
- function hitTestVectorNodes(nodes, p, tolerance = 0) {
2138
+ function hitTestVectorNodes(nodes, p, tolerance = 0, options = {}) {
1270
2139
  for (let i = nodes.length - 1; i >= 0; i--) {
1271
2140
  const node = nodes[i];
1272
2141
  if (node.visible === false || node.locked) continue;
1273
- if (hitTestVectorNode(node, p, tolerance)) return node;
2142
+ if (hitTestVectorNode(node, p, tolerance, IDENTITY_MATRIX, options)) return node;
1274
2143
  }
1275
2144
  return null;
1276
2145
  }
@@ -1349,6 +2218,7 @@ function mapAnchor(a, m) {
1349
2218
  }
1350
2219
  function bakeIntoGeometry(node, m) {
1351
2220
  if (isIdentityMatrix(m)) return node;
2221
+ if (node.effects?.length) node = { ...node, effects: transformVectorEffects(node.effects, m) };
1352
2222
  if (node.type === "group") {
1353
2223
  const group = node;
1354
2224
  const children = group.children.map((c) => transformVectorNode(c, m));
@@ -1416,7 +2286,7 @@ function moveAnchorHandle(a, side, pos, breakHandles = false) {
1416
2286
  const dy = pos.y - a.y;
1417
2287
  const len2 = Math.hypot(dx, dy);
1418
2288
  if (type === "symmetric") next[other] = { x: a.x - dx, y: a.y - dy };
1419
- else if (type === "smooth" && len2 > EPS2) {
2289
+ else if (type === "smooth" && len2 > EPS3) {
1420
2290
  const o = a[other];
1421
2291
  const olen = o ? Math.hypot(o.x - a.x, o.y - a.y) : len2;
1422
2292
  next[other] = { x: a.x - dx / len2 * olen, y: a.y - dy / len2 * olen };
@@ -1446,15 +2316,15 @@ function convertAnchor(sp, index, type) {
1446
2316
  else if (prev && next) dir = { x: next.x - prev.x, y: next.y - prev.y };
1447
2317
  else if (next) dir = { x: next.x - a.x, y: next.y - a.y };
1448
2318
  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 };
2319
+ if (!dir || Math.hypot(dir.x, dir.y) < EPS3) return { anchors, closed: sp.closed };
1450
2320
  const dl = Math.hypot(dir.x, dir.y);
1451
2321
  const ux = dir.x / dl;
1452
2322
  const uy = dir.y / dl;
1453
2323
  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
2324
  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
2325
  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 };
2326
+ if (lenIn > EPS3) a.in = { x: a.x - ux * lenIn, y: a.y - uy * lenIn };
2327
+ if (lenOut > EPS3) a.out = { x: a.x + ux * lenOut, y: a.y + uy * lenOut };
1458
2328
  a.type = type;
1459
2329
  return { anchors, closed: sp.closed };
1460
2330
  }
@@ -1674,13 +2544,33 @@ function vectorDocumentToSvg(doc, options = {}) {
1674
2544
  if (node.visible === false) out2 += ` display="none"`;
1675
2545
  if (node.blendStep) out2 += ` data-vector-blend-step=""`;
1676
2546
  if (node.type === "group" && node.blend) out2 += ` data-vector-blend="${escapeAttr(JSON.stringify(node.blend))}"`;
2547
+ if (node.booleanOperation) out2 += ` data-vector-boolean-operation="${node.booleanOperation}"`;
1677
2548
  return out2;
1678
2549
  };
1679
- const emit = (node, depth) => {
2550
+ const emit2 = (node, depth) => {
1680
2551
  if (node.visible === false && !options.includeHidden) return;
2552
+ if (hasOwnVectorAppearance(node)) {
2553
+ const live2 = {
2554
+ kind: node.type,
2555
+ ...node.effects?.length ? { effects: node.effects } : {},
2556
+ ...node.type === "group" && node.liveBoolean ? { liveBoolean: node.liveBoolean } : {}
2557
+ };
2558
+ lines.push(`${indent(depth)}<g${common(node)} data-vector-live="${escapeAttr(JSON.stringify(live2))}">`);
2559
+ lines.push(`${indent(depth + 1)}<g data-vector-part="appearance">`);
2560
+ for (const shape of vectorAppearanceNodes(node)) emit2(shape, depth + 2);
2561
+ lines.push(`${indent(depth + 1)}</g>`);
2562
+ lines.push(`${indent(depth + 1)}<g data-vector-part="source" display="none">`);
2563
+ if (node.type === "path") {
2564
+ const { effects: _e, transform: _t, opacity: _o, visible: _v, booleanOperation: _b, blendStep: _s, ...source } = node;
2565
+ emit2({ ...source, id: `${node.id}~src` }, depth + 2);
2566
+ } else for (const child of node.children) emit2(child, depth + 2);
2567
+ lines.push(`${indent(depth + 1)}</g>`);
2568
+ lines.push(`${indent(depth)}</g>`);
2569
+ return;
2570
+ }
1681
2571
  if (node.type === "group") {
1682
2572
  lines.push(`${indent(depth)}<g${common(node)}>`);
1683
- for (const child of node.children) emit(child, depth + 1);
2573
+ for (const child of node.children) emit2(child, depth + 1);
1684
2574
  lines.push(`${indent(depth)}</g>`);
1685
2575
  return;
1686
2576
  }
@@ -1744,7 +2634,7 @@ function vectorDocumentToSvg(doc, options = {}) {
1744
2634
  }
1745
2635
  lines.push(`${indent(depth)}</g>`);
1746
2636
  };
1747
- for (const node of doc.nodes) emit(node, 1);
2637
+ for (const node of doc.nodes) emit2(node, 1);
1748
2638
  const w = num(doc.width);
1749
2639
  const h = num(doc.height);
1750
2640
  const nl = pretty ? "\n" : "";
@@ -1756,6 +2646,7 @@ function vectorDocumentToSvg(doc, options = {}) {
1756
2646
  out.push(...lines, "</svg>");
1757
2647
  return out.join(nl);
1758
2648
  }
2649
+ var BOOLEAN_OPERATIONS = ["union", "subtract", "intersect", "exclude"];
1759
2650
  function parseXml(source) {
1760
2651
  const text = source.replace(/<!--[\s\S]*?-->/g, "").replace(/<\?[\s\S]*?\?>/g, "").replace(/<!\[CDATA\[[\s\S]*?\]\]>/g, "").replace(/<!DOCTYPE[^>[]*(\[[\s\S]*?\])?\s*>/gi, "");
1761
2652
  const root = { name: "#root", attrs: {}, children: [] };
@@ -2030,6 +2921,34 @@ function parseVectorSvg(svg, options = {}) {
2030
2921
  };
2031
2922
  return placed(node, m);
2032
2923
  };
2924
+ const liveNode = (g, style, m, meta) => {
2925
+ let raw = {};
2926
+ try {
2927
+ raw = JSON.parse(g.attrs["data-vector-live"]) ?? {};
2928
+ } catch {
2929
+ raw = {};
2930
+ }
2931
+ const sourceEl = g.children.find((c) => c.name === "g" && c.attrs["data-vector-part"] === "source");
2932
+ if (!sourceEl) return null;
2933
+ const inner = visit(sourceEl, style, m);
2934
+ const effects = sanitizeVectorEffects(raw.effects);
2935
+ const liveBoolean = BOOLEAN_OPERATIONS.includes(raw.liveBoolean) ? raw.liveBoolean : null;
2936
+ const id = idFor(g);
2937
+ if (raw.kind === "path") {
2938
+ const path = inner.find((n) => n.type === "path");
2939
+ if (!path) return null;
2940
+ const { residual } = decomposeRigid(m);
2941
+ const { visible: _v, ...rest } = path;
2942
+ return { ...rest, id, ...meta, ...effects.length ? { effects: transformVectorEffects(effects, residual) } : {} };
2943
+ }
2944
+ if (!inner.length) return null;
2945
+ const group = { id, type: "group", ...meta, children: inner };
2946
+ if (effects.length) group.effects = transformVectorEffects(effects, m);
2947
+ if (liveBoolean) group.liveBoolean = liveBoolean;
2948
+ const blend = blendFromAttribute(g.attrs["data-vector-blend"], m);
2949
+ if (blend) group.blend = blend;
2950
+ return group;
2951
+ };
2033
2952
  const visit = (el, style, matrix) => {
2034
2953
  const out = [];
2035
2954
  for (const child of el.children) {
@@ -2044,6 +2963,13 @@ function parseVectorSvg(svg, options = {}) {
2044
2963
  ...hidden ? { visible: false } : {}
2045
2964
  };
2046
2965
  if (child.attrs["data-vector-blend-step"] !== void 0) continue;
2966
+ const operation = child.attrs["data-vector-boolean-operation"];
2967
+ if (BOOLEAN_OPERATIONS.includes(operation)) meta.booleanOperation = operation;
2968
+ if (child.name === "g" && child.attrs["data-vector-live"] !== void 0) {
2969
+ const node2 = liveNode(child, s, m, meta);
2970
+ if (node2) out.push(node2);
2971
+ continue;
2972
+ }
2047
2973
  if (child.name === "g" && child.attrs["data-vector-path"] !== void 0) {
2048
2974
  const node2 = mergedPath(child, s, m, meta);
2049
2975
  if (node2) out.push(node2);
@@ -3197,8 +4123,9 @@ var VectorSnapTargets = class _VectorSnapTargets {
3197
4123
  let anchors = BUDGET;
3198
4124
  let segments = BUDGET;
3199
4125
  const visit = (list, parent) => {
3200
- for (const n of list) {
3201
- if (exclude.has(n.id) || n.visible === false) continue;
4126
+ for (const stored of list) {
4127
+ if (exclude.has(stored.id) || stored.visible === false) continue;
4128
+ const n = contains(stored, exclude) ? stored : expandVectorAppearance(stored);
3202
4129
  const m = multiplyMatrices(nodeMatrix(n), parent);
3203
4130
  if (n.type === "group") {
3204
4131
  visit(n.children, m);
@@ -3736,8 +4663,8 @@ function solveOnSegment(seg, base, cond) {
3736
4663
  }
3737
4664
 
3738
4665
  // src/vector/pathEdit.ts
3739
- var EPS3 = 1e-9;
3740
- var unit2 = (x, y) => {
4666
+ var EPS4 = 1e-9;
4667
+ var unit3 = (x, y) => {
3741
4668
  const l = Math.hypot(x, y);
3742
4669
  return l > 1e-12 && Number.isFinite(l) ? [x / l, y / l] : null;
3743
4670
  };
@@ -3748,16 +4675,16 @@ function cubicOf(sp, i) {
3748
4675
  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
4676
  }
3750
4677
  var isLine = (sp, i) => subpathSegment(sp, i).length === 2;
3751
- function startTangent(c) {
4678
+ function startTangent2(c) {
3752
4679
  for (let k = 1; k < 4; k++) {
3753
- const u = unit2(c[k][0] - c[0][0], c[k][1] - c[0][1]);
4680
+ const u = unit3(c[k][0] - c[0][0], c[k][1] - c[0][1]);
3754
4681
  if (u) return u;
3755
4682
  }
3756
4683
  return [1, 0];
3757
4684
  }
3758
- function endTangent(c) {
4685
+ function endTangent2(c) {
3759
4686
  for (let k = 2; k >= 0; k--) {
3760
- const u = unit2(c[3][0] - c[k][0], c[3][1] - c[k][1]);
4687
+ const u = unit3(c[3][0] - c[k][0], c[3][1] - c[k][1]);
3761
4688
  if (u) return u;
3762
4689
  }
3763
4690
  return [1, 0];
@@ -3765,7 +4692,7 @@ function endTangent(c) {
3765
4692
  function sample(c, n, out, withStart) {
3766
4693
  for (let i = withStart ? 0 : 1; i <= n; i++) out.push(segmentPoint(c, i / n));
3767
4694
  }
3768
- var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS3 && Math.abs(h.y - a.y) < EPS3;
4695
+ var onAnchor2 = (a, h) => !h || Math.abs(h.x - a.x) < EPS4 && Math.abs(h.y - a.y) < EPS4;
3769
4696
  function tidy(a, keepType = true) {
3770
4697
  const next = cloneAnchor(a);
3771
4698
  if (onAnchor2(next, next.in)) delete next.in;
@@ -3827,8 +4754,8 @@ function removeAnchorKeepingShape(sp, index) {
3827
4754
  const pts = [];
3828
4755
  sample(left, N, pts, true);
3829
4756
  sample(right, N, pts, false);
3830
- const t0 = startTangent(left);
3831
- const t1 = endTangent(right);
4757
+ const t0 = startTangent2(left);
4758
+ const t1 = endTangent2(right);
3832
4759
  const { curve } = fitAcross(pts, N, t0, t1);
3833
4760
  if ([curve.p1, curve.p2].every((p) => Number.isFinite(p[0]) && Number.isFinite(p[1]))) {
3834
4761
  anchors[pi] = tidy({ ...anchors[pi], out: { x: curve.p1[0], y: curve.p1[1] } });
@@ -4029,7 +4956,7 @@ function smoothSubpath(sp, amount = 1, only) {
4029
4956
  const prev = orig[(i + n - 1) % n];
4030
4957
  const next = orig[(i + 1) % n];
4031
4958
  const p = orig[i];
4032
- const dir = unit2(next.x - prev.x, next.y - prev.y);
4959
+ const dir = unit3(next.x - prev.x, next.y - prev.y);
4033
4960
  if (!dir) continue;
4034
4961
  const li = dist2(p, prev) / 3;
4035
4962
  const lo = dist2(p, next) / 3;
@@ -4095,13 +5022,13 @@ function mergePair(a, b, tol) {
4095
5022
  }
4096
5023
  const ca = a.line ? [a0, a0, a3, a3] : a.c;
4097
5024
  const cb = b.line ? [a3, a3, b3, b3] : b.c;
4098
- const ta = endTangent(ca);
4099
- const tb = startTangent(cb);
5025
+ const ta = endTangent2(ca);
5026
+ const tb = startTangent2(cb);
4100
5027
  if (ta[0] * tb[0] + ta[1] * tb[1] < 0.99) return null;
4101
5028
  const pts = [];
4102
5029
  sample(ca, 16, pts, true);
4103
5030
  sample(cb, 16, pts, false);
4104
- const { curve, error } = fitAcross(pts, 16, startTangent(ca), endTangent(cb));
5031
+ const { curve, error } = fitAcross(pts, 16, startTangent2(ca), endTangent2(cb));
4105
5032
  return error <= tol ? { c: [curve.p0, curve.p1, curve.p2, curve.p3], line: false } : null;
4106
5033
  }
4107
5034
  function removeRedundantPoints(sp, tolerance) {
@@ -4233,14 +5160,14 @@ function lineHits(p, q, seg) {
4233
5160
  return out;
4234
5161
  }
4235
5162
  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));
5163
+ var degenerate2 = (seg, eps) => seg.every((p) => near(p, seg[0], eps));
4237
5164
  function arc(l, a, b, eps) {
4238
5165
  const n = l.segs.length;
4239
5166
  const segs = [];
4240
5167
  const chord = [];
4241
5168
  const push = (k, t0, t1) => {
4242
5169
  const piece = t0 <= 0 && t1 >= 1 ? l.segs[k].slice() : subSegment(l.segs[k], t0, t1);
4243
- if (!degenerate(piece, eps)) {
5170
+ if (!degenerate2(piece, eps)) {
4244
5171
  segs.push(piece);
4245
5172
  chord.push(l.chord[k]);
4246
5173
  }
@@ -4698,8 +5625,9 @@ function recognizeInkShape(points, options = {}) {
4698
5625
  // src/vector/builder.ts
4699
5626
  function vectorPathEntries(doc, roots, options = {}) {
4700
5627
  const out = [];
4701
- const visit = (node, parent, inside) => {
4702
- if (node.visible === false || node.locked && !options.locked) return;
5628
+ const visit = (stored, parent, inside) => {
5629
+ if (stored.visible === false || stored.locked && !options.locked) return;
5630
+ const node = options.expand === false ? stored : expandVectorAppearance(stored);
4703
5631
  const picked = inside || roots === null || roots.includes(node.id);
4704
5632
  if (node.type === "group") {
4705
5633
  const m = multiplyMatrices(nodeMatrix(node), parent);
@@ -4753,7 +5681,7 @@ var STROKE_KEYS = [
4753
5681
  "strokeWidthProfile"
4754
5682
  ];
4755
5683
  function vectorResultStyle(node, paint, edge = false) {
4756
- const { id: _id, subpaths: _s, transform: _t, ...rest } = node;
5684
+ const { id: _id, subpaths: _s, transform: _t, effects: _e, booleanOperation: _b, ...rest } = node;
4757
5685
  const base = { ...rest, fillRule: "nonzero" };
4758
5686
  if (edge) {
4759
5687
  for (const k of STROKE_KEYS) delete base[k];
@@ -5281,6 +6209,12 @@ function spliceTree(nodes, id, replacements) {
5281
6209
  return changed ? out : nodes;
5282
6210
  }
5283
6211
  var toPoint2 = (p) => [p.x, p.y];
6212
+ var placementOf = (n) => ({
6213
+ ...n.transform ? { transform: n.transform } : {},
6214
+ ...n.visible === false ? { visible: false } : {},
6215
+ ...n.locked ? { locked: true } : {},
6216
+ ...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}
6217
+ });
5284
6218
  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
6219
  var idCounter = 0;
5286
6220
  var defaultCreateId = () => `vn-${Date.now().toString(36)}${(++idCounter).toString(36)}`;
@@ -5505,8 +6439,11 @@ var VectorEditorController = class {
5505
6439
  }
5506
6440
  this.notify(docChanged);
5507
6441
  }
5508
- /** Records the change from `before` to the current state as one undo step. */
5509
- commit(name, before) {
6442
+ /**
6443
+ * Records the change from `before` to the current state as one undo step. Consecutive
6444
+ * changes with the same `merge` key (a slider drag) fold into one step.
6445
+ */
6446
+ commit(name, before, merge) {
5510
6447
  this.doc = this.refreshed(this.doc);
5511
6448
  const after = this.snapshot();
5512
6449
  if (before.doc === after.doc) {
@@ -5517,17 +6454,23 @@ var VectorEditorController = class {
5517
6454
  name,
5518
6455
  execute: () => this.restore(after),
5519
6456
  undo: () => this.restore(before),
5520
- redo: () => this.restore(after)
6457
+ redo: () => this.restore(after),
6458
+ ...merge ? { tag: merge } : {}
5521
6459
  };
5522
6460
  this.committing = true;
5523
- this.history.execute(command);
6461
+ if (merge) this.history.executeMerged(command, { key: merge });
6462
+ else this.history.execute(command);
5524
6463
  this.committing = false;
5525
6464
  }
5526
- /** Runs `fn` (which updates fields) and records it as one undo step. */
5527
- change(name, fn) {
6465
+ /** Runs `fn` (which updates fields) and records it as one undo step (merged by `merge` key). */
6466
+ change(name, fn, merge) {
5528
6467
  const before = this.snapshot();
5529
6468
  fn();
5530
- this.commit(name, before);
6469
+ this.commit(name, before, merge);
6470
+ }
6471
+ /** Ends a merged edit (a slider released): the next change starts a new undo step. */
6472
+ endMergedEdit() {
6473
+ this.history.endGesture();
5531
6474
  }
5532
6475
  setDoc(nodes) {
5533
6476
  if (nodes !== this.doc.nodes) this.doc = { ...this.doc, nodes };
@@ -6077,7 +7020,7 @@ var VectorEditorController = class {
6077
7020
  const created = [];
6078
7021
  const replace = /* @__PURE__ */ new Map();
6079
7022
  for (const p of paths) {
6080
- const outline = bezierToSubpaths(vectorStrokeOutlinePath(subpathsToBezier(p.subpaths), p));
7023
+ const outline = bezierToSubpaths(vectorStrokeOutlinePath(vectorAppearanceOutline(p), p));
6081
7024
  if (!outline.length) continue;
6082
7025
  const {
6083
7026
  stroke,
@@ -6092,11 +7035,12 @@ var VectorEditorController = class {
6092
7035
  startArrowScale: _ss,
6093
7036
  endArrowScale: _es,
6094
7037
  strokeWidthProfile: _wp,
7038
+ effects,
6095
7039
  ...rest
6096
7040
  } = cloneVectorNode(p);
6097
7041
  const id = this.newId();
6098
7042
  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];
7043
+ const list = hasPaint(p.fill) ? [{ ...rest, ...effects ? { effects } : {}, stroke: null }, strokeNode] : [strokeNode];
6100
7044
  created.push(id);
6101
7045
  replace.set(p.id, list);
6102
7046
  }
@@ -6111,6 +7055,121 @@ var VectorEditorController = class {
6111
7055
  });
6112
7056
  return created;
6113
7057
  }
7058
+ // -------------------------------------------------------------------------
7059
+ // Path effects
7060
+ // -------------------------------------------------------------------------
7061
+ /** Selected (or given) nodes that can carry effects: the roots of the selection. */
7062
+ effectTargets(ids) {
7063
+ return (ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n) => !!n && !n.blendStep);
7064
+ }
7065
+ /**
7066
+ * Adds an effect of `type` to the selected (or given) nodes, at the bottom of each stack (or
7067
+ * at `index`), with default parameters merged with `params`. A repeater's copies start one
7068
+ * shape width apart. One undo step; returns the new effect ids (one per node, in order).
7069
+ */
7070
+ addEffect(type, options = {}) {
7071
+ const targets = this.effectTargets(options.ids);
7072
+ if (!targets.length || !createVectorEffect(type, "")) return [];
7073
+ const created = [];
7074
+ const next = /* @__PURE__ */ new Map();
7075
+ for (const node of targets) {
7076
+ const list = node.effects ?? [];
7077
+ let k = list.length + 1;
7078
+ while (list.some((e) => e.id === `fx${k}`)) k++;
7079
+ const params = { ...options.params ?? {} };
7080
+ if (type === "repeater" && params.position === void 0) {
7081
+ const b = bezierPathBounds(vectorAppearanceOutline(node));
7082
+ if (b && b.maxX - b.minX > 0) params.position = { x: Math.round((b.maxX - b.minX) * 1.25 * 100) / 100, y: 0 };
7083
+ }
7084
+ const effect = createVectorEffect(type, `fx${k}`, params);
7085
+ const at2 = options.index === void 0 ? list.length : Math.min(list.length, Math.max(0, Math.round(options.index)));
7086
+ next.set(node.id, { ...node, effects: [...list.slice(0, at2), effect, ...list.slice(at2)] });
7087
+ created.push(effect.id);
7088
+ }
7089
+ this.change(`Add ${VECTOR_EFFECT_LABELS[type]}`, () => this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n)));
7090
+ return created;
7091
+ }
7092
+ editEffects(nodeId, label, fn, merge) {
7093
+ const node = this.getNode(nodeId);
7094
+ if (!node) return { ok: false, message: `There is no node "${nodeId}".` };
7095
+ const result = fn(node.effects ?? []);
7096
+ if (typeof result === "string") return { ok: false, message: result };
7097
+ this.change(label, () => this.replaceNode(nodeId, result.length ? { ...node, effects: result } : (({ effects: _e, ...rest }) => rest)(node)), merge);
7098
+ return { ok: true };
7099
+ }
7100
+ /**
7101
+ * Turns an effect on or off and changes its parameters (unknown or invalid values are
7102
+ * ignored). With `merge` (true, or a key of your own), consecutive updates of the same effect
7103
+ * fold into one undo step, as a slider drag should; call `endMergedEdit` when it ends.
7104
+ */
7105
+ updateEffect(nodeId, effectId, patch, options = {}) {
7106
+ const merge = options.merge === true ? `effect:${nodeId}:${effectId}` : options.merge || void 0;
7107
+ const node = this.getNode(nodeId);
7108
+ const current = node?.effects?.find((e) => e.id === effectId);
7109
+ const label = current ? VECTOR_EFFECT_LABELS[current.type] ?? "Effect" : "Effect";
7110
+ return this.editEffects(
7111
+ nodeId,
7112
+ `Edit ${label}`,
7113
+ (list) => {
7114
+ const i = list.findIndex((e2) => e2.id === effectId);
7115
+ if (i < 0) return `Node "${nodeId}" has no effect "${effectId}".`;
7116
+ const e = list[i];
7117
+ const params = patch.params ? normalizeVectorEffectParams(e.type, patch.params, e.params) : e.params;
7118
+ const next = { ...e, params, ...patch.enabled !== void 0 ? { enabled: patch.enabled } : {} };
7119
+ if (next.enabled !== false) delete next.enabled;
7120
+ return list.map((x, j) => j === i ? next : x);
7121
+ },
7122
+ merge
7123
+ );
7124
+ }
7125
+ /** Removes an effect from a node's stack. */
7126
+ removeEffect(nodeId, effectId) {
7127
+ 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}".`);
7128
+ }
7129
+ /** Moves an effect to position `index` of its stack (0 is applied first). */
7130
+ moveEffect(nodeId, effectId, index) {
7131
+ return this.editEffects(nodeId, "Reorder Effects", (list) => {
7132
+ const i = list.findIndex((e2) => e2.id === effectId);
7133
+ if (i < 0) return `Node "${nodeId}" has no effect "${effectId}".`;
7134
+ const to = Math.min(list.length - 1, Math.max(0, Math.round(index)));
7135
+ const out = list.slice();
7136
+ const [e] = out.splice(i, 1);
7137
+ out.splice(to, 0, e);
7138
+ return out;
7139
+ });
7140
+ }
7141
+ /**
7142
+ * Expand Appearance: bakes the path effects and live booleans of the selected (or given)
7143
+ * nodes, and of everything inside them, into plain paths. A node keeps its id (a group when
7144
+ * it draws as several shapes); the shapes it made get new ids. Returns the ids of the expanded
7145
+ * nodes (empty when none was live).
7146
+ */
7147
+ expandAppearance(options = {}) {
7148
+ const roots = (options.ids ?? this.selectedRoots()).map((id) => this.getNode(id)).filter((n) => !!n && hasVectorAppearance(n));
7149
+ if (!roots.length) return [];
7150
+ const fresh = (n) => {
7151
+ const id = n.id.includes("~fx") ? this.newId() : n.id;
7152
+ return n.type === "group" ? { ...n, id, children: n.children.map(fresh) } : { ...n, id };
7153
+ };
7154
+ const bake = (n) => {
7155
+ if (!hasVectorAppearance(n)) return n;
7156
+ if (!hasOwnVectorAppearance(n)) return { ...n, children: n.children.map(bake) };
7157
+ let e = expandVectorAppearance(n);
7158
+ if (e.type === "group" && e.children.length === 1 && e.children[0].type === "path") {
7159
+ const { id: _id, opacity, name, ...child } = e.children[0];
7160
+ const k = (e.opacity ?? 1) * (opacity ?? 1);
7161
+ e = { ...child, id: e.id, name: e.name ?? name, ...placementOf(e), ...k < 1 ? { opacity: k } : {} };
7162
+ }
7163
+ return fresh(e);
7164
+ };
7165
+ const next = new Map(roots.map((n) => [n.id, bake(n)]));
7166
+ this.change("Expand Appearance", () => {
7167
+ this.setDoc(mapTree(this.doc.nodes, (n) => next.get(n.id) ?? n));
7168
+ this.anchors = [];
7169
+ this.editingId = null;
7170
+ });
7171
+ return roots.map((n) => n.id);
7172
+ }
6114
7173
  rename(id, name) {
6115
7174
  return this.updateNode(id, { name }, { label: "Rename" });
6116
7175
  }
@@ -6402,7 +7461,7 @@ var VectorEditorController = class {
6402
7461
  const opacity = n.opacity;
6403
7462
  const m = nodeMatrix(n);
6404
7463
  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)
7464
+ ...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
7465
  );
6407
7466
  } else if (n.type === "group") {
6408
7467
  const children = flatten2(n.children);
@@ -6439,34 +7498,33 @@ var VectorEditorController = class {
6439
7498
  /**
6440
7499
  * Combines the selected paths (two or more) into one with a boolean operation. `subtract`
6441
7500
  * removes the upper shapes from the lowest one. The result takes the lowest shape's style
6442
- * and place.
7501
+ * and place. Shapes take part as drawn (path effects and live booleans applied).
7502
+ *
7503
+ * With `live`, the operands stay editable instead: a lone live boolean group gets the
7504
+ * operation (its operands follow it again), operands of live boolean groups get it as their
7505
+ * own, and anything else (two or more nodes with one parent) is wrapped in a new live boolean
7506
+ * group. Returns the result's (or the live group's) id, or null.
6443
7507
  */
6444
7508
  booleanOp(op, options = {}) {
6445
7509
  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;
7510
+ if (options.live) return this.liveBooleanOp(op, roots);
7511
+ const entries = vectorPathEntries(this.doc, roots, { locked: true });
7512
+ if (entries.length < 2) return null;
6452
7513
  const result = pathBoolean(
6453
7514
  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"
7515
+ entries.map((e) => ({
7516
+ path: vectorPathWorldBezier({ ...e.node, subpaths: e.node.subpaths.filter((s) => s.anchors.length > 1).map((s) => ({ ...s, closed: true })) }, e.parent),
7517
+ fillRule: e.node.fillRule ?? "nonzero"
6460
7518
  }))
6461
7519
  );
6462
- const base = paths[0];
6463
- const anchorRoot = roots.find((r) => findVectorNode([this.getNode(r)], base.id)) ?? base.id;
7520
+ const base = entries[0].node;
7521
+ const anchorRoot = roots.find((r) => this.holds(r, base.id)) ?? base.id;
6464
7522
  const subpaths = bezierToSubpaths(this.intoParentSpace(anchorRoot, result.path));
6465
7523
  if (result.error || !subpaths.length) return null;
6466
7524
  const id = this.newId();
6467
- const { transform: _t, ...baseRest } = cloneVectorNode(base);
7525
+ const { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(base);
6468
7526
  const node = { ...baseRest, id, name: BOOLEAN_LABELS[op], subpaths };
6469
- const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
7527
+ const drop = /* @__PURE__ */ new Set([...entries.map((e) => e.node.id), ...roots]);
6470
7528
  this.change(BOOLEAN_LABELS[op], () => {
6471
7529
  this.setDoc(mapTree(this.doc.nodes, (n) => n.id === anchorRoot ? node : drop.has(n.id) ? null : n));
6472
7530
  this.selection = [id];
@@ -6475,18 +7533,75 @@ var VectorEditorController = class {
6475
7533
  });
6476
7534
  return id;
6477
7535
  }
7536
+ /** Whether stored node `rootId` holds `id` as drawn (`id` may be an expanded path inside it). */
7537
+ holds(rootId, id) {
7538
+ const n = this.getNode(rootId);
7539
+ return !!n && (!!findVectorNode([n], id) || !!findVectorNode([expandVectorAppearance(n)], id));
7540
+ }
7541
+ liveBooleanOp(op, roots) {
7542
+ const nodes = roots.map((id2) => this.getNode(id2));
7543
+ if (!nodes.length || nodes.some((n) => !n)) return null;
7544
+ const label = `Live ${BOOLEAN_LABELS[op]}`;
7545
+ const lone = nodes.length === 1 ? nodes[0] : null;
7546
+ if (isVectorLiveBoolean(lone)) {
7547
+ const children = lone.children.map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c);
7548
+ this.change(label, () => this.replaceNode(lone.id, { ...lone, liveBoolean: op, children }));
7549
+ return lone.id;
7550
+ }
7551
+ const parents = roots.map((id2) => locateVectorNode(this.doc.nodes, id2)?.parent ?? null);
7552
+ if (parents.every((g) => isVectorLiveBoolean(g))) {
7553
+ const set = new Set(roots);
7554
+ this.change(
7555
+ label,
7556
+ () => this.setDoc(
7557
+ mapTree(this.doc.nodes, (n) => {
7558
+ if (n.type !== "group" || !n.liveBoolean || !n.children.some((c) => set.has(c.id))) return n;
7559
+ const group = n;
7560
+ const children = group.children.map((c) => {
7561
+ if (!set.has(c.id)) return c;
7562
+ const { booleanOperation: _b, ...rest } = c;
7563
+ return op === group.liveBoolean ? rest : { ...rest, booleanOperation: op };
7564
+ });
7565
+ return { ...group, children };
7566
+ })
7567
+ )
7568
+ );
7569
+ return parents[0].id;
7570
+ }
7571
+ if (nodes.length < 2) return null;
7572
+ const parent = parents[0]?.id ?? null;
7573
+ if (parents.some((g) => (g?.id ?? null) !== parent)) return null;
7574
+ const id = this.newId();
7575
+ this.change(label, () => {
7576
+ const set = new Set(roots);
7577
+ const wrap = (list) => {
7578
+ const members = list.filter((n) => set.has(n.id)).map((c) => c.booleanOperation ? (({ booleanOperation: _b, ...rest }) => rest)(c) : c);
7579
+ const top = Math.max(...list.map((n, i) => set.has(n.id) ? i : -1));
7580
+ const out = [];
7581
+ list.forEach((n, i) => {
7582
+ if (i === top) out.push({ id, type: "group", name: BOOLEAN_LABELS[op], liveBoolean: op, children: members });
7583
+ if (!set.has(n.id)) out.push(n);
7584
+ });
7585
+ return out;
7586
+ };
7587
+ if (parent === null) this.setDoc(wrap(this.doc.nodes));
7588
+ else this.setDoc(mapTree(this.doc.nodes, (n) => n.id === parent && n.type === "group" ? { ...n, children: wrap(n.children) } : n));
7589
+ this.selection = [id];
7590
+ this.anchors = [];
7591
+ this.editingId = null;
7592
+ });
7593
+ return id;
7594
+ }
6478
7595
  /** Joins the selected paths into one compound path (subpaths kept, lowest style). */
6479
7596
  combine(options = {}) {
6480
7597
  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;
7598
+ const entries = vectorPathEntries(this.doc, roots, { locked: true });
7599
+ if (entries.length < 2) return null;
7600
+ const paths = entries.map((e) => e.node);
6486
7601
  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]);
7602
+ const first = roots.find((r) => this.holds(r, paths[0].id)) ?? paths[0].id;
7603
+ const subpaths = entries.flatMap((e) => bezierToSubpaths(this.intoParentSpace(first, vectorPathWorldBezier(e.node, e.parent))));
7604
+ const { transform: _t, effects: _e, booleanOperation: _b, ...baseRest } = cloneVectorNode(paths[0]);
6490
7605
  const node = { ...baseRest, id, name: "Compound Path", subpaths };
6491
7606
  const drop = /* @__PURE__ */ new Set([...paths.map((p) => p.id), ...roots]);
6492
7607
  this.change("Combine", () => {
@@ -6541,10 +7656,7 @@ var VectorEditorController = class {
6541
7656
  const result = pathfinder(op, shapes);
6542
7657
  if (result.error || !result.pieces.length) return [];
6543
7658
  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;
7659
+ const topRoot = [...roots].reverse().find((r) => this.holds(r, last)) ?? last;
6548
7660
  const nodes = [];
6549
7661
  for (const piece of result.pieces) {
6550
7662
  const subpaths = bezierToSubpaths(this.intoParentSpace(topRoot, piece.path)).map((sp) => piece.open ? { ...sp, closed: false } : sp);
@@ -6737,7 +7849,7 @@ var VectorEditorController = class {
6737
7849
  const roots = ids ?? this.selectedRoots();
6738
7850
  const key = roots.join("\0");
6739
7851
  if (this.builderCache?.doc === this.doc && this.builderCache.key === key) return this.builderCache.map;
6740
- const entries = roots.length ? vectorPathEntries(this.doc, roots) : [];
7852
+ const entries = roots.length ? vectorPathEntries(this.doc, roots, { expand: false }) : [];
6741
7853
  const map = entries.length ? vectorShapeBuilderMap(entries) : null;
6742
7854
  this.builderCache = { doc: this.doc, key, map };
6743
7855
  return map;
@@ -7521,16 +8633,22 @@ var VectorEditorController = class {
7521
8633
  return best;
7522
8634
  }
7523
8635
  /** Deepest unlocked visible path under `p`. */
8636
+ /** Live groups being edited (selected, or holding the selection or the edited path): their children are hit as stored. */
8637
+ enteredGroups() {
8638
+ return vectorEnteredGroups(this.doc.nodes, this.editingId ? [...this.selection, this.editingId] : this.selection);
8639
+ }
7524
8640
  hitPath(p) {
7525
8641
  const tol = this.tolerance();
8642
+ const entered = this.enteredGroups();
7526
8643
  const search = (list, parent) => {
7527
8644
  for (let i = list.length - 1; i >= 0; i--) {
7528
8645
  const n = list[i];
7529
8646
  if (n.visible === false || n.locked) continue;
7530
8647
  if (n.type === "group") {
8648
+ if (hasOwnVectorAppearance(n) && !entered.has(n.id) && !hitTestVectorNode(n, p, tol, parent, { entered })) continue;
7531
8649
  const found = search(n.children, multiplyMatrices(nodeMatrix(n), parent));
7532
8650
  if (found) return found;
7533
- } else if (hitTestVectorNode(n, p, tol, parent)) return n;
8651
+ } else if (hitTestVectorNode(n, p, tol, parent, { entered })) return n;
7534
8652
  }
7535
8653
  return null;
7536
8654
  };
@@ -7555,7 +8673,7 @@ var VectorEditorController = class {
7555
8673
  case "select": {
7556
8674
  const handle = this.hitTransformHandle(screen);
7557
8675
  if (handle) return { nodeId: null, transformHandle: handle };
7558
- const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
8676
+ const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });
7559
8677
  return node ? { nodeId: node.id } : null;
7560
8678
  }
7561
8679
  case "direct": {
@@ -7884,7 +9002,7 @@ var VectorEditorController = class {
7884
9002
  this.notify();
7885
9003
  return;
7886
9004
  }
7887
- const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance());
9005
+ const node = hitTestVectorNodes(this.doc.nodes, p, this.tolerance(), { entered: this.enteredGroups() });
7888
9006
  if (node) {
7889
9007
  const selected = this.selection.includes(node.id);
7890
9008
  if (input.shift) {
@@ -8809,8 +9927,9 @@ function pencilPressureProfile(points, pressures) {
8809
9927
  var fmt = (n) => Math.round(n * 100) / 100;
8810
9928
  var fmtMatrix = (m) => `matrix(${m.map((n) => Math.round(n * 1e6) / 1e6).join(" ")})`;
8811
9929
  var VECTOR_BUILDER_HATCH_GAP = 6;
8812
- function renderNode(node, defs, idPrefix) {
8813
- if (node.visible === false) return null;
9930
+ function renderNode(stored, defs, idPrefix) {
9931
+ if (stored.visible === false) return null;
9932
+ const node = expandVectorAppearance(stored);
8814
9933
  if (node.type === "group") {
8815
9934
  const children = node.children.map((c) => renderNode(c, defs, idPrefix)).filter((c) => !!c);
8816
9935
  return { kind: "group", id: node.id, ...node.transform ? { transform: fmtMatrix(node.transform) } : {}, opacity: node.opacity ?? 1, children };
@@ -8851,16 +9970,20 @@ function getVectorSceneRenderData(state, options = {}) {
8851
9970
  };
8852
9971
  }
8853
9972
  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) {
9973
+ function screenOutline(state, node, entered) {
8855
9974
  const v2 = state.viewport;
8856
9975
  const view = [v2.zoom, 0, 0, v2.zoom, v2.x, v2.y];
8857
9976
  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(" ");
9977
+ return outlinePaths(node, parent, entered).map((path) => bezierPathToSvg(transformBezierPath(path, view), { precision: 2 })).join(" ");
8859
9978
  }
8860
- function collectWithMatrix(node, parent) {
8861
- if (node.type === "path") return [{ path: node, parent }];
9979
+ function outlinePaths(node, parent, entered) {
9980
+ if (node.type === "path") return [vectorPathWorldBezier(node, parent, { effects: false })];
8862
9981
  const m = multiplyMatrices(nodeMatrix(node), parent);
8863
- return node.children.flatMap((c) => collectWithMatrix(c, m));
9982
+ if (hasOwnVectorAppearance(node) && !entered.has(node.id)) return [transformBezierPath(vectorAppearanceOutline(node), m)];
9983
+ return node.children.filter((c) => c.visible !== false).flatMap((c) => outlinePaths(c, m, entered));
9984
+ }
9985
+ function getVectorEnteredGroups(state) {
9986
+ return vectorEnteredGroups(state.document.nodes, state.editingId ? [...state.selection, state.editingId] : state.selection);
8864
9987
  }
8865
9988
  function collect(node) {
8866
9989
  if (node.type === "path") return [node];
@@ -8870,13 +9993,14 @@ function getVectorOverlayRenderData(state) {
8870
9993
  const doc = state.document;
8871
9994
  const z = state.viewport.zoom;
8872
9995
  const outlines = [];
9996
+ const entered = getVectorEnteredGroups(state);
8873
9997
  const seen = /* @__PURE__ */ new Set();
8874
9998
  const outline = (id, selected2) => {
8875
9999
  if (seen.has(id)) return;
8876
10000
  const node = findVectorNode(doc.nodes, id);
8877
10001
  if (!node || node.visible === false) return;
8878
10002
  seen.add(id);
8879
- outlines.push({ id, d: screenOutline(state, node), selected: selected2 });
10003
+ outlines.push({ id, d: screenOutline(state, node, entered), selected: selected2 });
8880
10004
  };
8881
10005
  for (const id of state.selection) outline(id, true);
8882
10006
  if (state.editingId) outline(state.editingId, true);
@@ -9041,7 +10165,7 @@ function blendOverlay(state) {
9041
10165
  const from = state.blendStart ? findVectorNode(doc.nodes, state.blendStart.nodeId) : void 0;
9042
10166
  if (from && state.blendStart) {
9043
10167
  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 };
10168
+ start = { d: screenOutline(state, from, /* @__PURE__ */ new Set()), anchor: a ? sc(a, view) : null };
9045
10169
  }
9046
10170
  const hovered = state.hover?.nodeId ? findVectorNode(doc.nodes, state.hover.nodeId) : void 0;
9047
10171
  if (hovered?.type === "path" && hovered.subpaths[0]) {
@@ -9226,8 +10350,28 @@ var VECTOR_EDIT_TOGGLES = [
9226
10350
  { key: "curvatureMoves", label: "Curv", title: "Curvature point moves (Alt flips while dragging)" },
9227
10351
  { key: "recognizeShapes", label: "Shapes", title: "Turn pencil scribbles into shapes" }
9228
10352
  ];
10353
+ var VECTOR_EFFECT_MENU = VECTOR_EFFECT_TYPES.map((type) => ({ type, label: VECTOR_EFFECT_LABELS[type] }));
10354
+ function getVectorEffectsPanel(state) {
10355
+ const nodes = state.selection.map((id) => findVectorNode(state.document.nodes, id)).filter((n) => !!n && !n.blendStep);
10356
+ if (!nodes.length) return null;
10357
+ const one = nodes.length === 1 ? nodes[0] : null;
10358
+ return {
10359
+ nodeId: one?.id ?? null,
10360
+ count: nodes.length,
10361
+ effects: (one?.effects ?? []).map((e) => ({
10362
+ id: e.id,
10363
+ type: e.type,
10364
+ label: VECTOR_EFFECT_LABELS[e.type] ?? e.type,
10365
+ enabled: e.enabled !== false,
10366
+ params: e.params,
10367
+ specs: VECTOR_EFFECT_PARAM_SPECS[e.type] ?? []
10368
+ })),
10369
+ expandable: nodes.some(hasVectorAppearance)
10370
+ };
10371
+ }
9229
10372
 
9230
10373
  // src/vector/commands.ts
10374
+ 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
10375
  var fail = (message) => {
9232
10376
  throw new Error(message);
9233
10377
  };
@@ -9338,7 +10482,10 @@ function createVectorCommands(controller, options = {}) {
9338
10482
  selected: state.selection.includes(n.id),
9339
10483
  ...n.type === "path" ? { fill: n.fill ?? null, stroke: n.stroke ?? null, strokeWidth: n.strokeWidth ?? 1, d: vectorPathData(n, 2) } : { children: n.children.length },
9340
10484
  ...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 } : {}
10485
+ ...n.blendStep ? { blendStep: true } : {},
10486
+ ...n.effects?.length ? { effects: n.effects } : {},
10487
+ ...n.type === "group" && n.liveBoolean ? { liveBoolean: n.liveBoolean } : {},
10488
+ ...n.booleanOperation ? { booleanOperation: n.booleanOperation } : {}
9342
10489
  });
9343
10490
  if (n.type === "group") visit(n.children, depth + 1);
9344
10491
  }
@@ -9574,15 +10721,101 @@ function createVectorCommands(controller, options = {}) {
9574
10721
  id: `${prefix}boolean`,
9575
10722
  label: "Boolean Operation\u2026",
9576
10723
  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"] },
10724
+ 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).",
10725
+ params: {
10726
+ type: "object",
10727
+ properties: {
10728
+ operation: { type: "string", enum: ["union", "subtract", "intersect", "exclude"] },
10729
+ ids: IDS,
10730
+ live: { type: "boolean", description: "Keep the operands editable in a live boolean group (default false: one new path)." }
10731
+ },
10732
+ required: ["operation"]
10733
+ },
9579
10734
  undoable: true,
9580
10735
  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).");
10736
+ const id = controller.booleanOp(params.operation, { ids: ids(params), live: params.live });
10737
+ 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
10738
  return { id };
9584
10739
  }
9585
10740
  },
10741
+ {
10742
+ ...base,
10743
+ id: `${prefix}addEffect`,
10744
+ label: "Add Path Effect\u2026",
10745
+ keywords: ["effect", "trim", "round corners", "pucker", "bloat", "twist", "zig zag", "wiggle", "offset", "merge", "repeater", "appearance", "live"],
10746
+ 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.",
10747
+ params: {
10748
+ type: "object",
10749
+ properties: {
10750
+ type: { type: "string", enum: VECTOR_EFFECT_TYPES },
10751
+ params: { type: "object", description: EFFECT_PARAMS_DESCRIPTION },
10752
+ index: { type: "integer", minimum: 0, description: "Position in the stack (default: last)." },
10753
+ ids: IDS
10754
+ },
10755
+ required: ["type"]
10756
+ },
10757
+ undoable: true,
10758
+ enabled: enabledFor,
10759
+ run: (_ctx, params) => {
10760
+ if (!VECTOR_EFFECT_TYPES.includes(params.type)) fail(`Unknown effect type "${params.type}".`);
10761
+ const targets = ids(params);
10762
+ const created = controller.addEffect(params.type, { ids: targets, params: params.params, index: params.index });
10763
+ if (!created.length) fail("Nothing to add the effect to.");
10764
+ return { effects: targets.map((id, i) => ({ nodeId: id, effectId: created[i] })) };
10765
+ }
10766
+ },
10767
+ {
10768
+ ...base,
10769
+ id: `${prefix}updateEffect`,
10770
+ label: "Edit Path Effect\u2026",
10771
+ keywords: ["effect", "parameters", "enable", "disable", "reorder"],
10772
+ description: "Changes a path effect: its parameters, whether it is enabled, and its position in the stack.",
10773
+ params: {
10774
+ type: "object",
10775
+ properties: {
10776
+ nodeId: { type: "string" },
10777
+ effectId: { type: "string" },
10778
+ params: { type: "object", description: EFFECT_PARAMS_DESCRIPTION },
10779
+ enabled: { type: "boolean" },
10780
+ index: { type: "integer", minimum: 0, description: "Move it to this position in the stack (0 applies first)." }
10781
+ },
10782
+ required: ["nodeId", "effectId"]
10783
+ },
10784
+ undoable: true,
10785
+ run: (_ctx, params) => {
10786
+ if (params.params !== void 0 || params.enabled !== void 0) check(controller.updateEffect(params.nodeId, params.effectId, { params: params.params, enabled: params.enabled }));
10787
+ if (params.index !== void 0) check(controller.moveEffect(params.nodeId, params.effectId, params.index));
10788
+ return { effects: controller.getNode(params.nodeId)?.effects ?? [] };
10789
+ }
10790
+ },
10791
+ {
10792
+ ...base,
10793
+ id: `${prefix}removeEffect`,
10794
+ label: "Remove Path Effect",
10795
+ keywords: ["effect", "delete"],
10796
+ description: "Removes a path effect from a node.",
10797
+ params: { type: "object", properties: { nodeId: { type: "string" }, effectId: { type: "string" } }, required: ["nodeId", "effectId"] },
10798
+ undoable: true,
10799
+ run: (_ctx, params) => {
10800
+ check(controller.removeEffect(params.nodeId, params.effectId));
10801
+ return { ok: true };
10802
+ }
10803
+ },
10804
+ {
10805
+ ...base,
10806
+ id: `${prefix}expandAppearance`,
10807
+ label: "Expand Appearance",
10808
+ keywords: ["effects", "bake", "flatten", "live boolean", "expand"],
10809
+ description: "Bakes the path effects and live booleans of nodes (and everything inside them) into plain paths.",
10810
+ params: { type: "object", properties: { ids: IDS } },
10811
+ undoable: true,
10812
+ enabled: enabledFor,
10813
+ run: (_ctx, params) => {
10814
+ const expanded = controller.expandAppearance({ ids: ids(params) });
10815
+ if (!expanded.length) fail("Nothing selected has path effects or a live boolean.");
10816
+ return { ids: expanded };
10817
+ }
10818
+ },
9586
10819
  {
9587
10820
  ...base,
9588
10821
  id: `${prefix}blend`,
@@ -10030,8 +11263,14 @@ export {
10030
11263
  VECTOR_CORNER_KIND_LABELS,
10031
11264
  VECTOR_CORNER_WIDGET_INSET,
10032
11265
  VECTOR_EDIT_TOGGLES,
11266
+ VECTOR_EFFECT_DEFAULTS,
11267
+ VECTOR_EFFECT_LABELS,
11268
+ VECTOR_EFFECT_MENU,
11269
+ VECTOR_EFFECT_PARAM_SPECS,
11270
+ VECTOR_EFFECT_TYPES,
10033
11271
  VECTOR_FILL_GAP,
10034
11272
  VECTOR_INK_MAX_CORNERS,
11273
+ VECTOR_MERGE_MODES,
10035
11274
  VECTOR_OVERLAY_THEME,
10036
11275
  VECTOR_PATHFINDER_ACTIONS,
10037
11276
  VECTOR_PATHFINDER_LABELS,
@@ -10053,11 +11292,13 @@ export {
10053
11292
  VECTOR_WIDTH_SAME_T,
10054
11293
  VectorEditorController,
10055
11294
  VectorSnapTargets,
11295
+ activeVectorEffects,
10056
11296
  addBuilderHit,
10057
11297
  applyMatrix,
10058
11298
  applyMatrixLinear,
10059
11299
  applyVectorBlendOptions,
10060
11300
  applyVectorCornersAttribute,
11301
+ applyVectorEffects,
10061
11302
  arrowheadInset,
10062
11303
  arrowheadPath,
10063
11304
  arrowheadWeight,
@@ -10065,6 +11306,7 @@ export {
10065
11306
  bezierToSubpaths,
10066
11307
  boundsFromPoints,
10067
11308
  boundsIntersect,
11309
+ clearVectorLiveBooleanCache,
10068
11310
  cloneAnchor,
10069
11311
  cloneSubpath,
10070
11312
  cloneVectorDocument,
@@ -10077,22 +11319,28 @@ export {
10077
11319
  cornerSetback,
10078
11320
  correspondMorphContours,
10079
11321
  createVectorCommands,
11322
+ createVectorEffect,
10080
11323
  createVectorMorph,
10081
11324
  curvatureMoveAnchor,
10082
11325
  cutSubpathAt,
10083
11326
  dashIntervals,
10084
11327
  decomposeRigid,
11328
+ effectPathLength,
11329
+ effectPathVertexCount,
10085
11330
  effectiveStrokeAlign,
10086
11331
  ellipseSubpath,
10087
11332
  eraseFromFill,
10088
11333
  eraseFromOutline,
10089
11334
  eraserRegion,
10090
11335
  evaluateVectorPathKeyframes,
11336
+ expandVectorAppearance,
11337
+ expandVectorAppearances,
10091
11338
  expandVectorBlend,
10092
11339
  expandVectorCorners,
10093
11340
  expandVectorCornersOf,
10094
11341
  findVectorNode,
10095
11342
  fitCornerRadius,
11343
+ foldVectorBooleanOperands,
10096
11344
  generateVectorBlendChildren,
10097
11345
  generateWidthProfile,
10098
11346
  getAnchor,
@@ -10101,6 +11349,8 @@ export {
10101
11349
  getTransformHandlePositions,
10102
11350
  getVectorBlendControls,
10103
11351
  getVectorCornerWidgets,
11352
+ getVectorEffectsPanel,
11353
+ getVectorEnteredGroups,
10104
11354
  getVectorOverlayRenderData,
10105
11355
  getVectorSceneRenderData,
10106
11356
  getVectorSelectionBoxGeometry,
@@ -10109,8 +11359,10 @@ export {
10109
11359
  getVectorWidthHandles,
10110
11360
  getVisibleAnchors,
10111
11361
  getVisibleHandles,
11362
+ hasOwnVectorAppearance,
10112
11363
  hasPaint,
10113
11364
  hasStrokeExtras,
11365
+ hasVectorAppearance,
10114
11366
  hasVectorCorners,
10115
11367
  hitTestVectorNode,
10116
11368
  hitTestVectorNodes,
@@ -10130,12 +11382,14 @@ export {
10130
11382
  isIdentityMatrix,
10131
11383
  isUniformWidthProfile,
10132
11384
  isVectorBlendGroup,
11385
+ isVectorLiveBoolean,
10133
11386
  isVectorTextTarget,
10134
11387
  joinSubpaths,
10135
11388
  knifePath,
10136
11389
  lineSubpath,
10137
11390
  locateVectorNode,
10138
11391
  matrixRotation,
11392
+ mergePaths,
10139
11393
  mirrorWidthProfile,
10140
11394
  morphContourToSubpath,
10141
11395
  morphPathsCross,
@@ -10145,12 +11399,16 @@ export {
10145
11399
  nearestStrokePosition,
10146
11400
  nextVectorCornerKind,
10147
11401
  nodeMatrix,
11402
+ normalizeVectorEffectParams,
11403
+ offsetPaths,
10148
11404
  parseSvgTransform,
10149
11405
  parseVectorSvg,
11406
+ pathsCenter,
10150
11407
  planShapeBuilder,
10151
11408
  polygonSubpath,
10152
11409
  polylineBounds,
10153
11410
  pressureWidthProfile,
11411
+ puckerBloatPaths,
10154
11412
  recognizeInkShape,
10155
11413
  rectSubpath,
10156
11414
  refineMorphContour,
@@ -10161,14 +11419,17 @@ export {
10161
11419
  removeAnchorsKeepingShape,
10162
11420
  removeRedundantPoints,
10163
11421
  removeWidthPoints,
11422
+ repeatPaths,
10164
11423
  resampleGradientStops,
10165
11424
  resolveVectorBlendOptions,
10166
11425
  reverseMorphContour,
10167
11426
  reverseSubpath,
10168
11427
  reverseWidthProfile,
10169
11428
  rotateMatrix,
11429
+ roundCornerPaths,
10170
11430
  roundSubpaths,
10171
11431
  samplePolyline,
11432
+ sanitizeVectorEffects,
10172
11433
  sanitizeWidthProfile,
10173
11434
  scaleFor,
10174
11435
  scaleMatrix,
@@ -10192,13 +11453,21 @@ export {
10192
11453
  toVectorKeyInput,
10193
11454
  toVectorModifiers,
10194
11455
  toVectorPointerInput,
11456
+ transformVectorEffects,
10195
11457
  transformVectorNode,
10196
11458
  translateAnchor,
10197
11459
  translateMatrix,
11460
+ trimIntervals,
11461
+ trimPaths,
11462
+ trimPathsIndividually,
10198
11463
  turnMorphContour,
11464
+ twistPaths,
10199
11465
  unionBounds,
10200
11466
  variableStrokeOutline,
10201
11467
  variableStrokeRings,
11468
+ vectorAppearanceNodes,
11469
+ vectorAppearanceOutline,
11470
+ vectorAppearanceShapes,
10202
11471
  vectorBlendColorDistance,
10203
11472
  vectorBlendKeyTargets,
10204
11473
  vectorBlendKeys,
@@ -10212,11 +11481,14 @@ export {
10212
11481
  vectorCornersAttribute,
10213
11482
  vectorDiamondPath,
10214
11483
  vectorDocumentToSvg,
11484
+ vectorEnteredGroups,
10215
11485
  vectorFillMap,
10216
11486
  vectorInkShapeName,
11487
+ vectorLiveBooleanSteps,
10217
11488
  vectorNodeBounds,
10218
11489
  vectorNodeRotation,
10219
11490
  vectorNodeToBezier,
11491
+ vectorOperandOperation,
10220
11492
  vectorPaintToHex,
10221
11493
  vectorPaintToSvg,
10222
11494
  vectorParentMatrix,
@@ -10224,6 +11496,7 @@ export {
10224
11496
  vectorPathEntries,
10225
11497
  vectorPathWorldBezier,
10226
11498
  vectorPathfinderShape,
11499
+ vectorRepeaterCopies,
10227
11500
  vectorResizeCursor,
10228
11501
  vectorResultStyle,
10229
11502
  vectorShapeBuilderHit,
@@ -10243,6 +11516,8 @@ export {
10243
11516
  widthProfileMaxScale,
10244
11517
  widthProfilePreset,
10245
11518
  widthProfilePresetId,
10246
- widthProfileToAttribute
11519
+ widthProfileToAttribute,
11520
+ wigglePaths,
11521
+ zigzagPaths
10247
11522
  };
10248
11523
  //# sourceMappingURL=index.js.map