@pixerate/editor 0.117.0 → 0.119.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. package/THIRD_PARTY_NOTICES.md +3 -0
  2. package/dist/animation-editor/index.cjs +54 -54
  3. package/dist/animation-editor/index.d.cts +3 -3
  4. package/dist/animation-editor/index.d.ts +3 -3
  5. package/dist/animation-editor/index.js +2 -2
  6. package/dist/{chunk-2J3LS7JZ.cjs → chunk-4S2AXNQF.cjs} +221 -186
  7. package/dist/chunk-4S2AXNQF.cjs.map +1 -0
  8. package/dist/{chunk-7LNZAWSJ.js → chunk-CSHEI3YT.js} +625 -451
  9. package/dist/chunk-CSHEI3YT.js.map +1 -0
  10. package/dist/{chunk-M7O4BJRH.cjs → chunk-KGNUFO4T.cjs} +36 -36
  11. package/dist/{chunk-M7O4BJRH.cjs.map → chunk-KGNUFO4T.cjs.map} +1 -1
  12. package/dist/{chunk-R2JE6SA7.js → chunk-KZLQY4CD.js} +42 -7
  13. package/dist/chunk-KZLQY4CD.js.map +1 -0
  14. package/dist/{chunk-L2T6EJ5S.cjs → chunk-L3PXFMOF.cjs} +38 -25
  15. package/dist/chunk-L3PXFMOF.cjs.map +1 -0
  16. package/dist/{chunk-FODKPD44.cjs → chunk-LI3KTY7K.cjs} +724 -550
  17. package/dist/chunk-LI3KTY7K.cjs.map +1 -0
  18. package/dist/{chunk-IVSNUXW7.js → chunk-T7IZDNSL.js} +37 -24
  19. package/dist/chunk-T7IZDNSL.js.map +1 -0
  20. package/dist/{chunk-T2P5TIZ6.js → chunk-TDQDMBUR.js} +2 -2
  21. package/dist/{chunk-BYAWOAST.cjs → chunk-TS5IEUW6.cjs} +26 -25
  22. package/dist/chunk-TS5IEUW6.cjs.map +1 -0
  23. package/dist/{chunk-VD2UDVF7.js → chunk-UMVW2Q66.js} +21 -5
  24. package/dist/chunk-UMVW2Q66.js.map +1 -0
  25. package/dist/{chunk-EBSYZRSW.cjs → chunk-WMRIG4XQ.cjs} +21 -5
  26. package/dist/chunk-WMRIG4XQ.cjs.map +1 -0
  27. package/dist/{chunk-JHXMNOMF.js → chunk-YHW26LS2.js} +14 -13
  28. package/dist/chunk-YHW26LS2.js.map +1 -0
  29. package/dist/{export-DMOuzgZZ.d.cts → export-B_jO2PrH.d.cts} +18 -4
  30. package/dist/{export-Dul1SbPv.d.ts → export-Ds8Ccw0K.d.ts} +18 -4
  31. package/dist/{flow-4-OC-UkQ.d.cts → flow-A2Dj-69w.d.cts} +21 -9
  32. package/dist/{flow-Cb7lkPm8.d.ts → flow-C95atNoQ.d.ts} +21 -9
  33. package/dist/image-editor/index.cjs +1104 -979
  34. package/dist/image-editor/index.cjs.map +1 -1
  35. package/dist/image-editor/index.d.cts +5 -5
  36. package/dist/image-editor/index.d.ts +5 -5
  37. package/dist/image-editor/index.js +157 -32
  38. package/dist/image-editor/index.js.map +1 -1
  39. package/dist/paint/index.cjs +5 -3
  40. package/dist/paint/index.cjs.map +1 -1
  41. package/dist/paint/index.d.cts +2 -2
  42. package/dist/paint/index.d.ts +2 -2
  43. package/dist/paint/index.js +4 -2
  44. package/dist/paint-editor/index.cjs +147 -147
  45. package/dist/paint-editor/index.d.cts +4 -4
  46. package/dist/paint-editor/index.d.ts +4 -4
  47. package/dist/paint-editor/index.js +4 -4
  48. package/dist/raster/index.cjs +2 -2
  49. package/dist/raster/index.js +1 -1
  50. package/dist/{svg-DlEuhwoX.d.ts → svg-CTxrB5xO.d.ts} +5 -1
  51. package/dist/{svg-CVdFc80-.d.cts → svg-hK0WQc7-.d.cts} +5 -1
  52. package/dist/{types-Tfly4Aya.d.cts → types-Dq-8qgoo.d.cts} +45 -3
  53. package/dist/{types-CDLGhAZp.d.ts → types-TrSSh_RB.d.ts} +45 -3
  54. package/dist/vector/index.cjs +1535 -1164
  55. package/dist/vector/index.cjs.map +1 -1
  56. package/dist/vector/index.d.cts +127 -9
  57. package/dist/vector/index.d.ts +127 -9
  58. package/dist/vector/index.js +386 -15
  59. package/dist/vector/index.js.map +1 -1
  60. package/package.json +1 -1
  61. package/skills/pixerate-image-editor/references/photoshop-files.md +9 -7
  62. package/skills/pixerate-vector/SKILL.md +13 -1
  63. package/skills/pixerate-vector/references/commands.md +1 -0
  64. package/dist/chunk-2J3LS7JZ.cjs.map +0 -1
  65. package/dist/chunk-7LNZAWSJ.js.map +0 -1
  66. package/dist/chunk-BYAWOAST.cjs.map +0 -1
  67. package/dist/chunk-EBSYZRSW.cjs.map +0 -1
  68. package/dist/chunk-FODKPD44.cjs.map +0 -1
  69. package/dist/chunk-IVSNUXW7.js.map +0 -1
  70. package/dist/chunk-JHXMNOMF.js.map +0 -1
  71. package/dist/chunk-L2T6EJ5S.cjs.map +0 -1
  72. package/dist/chunk-R2JE6SA7.js.map +0 -1
  73. package/dist/chunk-VD2UDVF7.js.map +0 -1
  74. /package/dist/{chunk-T2P5TIZ6.js.map → chunk-TDQDMBUR.js.map} +0 -0
@@ -40,7 +40,7 @@ import {
40
40
  reverseMorphContour,
41
41
  subpathToMorphContour,
42
42
  turnMorphContour
43
- } from "../chunk-T2P5TIZ6.js";
43
+ } from "../chunk-TDQDMBUR.js";
44
44
  import {
45
45
  DEFAULT_VECTOR_FONT,
46
46
  IDENTITY_MATRIX,
@@ -83,6 +83,8 @@ import {
83
83
  VECTOR_STROKE_GRADIENT_MODES,
84
84
  VECTOR_STROKE_SETTING_KEYS,
85
85
  VECTOR_SYMBOL_MAX_DEPTH,
86
+ VECTOR_SYMMETRY_MODES,
87
+ VECTOR_SYMMETRY_MODE_LABELS,
86
88
  VECTOR_TEXT_ALIGNS,
87
89
  VECTOR_TOOLS,
88
90
  VECTOR_TRANSFORM_HANDLES,
@@ -200,6 +202,7 @@ import {
200
202
  isVectorMaskType,
201
203
  isVectorMeshGradient,
202
204
  isVectorShadedPaint,
205
+ isVectorSymmetryGroup,
203
206
  knifePath,
204
207
  layoutVectorText,
205
208
  lineSubpath,
@@ -284,6 +287,7 @@ import {
284
287
  sanitizeVectorShadedPaint,
285
288
  sanitizeVectorStrokeSettings,
286
289
  sanitizeVectorStrokes,
290
+ sanitizeVectorSymmetry,
287
291
  sanitizeVectorTextFields,
288
292
  sanitizeVectorTextFrame,
289
293
  sanitizeWidthProfile,
@@ -316,6 +320,7 @@ import {
316
320
  transformVectorNode,
317
321
  transformVectorPaint,
318
322
  transformVectorShadedPaint,
323
+ transformVectorSymmetry,
319
324
  translateAnchor,
320
325
  translateMatrix,
321
326
  trimIntervals,
@@ -429,6 +434,9 @@ import {
429
434
  vectorStrokes,
430
435
  vectorSymbolUses,
431
436
  vectorSymbolsUsedIn,
437
+ vectorSymmetryAxes,
438
+ vectorSymmetryDrawn,
439
+ vectorSymmetryMatrices,
432
440
  vectorTargetPaint,
433
441
  vectorTextBoxOutline,
434
442
  vectorTextCaret,
@@ -467,7 +475,7 @@ import {
467
475
  withVectorStrokes,
468
476
  withVectorTargetPaint,
469
477
  zigzagPaths
470
- } from "../chunk-7LNZAWSJ.js";
478
+ } from "../chunk-CSHEI3YT.js";
471
479
  import {
472
480
  bezierPathBounds,
473
481
  bezierPathToSvg,
@@ -2167,7 +2175,24 @@ function renderText(node, defs, filters, idPrefix) {
2167
2175
  spans
2168
2176
  };
2169
2177
  }
2178
+ var renderCache = /* @__PURE__ */ new WeakMap();
2170
2179
  function renderNode(stored, defs, filters, idPrefix) {
2180
+ const gen = vectorTextShaperGeneration();
2181
+ let byPrefix = renderCache.get(stored);
2182
+ const hit = byPrefix?.get(idPrefix);
2183
+ if (hit && hit.gen === gen) {
2184
+ defs.push(...hit.defs);
2185
+ filters.push(...hit.filters);
2186
+ return hit.node;
2187
+ }
2188
+ const d0 = defs.length;
2189
+ const f0 = filters.length;
2190
+ const node = renderNodeFresh(stored, defs, filters, idPrefix);
2191
+ if (!byPrefix) renderCache.set(stored, byPrefix = /* @__PURE__ */ new Map());
2192
+ byPrefix.set(idPrefix, { gen, node, defs: defs.slice(d0), filters: filters.slice(f0) });
2193
+ return node;
2194
+ }
2195
+ function renderNodeFresh(stored, defs, filters, idPrefix) {
2171
2196
  if (stored.visible === false) return null;
2172
2197
  if (stored.type === "text" && !vectorTextHasOutlines()) return renderText(stored, defs, filters, idPrefix);
2173
2198
  const node = expandVectorAppearance(stored);
@@ -2234,13 +2259,64 @@ function renderMask(group, defs, filters, idPrefix) {
2234
2259
  const source = renderNode(group.children[0], defs, filters, idPrefix);
2235
2260
  return { ...mask, nodes: source ? [source] : [] };
2236
2261
  }
2262
+ var collapsedCache = /* @__PURE__ */ new WeakMap();
2263
+ function collapsedNode(node, box, prefix) {
2264
+ let byPrefix = collapsedCache.get(node);
2265
+ if (byPrefix?.has(prefix)) return byPrefix.get(prefix);
2266
+ let fill = null;
2267
+ const look = (n2) => {
2268
+ if (fill || n2.visible === false) return;
2269
+ if (n2.type === "path" || n2.type === "text") {
2270
+ const f = n2.fill;
2271
+ if (typeof f === "string" && f !== "none") fill = f;
2272
+ else if (isVectorGradient(f)) fill = f.stops[0]?.color ?? null;
2273
+ } else if (n2.type === "group") n2.children.forEach(look);
2274
+ else if (n2.type === "instance") (n2.art ?? []).forEach(look);
2275
+ };
2276
+ look(node);
2277
+ const out = fill ? { kind: "path", id: node.id, d: `M${fmt2(box.minX)} ${fmt2(box.minY)}H${fmt2(box.maxX)}V${fmt2(box.maxY)}H${fmt2(box.minX)}Z`, fill, stroke: "none", strokeWidth: 0, fillRule: "nonzero", opacity: node.opacity ?? 1 } : null;
2278
+ if (!byPrefix) collapsedCache.set(node, byPrefix = /* @__PURE__ */ new Map());
2279
+ byPrefix.set(prefix, out);
2280
+ return out;
2281
+ }
2237
2282
  function getVectorSceneRenderData(state, options = {}) {
2238
2283
  const defs = [];
2239
2284
  const filters = [];
2240
2285
  const prefix = options.idPrefix ?? "ve-";
2241
- const nodes = state.document.nodes.map((n2) => renderNode(n2, defs, filters, prefix)).filter((n2) => !!n2);
2242
2286
  const v = state.viewport;
2287
+ const all = state.document.nodes;
2288
+ const stats = { nodes: all.length, culled: 0, collapsed: 0 };
2289
+ let nodes;
2290
+ const size = options.viewportSize;
2291
+ if (size && size.width > 0 && size.height > 0 && v.zoom > 0) {
2292
+ const z = v.zoom;
2293
+ const pad = 2 / z;
2294
+ const view = { minX: -v.x / z - pad, minY: -v.y / z - pad, maxX: (size.width - v.x) / z + pad, maxY: (size.height - v.y) / z + pad };
2295
+ const lod = options.lod ?? 2;
2296
+ const near = vectorNodesNear(all, { x: view.minX, y: view.minY, width: view.maxX - view.minX, height: view.maxY - view.minY });
2297
+ stats.culled = all.length - near.length;
2298
+ nodes = [];
2299
+ for (const i of near) {
2300
+ const n2 = all[i];
2301
+ const box = vectorNodePaintBox(n2);
2302
+ if (box && (box.maxX < view.minX || box.minX > view.maxX || box.maxY < view.minY || box.minY > view.maxY)) {
2303
+ stats.culled++;
2304
+ continue;
2305
+ }
2306
+ if (box && lod > 0 && n2.type !== "path" && n2.visible !== false && Math.max(box.maxX - box.minX, box.maxY - box.minY) * z < lod) {
2307
+ const c = collapsedNode(n2, box, prefix);
2308
+ if (c) {
2309
+ stats.collapsed++;
2310
+ nodes.push(c);
2311
+ continue;
2312
+ }
2313
+ }
2314
+ const r = renderNode(n2, defs, filters, prefix);
2315
+ if (r) nodes.push(r);
2316
+ }
2317
+ } else nodes = all.map((n2) => renderNode(n2, defs, filters, prefix)).filter((n2) => !!n2);
2243
2318
  return {
2319
+ stats,
2244
2320
  transform: `translate(${fmt2(v.x)} ${fmt2(v.y)}) scale(${v.zoom})`,
2245
2321
  width: state.document.width,
2246
2322
  height: state.document.height,
@@ -2303,6 +2379,46 @@ function shadingOverlay(state) {
2303
2379
  }
2304
2380
  return out;
2305
2381
  }
2382
+ function symmetryOverlay(state) {
2383
+ const ids = /* @__PURE__ */ new Set([...state.selection, ...state.symmetryTarget ? [state.symmetryTarget] : []]);
2384
+ const out = [];
2385
+ for (const id of ids) {
2386
+ const n2 = findVectorNode(state.document.nodes, id);
2387
+ if (!isVectorSymmetryGroup(n2)) continue;
2388
+ const world = vectorWorldMatrix(state.document.nodes, id);
2389
+ if (!world) continue;
2390
+ const inv = invertMatrix(world);
2391
+ const corners2 = [
2392
+ { x: 0, y: 0 },
2393
+ { x: state.document.width, y: 0 },
2394
+ { x: 0, y: state.document.height },
2395
+ { x: state.document.width, y: state.document.height }
2396
+ ].map((p) => applyMatrix(inv, p));
2397
+ const { transform: _t, ...own } = n2;
2398
+ const b = unionBoxes([vectorNodeBounds(own), boundsFromCorners(corners2)]);
2399
+ if (!b) continue;
2400
+ const pad = Math.max(b.width, b.height) * 0.05;
2401
+ const lines = vectorSymmetryAxes(n2.symmetry, { x: b.x - pad, y: b.y - pad, width: b.width + 2 * pad, height: b.height + 2 * pad }).map(([a, c]) => {
2402
+ const p = toScreen(state, applyMatrix(world, a));
2403
+ const q = toScreen(state, applyMatrix(world, c));
2404
+ return { x1: p.x, y1: p.y, x2: q.x, y2: q.y };
2405
+ });
2406
+ out.push({ nodeId: id, drawing: state.symmetryTarget === id, lines });
2407
+ }
2408
+ return out;
2409
+ }
2410
+ function unionBoxes(list) {
2411
+ const ok = list.filter((b) => !!b);
2412
+ if (!ok.length) return null;
2413
+ const x = Math.min(...ok.map((b) => b.x));
2414
+ const y = Math.min(...ok.map((b) => b.y));
2415
+ return { x, y, width: Math.max(...ok.map((b) => b.x + b.width)) - x, height: Math.max(...ok.map((b) => b.y + b.height)) - y };
2416
+ }
2417
+ function boundsFromCorners(pts) {
2418
+ const xs = pts.map((p) => p.x);
2419
+ const ys = pts.map((p) => p.y);
2420
+ return { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) };
2421
+ }
2306
2422
  function connectorSiteOverlay(state) {
2307
2423
  if (state.tool !== "connector" || !state.pointer) return [];
2308
2424
  const drawing = state.interaction?.type === "draw" && state.interaction.tool === "connector" ? state.interaction.nodeId : void 0;
@@ -2575,6 +2691,7 @@ function getVectorOverlayRenderData(state) {
2575
2691
  frames: frameLabels(state),
2576
2692
  connectorSites: connectorSiteOverlay(state),
2577
2693
  shading: shadingOverlay(state),
2694
+ symmetry: symmetryOverlay(state),
2578
2695
  trackingPoints: (state.trackingPoints ?? []).map((p) => toScreen(state, p)),
2579
2696
  typedEntry: state.typedEntry ? { text: state.typedEntry, ...state.pointer ? toScreen(state, state.pointer) : { x: 0, y: 0 } } : null
2580
2697
  };
@@ -3595,9 +3712,9 @@ function nodeGroup(node, notes) {
3595
3712
  if (node.filters?.length) notes.push(`${label(node)}: filters (shadows, glows, blurs) are not part of Lottie (left out).`);
3596
3713
  const m = nodeMatrix(node);
3597
3714
  let items = [];
3598
- const live = (node.effects ?? []).some((e) => e.enabled !== false && (e.type === "wiggle" || e.type === "envelope"));
3599
- if (node.type === "text" || node.type === "instance" || live || isVectorMaskGroup(node) || isVectorFrame(node)) {
3600
- if (live) notes.push(`${label(node)}: wiggle and envelope effects have no Lottie equivalent (exported as drawn).`);
3715
+ const live = (node.effects ?? []).some((e) => e.enabled !== false && (e.type === "wiggle" || e.type === "envelope" || e.type === "contour"));
3716
+ if (node.type === "text" || node.type === "instance" || live || isVectorMaskGroup(node) || isVectorFrame(node) || isVectorSymmetryGroup(node)) {
3717
+ if (live) notes.push(`${label(node)}: wiggle, envelope and contour effects have no Lottie equivalent (exported as drawn).`);
3601
3718
  if (isVectorMaskGroup(node) && node.mask?.type !== "clip") notes.push(`${label(node)}: opacity masks are not exported (the contents are drawn whole).`);
3602
3719
  const drawn = isVectorFrame(node) ? vectorFrameDrawn(node) : node;
3603
3720
  const shapes = isVectorMaskGroup(drawn) && drawn.mask?.type === "clip" ? vectorAppearanceShapes({ ...drawn, transform: void 0 }) : null;
@@ -6831,6 +6948,7 @@ var VectorEditorController = class _VectorEditorController {
6831
6948
  trackingPoints: this.trackingPoints,
6832
6949
  typedEntry: this.typed,
6833
6950
  shadingPoint: this.getShadingSelection(),
6951
+ symmetryTarget: this.symmetryTarget && this.doc.nodes.some((n2) => n2.id === this.symmetryTarget) ? this.symmetryTarget : null,
6834
6952
  canUndo: h.canUndo,
6835
6953
  canRedo: h.canRedo,
6836
6954
  undoLabel: h.undoName,
@@ -9134,6 +9252,155 @@ var VectorEditorController = class _VectorEditorController {
9134
9252
  return { ok: true, id };
9135
9253
  }
9136
9254
  // -----------------------------------------------------------------------------------------
9255
+ // Live symmetry groups
9256
+ /** The symmetry group new pen, pencil, shape and text art goes into (see `enterSymmetry`), or null. */
9257
+ symmetryTarget = null;
9258
+ /** The symmetry group drawn art goes into, while it is entered and still a top-level, unturned symmetry group. */
9259
+ drawingTarget() {
9260
+ const id = this.symmetryTarget;
9261
+ if (!id) return null;
9262
+ const n2 = this.doc.nodes.find((x) => x.id === id);
9263
+ if (!isVectorSymmetryGroup(n2) || n2.transform || n2.locked) {
9264
+ this.symmetryTarget = null;
9265
+ return null;
9266
+ }
9267
+ return n2;
9268
+ }
9269
+ /** Adds a node a drawing tool makes: into the entered symmetry group, else on top. */
9270
+ addDrawn(node) {
9271
+ const target = this.tool === "frame" || this.tool === "connector" ? null : this.drawingTarget();
9272
+ if (!target) return this.setDoc([...this.doc.nodes, node]);
9273
+ this.setDoc(this.doc.nodes.map((n2) => n2.id === target.id && n2.type === "group" ? { ...n2, children: [...n2.children, node] } : n2));
9274
+ }
9275
+ /**
9276
+ * Make Symmetry: puts objects that share a parent (default: the selection) into a live symmetry
9277
+ * group that draws them with their mirrored or rotated copies, the axes through `center`
9278
+ * (default: the objects' centre, or the artboard's when there are none, making an empty group to
9279
+ * draw into). One undo step; selects the group (and enters it when empty). Returns its id.
9280
+ */
9281
+ makeSymmetry(input, options = {}) {
9282
+ const symmetry = sanitizeVectorSymmetry({ ...input, center: input.center ?? { x: 0, y: 0 } });
9283
+ if (!symmetry) return { ok: false, message: `Unknown symmetry "${input.mode}": use ${VECTOR_SYMMETRY_MODES.join(", ")}.` };
9284
+ if (input.copies !== void 0 && !(input.copies >= 2 && input.copies <= 64)) return { ok: false, message: "copies must be from 2 to 64." };
9285
+ const ids = [...new Set(options.ids ?? this.selectedRoots())];
9286
+ for (const id2 of ids) {
9287
+ const n2 = this.getNode(id2);
9288
+ if (!n2) return { ok: false, message: `There is no node "${id2}".` };
9289
+ if (n2.locked) return { ok: false, message: `"${id2}" is locked; unlock it first.` };
9290
+ }
9291
+ const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
9292
+ const parent = locs[0]?.parent ?? null;
9293
+ if (locs.some((l) => (l.parent?.id ?? null) !== (parent?.id ?? null))) return { ok: false, message: "The objects must share a parent (pick objects at one level)." };
9294
+ const b = unionBounds(ids.map((id2) => vectorNodeBounds(this.getNode(id2))));
9295
+ const center = input.center ?? (b ? { x: b.x + b.width / 2, y: b.y + b.height / 2 } : { x: this.doc.width / 2, y: this.doc.height / 2 });
9296
+ const id = this.newId();
9297
+ const group = { id, type: "group", name: input.name ?? "Symmetry", symmetry: { ...symmetry, center }, children: [] };
9298
+ const order = ids.slice().sort((a, c) => locs[ids.indexOf(a)].index - locs[ids.indexOf(c)].index);
9299
+ const set = new Set(order);
9300
+ this.change("Make Symmetry", () => {
9301
+ if (!order.length) this.setDoc([...this.doc.nodes, group]);
9302
+ else {
9303
+ const regroup = (list) => {
9304
+ const members = order.map((m) => list.find((n2) => n2.id === m));
9305
+ const top = Math.max(...members.map((m) => list.indexOf(m)));
9306
+ const out = [];
9307
+ list.forEach((n2, i) => {
9308
+ if (i === top) out.push({ ...group, children: members });
9309
+ if (!set.has(n2.id)) out.push(n2);
9310
+ });
9311
+ return out;
9312
+ };
9313
+ if (!parent) this.setDoc(regroup(this.doc.nodes));
9314
+ else this.setDoc(mapTree(this.doc.nodes, (n2) => n2.id === parent.id && n2.type === "group" ? { ...n2, children: regroup(n2.children) } : n2));
9315
+ }
9316
+ this.selection = [id];
9317
+ this.anchors = [];
9318
+ this.editingId = null;
9319
+ if (!order.length && !parent) this.symmetryTarget = id;
9320
+ });
9321
+ return { ok: true, id };
9322
+ }
9323
+ /** Symmetry groups among `ids` (default: the selection, or the group holding it). */
9324
+ symmetryTargets(ids) {
9325
+ const out = /* @__PURE__ */ new Map();
9326
+ for (const id of ids ?? this.selection) {
9327
+ const n2 = this.getNode(id);
9328
+ if (isVectorSymmetryGroup(n2)) out.set(n2.id, n2);
9329
+ else if (!ids) {
9330
+ const p = locateVectorNode(this.doc.nodes, id)?.parent;
9331
+ if (isVectorSymmetryGroup(p)) out.set(p.id, p);
9332
+ }
9333
+ }
9334
+ return [...out.values()];
9335
+ }
9336
+ /** Changes symmetry groups (default: the selected one, or the one holding the selection): mode, centre (the group's space), rotation, radial copies and mirroring. One undo step (merged while `merge` is the same key). */
9337
+ setSymmetry(patch, options = {}) {
9338
+ if (options.ids) {
9339
+ for (const id of options.ids) if (!isVectorSymmetryGroup(this.getNode(id))) return { ok: false, message: `"${id}" is not a symmetry group (make one with vector.makeSymmetry).` };
9340
+ }
9341
+ const groups = this.symmetryTargets(options.ids);
9342
+ if (!groups.length) return { ok: false, message: "Select a symmetry group (or something in one)." };
9343
+ if (patch.mode !== void 0 && !VECTOR_SYMMETRY_MODES.includes(patch.mode)) return { ok: false, message: `Unknown symmetry "${patch.mode}": use ${VECTOR_SYMMETRY_MODES.join(", ")}.` };
9344
+ if (patch.copies !== void 0 && !(patch.copies >= 2 && patch.copies <= 64)) return { ok: false, message: "copies must be from 2 to 64." };
9345
+ const next = new Map(groups.map((g) => [g.id, sanitizeVectorSymmetry({ ...g.symmetry, ...patch })]));
9346
+ if ([...next.values()].some((v) => !v)) return { ok: false, message: "center must be { x, y } numbers." };
9347
+ this.change(
9348
+ "Symmetry",
9349
+ () => {
9350
+ this.setDoc(mapTree(this.doc.nodes, (n2) => next.has(n2.id) && n2.type === "group" ? { ...n2, symmetry: next.get(n2.id) } : n2));
9351
+ },
9352
+ options.merge
9353
+ );
9354
+ return { ok: true };
9355
+ }
9356
+ /**
9357
+ * Release Symmetry: the groups stop repeating their children. With `expand`, the copies become
9358
+ * real objects in the group (new ids); otherwise they go. One undo step. Returns the ids of the
9359
+ * objects added by expanding.
9360
+ */
9361
+ releaseSymmetry(options = {}) {
9362
+ const groups = this.symmetryTargets(options.ids);
9363
+ if (!groups.length) return { ok: false, message: "Select a symmetry group (or something in one)." };
9364
+ const added = [];
9365
+ const next = /* @__PURE__ */ new Map();
9366
+ for (const g of groups) {
9367
+ const { symmetry: _s, ...rest } = g;
9368
+ if (!options.expand) {
9369
+ next.set(g.id, rest);
9370
+ continue;
9371
+ }
9372
+ const drawn = vectorSymmetryDrawn(g);
9373
+ const fresh = (n2) => {
9374
+ const id = this.newId();
9375
+ added.push(id);
9376
+ const { locked: _l, ...plain2 } = n2;
9377
+ return plain2.type === "group" ? { ...plain2, id, children: plain2.children.map(fresh) } : { ...plain2, id };
9378
+ };
9379
+ next.set(g.id, { ...rest, children: [...g.children, ...drawn.children.slice(g.children.length).map(fresh)] });
9380
+ }
9381
+ this.change(options.expand ? "Expand Symmetry" : "Release Symmetry", () => {
9382
+ this.setDoc(mapTree(this.doc.nodes, (n2) => next.get(n2.id) ?? n2));
9383
+ if (this.symmetryTarget && next.has(this.symmetryTarget)) this.symmetryTarget = null;
9384
+ });
9385
+ return { ok: true, ids: added.filter((id) => !!this.getNode(id)) };
9386
+ }
9387
+ /** Enters a top-level symmetry group: new pen, pencil, shape and text art goes into it (mirrored as it is drawn) until `exitSymmetry` or Escape. */
9388
+ enterSymmetry(id) {
9389
+ const n2 = this.doc.nodes.find((x) => x.id === id);
9390
+ if (!isVectorSymmetryGroup(n2)) return { ok: false, message: `"${id}" is not a top-level symmetry group.` };
9391
+ if (n2.transform) return { ok: false, message: "Drawing into a turned symmetry group is not supported; draw beside it and move the art in." };
9392
+ this.symmetryTarget = id;
9393
+ this.selection = [id];
9394
+ this.notify();
9395
+ return { ok: true };
9396
+ }
9397
+ /** Stops drawing into the entered symmetry group. */
9398
+ exitSymmetry() {
9399
+ if (!this.symmetryTarget) return;
9400
+ this.symmetryTarget = null;
9401
+ this.notify();
9402
+ }
9403
+ // -----------------------------------------------------------------------------------------
9137
9404
  // PDF
9138
9405
  /**
9139
9406
  * The document as a vector PDF (see `vectorDocumentToPdf`): one page of the artboard, or a page
@@ -11605,7 +11872,7 @@ var VectorEditorController = class _VectorEditorController {
11605
11872
  }
11606
11873
  const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
11607
11874
  const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt2.x, y: pt2.y }], closed: false }] };
11608
- this.setDoc([...this.doc.nodes, node]);
11875
+ this.addDrawn(node);
11609
11876
  this.pen = { nodeId: node.id, subpath: 0 };
11610
11877
  this.selection = [node.id];
11611
11878
  this.editingId = node.id;
@@ -11626,7 +11893,7 @@ var VectorEditorController = class _VectorEditorController {
11626
11893
  strokeLinejoin: this.defaults.strokeLinejoin ?? "round",
11627
11894
  subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
11628
11895
  };
11629
- this.setDoc([...this.doc.nodes, node]);
11896
+ this.addDrawn(node);
11630
11897
  const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
11631
11898
  lazy.push([p.x, p.y]);
11632
11899
  const mode = this.editing.pencilPressure;
@@ -11644,7 +11911,7 @@ var VectorEditorController = class _VectorEditorController {
11644
11911
  const kind = this.tool === "frame" ? "rect" : this.tool;
11645
11912
  const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
11646
11913
  if (this.tool === "frame") Object.assign(node, { fill: "#ffffff", stroke: "#a3a3a3", strokeWidth: 1 / this.viewport.zoom });
11647
- this.setDoc([...this.doc.nodes, node]);
11914
+ this.addDrawn(node);
11648
11915
  this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before, targets };
11649
11916
  this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
11650
11917
  this.notify(true);
@@ -12787,7 +13054,7 @@ var VectorEditorController = class _VectorEditorController {
12787
13054
  this.endTextEdit();
12788
13055
  const before = this.snapshot();
12789
13056
  if (replace) this.setDoc(mapTree(this.doc.nodes, (n2) => n2.id === replace ? node : n2));
12790
- else this.setDoc([...this.doc.nodes, node]);
13057
+ else this.addDrawn(node);
12791
13058
  this.selection = [node.id];
12792
13059
  this.anchors = [];
12793
13060
  this.textEdit = { nodeId: node.id, anchor: 0, focus: 0, before, key: `text-edit:${node.id}:${++this.textEditCount}`, created: true };
@@ -13341,6 +13608,7 @@ var VectorEditorController = class _VectorEditorController {
13341
13608
  case "Escape":
13342
13609
  if (this.drag) this.cancelDrag();
13343
13610
  else if (this.trackingPoints.length) this.clearTracking();
13611
+ else if (this.symmetryTarget && !this.pen) this.exitSymmetry();
13344
13612
  else if (this.widthEdit) this.closeWidthPointEdit();
13345
13613
  else if (this.tool === "width" && this.widthPoints.length) {
13346
13614
  this.widthPoints = [];
@@ -13534,11 +13802,12 @@ function pencilPressureProfile(points, pressures) {
13534
13802
  }
13535
13803
 
13536
13804
  // src/vector/commands.ts
13537
- 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. envelope: style (a warp preset: arc|arcLower|arcUpper|arch|bulge|shellLower|shellUpper|flag|wave|fish|rise|fisheye|inflate|squeeze|twist, or mesh), bend, horizontal, vertical (percent, \u2212100\u2026100), orientation horizontal|vertical, rows, columns, mesh { us, vs, points } (node space; prefer vector.envelope).";
13805
+ 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. contour: direction inside|outside|center, steps, offset, fillColor / strokeColor (end colours, null keeps the colour), acceleration (\u2212100\u2026100), join. envelope: style (a warp preset: arc|arcLower|arcUpper|arch|bulge|shellLower|shellUpper|flag|wave|fish|rise|fisheye|inflate|squeeze|twist, or mesh), bend, horizontal, vertical (percent, \u2212100\u2026100), orientation horizontal|vertical, rows, columns, mesh { us, vs, points } (node space; prefer vector.envelope).";
13538
13806
  var fail = (message) => {
13539
13807
  throw new Error(message);
13540
13808
  };
13541
13809
  var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
13810
+ var PARENT_ID = { type: "string", description: "A group to add into, on top of its children (default: the document). Coordinates are the group's own space (the document's for an unturned top-level group)." };
13542
13811
  var ANCHORS = {
13543
13812
  type: "array",
13544
13813
  items: {
@@ -13678,6 +13947,11 @@ function createVectorCommands(controller, options = {}) {
13678
13947
  const prefix = options.prefix ?? "vector.";
13679
13948
  const group = options.group ?? "Vector";
13680
13949
  const base = { group, ...options.scope ? { scope: options.scope } : {} };
13950
+ const parentOf = (parentId) => {
13951
+ if (parentId === void 0) return {};
13952
+ if (controller.getNode(parentId)?.type !== "group") fail(`"${parentId}" is not a group to add into.`);
13953
+ return { parentId };
13954
+ };
13681
13955
  const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
13682
13956
  const ids = (params) => {
13683
13957
  const list2 = params?.ids ?? controller.getState().selection;
@@ -13724,7 +13998,8 @@ function createVectorCommands(controller, options = {}) {
13724
13998
  ...isVectorMaskGroup(n2) ? { mask: vectorMaskSummary(n2) } : {},
13725
13999
  ...isVectorFrame(n2) ? { frameRect: n2.frame } : {},
13726
14000
  ...n2.resizing ? { resizing: n2.resizing } : {},
13727
- ...n2.type === "path" && n2.connector ? { connector: n2.connector } : {}
14001
+ ...n2.type === "path" && n2.connector ? { connector: n2.connector } : {},
14002
+ ...n2.type === "group" && n2.symmetry ? { symmetry: n2.symmetry } : {}
13728
14003
  });
13729
14004
  if (n2.type === "group") visit(n2.children, depth + 1);
13730
14005
  }
@@ -14605,6 +14880,89 @@ function createVectorCommands(controller, options = {}) {
14605
14880
  issues: state.constraintIssues
14606
14881
  };
14607
14882
  };
14883
+ const SYMMETRY_PROPS = {
14884
+ mode: { type: "string", enum: VECTOR_SYMMETRY_MODES, description: "vertical (mirror left / right), horizontal (top / bottom), dual (four quarters), diagonal, or radial (turns round the centre)." },
14885
+ center: { type: "object", properties: { x: { type: "number", description: "Document units (the group's space)." }, y: { type: "number", description: "Document units (the group's space)." } }, required: ["x", "y"], additionalProperties: false, description: "Where the axes cross." },
14886
+ rotation: { type: "number", description: "Turn of the axes, degrees clockwise (default 0)." },
14887
+ copies: { type: "integer", minimum: 2, maximum: 64, description: "Radial: how many turns round the circle (default 6)." },
14888
+ mirror: { type: "boolean", description: "Radial: mirror each turn too, like a kaleidoscope (default false)." }
14889
+ };
14890
+ const symmetryResult = (id) => {
14891
+ const n2 = controller.getNode(id);
14892
+ return n2?.type === "group" && n2.symmetry ? { id, symmetry: n2.symmetry, children: n2.children.map((c) => c.id) } : fail(`"${id}" is not a symmetry group.`);
14893
+ };
14894
+ const symmetryCommands = () => [
14895
+ {
14896
+ ...base,
14897
+ id: `${prefix}makeSymmetry`,
14898
+ label: "Make Symmetry\u2026",
14899
+ keywords: ["symmetry", "mirror", "kaleidoscope", "radial", "mandala", "reflect", "live symmetry"],
14900
+ description: "Puts objects sharing a parent (`ids`, default the selection) into a live symmetry group: it draws them with their mirrored or rotated copies, which follow every edit of the originals. center defaults to the objects' centre (the artboard's with no objects: an empty group to add art to with parentId). One undo step. Returns { id, symmetry, children }.",
14901
+ params: { type: "object", properties: { ids: { ...IDS, description: "Objects to repeat (default: the selection; [] for an empty group)." }, ...SYMMETRY_PROPS, name: { type: "string", description: 'Group name (default "Symmetry").' } }, required: ["mode"], additionalProperties: false },
14902
+ undoable: true,
14903
+ run: (_ctx, params) => {
14904
+ const { ids: list2, ...input } = params;
14905
+ const r = controller.makeSymmetry(input, list2 ? { ids: list2 } : {});
14906
+ if (!r.ok) fail(r.message);
14907
+ return symmetryResult(r.id);
14908
+ }
14909
+ },
14910
+ {
14911
+ ...base,
14912
+ id: `${prefix}setSymmetry`,
14913
+ label: "Symmetry Options\u2026",
14914
+ keywords: ["symmetry", "mirror", "radial", "axis", "copies"],
14915
+ description: "Changes symmetry groups (`ids`, default the selected one or the one holding the selection): mode, center, rotation, copies, mirror. Pass at least one. One undo step. Returns { groups }.",
14916
+ params: { type: "object", properties: { ids: { ...IDS, description: "Symmetry group ids (default: the selection)." }, ...SYMMETRY_PROPS }, additionalProperties: false },
14917
+ undoable: true,
14918
+ run: (_ctx, params) => {
14919
+ const { ids: list2, ...patch } = params ?? {};
14920
+ if (!Object.keys(patch).length) fail("Pass at least one of mode, center, rotation, copies, mirror.");
14921
+ check(controller.setSymmetry(patch, list2 ? { ids: list2 } : {}));
14922
+ const ids2 = list2 ?? controller.getState().selection.filter((id) => controller.getNode(id)?.type === "group");
14923
+ return { groups: ids2.filter((id) => controller.getNode(id)?.symmetry).map(symmetryResult) };
14924
+ }
14925
+ },
14926
+ {
14927
+ ...base,
14928
+ id: `${prefix}releaseSymmetry`,
14929
+ label: "Release Symmetry",
14930
+ keywords: ["symmetry", "release", "expand", "bake"],
14931
+ description: "Stops symmetry groups repeating their children. With expand: true the copies become real, editable objects in the group (new ids); otherwise they go. One undo step. Returns { ids } of the objects expanding added.",
14932
+ params: { type: "object", properties: { ids: { ...IDS, description: "Symmetry group ids (default: the selection)." }, expand: { type: "boolean", description: "Keep the copies as real objects (default false)." } }, additionalProperties: false },
14933
+ undoable: true,
14934
+ enabled: enabledFor,
14935
+ run: (_ctx, params) => {
14936
+ const r = controller.releaseSymmetry({ ...params?.ids ? { ids: params.ids } : {}, ...params?.expand ? { expand: true } : {} });
14937
+ check(r);
14938
+ return { ids: r.ids ?? [] };
14939
+ }
14940
+ },
14941
+ {
14942
+ ...base,
14943
+ id: `${prefix}enterSymmetry`,
14944
+ label: "Draw Inside Symmetry",
14945
+ keywords: ["symmetry", "draw inside", "mirror drawing"],
14946
+ description: "Hands the canvas to the user drawing inside a top-level symmetry group: new pen, pencil, shape and text art goes into it, mirrored as it is drawn, until vector.exitSymmetry or Escape. Returns { id }.",
14947
+ params: { type: "object", properties: { id: { type: "string", description: "The symmetry group." } }, required: ["id"], additionalProperties: false },
14948
+ run: (_ctx, params) => {
14949
+ check(controller.enterSymmetry(params.id));
14950
+ return { id: params.id };
14951
+ }
14952
+ },
14953
+ {
14954
+ ...base,
14955
+ id: `${prefix}exitSymmetry`,
14956
+ label: "Stop Drawing Inside Symmetry",
14957
+ keywords: ["symmetry", "exit"],
14958
+ description: "New art goes on top of the document again (no symmetry group is entered). Returns { ok: true }.",
14959
+ params: { type: "object", properties: {}, additionalProperties: false },
14960
+ run: () => {
14961
+ controller.exitSymmetry();
14962
+ return { ok: true };
14963
+ }
14964
+ }
14965
+ ];
14608
14966
  const pdfCommands = () => [
14609
14967
  {
14610
14968
  ...base,
@@ -15363,12 +15721,16 @@ function createVectorCommands(controller, options = {}) {
15363
15721
  points: { type: "integer", minimum: 3 },
15364
15722
  innerRatio: { type: "number", minimum: 0, maximum: 1 },
15365
15723
  name: { type: "string" },
15724
+ parentId: PARENT_ID,
15366
15725
  ...STYLE_PROPS
15367
15726
  },
15368
15727
  required: ["kind", "x", "y", "width", "height"]
15369
15728
  },
15370
15729
  undoable: true,
15371
- run: (_ctx, params) => ({ id: controller.addShape(params) })
15730
+ run: (_ctx, params) => {
15731
+ const { parentId, ...input } = params;
15732
+ return { id: controller.addShape(input, parentOf(parentId)) };
15733
+ }
15372
15734
  },
15373
15735
  {
15374
15736
  ...base,
@@ -15376,10 +15738,10 @@ function createVectorCommands(controller, options = {}) {
15376
15738
  label: "Add Path\u2026",
15377
15739
  keywords: ["svg", "path data", "bezier", "curve"],
15378
15740
  description: "Adds a path from SVG path data (`d`), in document units.",
15379
- params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, ...STYLE_PROPS }, required: ["d"] },
15741
+ params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, parentId: PARENT_ID, ...STYLE_PROPS }, required: ["d"] },
15380
15742
  undoable: true,
15381
15743
  run: (_ctx, params) => {
15382
- const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
15744
+ const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} }, parentOf(params.parentId));
15383
15745
  const node = controller.getNode(id);
15384
15746
  if (node?.type === "path" && !node.subpaths.length) {
15385
15747
  controller.undo();
@@ -15436,6 +15798,7 @@ function createVectorCommands(controller, options = {}) {
15436
15798
  ...traceCommands(),
15437
15799
  ...lottieCommands(),
15438
15800
  ...pdfCommands(),
15801
+ ...symmetryCommands(),
15439
15802
  {
15440
15803
  ...base,
15441
15804
  id: `${prefix}setCorners`,
@@ -16427,6 +16790,8 @@ export {
16427
16790
  VECTOR_STROKE_SETTING_KEYS,
16428
16791
  VECTOR_STYLE_KEYS,
16429
16792
  VECTOR_SYMBOL_MAX_DEPTH,
16793
+ VECTOR_SYMMETRY_MODES,
16794
+ VECTOR_SYMMETRY_MODE_LABELS,
16430
16795
  VECTOR_TEXT_ALIGNS,
16431
16796
  VECTOR_TEXT_ALIGN_LABELS,
16432
16797
  VECTOR_TEXT_ALIGN_OPTIONS,
@@ -16612,6 +16977,7 @@ export {
16612
16977
  isVectorMaskType,
16613
16978
  isVectorMeshGradient,
16614
16979
  isVectorShadedPaint,
16980
+ isVectorSymmetryGroup,
16615
16981
  isVectorTextTarget,
16616
16982
  joinSubpaths,
16617
16983
  knifePath,
@@ -16720,6 +17086,7 @@ export {
16720
17086
  sanitizeVectorShadedPaint,
16721
17087
  sanitizeVectorStrokeSettings,
16722
17088
  sanitizeVectorStrokes,
17089
+ sanitizeVectorSymmetry,
16723
17090
  sanitizeVectorTextFields,
16724
17091
  sanitizeVectorTextFrame,
16725
17092
  sanitizeWidthProfile,
@@ -16763,6 +17130,7 @@ export {
16763
17130
  transformVectorNode,
16764
17131
  transformVectorPaint,
16765
17132
  transformVectorShadedPaint,
17133
+ transformVectorSymmetry,
16766
17134
  translateAnchor,
16767
17135
  translateMatrix,
16768
17136
  trimIntervals,
@@ -16927,6 +17295,9 @@ export {
16927
17295
  vectorStrokes,
16928
17296
  vectorSymbolUses,
16929
17297
  vectorSymbolsUsedIn,
17298
+ vectorSymmetryAxes,
17299
+ vectorSymmetryDrawn,
17300
+ vectorSymmetryMatrices,
16930
17301
  vectorTargetPaint,
16931
17302
  vectorTextBoxOutline,
16932
17303
  vectorTextCaret,