@field/cascade 0.10.0 → 0.12.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 (229) hide show
  1. package/AGENTS.md +25 -5
  2. package/ARCHITECTURE.md +5 -4
  3. package/CHANGELOG.md +382 -0
  4. package/README.md +1 -2
  5. package/dist/assets/AgentPanel-1JugFjE4.css +1 -0
  6. package/dist/assets/AgentPanel-BDL5MddA.js +5 -0
  7. package/dist/assets/{CodeEditor-DSZBpzdy.js → CodeEditor-C-xYG-Wn.js} +120 -120
  8. package/dist/assets/CodePanel-CtXd50m6.js +2 -0
  9. package/dist/assets/CookInfoPanel-B3NMhKJF.css +1 -0
  10. package/dist/assets/CookInfoPanel-DG4_TRsX.js +2 -0
  11. package/dist/assets/CustomNodeDialog-CHfZUs-r.js +1 -0
  12. package/dist/assets/DefinitionNode-CaRTXQ_b.js +1 -0
  13. package/dist/assets/DefinitionPanel-C3CxZY9r.js +1 -0
  14. package/dist/assets/DefinitionPanel-VNLnAiQD.css +1 -0
  15. package/dist/assets/OpenGraphDialog-BLdr6k_q.js +1 -0
  16. package/dist/assets/ProjectSettings-ZnvmIAir.js +2 -0
  17. package/dist/assets/{SettingsDialog-p25h3tj0.js → SettingsDialog-CBX3Wa87.js} +1 -1
  18. package/dist/assets/TimelinePanel-CDeR0arY.css +1 -0
  19. package/dist/assets/TimelinePanel-CWgnFH1e.js +1 -0
  20. package/dist/assets/{VersionHistoryDialog-DzFLUotB.js → VersionHistoryDialog-BOSoW1R-.js} +1 -1
  21. package/dist/assets/assert-BO2_erPD.js +1 -0
  22. package/dist/assets/assert-hvykGCnP.js +1 -0
  23. package/dist/assets/builder-BmBKcDix.js +1 -0
  24. package/dist/assets/camera-DFF7YA3F.js +1 -0
  25. package/dist/assets/checks-DAvyO9Gf.js +1 -0
  26. package/dist/assets/contactSheet-N1NcaWxw.js +2 -0
  27. package/dist/assets/{core-BICrEt0y.js → core-DB-MkGjj.js} +3 -4
  28. package/dist/assets/coreTypes-DtnDATZ7.js +1 -0
  29. package/dist/assets/{cssMode-D7TXw57I.js → cssMode-DPVckSOc.js} +1 -1
  30. package/dist/assets/definitions-B227e7f_.js +1 -0
  31. package/dist/assets/definitions-D0WOUJd7.js +1 -0
  32. package/dist/assets/{editor.api-DvkCO3ko.js → editor.api-Dkutg_Z1.js} +1 -1
  33. package/dist/assets/expressions-X-Z-GrgW.js +5 -0
  34. package/dist/assets/file-DAL6IfjV.js +1 -0
  35. package/dist/assets/filters-D3k1LRMa.js +1 -0
  36. package/dist/assets/{freemarker2-BA2lxsF1.js → freemarker2-CLiLKk3B.js} +1 -1
  37. package/dist/assets/generators-BhDxRAFC.js +1 -0
  38. package/dist/assets/geo-BsMImJIJ.js +1 -0
  39. package/dist/assets/geo-C4YngDob.js +2 -0
  40. package/dist/assets/{handlebars-BcDiyPd8.js → handlebars-CcD3aMeL.js} +1 -1
  41. package/dist/assets/{html-C7O4TyhX.js → html-DvZWGfqb.js} +1 -1
  42. package/dist/assets/{htmlMode-WaDGF3Mj.js → htmlMode-DUD3VaoN.js} +1 -1
  43. package/dist/assets/image-0SfH-snh.js +1 -0
  44. package/dist/assets/image-Ck-RdGU4.js +2 -0
  45. package/dist/assets/index-BWnrmIB9.js +137 -0
  46. package/dist/assets/index-DIaxEONI.css +1 -0
  47. package/dist/assets/io-BGmt2g0D.js +1 -0
  48. package/dist/assets/{javascript-CpIEK7tz.js → javascript-Cap2ahgh.js} +1 -1
  49. package/dist/assets/{jsonMode-B2FltW0f.js → jsonMode-B0uLjp9C.js} +1 -1
  50. package/dist/assets/{liquid-CN6FFHZE.js → liquid-CnxwhfLn.js} +1 -1
  51. package/dist/assets/{lspLanguageFeatures-QhV9XtLp.js → lspLanguageFeatures-Dn9hlKX6.js} +1 -1
  52. package/dist/assets/math-GeULS-sk.js +1 -0
  53. package/dist/assets/{mdx-DE89pafV.js → mdx-BV3jbrVa.js} +1 -1
  54. package/dist/assets/pop-KMF_qloi.js +1 -0
  55. package/dist/assets/preload-helper-Czpn1I53.js +1 -0
  56. package/dist/assets/{python-ByTllTB9.js → python-CGyFk4XK.js} +1 -1
  57. package/dist/assets/{razor-USYlRRCE.js → razor-CQfzX3-N.js} +1 -1
  58. package/dist/assets/remap-5SjXQU-q.js +1 -0
  59. package/dist/assets/renderMarkdown-ork0Oir7.js +67 -0
  60. package/dist/assets/studioImages-CAyH-0Pl.js +1 -0
  61. package/dist/assets/text-ZpX7qNkx.js +2 -0
  62. package/dist/assets/time-P_OiBhcf.js +1 -0
  63. package/dist/assets/{tsMode-BXpN8iUz.js → tsMode-BOeXzO9H.js} +1 -1
  64. package/dist/assets/{typescript-DInBjGQQ.js → typescript-J1SnE_Tu.js} +1 -1
  65. package/dist/assets/{workers-C7Ye5B7D.js → workers-CYwwTiVF.js} +1 -1
  66. package/dist/assets/{xml-U0Uocqyo.js → xml-DnPRNzK9.js} +1 -1
  67. package/dist/assets/{yaml-CBw_PVYf.js → yaml-BmmYBNa9.js} +1 -1
  68. package/dist/cli/DefinitionNode-P7AA4TAU.js +15 -0
  69. package/dist/cli/Graph-OB6EWSGL.js +2975 -0
  70. package/dist/cli/assert-GROGQNCQ.js +25 -0
  71. package/dist/cli/checks-3WB44XBL.js +189 -0
  72. package/dist/cli/chunk-2JEU3MGB.js +1125 -0
  73. package/dist/cli/chunk-2KXB47FP.js +115 -0
  74. package/dist/cli/chunk-3RXL5GHB.js +816 -0
  75. package/dist/cli/chunk-53LGFPWX.js +138 -0
  76. package/dist/cli/chunk-5ORUQ6ES.js +267 -0
  77. package/dist/cli/chunk-73S4JZXK.js +336 -0
  78. package/dist/cli/chunk-7QE2ZNSN.js +121 -0
  79. package/dist/cli/chunk-BP7UKF36.js +376 -0
  80. package/dist/cli/chunk-BPODRH52.js +201 -0
  81. package/dist/cli/chunk-BZ63XQ3I.js +1301 -0
  82. package/dist/cli/chunk-CKS2FQWR.js +89 -0
  83. package/dist/cli/chunk-CO22XHNG.js +593 -0
  84. package/dist/cli/chunk-CTPLRSWF.js +2197 -0
  85. package/dist/cli/chunk-DEQSEFFH.js +153 -0
  86. package/dist/cli/chunk-EU4ZFZ7Y.js +122 -0
  87. package/dist/cli/chunk-EZRWJ4G2.js +104 -0
  88. package/dist/cli/chunk-FWMSGKEH.js +71 -0
  89. package/dist/cli/chunk-G23A2BZZ.js +705 -0
  90. package/dist/cli/chunk-GRIN2DDP.js +1465 -0
  91. package/dist/cli/chunk-HFHWYL55.js +139 -0
  92. package/dist/cli/chunk-HSNOBIVT.js +263 -0
  93. package/dist/cli/chunk-ICH7N5PN.js +26 -0
  94. package/dist/cli/chunk-J7VK7MNF.js +418 -0
  95. package/dist/cli/chunk-JVOCK5W4.js +83 -0
  96. package/dist/cli/chunk-KBGUO27Q.js +15 -0
  97. package/dist/cli/chunk-KH4MUBKF.js +1551 -0
  98. package/dist/cli/chunk-L6B6FUQP.js +330 -0
  99. package/dist/cli/chunk-LTXWBXOM.js +438 -0
  100. package/dist/cli/chunk-MI5JLQYA.js +220 -0
  101. package/dist/cli/chunk-MP653A5O.js +3766 -0
  102. package/dist/cli/chunk-NJFMDXHR.js +320 -0
  103. package/dist/cli/chunk-OM6BEME6.js +267 -0
  104. package/dist/cli/chunk-OWWBYLHC.js +136 -0
  105. package/dist/cli/chunk-QQPA2ZHH.js +273 -0
  106. package/dist/cli/chunk-RUU3YWXU.js +99 -0
  107. package/dist/cli/chunk-RXDIMXN5.js +98 -0
  108. package/dist/cli/chunk-ST6LROUG.js +267 -0
  109. package/dist/cli/chunk-TJW67QSI.js +225 -0
  110. package/dist/cli/chunk-WD62JIP6.js +118 -0
  111. package/dist/cli/chunk-WGKPJZDY.js +364 -0
  112. package/dist/cli/chunk-X4WKAOQP.js +634 -0
  113. package/dist/cli/chunk-X566FWBV.js +156 -0
  114. package/dist/cli/chunk-YH637PDL.js +317 -0
  115. package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
  116. package/dist/cli/chunk-ZSCUZE6I.js +112 -0
  117. package/dist/cli/cliCommands-JJN5Z3ZC.js +3470 -0
  118. package/dist/cli/commands-PHYFYA3F.js +1374 -0
  119. package/dist/cli/contactSheet-2PQVRUFW.js +11 -0
  120. package/dist/cli/definitions-BPOPW52L.js +32 -0
  121. package/dist/cli/doctorCommand-ORWQRNYP.js +106 -0
  122. package/dist/cli/edit-S5KEQ5RA.js +28 -0
  123. package/dist/cli/editorCommands-NFR32JOY.js +41 -0
  124. package/dist/cli/encode-ICPEPMH5.js +17 -0
  125. package/dist/cli/file-3HTQHPYQ.js +41 -0
  126. package/dist/cli/filters-6ODFWYK4.js +379 -0
  127. package/dist/cli/frames-LSAUBZ7P.js +16 -0
  128. package/dist/cli/generators-YU7GB4MD.js +210 -0
  129. package/dist/cli/geo-6ZPGKZRU.js +43 -0
  130. package/dist/cli/graphCommands-EOUOBOIJ.js +184 -0
  131. package/dist/cli/graphFile-YTGJB5F5.js +292 -0
  132. package/dist/cli/headlessCanvas-ENRHC7GS.js +16 -0
  133. package/dist/cli/helpCommand-PULH6ZYB.js +157 -0
  134. package/dist/cli/image-TXELZZK5.js +55 -0
  135. package/dist/cli/index.js +100 -30756
  136. package/dist/cli/math-RSYRQ3C6.js +16 -0
  137. package/dist/cli/mcp-JVZFQOEX.js +164 -0
  138. package/dist/cli/parity-X6DEH2DV.js +334 -0
  139. package/dist/cli/pop-B7SIIYKN.js +39 -0
  140. package/dist/cli/project-VYGIA7JX.js +12 -0
  141. package/dist/cli/projectDefinition-F3D7UIHJ.js +27 -0
  142. package/dist/cli/projectRuntime-2Q4JKEH3.js +55 -0
  143. package/dist/cli/registerStandardNodes-BXJCB6E4.js +744 -0
  144. package/dist/cli/render-PT4MWI6U.js +438 -0
  145. package/dist/cli/runCommand-HNPQVZMS.js +198 -0
  146. package/dist/cli/runner-5XPXVW7J.js +43 -0
  147. package/dist/cli/runtime/image.ts +302 -0
  148. package/dist/cli/runtime/io.ts +78 -0
  149. package/dist/cli/runtime/stage.ts +4 -1
  150. package/dist/cli/share-MHF6TNRK.js +166 -0
  151. package/dist/cli/stageCache-TY2BFZJR.js +15 -0
  152. package/dist/cli/stageRunner-KY2EJP7X.js +10 -0
  153. package/dist/cli/text-MB3EKR4H.js +126 -0
  154. package/dist/cli/verbs-PI2JH5FI.js +19 -0
  155. package/dist/cli/webBuild-WPPMGGW7.js +44 -0
  156. package/dist/contracts/capabilities.d.ts +18 -1
  157. package/dist/contracts/color.d.ts +8 -0
  158. package/dist/contracts/color.js +23 -0
  159. package/dist/contracts/definition.d.ts +10 -0
  160. package/dist/contracts/documentFormat.d.ts +18 -0
  161. package/dist/contracts/documentFormat.js +195 -0
  162. package/dist/contracts/editor.d.ts +17 -0
  163. package/dist/contracts/index.d.ts +1 -0
  164. package/dist/contracts/index.js +1 -0
  165. package/dist/contracts/schema.d.ts +1 -1
  166. package/dist/contracts/schema.js +1 -0
  167. package/dist/contracts/validate.js +11 -1
  168. package/dist/contracts/values.d.ts +24 -0
  169. package/dist/image.d.ts +156 -0
  170. package/dist/image.js +161 -0
  171. package/dist/index.html +8 -2
  172. package/dist/io.d.ts +29 -0
  173. package/dist/io.js +40 -0
  174. package/dist/player/app.js +3 -2
  175. package/dist/player/editor.js +35 -9
  176. package/dist/player/editorEngine.js +3 -2
  177. package/dist/runtime/builtins/assert/checks.d.ts +18 -0
  178. package/dist/runtime/builtins/assert/checks.js +207 -0
  179. package/dist/runtime/builtins/assert/definitions.d.ts +130 -0
  180. package/dist/runtime/builtins/assert/definitions.js +54 -0
  181. package/dist/runtime/builtins/assert/index.d.ts +152 -0
  182. package/dist/runtime/builtins/assert/index.js +36 -0
  183. package/dist/runtime/builtins/image/contactSheet.d.ts +64 -0
  184. package/dist/runtime/builtins/image/contactSheet.js +150 -0
  185. package/dist/runtime/builtins/image/definitions.d.ts +950 -0
  186. package/dist/runtime/builtins/image/definitions.js +343 -0
  187. package/dist/runtime/builtins/image/file.d.ts +8 -0
  188. package/dist/runtime/builtins/image/file.js +37 -0
  189. package/dist/runtime/builtins/image/filters.d.ts +8 -0
  190. package/dist/runtime/builtins/image/filters.js +394 -0
  191. package/dist/runtime/builtins/image/generators.d.ts +7 -0
  192. package/dist/runtime/builtins/image/generators.js +144 -0
  193. package/dist/runtime/builtins/image/index.d.ts +952 -0
  194. package/dist/runtime/builtins/image/index.js +44 -0
  195. package/dist/runtime/builtins/image/io.d.ts +100 -0
  196. package/dist/runtime/builtins/image/io.js +142 -0
  197. package/dist/runtime/builtins/image/planar.d.ts +55 -0
  198. package/dist/runtime/builtins/image/planar.js +274 -0
  199. package/dist/runtime/builtins/image/simplex.d.ts +22 -0
  200. package/dist/runtime/builtins/image/simplex.js +92 -0
  201. package/dist/runtime/builtins/image/text.d.ts +4 -0
  202. package/dist/runtime/builtins/image/text.js +141 -0
  203. package/dist/runtime/builtins/index.d.ts +3 -0
  204. package/dist/runtime/builtins/index.js +7 -0
  205. package/dist/runtime/gpu.d.ts +25 -0
  206. package/dist/runtime/gpu.js +40 -0
  207. package/dist/runtime/runtime.d.ts +2 -0
  208. package/dist/runtime/runtime.js +268 -44
  209. package/dist/runtime/types.d.ts +30 -0
  210. package/dist/stage.js +4 -1
  211. package/doc/CLI_REFERENCE.md +91 -2
  212. package/doc/EDITOR_EXPORT.md +37 -2
  213. package/doc/MCP.md +18 -1
  214. package/doc/NODE_REFERENCE.md +424 -10
  215. package/doc/PROJECT_AUTHORING.md +222 -7
  216. package/package.json +19 -4
  217. package/dist/assets/CodePanel-sY6sZa7B.js +0 -2
  218. package/dist/assets/CustomNodeDialog-BRKnqZhp.js +0 -1
  219. package/dist/assets/ExportDialog-C7dVGua4.js +0 -358
  220. package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
  221. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +0 -1
  222. package/dist/assets/ProjectSettings-Cw17nNe3.js +0 -2
  223. package/dist/assets/definitions-DUww2Rt-.js +0 -1
  224. package/dist/assets/geo-C2RHOVHC.js +0 -1
  225. package/dist/assets/image-DR4oeyJB.js +0 -2062
  226. package/dist/assets/index-7vae6YFC.css +0 -1
  227. package/dist/assets/index-DyvDC6pS.js +0 -219
  228. package/dist/assets/math-DaDEgWKt.js +0 -1
  229. package/dist/assets/pop-pLH7L0PW.js +0 -1
@@ -0,0 +1,1301 @@
1
+ import {
2
+ canConnectTypes
3
+ } from "./chunk-FWMSGKEH.js";
4
+
5
+ // packages/runtime/src/edit/document.ts
6
+ var SUBNET = "cascade.core.Subnet";
7
+ var INPUT = "cascade.core.Input";
8
+ var OUTPUT = "cascade.core.Output";
9
+ function readWire(raw) {
10
+ if (Array.isArray(raw) && raw.length === 2 && Array.isArray(raw[0]) && Array.isArray(raw[1])) {
11
+ const [from, to] = raw;
12
+ if (typeof from[0] !== "string" || typeof to[0] !== "string") return null;
13
+ return {
14
+ from: { node: from[0], index: Number(from[1] ?? 0), port: String(from[2] ?? "") },
15
+ to: { node: to[0], index: Number(to[1] ?? 0), port: String(to[2] ?? "") }
16
+ };
17
+ }
18
+ if (raw && typeof raw === "object" && "source" in raw && "target" in raw) {
19
+ const { source, target } = raw;
20
+ return {
21
+ from: { node: source.nodeId, port: source.outputName, index: 0 },
22
+ to: { node: target.nodeId, port: target.inputName, index: 0 }
23
+ };
24
+ }
25
+ return null;
26
+ }
27
+ function writeWire(wire, style) {
28
+ if (style === "object") {
29
+ return {
30
+ source: { nodeId: wire.from.node, outputName: wire.from.port },
31
+ target: { nodeId: wire.to.node, inputName: wire.to.port }
32
+ };
33
+ }
34
+ return [
35
+ [wire.from.node, wire.from.index, wire.from.port],
36
+ [wire.to.node, wire.to.index, wire.to.port]
37
+ ];
38
+ }
39
+ function wireStyleOf(document) {
40
+ const first = document.connections?.[0];
41
+ return first && !Array.isArray(first) ? "object" : "tuple";
42
+ }
43
+ function wireLabel(wire) {
44
+ return `${wire.from.node}.${wire.from.port} -> ${wire.to.node}.${wire.to.port}`;
45
+ }
46
+ function sameEndpoint(a, b) {
47
+ return a.node === b.node && a.port === b.port;
48
+ }
49
+ function moduleOf(node) {
50
+ return String(node.module ?? node.type ?? "");
51
+ }
52
+ function parentOf(node) {
53
+ return typeof node.parent === "string" && node.parent ? node.parent : null;
54
+ }
55
+ function positionOf(node) {
56
+ const position = node.position;
57
+ if (Array.isArray(position)) return { x: Number(position[0]) || 0, y: Number(position[1]) || 0 };
58
+ if (position && typeof position === "object") {
59
+ const { x, y } = position;
60
+ return { x: Number(x) || 0, y: Number(y) || 0 };
61
+ }
62
+ return { x: 0, y: 0 };
63
+ }
64
+ function setPosition(node, point) {
65
+ const position = node.position;
66
+ const value = position && typeof position === "object" && !Array.isArray(position) ? { x: point.x, y: point.y } : [point.x, point.y];
67
+ setOrderedKey(node, "position", value, NODE_KEY_ORDER);
68
+ }
69
+ var NODE_KEY_ORDER = [
70
+ "id",
71
+ "module",
72
+ "type",
73
+ "position",
74
+ "source",
75
+ "file",
76
+ "code",
77
+ "comment",
78
+ "color",
79
+ "parent",
80
+ "params",
81
+ "promotions",
82
+ "inputs",
83
+ "props",
84
+ "bypass",
85
+ "cook",
86
+ "state"
87
+ ];
88
+ function setOrderedKey(target, key, value, order) {
89
+ if (key in target) {
90
+ target[key] = value;
91
+ return;
92
+ }
93
+ const rank = order.indexOf(key);
94
+ const entries = Object.entries(target);
95
+ const at = rank === -1 ? -1 : entries.findIndex(([name]) => {
96
+ const other = order.indexOf(name);
97
+ return other > rank;
98
+ });
99
+ if (at === -1) {
100
+ target[key] = value;
101
+ return;
102
+ }
103
+ for (const [name] of entries) delete target[name];
104
+ entries.splice(at, 0, [key, value]);
105
+ for (const [name, entry] of entries) target[name] = entry;
106
+ }
107
+ function portsFromDefinition(definition) {
108
+ const list = (record) => Object.entries(record ?? {}).map(([name, port], index) => {
109
+ const value = port;
110
+ return {
111
+ name,
112
+ index,
113
+ kind: value.kind === "trigger" ? "trigger" : "data",
114
+ type: value.type ?? "any",
115
+ variadic: value.variadic === true
116
+ };
117
+ });
118
+ return {
119
+ inputs: list(definition.inputs),
120
+ outputs: list(definition.outputs)
121
+ };
122
+ }
123
+ function subnetPorts(children) {
124
+ const boundary = (module, indexProp, prefix) => {
125
+ const found = children.filter((child) => moduleOf(child) === module).map((child) => ({ index: integer(child.props?.[indexProp]), type: String(child.props?.dataType || "any") })).filter((entry) => entry.index >= 0);
126
+ const max = found.length ? Math.max(...found.map((entry) => entry.index)) : -1;
127
+ return Array.from({ length: max + 1 }, (_, index) => ({
128
+ name: `${prefix}_${index}`,
129
+ index,
130
+ kind: "data",
131
+ type: found.find((entry) => entry.index === index)?.type ?? "any",
132
+ variadic: false
133
+ }));
134
+ };
135
+ return {
136
+ inputs: boundary(INPUT, "inputIndex", "input"),
137
+ outputs: boundary(OUTPUT, "outputIndex", "output")
138
+ };
139
+ }
140
+ function typesConnect(from, to) {
141
+ if (!from || !to) return true;
142
+ if (from.kind !== to.kind) return false;
143
+ return from.kind === "trigger" || canConnectTypes(from.type, to.type);
144
+ }
145
+ function integer(value) {
146
+ const number = Number(value ?? 0);
147
+ return Number.isInteger(number) ? number : -1;
148
+ }
149
+ function jsonEqual(a, b) {
150
+ if (a === b) return true;
151
+ if (typeof a !== typeof b || a === null || b === null || typeof a !== "object") return false;
152
+ if (Array.isArray(a) !== Array.isArray(b)) return false;
153
+ if (Array.isArray(a)) {
154
+ const other = b;
155
+ return a.length === other.length && a.every((value, index) => jsonEqual(value, other[index]));
156
+ }
157
+ const left = a;
158
+ const right = b;
159
+ const keys = Object.keys(left);
160
+ return keys.length === Object.keys(right).length && keys.every((key) => key in right && jsonEqual(left[key], right[key]));
161
+ }
162
+
163
+ // packages/runtime/src/edit/layout.ts
164
+ var DEFAULT_STEP = Object.freeze({ x: 0, y: 190 });
165
+ var COLUMN_GAP = 260;
166
+ var ROW_GAP = 190;
167
+ var ORIGIN = Object.freeze({ x: 40, y: 40 });
168
+ var CLEARANCE = Object.freeze({ x: 210, y: 120 });
169
+ var MAX_LANES = 24;
170
+ function flowStep(wires) {
171
+ const deltas = wires.map(({ from, to }) => ({ x: to.x - from.x, y: to.y - from.y })).filter((delta) => Math.hypot(delta.x, delta.y) >= 60);
172
+ if (!deltas.length) return DEFAULT_STEP;
173
+ const step = { x: median(deltas.map((d) => d.x)), y: median(deltas.map((d) => d.y)) };
174
+ const down = Math.abs(step.y) >= Math.abs(step.x);
175
+ const across = deltas.map((d) => down ? d.x : d.y);
176
+ const agree = across.every((value) => Math.sign(value) === Math.sign(across[0]) && Math.abs(value - median(across)) <= Math.abs(median(across)) * 0.25);
177
+ if (agree) return step;
178
+ return down ? { x: 0, y: step.y } : { x: step.x, y: 0 };
179
+ }
180
+ function placeNode({ siblings, upstream, downstream, step }) {
181
+ const anchor = anchorFor(siblings, upstream, downstream, step);
182
+ const lane = laneStep(step);
183
+ for (let attempt = 0; attempt <= MAX_LANES * 2; attempt += 1) {
184
+ const offset = attempt === 0 ? 0 : (attempt % 2 === 1 ? 1 : -1) * Math.ceil(attempt / 2);
185
+ const candidate = round({ x: anchor.x + lane.x * offset, y: anchor.y + lane.y * offset });
186
+ if (!siblings.some((other) => overlaps(other, candidate))) return candidate;
187
+ }
188
+ return round(anchor);
189
+ }
190
+ function isOccupied(siblings, point) {
191
+ return siblings.some((other) => overlaps(other, point));
192
+ }
193
+ function anchorFor(siblings, upstream, downstream, step) {
194
+ const unit = normalise(step);
195
+ if (upstream.length) {
196
+ return add(alongFlow(mean(upstream), unit, Math.max(...upstream.map((p) => dot(p, unit)))), step);
197
+ }
198
+ if (downstream.length) {
199
+ return subtract(alongFlow(mean(downstream), unit, Math.min(...downstream.map((p) => dot(p, unit)))), step);
200
+ }
201
+ if (!siblings.length) return ORIGIN;
202
+ const lane = laneStep(step);
203
+ const across = normalise(lane);
204
+ const furthest = Math.max(...siblings.map((p) => dot(p, across)));
205
+ const start = Math.min(...siblings.map((p) => dot(p, unit)));
206
+ return {
207
+ x: across.x * furthest + unit.x * start + lane.x,
208
+ y: across.y * furthest + unit.y * start + lane.y
209
+ };
210
+ }
211
+ function laneStep(step) {
212
+ return Math.abs(step.y) >= Math.abs(step.x) ? { x: COLUMN_GAP, y: 0 } : { x: 0, y: ROW_GAP };
213
+ }
214
+ function alongFlow(point, unit, target) {
215
+ const shift = target - dot(point, unit);
216
+ return { x: point.x + unit.x * shift, y: point.y + unit.y * shift };
217
+ }
218
+ function overlaps(a, b) {
219
+ return Math.abs(a.x - b.x) < CLEARANCE.x && Math.abs(a.y - b.y) < CLEARANCE.y;
220
+ }
221
+ function median(values) {
222
+ const sorted = [...values].sort((a, b) => a - b);
223
+ const middle = Math.floor(sorted.length / 2);
224
+ return sorted.length % 2 ? sorted[middle] : (sorted[middle - 1] + sorted[middle]) / 2;
225
+ }
226
+ function mean(points) {
227
+ return {
228
+ x: points.reduce((sum, p) => sum + p.x, 0) / points.length,
229
+ y: points.reduce((sum, p) => sum + p.y, 0) / points.length
230
+ };
231
+ }
232
+ function normalise(p) {
233
+ const length = Math.hypot(p.x, p.y) || 1;
234
+ return { x: p.x / length, y: p.y / length };
235
+ }
236
+ var dot = (a, b) => a.x * b.x + a.y * b.y;
237
+ var add = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
238
+ var subtract = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
239
+ var round = (p) => ({ x: Math.round(p.x), y: Math.round(p.y) });
240
+
241
+ // packages/runtime/src/edit/parameters.ts
242
+ function parameterSlot(definition, name) {
243
+ if (!definition) {
244
+ return { field: "props", type: void 0, default: void 0, hasDefault: false, defaultExpression: false, options: void 0 };
245
+ }
246
+ const prop = definition.props?.[name];
247
+ if (prop) {
248
+ return {
249
+ field: "props",
250
+ type: String(prop.type),
251
+ default: prop.default,
252
+ hasDefault: "default" in prop,
253
+ defaultExpression: typeof prop.expression === "string",
254
+ options: Array.isArray(prop.options) ? prop.options.map(optionValue) : void 0
255
+ };
256
+ }
257
+ const input = definition.inputs?.[name];
258
+ if (input && input.kind === "data") {
259
+ return {
260
+ field: "inputs",
261
+ type: String(input.type),
262
+ default: input.default,
263
+ hasDefault: "default" in input,
264
+ defaultExpression: false,
265
+ options: Array.isArray(input.options) ? input.options.map(optionValue) : void 0
266
+ };
267
+ }
268
+ const props = Object.keys(definition.props ?? {});
269
+ const inputs = Object.entries(definition.inputs ?? {}).filter(([, port]) => port.kind === "data").map(([portName]) => portName);
270
+ return {
271
+ error: `has no parameter "${name}"` + (props.length ? `; props: ${props.join(", ")}` : "") + (inputs.length ? `; inputs: ${inputs.join(", ")}` : "")
272
+ };
273
+ }
274
+ function applyParameterChange(node, name, slot, change) {
275
+ if (slot.field === "inputs" && (change.expression !== void 0 || change.keys?.length || change.removeKeys?.length)) {
276
+ throw new Error(`${node.id}.${name} is an input; expressions and keyframes bind props only. Wire a node into it instead.`);
277
+ }
278
+ const before = readStored(node, name, slot.field);
279
+ if (change.unset) {
280
+ writeStored(node, name, slot.field, void 0, slot);
281
+ return { before, after: void 0 };
282
+ }
283
+ let binding = toBinding(before, slot);
284
+ if (change.value !== void 0) {
285
+ binding = { ...binding, value: coerceValue(change.value, slot, change.fromText === true, `${node.id}.${name}`) };
286
+ }
287
+ if (change.expression !== void 0) {
288
+ binding = { ...binding, expression: change.expression || void 0 };
289
+ }
290
+ if (change.keys?.length || change.removeKeys?.length) {
291
+ binding = { ...binding, keys: updateKeys(binding.keys, change) };
292
+ }
293
+ const after = fromBinding(binding, slot);
294
+ writeStored(node, name, slot.field, after, slot);
295
+ return { before, after: readStored(node, name, slot.field) };
296
+ }
297
+ function readStored(node, name, field) {
298
+ if (field === "props") return node.props?.[name];
299
+ const inputs = node.inputs;
300
+ if (Array.isArray(inputs)) {
301
+ const entry = inputs.find((item) => item?.name === name);
302
+ return entry ? "defaultValue" in entry ? entry.defaultValue : entry.value : void 0;
303
+ }
304
+ return inputs && typeof inputs === "object" ? inputs[name] : void 0;
305
+ }
306
+ function isBoundValue(stored) {
307
+ return !!stored && typeof stored === "object" && !Array.isArray(stored) && "value" in stored && ("expression" in stored || "channel" in stored);
308
+ }
309
+ function toBinding(stored, slot) {
310
+ if (isBoundValue(stored)) {
311
+ return {
312
+ value: stored.value,
313
+ expression: stored.expression,
314
+ keys: Array.isArray(stored.channel?.keys) ? stored.channel.keys : []
315
+ };
316
+ }
317
+ return { value: stored === void 0 ? slot.default : stored, keys: [] };
318
+ }
319
+ function fromBinding(binding, slot) {
320
+ if (binding.expression || binding.keys.length) {
321
+ return {
322
+ value: binding.value,
323
+ ...binding.expression ? { expression: binding.expression } : {},
324
+ ...binding.keys.length ? { channel: { keys: binding.keys } } : {}
325
+ };
326
+ }
327
+ if (binding.value === void 0) return void 0;
328
+ if (slot.hasDefault && !slot.defaultExpression && jsonEqual(binding.value, slot.default)) return void 0;
329
+ return binding.value;
330
+ }
331
+ function updateKeys(keys, change) {
332
+ const byFrame = new Map(keys.map((key) => [key.frame, key]));
333
+ for (const frame of change.removeKeys ?? []) byFrame.delete(frame);
334
+ for (const key of change.keys ?? []) {
335
+ if (!Number.isFinite(key.frame) || !Number.isFinite(key.value)) {
336
+ throw new Error(`A keyframe needs a numeric frame and value (got ${key.frame}=${key.value})`);
337
+ }
338
+ const interpolation = change.interpolation ?? byFrame.get(key.frame)?.interpolation;
339
+ byFrame.set(key.frame, interpolation && interpolation !== "smooth" ? { frame: key.frame, value: key.value, interpolation } : { frame: key.frame, value: key.value });
340
+ }
341
+ return [...byFrame.values()].sort((a, b) => a.frame - b.frame);
342
+ }
343
+ function writeStored(node, name, field, value, slot) {
344
+ if (field === "props") {
345
+ const props = node.props ?? {};
346
+ if (value === void 0) delete props[name];
347
+ else props[name] = value;
348
+ if (Object.keys(props).length) setOrderedKey(node, "props", props, NODE_KEY_ORDER);
349
+ else delete node.props;
350
+ return;
351
+ }
352
+ const inputs = node.inputs;
353
+ if (inputs && typeof inputs === "object" && !Array.isArray(inputs)) {
354
+ const record = inputs;
355
+ if (value === void 0) delete record[name];
356
+ else record[name] = value;
357
+ if (!Object.keys(record).length) delete node.inputs;
358
+ return;
359
+ }
360
+ const list = Array.isArray(inputs) ? inputs : [];
361
+ const at = list.findIndex((item) => item?.name === name);
362
+ if (value === void 0) {
363
+ if (at !== -1) list.splice(at, 1);
364
+ } else if (at !== -1) {
365
+ const entry = list[at];
366
+ const key = "value" in entry && !("defaultValue" in entry) ? "value" : "defaultValue";
367
+ list[at] = { ...entry, [key]: value };
368
+ } else {
369
+ list.push({ name, defaultValue: value, ...slot.type ? { dataType: slot.type } : {} });
370
+ }
371
+ if (list.length) setOrderedKey(node, "inputs", list, NODE_KEY_ORDER);
372
+ else delete node.inputs;
373
+ }
374
+ function coerceValue(raw, slot, fromText, where) {
375
+ const type = slot.type;
376
+ let value = raw;
377
+ if (fromText && typeof raw === "string") {
378
+ value = type === "string" ? unquote(raw) : parseText(raw, type);
379
+ }
380
+ if (!type) return value;
381
+ const fail = (expected) => {
382
+ throw new Error(`${where} expects ${expected} (got ${JSON.stringify(value)})`);
383
+ };
384
+ switch (type) {
385
+ case "float":
386
+ if (typeof value !== "number" || !Number.isFinite(value)) fail("a number");
387
+ break;
388
+ case "int":
389
+ if (typeof value !== "number" || !Number.isInteger(value)) fail("an integer");
390
+ break;
391
+ case "bool":
392
+ if (typeof value !== "boolean") fail("true or false");
393
+ break;
394
+ case "string":
395
+ if (typeof value !== "string") value = String(value);
396
+ break;
397
+ case "color":
398
+ value = colorValue(value) ?? fail("a colour: #rrggbb, #rrggbbaa or [r, g, b, a] in 0..1");
399
+ break;
400
+ default: {
401
+ const size = /^(?:vec|mat)(\d)i?$/.exec(type);
402
+ if (size) {
403
+ const length = type.startsWith("mat") ? Number(size[1]) ** 2 : Number(size[1]);
404
+ if (!Array.isArray(value) || value.length !== length || !value.every((n) => typeof n === "number" && Number.isFinite(n))) {
405
+ fail(`${length} numbers, e.g. ${Array.from({ length }, () => "0").join(",")}`);
406
+ }
407
+ if (type.endsWith("i") && !value.every(Number.isInteger)) fail(`${length} integers`);
408
+ }
409
+ }
410
+ }
411
+ if (slot.options && !slot.options.some((option) => jsonEqual(option, value))) {
412
+ fail(`one of ${slot.options.map((option) => JSON.stringify(option)).join(", ")}`);
413
+ }
414
+ return value;
415
+ }
416
+ function parseText(text, type) {
417
+ const trimmed = text.trim();
418
+ if (type && /^(?:vec|mat)\d/.test(type) && !trimmed.startsWith("[")) {
419
+ return trimmed.split(",").map((part) => Number(part.trim()));
420
+ }
421
+ if (type === "color" && trimmed.startsWith("#")) return trimmed;
422
+ try {
423
+ return JSON.parse(trimmed);
424
+ } catch {
425
+ return trimmed;
426
+ }
427
+ }
428
+ function unquote(text) {
429
+ if (text.length >= 2 && text.startsWith('"') && text.endsWith('"')) {
430
+ try {
431
+ return JSON.parse(text);
432
+ } catch {
433
+ return text;
434
+ }
435
+ }
436
+ return text;
437
+ }
438
+ function colorValue(value) {
439
+ if (typeof value === "string") {
440
+ const hex = /^#([0-9a-f]{6})([0-9a-f]{2})?$/i.exec(value.trim());
441
+ if (!hex) return null;
442
+ const digits = hex[1] + (hex[2] ?? "ff");
443
+ return [0, 2, 4, 6].map((at) => Math.round(parseInt(digits.slice(at, at + 2), 16) / 255 * 1e4) / 1e4);
444
+ }
445
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4) && value.every((n) => typeof n === "number")) {
446
+ return value.length === 3 ? [...value, 1] : [...value];
447
+ }
448
+ return null;
449
+ }
450
+ function optionValue(option) {
451
+ return option && typeof option === "object" && !Array.isArray(option) && "value" in option ? option.value : option;
452
+ }
453
+
454
+ // packages/runtime/src/edit/graphEditor.ts
455
+ var GraphEditError = class extends Error {
456
+ constructor(message) {
457
+ super(message);
458
+ this.name = "GraphEditError";
459
+ }
460
+ };
461
+ var GraphEditor = class {
462
+ constructor(document, options) {
463
+ this.document = document;
464
+ this.options = options;
465
+ this.warnings = [];
466
+ if (!Array.isArray(document.nodes)) throw new GraphEditError("Graph document must contain a nodes array");
467
+ }
468
+ // -------------------------------------------------------------------------
469
+ // Nodes
470
+ addNode(input) {
471
+ const parent = input.parent ? this.requireContainer(input.parent) : null;
472
+ const id = input.id ?? this.uniqueId(defaultIdFor(input.module));
473
+ assertValidId(id);
474
+ if (this.findNode(id)) throw new GraphEditError(`A node with id "${id}" already exists`);
475
+ const source = sourceFor(input.module);
476
+ const node = { id, module: input.module, position: [0, 0] };
477
+ if (source) node.source = source;
478
+ if (input.comment) node.comment = input.comment;
479
+ if (parent) node.parent = parent.id;
480
+ this.insertNode(node, parent?.id ?? null);
481
+ for (const [name, value] of Object.entries(input.props ?? {})) {
482
+ this.setParameterOn(node, name, { value, fromText: input.fromText === true });
483
+ }
484
+ setPosition(node, input.position ?? this.placeFor(node));
485
+ return { node: this.summary(node) };
486
+ }
487
+ /**
488
+ * Remove a node and every wire touching it. A subnet with children is
489
+ * refused unless `recursive`, because removing a container silently would
490
+ * remove everything in it.
491
+ */
492
+ removeNode(id, options = {}) {
493
+ const node = this.requireNode(id);
494
+ const descendants2 = this.descendantsOf(id);
495
+ if (descendants2.length && !options.recursive) {
496
+ throw new GraphEditError(`${id} contains ${descendants2.length} node${descendants2.length === 1 ? "" : "s"}; remove with --recursive, or dissolve it with "subnet dissolve"`);
497
+ }
498
+ const parent = parentOf(node);
499
+ const boundary = parent ? this.boundaryIndex(node) : null;
500
+ if (boundary && parent) {
501
+ return { removed: [id], wires: this.removeBoundary(this.requireNode(parent), boundary.kind, boundary.index) };
502
+ }
503
+ const removed = /* @__PURE__ */ new Set([id, ...descendants2.map((child) => child.id)]);
504
+ const wires = this.wires().filter(({ wire }) => wire && (removed.has(wire.from.node) || removed.has(wire.to.node))).map(({ index, wire }) => ({ index, label: wireLabel(wire) }));
505
+ this.removeWires(wires.map((wire) => wire.index));
506
+ this.document.nodes = this.document.nodes.filter((candidate) => !removed.has(candidate.id));
507
+ this.forgetInAnnotations(removed);
508
+ if (this.document.editor && JSON.stringify(this.document.editor).includes(JSON.stringify(id))) {
509
+ this.warnings.push(`The editor section still names ${id}; republish or unpublish its parameters`);
510
+ }
511
+ return { removed: [...removed], wires: wires.map((wire) => wire.label) };
512
+ }
513
+ // -------------------------------------------------------------------------
514
+ // Wires
515
+ /**
516
+ * Wire `from` (`node.output`) to `to` (`node.input`). A port name may be
517
+ * left off when the node has exactly one port on that side.
518
+ *
519
+ * The two nodes may be in different subnets: the wire is then routed
520
+ * through the subnets' `Output` and `Input` nodes, which are created (or
521
+ * reused) as needed, so no wire ever crosses a boundary directly.
522
+ *
523
+ * A node wired for the first time is moved next to its new neighbour, so a
524
+ * node created by `node add` and then connected ends up where a person would
525
+ * have put it. Pass `layout: false` to leave positions alone.
526
+ */
527
+ connect(from, to, options = {}) {
528
+ const source = this.resolveEndpoint(from, "output");
529
+ const target = this.resolveEndpoint(to, "input");
530
+ const targetNode = this.requireNode(target.endpoint.node);
531
+ if (!typesConnect(source.info, target.info)) {
532
+ throw new GraphEditError(`${from} (${source.info?.type}) cannot feed ${to} (${target.info?.type})`);
533
+ }
534
+ const routed = this.routeAcross(source.endpoint, parentOf(targetNode));
535
+ const sourceNode = this.requireNode(routed.node);
536
+ const wires = this.wires();
537
+ const wire = { from: routed, to: target.endpoint };
538
+ if (wires.some(({ wire: other }) => other && sameEndpoint(other.from, wire.from) && sameEndpoint(other.to, wire.to))) {
539
+ this.warnings.push(`${wireLabel(wire)} was already connected`);
540
+ return { wire: wireLabel(wire) };
541
+ }
542
+ const sourceWasFree = routed === source.endpoint && !this.isWired(routed.node);
543
+ const targetWasFree = !this.isWired(target.endpoint.node);
544
+ let replaced;
545
+ const existing = target.info?.variadic ? void 0 : wires.find(({ wire: other }) => other && sameEndpoint(other.to, wire.to));
546
+ if (existing?.wire) {
547
+ replaced = wireLabel(existing.wire);
548
+ this.document.connections[existing.index] = writeWire(wire, wireStyleOf(this.document));
549
+ this.warnings.push(`${target.endpoint.node}.${target.endpoint.port} was fed by ${existing.wire.from.node}.${existing.wire.from.port}; replaced`);
550
+ } else {
551
+ this.appendWire(wire);
552
+ }
553
+ let moved;
554
+ if (options.layout !== false) {
555
+ if (targetWasFree && !sourceWasFree) moved = this.relayout(targetNode);
556
+ else if (sourceWasFree && !targetWasFree) moved = this.relayout(sourceNode);
557
+ else if (sourceWasFree && targetWasFree) moved = this.relayout(targetNode);
558
+ }
559
+ return { wire: wireLabel(wire), ...replaced ? { replaced } : {}, ...moved ? { moved } : {} };
560
+ }
561
+ /**
562
+ * The endpoint on `level`'s canvas that carries `source`: the source itself
563
+ * when it is already there, otherwise a subnet port reached by passing out
564
+ * through `Output` nodes to the common ancestor and in through `Input` nodes
565
+ * to `level`. Existing boundaries carrying the same value are reused.
566
+ */
567
+ routeAcross(source, level) {
568
+ const start = parentOf(this.requireNode(source.node));
569
+ if (start === level) return source;
570
+ const down = [...this.ancestorsOf(level), ...level ? [level] : []];
571
+ const up = [...this.ancestorsOf(start), ...start ? [start] : []];
572
+ let common = 0;
573
+ while (common < down.length && common < up.length && down[common] === up[common]) common += 1;
574
+ let current = source;
575
+ const touched = /* @__PURE__ */ new Map();
576
+ for (const subnetId of up.slice(common).reverse()) {
577
+ const subnet = this.requireNode(subnetId);
578
+ const created = touched.get(subnetId) ?? [];
579
+ const { node: output, isNew } = this.outputFor(subnet, current, created);
580
+ if (isNew) this.appendWire({ from: current, to: { node: output.id, port: "input", index: 0 } });
581
+ touched.set(subnetId, created);
582
+ current = this.subnetPort(subnet, "output", output);
583
+ }
584
+ let lastInput;
585
+ for (const subnetId of down.slice(common)) {
586
+ const subnet = this.requireNode(subnetId);
587
+ const created = touched.get(subnetId) ?? [];
588
+ const existing = this.inputCarrying(subnet, current, /* @__PURE__ */ new Set());
589
+ lastInput = existing ?? this.createBoundary(subnet, "input", current, created);
590
+ if (!existing) this.appendWire({ from: current, to: this.subnetPort(subnet, "input", lastInput) });
591
+ touched.set(subnetId, created);
592
+ current = { node: lastInput.id, port: "output", index: 0 };
593
+ }
594
+ for (const [subnetId, created] of touched) {
595
+ this.nameBoundaries(this.requireNode(subnetId), created);
596
+ for (const node of created) setPosition(node, this.placeFor(node));
597
+ }
598
+ if (lastInput) current = { node: lastInput.id, port: "output", index: 0 };
599
+ return current;
600
+ }
601
+ /**
602
+ * Remove wires. `disconnect a.out b.in` removes that wire; `disconnect b.in`
603
+ * removes whatever feeds that input; `disconnect a.out` removes everything
604
+ * that output feeds.
605
+ */
606
+ disconnect(first, second) {
607
+ let matches;
608
+ if (second !== void 0) {
609
+ const source = this.resolveEndpoint(first, "output").endpoint;
610
+ const target = this.resolveEndpoint(second, "input").endpoint;
611
+ matches = (wire) => sameEndpoint(wire.from, source) && sameEndpoint(wire.to, target);
612
+ } else {
613
+ const { node, port } = splitEndpoint(first);
614
+ this.requireNode(node);
615
+ matches = (wire) => wire.to.node === node && (!port || wire.to.port === port) || wire.from.node === node && (!port || wire.from.port === port) && !!port;
616
+ }
617
+ const found = this.wires().filter(({ wire }) => wire && matches(wire));
618
+ if (!found.length) throw new GraphEditError(`No wire matches ${second === void 0 ? first : `${first} -> ${second}`}`);
619
+ this.removeWires(found.map(({ index }) => index));
620
+ return { removed: found.map(({ wire }) => wireLabel(wire)) };
621
+ }
622
+ // -------------------------------------------------------------------------
623
+ // Parameters
624
+ /** Change `node.param`: its value, expression or keyframes. */
625
+ setParameter(target, change) {
626
+ const { node: id, port: name } = splitEndpoint(target);
627
+ if (!name) throw new GraphEditError(`Name a parameter: ${target}.<param>`);
628
+ const node = this.requireNode(id);
629
+ const result = this.setParameterOn(node, name, change);
630
+ if (result.field === "inputs" && this.wires().some(({ wire }) => wire && wire.to.node === id && wire.to.port === name)) {
631
+ this.warnings.push(`${id}.${name} is wired; the stored value is used only when the wire is removed`);
632
+ }
633
+ return { node: id, param: name, field: result.field, before: result.before ?? null, after: result.after ?? null };
634
+ }
635
+ // -------------------------------------------------------------------------
636
+ // Subnets
637
+ /**
638
+ * Group sibling nodes into a new subnet. Wires that now cross its boundary
639
+ * are routed through new `Input` and `Output` nodes, named after the port
640
+ * they carry, so the subnet's ports read as what flows through them.
641
+ */
642
+ createSubnet(input) {
643
+ const members = this.requireSiblings(input.nodes);
644
+ const parent = parentOf(members[0]);
645
+ const id = input.id ?? this.uniqueId(input.label ? slug(input.label) : "subnet");
646
+ assertValidId(id);
647
+ if (this.findNode(id)) throw new GraphEditError(`A node with id "${id}" already exists`);
648
+ const step = this.stepIn(parent);
649
+ const first = members.reduce((best, node) => along(positionOf(node), step) < along(positionOf(best), step) ? node : best);
650
+ const subnet = { id, module: SUBNET, position: [0, 0], source: "stdlib" };
651
+ if (input.label) subnet.comment = input.label;
652
+ if (parent) subnet.parent = parent;
653
+ setPosition(subnet, positionOf(first));
654
+ const at = Math.min(...members.map((node) => this.document.nodes.indexOf(node)));
655
+ this.document.nodes.splice(at, 0, subnet);
656
+ const created = this.moveIn(new Set(members.map((node) => node.id)), subnet);
657
+ return {
658
+ subnet: this.summary(subnet),
659
+ inputs: created.filter((node) => moduleOf(node) === INPUT).map((node) => node.id),
660
+ outputs: created.filter((node) => moduleOf(node) === OUTPUT).map((node) => node.id)
661
+ };
662
+ }
663
+ /** The inverse of `createSubnet`: lift the children out and remove the subnet. */
664
+ dissolveSubnet(id) {
665
+ const subnet = this.requireNode(id);
666
+ if (moduleOf(subnet) !== SUBNET) throw new GraphEditError(`${id} is not a subnet`);
667
+ const members = this.childrenOf(id).filter((node) => !isBoundary(node));
668
+ if (members.length) this.moveOut(new Set(members.map((node) => node.id)), subnet);
669
+ for (const leftover of this.childrenOf(id)) {
670
+ this.warnings.push(`${leftover.id} carried nothing after the dissolve and was removed`);
671
+ }
672
+ this.removeNode(id, { recursive: true });
673
+ return { lifted: members.map((node) => node.id) };
674
+ }
675
+ /**
676
+ * Move sibling nodes to another subnet (`null` for the top level), one level
677
+ * at a time — out to the common ancestor, then in — rewiring through each
678
+ * boundary on the way.
679
+ */
680
+ moveNodes(ids, to) {
681
+ const members = this.requireSiblings(ids);
682
+ const target = to ? this.requireNode(to) : null;
683
+ if (target && moduleOf(target) !== SUBNET) throw new GraphEditError(`${to} is not a subnet`);
684
+ const memberIds = new Set(members.map((node) => node.id));
685
+ if (target && (memberIds.has(target.id) || this.ancestorsOf(target.id).some((id) => memberIds.has(id)))) {
686
+ throw new GraphEditError(`Cannot move ${target.id} into itself`);
687
+ }
688
+ const destination = target?.id ?? null;
689
+ const targetPath = [...this.ancestorsOf(destination), ...destination ? [destination] : []];
690
+ let current = parentOf(members[0]);
691
+ while (current !== null && !targetPath.includes(current)) {
692
+ const container = this.requireNode(current);
693
+ this.moveOut(memberIds, container);
694
+ current = parentOf(container);
695
+ }
696
+ while (current !== destination) {
697
+ const next = targetPath[targetPath.indexOf(current) + 1];
698
+ this.moveIn(memberIds, this.requireNode(next));
699
+ current = next;
700
+ }
701
+ const placed = [];
702
+ for (const node of members) {
703
+ const others = this.childrenOf(destination).filter((other) => !memberIds.has(other.id)).map(positionOf);
704
+ const moved = isOccupied(others, positionOf(node)) ? this.relayout(node) : void 0;
705
+ if (moved) placed.push(moved);
706
+ }
707
+ return { moved: [...memberIds], to: destination, placed };
708
+ }
709
+ // -------------------------------------------------------------------------
710
+ // Internals: boundary rewiring
711
+ /**
712
+ * Move sibling nodes from the subnet's parent into the subnet.
713
+ *
714
+ * A wire from a member to the subnet's own input collapses (the member is
715
+ * now inside, so it feeds that input's consumers directly), and a wire from
716
+ * outside to a member is routed through an `Input` — reusing one that
717
+ * already carries the same source. The mirror holds for outputs. Wires are
718
+ * rewritten in place where possible, which is what lets `moveOut` restore
719
+ * the original file exactly.
720
+ */
721
+ moveIn(members, subnet) {
722
+ for (const id of members) setOrderedKey(this.requireNode(id), "parent", subnet.id, NODE_KEY_ORDER);
723
+ const created = [];
724
+ const removed = /* @__PURE__ */ new Set();
725
+ const total = this.connections().length;
726
+ for (let index = 0; index < total; index += 1) {
727
+ const wire = readWire(this.connections()[index]);
728
+ if (!wire || removed.has(index)) continue;
729
+ const fromMember = members.has(wire.from.node);
730
+ const toMember = members.has(wire.to.node);
731
+ if (fromMember === toMember) continue;
732
+ if (fromMember && wire.to.node === subnet.id) {
733
+ const k = portNumber(wire.to.port);
734
+ const boundary = this.boundaryNode(subnet, "input", k);
735
+ if (boundary) {
736
+ for (const consumer of this.wires().filter(({ wire: w }) => w?.from.node === boundary.id)) {
737
+ this.replaceWire(consumer.index, { from: wire.from, to: consumer.wire.to });
738
+ }
739
+ }
740
+ removed.add(index);
741
+ this.removeBoundaryNode(subnet, "input", k, removed);
742
+ } else if (fromMember) {
743
+ const { node: output, isNew } = this.outputFor(subnet, wire.from, created);
744
+ this.replaceWire(index, { from: this.subnetPort(subnet, "output", output), to: wire.to });
745
+ if (isNew) this.appendWire({ from: wire.from, to: { node: output.id, port: "input", index: 0 } });
746
+ } else if (wire.from.node === subnet.id) {
747
+ const k = portNumber(wire.from.port);
748
+ const boundary = this.boundaryNode(subnet, "output", k);
749
+ const feed = boundary && this.wires().find(({ wire: w }) => w?.to.node === boundary.id);
750
+ if (feed?.wire) this.replaceWire(index, { from: feed.wire.from, to: wire.to });
751
+ else removed.add(index);
752
+ const stillUsed = this.wires().some(({ index: i, wire: w }) => !removed.has(i) && w?.from.node === subnet.id && w.from.port === wire.from.port && i !== index);
753
+ if (!stillUsed && boundary) {
754
+ if (feed) removed.add(feed.index);
755
+ this.removeBoundaryNode(subnet, "output", k, removed);
756
+ }
757
+ } else {
758
+ const existing = this.inputCarrying(subnet, wire.from, removed);
759
+ if (existing) {
760
+ removed.add(index);
761
+ this.appendWire({ from: { node: existing.id, port: "output", index: 0 }, to: wire.to });
762
+ } else {
763
+ const input = this.createBoundary(subnet, "input", wire.from, created);
764
+ this.replaceWire(index, { from: wire.from, to: this.subnetPort(subnet, "input", input) });
765
+ this.appendWire({ from: { node: input.id, port: "output", index: 0 }, to: wire.to });
766
+ }
767
+ }
768
+ }
769
+ this.removeWires([...removed]);
770
+ this.nameBoundaries(subnet, created);
771
+ for (const node of created) setPosition(node, this.placeFor(node));
772
+ return created;
773
+ }
774
+ /** Move nodes out of a subnet into its parent. The mirror of `moveIn`. */
775
+ moveOut(members, subnet) {
776
+ const created = [];
777
+ const removed = /* @__PURE__ */ new Set();
778
+ const total = this.connections().length;
779
+ for (let index = 0; index < total; index += 1) {
780
+ const wire = readWire(this.connections()[index]);
781
+ if (!wire || removed.has(index)) continue;
782
+ const fromMember = members.has(wire.from.node);
783
+ const toMember = members.has(wire.to.node);
784
+ if (fromMember === toMember) continue;
785
+ const other = this.findNode(fromMember ? wire.to.node : wire.from.node);
786
+ if (fromMember && other && moduleOf(other) === OUTPUT && parentOf(other) === subnet.id) {
787
+ const k = integer(other.props?.outputIndex);
788
+ for (const consumer of this.wires().filter(({ wire: w }) => w?.from.node === subnet.id && w.from.port === `output_${k}`)) {
789
+ this.replaceWire(consumer.index, { from: wire.from, to: consumer.wire.to });
790
+ }
791
+ removed.add(index);
792
+ this.removeBoundaryNode(subnet, "output", k, removed);
793
+ } else if (fromMember) {
794
+ const existing = this.inputCarrying(subnet, wire.from, removed);
795
+ const input = existing ?? this.createBoundary(subnet, "input", wire.from, created);
796
+ this.replaceWire(index, { from: { node: input.id, port: "output", index: 0 }, to: wire.to });
797
+ if (!existing) this.appendWire({ from: wire.from, to: this.subnetPort(subnet, "input", input) });
798
+ } else if (other && moduleOf(other) === INPUT && parentOf(other) === subnet.id) {
799
+ const k = integer(other.props?.inputIndex);
800
+ const feed = this.wires().find(({ index: i, wire: w }) => !removed.has(i) && w?.to.node === subnet.id && w.to.port === `input_${k}`);
801
+ const otherConsumers = this.wires().some(({ index: i, wire: w }) => i !== index && !removed.has(i) && w?.from.node === other.id);
802
+ if (!feed?.wire) {
803
+ removed.add(index);
804
+ this.warnings.push(`${wire.to.node}.${wire.to.port} was fed by an unwired subnet input; it is now unwired`);
805
+ } else if (!otherConsumers) {
806
+ this.replaceWire(feed.index, { from: feed.wire.from, to: wire.to });
807
+ removed.add(index);
808
+ } else {
809
+ this.replaceWire(index, { from: feed.wire.from, to: wire.to });
810
+ }
811
+ if (!otherConsumers) this.removeBoundaryNode(subnet, "input", k, removed);
812
+ } else {
813
+ const { node: output, isNew } = this.outputFor(subnet, wire.from, created);
814
+ this.replaceWire(index, { from: this.subnetPort(subnet, "output", output), to: wire.to });
815
+ if (isNew) this.appendWire({ from: wire.from, to: { node: output.id, port: "input", index: 0 } });
816
+ }
817
+ }
818
+ this.removeWires([...removed]);
819
+ const parent = parentOf(subnet);
820
+ for (const id of members) {
821
+ const node = this.requireNode(id);
822
+ if (parent) setOrderedKey(node, "parent", parent, NODE_KEY_ORDER);
823
+ else delete node.parent;
824
+ }
825
+ this.nameBoundaries(subnet, created);
826
+ for (const node of created) setPosition(node, this.placeFor(node));
827
+ return created;
828
+ }
829
+ /** An `Output` in the subnet already fed by `source`, or a new one. */
830
+ outputFor(subnet, source, created) {
831
+ const existing = this.childrenOf(subnet.id).find((node) => moduleOf(node) === OUTPUT && this.wires().some(({ wire }) => wire?.to.node === node.id && sameEndpoint(wire.from, source)));
832
+ return existing ? { node: existing, isNew: false } : { node: this.createBoundary(subnet, "output", source, created), isNew: true };
833
+ }
834
+ /** An `Input` in the subnet whose outside feed is `source`. */
835
+ inputCarrying(subnet, source, removed) {
836
+ const feed = this.wires().find(({ index, wire }) => !removed.has(index) && wire?.to.node === subnet.id && sameEndpoint(wire.from, source));
837
+ return feed ? this.boundaryNode(subnet, "input", portNumber(feed.wire.to.port)) : void 0;
838
+ }
839
+ createBoundary(subnet, kind, carries, created) {
840
+ const ports = subnetPorts(this.childrenOf(subnet.id));
841
+ const index = (kind === "input" ? ports.inputs : ports.outputs).length;
842
+ const label = this.labelOf(carries);
843
+ const type = this.portInfo(carries.node, carries.port, "output")?.type ?? "any";
844
+ const id = this.uniqueId(`${subnet.id}-${kind === "input" ? "in" : "out"}-${slug(label)}`);
845
+ const node = {
846
+ id,
847
+ module: kind === "input" ? INPUT : OUTPUT,
848
+ position: [0, 0],
849
+ source: "stdlib",
850
+ comment: label,
851
+ parent: subnet.id,
852
+ props: kind === "input" ? { inputIndex: index, inputName: "", dataType: type } : { outputIndex: index, outputName: "", dataType: type }
853
+ };
854
+ this.insertNode(node, subnet.id);
855
+ created.push(node);
856
+ return node;
857
+ }
858
+ /**
859
+ * Name new boundary nodes after what they carry, readably. A port name is
860
+ * the natural label (`moment`, `canvas`), but not when it says nothing
861
+ * (`value`, `output`) or when two ports of the subnet would share it — three
862
+ * `mask` outputs read as one. Then the producing node's id is the label:
863
+ * `masks-out-cloud-mask` beside `masks-out-primary-lobe`.
864
+ */
865
+ nameBoundaries(subnet, created) {
866
+ const carried = (node) => {
867
+ const wire = moduleOf(node) === INPUT ? this.wires().find(({ wire: w }) => w?.to.node === subnet.id && w.to.port === `input_${integer(node.props?.inputIndex)}`)?.wire : this.wires().find(({ wire: w }) => w?.to.node === node.id)?.wire;
868
+ return wire?.from;
869
+ };
870
+ const relabel = created.flatMap((node) => {
871
+ const label = String(node.comment ?? "");
872
+ const sameSide = this.childrenOf(subnet.id).filter((other) => moduleOf(other) === moduleOf(node));
873
+ const shared = sameSide.filter((other) => other.comment === label).length > 1;
874
+ const source = carried(node);
875
+ return source && (shared || GENERIC_PORTS.has(label)) ? [{ node, label: source.node }] : [];
876
+ });
877
+ for (const { node, label } of relabel) this.relabelBoundary(subnet, node, label);
878
+ }
879
+ relabelBoundary(subnet, node, label) {
880
+ const kind = moduleOf(node) === INPUT ? "in" : "out";
881
+ const id = this.uniqueId(`${subnet.id}-${kind}-${slug(label)}`);
882
+ const previous = node.id;
883
+ node.id = id;
884
+ node.comment = label;
885
+ for (const { index, wire } of this.wires()) {
886
+ if (!wire || wire.from.node !== previous && wire.to.node !== previous) continue;
887
+ const rename = (end) => end.node === previous ? { ...end, node: id } : end;
888
+ this.replaceWire(index, { from: rename(wire.from), to: rename(wire.to) });
889
+ }
890
+ }
891
+ /** What a value is called: its port name, or for a subnet's own port, the name of the boundary behind it. */
892
+ labelOf(endpoint) {
893
+ const node = this.findNode(endpoint.node);
894
+ const port = /^(input|output)_(\d+)$/.exec(endpoint.port);
895
+ if (node && moduleOf(node) === SUBNET && port) {
896
+ const boundary = this.boundaryNode(node, port[1], Number(port[2]));
897
+ const name = boundary?.comment || boundary?.props?.[port[1] === "input" ? "inputName" : "outputName"];
898
+ if (typeof name === "string" && name) return name;
899
+ }
900
+ return endpoint.port || endpoint.node;
901
+ }
902
+ /**
903
+ * Remove boundary node k and keep the subnet's ports contiguous: the ports
904
+ * above it shift down one, and so do the wires that use them. Wires to the
905
+ * removed port itself are marked in `removed` for the caller to drop.
906
+ */
907
+ removeBoundaryNode(subnet, kind, k, removed) {
908
+ const node = this.boundaryNode(subnet, kind, k);
909
+ if (!node) return;
910
+ const indexProp = kind === "input" ? "inputIndex" : "outputIndex";
911
+ const port = `${kind}_${k}`;
912
+ for (const { index, wire } of this.wires()) {
913
+ if (!wire) continue;
914
+ const touchesPort = kind === "input" ? wire.to.node === subnet.id && wire.to.port === port : wire.from.node === subnet.id && wire.from.port === port;
915
+ if (touchesPort || wire.from.node === node.id || wire.to.node === node.id) removed.add(index);
916
+ }
917
+ this.document.nodes.splice(this.document.nodes.indexOf(node), 1);
918
+ for (const sibling of this.childrenOf(subnet.id)) {
919
+ if (moduleOf(sibling) !== (kind === "input" ? INPUT : OUTPUT)) continue;
920
+ const index = integer(sibling.props?.[indexProp]);
921
+ if (index > k) sibling.props[indexProp] = index - 1;
922
+ }
923
+ for (const { index, wire } of this.wires()) {
924
+ if (!wire || removed.has(index)) continue;
925
+ const end = kind === "input" ? wire.to : wire.from;
926
+ if (end.node !== subnet.id) continue;
927
+ const n = portNumber(end.port);
928
+ if (n <= k) continue;
929
+ const shifted = { node: subnet.id, port: `${kind}_${n - 1}`, index: n - 1 };
930
+ this.replaceWire(index, kind === "input" ? { from: wire.from, to: shifted } : { from: shifted, to: wire.to });
931
+ }
932
+ }
933
+ /** `rm` of a boundary node: the subnet loses that port. */
934
+ removeBoundary(subnet, kind, k) {
935
+ const removed = /* @__PURE__ */ new Set();
936
+ const before = this.wires();
937
+ this.removeBoundaryNode(subnet, kind, k, removed);
938
+ const labels = before.filter(({ index }) => removed.has(index)).map(({ wire }) => wireLabel(wire));
939
+ this.removeWires([...removed]);
940
+ this.warnings.push(`${subnet.id} lost its ${kind}_${k} port; higher ports were renumbered`);
941
+ return labels;
942
+ }
943
+ boundaryIndex(node) {
944
+ if (moduleOf(node) === INPUT) return { kind: "input", index: integer(node.props?.inputIndex) };
945
+ if (moduleOf(node) === OUTPUT) return { kind: "output", index: integer(node.props?.outputIndex) };
946
+ return null;
947
+ }
948
+ boundaryNode(subnet, kind, k) {
949
+ const module = kind === "input" ? INPUT : OUTPUT;
950
+ const indexProp = kind === "input" ? "inputIndex" : "outputIndex";
951
+ return this.childrenOf(subnet.id).find((node) => moduleOf(node) === module && integer(node.props?.[indexProp]) === k);
952
+ }
953
+ subnetPort(subnet, kind, boundary) {
954
+ const k = integer(boundary.props?.[kind === "input" ? "inputIndex" : "outputIndex"]);
955
+ return { node: subnet.id, port: `${kind}_${k}`, index: k };
956
+ }
957
+ // -------------------------------------------------------------------------
958
+ // Internals: lookup
959
+ findNode(id) {
960
+ return this.document.nodes.find((node) => node.id === id);
961
+ }
962
+ requireNode(id) {
963
+ const node = this.findNode(id);
964
+ if (node) return node;
965
+ const near = this.document.nodes.map((candidate) => candidate.id).filter((candidate) => candidate.includes(id) || id.includes(candidate)).slice(0, 5);
966
+ throw new GraphEditError(`No node "${id}"${near.length ? `; did you mean ${near.join(", ")}?` : ""}`);
967
+ }
968
+ childrenOf(parent) {
969
+ return this.document.nodes.filter((node) => parentOf(node) === parent);
970
+ }
971
+ descendantsOf(id) {
972
+ const result = [];
973
+ const queue = [id];
974
+ while (queue.length) {
975
+ const current = queue.shift();
976
+ for (const child of this.childrenOf(current)) {
977
+ result.push(child);
978
+ queue.push(child.id);
979
+ }
980
+ }
981
+ return result;
982
+ }
983
+ ancestorsOf(id) {
984
+ const chain = [];
985
+ let current = id ? parentOf(this.requireNode(id)) : null;
986
+ while (current) {
987
+ chain.unshift(current);
988
+ current = parentOf(this.requireNode(current));
989
+ }
990
+ return chain;
991
+ }
992
+ requireContainer(id) {
993
+ const node = this.requireNode(id);
994
+ const definition = this.options.definitionOf(moduleOf(node));
995
+ if (moduleOf(node) !== SUBNET && !definition?.container) throw new GraphEditError(`${id} is not a subnet`);
996
+ return node;
997
+ }
998
+ requireSiblings(ids) {
999
+ if (!ids.length) throw new GraphEditError("Name at least one node");
1000
+ const nodes = [...new Set(ids)].map((id) => this.requireNode(id));
1001
+ const parent = parentOf(nodes[0]);
1002
+ const stray = nodes.find((node) => parentOf(node) !== parent);
1003
+ if (stray) throw new GraphEditError(`${stray.id} is not in the same subnet as ${nodes[0].id}; move nodes from one subnet at a time`);
1004
+ const boundary = nodes.find(isBoundary);
1005
+ if (boundary && parent) throw new GraphEditError(`${boundary.id} is ${parent}'s ${moduleOf(boundary) === INPUT ? "input" : "output"}; it stays with its subnet`);
1006
+ return nodes;
1007
+ }
1008
+ /** The ports a node has, or `null` when they are not statically known. */
1009
+ portsOf(node) {
1010
+ if (moduleOf(node) === SUBNET) return subnetPorts(this.childrenOf(node.id));
1011
+ const definition = this.options.definitionOf(moduleOf(node));
1012
+ return definition ? portsFromDefinition(definition) : null;
1013
+ }
1014
+ portInfo(nodeId, port, side) {
1015
+ const node = this.findNode(nodeId);
1016
+ const ports = node && this.portsOf(node);
1017
+ return ports ? (side === "input" ? ports.inputs : ports.outputs).find((info) => info.name === port) : void 0;
1018
+ }
1019
+ /** `node.port` to a checked endpoint. A missing port name is filled in when only one would fit. */
1020
+ resolveEndpoint(text, side) {
1021
+ const { node: id, port } = splitEndpoint(text);
1022
+ const node = this.requireNode(id);
1023
+ const ports = this.portsOf(node);
1024
+ if (!ports) {
1025
+ if (!port) throw new GraphEditError(`${id} declares no static ports; name one: ${id}.<${side}>`);
1026
+ this.warnings.push(`${id} (${moduleOf(node)}) has no static definition; ${port} is not checked`);
1027
+ return { endpoint: { node: id, port, index: this.indexFromExistingWires(id, port, side) } };
1028
+ }
1029
+ const list = side === "input" ? ports.inputs : ports.outputs;
1030
+ const info = port ? list.find((candidate) => candidate.name === port) : list.length === 1 ? list[0] : void 0;
1031
+ if (!info) {
1032
+ const names = list.map((candidate) => candidate.name).join(", ") || "none";
1033
+ throw new GraphEditError(port ? `${id} has no ${side} "${port}"; ${side}s: ${names}` : `${id} has several ${side}s; name one: ${names}`);
1034
+ }
1035
+ return { endpoint: { node: id, port: info.name, index: info.index }, info };
1036
+ }
1037
+ indexFromExistingWires(node, port, side) {
1038
+ for (const { wire } of this.wires()) {
1039
+ const end = wire && (side === "input" ? wire.to : wire.from);
1040
+ if (end && end.node === node && end.port === port) return end.index;
1041
+ }
1042
+ return 0;
1043
+ }
1044
+ isWired(id) {
1045
+ return this.wires().some(({ wire }) => wire && (wire.from.node === id || wire.to.node === id));
1046
+ }
1047
+ setParameterOn(node, name, change) {
1048
+ const slot = parameterSlot(this.options.definitionOf(moduleOf(node)), name);
1049
+ if ("error" in slot) throw new GraphEditError(`${node.id} ${slot.error}`);
1050
+ if (!this.options.definitionOf(moduleOf(node))) {
1051
+ this.warnings.push(`${node.id} (${moduleOf(node)}) has no static definition; ${name} is stored as a prop unchecked`);
1052
+ }
1053
+ try {
1054
+ return { field: slot.field, ...applyParameterChange(node, name, slot, change) };
1055
+ } catch (error) {
1056
+ throw new GraphEditError(error instanceof Error ? error.message : String(error));
1057
+ }
1058
+ }
1059
+ // -------------------------------------------------------------------------
1060
+ // Internals: the connections array
1061
+ connections() {
1062
+ return this.document.connections ??= [];
1063
+ }
1064
+ /** Every connection with its array index; unreadable ones have `wire: null` and are left alone. */
1065
+ wires() {
1066
+ return this.connections().map((raw, index) => ({ index, wire: readWire(raw) }));
1067
+ }
1068
+ appendWire(wire) {
1069
+ const style = wireStyleOf(this.document);
1070
+ if (!this.document.connections) setOrderedKey(this.document, "connections", [], ["version", "metadata", "annotations", "nodes", "connections", "editor"]);
1071
+ this.connections().push(writeWire(wire, style));
1072
+ }
1073
+ replaceWire(index, wire) {
1074
+ this.connections()[index] = writeWire(wire, wireStyleOf(this.document));
1075
+ }
1076
+ removeWires(indices) {
1077
+ const drop = new Set(indices);
1078
+ if (!drop.size) return;
1079
+ this.document.connections = this.connections().filter((_, index) => !drop.has(index));
1080
+ if (!this.document.connections.length) delete this.document.connections;
1081
+ }
1082
+ // -------------------------------------------------------------------------
1083
+ // Internals: placement and housekeeping
1084
+ /** Insert after the last node of the same subnet, so a subnet's children stay together in the file. */
1085
+ insertNode(node, parent) {
1086
+ if (parent === null) {
1087
+ this.document.nodes.push(node);
1088
+ return;
1089
+ }
1090
+ const family = /* @__PURE__ */ new Set([parent, ...this.descendantsOf(parent).map((child) => child.id)]);
1091
+ let at = -1;
1092
+ this.document.nodes.forEach((candidate, index) => {
1093
+ if (family.has(candidate.id)) at = index;
1094
+ });
1095
+ this.document.nodes.splice(at + 1, 0, node);
1096
+ }
1097
+ placeFor(node) {
1098
+ const parent = parentOf(node);
1099
+ const siblings = this.childrenOf(parent).filter((other) => other.id !== node.id);
1100
+ const upstream = [];
1101
+ const downstream = [];
1102
+ for (const { wire } of this.wires()) {
1103
+ if (!wire) continue;
1104
+ if (wire.to.node === node.id && wire.from.node !== node.id) {
1105
+ const source = this.findNode(wire.from.node);
1106
+ if (source && parentOf(source) === parent) upstream.push(positionOf(source));
1107
+ }
1108
+ if (wire.from.node === node.id && wire.to.node !== node.id) {
1109
+ const target = this.findNode(wire.to.node);
1110
+ if (target && parentOf(target) === parent) downstream.push(positionOf(target));
1111
+ }
1112
+ }
1113
+ return placeNode({ siblings: siblings.map(positionOf), upstream, downstream, step: this.stepIn(parent, node.id) });
1114
+ }
1115
+ relayout(node) {
1116
+ const before = positionOf(node);
1117
+ const after = this.placeFor(node);
1118
+ if (before.x === after.x && before.y === after.y) return void 0;
1119
+ setPosition(node, after);
1120
+ return this.summary(node);
1121
+ }
1122
+ /** The flow of one canvas: the median offset along its wires. */
1123
+ stepIn(parent, exclude) {
1124
+ const wires = [];
1125
+ for (const { wire } of this.wires()) {
1126
+ if (!wire || wire.from.node === exclude || wire.to.node === exclude) continue;
1127
+ const source = this.findNode(wire.from.node);
1128
+ const target = this.findNode(wire.to.node);
1129
+ if (!source || !target || parentOf(source) !== parent || parentOf(target) !== parent) continue;
1130
+ wires.push({ from: positionOf(source), to: positionOf(target) });
1131
+ }
1132
+ return flowStep(wires);
1133
+ }
1134
+ forgetInAnnotations(removed) {
1135
+ const annotations = this.document.annotations;
1136
+ if (!Array.isArray(annotations)) return;
1137
+ this.document.annotations = annotations.filter((annotation) => !removed.has(String(annotation.parent ?? "")));
1138
+ for (const annotation of this.document.annotations) {
1139
+ if (Array.isArray(annotation.containedElements)) {
1140
+ annotation.containedElements = annotation.containedElements.filter((id) => !removed.has(String(id)));
1141
+ }
1142
+ }
1143
+ if (!this.document.annotations.length && annotations.length) delete this.document.annotations;
1144
+ }
1145
+ uniqueId(base) {
1146
+ if (!this.findNode(base)) return base;
1147
+ for (let n = 2; ; n += 1) if (!this.findNode(`${base}-${n}`)) return `${base}-${n}`;
1148
+ }
1149
+ summary(node) {
1150
+ return { id: node.id, module: moduleOf(node), parent: parentOf(node), position: positionOf(node) };
1151
+ }
1152
+ };
1153
+ function splitEndpoint(text) {
1154
+ const at = text.lastIndexOf(".");
1155
+ return at === -1 ? { node: text, port: "" } : { node: text.slice(0, at), port: text.slice(at + 1) };
1156
+ }
1157
+ function defaultIdFor(module) {
1158
+ return slug(module.slice(module.lastIndexOf(".") + 1)) || "node";
1159
+ }
1160
+ function slug(text) {
1161
+ return text.replace(/([a-z0-9])([A-Z])/g, "$1-$2").replace(/[^A-Za-z0-9]+/g, "-").replace(/^-+|-+$/g, "").toLowerCase();
1162
+ }
1163
+ function assertValidId(id) {
1164
+ if (!/^[A-Za-z_][A-Za-z0-9_-]*$/.test(id)) {
1165
+ throw new GraphEditError(`"${id}" is not a usable node id: letters, digits, - and _, starting with a letter`);
1166
+ }
1167
+ }
1168
+ function sourceFor(module) {
1169
+ if (module.startsWith("cascade.")) return "stdlib";
1170
+ if (module.startsWith("project.")) return "project";
1171
+ return void 0;
1172
+ }
1173
+ var GENERIC_PORTS = /* @__PURE__ */ new Set(["value", "output", "out", "input", "in", "result", "data"]);
1174
+ function isBoundary(node) {
1175
+ return moduleOf(node) === INPUT || moduleOf(node) === OUTPUT;
1176
+ }
1177
+ function portNumber(port) {
1178
+ const match = /_(\d+)$/.exec(port);
1179
+ return match ? Number(match[1]) : -1;
1180
+ }
1181
+ function along(point, step) {
1182
+ return point.x * step.x + point.y * step.y;
1183
+ }
1184
+
1185
+ // packages/runtime/src/edit/operations.ts
1186
+ var GRAPH_EDIT_OPERATIONS = [
1187
+ "node.add",
1188
+ "node.remove",
1189
+ "connect",
1190
+ "disconnect",
1191
+ "set",
1192
+ "subnet.create",
1193
+ "subnet.move",
1194
+ "subnet.dissolve"
1195
+ ];
1196
+ function applyOperation(editor, operation) {
1197
+ switch (operation.op) {
1198
+ case "node.add":
1199
+ return editor.addNode({
1200
+ module: operation.module,
1201
+ ...operation.id ? { id: operation.id } : {},
1202
+ ...operation.in ? { parent: operation.in } : {},
1203
+ ...operation.position ? { position: operation.position } : {},
1204
+ ...operation.set ? { props: operation.set } : {},
1205
+ ...operation.fromText ? { fromText: true } : {},
1206
+ ...operation.comment ? { comment: operation.comment } : {}
1207
+ });
1208
+ case "node.remove":
1209
+ return editor.removeNode(operation.id, { recursive: operation.recursive === true });
1210
+ case "connect":
1211
+ return editor.connect(operation.from, operation.to, { layout: operation.layout !== false });
1212
+ case "disconnect":
1213
+ return operation.from ? editor.disconnect(operation.from, operation.to) : editor.disconnect(operation.to);
1214
+ case "set": {
1215
+ const { op: _op, target, ...change } = operation;
1216
+ return editor.setParameter(target, change);
1217
+ }
1218
+ case "subnet.create":
1219
+ return editor.createSubnet({
1220
+ nodes: operation.nodes,
1221
+ ...operation.id ? { id: operation.id } : {},
1222
+ ...operation.label ? { label: operation.label } : {}
1223
+ });
1224
+ case "subnet.move":
1225
+ return editor.moveNodes(operation.nodes, operation.to === "root" ? null : operation.to);
1226
+ case "subnet.dissolve":
1227
+ return editor.dissolveSubnet(operation.id);
1228
+ default: {
1229
+ const unknown = operation.op;
1230
+ throw new Error(`Unknown edit operation ${JSON.stringify(unknown)}; expected one of ${GRAPH_EDIT_OPERATIONS.join(", ")}`);
1231
+ }
1232
+ }
1233
+ }
1234
+
1235
+ // packages/runtime/src/edit/read.ts
1236
+ function readGraph(document, options = {}) {
1237
+ const scope = options.in ?? null;
1238
+ const inScope = new Set(scope === null ? document.nodes.map((node) => node.id) : descendants(document, scope));
1239
+ const nodes = document.nodes.filter((node) => inScope.has(node.id)).map((node) => {
1240
+ const children = document.nodes.filter((child) => parentOf(child) === node.id);
1241
+ const labelled = (prefix, indexProp, nameProp) => (port) => {
1242
+ const boundary = children.find((child) => Number(child.props?.[indexProp]) === port.index && moduleOf(child) === `cascade.core.${prefix}`);
1243
+ const label = boundary?.comment || boundary?.props?.[nameProp];
1244
+ return label ? `${port.name} (${label})` : port.name;
1245
+ };
1246
+ const ports = moduleOf(node) === SUBNET ? subnetPorts(children) : null;
1247
+ return {
1248
+ id: node.id,
1249
+ module: moduleOf(node),
1250
+ parent: parentOf(node),
1251
+ position: positionOf(node),
1252
+ ...typeof node.comment === "string" && node.comment ? { comment: node.comment } : {},
1253
+ ...node.props && Object.keys(node.props).length ? { props: node.props } : {},
1254
+ ...node.inputs !== void 0 ? { inputs: node.inputs } : {},
1255
+ ...ports ? {
1256
+ ports: {
1257
+ inputs: ports.inputs.map(labelled("Input", "inputIndex", "inputName")),
1258
+ outputs: ports.outputs.map(labelled("Output", "outputIndex", "outputName"))
1259
+ }
1260
+ } : {}
1261
+ };
1262
+ });
1263
+ const wires = (document.connections ?? []).map(readWire).filter((wire) => wire && inScope.has(wire.from.node) && inScope.has(wire.to.node)).map((wire) => wireLabel(wire));
1264
+ const metadata = document.metadata;
1265
+ return {
1266
+ name: typeof metadata?.name === "string" ? metadata.name : null,
1267
+ nodes,
1268
+ wires,
1269
+ top: nodes.filter((node) => node.parent === scope).map((node) => node.id)
1270
+ };
1271
+ }
1272
+ function descendants(document, id) {
1273
+ if (!document.nodes.some((node) => node.id === id)) throw new Error(`No node "${id}"`);
1274
+ const result = [];
1275
+ const queue = [id];
1276
+ while (queue.length) {
1277
+ const current = queue.shift();
1278
+ for (const child of document.nodes) {
1279
+ if (parentOf(child) === current) {
1280
+ result.push(child.id);
1281
+ queue.push(child.id);
1282
+ }
1283
+ }
1284
+ }
1285
+ return result;
1286
+ }
1287
+
1288
+ export {
1289
+ readWire,
1290
+ wireLabel,
1291
+ flowStep,
1292
+ placeNode,
1293
+ GraphEditError,
1294
+ GraphEditor,
1295
+ splitEndpoint,
1296
+ defaultIdFor,
1297
+ GRAPH_EDIT_OPERATIONS,
1298
+ applyOperation,
1299
+ readGraph
1300
+ };
1301
+ //# sourceMappingURL=chunk-BZ63XQ3I.js.map