@pixerate/editor 0.118.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 +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-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 +1312 -1009
  55. package/dist/vector/index.cjs.map +1 -1
  56. package/dist/vector/index.d.cts +103 -5
  57. package/dist/vector/index.d.ts +103 -5
  58. package/dist/vector/index.js +317 -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 +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,
@@ -2371,6 +2379,46 @@ function shadingOverlay(state) {
2371
2379
  }
2372
2380
  return out;
2373
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
+ }
2374
2422
  function connectorSiteOverlay(state) {
2375
2423
  if (state.tool !== "connector" || !state.pointer) return [];
2376
2424
  const drawing = state.interaction?.type === "draw" && state.interaction.tool === "connector" ? state.interaction.nodeId : void 0;
@@ -2643,6 +2691,7 @@ function getVectorOverlayRenderData(state) {
2643
2691
  frames: frameLabels(state),
2644
2692
  connectorSites: connectorSiteOverlay(state),
2645
2693
  shading: shadingOverlay(state),
2694
+ symmetry: symmetryOverlay(state),
2646
2695
  trackingPoints: (state.trackingPoints ?? []).map((p) => toScreen(state, p)),
2647
2696
  typedEntry: state.typedEntry ? { text: state.typedEntry, ...state.pointer ? toScreen(state, state.pointer) : { x: 0, y: 0 } } : null
2648
2697
  };
@@ -3663,9 +3712,9 @@ function nodeGroup(node, notes) {
3663
3712
  if (node.filters?.length) notes.push(`${label(node)}: filters (shadows, glows, blurs) are not part of Lottie (left out).`);
3664
3713
  const m = nodeMatrix(node);
3665
3714
  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).`);
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).`);
3669
3718
  if (isVectorMaskGroup(node) && node.mask?.type !== "clip") notes.push(`${label(node)}: opacity masks are not exported (the contents are drawn whole).`);
3670
3719
  const drawn = isVectorFrame(node) ? vectorFrameDrawn(node) : node;
3671
3720
  const shapes = isVectorMaskGroup(drawn) && drawn.mask?.type === "clip" ? vectorAppearanceShapes({ ...drawn, transform: void 0 }) : null;
@@ -6899,6 +6948,7 @@ var VectorEditorController = class _VectorEditorController {
6899
6948
  trackingPoints: this.trackingPoints,
6900
6949
  typedEntry: this.typed,
6901
6950
  shadingPoint: this.getShadingSelection(),
6951
+ symmetryTarget: this.symmetryTarget && this.doc.nodes.some((n2) => n2.id === this.symmetryTarget) ? this.symmetryTarget : null,
6902
6952
  canUndo: h.canUndo,
6903
6953
  canRedo: h.canRedo,
6904
6954
  undoLabel: h.undoName,
@@ -9202,6 +9252,155 @@ var VectorEditorController = class _VectorEditorController {
9202
9252
  return { ok: true, id };
9203
9253
  }
9204
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
+ // -----------------------------------------------------------------------------------------
9205
9404
  // PDF
9206
9405
  /**
9207
9406
  * The document as a vector PDF (see `vectorDocumentToPdf`): one page of the artboard, or a page
@@ -11673,7 +11872,7 @@ var VectorEditorController = class _VectorEditorController {
11673
11872
  }
11674
11873
  const pt2 = this.snapDrawn(p, null, input, () => this.drawingTargets(null));
11675
11874
  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]);
11875
+ this.addDrawn(node);
11677
11876
  this.pen = { nodeId: node.id, subpath: 0 };
11678
11877
  this.selection = [node.id];
11679
11878
  this.editingId = node.id;
@@ -11694,7 +11893,7 @@ var VectorEditorController = class _VectorEditorController {
11694
11893
  strokeLinejoin: this.defaults.strokeLinejoin ?? "round",
11695
11894
  subpaths: [{ anchors: [{ x: p.x, y: p.y }], closed: false }]
11696
11895
  };
11697
- this.setDoc([...this.doc.nodes, node]);
11896
+ this.addDrawn(node);
11698
11897
  const lazy = new LazyMouse(this.editing.pencilSmoothing * VECTOR_PENCIL_STRING_PX / this.viewport.zoom);
11699
11898
  lazy.push([p.x, p.y]);
11700
11899
  const mode = this.editing.pencilPressure;
@@ -11712,7 +11911,7 @@ var VectorEditorController = class _VectorEditorController {
11712
11911
  const kind = this.tool === "frame" ? "rect" : this.tool;
11713
11912
  const node = this.createShapeNode({ kind, x: start.x, y: start.y, width: 0, height: 0 });
11714
11913
  if (this.tool === "frame") Object.assign(node, { fill: "#ffffff", stroke: "#a3a3a3", strokeWidth: 1 / this.viewport.zoom });
11715
- this.setDoc([...this.doc.nodes, node]);
11914
+ this.addDrawn(node);
11716
11915
  this.drag = { kind: "shape", tool: this.tool, start, nodeId: node.id, before, targets };
11717
11916
  this.interaction = { type: "draw", tool: this.tool, nodeId: node.id, bounds: { x: start.x, y: start.y, width: 0, height: 0 } };
11718
11917
  this.notify(true);
@@ -12855,7 +13054,7 @@ var VectorEditorController = class _VectorEditorController {
12855
13054
  this.endTextEdit();
12856
13055
  const before = this.snapshot();
12857
13056
  if (replace) this.setDoc(mapTree(this.doc.nodes, (n2) => n2.id === replace ? node : n2));
12858
- else this.setDoc([...this.doc.nodes, node]);
13057
+ else this.addDrawn(node);
12859
13058
  this.selection = [node.id];
12860
13059
  this.anchors = [];
12861
13060
  this.textEdit = { nodeId: node.id, anchor: 0, focus: 0, before, key: `text-edit:${node.id}:${++this.textEditCount}`, created: true };
@@ -13409,6 +13608,7 @@ var VectorEditorController = class _VectorEditorController {
13409
13608
  case "Escape":
13410
13609
  if (this.drag) this.cancelDrag();
13411
13610
  else if (this.trackingPoints.length) this.clearTracking();
13611
+ else if (this.symmetryTarget && !this.pen) this.exitSymmetry();
13412
13612
  else if (this.widthEdit) this.closeWidthPointEdit();
13413
13613
  else if (this.tool === "width" && this.widthPoints.length) {
13414
13614
  this.widthPoints = [];
@@ -13602,11 +13802,12 @@ function pencilPressureProfile(points, pressures) {
13602
13802
  }
13603
13803
 
13604
13804
  // 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).";
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).";
13606
13806
  var fail = (message) => {
13607
13807
  throw new Error(message);
13608
13808
  };
13609
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)." };
13610
13811
  var ANCHORS = {
13611
13812
  type: "array",
13612
13813
  items: {
@@ -13746,6 +13947,11 @@ function createVectorCommands(controller, options = {}) {
13746
13947
  const prefix = options.prefix ?? "vector.";
13747
13948
  const group = options.group ?? "Vector";
13748
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
+ };
13749
13955
  const hasSelection = () => controller.getState().selection.length ? true : "Nothing is selected.";
13750
13956
  const ids = (params) => {
13751
13957
  const list2 = params?.ids ?? controller.getState().selection;
@@ -13792,7 +13998,8 @@ function createVectorCommands(controller, options = {}) {
13792
13998
  ...isVectorMaskGroup(n2) ? { mask: vectorMaskSummary(n2) } : {},
13793
13999
  ...isVectorFrame(n2) ? { frameRect: n2.frame } : {},
13794
14000
  ...n2.resizing ? { resizing: n2.resizing } : {},
13795
- ...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 } : {}
13796
14003
  });
13797
14004
  if (n2.type === "group") visit(n2.children, depth + 1);
13798
14005
  }
@@ -14673,6 +14880,89 @@ function createVectorCommands(controller, options = {}) {
14673
14880
  issues: state.constraintIssues
14674
14881
  };
14675
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
+ ];
14676
14966
  const pdfCommands = () => [
14677
14967
  {
14678
14968
  ...base,
@@ -15431,12 +15721,16 @@ function createVectorCommands(controller, options = {}) {
15431
15721
  points: { type: "integer", minimum: 3 },
15432
15722
  innerRatio: { type: "number", minimum: 0, maximum: 1 },
15433
15723
  name: { type: "string" },
15724
+ parentId: PARENT_ID,
15434
15725
  ...STYLE_PROPS
15435
15726
  },
15436
15727
  required: ["kind", "x", "y", "width", "height"]
15437
15728
  },
15438
15729
  undoable: true,
15439
- 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
+ }
15440
15734
  },
15441
15735
  {
15442
15736
  ...base,
@@ -15444,10 +15738,10 @@ function createVectorCommands(controller, options = {}) {
15444
15738
  label: "Add Path\u2026",
15445
15739
  keywords: ["svg", "path data", "bezier", "curve"],
15446
15740
  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"] },
15741
+ params: { type: "object", properties: { d: { type: "string" }, name: { type: "string" }, parentId: PARENT_ID, ...STYLE_PROPS }, required: ["d"] },
15448
15742
  undoable: true,
15449
15743
  run: (_ctx, params) => {
15450
- 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));
15451
15745
  const node = controller.getNode(id);
15452
15746
  if (node?.type === "path" && !node.subpaths.length) {
15453
15747
  controller.undo();
@@ -15504,6 +15798,7 @@ function createVectorCommands(controller, options = {}) {
15504
15798
  ...traceCommands(),
15505
15799
  ...lottieCommands(),
15506
15800
  ...pdfCommands(),
15801
+ ...symmetryCommands(),
15507
15802
  {
15508
15803
  ...base,
15509
15804
  id: `${prefix}setCorners`,
@@ -16495,6 +16790,8 @@ export {
16495
16790
  VECTOR_STROKE_SETTING_KEYS,
16496
16791
  VECTOR_STYLE_KEYS,
16497
16792
  VECTOR_SYMBOL_MAX_DEPTH,
16793
+ VECTOR_SYMMETRY_MODES,
16794
+ VECTOR_SYMMETRY_MODE_LABELS,
16498
16795
  VECTOR_TEXT_ALIGNS,
16499
16796
  VECTOR_TEXT_ALIGN_LABELS,
16500
16797
  VECTOR_TEXT_ALIGN_OPTIONS,
@@ -16680,6 +16977,7 @@ export {
16680
16977
  isVectorMaskType,
16681
16978
  isVectorMeshGradient,
16682
16979
  isVectorShadedPaint,
16980
+ isVectorSymmetryGroup,
16683
16981
  isVectorTextTarget,
16684
16982
  joinSubpaths,
16685
16983
  knifePath,
@@ -16788,6 +17086,7 @@ export {
16788
17086
  sanitizeVectorShadedPaint,
16789
17087
  sanitizeVectorStrokeSettings,
16790
17088
  sanitizeVectorStrokes,
17089
+ sanitizeVectorSymmetry,
16791
17090
  sanitizeVectorTextFields,
16792
17091
  sanitizeVectorTextFrame,
16793
17092
  sanitizeWidthProfile,
@@ -16831,6 +17130,7 @@ export {
16831
17130
  transformVectorNode,
16832
17131
  transformVectorPaint,
16833
17132
  transformVectorShadedPaint,
17133
+ transformVectorSymmetry,
16834
17134
  translateAnchor,
16835
17135
  translateMatrix,
16836
17136
  trimIntervals,
@@ -16995,6 +17295,9 @@ export {
16995
17295
  vectorStrokes,
16996
17296
  vectorSymbolUses,
16997
17297
  vectorSymbolsUsedIn,
17298
+ vectorSymmetryAxes,
17299
+ vectorSymmetryDrawn,
17300
+ vectorSymmetryMatrices,
16998
17301
  vectorTargetPaint,
16999
17302
  vectorTextBoxOutline,
17000
17303
  vectorTextCaret,