@pixerate/editor 0.118.0 → 0.120.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 +1 -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-R2JE6SA7.js → chunk-BOWUU4LJ.js} +42 -7
  7. package/dist/chunk-BOWUU4LJ.js.map +1 -0
  8. package/dist/{chunk-M7O4BJRH.cjs → chunk-DFLJHLKA.cjs} +36 -36
  9. package/dist/{chunk-M7O4BJRH.cjs.map → chunk-DFLJHLKA.cjs.map} +1 -1
  10. package/dist/{chunk-2J3LS7JZ.cjs → chunk-HTAFDTNR.cjs} +221 -186
  11. package/dist/chunk-HTAFDTNR.cjs.map +1 -0
  12. package/dist/{chunk-L2T6EJ5S.cjs → chunk-L3PXFMOF.cjs} +38 -25
  13. package/dist/chunk-L3PXFMOF.cjs.map +1 -0
  14. package/dist/{chunk-IVSNUXW7.js → chunk-T7IZDNSL.js} +37 -24
  15. package/dist/chunk-T7IZDNSL.js.map +1 -0
  16. package/dist/{chunk-BYAWOAST.cjs → chunk-TS5IEUW6.cjs} +26 -25
  17. package/dist/chunk-TS5IEUW6.cjs.map +1 -0
  18. package/dist/{chunk-VD2UDVF7.js → chunk-UMVW2Q66.js} +21 -5
  19. package/dist/chunk-UMVW2Q66.js.map +1 -0
  20. package/dist/{chunk-FODKPD44.cjs → chunk-VYW7JBTM.cjs} +790 -549
  21. package/dist/chunk-VYW7JBTM.cjs.map +1 -0
  22. package/dist/{chunk-T2P5TIZ6.js → chunk-WAI6ATHC.js} +2 -2
  23. package/dist/{chunk-EBSYZRSW.cjs → chunk-WMRIG4XQ.cjs} +21 -5
  24. package/dist/chunk-WMRIG4XQ.cjs.map +1 -0
  25. package/dist/{chunk-7LNZAWSJ.js → chunk-WORWH5K5.js} +699 -458
  26. package/dist/chunk-WORWH5K5.js.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-Dul1SbPv.d.ts → export-D2D8hb_u.d.ts} +18 -4
  30. package/dist/{export-DMOuzgZZ.d.cts → export-fP2hZYjm.d.cts} +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-CVdFc80-.d.cts → svg-S9zzvqaZ.d.cts} +5 -1
  51. package/dist/{svg-DlEuhwoX.d.ts → svg-leZpRsJB.d.ts} +5 -1
  52. package/dist/{types-CDLGhAZp.d.ts → types-BgwxFWvn.d.ts} +59 -3
  53. package/dist/{types-Tfly4Aya.d.cts → types-DdRTDnza.d.cts} +59 -3
  54. package/dist/vector/index.cjs +1314 -1009
  55. package/dist/vector/index.cjs.map +1 -1
  56. package/dist/vector/index.d.cts +110 -5
  57. package/dist/vector/index.d.ts +110 -5
  58. package/dist/vector/index.js +319 -14
  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 +5 -2
  62. package/skills/pixerate-vector/SKILL.md +13 -1
  63. package/skills/pixerate-vector/references/commands.md +2 -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-WAI6ATHC.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-WAI6ATHC.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,
@@ -309,6 +313,7 @@ import {
309
313
  subpathSegment,
310
314
  subpathSegmentCount,
311
315
  subpathsToBezier,
316
+ sweepPath,
312
317
  taperWaveFactor,
313
318
  tintVectorNode,
314
319
  transformVectorEffects,
@@ -316,6 +321,7 @@ import {
316
321
  transformVectorNode,
317
322
  transformVectorPaint,
318
323
  transformVectorShadedPaint,
324
+ transformVectorSymmetry,
319
325
  translateAnchor,
320
326
  translateMatrix,
321
327
  trimIntervals,
@@ -429,6 +435,9 @@ import {
429
435
  vectorStrokes,
430
436
  vectorSymbolUses,
431
437
  vectorSymbolsUsedIn,
438
+ vectorSymmetryAxes,
439
+ vectorSymmetryDrawn,
440
+ vectorSymmetryMatrices,
432
441
  vectorTargetPaint,
433
442
  vectorTextBoxOutline,
434
443
  vectorTextCaret,
@@ -467,7 +476,7 @@ import {
467
476
  withVectorStrokes,
468
477
  withVectorTargetPaint,
469
478
  zigzagPaths
470
- } from "../chunk-7LNZAWSJ.js";
479
+ } from "../chunk-WORWH5K5.js";
471
480
  import {
472
481
  bezierPathBounds,
473
482
  bezierPathToSvg,
@@ -2371,6 +2380,46 @@ function shadingOverlay(state) {
2371
2380
  }
2372
2381
  return out;
2373
2382
  }
2383
+ function symmetryOverlay(state) {
2384
+ const ids = /* @__PURE__ */ new Set([...state.selection, ...state.symmetryTarget ? [state.symmetryTarget] : []]);
2385
+ const out = [];
2386
+ for (const id of ids) {
2387
+ const n2 = findVectorNode(state.document.nodes, id);
2388
+ if (!isVectorSymmetryGroup(n2)) continue;
2389
+ const world = vectorWorldMatrix(state.document.nodes, id);
2390
+ if (!world) continue;
2391
+ const inv = invertMatrix(world);
2392
+ const corners2 = [
2393
+ { x: 0, y: 0 },
2394
+ { x: state.document.width, y: 0 },
2395
+ { x: 0, y: state.document.height },
2396
+ { x: state.document.width, y: state.document.height }
2397
+ ].map((p) => applyMatrix(inv, p));
2398
+ const { transform: _t, ...own } = n2;
2399
+ const b = unionBoxes([vectorNodeBounds(own), boundsFromCorners(corners2)]);
2400
+ if (!b) continue;
2401
+ const pad = Math.max(b.width, b.height) * 0.05;
2402
+ 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]) => {
2403
+ const p = toScreen(state, applyMatrix(world, a));
2404
+ const q = toScreen(state, applyMatrix(world, c));
2405
+ return { x1: p.x, y1: p.y, x2: q.x, y2: q.y };
2406
+ });
2407
+ out.push({ nodeId: id, drawing: state.symmetryTarget === id, lines });
2408
+ }
2409
+ return out;
2410
+ }
2411
+ function unionBoxes(list) {
2412
+ const ok = list.filter((b) => !!b);
2413
+ if (!ok.length) return null;
2414
+ const x = Math.min(...ok.map((b) => b.x));
2415
+ const y = Math.min(...ok.map((b) => b.y));
2416
+ 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 };
2417
+ }
2418
+ function boundsFromCorners(pts) {
2419
+ const xs = pts.map((p) => p.x);
2420
+ const ys = pts.map((p) => p.y);
2421
+ return { x: Math.min(...xs), y: Math.min(...ys), width: Math.max(...xs) - Math.min(...xs), height: Math.max(...ys) - Math.min(...ys) };
2422
+ }
2374
2423
  function connectorSiteOverlay(state) {
2375
2424
  if (state.tool !== "connector" || !state.pointer) return [];
2376
2425
  const drawing = state.interaction?.type === "draw" && state.interaction.tool === "connector" ? state.interaction.nodeId : void 0;
@@ -2643,6 +2692,7 @@ function getVectorOverlayRenderData(state) {
2643
2692
  frames: frameLabels(state),
2644
2693
  connectorSites: connectorSiteOverlay(state),
2645
2694
  shading: shadingOverlay(state),
2695
+ symmetry: symmetryOverlay(state),
2646
2696
  trackingPoints: (state.trackingPoints ?? []).map((p) => toScreen(state, p)),
2647
2697
  typedEntry: state.typedEntry ? { text: state.typedEntry, ...state.pointer ? toScreen(state, state.pointer) : { x: 0, y: 0 } } : null
2648
2698
  };
@@ -3663,9 +3713,9 @@ function nodeGroup(node, notes) {
3663
3713
  if (node.filters?.length) notes.push(`${label(node)}: filters (shadows, glows, blurs) are not part of Lottie (left out).`);
3664
3714
  const m = nodeMatrix(node);
3665
3715
  let items = [];
3666
- const live = (node.effects ?? []).some((e) => e.enabled !== false && (e.type === "wiggle" || e.type === "envelope"));
3667
- if (node.type === "text" || node.type === "instance" || live || isVectorMaskGroup(node) || isVectorFrame(node)) {
3668
- if (live) notes.push(`${label(node)}: wiggle and envelope effects have no Lottie equivalent (exported as drawn).`);
3716
+ const live = (node.effects ?? []).some((e) => e.enabled !== false && (e.type === "wiggle" || e.type === "envelope" || e.type === "contour" || e.type === "blockShadow"));
3717
+ if (node.type === "text" || node.type === "instance" || live || isVectorMaskGroup(node) || isVectorFrame(node) || isVectorSymmetryGroup(node)) {
3718
+ if (live) notes.push(`${label(node)}: wiggle, envelope, contour and block shadow effects have no Lottie equivalent (exported as drawn).`);
3669
3719
  if (isVectorMaskGroup(node) && node.mask?.type !== "clip") notes.push(`${label(node)}: opacity masks are not exported (the contents are drawn whole).`);
3670
3720
  const drawn = isVectorFrame(node) ? vectorFrameDrawn(node) : node;
3671
3721
  const shapes = isVectorMaskGroup(drawn) && drawn.mask?.type === "clip" ? vectorAppearanceShapes({ ...drawn, transform: void 0 }) : null;
@@ -6899,6 +6949,7 @@ var VectorEditorController = class _VectorEditorController {
6899
6949
  trackingPoints: this.trackingPoints,
6900
6950
  typedEntry: this.typed,
6901
6951
  shadingPoint: this.getShadingSelection(),
6952
+ symmetryTarget: this.symmetryTarget && this.doc.nodes.some((n2) => n2.id === this.symmetryTarget) ? this.symmetryTarget : null,
6902
6953
  canUndo: h.canUndo,
6903
6954
  canRedo: h.canRedo,
6904
6955
  undoLabel: h.undoName,
@@ -9202,6 +9253,155 @@ var VectorEditorController = class _VectorEditorController {
9202
9253
  return { ok: true, id };
9203
9254
  }
9204
9255
  // -----------------------------------------------------------------------------------------
9256
+ // Live symmetry groups
9257
+ /** The symmetry group new pen, pencil, shape and text art goes into (see `enterSymmetry`), or null. */
9258
+ symmetryTarget = null;
9259
+ /** The symmetry group drawn art goes into, while it is entered and still a top-level, unturned symmetry group. */
9260
+ drawingTarget() {
9261
+ const id = this.symmetryTarget;
9262
+ if (!id) return null;
9263
+ const n2 = this.doc.nodes.find((x) => x.id === id);
9264
+ if (!isVectorSymmetryGroup(n2) || n2.transform || n2.locked) {
9265
+ this.symmetryTarget = null;
9266
+ return null;
9267
+ }
9268
+ return n2;
9269
+ }
9270
+ /** Adds a node a drawing tool makes: into the entered symmetry group, else on top. */
9271
+ addDrawn(node) {
9272
+ const target = this.tool === "frame" || this.tool === "connector" ? null : this.drawingTarget();
9273
+ if (!target) return this.setDoc([...this.doc.nodes, node]);
9274
+ this.setDoc(this.doc.nodes.map((n2) => n2.id === target.id && n2.type === "group" ? { ...n2, children: [...n2.children, node] } : n2));
9275
+ }
9276
+ /**
9277
+ * Make Symmetry: puts objects that share a parent (default: the selection) into a live symmetry
9278
+ * group that draws them with their mirrored or rotated copies, the axes through `center`
9279
+ * (default: the objects' centre, or the artboard's when there are none, making an empty group to
9280
+ * draw into). One undo step; selects the group (and enters it when empty). Returns its id.
9281
+ */
9282
+ makeSymmetry(input, options = {}) {
9283
+ const symmetry = sanitizeVectorSymmetry({ ...input, center: input.center ?? { x: 0, y: 0 } });
9284
+ if (!symmetry) return { ok: false, message: `Unknown symmetry "${input.mode}": use ${VECTOR_SYMMETRY_MODES.join(", ")}.` };
9285
+ if (input.copies !== void 0 && !(input.copies >= 2 && input.copies <= 64)) return { ok: false, message: "copies must be from 2 to 64." };
9286
+ const ids = [...new Set(options.ids ?? this.selectedRoots())];
9287
+ for (const id2 of ids) {
9288
+ const n2 = this.getNode(id2);
9289
+ if (!n2) return { ok: false, message: `There is no node "${id2}".` };
9290
+ if (n2.locked) return { ok: false, message: `"${id2}" is locked; unlock it first.` };
9291
+ }
9292
+ const locs = ids.map((id2) => locateVectorNode(this.doc.nodes, id2));
9293
+ const parent = locs[0]?.parent ?? null;
9294
+ 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)." };
9295
+ const b = unionBounds(ids.map((id2) => vectorNodeBounds(this.getNode(id2))));
9296
+ 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 });
9297
+ const id = this.newId();
9298
+ const group = { id, type: "group", name: input.name ?? "Symmetry", symmetry: { ...symmetry, center }, children: [] };
9299
+ const order = ids.slice().sort((a, c) => locs[ids.indexOf(a)].index - locs[ids.indexOf(c)].index);
9300
+ const set = new Set(order);
9301
+ this.change("Make Symmetry", () => {
9302
+ if (!order.length) this.setDoc([...this.doc.nodes, group]);
9303
+ else {
9304
+ const regroup = (list) => {
9305
+ const members = order.map((m) => list.find((n2) => n2.id === m));
9306
+ const top = Math.max(...members.map((m) => list.indexOf(m)));
9307
+ const out = [];
9308
+ list.forEach((n2, i) => {
9309
+ if (i === top) out.push({ ...group, children: members });
9310
+ if (!set.has(n2.id)) out.push(n2);
9311
+ });
9312
+ return out;
9313
+ };
9314
+ if (!parent) this.setDoc(regroup(this.doc.nodes));
9315
+ else this.setDoc(mapTree(this.doc.nodes, (n2) => n2.id === parent.id && n2.type === "group" ? { ...n2, children: regroup(n2.children) } : n2));
9316
+ }
9317
+ this.selection = [id];
9318
+ this.anchors = [];
9319
+ this.editingId = null;
9320
+ if (!order.length && !parent) this.symmetryTarget = id;
9321
+ });
9322
+ return { ok: true, id };
9323
+ }
9324
+ /** Symmetry groups among `ids` (default: the selection, or the group holding it). */
9325
+ symmetryTargets(ids) {
9326
+ const out = /* @__PURE__ */ new Map();
9327
+ for (const id of ids ?? this.selection) {
9328
+ const n2 = this.getNode(id);
9329
+ if (isVectorSymmetryGroup(n2)) out.set(n2.id, n2);
9330
+ else if (!ids) {
9331
+ const p = locateVectorNode(this.doc.nodes, id)?.parent;
9332
+ if (isVectorSymmetryGroup(p)) out.set(p.id, p);
9333
+ }
9334
+ }
9335
+ return [...out.values()];
9336
+ }
9337
+ /** 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). */
9338
+ setSymmetry(patch, options = {}) {
9339
+ if (options.ids) {
9340
+ 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).` };
9341
+ }
9342
+ const groups = this.symmetryTargets(options.ids);
9343
+ if (!groups.length) return { ok: false, message: "Select a symmetry group (or something in one)." };
9344
+ if (patch.mode !== void 0 && !VECTOR_SYMMETRY_MODES.includes(patch.mode)) return { ok: false, message: `Unknown symmetry "${patch.mode}": use ${VECTOR_SYMMETRY_MODES.join(", ")}.` };
9345
+ if (patch.copies !== void 0 && !(patch.copies >= 2 && patch.copies <= 64)) return { ok: false, message: "copies must be from 2 to 64." };
9346
+ const next = new Map(groups.map((g) => [g.id, sanitizeVectorSymmetry({ ...g.symmetry, ...patch })]));
9347
+ if ([...next.values()].some((v) => !v)) return { ok: false, message: "center must be { x, y } numbers." };
9348
+ this.change(
9349
+ "Symmetry",
9350
+ () => {
9351
+ this.setDoc(mapTree(this.doc.nodes, (n2) => next.has(n2.id) && n2.type === "group" ? { ...n2, symmetry: next.get(n2.id) } : n2));
9352
+ },
9353
+ options.merge
9354
+ );
9355
+ return { ok: true };
9356
+ }
9357
+ /**
9358
+ * Release Symmetry: the groups stop repeating their children. With `expand`, the copies become
9359
+ * real objects in the group (new ids); otherwise they go. One undo step. Returns the ids of the
9360
+ * objects added by expanding.
9361
+ */
9362
+ releaseSymmetry(options = {}) {
9363
+ const groups = this.symmetryTargets(options.ids);
9364
+ if (!groups.length) return { ok: false, message: "Select a symmetry group (or something in one)." };
9365
+ const added = [];
9366
+ const next = /* @__PURE__ */ new Map();
9367
+ for (const g of groups) {
9368
+ const { symmetry: _s, ...rest } = g;
9369
+ if (!options.expand) {
9370
+ next.set(g.id, rest);
9371
+ continue;
9372
+ }
9373
+ const drawn = vectorSymmetryDrawn(g);
9374
+ const fresh = (n2) => {
9375
+ const id = this.newId();
9376
+ added.push(id);
9377
+ const { locked: _l, ...plain2 } = n2;
9378
+ return plain2.type === "group" ? { ...plain2, id, children: plain2.children.map(fresh) } : { ...plain2, id };
9379
+ };
9380
+ next.set(g.id, { ...rest, children: [...g.children, ...drawn.children.slice(g.children.length).map(fresh)] });
9381
+ }
9382
+ this.change(options.expand ? "Expand Symmetry" : "Release Symmetry", () => {
9383
+ this.setDoc(mapTree(this.doc.nodes, (n2) => next.get(n2.id) ?? n2));
9384
+ if (this.symmetryTarget && next.has(this.symmetryTarget)) this.symmetryTarget = null;
9385
+ });
9386
+ return { ok: true, ids: added.filter((id) => !!this.getNode(id)) };
9387
+ }
9388
+ /** 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. */
9389
+ enterSymmetry(id) {
9390
+ const n2 = this.doc.nodes.find((x) => x.id === id);
9391
+ if (!isVectorSymmetryGroup(n2)) return { ok: false, message: `"${id}" is not a top-level symmetry group.` };
9392
+ if (n2.transform) return { ok: false, message: "Drawing into a turned symmetry group is not supported; draw beside it and move the art in." };
9393
+ this.symmetryTarget = id;
9394
+ this.selection = [id];
9395
+ this.notify();
9396
+ return { ok: true };
9397
+ }
9398
+ /** Stops drawing into the entered symmetry group. */
9399
+ exitSymmetry() {
9400
+ if (!this.symmetryTarget) return;
9401
+ this.symmetryTarget = null;
9402
+ this.notify();
9403
+ }
9404
+ // -----------------------------------------------------------------------------------------
9205
9405
  // PDF
9206
9406
  /**
9207
9407
  * The document as a vector PDF (see `vectorDocumentToPdf`): one page of the artboard, or a page
@@ -11673,7 +11873,7 @@ var VectorEditorController = class _VectorEditorController {
11673
11873
  }
11674
11874
  const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
11675
11875
  const node = { id: this.newId(), type: "path", name: "Path", ...this.styleFor(true), subpaths: [{ anchors: [{ x: pt2.x, y: pt2.y }], closed: false }] };
11676
- this.setDoc([...this.doc.nodes, node]);
11876
+ this.addDrawn(node);
11677
11877
  this.pen = { nodeId: node.id, subpath: 0 };
11678
11878
  this.selection = [node.id];
11679
11879
  this.editingId = node.id;
@@ -11694,7 +11894,7 @@ var VectorEditorController = class _VectorEditorController {
11694
11894
  strokeLinejoin: this.defaults.strokeLinejoin ?? "round",
11695
11895
  subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
11696
11896
  };
11697
- this.setDoc([...this.doc.nodes, node]);
11897
+ this.addDrawn(node);
11698
11898
  const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
11699
11899
  lazy.push([p.x, p.y]);
11700
11900
  const mode = this.editing.pencilPressure;
@@ -11712,7 +11912,7 @@ var VectorEditorController = class _VectorEditorController {
11712
11912
  const kind = this.tool === "frame" ? "rect" : this.tool;
11713
11913
  const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
11714
11914
  if (this.tool === "frame") Object.assign(node, { fill: "#ffffff", stroke: "#a3a3a3", strokeWidth: 1 / this.viewport.zoom });
11715
- this.setDoc([...this.doc.nodes, node]);
11915
+ this.addDrawn(node);
11716
11916
  this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before, targets };
11717
11917
  this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
11718
11918
  this.notify(true);
@@ -12855,7 +13055,7 @@ var VectorEditorController = class _VectorEditorController {
12855
13055
  this.endTextEdit();
12856
13056
  const before = this.snapshot();
12857
13057
  if (replace) this.setDoc(mapTree(this.doc.nodes, (n2) => n2.id === replace ? node : n2));
12858
- else this.setDoc([...this.doc.nodes, node]);
13058
+ else this.addDrawn(node);
12859
13059
  this.selection = [node.id];
12860
13060
  this.anchors = [];
12861
13061
  this.textEdit = { nodeId: node.id, anchor: 0, focus: 0, before, key: `text-edit:${node.id}:${++this.textEditCount}`, created: true };
@@ -13409,6 +13609,7 @@ var VectorEditorController = class _VectorEditorController {
13409
13609
  case "Escape":
13410
13610
  if (this.drag) this.cancelDrag();
13411
13611
  else if (this.trackingPoints.length) this.clearTracking();
13612
+ else if (this.symmetryTarget && !this.pen) this.exitSymmetry();
13412
13613
  else if (this.widthEdit) this.closeWidthPointEdit();
13413
13614
  else if (this.tool === "width" && this.widthPoints.length) {
13414
13615
  this.widthPoints = [];
@@ -13602,11 +13803,12 @@ function pencilPressureProfile(points, pressures) {
13602
13803
  }
13603
13804
 
13604
13805
  // src/vector/commands.ts
13605
- 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).";
13806
+ 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. blockShadow: angle (degrees, 0 right, 90 down), distance, color (null: the shape fill colour), opacity, endOpacity (percent; lower than opacity fades it into a long shadow). 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).";
13606
13807
  var fail = (message) => {
13607
13808
  throw new Error(message);
13608
13809
  };
13609
13810
  var IDS = { type: "array", items: { type: "string" }, description: "Node ids (default: the selection)." };
13811
+ 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)." };
13610
13812
  var ANCHORS = {
13611
13813
  type: "array",
13612
13814
  items: {
@@ -13746,6 +13948,11 @@ function createVectorCommands(controller, options = {}) {
13746
13948
  const prefix = options.prefix ?? "vector.";
13747
13949
  const group = options.group ?? "Vector";
13748
13950
  const base = { group, ...options.scope ? { scope: options.scope } : {} };
13951
+ const parentOf = (parentId) => {
13952
+ if (parentId === void 0) return {};
13953
+ if (controller.getNode(parentId)?.type !== "group") fail(`"${parentId}" is not a group to add into.`);
13954
+ return { parentId };
13955
+ };
13749
13956
  const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
13750
13957
  const ids = (params) => {
13751
13958
  const list2 = params?.ids ?? controller.getState().selection;
@@ -13792,7 +13999,8 @@ function createVectorCommands(controller, options = {}) {
13792
13999
  ...isVectorMaskGroup(n2) ? { mask: vectorMaskSummary(n2) } : {},
13793
14000
  ...isVectorFrame(n2) ? { frameRect: n2.frame } : {},
13794
14001
  ...n2.resizing ? { resizing: n2.resizing } : {},
13795
- ...n2.type === "path" && n2.connector ? { connector: n2.connector } : {}
14002
+ ...n2.type === "path" && n2.connector ? { connector: n2.connector } : {},
14003
+ ...n2.type === "group" && n2.symmetry ? { symmetry: n2.symmetry } : {}
13796
14004
  });
13797
14005
  if (n2.type === "group") visit(n2.children, depth + 1);
13798
14006
  }
@@ -14673,6 +14881,89 @@ function createVectorCommands(controller, options = {}) {
14673
14881
  issues: state.constraintIssues
14674
14882
  };
14675
14883
  };
14884
+ const SYMMETRY_PROPS = {
14885
+ 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)." },
14886
+ 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." },
14887
+ rotation: { type: "number", description: "Turn of the axes, degrees clockwise (default 0)." },
14888
+ copies: { type: "integer", minimum: 2, maximum: 64, description: "Radial: how many turns round the circle (default 6)." },
14889
+ mirror: { type: "boolean", description: "Radial: mirror each turn too, like a kaleidoscope (default false)." }
14890
+ };
14891
+ const symmetryResult = (id) => {
14892
+ const n2 = controller.getNode(id);
14893
+ return n2?.type === "group" && n2.symmetry ? { id, symmetry: n2.symmetry, children: n2.children.map((c) => c.id) } : fail(`"${id}" is not a symmetry group.`);
14894
+ };
14895
+ const symmetryCommands = () => [
14896
+ {
14897
+ ...base,
14898
+ id: `${prefix}makeSymmetry`,
14899
+ label: "Make Symmetry\u2026",
14900
+ keywords: ["symmetry", "mirror", "kaleidoscope", "radial", "mandala", "reflect", "live symmetry"],
14901
+ 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 }.",
14902
+ 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 },
14903
+ undoable: true,
14904
+ run: (_ctx, params) => {
14905
+ const { ids: list2, ...input } = params;
14906
+ const r = controller.makeSymmetry(input, list2 ? { ids: list2 } : {});
14907
+ if (!r.ok) fail(r.message);
14908
+ return symmetryResult(r.id);
14909
+ }
14910
+ },
14911
+ {
14912
+ ...base,
14913
+ id: `${prefix}setSymmetry`,
14914
+ label: "Symmetry Options\u2026",
14915
+ keywords: ["symmetry", "mirror", "radial", "axis", "copies"],
14916
+ 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 }.",
14917
+ params: { type: "object", properties: { ids: { ...IDS, description: "Symmetry group ids (default: the selection)." }, ...SYMMETRY_PROPS }, additionalProperties: false },
14918
+ undoable: true,
14919
+ run: (_ctx, params) => {
14920
+ const { ids: list2, ...patch } = params ?? {};
14921
+ if (!Object.keys(patch).length) fail("Pass at least one of mode, center, rotation, copies, mirror.");
14922
+ check(controller.setSymmetry(patch, list2 ? { ids: list2 } : {}));
14923
+ const ids2 = list2 ?? controller.getState().selection.filter((id) => controller.getNode(id)?.type === "group");
14924
+ return { groups: ids2.filter((id) => controller.getNode(id)?.symmetry).map(symmetryResult) };
14925
+ }
14926
+ },
14927
+ {
14928
+ ...base,
14929
+ id: `${prefix}releaseSymmetry`,
14930
+ label: "Release Symmetry",
14931
+ keywords: ["symmetry", "release", "expand", "bake"],
14932
+ 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.",
14933
+ 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 },
14934
+ undoable: true,
14935
+ enabled: enabledFor,
14936
+ run: (_ctx, params) => {
14937
+ const r = controller.releaseSymmetry({ ...params?.ids ? { ids: params.ids } : {}, ...params?.expand ? { expand: true } : {} });
14938
+ check(r);
14939
+ return { ids: r.ids ?? [] };
14940
+ }
14941
+ },
14942
+ {
14943
+ ...base,
14944
+ id: `${prefix}enterSymmetry`,
14945
+ label: "Draw Inside Symmetry",
14946
+ keywords: ["symmetry", "draw inside", "mirror drawing"],
14947
+ 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 }.",
14948
+ params: { type: "object", properties: { id: { type: "string", description: "The symmetry group." } }, required: ["id"], additionalProperties: false },
14949
+ run: (_ctx, params) => {
14950
+ check(controller.enterSymmetry(params.id));
14951
+ return { id: params.id };
14952
+ }
14953
+ },
14954
+ {
14955
+ ...base,
14956
+ id: `${prefix}exitSymmetry`,
14957
+ label: "Stop Drawing Inside Symmetry",
14958
+ keywords: ["symmetry", "exit"],
14959
+ description: "New art goes on top of the document again (no symmetry group is entered). Returns { ok: true }.",
14960
+ params: { type: "object", properties: {}, additionalProperties: false },
14961
+ run: () => {
14962
+ controller.exitSymmetry();
14963
+ return { ok: true };
14964
+ }
14965
+ }
14966
+ ];
14676
14967
  const pdfCommands = () => [
14677
14968
  {
14678
14969
  ...base,
@@ -15431,12 +15722,16 @@ function createVectorCommands(controller, options = {}) {
15431
15722
  points: { type: "integer", minimum: 3 },
15432
15723
  innerRatio: { type: "number", minimum: 0, maximum: 1 },
15433
15724
  name: { type: "string" },
15725
+ parentId: PARENT_ID,
15434
15726
  ...STYLE_PROPS
15435
15727
  },
15436
15728
  required: ["kind", "x", "y", "width", "height"]
15437
15729
  },
15438
15730
  undoable: true,
15439
- run: (_ctx, params) => ({ id: controller.addShape(params) })
15731
+ run: (_ctx, params) => {
15732
+ const { parentId, ...input } = params;
15733
+ return { id: controller.addShape(input, parentOf(parentId)) };
15734
+ }
15440
15735
  },
15441
15736
  {
15442
15737
  ...base,
@@ -15444,10 +15739,10 @@ function createVectorCommands(controller, options = {}) {
15444
15739
  label: "Add Path\u2026",
15445
15740
  keywords: ["svg", "path data", "bezier", "curve"],
15446
15741
  description: "Adds a path from SVG path data (`d`), in document units.",
15447
- params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, ...STYLE_PROPS }, required: ["d"] },
15742
+ params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, parentId: PARENT_ID, ...STYLE_PROPS }, required: ["d"] },
15448
15743
  undoable: true,
15449
15744
  run: (_ctx, params) => {
15450
- const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} });
15745
+ const id = controller.addPath(params.d, { ...pickStyle(params), ...params.name ? { name: params.name } : {} }, parentOf(params.parentId));
15451
15746
  const node = controller.getNode(id);
15452
15747
  if (node?.type === "path" && !node.subpaths.length) {
15453
15748
  controller.undo();
@@ -15504,6 +15799,7 @@ function createVectorCommands(controller, options = {}) {
15504
15799
  ...traceCommands(),
15505
15800
  ...lottieCommands(),
15506
15801
  ...pdfCommands(),
15802
+ ...symmetryCommands(),
15507
15803
  {
15508
15804
  ...base,
15509
15805
  id: `${prefix}setCorners`,
@@ -16495,6 +16791,8 @@ export {
16495
16791
  VECTOR_STROKE_SETTING_KEYS,
16496
16792
  VECTOR_STYLE_KEYS,
16497
16793
  VECTOR_SYMBOL_MAX_DEPTH,
16794
+ VECTOR_SYMMETRY_MODES,
16795
+ VECTOR_SYMMETRY_MODE_LABELS,
16498
16796
  VECTOR_TEXT_ALIGNS,
16499
16797
  VECTOR_TEXT_ALIGN_LABELS,
16500
16798
  VECTOR_TEXT_ALIGN_OPTIONS,
@@ -16680,6 +16978,7 @@ export {
16680
16978
  isVectorMaskType,
16681
16979
  isVectorMeshGradient,
16682
16980
  isVectorShadedPaint,
16981
+ isVectorSymmetryGroup,
16683
16982
  isVectorTextTarget,
16684
16983
  joinSubpaths,
16685
16984
  knifePath,
@@ -16788,6 +17087,7 @@ export {
16788
17087
  sanitizeVectorShadedPaint,
16789
17088
  sanitizeVectorStrokeSettings,
16790
17089
  sanitizeVectorStrokes,
17090
+ sanitizeVectorSymmetry,
16791
17091
  sanitizeVectorTextFields,
16792
17092
  sanitizeVectorTextFrame,
16793
17093
  sanitizeWidthProfile,
@@ -16820,6 +17120,7 @@ export {
16820
17120
  subpathSegmentCount,
16821
17121
  subpathToMorphContour,
16822
17122
  subpathsToBezier,
17123
+ sweepPath,
16823
17124
  taperWaveFactor,
16824
17125
  tintVectorNode,
16825
17126
  toVectorKeyInput,
@@ -16831,6 +17132,7 @@ export {
16831
17132
  transformVectorNode,
16832
17133
  transformVectorPaint,
16833
17134
  transformVectorShadedPaint,
17135
+ transformVectorSymmetry,
16834
17136
  translateAnchor,
16835
17137
  translateMatrix,
16836
17138
  trimIntervals,
@@ -16995,6 +17297,9 @@ export {
16995
17297
  vectorStrokes,
16996
17298
  vectorSymbolUses,
16997
17299
  vectorSymbolsUsedIn,
17300
+ vectorSymmetryAxes,
17301
+ vectorSymmetryDrawn,
17302
+ vectorSymmetryMatrices,
16998
17303
  vectorTargetPaint,
16999
17304
  vectorTextBoxOutline,
17000
17305
  vectorTextCaret,