@field/cascade 0.8.0 → 0.11.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 (195) hide show
  1. package/AGENTS.md +36 -3
  2. package/CHANGELOG.md +401 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -26
  5. package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
  6. package/dist/assets/CodePanel-BU-autmq.js +2 -0
  7. package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
  8. package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
  9. package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
  10. package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
  11. package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
  12. package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
  13. package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
  14. package/dist/assets/definitions-GreEMXLV.js +1 -0
  15. package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
  16. package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
  17. package/dist/assets/geo-u0n_jOsD.js +1 -0
  18. package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
  19. package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
  20. package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
  21. package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
  22. package/dist/assets/index-5ZtDAuy2.js +220 -0
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-client-DqYmKstU.js +1 -0
  25. package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
  26. package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
  27. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  28. package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
  29. package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
  30. package/dist/assets/math-DioIQOPJ.js +1 -0
  31. package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
  32. package/dist/assets/pop-DC_TQG9t.js +1 -0
  33. package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
  34. package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
  35. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  36. package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
  37. package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
  38. package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
  39. package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
  40. package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
  41. package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
  42. package/dist/cli/chunk-2KXB47FP.js +115 -0
  43. package/dist/cli/chunk-35D343DC.js +1447 -0
  44. package/dist/cli/chunk-36P5K3SD.js +631 -0
  45. package/dist/cli/chunk-3JTJHF7V.js +267 -0
  46. package/dist/cli/chunk-4UDFYF5G.js +132 -0
  47. package/dist/cli/chunk-53NMBJAO.js +35 -0
  48. package/dist/cli/chunk-A4OAMOZH.js +310 -0
  49. package/dist/cli/chunk-BIJO5OFS.js +71 -0
  50. package/dist/cli/chunk-BP7UKF36.js +376 -0
  51. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  52. package/dist/cli/chunk-CO22XHNG.js +593 -0
  53. package/dist/cli/chunk-D6BUOHUG.js +634 -0
  54. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  55. package/dist/cli/chunk-DUIUFVU2.js +241 -0
  56. package/dist/cli/chunk-EUIAC3U3.js +3006 -0
  57. package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
  58. package/dist/cli/chunk-GFS6W3C5.js +308 -0
  59. package/dist/cli/chunk-HFHWYL55.js +139 -0
  60. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  61. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  62. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  63. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  64. package/dist/cli/chunk-MIIEFUGG.js +623 -0
  65. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  66. package/dist/cli/chunk-POMMNKZZ.js +672 -0
  67. package/dist/cli/chunk-QBBKRNGY.js +90 -0
  68. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  69. package/dist/cli/chunk-RLCAJBVR.js +224 -0
  70. package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
  71. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  72. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  73. package/dist/cli/chunk-SGJAET2L.js +197 -0
  74. package/dist/cli/chunk-VVGSLVSF.js +151 -0
  75. package/dist/cli/chunk-WD62JIP6.js +118 -0
  76. package/dist/cli/chunk-WIH4TEIS.js +104 -0
  77. package/dist/cli/chunk-WKURDVYP.js +1119 -0
  78. package/dist/cli/chunk-WPUDGEFY.js +20 -0
  79. package/dist/cli/chunk-XWDSKG7N.js +1301 -0
  80. package/dist/cli/chunk-YAT5Y25U.js +121 -0
  81. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  82. package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
  83. package/dist/cli/commands-Q23M26QM.js +1292 -0
  84. package/dist/cli/definitions-HF2FTHYU.js +32 -0
  85. package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
  86. package/dist/cli/edit-JHCPTQFT.js +28 -0
  87. package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
  88. package/dist/cli/encode-HAUZTYPT.js +17 -0
  89. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  90. package/dist/cli/geo-4RNK3QVN.js +43 -0
  91. package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
  92. package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
  93. package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
  94. package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
  95. package/dist/cli/index.js +106 -14207
  96. package/dist/cli/math-ZRMLKU2D.js +16 -0
  97. package/dist/cli/mcp-ZLEEMYGF.js +164 -0
  98. package/dist/cli/parity-KINARCP2.js +328 -0
  99. package/dist/cli/pop-USDFXB7H.js +39 -0
  100. package/dist/cli/project-K3QIPGBB.js +12 -0
  101. package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
  102. package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
  103. package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
  104. package/dist/cli/render-YURAPPUI.js +436 -0
  105. package/dist/cli/runCommand-PI764LEG.js +198 -0
  106. package/dist/cli/runner-RA6UKEPD.js +37 -0
  107. package/dist/cli/runtime/image.ts +277 -0
  108. package/dist/cli/runtime/stage.ts +74 -11
  109. package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
  110. package/dist/cli/verbs-RCE3K4VQ.js +19 -0
  111. package/dist/cli/webBuild-GT4PZT5D.js +37 -0
  112. package/dist/contracts/bundle.d.ts +75 -0
  113. package/dist/contracts/bundle.js +112 -0
  114. package/dist/contracts/definition.d.ts +67 -4
  115. package/dist/contracts/document.d.ts +29 -0
  116. package/dist/contracts/documentFormat.d.ts +18 -0
  117. package/dist/contracts/documentFormat.js +195 -0
  118. package/dist/contracts/editor.d.ts +189 -0
  119. package/dist/contracts/editor.js +2 -0
  120. package/dist/contracts/index.d.ts +3 -0
  121. package/dist/contracts/index.js +3 -0
  122. package/dist/contracts/validate.js +59 -3
  123. package/dist/contracts/values.d.ts +3 -3
  124. package/dist/contracts/values.js +1 -0
  125. package/dist/image.d.ts +141 -0
  126. package/dist/image.js +127 -0
  127. package/dist/index.html +4 -4
  128. package/dist/player/app.js +17 -5
  129. package/dist/player/editor.js +1919 -0
  130. package/dist/player/editorEngine.js +477 -0
  131. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  132. package/dist/runtime/builtins/core/bundle.js +81 -0
  133. package/dist/runtime/builtins/core/index.d.ts +3 -2
  134. package/dist/runtime/builtins/core/index.js +6 -2
  135. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  136. package/dist/runtime/builtins/core/routing.js +28 -0
  137. package/dist/runtime/builtins/headless.d.ts +19 -0
  138. package/dist/runtime/builtins/headless.js +25 -0
  139. package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
  140. package/dist/runtime/builtins/image/contactSheet.js +199 -0
  141. package/dist/runtime/builtins/image/index.d.ts +9 -0
  142. package/dist/runtime/builtins/image/index.js +9 -0
  143. package/dist/runtime/edit/document.d.ts +94 -0
  144. package/dist/runtime/edit/document.js +194 -0
  145. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  146. package/dist/runtime/edit/graphEditor.js +893 -0
  147. package/dist/runtime/edit/index.d.ts +12 -0
  148. package/dist/runtime/edit/index.js +6 -0
  149. package/dist/runtime/edit/layout.d.ts +50 -0
  150. package/dist/runtime/edit/layout.js +126 -0
  151. package/dist/runtime/edit/operations.d.ts +65 -0
  152. package/dist/runtime/edit/operations.js +57 -0
  153. package/dist/runtime/edit/parameters.d.ts +75 -0
  154. package/dist/runtime/edit/parameters.js +284 -0
  155. package/dist/runtime/edit/read.d.ts +35 -0
  156. package/dist/runtime/edit/read.js +65 -0
  157. package/dist/runtime/editor/index.d.ts +104 -0
  158. package/dist/runtime/editor/index.js +439 -0
  159. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  160. package/dist/runtime/expressions/bundlePaths.js +55 -0
  161. package/dist/runtime/expressions/index.d.ts +2 -0
  162. package/dist/runtime/expressions/index.js +1 -0
  163. package/dist/runtime/index.d.ts +2 -0
  164. package/dist/runtime/index.js +2 -0
  165. package/dist/runtime/params/animator.d.ts +5 -0
  166. package/dist/runtime/params/animator.js +16 -0
  167. package/dist/runtime/runtime.js +238 -12
  168. package/dist/runtime/seed.d.ts +29 -0
  169. package/dist/runtime/seed.js +52 -0
  170. package/dist/runtime/types.d.ts +12 -0
  171. package/dist/stage.js +43 -9
  172. package/dist/studio/panel.d.ts +2 -0
  173. package/doc/CLI_REFERENCE.md +171 -0
  174. package/doc/EDITOR_EXPORT.md +235 -0
  175. package/doc/HEADLESS_GPU.md +27 -0
  176. package/doc/MCP.md +69 -0
  177. package/doc/NODE_REFERENCE.md +208 -0
  178. package/doc/PROJECT_AUTHORING.md +258 -1
  179. package/package.json +29 -8
  180. package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
  181. package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
  182. package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
  183. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  184. package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
  185. package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
  186. package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
  187. package/dist/assets/definitions-DZpOjWKR.js +0 -1
  188. package/dist/assets/geo-D2VPjAdt.js +0 -1
  189. package/dist/assets/index-CMiBdEuT.js +0 -212
  190. package/dist/assets/index-DMIuml9a.css +0 -1
  191. package/dist/assets/index-client-BI2H82FJ.js +0 -1
  192. package/dist/assets/legacy-ZA7textG.js +0 -2
  193. package/dist/assets/math-B6hpxPJO.js +0 -1
  194. package/dist/assets/pop-CYUIjT6x.js +0 -1
  195. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -0,0 +1,12 @@
1
+ export { GraphEditor, GraphEditError, defaultIdFor, splitEndpoint } from "./graphEditor.js";
2
+ export type { AddNodeInput, GraphEditorOptions, NodeSummary } from "./graphEditor.js";
3
+ export { applyOperation, GRAPH_EDIT_OPERATIONS } from "./operations.js";
4
+ export type { GraphEditOperation } from "./operations.js";
5
+ export { readGraph } from "./read.js";
6
+ export type { GraphRead, GraphReadNode } from "./read.js";
7
+ export { flowStep, placeNode } from "./layout.js";
8
+ export type { Point } from "./layout.js";
9
+ export { readWire, wireLabel } from "./document.js";
10
+ export type { EditableDocument, EditableNode, Wire, Endpoint } from "./document.js";
11
+ export type { ParameterChange } from "./parameters.js";
12
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,6 @@
1
+ export { GraphEditor, GraphEditError, defaultIdFor, splitEndpoint } from "./graphEditor.js";
2
+ export { applyOperation, GRAPH_EDIT_OPERATIONS } from "./operations.js";
3
+ export { readGraph } from "./read.js";
4
+ export { flowStep, placeNode } from "./layout.js";
5
+ export { readWire, wireLabel } from "./document.js";
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Where an agent-created node goes on the canvas.
3
+ *
4
+ * Not a graph layout. Studio has one (`src/utils/autoLayout.ts`) and running it
5
+ * on every agent edit would move every node the author placed by hand, which
6
+ * is the opposite of a minimal diff. This places ONE node, among siblings that
7
+ * stay where they are, so that opening Studio after an agent session shows the
8
+ * new node next to what it is wired to and in line with the existing flow.
9
+ *
10
+ * The flow is read from the graph rather than assumed. Studio draws inputs on
11
+ * top and outputs on the bottom, so a fresh graph flows down; but a graph
12
+ * whose stages step diagonally (cloud-plots' top level) should get its next
13
+ * stage on the same diagonal. The step is the median offset between wired
14
+ * siblings, so one odd wire does not set the direction.
15
+ */
16
+ export interface Point {
17
+ readonly x: number;
18
+ readonly y: number;
19
+ }
20
+ /** Studio's own spacing (`autoLayout.ts`): rows 190 apart, columns 260. */
21
+ export declare const DEFAULT_STEP: Point;
22
+ /**
23
+ * The direction and distance from a node to the one it feeds, measured as the
24
+ * component-wise median over the given wires. Falls back to Studio's downward
25
+ * flow when nothing is wired yet, and ignores steps too short to be a layout
26
+ * decision (two nodes stacked by accident).
27
+ */
28
+ export declare function flowStep(wires: readonly {
29
+ from: Point;
30
+ to: Point;
31
+ }[]): Point;
32
+ export interface PlacementRequest {
33
+ /** Where the other nodes on the same canvas already are. */
34
+ readonly siblings: readonly Point[];
35
+ /** Positions of the nodes wired into the new node. */
36
+ readonly upstream: readonly Point[];
37
+ /** Positions of the nodes the new node feeds. */
38
+ readonly downstream: readonly Point[];
39
+ readonly step: Point;
40
+ }
41
+ /**
42
+ * One free position: a step past what feeds the node, or a step before what it
43
+ * feeds, or a staging lane beside the graph when it is wired to nothing yet. If that spot
44
+ * is taken it moves sideways across the flow, alternating sides, until it is
45
+ * clear — so a second consumer of the same node lands beside the first.
46
+ */
47
+ export declare function placeNode({ siblings, upstream, downstream, step }: PlacementRequest): Point;
48
+ /** True when a node at `point` would sit on top of an existing one. */
49
+ export declare function isOccupied(siblings: readonly Point[], point: Point): boolean;
50
+ //# sourceMappingURL=layout.d.ts.map
@@ -0,0 +1,126 @@
1
+ /**
2
+ * Where an agent-created node goes on the canvas.
3
+ *
4
+ * Not a graph layout. Studio has one (`src/utils/autoLayout.ts`) and running it
5
+ * on every agent edit would move every node the author placed by hand, which
6
+ * is the opposite of a minimal diff. This places ONE node, among siblings that
7
+ * stay where they are, so that opening Studio after an agent session shows the
8
+ * new node next to what it is wired to and in line with the existing flow.
9
+ *
10
+ * The flow is read from the graph rather than assumed. Studio draws inputs on
11
+ * top and outputs on the bottom, so a fresh graph flows down; but a graph
12
+ * whose stages step diagonally (cloud-plots' top level) should get its next
13
+ * stage on the same diagonal. The step is the median offset between wired
14
+ * siblings, so one odd wire does not set the direction.
15
+ */
16
+ /** Studio's own spacing (`autoLayout.ts`): rows 190 apart, columns 260. */
17
+ export const DEFAULT_STEP = Object.freeze({ x: 0, y: 190 });
18
+ const COLUMN_GAP = 260;
19
+ const ROW_GAP = 190;
20
+ /** Where Studio's layout starts an empty canvas. */
21
+ const ORIGIN = Object.freeze({ x: 40, y: 40 });
22
+ /** Two nodes closer than this overlap on screen. A node is about 200 x 100. */
23
+ const CLEARANCE = Object.freeze({ x: 210, y: 120 });
24
+ const MAX_LANES = 24;
25
+ /**
26
+ * The direction and distance from a node to the one it feeds, measured as the
27
+ * component-wise median over the given wires. Falls back to Studio's downward
28
+ * flow when nothing is wired yet, and ignores steps too short to be a layout
29
+ * decision (two nodes stacked by accident).
30
+ */
31
+ export function flowStep(wires) {
32
+ const deltas = wires
33
+ .map(({ from, to }) => ({ x: to.x - from.x, y: to.y - from.y }))
34
+ .filter((delta) => Math.hypot(delta.x, delta.y) >= 60);
35
+ if (!deltas.length)
36
+ return DEFAULT_STEP;
37
+ const step = { x: median(deltas.map((d) => d.x)), y: median(deltas.map((d) => d.y)) };
38
+ // The sideways part of the step is kept only when every wire agrees on it,
39
+ // as on a staircase of stages. A fan-in (two sources side by side feeding
40
+ // one node) has sideways parts of both signs, and averaging them would tilt
41
+ // the whole flow for no reason.
42
+ const down = Math.abs(step.y) >= Math.abs(step.x);
43
+ const across = deltas.map((d) => (down ? d.x : d.y));
44
+ const agree = across.every((value) => Math.sign(value) === Math.sign(across[0]) && Math.abs(value - median(across)) <= Math.abs(median(across)) * 0.25);
45
+ if (agree)
46
+ return step;
47
+ return down ? { x: 0, y: step.y } : { x: step.x, y: 0 };
48
+ }
49
+ /**
50
+ * One free position: a step past what feeds the node, or a step before what it
51
+ * feeds, or a staging lane beside the graph when it is wired to nothing yet. If that spot
52
+ * is taken it moves sideways across the flow, alternating sides, until it is
53
+ * clear — so a second consumer of the same node lands beside the first.
54
+ */
55
+ export function placeNode({ siblings, upstream, downstream, step }) {
56
+ const anchor = anchorFor(siblings, upstream, downstream, step);
57
+ const lane = laneStep(step);
58
+ for (let attempt = 0; attempt <= MAX_LANES * 2; attempt += 1) {
59
+ const offset = attempt === 0 ? 0 : (attempt % 2 === 1 ? 1 : -1) * Math.ceil(attempt / 2);
60
+ const candidate = round({ x: anchor.x + lane.x * offset, y: anchor.y + lane.y * offset });
61
+ if (!siblings.some((other) => overlaps(other, candidate)))
62
+ return candidate;
63
+ }
64
+ return round(anchor);
65
+ }
66
+ /** True when a node at `point` would sit on top of an existing one. */
67
+ export function isOccupied(siblings, point) {
68
+ return siblings.some((other) => overlaps(other, point));
69
+ }
70
+ function anchorFor(siblings, upstream, downstream, step) {
71
+ const unit = normalise(step);
72
+ if (upstream.length) {
73
+ // Beside the average of its inputs, but past the furthest of them, so a
74
+ // node fed from two rows never lands between them.
75
+ return add(alongFlow(mean(upstream), unit, Math.max(...upstream.map((p) => dot(p, unit)))), step);
76
+ }
77
+ if (downstream.length) {
78
+ return subtract(alongFlow(mean(downstream), unit, Math.min(...downstream.map((p) => dot(p, unit)))), step);
79
+ }
80
+ if (!siblings.length)
81
+ return ORIGIN;
82
+ // Wired to nothing yet: a staging lane beside the graph, at the start of the
83
+ // flow. Parking it in the flow would take the very slot the next wire wants;
84
+ // here it waits until `connect` moves it into place, and anything left in
85
+ // the lane reads as not wired yet.
86
+ const lane = laneStep(step);
87
+ const across = normalise(lane);
88
+ const furthest = Math.max(...siblings.map((p) => dot(p, across)));
89
+ const start = Math.min(...siblings.map((p) => dot(p, unit)));
90
+ return {
91
+ x: across.x * furthest + unit.x * start + lane.x,
92
+ y: across.y * furthest + unit.y * start + lane.y,
93
+ };
94
+ }
95
+ /** Across the flow: columns for a downward graph, rows for a sideways one. */
96
+ function laneStep(step) {
97
+ return Math.abs(step.y) >= Math.abs(step.x) ? { x: COLUMN_GAP, y: 0 } : { x: 0, y: ROW_GAP };
98
+ }
99
+ /** Move `point` along `unit` until its projection onto `unit` equals `target`. */
100
+ function alongFlow(point, unit, target) {
101
+ const shift = target - dot(point, unit);
102
+ return { x: point.x + unit.x * shift, y: point.y + unit.y * shift };
103
+ }
104
+ function overlaps(a, b) {
105
+ return Math.abs(a.x - b.x) < CLEARANCE.x && Math.abs(a.y - b.y) < CLEARANCE.y;
106
+ }
107
+ function median(values) {
108
+ const sorted = [...values].sort((a, b) => a - b);
109
+ const middle = Math.floor(sorted.length / 2);
110
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
111
+ }
112
+ function mean(points) {
113
+ return {
114
+ x: points.reduce((sum, p) => sum + p.x, 0) / points.length,
115
+ y: points.reduce((sum, p) => sum + p.y, 0) / points.length,
116
+ };
117
+ }
118
+ function normalise(p) {
119
+ const length = Math.hypot(p.x, p.y) || 1;
120
+ return { x: p.x / length, y: p.y / length };
121
+ }
122
+ const dot = (a, b) => a.x * b.x + a.y * b.y;
123
+ const add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
124
+ const subtract = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
125
+ const round = (p) => ({ x: Math.round(p.x), y: Math.round(p.y) });
126
+ //# sourceMappingURL=layout.js.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * The edit vocabulary as data.
3
+ *
4
+ * Every CLI editing command parses to one of these and every MCP editing tool
5
+ * takes one, so the two surfaces share names, fields and behaviour. The names
6
+ * are the graph's own (node, connect, set, subnet), not a second vocabulary.
7
+ */
8
+ import type { GraphEditor } from "./graphEditor.js";
9
+ import type { Point } from "./layout.js";
10
+ import type { Interpolation } from "./parameters.js";
11
+ export type GraphEditOperation = {
12
+ readonly op: "node.add";
13
+ readonly module: string;
14
+ readonly id?: string;
15
+ /** Subnet to create the node in. */
16
+ readonly in?: string;
17
+ readonly position?: Point;
18
+ readonly set?: Readonly<Record<string, unknown>>;
19
+ /** `set` values are command-line text, read by each parameter's type. */
20
+ readonly fromText?: boolean;
21
+ readonly comment?: string;
22
+ } | {
23
+ readonly op: "node.remove";
24
+ readonly id: string;
25
+ readonly recursive?: boolean;
26
+ } | {
27
+ readonly op: "connect";
28
+ readonly from: string;
29
+ readonly to: string;
30
+ readonly layout?: boolean;
31
+ } | {
32
+ readonly op: "disconnect";
33
+ readonly from?: string;
34
+ readonly to: string;
35
+ } | {
36
+ readonly op: "set";
37
+ /** `node.param` */
38
+ readonly target: string;
39
+ readonly value?: unknown;
40
+ readonly fromText?: boolean;
41
+ readonly expression?: string | null;
42
+ readonly keys?: readonly {
43
+ frame: number;
44
+ value: number;
45
+ }[];
46
+ readonly removeKeys?: readonly number[];
47
+ readonly interpolation?: Interpolation;
48
+ readonly unset?: boolean;
49
+ } | {
50
+ readonly op: "subnet.create";
51
+ readonly nodes: readonly string[];
52
+ readonly id?: string;
53
+ readonly label?: string;
54
+ } | {
55
+ readonly op: "subnet.move";
56
+ readonly nodes: readonly string[];
57
+ readonly to: string | null;
58
+ } | {
59
+ readonly op: "subnet.dissolve";
60
+ readonly id: string;
61
+ };
62
+ export declare const GRAPH_EDIT_OPERATIONS: readonly ["node.add", "node.remove", "connect", "disconnect", "set", "subnet.create", "subnet.move", "subnet.dissolve"];
63
+ /** Apply one operation and return what it changed, in JSON-ready form. */
64
+ export declare function applyOperation(editor: GraphEditor, operation: GraphEditOperation): Record<string, unknown>;
65
+ //# sourceMappingURL=operations.d.ts.map
@@ -0,0 +1,57 @@
1
+ /**
2
+ * The edit vocabulary as data.
3
+ *
4
+ * Every CLI editing command parses to one of these and every MCP editing tool
5
+ * takes one, so the two surfaces share names, fields and behaviour. The names
6
+ * are the graph's own (node, connect, set, subnet), not a second vocabulary.
7
+ */
8
+ export const GRAPH_EDIT_OPERATIONS = [
9
+ "node.add",
10
+ "node.remove",
11
+ "connect",
12
+ "disconnect",
13
+ "set",
14
+ "subnet.create",
15
+ "subnet.move",
16
+ "subnet.dissolve",
17
+ ];
18
+ /** Apply one operation and return what it changed, in JSON-ready form. */
19
+ export function applyOperation(editor, operation) {
20
+ switch (operation.op) {
21
+ case "node.add":
22
+ return editor.addNode({
23
+ module: operation.module,
24
+ ...(operation.id ? { id: operation.id } : {}),
25
+ ...(operation.in ? { parent: operation.in } : {}),
26
+ ...(operation.position ? { position: operation.position } : {}),
27
+ ...(operation.set ? { props: operation.set } : {}),
28
+ ...(operation.fromText ? { fromText: true } : {}),
29
+ ...(operation.comment ? { comment: operation.comment } : {}),
30
+ });
31
+ case "node.remove":
32
+ return editor.removeNode(operation.id, { recursive: operation.recursive === true });
33
+ case "connect":
34
+ return editor.connect(operation.from, operation.to, { layout: operation.layout !== false });
35
+ case "disconnect":
36
+ return operation.from ? editor.disconnect(operation.from, operation.to) : editor.disconnect(operation.to);
37
+ case "set": {
38
+ const { op: _op, target, ...change } = operation;
39
+ return editor.setParameter(target, change);
40
+ }
41
+ case "subnet.create":
42
+ return editor.createSubnet({
43
+ nodes: operation.nodes,
44
+ ...(operation.id ? { id: operation.id } : {}),
45
+ ...(operation.label ? { label: operation.label } : {}),
46
+ });
47
+ case "subnet.move":
48
+ return editor.moveNodes(operation.nodes, operation.to === "root" ? null : operation.to);
49
+ case "subnet.dissolve":
50
+ return editor.dissolveSubnet(operation.id);
51
+ default: {
52
+ const unknown = operation.op;
53
+ throw new Error(`Unknown edit operation ${JSON.stringify(unknown)}; expected one of ${GRAPH_EDIT_OPERATIONS.join(", ")}`);
54
+ }
55
+ }
56
+ }
57
+ //# sourceMappingURL=operations.js.map
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Setting one parameter: where its value lives in the document, what a typed
3
+ * value looks like, and how a value, an expression and a keyframe channel sit
4
+ * together on one prop.
5
+ *
6
+ * The stored shapes are the ones Studio writes and the runtime reads, nothing
7
+ * new: a plain value, or `{ value, expression?, channel? }` when the prop is
8
+ * bound (`Node.serializePropValue`, `contracts/animation.ts`). A data input's
9
+ * value goes in the node's `inputs`, as `{ name, defaultValue, dataType }`.
10
+ */
11
+ import type { NodeDefinition } from "@field/cascade/contracts";
12
+ import { type EditableNode } from "./document.js";
13
+ import type { Interpolation } from "../animation/channel.js";
14
+ export type { Interpolation };
15
+ /** What `cascade set` can change on one parameter. Every field is optional. */
16
+ export interface ParameterChange {
17
+ /**
18
+ * The new stored value. With `fromText`, a string as typed on a command line,
19
+ * read according to the parameter's declared type (`0.5`, `true`, `1,2`,
20
+ * `#ff8800`); otherwise a JSON value used as given.
21
+ */
22
+ readonly value?: unknown;
23
+ readonly fromText?: boolean;
24
+ /** An expression to bind, or `null` to remove the one there. */
25
+ readonly expression?: string | null;
26
+ /** Keyframes to set, replacing any key already at the same frame. */
27
+ readonly keys?: readonly {
28
+ frame: number;
29
+ value: number;
30
+ }[];
31
+ /** Frames whose keys are removed. */
32
+ readonly removeKeys?: readonly number[];
33
+ /** Interpolation for the keys being set. Studio's default is `smooth`. */
34
+ readonly interpolation?: Interpolation;
35
+ /** Forget the stored value and every binding: the parameter is back on its default. */
36
+ readonly unset?: boolean;
37
+ }
38
+ export type ParameterField = "props" | "inputs";
39
+ export interface ParameterSlot {
40
+ readonly field: ParameterField;
41
+ readonly type: string | undefined;
42
+ /** The declared default, `undefined` when there is none or the node is undeclared. */
43
+ readonly default: unknown;
44
+ readonly hasDefault: boolean;
45
+ /** Declared with a default expression, so storing a plain value is a decision even when it equals the default. */
46
+ readonly defaultExpression: boolean;
47
+ readonly options: readonly unknown[] | undefined;
48
+ }
49
+ /**
50
+ * Where a parameter is stored, from the node's definition. Props first, then
51
+ * data inputs. Unknown names are an error with the list of real ones, because
52
+ * a typo stored under a name nothing declares is dropped on load — the
53
+ * `runtime/unknown-props` failure, which renders on defaults and reports success.
54
+ */
55
+ export declare function parameterSlot(definition: NodeDefinition | undefined, name: string): ParameterSlot | {
56
+ error: string;
57
+ };
58
+ /**
59
+ * Apply a change to one parameter of `node`. Returns what was stored before
60
+ * and after (`undefined` = nothing stored, the default applies), so a caller
61
+ * can report the change and undo it.
62
+ */
63
+ export declare function applyParameterChange(node: EditableNode, name: string, slot: ParameterSlot, change: ParameterChange): {
64
+ before: unknown;
65
+ after: unknown;
66
+ };
67
+ /** The value as stored in the document, or `undefined` when nothing is. */
68
+ export declare function readStored(node: EditableNode, name: string, field: ParameterField): unknown;
69
+ /**
70
+ * A value of the declared type, or an error naming what was expected. Text
71
+ * from a command line is read by type, so `cascade set g text.content 300`
72
+ * stores the string "300" while `scale 300` stores a number.
73
+ */
74
+ export declare function coerceValue(raw: unknown, slot: ParameterSlot, fromText: boolean, where: string): unknown;
75
+ //# sourceMappingURL=parameters.d.ts.map
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Setting one parameter: where its value lives in the document, what a typed
3
+ * value looks like, and how a value, an expression and a keyframe channel sit
4
+ * together on one prop.
5
+ *
6
+ * The stored shapes are the ones Studio writes and the runtime reads, nothing
7
+ * new: a plain value, or `{ value, expression?, channel? }` when the prop is
8
+ * bound (`Node.serializePropValue`, `contracts/animation.ts`). A data input's
9
+ * value goes in the node's `inputs`, as `{ name, defaultValue, dataType }`.
10
+ */
11
+ import { jsonEqual, setOrderedKey, NODE_KEY_ORDER } from "./document.js";
12
+ /**
13
+ * Where a parameter is stored, from the node's definition. Props first, then
14
+ * data inputs. Unknown names are an error with the list of real ones, because
15
+ * a typo stored under a name nothing declares is dropped on load — the
16
+ * `runtime/unknown-props` failure, which renders on defaults and reports success.
17
+ */
18
+ export function parameterSlot(definition, name) {
19
+ if (!definition) {
20
+ return { field: "props", type: undefined, default: undefined, hasDefault: false, defaultExpression: false, options: undefined };
21
+ }
22
+ const prop = definition.props?.[name];
23
+ if (prop) {
24
+ return {
25
+ field: "props",
26
+ type: String(prop.type),
27
+ default: prop.default,
28
+ hasDefault: "default" in prop,
29
+ defaultExpression: typeof prop.expression === "string",
30
+ options: Array.isArray(prop.options) ? prop.options.map(optionValue) : undefined,
31
+ };
32
+ }
33
+ const input = definition.inputs?.[name];
34
+ if (input && input.kind === "data") {
35
+ return {
36
+ field: "inputs",
37
+ type: String(input.type),
38
+ default: input.default,
39
+ hasDefault: "default" in input,
40
+ defaultExpression: false,
41
+ options: Array.isArray(input.options) ? input.options.map(optionValue) : undefined,
42
+ };
43
+ }
44
+ const props = Object.keys(definition.props ?? {});
45
+ const inputs = Object.entries(definition.inputs ?? {})
46
+ .filter(([, port]) => port.kind === "data")
47
+ .map(([portName]) => portName);
48
+ return {
49
+ error: `has no parameter "${name}"` +
50
+ (props.length ? `; props: ${props.join(", ")}` : "") +
51
+ (inputs.length ? `; inputs: ${inputs.join(", ")}` : ""),
52
+ };
53
+ }
54
+ /**
55
+ * Apply a change to one parameter of `node`. Returns what was stored before
56
+ * and after (`undefined` = nothing stored, the default applies), so a caller
57
+ * can report the change and undo it.
58
+ */
59
+ export function applyParameterChange(node, name, slot, change) {
60
+ if (slot.field === "inputs" && (change.expression !== undefined || change.keys?.length || change.removeKeys?.length)) {
61
+ throw new Error(`${node.id}.${name} is an input; expressions and keyframes bind props only. Wire a node into it instead.`);
62
+ }
63
+ const before = readStored(node, name, slot.field);
64
+ if (change.unset) {
65
+ writeStored(node, name, slot.field, undefined, slot);
66
+ return { before, after: undefined };
67
+ }
68
+ let binding = toBinding(before, slot);
69
+ if (change.value !== undefined) {
70
+ binding = { ...binding, value: coerceValue(change.value, slot, change.fromText === true, `${node.id}.${name}`) };
71
+ }
72
+ if (change.expression !== undefined) {
73
+ binding = { ...binding, expression: change.expression || undefined };
74
+ }
75
+ if (change.keys?.length || change.removeKeys?.length) {
76
+ binding = { ...binding, keys: updateKeys(binding.keys, change) };
77
+ }
78
+ const after = fromBinding(binding, slot);
79
+ writeStored(node, name, slot.field, after, slot);
80
+ return { before, after: readStored(node, name, slot.field) };
81
+ }
82
+ /** The value as stored in the document, or `undefined` when nothing is. */
83
+ export function readStored(node, name, field) {
84
+ if (field === "props")
85
+ return node.props?.[name];
86
+ const inputs = node.inputs;
87
+ if (Array.isArray(inputs)) {
88
+ const entry = inputs.find((item) => item?.name === name);
89
+ return entry ? ("defaultValue" in entry ? entry.defaultValue : entry.value) : undefined;
90
+ }
91
+ return inputs && typeof inputs === "object" ? inputs[name] : undefined;
92
+ }
93
+ function isBoundValue(stored) {
94
+ return !!stored && typeof stored === "object" && !Array.isArray(stored) &&
95
+ "value" in stored && ("expression" in stored || "channel" in stored);
96
+ }
97
+ function toBinding(stored, slot) {
98
+ if (isBoundValue(stored)) {
99
+ return {
100
+ value: stored.value,
101
+ expression: stored.expression,
102
+ keys: Array.isArray(stored.channel?.keys) ? stored.channel.keys : [],
103
+ };
104
+ }
105
+ return { value: stored === undefined ? slot.default : stored, keys: [] };
106
+ }
107
+ /**
108
+ * The stored form of a binding, or `undefined` when there is nothing worth
109
+ * storing: a plain value equal to the declared default is not written, the way
110
+ * Studio writes only the parameters that differ. The exception is a prop
111
+ * declared with a default expression, where a stored plain value is what turns
112
+ * that expression off.
113
+ */
114
+ function fromBinding(binding, slot) {
115
+ if (binding.expression || binding.keys.length) {
116
+ return {
117
+ value: binding.value,
118
+ ...(binding.expression ? { expression: binding.expression } : {}),
119
+ ...(binding.keys.length ? { channel: { keys: binding.keys } } : {}),
120
+ };
121
+ }
122
+ if (binding.value === undefined)
123
+ return undefined;
124
+ if (slot.hasDefault && !slot.defaultExpression && jsonEqual(binding.value, slot.default))
125
+ return undefined;
126
+ return binding.value;
127
+ }
128
+ function updateKeys(keys, change) {
129
+ const byFrame = new Map(keys.map((key) => [key.frame, key]));
130
+ for (const frame of change.removeKeys ?? [])
131
+ byFrame.delete(frame);
132
+ for (const key of change.keys ?? []) {
133
+ if (!Number.isFinite(key.frame) || !Number.isFinite(key.value)) {
134
+ throw new Error(`A keyframe needs a numeric frame and value (got ${key.frame}=${key.value})`);
135
+ }
136
+ const interpolation = change.interpolation ?? byFrame.get(key.frame)?.interpolation;
137
+ byFrame.set(key.frame, interpolation && interpolation !== "smooth"
138
+ ? { frame: key.frame, value: key.value, interpolation }
139
+ : { frame: key.frame, value: key.value });
140
+ }
141
+ return [...byFrame.values()].sort((a, b) => a.frame - b.frame);
142
+ }
143
+ function writeStored(node, name, field, value, slot) {
144
+ if (field === "props") {
145
+ const props = node.props ?? {};
146
+ if (value === undefined)
147
+ delete props[name];
148
+ else
149
+ props[name] = value;
150
+ if (Object.keys(props).length)
151
+ setOrderedKey(node, "props", props, NODE_KEY_ORDER);
152
+ else
153
+ delete node.props;
154
+ return;
155
+ }
156
+ const inputs = node.inputs;
157
+ if (inputs && typeof inputs === "object" && !Array.isArray(inputs)) {
158
+ const record = inputs;
159
+ if (value === undefined)
160
+ delete record[name];
161
+ else
162
+ record[name] = value;
163
+ if (!Object.keys(record).length)
164
+ delete node.inputs;
165
+ return;
166
+ }
167
+ // Studio's array form, `{ name, defaultValue, dataType }`.
168
+ const list = Array.isArray(inputs) ? inputs : [];
169
+ const at = list.findIndex((item) => item?.name === name);
170
+ if (value === undefined) {
171
+ if (at !== -1)
172
+ list.splice(at, 1);
173
+ }
174
+ else if (at !== -1) {
175
+ const entry = list[at];
176
+ const key = "value" in entry && !("defaultValue" in entry) ? "value" : "defaultValue";
177
+ list[at] = { ...entry, [key]: value };
178
+ }
179
+ else {
180
+ list.push({ name, defaultValue: value, ...(slot.type ? { dataType: slot.type } : {}) });
181
+ }
182
+ if (list.length)
183
+ setOrderedKey(node, "inputs", list, NODE_KEY_ORDER);
184
+ else
185
+ delete node.inputs;
186
+ }
187
+ /**
188
+ * A value of the declared type, or an error naming what was expected. Text
189
+ * from a command line is read by type, so `cascade set g text.content 300`
190
+ * stores the string "300" while `scale 300` stores a number.
191
+ */
192
+ export function coerceValue(raw, slot, fromText, where) {
193
+ const type = slot.type;
194
+ let value = raw;
195
+ if (fromText && typeof raw === "string") {
196
+ value = type === "string" ? unquote(raw) : parseText(raw, type);
197
+ }
198
+ if (!type)
199
+ return value;
200
+ const fail = (expected) => {
201
+ throw new Error(`${where} expects ${expected} (got ${JSON.stringify(value)})`);
202
+ };
203
+ switch (type) {
204
+ case "float":
205
+ if (typeof value !== "number" || !Number.isFinite(value))
206
+ fail("a number");
207
+ break;
208
+ case "int":
209
+ if (typeof value !== "number" || !Number.isInteger(value))
210
+ fail("an integer");
211
+ break;
212
+ case "bool":
213
+ if (typeof value !== "boolean")
214
+ fail("true or false");
215
+ break;
216
+ case "string":
217
+ if (typeof value !== "string")
218
+ value = String(value);
219
+ break;
220
+ case "color":
221
+ value = colorValue(value) ?? fail("a colour: #rrggbb, #rrggbbaa or [r, g, b, a] in 0..1");
222
+ break;
223
+ default: {
224
+ const size = /^(?:vec|mat)(\d)i?$/.exec(type);
225
+ if (size) {
226
+ const length = type.startsWith("mat") ? Number(size[1]) ** 2 : Number(size[1]);
227
+ if (!Array.isArray(value) || value.length !== length || !value.every((n) => typeof n === "number" && Number.isFinite(n))) {
228
+ fail(`${length} numbers, e.g. ${Array.from({ length }, () => "0").join(",")}`);
229
+ }
230
+ if (type.endsWith("i") && !value.every(Number.isInteger))
231
+ fail(`${length} integers`);
232
+ }
233
+ }
234
+ }
235
+ if (slot.options && !slot.options.some((option) => jsonEqual(option, value))) {
236
+ fail(`one of ${slot.options.map((option) => JSON.stringify(option)).join(", ")}`);
237
+ }
238
+ return value;
239
+ }
240
+ function parseText(text, type) {
241
+ const trimmed = text.trim();
242
+ if (type && /^(?:vec|mat)\d/.test(type) && !trimmed.startsWith("[")) {
243
+ return trimmed.split(",").map((part) => Number(part.trim()));
244
+ }
245
+ if (type === "color" && trimmed.startsWith("#"))
246
+ return trimmed;
247
+ try {
248
+ return JSON.parse(trimmed);
249
+ }
250
+ catch {
251
+ return trimmed;
252
+ }
253
+ }
254
+ function unquote(text) {
255
+ if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
256
+ try {
257
+ return JSON.parse(text);
258
+ }
259
+ catch {
260
+ return text;
261
+ }
262
+ }
263
+ return text;
264
+ }
265
+ /** `[r, g, b, a]` in 0..1, Studio's stored colour. Hex is converted, rounded to four places. */
266
+ function colorValue(value) {
267
+ if (typeof value === "string") {
268
+ const hex = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(value.trim());
269
+ if (!hex)
270
+ return null;
271
+ const digits = hex[1] + (hex[2] ?? "ff");
272
+ return [0, 2, 4, 6].map((at) => Math.round((parseInt(digits.slice(at, at + 2), 16) / 255) * 10000) / 10000);
273
+ }
274
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4) && value.every((n) => typeof n === "number")) {
275
+ return value.length === 3 ? [...value, 1] : [...value];
276
+ }
277
+ return null;
278
+ }
279
+ function optionValue(option) {
280
+ return option && typeof option === "object" && !Array.isArray(option) && "value" in option
281
+ ? option.value
282
+ : option;
283
+ }
284
+ //# sourceMappingURL=parameters.js.map