@pixerate/editor 0.162.1 → 0.164.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 (75) hide show
  1. package/README.md +1 -1
  2. package/dist/agent/index.cjs +34 -34
  3. package/dist/agent/index.js +5 -5
  4. package/dist/canvas/displacement.d.cts +34 -2
  5. package/dist/canvas/displacement.d.ts +34 -2
  6. package/dist/canvas/explosion-tools.d.cts +43 -0
  7. package/dist/canvas/explosion-tools.d.ts +43 -0
  8. package/dist/canvas/explosionLayout.d.cts +50 -0
  9. package/dist/canvas/explosionLayout.d.ts +50 -0
  10. package/dist/canvas/explosionState.d.cts +52 -0
  11. package/dist/canvas/explosionState.d.ts +52 -0
  12. package/dist/canvas/index.cjs +430 -88
  13. package/dist/canvas/index.d.cts +3 -0
  14. package/dist/canvas/index.d.ts +3 -0
  15. package/dist/canvas/index.js +425 -83
  16. package/dist/canvas/types.d.cts +27 -2
  17. package/dist/canvas/types.d.ts +27 -2
  18. package/dist/chart/commands.d.cts +2 -1
  19. package/dist/chart/commands.d.ts +2 -1
  20. package/dist/chart/controller.d.cts +109 -1
  21. package/dist/chart/controller.d.ts +109 -1
  22. package/dist/chart/index.cjs +605 -130
  23. package/dist/chart/index.d.cts +1 -0
  24. package/dist/chart/index.d.ts +1 -0
  25. package/dist/chart/index.js +490 -15
  26. package/dist/chart/layout-annotations.d.cts +29 -0
  27. package/dist/chart/layout-annotations.d.ts +29 -0
  28. package/dist/chart/layout-families.d.cts +23 -0
  29. package/dist/chart/layout-families.d.ts +23 -0
  30. package/dist/chart/mermaid.d.cts +2 -1
  31. package/dist/chart/mermaid.d.ts +2 -1
  32. package/dist/chart/schemas.d.cts +13 -0
  33. package/dist/chart/schemas.d.ts +13 -0
  34. package/dist/chart/spec.d.cts +30 -5
  35. package/dist/chart/spec.d.ts +30 -5
  36. package/dist/chart/stats.d.cts +9 -1
  37. package/dist/chart/stats.d.ts +9 -1
  38. package/dist/chart/tools.d.cts +1 -1
  39. package/dist/chart/tools.d.ts +1 -1
  40. package/dist/chart/tree.d.cts +62 -0
  41. package/dist/chart/tree.d.ts +62 -0
  42. package/dist/chart/types.d.cts +77 -3
  43. package/dist/chart/types.d.ts +77 -3
  44. package/dist/{chunk-7YBDRJXT.cjs → chunk-22KLZZGH.cjs} +8 -8
  45. package/dist/{chunk-HLOKB7WW.js → chunk-3NEZJCBY.js} +366 -6
  46. package/dist/{chunk-EVPZ5IKN.js → chunk-5FNGLV2D.js} +65 -3
  47. package/dist/{chunk-EQ46F75O.cjs → chunk-D54JHQK2.cjs} +51 -51
  48. package/dist/{chunk-NO5QQDHZ.js → chunk-DAPQGA55.js} +2 -2
  49. package/dist/{chunk-3IHNXRUV.cjs → chunk-GKNEJ6EN.cjs} +399 -67
  50. package/dist/{chunk-4NGVHHJB.cjs → chunk-NZD2MZJC.cjs} +80 -18
  51. package/dist/{chunk-CX6JLDFC.js → chunk-OXERWBKE.js} +3 -3
  52. package/dist/{chunk-7CKPUUN4.js → chunk-POR54B55.js} +61 -10
  53. package/dist/{chunk-LL3H5XUH.cjs → chunk-R3P2M7HU.cjs} +367 -7
  54. package/dist/{chunk-HDGBM7EA.cjs → chunk-SBQVEBSU.cjs} +580 -53
  55. package/dist/{chunk-XDCFJEU7.js → chunk-SZA42R4N.js} +346 -14
  56. package/dist/{chunk-GLMP22FT.js → chunk-TALQONZ5.js} +3 -3
  57. package/dist/{chunk-NA4UVVJ4.cjs → chunk-YGG7WDWT.cjs} +76 -25
  58. package/dist/{chunk-EY22AY6R.cjs → chunk-YSAGBIRW.cjs} +16 -16
  59. package/dist/{chunk-DTSTNRPH.js → chunk-Z4P2RBMJ.js} +536 -9
  60. package/dist/export/index.cjs +4 -4
  61. package/dist/export/index.js +3 -3
  62. package/dist/extensions/index.cjs +7 -7
  63. package/dist/extensions/index.js +6 -6
  64. package/dist/index.cjs +13 -13
  65. package/dist/index.js +7 -7
  66. package/dist/paint-editor/index.cjs +0 -20
  67. package/dist/paint-editor/index.js +0 -20
  68. package/package.json +1 -1
  69. package/skills/README.md +12 -0
  70. package/skills/pixerate-agent-tools/SKILL.md +34 -1
  71. package/skills/pixerate-canvas/SKILL.md +16 -0
  72. package/skills/pixerate-chart/SKILL.md +41 -9
  73. package/skills/pixerate-image-editor/SKILL.md +2 -0
  74. package/skills/pixerate-paint-editor/SKILL.md +2 -0
  75. package/skills/pixerate-tokens/SKILL.md +5 -0
@@ -37,3 +37,6 @@ export * from './effects.cjs';
37
37
  export * from './effect-player.cjs';
38
38
  export * from './effect-tools.cjs';
39
39
  export * from './editorDocument.cjs';
40
+ export * from './explosionLayout.cjs';
41
+ export * from './explosionState.cjs';
42
+ export * from './explosion-tools.cjs';
@@ -37,3 +37,6 @@ export * from './effects.js';
37
37
  export * from './effect-player.js';
38
38
  export * from './effect-tools.js';
39
39
  export * from './editorDocument.js';
40
+ export * from './explosionLayout.js';
41
+ export * from './explosionState.js';
42
+ export * from './explosion-tools.js';
@@ -113,10 +113,10 @@ import {
113
113
  } from "../chunk-5ED73EC6.js";
114
114
  import {
115
115
  layoutChart
116
- } from "../chunk-DTSTNRPH.js";
116
+ } from "../chunk-Z4P2RBMJ.js";
117
117
  import {
118
118
  createChartSpec
119
- } from "../chunk-HLOKB7WW.js";
119
+ } from "../chunk-3NEZJCBY.js";
120
120
  import "../chunk-II2WIF5Z.js";
121
121
  import {
122
122
  PathOpsError,
@@ -428,109 +428,162 @@ function computeNodesBoundingBox(nodes, defaultWidth = 200, defaultHeight = 60)
428
428
  height: maxY - minY
429
429
  };
430
430
  }
431
+ function nodeSizeOf(node, defaultWidth = 200, defaultHeight = 60) {
432
+ return {
433
+ width: node.measured?.width ?? node.width ?? defaultWidth,
434
+ height: node.measured?.height ?? node.height ?? defaultHeight
435
+ };
436
+ }
431
437
  function calculateDirectionalDisplacement(params) {
432
- const {
433
- existingNodes,
434
- originNode,
435
- childNodes,
436
- getNodesBounds,
437
- options = {}
438
- } = params;
438
+ const { existingNodes, originNode, childNodes, getNodesBounds, options = {} } = params;
439
439
  const {
440
440
  direction = "right",
441
441
  gap = 40,
442
442
  spreadSiblingLanes = false,
443
443
  siblingTolerance = 50,
444
- minY
444
+ minY,
445
+ maxY,
446
+ layoutMode = "freeform",
447
+ rowSpacing
445
448
  } = options;
449
+ const finite2 = (name, value) => {
450
+ if (value !== void 0 && !Number.isFinite(value)) {
451
+ throw new RangeError(`${name} must be a finite number (got ${value}).`);
452
+ }
453
+ };
454
+ finite2("gap", gap);
455
+ finite2("minY", minY);
456
+ finite2("maxY", maxY);
457
+ finite2("rowSpacing", rowSpacing);
458
+ if (rowSpacing !== void 0 && rowSpacing <= 0) {
459
+ throw new RangeError(`rowSpacing must be greater than 0 (got ${rowSpacing}).`);
460
+ }
461
+ if (minY !== void 0 && maxY !== void 0 && minY > maxY) {
462
+ throw new RangeError(`minY (${minY}) must not be greater than maxY (${maxY}).`);
463
+ }
446
464
  const displacedMap = /* @__PURE__ */ new Map();
447
465
  if (childNodes.length === 0 || existingNodes.length === 0) {
448
466
  return displacedMap;
449
467
  }
450
468
  const childBounds = getNodesBounds ? getNodesBounds(childNodes) : computeNodesBoundingBox(childNodes);
451
- const originWidth = originNode.measured?.width ?? originNode.width ?? 200;
452
- const originHeight = originNode.measured?.height ?? originNode.height ?? 60;
453
- const deltaPrimary = direction === "right" || direction === "left" ? childBounds.width + gap : childBounds.height + gap;
469
+ const { width: originWidth, height: originHeight } = nodeSizeOf(originNode);
470
+ const rowLane = layoutMode === "row-lane";
471
+ const lane = rowSpacing ?? originHeight;
472
+ const quantize = (delta) => rowLane && delta > 0 && lane > 0 ? Math.ceil(delta / lane) * lane : delta;
473
+ const laneOf = (node) => lane > 0 ? Math.round((node.position.y - originNode.position.y) / lane) : 0;
474
+ const isAbove = (node) => rowLane ? laneOf(node) < 0 : node.position.y < originNode.position.y;
475
+ const isBelow = (node) => rowLane ? laneOf(node) > 0 : node.position.y >= originNode.position.y + originHeight * 0.5;
476
+ const horizontal = direction === "right" || direction === "left";
477
+ const deltaPrimary = horizontal ? childBounds.width + gap : quantize(childBounds.height + gap);
454
478
  const deltaUp = Math.max(0, originNode.position.y - childBounds.y + gap);
455
- const deltaDown = Math.max(
456
- 0,
457
- childBounds.y + childBounds.height - (originNode.position.y + originHeight) + gap
458
- );
459
- const equalDelta = Math.max(deltaUp, deltaDown);
479
+ const deltaDown = Math.max(0, childBounds.y + childBounds.height - (originNode.position.y + originHeight) + gap);
480
+ const equalDelta = quantize(Math.max(deltaUp, deltaDown));
460
481
  for (const node of existingNodes) {
461
482
  if (node.id === originNode.id) continue;
462
- const isSiblingRow = Math.abs(node.position.y - originNode.position.y) < siblingTolerance;
463
- switch (direction) {
464
- case "right": {
465
- if (node.position.x > originNode.position.x) {
466
- displacedMap.set(node.id, {
467
- x: node.position.x + deltaPrimary,
468
- y: node.position.y
469
- });
470
- }
471
- break;
472
- }
473
- case "left": {
474
- if (node.position.x < originNode.position.x) {
475
- displacedMap.set(node.id, {
476
- x: node.position.x - deltaPrimary,
477
- y: node.position.y
478
- });
483
+ let newX = node.position.x;
484
+ let newY = node.position.y;
485
+ if (direction === "right") {
486
+ if (node.position.x >= originNode.position.x + originWidth * 0.5) {
487
+ newX += deltaPrimary;
488
+ } else if (spreadSiblingLanes) {
489
+ const { height } = nodeSizeOf(node);
490
+ const overlapsCluster = node.position.y + height >= childBounds.y - gap && node.position.y <= childBounds.y + childBounds.height + gap;
491
+ if (overlapsCluster && Math.abs(node.position.x - originNode.position.x) <= siblingTolerance) {
492
+ const push2 = node.position.y >= originNode.position.y ? 1 : -1;
493
+ newY += push2 * (childBounds.height * 0.5 + gap);
479
494
  }
480
- break;
481
495
  }
482
- case "down": {
483
- if (node.position.y > originNode.position.y) {
484
- displacedMap.set(node.id, {
485
- x: node.position.x,
486
- y: node.position.y + deltaPrimary
487
- });
488
- }
489
- break;
490
- }
491
- case "up": {
492
- if (node.position.y < originNode.position.y) {
493
- displacedMap.set(node.id, {
494
- x: node.position.x,
495
- y: node.position.y - deltaPrimary
496
- });
497
- }
498
- break;
496
+ } else if (direction === "left") {
497
+ if (node.position.x < originNode.position.x) newX -= deltaPrimary;
498
+ } else if (direction === "down") {
499
+ if (isBelow(node)) newY += deltaPrimary;
500
+ } else if (direction === "up") {
501
+ if (isAbove(node)) newY -= deltaPrimary;
502
+ } else if (direction === "vertical") {
503
+ if (isAbove(node)) newY -= equalDelta;
504
+ else if (isBelow(node)) newY += equalDelta;
505
+ }
506
+ if (newX !== node.position.x || newY !== node.position.y) {
507
+ displacedMap.set(node.id, { x: newX, y: newY });
508
+ }
509
+ }
510
+ if (minY !== void 0 || maxY !== void 0) {
511
+ const byId = new Map(existingNodes.map((n3) => [n3.id, n3]));
512
+ const rowsOf = (moved2) => {
513
+ const rows = /* @__PURE__ */ new Map();
514
+ for (const node of moved2) {
515
+ const key = rowLane ? laneOf(node) : node.position.y;
516
+ rows.set(key, [...rows.get(key) ?? [], node]);
499
517
  }
500
- case "vertical": {
501
- if (isSiblingRow) break;
502
- if (node.position.y < originNode.position.y) {
503
- displacedMap.set(node.id, {
504
- x: node.position.x,
505
- y: node.position.y - equalDelta
506
- });
507
- } else if (node.position.y > originNode.position.y) {
508
- displacedMap.set(node.id, {
509
- x: node.position.x,
510
- y: node.position.y + equalDelta
511
- });
512
- }
513
- break;
518
+ return [...rows.entries()].sort((a, b) => a[0] - b[0]).map(([, nodes]) => nodes);
519
+ };
520
+ const spacingFor = (nodes) => rowSpacing ?? Math.max(...nodes.map((n3) => nodeSizeOf(n3).height));
521
+ const moved = (sign) => [...displacedMap.entries()].filter(([id, pos]) => Math.sign(pos.y - byId.get(id).position.y) === sign).map(([id]) => byId.get(id));
522
+ if (minY !== void 0) {
523
+ const up = moved(-1);
524
+ if (up.length > 0) {
525
+ const spacing = spacingFor(up);
526
+ rowsOf(up).forEach((row, index) => {
527
+ const floor = minY + index * spacing;
528
+ for (const node of row) {
529
+ const pos = displacedMap.get(node.id);
530
+ displacedMap.set(node.id, { x: pos.x, y: Math.min(node.position.y, Math.max(floor, pos.y)) });
531
+ }
532
+ });
514
533
  }
515
534
  }
516
- }
517
- if (spreadSiblingLanes && (direction === "right" || direction === "left")) {
518
- for (const [nodeId, pos] of displacedMap.entries()) {
519
- const node = existingNodes.find((n3) => n3.id === nodeId);
520
- if (node && Math.abs(node.position.y - originNode.position.y) < siblingTolerance) {
521
- const isAbove = node.position.y < originNode.position.y;
522
- displacedMap.set(nodeId, {
523
- x: pos.x,
524
- y: pos.y + (isAbove ? -deltaUp : deltaDown)
535
+ if (maxY !== void 0) {
536
+ const down = moved(1);
537
+ if (down.length > 0) {
538
+ const spacing = spacingFor(down);
539
+ rowsOf(down).reverse().forEach((row, index) => {
540
+ for (const node of row) {
541
+ const pos = displacedMap.get(node.id);
542
+ const ceiling = maxY - index * spacing - nodeSizeOf(node).height;
543
+ displacedMap.set(node.id, { x: pos.x, y: Math.max(node.position.y, Math.min(ceiling, pos.y)) });
544
+ }
525
545
  });
526
546
  }
527
547
  }
548
+ for (const [id, pos] of [...displacedMap.entries()]) {
549
+ const node = byId.get(id);
550
+ if (pos.x === node.position.x && pos.y === node.position.y) displacedMap.delete(id);
551
+ }
528
552
  }
529
- if (minY !== void 0) {
530
- for (const [nodeId, pos] of displacedMap.entries()) {
531
- if (pos.y < minY) {
532
- displacedMap.set(nodeId, { x: pos.x, y: minY });
533
- }
553
+ return displacedMap;
554
+ }
555
+ function calculateReflowDisplacement(params) {
556
+ const { existingNodes, existingEdges, originNode, childNodes, childEdges = [], options = {} } = params;
557
+ const { direction = "LR", ranksep = 80, nodesep = 30, defaultNodeWidth = 200, defaultNodeHeight = 60 } = options;
558
+ const displacedMap = /* @__PURE__ */ new Map();
559
+ if (existingNodes.length === 0) return displacedMap;
560
+ const allNodes = [...existingNodes, ...childNodes];
561
+ const allEdges = [...existingEdges, ...childEdges];
562
+ const g = new dagre.graphlib.Graph();
563
+ g.setDefaultEdgeLabel(() => ({}));
564
+ g.setGraph({ rankdir: direction, ranksep, nodesep });
565
+ const allNodeIds = new Set(allNodes.map((n3) => n3.id));
566
+ for (const node of allNodes) {
567
+ g.setNode(node.id, nodeSizeOf(node, defaultNodeWidth, defaultNodeHeight));
568
+ }
569
+ for (const edge of allEdges) {
570
+ if (allNodeIds.has(edge.source) && allNodeIds.has(edge.target)) g.setEdge(edge.source, edge.target);
571
+ }
572
+ dagre.layout(g);
573
+ const dagreOrigin = g.node(originNode.id);
574
+ if (!dagreOrigin) return displacedMap;
575
+ const origin = nodeSizeOf(originNode, defaultNodeWidth, defaultNodeHeight);
576
+ const anchorOffsetX = originNode.position.x - (dagreOrigin.x - origin.width / 2);
577
+ const anchorOffsetY = originNode.position.y - (dagreOrigin.y - origin.height / 2);
578
+ for (const node of existingNodes) {
579
+ if (node.id === originNode.id) continue;
580
+ const laidOut = g.node(node.id);
581
+ if (!laidOut) continue;
582
+ const { width, height } = nodeSizeOf(node, defaultNodeWidth, defaultNodeHeight);
583
+ const targetX = laidOut.x - width / 2 + anchorOffsetX;
584
+ const targetY = laidOut.y - height / 2 + anchorOffsetY;
585
+ if (Math.abs(targetX - node.position.x) > 0.5 || Math.abs(targetY - node.position.y) > 0.5) {
586
+ displacedMap.set(node.id, { x: targetX, y: targetY });
534
587
  }
535
588
  }
536
589
  return displacedMap;
@@ -5142,7 +5195,7 @@ var defaultLabel = (node) => typeof node.data?.label === "string" && node.data.l
5142
5195
  function getTimelineRows(nodes, ids, options = {}) {
5143
5196
  const byId = new Map(nodes.map((n3) => [n3.id, n3]));
5144
5197
  const selection = options.selection ?? [];
5145
- const labelOf = options.labelOf ?? defaultLabel;
5198
+ const labelOf2 = options.labelOf ?? defaultLabel;
5146
5199
  const rows = [];
5147
5200
  for (const id of new Set(ids)) {
5148
5201
  const node = byId.get(id);
@@ -5153,7 +5206,7 @@ function getTimelineRows(nodes, ids, options = {}) {
5153
5206
  rows.push({
5154
5207
  id: `${id}:${property}`,
5155
5208
  nodeId: id,
5156
- nodeLabel: labelOf(node),
5209
+ nodeLabel: labelOf2(node),
5157
5210
  property,
5158
5211
  label,
5159
5212
  enabled: track.enabled !== false,
@@ -11801,6 +11854,288 @@ function createCanvasGestureRecorder(options) {
11801
11854
  }
11802
11855
  };
11803
11856
  }
11857
+
11858
+ // src/canvas/explosionLayout.ts
11859
+ var explosionEdgeId = (sourceId, targetId) => `explode:${sourceId}:${targetId}`;
11860
+ function computeCanvasExplosionLayout(params) {
11861
+ const {
11862
+ existingNodes,
11863
+ existingEdges = [],
11864
+ originNode,
11865
+ childNodes,
11866
+ childEdges = [],
11867
+ strategy = "directional",
11868
+ options,
11869
+ reflowOptions,
11870
+ connectParentToChildren = false,
11871
+ createEdge,
11872
+ getNodesBounds
11873
+ } = params;
11874
+ const existingIds = new Set(existingNodes.map((n3) => n3.id));
11875
+ if (!existingIds.has(originNode.id)) {
11876
+ throw new Error(`The origin node "${originNode.id}" is not in existingNodes; pass the canvas nodes including the origin.`);
11877
+ }
11878
+ const childIds = /* @__PURE__ */ new Set();
11879
+ for (const child of childNodes) {
11880
+ if (existingIds.has(child.id)) {
11881
+ throw new Error(`Child node id "${child.id}" is already on the canvas; give exploded children unique ids.`);
11882
+ }
11883
+ if (childIds.has(child.id)) throw new Error(`Child node id "${child.id}" is repeated; give exploded children unique ids.`);
11884
+ childIds.add(child.id);
11885
+ }
11886
+ const children = childNodes.map((child) => ({ ...child, position: { ...child.position } }));
11887
+ const edges = [...childEdges];
11888
+ const makeEdge = createEdge ?? ((source, target) => ({ id: explosionEdgeId(source, target), type: "default", source, target }));
11889
+ const connect = (child) => {
11890
+ if (!edges.some((e) => e.source === originNode.id && e.target === child.id)) edges.push(makeEdge(originNode.id, child.id));
11891
+ };
11892
+ if (connectParentToChildren === true || connectParentToChildren === "all") children.forEach(connect);
11893
+ else if (connectParentToChildren === "first" && children.length > 0) connect(children[0]);
11894
+ let displacedPositions;
11895
+ if (children.length === 0 || strategy === "none") {
11896
+ displacedPositions = /* @__PURE__ */ new Map();
11897
+ } else if (strategy === "dagre") {
11898
+ displacedPositions = calculateReflowDisplacement({
11899
+ existingNodes,
11900
+ existingEdges,
11901
+ originNode,
11902
+ childNodes: children,
11903
+ childEdges: edges,
11904
+ options: reflowOptions
11905
+ });
11906
+ } else {
11907
+ displacedPositions = calculateDirectionalDisplacement({
11908
+ existingNodes,
11909
+ originNode,
11910
+ childNodes: children,
11911
+ getNodesBounds,
11912
+ options
11913
+ });
11914
+ }
11915
+ const byId = new Map(existingNodes.map((n3) => [n3.id, n3]));
11916
+ const originalPositions = /* @__PURE__ */ new Map();
11917
+ let totalDisplacementDelta = 0;
11918
+ for (const [id, to] of displacedPositions) {
11919
+ const from = byId.get(id).position;
11920
+ originalPositions.set(id, { ...from });
11921
+ totalDisplacementDelta = Math.max(totalDisplacementDelta, Math.abs(to.x - from.x), Math.abs(to.y - from.y));
11922
+ }
11923
+ return {
11924
+ displacedPositions,
11925
+ originalPositions,
11926
+ childNodes: children,
11927
+ childEdges: edges,
11928
+ childBounds: getNodesBounds ? getNodesBounds(children) : computeNodesBoundingBox(children),
11929
+ totalDisplacementDelta
11930
+ };
11931
+ }
11932
+
11933
+ // src/canvas/explosionState.ts
11934
+ function createCanvasExplosionStateController(options = {}) {
11935
+ const duration = options.collapseDurationMs ?? 380;
11936
+ if (!Number.isFinite(duration) || duration < 0) {
11937
+ throw new RangeError(`collapseDurationMs must be a finite number >= 0 (got ${duration}).`);
11938
+ }
11939
+ let snapshot = { activeId: options.initialActiveId ?? null, collapsingId: null };
11940
+ let timer = null;
11941
+ let destroyed = false;
11942
+ const listeners = /* @__PURE__ */ new Set();
11943
+ const clearTimer = () => {
11944
+ if (timer !== null) clearTimeout(timer);
11945
+ timer = null;
11946
+ };
11947
+ const set = (next) => {
11948
+ if (next.activeId === snapshot.activeId && next.collapsingId === snapshot.collapsingId) return;
11949
+ snapshot = next;
11950
+ for (const listener of [...listeners]) listener();
11951
+ };
11952
+ const finishCollapse = () => {
11953
+ clearTimer();
11954
+ if (snapshot.collapsingId !== null) set({ activeId: snapshot.activeId, collapsingId: null });
11955
+ };
11956
+ const explode = (nodeId) => {
11957
+ if (destroyed) return false;
11958
+ if (typeof nodeId !== "string" || nodeId === "") throw new TypeError("explode needs a node id.");
11959
+ if (snapshot.activeId === nodeId) return false;
11960
+ clearTimer();
11961
+ const previous = snapshot.activeId;
11962
+ set({ activeId: nodeId, collapsingId: null });
11963
+ if (previous !== null) options.onCollapse?.(previous);
11964
+ options.onExplode?.(nodeId);
11965
+ return true;
11966
+ };
11967
+ const collapse = (nodeId) => {
11968
+ if (destroyed) return false;
11969
+ const active = snapshot.activeId;
11970
+ if (active === null || nodeId !== void 0 && nodeId !== active) return false;
11971
+ clearTimer();
11972
+ set({ activeId: null, collapsingId: active });
11973
+ options.onCollapse?.(active);
11974
+ if (duration === 0) finishCollapse();
11975
+ else timer = setTimeout(() => {
11976
+ timer = null;
11977
+ finishCollapse();
11978
+ }, duration);
11979
+ return true;
11980
+ };
11981
+ return {
11982
+ get activeId() {
11983
+ return snapshot.activeId;
11984
+ },
11985
+ get collapsingId() {
11986
+ return snapshot.collapsingId;
11987
+ },
11988
+ get isExplodingOrCollapsing() {
11989
+ return snapshot.activeId !== null || snapshot.collapsingId !== null;
11990
+ },
11991
+ get destroyed() {
11992
+ return destroyed;
11993
+ },
11994
+ getSnapshot: () => snapshot,
11995
+ subscribe(listener) {
11996
+ if (destroyed) return () => {
11997
+ };
11998
+ listeners.add(listener);
11999
+ return () => listeners.delete(listener);
12000
+ },
12001
+ isExploded: (nodeId) => snapshot.activeId === nodeId,
12002
+ isCollapsing: (nodeId) => snapshot.collapsingId === nodeId,
12003
+ shouldRenderChildren: (nodeId) => snapshot.activeId === nodeId || snapshot.collapsingId === nodeId,
12004
+ explode,
12005
+ collapse,
12006
+ toggle: (nodeId) => snapshot.activeId === nodeId ? (collapse(nodeId), false) : explode(nodeId),
12007
+ finishCollapse: () => {
12008
+ if (!destroyed) finishCollapse();
12009
+ },
12010
+ destroy() {
12011
+ clearTimer();
12012
+ destroyed = true;
12013
+ listeners.clear();
12014
+ }
12015
+ };
12016
+ }
12017
+
12018
+ // src/canvas/explosion-tools.ts
12019
+ function canvasExplosionToolTarget(state, options) {
12020
+ return {
12021
+ getNodes: options.getNodes,
12022
+ canExplode: options.canExplode,
12023
+ exploded: () => state.activeId ? [state.activeId] : [],
12024
+ collapsing: () => state.collapsingId ? [state.collapsingId] : [],
12025
+ explode: (id) => {
12026
+ state.explode(id);
12027
+ },
12028
+ collapse: (id) => state.collapse(id)
12029
+ };
12030
+ }
12031
+ var READ = { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: false };
12032
+ var WRITE_IDEMPOTENT = { readOnlyHint: false, destructiveHint: false, idempotentHint: true, openWorldHint: false };
12033
+ var labelOf = (node) => {
12034
+ const label = node.data?.label ?? node.data?.name;
12035
+ return typeof label === "string" && label !== "" ? label : void 0;
12036
+ };
12037
+ function createCanvasExplosionToolProvider(target, options = {}) {
12038
+ const readOnlyReason = () => {
12039
+ const ro = typeof options.readOnly === "function" ? options.readOnly() : options.readOnly;
12040
+ return ro ? typeof ro === "string" ? ro : "Exploding nodes is disabled on this canvas." : null;
12041
+ };
12042
+ const reasonNotExplodable = (node) => {
12043
+ const ok = target.canExplode ? target.canExplode(node) : true;
12044
+ if (ok === true) return null;
12045
+ return typeof ok === "string" && ok ? ok : `Node "${node.id}" has nothing to explode into.`;
12046
+ };
12047
+ const state = () => ({ exploded: [...target.exploded()], collapsing: [...target.collapsing?.() ?? []] });
12048
+ const findNode = (id) => {
12049
+ const node = target.getNodes().find((n3) => n3.id === id);
12050
+ if (!node) throw new Error(`No node "${id}" on the canvas. Call list_explodable_nodes for node ids.`);
12051
+ return node;
12052
+ };
12053
+ const refuseIfReadOnly = () => {
12054
+ const reason = readOnlyReason();
12055
+ if (reason) throw new Error(reason);
12056
+ };
12057
+ const listTool = {
12058
+ name: "list_explodable_nodes",
12059
+ title: "List explodable nodes",
12060
+ description: "Lists the canvas nodes that can explode into child nodes (id, label, exploded, collapsing), plus the ids currently exploded and collapsing, and whether exploding is read-only. Call before explode_node or collapse_node.",
12061
+ inputSchema: { type: "object", properties: {}, additionalProperties: false },
12062
+ annotations: { title: "List explodable nodes", ...READ },
12063
+ handler: () => {
12064
+ const { exploded, collapsing } = state();
12065
+ const nodes = target.getNodes().filter((n3) => exploded.includes(n3.id) || reasonNotExplodable(n3) === null).map((n3) => ({
12066
+ id: n3.id,
12067
+ ...labelOf(n3) ? { label: labelOf(n3) } : {},
12068
+ exploded: exploded.includes(n3.id),
12069
+ collapsing: collapsing.includes(n3.id)
12070
+ }));
12071
+ return { nodes, exploded, collapsing, multiple: target.multiple === true, readOnly: readOnlyReason() };
12072
+ }
12073
+ };
12074
+ const explodeTool = {
12075
+ name: "explode_node",
12076
+ title: "Explode a node",
12077
+ description: "Expands a node into its child nodes (subtasks, steps, details), moving surrounding nodes to make room. Unless several can be open at once, the node exploded before collapses. Exploding an exploded node changes nothing. Returns the node, the ids now exploded, and the ids that collapsed.",
12078
+ inputSchema: {
12079
+ type: "object",
12080
+ properties: {
12081
+ node: { type: "string", minLength: 1, description: "Id of the node to explode, from list_explodable_nodes." }
12082
+ },
12083
+ required: ["node"],
12084
+ additionalProperties: false
12085
+ },
12086
+ annotations: { title: "Explode a node", ...WRITE_IDEMPOTENT },
12087
+ handler: async (args) => {
12088
+ refuseIfReadOnly();
12089
+ const node = findNode(args.node);
12090
+ const before = state().exploded;
12091
+ if (before.includes(node.id)) return { node: node.id, changed: false, exploded: before, collapsed: [] };
12092
+ const reason = reasonNotExplodable(node);
12093
+ if (reason) throw new Error(reason);
12094
+ if (await target.explode(node.id) === false) {
12095
+ throw new Error(`Node "${node.id}" could not explode (the canvas refused, e.g. an animation is running). Try again or call list_explodable_nodes.`);
12096
+ }
12097
+ const after = state().exploded;
12098
+ return { node: node.id, changed: true, exploded: after, collapsed: before.filter((id) => !after.includes(id)) };
12099
+ }
12100
+ };
12101
+ const collapseTool = {
12102
+ name: "collapse_node",
12103
+ title: "Collapse a node",
12104
+ description: "Collapses an exploded node: its child nodes are removed (after the collapse animation) and moved nodes return to where they were. Returns the node and the ids still exploded.",
12105
+ inputSchema: {
12106
+ type: "object",
12107
+ properties: {
12108
+ node: { type: "string", minLength: 1, description: "Id of an exploded node, from list_explodable_nodes (exploded: true)." }
12109
+ },
12110
+ required: ["node"],
12111
+ additionalProperties: false
12112
+ },
12113
+ annotations: { title: "Collapse a node", ...WRITE_IDEMPOTENT },
12114
+ handler: async (args) => {
12115
+ refuseIfReadOnly();
12116
+ const before = state().exploded;
12117
+ if (!before.includes(args.node)) {
12118
+ findNode(args.node);
12119
+ throw new Error(
12120
+ `Node "${args.node}" is not exploded${before.length ? ` (exploded: ${before.join(", ")})` : " (nothing is exploded)"}. Call list_explodable_nodes.`
12121
+ );
12122
+ }
12123
+ if (await target.collapse(args.node) === false) {
12124
+ throw new Error(`Node "${args.node}" could not collapse (the canvas refused). Try again or call list_explodable_nodes.`);
12125
+ }
12126
+ return { node: args.node, exploded: state().exploded, collapsing: state().collapsing };
12127
+ }
12128
+ };
12129
+ return {
12130
+ id: options.id ?? "canvas-explosion",
12131
+ namespace: options.namespace,
12132
+ title: "Canvas explosion",
12133
+ description: "Explode canvas nodes into their child nodes and collapse them again.",
12134
+ skill: "pixerate-canvas",
12135
+ tools: () => readOnlyReason() ? [listTool] : [listTool, explodeTool, collapseTool],
12136
+ subscribe: options.subscribe
12137
+ };
12138
+ }
11804
12139
  export {
11805
12140
  ANIMATABLE_PROPERTIES,
11806
12141
  ANIMATION_DATA_KEY,
@@ -11896,10 +12231,12 @@ export {
11896
12231
  calculateCurvedPath,
11897
12232
  calculateDagreReflowDisplacement,
11898
12233
  calculateDirectionalDisplacement,
12234
+ calculateReflowDisplacement,
11899
12235
  canCombineShapeNodes,
11900
12236
  canOffsetShapeNodes,
11901
12237
  canOutlineShapeNodeStrokes,
11902
12238
  canvasEffectsPool,
12239
+ canvasExplosionToolTarget,
11903
12240
  canvasHeight,
11904
12241
  canvasNodeCaptureRect,
11905
12242
  canvasNodeEffect,
@@ -11912,6 +12249,7 @@ export {
11912
12249
  collectDescendantIds,
11913
12250
  combineCanvasComponentsAsVariants,
11914
12251
  combineShapeNodes,
12252
+ computeCanvasExplosionLayout,
11915
12253
  computeDragBounds,
11916
12254
  computeDragBoundsFromPoints,
11917
12255
  computeNodesBoundingBox,
@@ -11936,6 +12274,8 @@ export {
11936
12274
  createCanvasEditorDocument,
11937
12275
  createCanvasEffectsController,
11938
12276
  createCanvasEffectsToolProvider,
12277
+ createCanvasExplosionStateController,
12278
+ createCanvasExplosionToolProvider,
11939
12279
  createCanvasGestureRecorder,
11940
12280
  createCanvasInstance,
11941
12281
  createCanvasPlaybackLocator,
@@ -11986,6 +12326,7 @@ export {
11986
12326
  evaluateEasing,
11987
12327
  evaluateTrack,
11988
12328
  excludePaths,
12329
+ explosionEdgeId,
11989
12330
  exportCssAnimation,
11990
12331
  familySceneFromLayout,
11991
12332
  findConnectorSnap,
@@ -12145,6 +12486,7 @@ export {
12145
12486
  multiPolygonArea,
12146
12487
  multiPolygonBounds,
12147
12488
  nearestHandlePositions,
12489
+ nodeSizeOf,
12148
12490
  normalizeBezierPath,
12149
12491
  normalizeKeyframes,
12150
12492
  offsetPath,
@@ -149,12 +149,38 @@ export type DisplacementDirection = 'right' | 'left' | 'down' | 'up' | 'vertical
149
149
  * Options for directional displacement calculation.
150
150
  */
151
151
  export interface DirectionalDisplacementOptions {
152
+ /** Which nodes move and where: `right` / `left` / `up` / `down` push one side; `vertical` opens the row up and down. Default `right`. */
152
153
  direction?: DisplacementDirection;
154
+ /** Clearance between the child cluster and displaced nodes, in canvas units. Default 40. */
153
155
  gap?: number;
156
+ /** With `right`, also push nodes in the origin's column that overlap the cluster up or down. Default false. */
154
157
  spreadSiblingLanes?: boolean;
158
+ /** Distance in canvas units within which a node counts as a sibling (same column for `right`, same row for `row-lane`). Default 50. */
155
159
  siblingTolerance?: number;
160
+ /**
161
+ * Floor for the top edge of nodes moved upward (e.g. below a pinned header). Rows pushed into it
162
+ * stack from `minY` down by `rowSpacing` so they never overlap, and never move below where they started.
163
+ */
156
164
  minY?: number;
165
+ /**
166
+ * Ceiling for the bottom edge of nodes moved downward. Rows pushed into it stack from `maxY` up by
167
+ * `rowSpacing`, and never move above where they started.
168
+ */
169
+ maxY?: number;
170
+ /**
171
+ * `freeform` (default) moves nodes by the exact clearance. `row-lane` treats the canvas as rows
172
+ * `rowSpacing` apart: nodes are bucketed into lanes relative to the origin row, the origin's lane
173
+ * stays put, and vertical deltas are rounded up to whole lanes so rows glide apart by exact lane heights.
174
+ */
175
+ layoutMode?: 'freeform' | 'row-lane';
176
+ /**
177
+ * Lane height in canvas units. Required for `row-lane` (default: the origin's height); also the
178
+ * spacing between rows stacked against `minY` / `maxY` (default: the tallest node in those rows).
179
+ */
180
+ rowSpacing?: number;
157
181
  }
182
+ /** How existing nodes make room when a node explodes. */
183
+ export type ExplosionDisplacementStrategy = 'directional' | 'dagre' | 'none';
158
184
  /**
159
185
  * Options for Dagre reflow displacement calculation.
160
186
  */
@@ -181,8 +207,7 @@ export interface ExplodeNodeOptions<TNode extends CanvasNode = CanvasNode, TEdge
181
207
  childNodes: TNode[];
182
208
  childEdges?: TEdge[];
183
209
  connectParentToChildren?: boolean | 'first' | 'all';
184
- connectChildrenToDownstream?: boolean;
185
- displacementStrategy?: 'directional' | 'dagre' | 'none';
210
+ displacementStrategy?: ExplosionDisplacementStrategy;
186
211
  directionalOptions?: DirectionalDisplacementOptions;
187
212
  reflowOptions?: Partial<ReflowDisplacementOptions>;
188
213
  trajectory?: TrajectoryFunction;