@pixerate/editor 0.140.3 → 0.141.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 (85) hide show
  1. package/dist/agent/canvasPlayback.d.cts +15 -0
  2. package/dist/agent/canvasPlayback.d.ts +15 -0
  3. package/dist/agent/index.cjs +144 -23
  4. package/dist/agent/index.d.cts +1 -0
  5. package/dist/agent/index.d.ts +1 -0
  6. package/dist/agent/index.js +125 -4
  7. package/dist/agent/providers.d.cts +8 -0
  8. package/dist/agent/providers.d.ts +8 -0
  9. package/dist/agent/types.d.cts +3 -0
  10. package/dist/agent/types.d.ts +3 -0
  11. package/dist/animation/index.cjs +2 -2
  12. package/dist/animation/index.js +1 -1
  13. package/dist/animation/presence.d.cts +1 -75
  14. package/dist/animation/presence.d.ts +1 -75
  15. package/dist/animation-editor/index.cjs +55 -55
  16. package/dist/animation-editor/index.js +2 -2
  17. package/dist/bridge/cli.cjs +3 -3
  18. package/dist/bridge/cli.js +2 -2
  19. package/dist/bridge/index.cjs +3 -3
  20. package/dist/bridge/index.js +2 -2
  21. package/dist/canvas/index.cjs +336 -646
  22. package/dist/canvas/index.d.cts +1 -0
  23. package/dist/canvas/index.d.ts +1 -0
  24. package/dist/canvas/index.js +44 -354
  25. package/dist/canvas/playback.d.cts +14 -0
  26. package/dist/canvas/playback.d.ts +14 -0
  27. package/dist/chart/index.cjs +7 -7
  28. package/dist/chart/index.js +1 -1
  29. package/dist/chunk-6SFREX6H.cjs +406 -0
  30. package/dist/{chunk-XGOU44RJ.js → chunk-77SG47WT.js} +80 -9
  31. package/dist/{chunk-2SHFIVC3.js → chunk-DAS7M4RY.js} +1 -1
  32. package/dist/chunk-DW4CV5H4.js +591 -0
  33. package/dist/{chunk-UMS6YFA6.js → chunk-EAQ4BGNS.js} +1 -1
  34. package/dist/{chunk-AGCD3EHB.cjs → chunk-ENIZYNEF.cjs} +7 -7
  35. package/dist/{chunk-VSR5A22L.cjs → chunk-H6FX45CP.cjs} +35 -35
  36. package/dist/{chunk-RCUX6CT2.cjs → chunk-HRI3ZMDM.cjs} +110 -39
  37. package/dist/chunk-MFFE55CS.js +406 -0
  38. package/dist/{chunk-BSGPXPTW.js → chunk-PEEP2YR7.js} +1 -1
  39. package/dist/{chunk-RB62U6MS.js → chunk-RFBFTLVN.js} +27 -5
  40. package/dist/chunk-UBRYHNRU.cjs +591 -0
  41. package/dist/{chunk-GEY73RUW.cjs → chunk-UJGAA6AK.cjs} +1 -1
  42. package/dist/{chunk-6UZCFD7Y.js → chunk-VM6JYWXQ.js} +1 -1
  43. package/dist/{chunk-O3ZU6QIZ.cjs → chunk-XNZWATSB.cjs} +81 -81
  44. package/dist/{chunk-HDK3T2QS.cjs → chunk-Y4BWZB6J.cjs} +29 -7
  45. package/dist/commands/index.cjs +37 -1
  46. package/dist/commands/index.js +36 -0
  47. package/dist/commands/registry.d.cts +11 -0
  48. package/dist/commands/registry.d.ts +11 -0
  49. package/dist/diagram/index.cjs +7 -7
  50. package/dist/diagram/index.js +1 -1
  51. package/dist/image-editor/index.cjs +720 -628
  52. package/dist/image-editor/index.d.cts +1 -0
  53. package/dist/image-editor/index.d.ts +1 -0
  54. package/dist/image-editor/index.js +109 -17
  55. package/dist/image-editor/playback.d.cts +22 -0
  56. package/dist/image-editor/playback.d.ts +22 -0
  57. package/dist/paint-editor/index.cjs +13 -13
  58. package/dist/paint-editor/index.js +3 -3
  59. package/dist/tools/index.cjs +33 -3
  60. package/dist/tools/index.d.cts +2 -0
  61. package/dist/tools/index.d.ts +2 -0
  62. package/dist/tools/index.js +32 -2
  63. package/dist/tools/playback.d.cts +205 -0
  64. package/dist/tools/playback.d.ts +205 -0
  65. package/dist/tools/presence.d.cts +82 -0
  66. package/dist/tools/presence.d.ts +82 -0
  67. package/dist/tools/registry.d.cts +45 -6
  68. package/dist/tools/registry.d.ts +45 -6
  69. package/dist/tools/view.d.cts +2 -4
  70. package/dist/tools/view.d.ts +2 -4
  71. package/dist/vector/index.cjs +1159 -1042
  72. package/dist/vector/index.d.cts +1 -0
  73. package/dist/vector/index.d.ts +1 -0
  74. package/dist/vector/index.js +142 -25
  75. package/dist/vector/playback.d.cts +13 -0
  76. package/dist/vector/playback.d.ts +13 -0
  77. package/package.json +1 -1
  78. package/skills/pixerate-agent-tools/SKILL.md +13 -0
  79. package/skills/pixerate-canvas/SKILL.md +3 -0
  80. package/skills/pixerate-image-editor/SKILL.md +2 -2
  81. package/skills/pixerate-vector/SKILL.md +5 -0
  82. package/dist/chunk-BTZS3KZY.js +0 -221
  83. package/dist/chunk-MRD3NXKR.cjs +0 -221
  84. /package/dist/{chunk-K73HMKQP.js → chunk-PLKIYNJZ.js} +0 -0
  85. /package/dist/{chunk-LACYURUW.cjs → chunk-TNCWKC5M.cjs} +0 -0
@@ -52,3 +52,4 @@ export { createCanvasTextShaper, createMetricTextShaper, createOutlineTextShaper
52
52
  export { loadOpenTypeFont, parseOpenTypeFont, type OpenTypeFont } from '../text/opentype.cjs';
53
53
  export type { TextFont, TextShaper } from '../text/types.cjs';
54
54
  export * from './effect-tools.cjs';
55
+ export * from './playback.cjs';
@@ -52,3 +52,4 @@ export { createCanvasTextShaper, createMetricTextShaper, createOutlineTextShaper
52
52
  export { loadOpenTypeFont, parseOpenTypeFont, type OpenTypeFont } from '../text/opentype.js';
53
53
  export type { TextFont, TextShaper } from '../text/types.js';
54
54
  export * from './effect-tools.js';
55
+ export * from './playback.js';
@@ -38,7 +38,7 @@ import {
38
38
  reverseMorphContour,
39
39
  subpathToMorphContour,
40
40
  turnMorphContour
41
- } from "../chunk-2SHFIVC3.js";
41
+ } from "../chunk-DAS7M4RY.js";
42
42
  import {
43
43
  DEFAULT_VECTOR_FONT,
44
44
  IDENTITY_MATRIX,
@@ -507,7 +507,7 @@ import {
507
507
  withVectorStrokes,
508
508
  withVectorTargetPaint,
509
509
  zigzagPaths
510
- } from "../chunk-XGOU44RJ.js";
510
+ } from "../chunk-77SG47WT.js";
511
511
  import "../chunk-ORDXMBD4.js";
512
512
  import {
513
513
  createProvenance
@@ -4169,8 +4169,8 @@ var Page = class {
4169
4169
  const fns = list.slice(1).map((s, k) => `<< /FunctionType 2 /Domain [0 1] /C0 ${rgb(list[k].c)} /C1 ${rgb(s.c)} /N 1 >>`);
4170
4170
  const fn = fns.length === 1 ? fns[0] : `<< /FunctionType 3 /Domain [${n(list[0].t)} ${n(list[list.length - 1].t)}] /Functions [${fns.join(" ")}] /Bounds [${list.slice(1, -1).map((s) => n(s.t)).join(" ")}] /Encode [${fns.map(() => "0 1").join(" ")}] >>`;
4171
4171
  const a = def.attrs;
4172
- const num = (k, d = 0) => a[k] === void 0 ? d : parseFloat(String(a[k]));
4173
- const coords = def.type === "linear" ? `/ShadingType 2 /Coords [${n(num("x1"))} ${n(num("y1"))} ${n(num("x2", 1))} ${n(num("y2"))}]` : `/ShadingType 3 /Coords [${n(num("fx", num("cx", 0.5)))} ${n(num("fy", num("cy", 0.5)))} ${n(num("fr"))} ${n(num("cx", 0.5))} ${n(num("cy", 0.5))} ${n(num("r", 0.5))}]`;
4172
+ const num2 = (k, d = 0) => a[k] === void 0 ? d : parseFloat(String(a[k]));
4173
+ const coords = def.type === "linear" ? `/ShadingType 2 /Coords [${n(num2("x1"))} ${n(num2("y1"))} ${n(num2("x2", 1))} ${n(num2("y2"))}]` : `/ShadingType 3 /Coords [${n(num2("fx", num2("cx", 0.5)))} ${n(num2("fy", num2("cy", 0.5)))} ${n(num2("fr"))} ${n(num2("cx", 0.5))} ${n(num2("cy", 0.5))} ${n(num2("r", 0.5))}]`;
4174
4174
  this.writer.shadingDicts.set(`${this.writer.pageIndex}:${name}`, `<< ${coords} /ColorSpace /DeviceRGB /Function ${fn} /Extend [true true] >>`);
4175
4175
  return name;
4176
4176
  }
@@ -4807,28 +4807,28 @@ function gradientOf(it, frame) {
4807
4807
  }
4808
4808
  function effectOf(it, frame, c, id) {
4809
4809
  const v = (k) => lottieValueAt(it[k], frame);
4810
- const num = (k, d = 0) => v(k)[0] ?? d;
4810
+ const num2 = (k, d = 0) => v(k)[0] ?? d;
4811
4811
  let raw = null;
4812
4812
  switch (it.ty) {
4813
4813
  case "tm":
4814
- raw = { type: "trim", params: { start: num("s"), end: num("e", 100), offset: num("o"), mode: Number(it.m) === 2 ? "individual" : "simultaneous" } };
4814
+ raw = { type: "trim", params: { start: num2("s"), end: num2("e", 100), offset: num2("o"), mode: Number(it.m) === 2 ? "individual" : "simultaneous" } };
4815
4815
  break;
4816
4816
  case "rd":
4817
- raw = { type: "roundCorners", params: { radius: num("r") } };
4817
+ raw = { type: "roundCorners", params: { radius: num2("r") } };
4818
4818
  break;
4819
4819
  case "pb":
4820
- raw = { type: "puckerBloat", params: { amount: num("a") } };
4820
+ raw = { type: "puckerBloat", params: { amount: num2("a") } };
4821
4821
  break;
4822
4822
  case "tw": {
4823
4823
  const [x = c.x, y = c.y] = v("c");
4824
- raw = { type: "twist", params: { angle: num("a"), center: { x: x - c.x, y: y - c.y } } };
4824
+ raw = { type: "twist", params: { angle: num2("a"), center: { x: x - c.x, y: y - c.y } } };
4825
4825
  break;
4826
4826
  }
4827
4827
  case "zz":
4828
- raw = { type: "zigzag", params: { size: num("s"), ridges: num("r"), smooth: num("pt", 1) === 2 } };
4828
+ raw = { type: "zigzag", params: { size: num2("s"), ridges: num2("r"), smooth: num2("pt", 1) === 2 } };
4829
4829
  break;
4830
4830
  case "op":
4831
- raw = { type: "offset", params: { amount: num("a"), join: ["miter", "round", "bevel"][Number(it.lj ?? 1) - 1] ?? "miter", miterLimit: num("ml", 4) || 4 } };
4831
+ raw = { type: "offset", params: { amount: num2("a"), join: ["miter", "round", "bevel"][Number(it.lj ?? 1) - 1] ?? "miter", miterLimit: num2("ml", 4) || 4 } };
4832
4832
  break;
4833
4833
  case "mm":
4834
4834
  raw = { type: "merge", params: { mode: MERGE[Number(it.mm ?? 1) - 1] ?? "merge" } };
@@ -4844,7 +4844,7 @@ function effectOf(it, frame, c, id) {
4844
4844
  const [sx, sy] = at("s", [100, 100]);
4845
4845
  raw = {
4846
4846
  type: "repeater",
4847
- params: { copies: num("c", 1), offset: num("o"), position: { x: px, y: py }, anchor: { x: ax - c.x, y: ay - c.y }, rotation: at("r", [0])[0], scale: { x: sx, y: sy }, startOpacity: at("so", [100])[0], endOpacity: at("eo", [100])[0], composite: Number(it.m) === 1 ? "above" : "below" }
4847
+ params: { copies: num2("c", 1), offset: num2("o"), position: { x: px, y: py }, anchor: { x: ax - c.x, y: ay - c.y }, rotation: at("r", [0])[0], scale: { x: sx, y: sy }, startOpacity: at("so", [100])[0], endOpacity: at("eo", [100])[0], composite: Number(it.m) === 1 ? "above" : "below" }
4848
4848
  };
4849
4849
  break;
4850
4850
  }
@@ -6456,11 +6456,11 @@ function segsToSubpath(segs, closed, types) {
6456
6456
  const last = anchors.pop();
6457
6457
  if (last.in) anchors[0].in = last.in;
6458
6458
  }
6459
- const typed = anchors.map((a) => {
6459
+ const typed2 = anchors.map((a) => {
6460
6460
  const type = types.get(`${a.x},${a.y}`);
6461
6461
  return tidy(type ? { ...a, type } : a, !!type);
6462
6462
  });
6463
- return { anchors: typed, closed };
6463
+ return { anchors: typed2, closed };
6464
6464
  }
6465
6465
  var isZeroSeg = (s) => s.c.every((p) => Math.abs(p[0] - s.c[0][0]) < 1e-9 && Math.abs(p[1] - s.c[0][1]) < 1e-9);
6466
6466
  function mergePair(a, b, tol) {
@@ -8497,10 +8497,10 @@ var VectorEditorController = class _VectorEditorController {
8497
8497
  if (node?.type !== "path") return { ok: false, message: "There is no such path." };
8498
8498
  const g = vectorStrokeWidthGeometry(node);
8499
8499
  if (!g || !(g.unit > 0)) return { ok: false, message: "The path has no visible stroke." };
8500
- const num = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
8501
- const t = num(values.t);
8502
- const left = num(values.left);
8503
- const right = num(values.right);
8500
+ const num2 = (v) => typeof v === "number" && Number.isFinite(v) ? v : void 0;
8501
+ const t = num2(values.t);
8502
+ const left = num2(values.left);
8503
+ const right = num2(values.right);
8504
8504
  const scale = (w) => w === void 0 ? void 0 : Math.max(0, w) / g.unit;
8505
8505
  const profile = sanitizeWidthProfile(node.strokeWidthProfile);
8506
8506
  let points;
@@ -10768,18 +10768,18 @@ var VectorEditorController = class _VectorEditorController {
10768
10768
  const sp = pen && node ? node.subpaths[pen.subpath] : void 0;
10769
10769
  if (!pen || !node || !sp?.anchors.length) return { ok: false, message: "Start a path with the pen first: typed entry places its next point." };
10770
10770
  const last = getAnchorWorld(this.doc, { nodeId: pen.nodeId, subpath: pen.subpath, index: sp.anchors.length - 1 });
10771
- const num = "(-?(?:\\d+\\.?\\d*|\\.\\d+))";
10771
+ const num2 = "(-?(?:\\d+\\.?\\d*|\\.\\d+))";
10772
10772
  const t = text.replace(/\s+/g, "");
10773
10773
  let target = null;
10774
10774
  let m;
10775
- if (m = t.match(new RegExp(`^@${num},${num}$`))) target = { x: last.x + parseFloat(m[1]), y: last.y + parseFloat(m[2]) };
10776
- else if (m = t.match(new RegExp(`^${num},${num}$`))) target = { x: parseFloat(m[1]), y: parseFloat(m[2]) };
10777
- else if (m = t.match(new RegExp(`^${num}<${num}$`))) {
10775
+ if (m = t.match(new RegExp(`^@${num2},${num2}$`))) target = { x: last.x + parseFloat(m[1]), y: last.y + parseFloat(m[2]) };
10776
+ else if (m = t.match(new RegExp(`^${num2},${num2}$`))) target = { x: parseFloat(m[1]), y: parseFloat(m[2]) };
10777
+ else if (m = t.match(new RegExp(`^${num2}<${num2}$`))) {
10778
10778
  const base = this.snap.polarRelative ? (this.penAngles().angles?.[0] ?? this.snap.angles[0] ?? 0) - (this.snap.angles[0] ?? 0) : 0;
10779
10779
  const dir = vectorAngleDirection(parseFloat(m[2]) + base);
10780
10780
  const len2 = parseFloat(m[1]);
10781
10781
  target = { x: last.x + dir.x * len2, y: last.y + dir.y * len2 };
10782
- } else if (m = t.match(new RegExp(`^${num}$`))) {
10782
+ } else if (m = t.match(new RegExp(`^${num2}$`))) {
10783
10783
  const toward = this.pointer;
10784
10784
  const d = toward ? Math.hypot(toward.x - last.x, toward.y - last.y) : 0;
10785
10785
  if (!toward || d < 1e-9) return { ok: false, message: "Point the pointer the way the distance should go (or type length<angle)." };
@@ -14974,6 +14974,120 @@ function pencilPressureProfile(points, pressures) {
14974
14974
  return pressureWidthProfile(s.map((d, i) => ({ t: d / total, pressure: pressures[i] })));
14975
14975
  }
14976
14976
 
14977
+ // src/vector/playback.ts
14978
+ var VECTOR_PLAYBACK_KIND = "vector";
14979
+ function createVectorPlaybackLocator(controller) {
14980
+ return (ref) => {
14981
+ if (ref.kind !== VECTOR_PLAYBACK_KIND && ref.kind !== "item") return null;
14982
+ const node = controller.getNode(ref.id);
14983
+ if (!node) return null;
14984
+ const b = vectorNodeBounds(node, vectorParentMatrix(controller.getState().document.nodes, ref.id) ?? IDENTITY_MATRIX);
14985
+ if (!b) return null;
14986
+ const w = Math.max(b.width, 4);
14987
+ const h = Math.max(b.height, 4);
14988
+ return { x: b.x - (w - b.width) / 2, y: b.y - (h - b.height) / 2, width: w, height: h };
14989
+ };
14990
+ }
14991
+ var num = (v) => typeof v === "number" && Number.isFinite(v);
14992
+ var dragOut = (from, to) => [
14993
+ { type: "move", to: from },
14994
+ { type: "press" },
14995
+ { type: "drag", to },
14996
+ { type: "release" }
14997
+ ];
14998
+ var click = (at) => [{ type: "move", to: at }, { type: "press" }, { type: "release" }];
14999
+ var typed = (text) => {
15000
+ if (typeof text !== "string" || !text.trim()) return [];
15001
+ const line2 = text.replace(/\s+/g, " ").trim();
15002
+ return [{ type: "type", text: line2.length > 60 ? `${line2.slice(0, 59)}\u2026` : line2 }];
15003
+ };
15004
+ function pathPoints(d, max = 24) {
15005
+ let anchors;
15006
+ try {
15007
+ anchors = bezierToSubpaths(d).flatMap((s) => s.anchors.map((a) => ({ x: a.x, y: a.y })));
15008
+ } catch {
15009
+ return [];
15010
+ }
15011
+ if (anchors.length <= max) return anchors;
15012
+ const step = (anchors.length - 1) / (max - 1);
15013
+ return Array.from({ length: max }, (_, i) => anchors[Math.round(i * step)]);
15014
+ }
15015
+ function trace(points) {
15016
+ if (!points.length) return [];
15017
+ const [first, ...rest] = points;
15018
+ const total = rest.reduce((sum, p, i) => sum + Math.hypot(p.x - (i ? rest[i - 1] : first).x, p.y - (i ? rest[i - 1] : first).y), 0);
15019
+ const ms = (p, i) => {
15020
+ const prev = i ? rest[i - 1] : first;
15021
+ return total > 0 ? Math.max(16, 900 * Math.hypot(p.x - prev.x, p.y - prev.y) / total) : 16;
15022
+ };
15023
+ return [{ type: "move", to: first }, { type: "press" }, ...rest.map((p, i) => ({ type: "drag", to: p, ms: ms(p, i) })), { type: "release" }];
15024
+ }
15025
+ var PLANS = {
15026
+ addShape: (p) => {
15027
+ if (![p.x, p.y, p.width, p.height].every(num)) return null;
15028
+ return dragOut({ x: p.x, y: p.y }, { x: p.x + p.width, y: p.y + p.height });
15029
+ },
15030
+ addPath: (p) => typeof p.d === "string" ? trace(pathPoints(p.d)) : null,
15031
+ addText: (p) => {
15032
+ if (typeof p.path === "string") {
15033
+ const points = pathPoints(p.path, 2);
15034
+ return points.length ? [...click(points[0]), ...typed(p.text)] : null;
15035
+ }
15036
+ const x = num(p.x) ? p.x : 0;
15037
+ const y = num(p.y) ? p.y : 0;
15038
+ const start = num(p.width) && num(p.height) ? dragOut({ x, y }, { x: x + p.width, y: y + p.height }) : click({ x, y });
15039
+ return [...start, ...typed(p.text)];
15040
+ },
15041
+ placeSymbol: (p, controller) => {
15042
+ const doc = controller.getState().document;
15043
+ const t = Array.isArray(p.transform) && p.transform.length === 6 ? p.transform : null;
15044
+ const at = t ? { x: t[4], y: t[5] } : { x: num(p.x) ? p.x : doc.width / 2, y: num(p.y) ? p.y : doc.height / 2 };
15045
+ return click(at);
15046
+ },
15047
+ setTextStyle: (p, controller) => {
15048
+ const node = controller.getNode(p.id);
15049
+ if (node?.type !== "text") return null;
15050
+ const rect = createVectorPlaybackLocator(controller)({ kind: VECTOR_PLAYBACK_KIND, id: p.id });
15051
+ if (!rect) return null;
15052
+ const length = Math.max(1, node.text.length);
15053
+ const start = Math.min(length, Math.max(0, num(p.start) ? p.start : 0));
15054
+ const end = Math.min(length, Math.max(start, num(p.end) ? p.end : length));
15055
+ const y = rect.y + rect.height / 2;
15056
+ return [
15057
+ { type: "focus", rect },
15058
+ ...dragOut({ x: rect.x + rect.width * start / length, y }, { x: rect.x + rect.width * end / length, y })
15059
+ ];
15060
+ },
15061
+ setMeshPoint: (p, controller) => {
15062
+ const shading = controller.getShading(p.id);
15063
+ const point = shading?.points[p.index];
15064
+ if (!point) return null;
15065
+ return num(p.x) && num(p.y) ? dragOut(point, { x: p.x, y: p.y }) : click(point);
15066
+ }
15067
+ };
15068
+ var placeImage = (p, controller) => {
15069
+ const doc = controller.getState().document;
15070
+ const x = num(p.x) ? p.x : 0;
15071
+ const y = num(p.y) ? p.y : 0;
15072
+ const w = num(p.width) ? p.width : doc.width - x;
15073
+ return dragOut({ x, y }, { x: x + w, y: y + w });
15074
+ };
15075
+ function withVectorPlayback(controller, prefix, commands) {
15076
+ return commands.map((spec) => {
15077
+ if (!spec.id.startsWith(prefix)) return spec;
15078
+ const name = spec.id.slice(prefix.length);
15079
+ const props = spec.params?.properties ?? {};
15080
+ const planner = PLANS[name] ?? (/Image$/.test(name) && "x" in props && "width" in props ? placeImage : void 0);
15081
+ const surface = spec.playbackSurface ?? spec.scope ?? "vector";
15082
+ if (!planner || spec.plan) return { ...spec, playbackSurface: surface };
15083
+ return {
15084
+ ...spec,
15085
+ playbackSurface: surface,
15086
+ plan: (_ctx, params) => planner(params ?? {}, controller)
15087
+ };
15088
+ });
15089
+ }
15090
+
14977
15091
  // src/vector/commands.ts
14978
15092
  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). extrude: mode parallel|perspective, angle (degrees) and depth (units) for parallel, vanishingPoint {x, y} (offset from the middle) and toward (percent of the way there) for perspective, color (null: the shape fill colour), lightAngle (degrees the light comes from, 0 right, -90 above), shading (percent). bevel: width (units), lightAngle (degrees), shading (percent), color (null: the shape fill colour). 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).";
14979
15093
  var fail = (message) => {
@@ -17275,7 +17389,7 @@ function createVectorCommands(controller, options = {}) {
17275
17389
  }
17276
17390
  }
17277
17391
  ];
17278
- return [
17392
+ return withVectorPlayback(controller, prefix, [
17279
17393
  {
17280
17394
  ...base,
17281
17395
  id: `${prefix}list`,
@@ -18186,7 +18300,7 @@ function createVectorCommands(controller, options = {}) {
18186
18300
  return { nodes: controller.getState().document.nodes.length, selection: controller.getState().selection };
18187
18301
  }
18188
18302
  }
18189
- ];
18303
+ ]);
18190
18304
  }
18191
18305
 
18192
18306
  // src/vector/generate.ts
@@ -18680,6 +18794,7 @@ export {
18680
18794
  VECTOR_PATH_TEXT_BASELINES,
18681
18795
  VECTOR_PENCIL_STRING_PX,
18682
18796
  VECTOR_PERSPECTIVE_PLANES,
18797
+ VECTOR_PLAYBACK_KIND,
18683
18798
  VECTOR_PRESSURE_NOMINAL,
18684
18799
  VECTOR_ROTATE_HANDLE_OFFSET,
18685
18800
  VECTOR_SPATIAL_MIN,
@@ -18768,6 +18883,7 @@ export {
18768
18883
  createVectorGenerationToolProvider,
18769
18884
  createVectorMeshGradient,
18770
18885
  createVectorMorph,
18886
+ createVectorPlaybackLocator,
18771
18887
  cssFontFamily,
18772
18888
  curvatureMoveAnchor,
18773
18889
  cutSubpathAt,
@@ -19283,6 +19399,7 @@ export {
19283
19399
  withVectorAnchorIds,
19284
19400
  withVectorDocumentAnchorIds,
19285
19401
  withVectorFills,
19402
+ withVectorPlayback,
19286
19403
  withVectorStrokes,
19287
19404
  withVectorTargetPaint,
19288
19405
  zigzagPaths
@@ -0,0 +1,13 @@
1
+ import type { CommandSpec } from '../commands/registry.cjs';
2
+ import type { PlaybackRect, PlaybackRef } from '../tools/playback.cjs';
3
+ import type { VectorEditorController } from './controller.cjs';
4
+ /** The playback kind of a vector node (`{ kind: 'vector', id }`, as in `data-vector-id`). */
5
+ export declare const VECTOR_PLAYBACK_KIND = "vector";
6
+ /** Finds `{ kind: 'vector', id }` (a node's document-space bounds) for agent playback. */
7
+ export declare function createVectorPlaybackLocator(controller: Pick<VectorEditorController, 'getState' | 'getNode'>): (ref: PlaybackRef) => PlaybackRect | null;
8
+ /**
9
+ * Adds agent playback plans to vector commands whose effect has a place but no ids yet (shapes,
10
+ * paths, text, symbols, stylized images) and to edits of a part of a node (a text range, a mesh
11
+ * point). Other commands play from the ids in their params.
12
+ */
13
+ export declare function withVectorPlayback(controller: VectorEditorController, prefix: string, commands: CommandSpec[]): CommandSpec[];
@@ -0,0 +1,13 @@
1
+ import type { CommandSpec } from '../commands/registry.js';
2
+ import type { PlaybackRect, PlaybackRef } from '../tools/playback.js';
3
+ import type { VectorEditorController } from './controller.js';
4
+ /** The playback kind of a vector node (`{ kind: 'vector', id }`, as in `data-vector-id`). */
5
+ export declare const VECTOR_PLAYBACK_KIND = "vector";
6
+ /** Finds `{ kind: 'vector', id }` (a node's document-space bounds) for agent playback. */
7
+ export declare function createVectorPlaybackLocator(controller: Pick<VectorEditorController, 'getState' | 'getNode'>): (ref: PlaybackRef) => PlaybackRect | null;
8
+ /**
9
+ * Adds agent playback plans to vector commands whose effect has a place but no ids yet (shapes,
10
+ * paths, text, symbols, stylized images) and to edits of a part of a node (a text range, a mesh
11
+ * point). Other commands play from the ids in their params.
12
+ */
13
+ export declare function withVectorPlayback(controller: VectorEditorController, prefix: string, commands: CommandSpec[]): CommandSpec[];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pixerate/editor",
3
- "version": "0.140.3",
3
+ "version": "0.141.0",
4
4
  "description": "UI-agnostic rich text and media editing core library",
5
5
  "author": "Pixerate",
6
6
  "license": "MIT",
@@ -61,6 +61,19 @@ editor's, or (in discovery mode) only `list_toolsets`, `search_tools`, `enable_t
61
61
  6. **Undo your own mistakes** with the domain's `undo_*` tool or the `history.undo` command, not
62
62
  by hand-reverting with more edits.
63
63
 
64
+ ## Playback
65
+
66
+ The host may act your calls out on screen (agent playback): your cursor glides to what a call
67
+ names, presses, drags or types, and the change lands as one undo step. Results are the same with
68
+ or without it, and calls simply take a little longer while it plays.
69
+
70
+ - Do not add waits or poll for the animation; each call returns once its change is applied.
71
+ - Name your targets with the ids read tools return (`ids`, `nodeId`, `layerId`, a range). They are
72
+ also what the cursor heads for, so the user can follow along.
73
+ - A batch (`run_commands`) plays its steps' moves first, then applies once as one undo step.
74
+ - If you connect over MCP, your client's name labels your cursor; several agents get separate
75
+ cursors.
76
+
64
77
  ## Errors
65
78
 
66
79
  A failed call comes back as an `isError` result whose text says what was wrong and how to fix
@@ -271,6 +271,9 @@ effects provider (for example Flair); without one the read tools say so and the
271
271
  `get_editor_state` again, not guess.
272
272
  - `canvas_add_node` with `nearNodeId` also adds an edge from that node. Pass an explicit
273
273
  `position` (and no `nearNodeId`) for a free-standing design element.
274
+ - With agent playback on, the cursor clicks where `canvas_add_node` will place the node, drags
275
+ `canvas_connect` from source to target, and rings the nodes a layout or arrange command moved.
276
+ Nothing about the calls changes: still read ids with `get_editor_state` and pass them back.
274
277
  - `canvas_update_node` changes `data` only. Move nodes with layout, align or distribute commands,
275
278
  or by the host's own means; do not expect `position` inside `data` to move anything.
276
279
  - Align needs at least two nodes and distribute at least three; locked nodes never move. Shape
@@ -415,8 +415,8 @@ in the source frame at the document's bit depth, colour mode and profile. Read
415
415
 
416
416
  ## Pitfalls
417
417
 
418
- - Effects need a shape: on an empty layer they draw nothing. They cannot go on adjustment
419
- layers. A clipped layer's effects stay inside the layer it is clipped to.
418
+ - Agent playback (if on) opens each write's panel, then traces `points`, drags boxes or visits the layers and sliders named; results are unchanged.
419
+ - Effects need a shape: on an empty layer they draw nothing. They cannot go on adjustment layers. A clipped layer's effects stay inside the layer it is clipped to.
420
420
  - `bevel-emboss` has no `blend` / `opacity`: it uses `highlightBlend`, `highlightOpacity`,
421
421
  `shadowBlend` and `shadowOpacity`. Inner shadows and inner glows use `choke`, not `spread`.
422
422
  - Glows paint a colour or a gradient, never a pattern; strokes take all three. Pattern effects
@@ -459,6 +459,11 @@ the animation editor.
459
459
 
460
460
  The most common ones are below. Read [references/pitfalls.md](references/pitfalls.md) before working with connectors, frames, masks, filters, text, symbols, constraints, blends, gradient meshes or live corners.
461
461
 
462
+ - With agent playback on, `vector.addShape` is dragged out from (x, y) to (x + width, y + height),
463
+ `vector.addPath` is traced through its anchors, and text is clicked in and typed out. So give
464
+ shapes their real place and size in the first call rather than adding them at 0, 0 and moving
465
+ them after: the user watches the cursor go where the shape lands. Commands with `ids` visit those
466
+ nodes; nothing else about the calls changes.
462
467
  - `d` and anchor positions are in the node's own space; `bounds` are in document space. For
463
468
  rotated nodes (`rotation` not 0), `vector.moveAnchor` and `vector.bendSegment` take document
464
469
  coordinates, so compute them from the drawn position, not from `d`.
@@ -1,221 +0,0 @@
1
- // src/canvas/layout.ts
2
- import dagre from "@dagrejs/dagre";
3
- var DEFAULT_NODE_WIDTH = 200;
4
- var DEFAULT_NODE_HEIGHT = 60;
5
- var DEFAULT_PLACEMENT_GAP = 48;
6
- function getCenteredNodePosition(width = 0, height = 0, screenToFlowPosition, screenDimensions) {
7
- const rect = screenDimensions ?? (typeof document !== "undefined" ? { width: document.documentElement.clientWidth, height: document.documentElement.clientHeight } : { width: 1920, height: 1080 });
8
- const flowPos = screenToFlowPosition({ x: rect.width / 2, y: rect.height / 2 });
9
- return {
10
- x: flowPos.x - width / 2,
11
- y: flowPos.y - height / 2
12
- };
13
- }
14
- function centerNodes(nodes, screenToFlowPosition, getNodesBounds, screenDimensions) {
15
- if (nodes.length === 0) {
16
- return [];
17
- }
18
- const bounds = getNodesBounds(nodes);
19
- const boundingBoxOrigin = { x: bounds.x, y: bounds.y };
20
- const centeredPosition = getCenteredNodePosition(
21
- bounds.width,
22
- bounds.height,
23
- screenToFlowPosition,
24
- screenDimensions
25
- );
26
- const deltaX = centeredPosition.x - boundingBoxOrigin.x;
27
- const deltaY = centeredPosition.y - boundingBoxOrigin.y;
28
- return nodes.map((node) => ({
29
- ...node,
30
- position: {
31
- x: node.position.x + deltaX,
32
- y: node.position.y + deltaY
33
- }
34
- }));
35
- }
36
- function getLayoutedNodes(nodes, edges, getNodesBounds, options = {}) {
37
- if (nodes.length === 0) return [];
38
- const {
39
- direction = "LR",
40
- ranksep = 100,
41
- nodesep = 25,
42
- defaultNodeWidth = DEFAULT_NODE_WIDTH,
43
- defaultNodeHeight = DEFAULT_NODE_HEIGHT
44
- } = options;
45
- const dagreGraph = new dagre.graphlib.Graph();
46
- dagreGraph.setDefaultEdgeLabel(() => ({}));
47
- dagreGraph.setGraph({ rankdir: direction, ranksep, nodesep });
48
- const nodeIds = new Set(nodes.map((node) => node.id));
49
- nodes.forEach((node) => {
50
- const nodeWidth = node.measured?.width ?? node.width ?? defaultNodeWidth;
51
- const nodeHeight = node.measured?.height ?? node.height ?? defaultNodeHeight;
52
- dagreGraph.setNode(node.id, { width: nodeWidth, height: nodeHeight });
53
- });
54
- edges.forEach((edge) => {
55
- if (nodeIds.has(edge.source) && nodeIds.has(edge.target)) {
56
- dagreGraph.setEdge(edge.source, edge.target);
57
- }
58
- });
59
- dagre.layout(dagreGraph);
60
- return nodes.map((node) => {
61
- const nodeWithPosition = dagreGraph.node(node.id);
62
- const nodeWidth = node.measured?.width ?? node.width ?? defaultNodeWidth;
63
- const nodeHeight = node.measured?.height ?? node.height ?? defaultNodeHeight;
64
- return {
65
- ...node,
66
- position: {
67
- x: nodeWithPosition.x - nodeWidth / 2,
68
- y: nodeWithPosition.y - nodeHeight / 2
69
- }
70
- };
71
- });
72
- }
73
- function estimateNodeSize(node, defaultWidth = DEFAULT_NODE_WIDTH, defaultHeight = DEFAULT_NODE_HEIGHT) {
74
- const label = String(node.data?.label ?? node.data?.title ?? node.id ?? "");
75
- const lines = label.split(/\n|<br\s*\/?>/);
76
- const longest = Math.max(...lines.map((l) => l.replace(/<[^>]+>/g, "").length), 1);
77
- const width = Math.min(480, Math.max(defaultWidth, longest * 8 + 48));
78
- const height = Math.max(defaultHeight, lines.length * 20 + 24);
79
- return { width, height };
80
- }
81
- function boundsOf(rects) {
82
- if (!rects.length) return null;
83
- const minX = Math.min(...rects.map((r) => r.x));
84
- const minY = Math.min(...rects.map((r) => r.y));
85
- const maxX = Math.max(...rects.map((r) => r.x + r.width));
86
- const maxY = Math.max(...rects.map((r) => r.y + r.height));
87
- return { x: minX, y: minY, width: maxX - minX, height: maxY - minY };
88
- }
89
- function centre(r) {
90
- return { x: r.x + r.width / 2, y: r.y + r.height / 2 };
91
- }
92
- function overlaps(a, b, pad = 16) {
93
- return a.x < b.x + b.width + pad && a.x + a.width + pad > b.x && a.y < b.y + b.height + pad && a.y + a.height + pad > b.y;
94
- }
95
- function flowSide(direction = "LR") {
96
- switch (direction) {
97
- case "LR":
98
- return "right";
99
- case "RL":
100
- return "left";
101
- case "BT":
102
- return "above";
103
- case "TB":
104
- default:
105
- return "below";
106
- }
107
- }
108
- function ensureLayout(nodes, edges, options = {}) {
109
- if (nodes.length === 0) return [];
110
- const stored = options.storedPositions ?? {};
111
- const missing = nodes.filter((n) => !stored[n.id]);
112
- if (missing.length === 0) {
113
- return nodes.map((n) => ({
114
- ...n,
115
- position: { ...stored[n.id] }
116
- }));
117
- }
118
- const autoLayouted = getLayoutedNodes(nodes, edges, void 0, options);
119
- const autoMap = new Map(
120
- autoLayouted.map((n) => [n.id, n.position])
121
- );
122
- const both = nodes.filter((n) => stored[n.id] && autoMap.has(n.id));
123
- if (both.length === 0) {
124
- return autoLayouted;
125
- }
126
- let dx = 0;
127
- let dy = 0;
128
- for (const n of both) {
129
- const s = stored[n.id];
130
- const a = autoMap.get(n.id);
131
- dx += s.x - a.x;
132
- dy += s.y - a.y;
133
- }
134
- dx /= both.length;
135
- dy /= both.length;
136
- return nodes.map((node) => {
137
- if (stored[node.id]) {
138
- return {
139
- ...node,
140
- position: { ...stored[node.id] }
141
- };
142
- }
143
- const autoPos = autoMap.get(node.id) ?? { x: 0, y: 0 };
144
- return {
145
- ...node,
146
- position: {
147
- x: Math.round(autoPos.x + dx),
148
- y: Math.round(autoPos.y + dy)
149
- }
150
- };
151
- });
152
- }
153
- function placeBeside(rects, size, side, nearNodeId, options = {}) {
154
- const {
155
- gap = DEFAULT_PLACEMENT_GAP,
156
- ignore = [],
157
- maxAttempts = 32
158
- } = options;
159
- const skip = new Set([nearNodeId, ...ignore].filter(Boolean));
160
- const obstacles = Object.entries(rects).filter(([id]) => !skip.has(id)).map(([, r]) => r);
161
- const anchor = nearNodeId && rects[nearNodeId] || boundsOf(Object.values(rects)) || { x: 0, y: 0, width: 0, height: 0 };
162
- const anchorCenter = centre(anchor);
163
- let baseX;
164
- let baseY;
165
- switch (side) {
166
- case "right":
167
- baseX = anchor.x + anchor.width + gap;
168
- baseY = anchorCenter.y - size.height / 2;
169
- break;
170
- case "left":
171
- baseX = anchor.x - gap - size.width;
172
- baseY = anchorCenter.y - size.height / 2;
173
- break;
174
- case "above":
175
- baseX = anchorCenter.x - size.width / 2;
176
- baseY = anchor.y - gap - size.height;
177
- break;
178
- case "below":
179
- default:
180
- baseX = anchorCenter.x - size.width / 2;
181
- baseY = anchor.y + anchor.height + gap;
182
- break;
183
- }
184
- const isHorizontal = side === "right" || side === "left";
185
- const step = (isHorizontal ? size.height : size.width) + 20;
186
- for (let i = 0; i < maxAttempts; i++) {
187
- const offset = i === 0 ? 0 : Math.ceil(i / 2) * step * (i % 2 === 1 ? 1 : -1);
188
- const candidateBox = {
189
- x: isHorizontal ? baseX : baseX + offset,
190
- y: isHorizontal ? baseY + offset : baseY,
191
- width: size.width,
192
- height: size.height
193
- };
194
- if (!obstacles.some((obstacle) => overlaps(candidateBox, obstacle, 8))) {
195
- return {
196
- x: Math.round(candidateBox.x),
197
- y: Math.round(candidateBox.y)
198
- };
199
- }
200
- }
201
- return {
202
- x: Math.round(baseX),
203
- y: Math.round(baseY)
204
- };
205
- }
206
-
207
- export {
208
- DEFAULT_NODE_WIDTH,
209
- DEFAULT_NODE_HEIGHT,
210
- DEFAULT_PLACEMENT_GAP,
211
- getCenteredNodePosition,
212
- centerNodes,
213
- getLayoutedNodes,
214
- estimateNodeSize,
215
- boundsOf,
216
- centre,
217
- overlaps,
218
- flowSide,
219
- ensureLayout,
220
- placeBeside
221
- };