@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,2197 @@
1
+ import {
2
+ expressionEngine
3
+ } from "./chunk-RUU3YWXU.js";
4
+ import {
5
+ resolvePropBinding
6
+ } from "./chunk-HFHWYL55.js";
7
+ import {
8
+ bundleFieldReferences,
9
+ deleteKey,
10
+ deserializeChannel,
11
+ isEmptyChannel,
12
+ serializeChannel,
13
+ setKey
14
+ } from "./chunk-CO22XHNG.js";
15
+ import {
16
+ CORE_TYPES,
17
+ canConnectTypes,
18
+ normalizeCascadeType
19
+ } from "./chunk-FWMSGKEH.js";
20
+
21
+ // src/utils/nodeTypeUtils.ts
22
+ var nodeClassRegistries = /* @__PURE__ */ new Map();
23
+ var nodeMetadataRegistry = /* @__PURE__ */ new Map();
24
+ function registerNodeMetadata(metadata) {
25
+ nodeMetadataRegistry.set(metadata.type, metadata);
26
+ }
27
+ function getNodeMetadata(type) {
28
+ if (nodeMetadataRegistry.has(type)) {
29
+ return nodeMetadataRegistry.get(type) || null;
30
+ }
31
+ const shortType = packagePathToType(type);
32
+ if (nodeMetadataRegistry.has(shortType)) {
33
+ return nodeMetadataRegistry.get(shortType) || null;
34
+ }
35
+ return null;
36
+ }
37
+ function getNodeDisplayName(type) {
38
+ const metadata = getNodeMetadata(type);
39
+ if (metadata?.name) {
40
+ return metadata.name;
41
+ }
42
+ return packagePathToType(type);
43
+ }
44
+ function registerNodeClasses(libraryId, classes) {
45
+ const existing = nodeClassRegistries.get(libraryId);
46
+ nodeClassRegistries.set(libraryId, existing ? { ...existing, ...classes } : classes);
47
+ }
48
+ function typeToPackagePath(type) {
49
+ if (type.includes(".")) {
50
+ return type;
51
+ }
52
+ for (const [libraryId, classes] of nodeClassRegistries) {
53
+ if (type in classes) {
54
+ return `cascade.${libraryId}.${type}`;
55
+ }
56
+ }
57
+ return type;
58
+ }
59
+ function packagePathToType(packagePath) {
60
+ if (!packagePath.includes(".")) {
61
+ return packagePath;
62
+ }
63
+ const parts = packagePath.split(".");
64
+ return parts[parts.length - 1];
65
+ }
66
+ function isStandardLibraryNode(type) {
67
+ return type.includes(".") && type.startsWith("cascade.");
68
+ }
69
+ function getLibraryIdFromType(type) {
70
+ if (!type.includes(".")) {
71
+ return null;
72
+ }
73
+ const parts = type.split(".");
74
+ if (parts.length >= 2 && parts[0] === "cascade") {
75
+ return parts[1];
76
+ }
77
+ return null;
78
+ }
79
+ function getNodeClass(type) {
80
+ const libraryId = getLibraryIdFromType(type);
81
+ const nodeType = packagePathToType(type);
82
+ if (!libraryId) {
83
+ return null;
84
+ }
85
+ const registry = nodeClassRegistries.get(libraryId);
86
+ if (registry && registry[nodeType]) {
87
+ return registry[nodeType];
88
+ }
89
+ return null;
90
+ }
91
+
92
+ // src/utils/colorUtils.ts
93
+ function normalizeColor(input) {
94
+ if (typeof input === "object" && !Array.isArray(input) && "r" in input && "g" in input && "b" in input) {
95
+ const obj = input;
96
+ const isNormalized = obj.r <= 1 && obj.g <= 1 && obj.b <= 1;
97
+ return {
98
+ r: isNormalized ? obj.r : obj.r / 255,
99
+ g: isNormalized ? obj.g : obj.g / 255,
100
+ b: isNormalized ? obj.b : obj.b / 255,
101
+ a: obj.a !== void 0 ? isNormalized ? obj.a : obj.a / 255 : 1
102
+ };
103
+ }
104
+ if (Array.isArray(input)) {
105
+ const isNormalized = input.every((v) => v <= 1);
106
+ return {
107
+ r: isNormalized ? input[0] : input[0] / 255,
108
+ g: isNormalized ? input[1] : input[1] / 255,
109
+ b: isNormalized ? input[2] : input[2] / 255,
110
+ a: input[3] !== void 0 ? isNormalized ? input[3] : input[3] / 255 : 1
111
+ };
112
+ }
113
+ if (typeof input === "string") {
114
+ const rgbMatch = input.match(/rgb\s*\(\s*(\d+)\s*,\s*(\d+)\s*,\s*(\d+)\s*\)/);
115
+ if (rgbMatch) {
116
+ return {
117
+ r: parseInt(rgbMatch[1], 10) / 255,
118
+ g: parseInt(rgbMatch[2], 10) / 255,
119
+ b: parseInt(rgbMatch[3], 10) / 255,
120
+ a: 1
121
+ };
122
+ }
123
+ const hex = input.replace("#", "");
124
+ if (hex.length === 3 || hex.length === 6) {
125
+ const expanded = hex.length === 3 ? hex.split("").map((c) => c + c).join("") : hex;
126
+ const result = /^([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(expanded);
127
+ if (result) {
128
+ return {
129
+ r: parseInt(result[1], 16) / 255,
130
+ g: parseInt(result[2], 16) / 255,
131
+ b: parseInt(result[3], 16) / 255,
132
+ a: 1
133
+ };
134
+ }
135
+ }
136
+ }
137
+ return { r: 0, g: 0, b: 0, a: 1 };
138
+ }
139
+ function isColorValue(value) {
140
+ if (typeof value === "string") {
141
+ return value.startsWith("#") || value.startsWith("rgb(");
142
+ }
143
+ if (typeof value === "object" && value !== null) {
144
+ if ("r" in value && "g" in value && "b" in value) {
145
+ return true;
146
+ }
147
+ if (Array.isArray(value) && (value.length === 3 || value.length === 4)) {
148
+ return value.every((v) => typeof v === "number");
149
+ }
150
+ }
151
+ return false;
152
+ }
153
+
154
+ // src/types/coreTypes.ts
155
+ var CORE_TYPES2 = CORE_TYPES;
156
+ var CORE_SET = new Set(CORE_TYPES2);
157
+ function isCoreType(type) {
158
+ return CORE_SET.has(type);
159
+ }
160
+ var normalizeType = normalizeCascadeType;
161
+ function canConnect(fromType, toType) {
162
+ return canConnectTypes(fromType, toType);
163
+ }
164
+
165
+ // src/utils/thrownError.ts
166
+ function asError(thrown) {
167
+ if (thrown instanceof Error) return thrown;
168
+ const described = describeThrown(thrown);
169
+ const error = new Error(described);
170
+ error.cause = thrown;
171
+ return error;
172
+ }
173
+ function describeThrown(thrown) {
174
+ if (thrown === null) return "null was thrown";
175
+ if (thrown === void 0) return "undefined was thrown";
176
+ if (typeof thrown === "string") return thrown || "an empty string was thrown";
177
+ if (typeof thrown !== "object") return `${String(thrown)} was thrown`;
178
+ const value = thrown;
179
+ const kind = typeof value.name === "string" && value.name ? value.name : value.constructor?.name || "object";
180
+ const parts = [];
181
+ if (typeof value.message === "string" && value.message) parts.push(value.message);
182
+ if (typeof value.reason === "string" && value.reason) parts.push(`reason: ${value.reason}`);
183
+ if (!parts.length) {
184
+ try {
185
+ const json = JSON.stringify(thrown);
186
+ if (json && json !== "{}") parts.push(json);
187
+ } catch {
188
+ }
189
+ }
190
+ return parts.length ? `${kind}: ${parts.join(", ")}` : `${kind} was thrown with no message`;
191
+ }
192
+
193
+ // src/nodes/outputEquality.ts
194
+ function sameOutputValue(previous, next, budget = 2e6) {
195
+ const state = { remaining: budget };
196
+ return same(previous, next, state, true);
197
+ }
198
+ function sameBundleField(previous, next, budget = 2e6) {
199
+ return same(previous, next, { remaining: budget }, false);
200
+ }
201
+ function isPlain(value) {
202
+ const proto = Object.getPrototypeOf(value);
203
+ return proto === Object.prototype || proto === null || proto === Array.prototype;
204
+ }
205
+ function same(a, b, state, top) {
206
+ if (--state.remaining < 0) return false;
207
+ if (a === null || b === null || typeof a !== "object" || typeof b !== "object") {
208
+ if (typeof a === "function" || typeof b === "function") return false;
209
+ return Object.is(a, b);
210
+ }
211
+ if (a === b && (top || ArrayBuffer.isView(a) || !isPlain(a))) return false;
212
+ if (ArrayBuffer.isView(a) || ArrayBuffer.isView(b)) {
213
+ if (!ArrayBuffer.isView(a) || !ArrayBuffer.isView(b)) return false;
214
+ if (a.constructor !== b.constructor || a.byteLength !== b.byteLength) return false;
215
+ state.remaining -= a.byteLength / 8;
216
+ if (state.remaining < 0) return false;
217
+ const x2 = new Uint8Array(a.buffer, a.byteOffset, a.byteLength);
218
+ const y2 = new Uint8Array(b.buffer, b.byteOffset, b.byteLength);
219
+ for (let i = 0; i < x2.length; i++) if (x2[i] !== y2[i]) return false;
220
+ return true;
221
+ }
222
+ if (!isPlain(a) || !isPlain(b)) return false;
223
+ const arrayA = Array.isArray(a);
224
+ if (arrayA !== Array.isArray(b)) return false;
225
+ if (arrayA) {
226
+ const x2 = a;
227
+ const y2 = b;
228
+ if (x2.length !== y2.length) return false;
229
+ for (let i = 0; i < x2.length; i++) if (!same(x2[i], y2[i], state, false)) return false;
230
+ return true;
231
+ }
232
+ const x = a;
233
+ const y = b;
234
+ const keys = Object.keys(x);
235
+ if (keys.length !== Object.keys(y).length) return false;
236
+ for (const key of keys) {
237
+ if (!Object.prototype.hasOwnProperty.call(y, key)) return false;
238
+ if (!same(x[key], y[key], state, false)) return false;
239
+ }
240
+ return true;
241
+ }
242
+
243
+ // src/nodes/inputFingerprint.ts
244
+ var identities = /* @__PURE__ */ new WeakMap();
245
+ var nextIdentity = 1;
246
+ var identityOf = (value) => identities.get(value) ?? (identities.set(value, nextIdentity), nextIdentity++);
247
+ function hashBytes(view) {
248
+ const length = view.byteLength;
249
+ let words;
250
+ if ((view.byteOffset | length) & 3) {
251
+ const padded = new Uint8Array(length + 3 & ~3);
252
+ padded.set(new Uint8Array(view.buffer, view.byteOffset, length));
253
+ words = new Int32Array(padded.buffer);
254
+ } else {
255
+ words = new Int32Array(view.buffer, view.byteOffset, length >> 2);
256
+ }
257
+ let a = 2166136261 ^ length, b = 2654435769, c = 2246822507, d = 3266489909;
258
+ const count = words.length;
259
+ const blocks = count & ~3;
260
+ for (let i = 0; i < blocks; i += 4) {
261
+ a = Math.imul(a ^ words[i], 16777619);
262
+ a = a << 13 | a >>> 19;
263
+ b = Math.imul(b ^ words[i + 1], 1540483477);
264
+ b = b << 13 | b >>> 19;
265
+ c = Math.imul(c ^ words[i + 2], 16777619);
266
+ c = c << 13 | c >>> 19;
267
+ d = Math.imul(d ^ words[i + 3], 1540483477);
268
+ d = d << 13 | d >>> 19;
269
+ }
270
+ for (let i = blocks; i < count; i++) {
271
+ a = Math.imul(a ^ words[i], 16777619);
272
+ a = a << 13 | a >>> 19;
273
+ }
274
+ return [a, b, c, d].map((lane) => (lane >>> 0).toString(36)).join(".");
275
+ }
276
+ var hashingReplacer = (_key, item) => ArrayBuffer.isView(item) ? `#${hashBytes(item)}` : item;
277
+ function valueFingerprint(value) {
278
+ const type = typeof value;
279
+ if (value == null || type !== "object" && type !== "function") return `${type}:${String(value)}`;
280
+ const object = value;
281
+ const channels = object.channels;
282
+ if (Array.isArray(channels) && channels.every((channel) => ArrayBuffer.isView(channel))) {
283
+ return `${object.width}x${object.height}:${object.colorSpace}:${channels.map(hashBytes).join(",")}`;
284
+ }
285
+ let json;
286
+ try {
287
+ json = JSON.stringify(object, hashingReplacer);
288
+ } catch {
289
+ json = `#${identityOf(object)}`;
290
+ }
291
+ const proto = Object.getPrototypeOf(object);
292
+ const content = proto === Object.prototype || proto === null || Array.isArray(object) || ArrayBuffer.isView(object);
293
+ return content ? `json:${json}` : `obj#${identityOf(object)}:${json}`;
294
+ }
295
+
296
+ // src/nodes/Node.ts
297
+ function inferParamType(value) {
298
+ if (typeof value === "number") return Number.isInteger(value) ? "int" : "float";
299
+ if (typeof value === "boolean") return "bool";
300
+ if (typeof value === "string") return "string";
301
+ if (Array.isArray(value) && value.every((v) => typeof v === "number")) {
302
+ if (value.length === 2) return "vec2";
303
+ if (value.length === 3) return "vec3";
304
+ if (value.length === 4) return "vec4";
305
+ }
306
+ return "any";
307
+ }
308
+ function paramControlType(dataType) {
309
+ switch (normalizeType(dataType ?? "any")) {
310
+ case "int":
311
+ return "int";
312
+ case "float":
313
+ return "number";
314
+ case "bool":
315
+ return "boolean";
316
+ case "string":
317
+ return "text";
318
+ case "color":
319
+ return "color";
320
+ case "vec2":
321
+ return "vec2";
322
+ case "vec3":
323
+ return "vec3";
324
+ case "vec4":
325
+ return "vec4";
326
+ default:
327
+ return void 0;
328
+ }
329
+ }
330
+ var PROP_CONTROL_TO_DATA_TYPE = {
331
+ number: "float",
332
+ slider: "float",
333
+ range: "float",
334
+ float: "float",
335
+ int: "int",
336
+ text: "string",
337
+ textarea: "string",
338
+ string: "string",
339
+ select: "string",
340
+ boolean: "bool",
341
+ bool: "bool",
342
+ vector: "vec3"
343
+ };
344
+ function carryType(type) {
345
+ if (!type) return "any";
346
+ return normalizeType(PROP_CONTROL_TO_DATA_TYPE[type] ?? type);
347
+ }
348
+ var NUMERIC_CARRY = /* @__PURE__ */ new Set(["int", "float"]);
349
+ function carryTypesMatch(before, after) {
350
+ if (before === "any" || after === "any") return true;
351
+ if (NUMERIC_CARRY.has(before) && NUMERIC_CARRY.has(after)) return true;
352
+ return before === after;
353
+ }
354
+ function sameStoredValue(a, b) {
355
+ if (a === b) return true;
356
+ if (Array.isArray(a) && Array.isArray(b)) {
357
+ return a.length === b.length && a.every((item, index) => sameStoredValue(item, b[index]));
358
+ }
359
+ if (isPlainRecord(a) && isPlainRecord(b)) {
360
+ const keys = Object.keys(a);
361
+ if (keys.length !== Object.keys(b).length) return false;
362
+ return keys.every((key) => key in b && sameStoredValue(a[key], b[key]));
363
+ }
364
+ return typeof a === "number" && typeof b === "number" && Number.isNaN(a) && Number.isNaN(b);
365
+ }
366
+ function isPlainRecord(value) {
367
+ if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
368
+ const prototype = Object.getPrototypeOf(value);
369
+ return prototype === Object.prototype || prototype === null;
370
+ }
371
+ var Node = class _Node {
372
+ constructor(id, type, graph) {
373
+ // Ports - available to all nodes
374
+ this.inputs = [];
375
+ this.outputs = [];
376
+ // Props system
377
+ this.props = {};
378
+ this.propWatchers = /* @__PURE__ */ new Map();
379
+ // Actions system - button-triggered callbacks
380
+ this.actions = {};
381
+ // Variadic inputs
382
+ /** The node's own values — see param(). Distinct from `props`, which is the
383
+ * older image-node control system. */
384
+ this.parameters = [];
385
+ this.parametersUsedDuringSetup = /* @__PURE__ */ new Set();
386
+ /**
387
+ * Props the node's own code declared with `addParm()`, and which its cook may
388
+ * therefore read as `this.props.name.value`.
389
+ *
390
+ * A prop restored from a document is NOT in here until the code that owns it
391
+ * has run, which for any on-demand-compiled module is after the first cook
392
+ * has begun. See evaluateAllExpressions for why that distinction has to be
393
+ * made rather than assumed.
394
+ */
395
+ this.propsDeclaredByCode = /* @__PURE__ */ new Set();
396
+ /** Non-null between a retarget and the cook that resolves it. */
397
+ this.carryOver = null;
398
+ /** The last completed reconciliation, for a panel that arrives after the fact. */
399
+ this.lastCarryOverReport = null;
400
+ this.variadic = false;
401
+ this.variadicDefault = null;
402
+ this.variadicOptions = {};
403
+ // Visual
404
+ this.preview = null;
405
+ // Metadata
406
+ this.comment = "";
407
+ this.error = null;
408
+ this.warning = null;
409
+ // Computation-specific
410
+ this.code = "";
411
+ this.bypass = false;
412
+ this.cook = false;
413
+ // Path system - hierarchical node organization
414
+ this.parent = null;
415
+ /**
416
+ * Child parameters this node shows as its own — Houdini's promoted
417
+ * parameters.
418
+ *
419
+ * Marcus, 2026-09-09, on where a force's settings should live: *"keep the
420
+ * force settings on the inside, but allow to promote parameters to the level
421
+ * above with 1 click."*
422
+ *
423
+ * **This is presentation, not state.** The value stays on the child and there
424
+ * is exactly one of it; the container only says which ones to show. The
425
+ * alternative — a real parameter on the container that writes through — would
426
+ * be a second copy of the value, a resolution order between the two, and a
427
+ * way for them to disagree. Props already carry a value, an expression and a
428
+ * channel with an order between them; doing it again for promotion is the
429
+ * mistake this avoids.
430
+ *
431
+ * Which is also why the runtime ignores this field entirely. Nothing about a
432
+ * cook changes when a parameter is promoted, so a headless render and a
433
+ * Studio session cannot diverge over it.
434
+ */
435
+ this.promotions = [];
436
+ this._children = [];
437
+ // ============ Early cutoff ============
438
+ /**
439
+ * Whether this node may be skipped when it is stale only because something
440
+ * upstream re-cooked and every input came back identical.
441
+ *
442
+ * On by default. A node whose result depends on something other than its
443
+ * inputs and its own parameters — the clock, a file it reads behind the
444
+ * graph's back, a random draw it wants fresh each time — sets this false, or
445
+ * marks itself dirty when that other thing changes (which is the better fix,
446
+ * and what time-dependent expressions already do).
447
+ */
448
+ this.earlyCutoff = true;
449
+ /** Per output port: bumped whenever a committed value is not provably the
450
+ * same as the last one. What downstream nodes compare instead of values. */
451
+ this.outputVersions = /* @__PURE__ */ new Map();
452
+ /** Set when this node itself was invalidated (a parameter, its code, a
453
+ * connection), as opposed to only being downstream of something that was.
454
+ * A self-dirty node always cooks. */
455
+ this.selfDirty = true;
456
+ /** The input versions this node last cooked cleanly against, or null. */
457
+ this.cutoffKey = null;
458
+ /**
459
+ * Per bundle output: a version per field, plus an epoch bumped whenever a
460
+ * change could not be attributed to fields (an output the cook did not set,
461
+ * the same object committed twice). A consumer that declares the fields it
462
+ * reads compares these instead of the port's version, so a change to one
463
+ * field does not re-cook the consumers of the others.
464
+ */
465
+ this.bundleFieldVersions = /* @__PURE__ */ new Map();
466
+ // Execution state
467
+ this.hasExecuted = false;
468
+ this.lastInputHash = "";
469
+ /** The inputs an execute threw on, or null if the last cook did not throw.
470
+ * Read only by `isDirty` — see the comment there for why it exists. */
471
+ this.failedInputHash = null;
472
+ /**
473
+ * The abort controllers for cooks currently in flight.
474
+ *
475
+ * A set rather than one, for the same reason `stagingStack` is a stack: two
476
+ * cooks can overlap, and the one that finishes first must not cancel the
477
+ * other. Aborted together on invalidation, because an invalidation
478
+ * invalidates every version of this node's work rather than the newest.
479
+ */
480
+ this.cookAborts = /* @__PURE__ */ new Set();
481
+ this.cookState = "stale";
482
+ this.cookGeneration = 0;
483
+ /**
484
+ * Output values staged by the cook currently running, one map per cook.
485
+ *
486
+ * A stack rather than a field, because it was a field and that was a race.
487
+ * `stagedOutputs` was instance state created per cook, iterated under a
488
+ * `generation === this.cookGeneration` guard, and nulled in `finally` — but
489
+ * `cookGeneration` only advances in `invalidateCook()`, and `cook()` has no
490
+ * re-entrancy guard. So two overlapping cooks of the same node with no
491
+ * invalidation between them both ran at the same generation: the second's
492
+ * `finally` nulled the field, the first resumed, passed the generation check
493
+ * because nothing had invalidated it, and iterated `null` —
494
+ * `stagedOutputs is not iterable`.
495
+ *
496
+ * Worse than the crash, and the reason this is a correctness fix rather than
497
+ * a robustness one: the second cook's `new Map()` **replaced the first's
498
+ * mid-flight**, so the first cook's staged writes were lost whenever the
499
+ * timing did not happen to produce the throw.
500
+ *
501
+ * The underlying fault is that per-cook state lived on the instance, and the
502
+ * thing protecting it (the generation) was not the thing destroying it (the
503
+ * `finally`) — two mechanisms guarding one field, neither aware of the other.
504
+ * Each cook now owns its own map, holds it locally, and removes it **by
505
+ * identity**, so an interleaving cannot make one cook drop another's.
506
+ *
507
+ * Diagnosed by MW-OBSERVATORY-ART, 2026-09-08.
508
+ */
509
+ this.stagingStack = [];
510
+ this.executionTimeout = 3e4;
511
+ // Cook info - performance tracking
512
+ this.cookInfo = {
513
+ lastCookTime: 0,
514
+ totalCookTime: 0,
515
+ cookCount: 0,
516
+ averageCookTime: 0,
517
+ lastCookTimestamp: 0,
518
+ peakMemory: 0
519
+ };
520
+ // Time dependency tracking (for expression engine)
521
+ this.isTimeDependent = false;
522
+ // Port tracking for cleanup
523
+ this.portsUsedDuringSetup = /* @__PURE__ */ new Set();
524
+ // Port index map for O(1) lookups (portId -> port)
525
+ this._portMap = /* @__PURE__ */ new Map();
526
+ // ============ Props System ============
527
+ /**
528
+ * Add a parameter to this node
529
+ * Short alias for defineProp - use in setup()
530
+ */
531
+ /** Props whose declared default expression has already been applied — see
532
+ * `bindParameterProp`, which must not reinstate one the author deleted. */
533
+ this.defaultExpressionsApplied = /* @__PURE__ */ new Set();
534
+ /** Props whose plain stored value explicitly overrides a declared default
535
+ * expression. The marker survives even when that value equals the default. */
536
+ this.plainPropOverrides = /* @__PURE__ */ new Set();
537
+ /**
538
+ * A container that a declaration said is one.
539
+ *
540
+ * Set by `attachDefinition` from `definition.container`, because `isNetwork`
541
+ * was a class override and a definition-v1 node is a plain `Node` — so a
542
+ * `cascade.core.Subnet` created through the adapter reported `false` and
543
+ * nothing that asks could tell it was a subnet.
544
+ *
545
+ * Marcus, 2026-09-09, hitting both symptoms of that at once: *"double
546
+ * clicking on the node goes to the code view ... not the subnet ... also the
547
+ * i and o keys are not doing anything."* Two features, one cause — the
548
+ * double-click handler and the i/o shortcuts both gate on `isNetwork()`, and
549
+ * both were correct code asking a question that could only ever answer no.
550
+ */
551
+ this.declaredContainer = null;
552
+ this.id = id;
553
+ this.type = type;
554
+ this.graph = graph;
555
+ this.position = { x: 0, y: 0 };
556
+ if (this.constructor !== _Node) {
557
+ this.setup();
558
+ queueMicrotask(() => {
559
+ if (this.onReady) {
560
+ try {
561
+ this.onReady();
562
+ } catch (err) {
563
+ console.error(`Error in onReady for node ${this.id}:`, err);
564
+ }
565
+ }
566
+ });
567
+ }
568
+ }
569
+ /** Is this child parameter shown on the parent? */
570
+ isPromotedToParent(param) {
571
+ return (this.parent?.promotions ?? []).some(
572
+ (entry) => entry.nodeId === this.id && entry.param === param
573
+ );
574
+ }
575
+ /**
576
+ * One click, both directions.
577
+ *
578
+ * A no-op without a parent rather than an error: the button is only rendered
579
+ * for a child, and a guard that throws would turn a stale UI into a crash.
580
+ */
581
+ setPromotedToParent(param, promoted) {
582
+ const parent = this.parent;
583
+ if (!parent) return;
584
+ const index = parent.promotions.findIndex(
585
+ (entry) => entry.nodeId === this.id && entry.param === param
586
+ );
587
+ if (promoted && index < 0) parent.promotions.push({ nodeId: this.id, param });
588
+ else if (!promoted && index >= 0) parent.promotions.splice(index, 1);
589
+ }
590
+ /** @internal The version of one output, for downstream cutoff. */
591
+ outputVersion(portId) {
592
+ return this.outputVersions.get(portId) ?? 0;
593
+ }
594
+ bumpOutputVersion(portId) {
595
+ this.outputVersions.set(portId, (this.outputVersions.get(portId) ?? 0) + 1);
596
+ const fields = this.bundleFieldVersions.get(portId);
597
+ if (fields) fields.epoch += 1;
598
+ }
599
+ /** @internal The version of one field of a bundle output. */
600
+ outputFieldVersion(portId, field) {
601
+ const entry = this.bundleFieldVersions.get(portId);
602
+ return entry ? `${entry.epoch}.${entry.fields.get(field) ?? 0}` : `0.0`;
603
+ }
604
+ /** Bump the versions of the fields a bundle commit changed. */
605
+ bumpBundleFields(portId, previous, next) {
606
+ let entry = this.bundleFieldVersions.get(portId);
607
+ if (!entry) {
608
+ entry = { epoch: 0, fields: /* @__PURE__ */ new Map() };
609
+ this.bundleFieldVersions.set(portId, entry);
610
+ }
611
+ const prev = previous !== null && typeof previous === "object" ? previous : null;
612
+ const curr = next !== null && typeof next === "object" ? next : null;
613
+ if (!prev || !curr || prev === curr) {
614
+ entry.epoch += 1;
615
+ return;
616
+ }
617
+ const has = Object.prototype.hasOwnProperty;
618
+ for (const field of /* @__PURE__ */ new Set([...Object.keys(prev), ...Object.keys(curr)])) {
619
+ const changed = has.call(prev, field) !== has.call(curr, field) || !sameBundleField(prev[field], curr[field]);
620
+ if (changed) entry.fields.set(field, (entry.fields.get(field) ?? 0) + 1);
621
+ }
622
+ }
623
+ /**
624
+ * The fields of a bundle input this node depends on, or null for the whole
625
+ * bundle. Declared by the node (`fieldSelection`), plus whatever its
626
+ * parameter expressions read through `ch("port.field")`. A node that does
627
+ * not declare its fields may read any of them, so it depends on all.
628
+ */
629
+ bundleFieldsRead(input) {
630
+ if (input.dataType !== "bundle" || !input.options?.fieldSelection) return null;
631
+ const fields = new Set(input.options.fieldSelection);
632
+ for (const prop of Object.values(this.props)) {
633
+ if (!prop?.expression) continue;
634
+ for (const reference of bundleFieldReferences(prop.expression))
635
+ if (reference.port === input.name) fields.add(reference.field);
636
+ }
637
+ return [...fields].sort();
638
+ }
639
+ /** @internal The scheduler calls this on the node an invalidation started at. */
640
+ markSelfDirty() {
641
+ this.selfDirty = true;
642
+ }
643
+ /** What every input currently comes from, as upstream output versions. Null
644
+ * when an input comes from something that keeps no versions (an annotation),
645
+ * which disables the cutoff for this node. */
646
+ inputVersionKey() {
647
+ const parts = [];
648
+ for (const input of this.inputs) {
649
+ if (input.connections.length === 0) {
650
+ parts.push(`${input.id}=${valueFingerprint(input.value)}`);
651
+ continue;
652
+ }
653
+ const fields = this.bundleFieldsRead(input);
654
+ for (const connection of input.connections) {
655
+ const upstream = this.graph.getNode(connection.from.nodeId);
656
+ if (!upstream) return null;
657
+ const source = `${input.id}<${connection.from.nodeId}:${connection.from.portId}`;
658
+ parts.push(fields ? `${source}{${fields.map((field) => `${field}@${upstream.outputFieldVersion(connection.from.portId, field)}`).join(",")}}` : `${source}@${upstream.outputVersion(connection.from.portId)}`);
659
+ }
660
+ }
661
+ return parts.join("|");
662
+ }
663
+ /**
664
+ * @internal Early cutoff: true when this node is stale only transitively,
665
+ * cooked cleanly last time, and every upstream output it reads still has the
666
+ * version it cooked against — i.e. the upstream re-cook produced the same
667
+ * values. Skipping it then is exact, and the nodes below it see unchanged
668
+ * versions in turn, so the whole subgraph stays cached.
669
+ */
670
+ canSkipCook() {
671
+ if (!this.earlyCutoff || this.selfDirty || !this.hasExecuted) return false;
672
+ if (this.cutoffKey === null || this.error !== null) return false;
673
+ if (!this.nodeFunction && this.onCook === _Node.prototype.onCook) return false;
674
+ return this.inputVersionKey() === this.cutoffKey;
675
+ }
676
+ /** @internal Mark clean without cooking; see `canSkipCook`. */
677
+ skipCook() {
678
+ this.setCookState("clean");
679
+ }
680
+ /** The owning graph's document `seed`, which `context.seed` derives from. */
681
+ get documentSeed() {
682
+ return this.graph?.seed;
683
+ }
684
+ // ============ Port Creation ============
685
+ in(name, defaultValue, options = {}) {
686
+ const portType = name === "trigger" ? "trigger" : "param";
687
+ const existingPort = this.inputs.find((p) => p.name === name);
688
+ if (existingPort) {
689
+ this.portsUsedDuringSetup.add(`input_${name}`);
690
+ if (options.type) existingPort.dataType = options.type;
691
+ if (defaultValue !== void 0 && existingPort.defaultValue !== defaultValue) {
692
+ existingPort.defaultValue = defaultValue;
693
+ if (existingPort.value === void 0 || existingPort.value === existingPort.defaultValue) {
694
+ existingPort.value = defaultValue;
695
+ }
696
+ }
697
+ existingPort.options = Object.assign(existingPort.options ?? {}, options);
698
+ return existingPort;
699
+ }
700
+ const portIndex = this.inputs.length;
701
+ const port = {
702
+ id: `${this.id}_in_${portIndex}`,
703
+ name,
704
+ portType,
705
+ dataType: options.type || "any",
706
+ value: defaultValue,
707
+ defaultValue,
708
+ options,
709
+ connections: []
710
+ };
711
+ this.inputs.push(port);
712
+ this._portMap.set(port.id, port);
713
+ this.portsUsedDuringSetup.add(`input_${name}`);
714
+ return port;
715
+ }
716
+ /**
717
+ * Declare an output port. Takes the same `options` as `in()` — an output that
718
+ * cannot state its type is a hole in the type system: the connection check
719
+ * only ever sees `any` on one side, and the port draws in the neutral colour
720
+ * whatever it actually carries.
721
+ */
722
+ out(name, portType = "param", options = {}) {
723
+ const existingPort = this.outputs.find((p) => p.name === name);
724
+ if (existingPort) {
725
+ this.portsUsedDuringSetup.add(`output_${name}`);
726
+ if (existingPort.portType !== portType) existingPort.portType = portType;
727
+ if (options.type) existingPort.dataType = options.type;
728
+ existingPort.options = { ...existingPort.options ?? {}, ...options };
729
+ return existingPort;
730
+ }
731
+ const portIndex = this.outputs.length;
732
+ const port = {
733
+ id: `${this.id}_out_${portIndex}`,
734
+ name,
735
+ portType,
736
+ dataType: options.type || "any",
737
+ value: void 0,
738
+ connections: [],
739
+ options,
740
+ setValue: (value) => this.setOutputValue(port, value),
741
+ trigger: (props) => {
742
+ port.connections.forEach((conn) => {
743
+ const targetNode = this.graph.getNode(conn.to.nodeId);
744
+ if (targetNode) {
745
+ const targetPort = targetNode.getInputPortById(conn.to.portId);
746
+ if (targetPort && targetPort.onTrigger) targetPort.onTrigger(props);
747
+ }
748
+ });
749
+ }
750
+ };
751
+ this.outputs.push(port);
752
+ this._portMap.set(port.id, port);
753
+ this.portsUsedDuringSetup.add(`output_${name}`);
754
+ return port;
755
+ }
756
+ // ============ Port Access ============
757
+ getInputPort(index) {
758
+ return index >= 0 && index < this.inputs.length ? this.inputs[index] : null;
759
+ }
760
+ getOutputPort(index) {
761
+ return index >= 0 && index < this.outputs.length ? this.outputs[index] : null;
762
+ }
763
+ /** Update the element id and every derived port id as one operation. */
764
+ renameId(newId) {
765
+ const oldId = this.id;
766
+ const renamed = /* @__PURE__ */ new Map();
767
+ this.id = newId;
768
+ this._portMap.clear();
769
+ for (const port of [...this.inputs, ...this.outputs]) {
770
+ const oldPortId = port.id;
771
+ port.id = oldPortId.startsWith(`${oldId}_`) ? `${newId}${oldPortId.slice(oldId.length)}` : oldPortId;
772
+ renamed.set(oldPortId, port.id);
773
+ this._portMap.set(port.id, port);
774
+ }
775
+ return renamed;
776
+ }
777
+ getPort(portId) {
778
+ return this._portMap.get(portId) || null;
779
+ }
780
+ getInputPortById(portId) {
781
+ const port = this._portMap.get(portId);
782
+ return port && "defaultValue" in port ? port : null;
783
+ }
784
+ setOutputValue(port, value) {
785
+ const staging = this.stagingStack[this.stagingStack.length - 1];
786
+ if (staging) {
787
+ staging.set(port, value);
788
+ return;
789
+ }
790
+ this.commitOutputValue(port, value);
791
+ }
792
+ commitOutputValue(port, value) {
793
+ if (!sameOutputValue(port.value, value)) {
794
+ if (port.dataType === "bundle") {
795
+ this.outputVersions.set(port.id, (this.outputVersions.get(port.id) ?? 0) + 1);
796
+ this.bumpBundleFields(port.id, port.value, value);
797
+ } else {
798
+ this.bumpOutputVersion(port.id);
799
+ }
800
+ }
801
+ port.value = value;
802
+ for (const connection of port.connections) {
803
+ const targetNode = this.graph.getNode(connection.to.nodeId);
804
+ const targetPort = targetNode?.getInputPortById(connection.to.portId);
805
+ if (!targetPort) continue;
806
+ targetPort.value = value;
807
+ targetPort.onChange?.(value);
808
+ }
809
+ }
810
+ addParm(name, config) {
811
+ this.props[name] = config;
812
+ this.propsDeclaredByCode.add(name);
813
+ this.reconcileProp(name);
814
+ if (this.cookState !== "cooking") this.markDirty();
815
+ }
816
+ /**
817
+ * Define a parameter on this node
818
+ * @deprecated Use addParm() instead
819
+ */
820
+ defineProp(name, config) {
821
+ this.addParm(name, config);
822
+ }
823
+ /**
824
+ * Add an action button to this node
825
+ * Actions appear in the Inspector and can be triggered by keyboard shortcuts
826
+ */
827
+ addAction(name, config) {
828
+ this.actions[name] = config;
829
+ }
830
+ /**
831
+ * Execute an action by name
832
+ */
833
+ async executeAction(name) {
834
+ const action = this.actions[name];
835
+ if (!action) return;
836
+ if (action.condition && !action.condition()) return;
837
+ await action.callback();
838
+ }
839
+ updateProp(name, value) {
840
+ if (!this.props[name]) return;
841
+ const newValue = Array.isArray(value) ? [...value] : value;
842
+ this.props[name] = { ...this.props[name], value: newValue };
843
+ if (this.props[name].onChange) {
844
+ try {
845
+ this.props[name].onChange(this.props[name], this);
846
+ } catch (err) {
847
+ console.error(`Error in prop onChange for ${name}:`, err);
848
+ }
849
+ }
850
+ const watchers = this.propWatchers.get(name);
851
+ if (watchers) {
852
+ watchers.forEach((cb) => {
853
+ try {
854
+ cb(newValue, this.props[name]);
855
+ } catch (err) {
856
+ console.error(`Error in prop watcher for ${name}:`, err);
857
+ }
858
+ });
859
+ }
860
+ this.props = { ...this.props };
861
+ this.markDirty();
862
+ _Node.onPropParamsChanged?.(this.id);
863
+ }
864
+ /**
865
+ * Update prop params (e.g., min, max) and trigger UI reactivity
866
+ */
867
+ updatePropParams(name, params) {
868
+ if (!this.props[name]) return;
869
+ this.props[name] = {
870
+ ...this.props[name],
871
+ params: { ...this.props[name].params, ...params }
872
+ };
873
+ this.props = { ...this.props };
874
+ _Node.onPropParamsChanged?.(this.id);
875
+ }
876
+ watchProp(name, callback) {
877
+ if (!this.propWatchers.has(name)) this.propWatchers.set(name, []);
878
+ this.propWatchers.get(name).push(callback);
879
+ }
880
+ // ============ Parm API (HOM-style wrappers) ============
881
+ /**
882
+ * Get a parameter wrapper object for HOM-style access
883
+ */
884
+ parm(name) {
885
+ const prop = this.props[name];
886
+ if (!prop) return null;
887
+ const self = this;
888
+ const live = () => self.props[name] ?? prop;
889
+ return {
890
+ name: () => name,
891
+ path: () => `${self.path()}/${name}`,
892
+ node: () => self,
893
+ eval: () => live().value,
894
+ evalAsFloat: () => parseFloat(live().value) || 0,
895
+ evalAsInt: () => parseInt(live().value, 10) || 0,
896
+ evalAsString: () => String(live().value ?? ""),
897
+ rawValue: () => live().value,
898
+ set: (value) => self.updateProp(name, value),
899
+ parmType: () => live().type || "any",
900
+ // Expression support
901
+ expression: () => live().expression ?? null,
902
+ setExpression: (expr) => {
903
+ self.props[name] = { ...self.props[name], expression: expr };
904
+ self.updateTimeDependent();
905
+ self.markDirty();
906
+ },
907
+ deleteExpression: () => {
908
+ const { expression: _, expressionError: __, ...rest } = self.props[name];
909
+ self.props[name] = rest;
910
+ if (self.defaultExpressionsApplied.has(name)) {
911
+ self.plainPropOverrides.add(name);
912
+ }
913
+ self.updateTimeDependent();
914
+ self.markDirty();
915
+ },
916
+ hasExpression: () => !!live().expression,
917
+ expressionError: () => live().expressionError ?? null,
918
+ // ---- Keyframe channel ----
919
+ // Same shape as the expression API above, deliberately: a channel and an
920
+ // expression are the same kind of binding, one drawn and one written.
921
+ channel: () => self.props[name]?.channel ?? null,
922
+ hasChannel: () => !isEmptyChannel(self.props[name]?.channel),
923
+ keys: () => self.props[name]?.channel?.keys ?? [],
924
+ /**
925
+ * Key the parameter. Both arguments default to "here, now": the current
926
+ * frame and the parameter's current evaluated value, which is what
927
+ * alt-clicking a parameter has to mean.
928
+ */
929
+ setKey: (frame, value, interpolation) => {
930
+ const at = frame ?? self.currentFrame();
931
+ const v = value ?? Number(self.evalParm(name));
932
+ if (!Number.isFinite(v)) return;
933
+ self.setPropChannel(name, setKey(self.props[name]?.channel, at, v, interpolation));
934
+ },
935
+ deleteKey: (frame) => {
936
+ const current = self.props[name]?.channel;
937
+ if (!current) return;
938
+ self.setPropChannel(name, deleteKey(current, frame));
939
+ },
940
+ clearChannel: () => self.setPropChannel(name, null)
941
+ };
942
+ }
943
+ // ============ Keyframe channels ============
944
+ /**
945
+ * The frame a channel is sampled against.
946
+ *
947
+ * One clock today — the expression engine's, which the host keeps in step
948
+ * with its own frame — so `sin($T)` and a keyed curve on the same node
949
+ * always agree. When timelines become document objects this is the single
950
+ * place that has to learn which one a node belongs to.
951
+ */
952
+ currentFrame() {
953
+ return expressionEngine.fframe;
954
+ }
955
+ /**
956
+ * Attach, replace or clear a channel. An empty channel is removed rather
957
+ * than stored, so `hasChannel()` and the serialised form never disagree
958
+ * about whether a parameter is animated.
959
+ */
960
+ setPropChannel(name, channel) {
961
+ const prop = this.props[name];
962
+ if (!prop) return;
963
+ if (!channel || isEmptyChannel(channel)) {
964
+ const { channel: _dropped, ...rest } = this.props[name];
965
+ this.props[name] = rest;
966
+ } else {
967
+ this.props[name] = { ...prop, channel };
968
+ }
969
+ this.props = { ...this.props };
970
+ this.updateTimeDependent();
971
+ this.markDirty();
972
+ _Node.onPropParamsChanged?.(this.id);
973
+ }
974
+ /**
975
+ * A prop as it goes to disk: the bare value when there is nothing more to
976
+ * say, the object form when it carries an expression, a channel, or both.
977
+ *
978
+ * Lives here rather than in the serializer because the channel type is the
979
+ * node's, and one writer is what stops the two forms disagreeing.
980
+ */
981
+ serializePropValue(name, value) {
982
+ const prop = this.props[name];
983
+ if (!prop) return value;
984
+ const channel = serializeChannel(prop.channel);
985
+ if (!prop.expression && !channel) return value;
986
+ return {
987
+ value,
988
+ ...prop.expression ? { expression: prop.expression } : {},
989
+ ...channel ? { channel } : {}
990
+ };
991
+ }
992
+ preservePlainProp(name) {
993
+ this.plainPropOverrides.add(name);
994
+ }
995
+ preservesPlainProp(name) {
996
+ return this.plainPropOverrides.has(name) && this.defaultExpressionsApplied.has(name);
997
+ }
998
+ /** Read a channel back off disk. Bad data yields no channel, never a throw. */
999
+ restorePropChannel(name, data) {
1000
+ if (!this.props[name]) return;
1001
+ const channel = deserializeChannel(data);
1002
+ this.setPropChannel(name, isEmptyChannel(channel) ? null : channel);
1003
+ }
1004
+ /** The serialised channel for one prop, or undefined when it is not keyed. */
1005
+ serializePropChannel(name) {
1006
+ return serializeChannel(this.props[name]?.channel);
1007
+ }
1008
+ /**
1009
+ * The one place a parameter becomes a value — in Studio.
1010
+ *
1011
+ * Three bindings, in one order: a keyframe channel, an expression, the raw
1012
+ * value. All of them resolve BEFORE execute, so the node receives a plain
1013
+ * number and never reads a clock — determinism is "same graph plus same
1014
+ * frame gives the same output", which is still cacheable once the cache key
1015
+ * gains a frame.
1016
+ *
1017
+ * The ORDER itself is no longer written here. It lives in
1018
+ * `@cascade/runtime/params`, because the deterministic runtime resolves the
1019
+ * same three bindings for the same document and a second implementation of
1020
+ * the order would be a second answer to the same question. What stays here
1021
+ * is the Studio-specific half: the clock the channel is sampled against, and
1022
+ * the expression engine with its Inspector-facing error bookkeeping.
1023
+ */
1024
+ evalParm(name) {
1025
+ const prop = this.props[name];
1026
+ if (!prop) return void 0;
1027
+ return resolvePropBinding(prop, {
1028
+ frame: this.currentFrame(),
1029
+ // `evaluateExpression` already stores the message on the prop for the
1030
+ // panel and falls back to the raw value, so it reports no error upward:
1031
+ // the resolver's own fallback would be the same value twice.
1032
+ evaluateExpression: () => ({ value: this.evaluateExpression(name) })
1033
+ }).value;
1034
+ }
1035
+ /**
1036
+ * Set a parameter value (shorthand for updateProp)
1037
+ */
1038
+ setParm(name, value) {
1039
+ this.updateProp(name, value);
1040
+ }
1041
+ /**
1042
+ * Evaluate an expression for a parameter
1043
+ * Returns the evaluated value or the raw value if evaluation fails
1044
+ */
1045
+ evaluateExpression(name) {
1046
+ const prop = this.props[name];
1047
+ if (!prop?.expression) return prop?.value;
1048
+ expressionEngine.setGraph(this.graph);
1049
+ const result = expressionEngine.evaluate(prop.expression, this);
1050
+ if (result.error) {
1051
+ this.props[name] = { ...this.props[name], expressionError: result.error };
1052
+ return prop.value;
1053
+ }
1054
+ if (prop.expressionError) {
1055
+ const { expressionError: _, ...rest } = this.props[name];
1056
+ this.props[name] = rest;
1057
+ }
1058
+ return result.value;
1059
+ }
1060
+ /**
1061
+ * Evaluate all expressions on this node and update values
1062
+ * Called during execution to get current expression values
1063
+ */
1064
+ evaluateAllExpressions() {
1065
+ for (const [name, prop] of Object.entries(this.props)) {
1066
+ if (prop.expression && !prop.fromParameter && this.propsDeclaredByCode.has(name)) {
1067
+ const evaluated = this.evaluateExpression(name);
1068
+ if (evaluated !== void 0) {
1069
+ this.props[name] = { ...this.props[name], value: evaluated };
1070
+ }
1071
+ }
1072
+ }
1073
+ this.updateTimeDependent();
1074
+ }
1075
+ // ============ Parameters ============
1076
+ /**
1077
+ * A value that belongs to the node — not a pin.
1078
+ *
1079
+ * Cascade only had inputs and outputs, so every setting a node had was
1080
+ * declared as an input port that nobody ever wired. That reads wrong in the
1081
+ * graph (density-blend showed twenty input pins when it has four real inputs
1082
+ * and sixteen settings) and it reads wrong in the Inspector, where a crop
1083
+ * fraction and an incoming image sat in the same list.
1084
+ *
1085
+ * So: inputs are edges from other nodes, parameters are the node's own
1086
+ * values, outputs are edges out.
1087
+ *
1088
+ * A parameter can be PROMOTED to an input pin when you want it driven from
1089
+ * upstream rather than typed — a signal's weight computed by another node,
1090
+ * say. Promotion is per parameter and off by default: making every setting a
1091
+ * pin is how the two got conflated in the first place.
1092
+ */
1093
+ param(name, defaultValue, options = {}) {
1094
+ let parameter = this.parameters.find((p) => p.name === name);
1095
+ if (!parameter) {
1096
+ parameter = {
1097
+ name,
1098
+ value: defaultValue,
1099
+ defaultValue,
1100
+ dataType: options.type || inferParamType(defaultValue),
1101
+ promoted: false,
1102
+ options
1103
+ };
1104
+ this.parameters.push(parameter);
1105
+ } else {
1106
+ if (options.type) parameter.dataType = options.type;
1107
+ parameter.defaultValue = defaultValue;
1108
+ Object.assign(parameter.options, options);
1109
+ }
1110
+ this.parametersUsedDuringSetup.add(name);
1111
+ this.reconcileParameter(parameter, defaultValue, options);
1112
+ this.bindParameterProp(parameter, name, options);
1113
+ if (parameter.promoted) {
1114
+ const port = this.in(name, parameter.value, {
1115
+ ...parameter.options,
1116
+ type: parameter.dataType,
1117
+ promoted: true
1118
+ });
1119
+ port.fromParameter = name;
1120
+ if (port.connections.length > 0 && port.value !== void 0 && port.value !== null) {
1121
+ return { ...parameter, value: port.value };
1122
+ }
1123
+ }
1124
+ return parameter;
1125
+ }
1126
+ /**
1127
+ * Give a parameter the prop that holds its value, and make `parameter.value`
1128
+ * a view onto it.
1129
+ *
1130
+ * Called from `param()` on every declaration, so everything here is
1131
+ * idempotent: an existing prop keeps its value, expression and channel, and
1132
+ * only the declaration metadata is refreshed. The prop object is mutated
1133
+ * rather than replaced for the same reason the Inspector does not re-key a
1134
+ * row on commit — replacing it mid-drag destroys the control in use.
1135
+ */
1136
+ bindParameterProp(parameter, name, options) {
1137
+ const descriptor = Object.getOwnPropertyDescriptor(parameter, "value");
1138
+ const seeded = descriptor && !descriptor.get ? descriptor.value : void 0;
1139
+ let prop = this.props[name];
1140
+ if (!prop) {
1141
+ prop = { value: seeded !== void 0 ? seeded : parameter.defaultValue };
1142
+ this.props[name] = prop;
1143
+ this.props = { ...this.props };
1144
+ }
1145
+ if (options.defaultExpression && !this.defaultExpressionsApplied.has(name)) {
1146
+ this.defaultExpressionsApplied.add(name);
1147
+ if (!this.plainPropOverrides.has(name) && !prop.expression) {
1148
+ prop.expression = options.defaultExpression;
1149
+ this.props = { ...this.props };
1150
+ this.updateTimeDependent();
1151
+ }
1152
+ }
1153
+ prop.fromParameter = name;
1154
+ prop.hidden = true;
1155
+ const control = paramControlType(parameter.dataType);
1156
+ if (control) prop.type = control;
1157
+ const params = { ...prop.params ?? {} };
1158
+ if (options.min !== void 0) params.min = options.min;
1159
+ if (options.max !== void 0) params.max = options.max;
1160
+ if (options.step !== void 0) params.step = options.step;
1161
+ if (normalizeType(parameter.dataType ?? "any") === "int") params.integer = true;
1162
+ prop.params = params;
1163
+ if (descriptor?.get) return;
1164
+ Object.defineProperty(parameter, "value", {
1165
+ // Resolved, not raw: a channel wins, then an expression, then the stored
1166
+ // value — the same order `evalParm` uses, because it IS `evalParm`. This
1167
+ // is what carries a key or `$T * 0.25` into a sketch's `execute()`
1168
+ // without the sketch knowing anything about either.
1169
+ get: () => this.evalParm(name),
1170
+ set: (next) => this.writeParameterValue(name, next),
1171
+ enumerable: true,
1172
+ configurable: true
1173
+ });
1174
+ }
1175
+ /**
1176
+ * Write a parameter's stored value.
1177
+ *
1178
+ * **A written value clears a DEFAULT expression**, which is the rule the rest
1179
+ * of the system already states — *a default expression is a default, and the
1180
+ * author's number wins* — and which this method did not implement. The
1181
+ * asymmetry was real and silent: `parameter.value` reads *resolved*, so
1182
+ * writing 42 over a prop declaring `defaultExpression: '2 * 5'` stored 42 and
1183
+ * then read back 10. A panel doing exactly that (`setParam` then `getParam`)
1184
+ * saw its own write vanish, concluded the write had failed, and re-rendered
1185
+ * every series instance at the value it had just replaced — identical cache
1186
+ * keys, identical output, and nothing thrown or logged. Found by
1187
+ * MW-OBSERVATORY-ART building the series panel.
1188
+ *
1189
+ * An expression the author wrote is a different matter and is NOT cleared
1190
+ * here: it legitimately wins, and deleting it to make a write stick would
1191
+ * destroy work. `setParameter` reports that case instead of hiding it.
1192
+ *
1193
+ * The prop object is replaced so a panel reading it sees the change; no cook
1194
+ * is scheduled here — the callers that should (setParameter, updateProp) do
1195
+ * it themselves.
1196
+ */
1197
+ writeParameterValue(name, value) {
1198
+ const prop = this.props[name];
1199
+ if (!prop) return;
1200
+ const next = { ...prop, value: Array.isArray(value) ? [...value] : value };
1201
+ if (prop.expression && this.defaultExpressionsApplied.has(name)) {
1202
+ delete next.expression;
1203
+ delete next.expressionError;
1204
+ this.plainPropOverrides.add(name);
1205
+ }
1206
+ this.props[name] = next;
1207
+ this.props = { ...this.props };
1208
+ this.updateTimeDependent();
1209
+ }
1210
+ /**
1211
+ * Why a written value would not be the value read back, or null if it would.
1212
+ *
1213
+ * `parameter.value` resolves a channel first, then an expression, then the
1214
+ * stored value — so a write beneath either of the first two is stored and
1215
+ * then shadowed. That is correct precedence and a terrible silence: the
1216
+ * caller has no way to tell a shadowed write from a failed one, and both
1217
+ * look like the parameter ignoring them.
1218
+ */
1219
+ parameterWriteShadowedBy(name) {
1220
+ const prop = this.props[name];
1221
+ if (!prop) return null;
1222
+ if ((prop.channel?.keys?.length ?? 0) > 0) return "channel";
1223
+ if (prop.expression && !this.defaultExpressionsApplied.has(name)) return "expression";
1224
+ return null;
1225
+ }
1226
+ /**
1227
+ * A parameter's stored value, before any channel or expression resolves it.
1228
+ *
1229
+ * `parameter.value` is deliberately the resolved one, so anything that has
1230
+ * to record or re-key what the author actually set — serialisation, the
1231
+ * alt-click gesture — must ask for the raw one instead. Writing the sampled
1232
+ * value back to disk would freeze an animated parameter at whatever frame
1233
+ * the save happened on.
1234
+ */
1235
+ rawParameterValue(name) {
1236
+ if (this.props[name]) return this.props[name].value;
1237
+ return this.parameters.find((parameter) => parameter.name === name)?.value;
1238
+ }
1239
+ /** Turn a parameter into an input pin, or back. */
1240
+ setParameterPromoted(name, promoted) {
1241
+ const parameter = this.parameters.find((p) => p.name === name);
1242
+ if (!parameter || parameter.promoted === promoted) return;
1243
+ parameter.promoted = promoted;
1244
+ if (promoted) {
1245
+ const port = this.in(name, parameter.value, {
1246
+ ...parameter.options,
1247
+ type: parameter.dataType,
1248
+ promoted: true
1249
+ });
1250
+ port.fromParameter = name;
1251
+ } else {
1252
+ const index = this.inputs.findIndex((p) => p.name === name);
1253
+ if (index >= 0) {
1254
+ [...this.inputs[index].connections].forEach((conn) => this.graph?.disconnect(conn.id));
1255
+ this.inputs.splice(index, 1);
1256
+ }
1257
+ }
1258
+ this.markDirty();
1259
+ }
1260
+ /**
1261
+ * Write a parameter and cook — unless the write would change nothing.
1262
+ *
1263
+ * It marked the node dirty unconditionally, and MW-OBSERVATORY-ART found
1264
+ * what that costs a panel: the series panel re-writes its filter on every
1265
+ * load, so **mounting it cooked the whole document to arrive at values it
1266
+ * already held.** Every panel that restores state on mount hits this, and
1267
+ * each of them would otherwise write the same guard.
1268
+ *
1269
+ * The equality test is on the RAW stored value, because that is what a write
1270
+ * replaces. Two things it must not treat as unchanged, both of which would
1271
+ * make this quietly wrong:
1272
+ *
1273
+ * - a write that **clears a declared default expression** changes the
1274
+ * resolved value even though the stored one may match, so a bound prop
1275
+ * is never skipped;
1276
+ * - an array or object, which is compared structurally rather than by
1277
+ * reference — a panel that hands back a copy of the same numbers is the
1278
+ * exact case this exists for, and `===` would call it a change.
1279
+ */
1280
+ setParameter(name, value) {
1281
+ const parameter = this.parameters.find((p) => p.name === name);
1282
+ if (!parameter) return;
1283
+ if (this.parameterWriteIsNoop(name, value)) return;
1284
+ parameter.value = value;
1285
+ this.markDirty();
1286
+ }
1287
+ /** Whether writing this value would leave the node exactly as it is. */
1288
+ parameterWriteIsNoop(name, value) {
1289
+ const prop = this.props[name];
1290
+ if (!prop || prop.expression || (prop.channel?.keys?.length ?? 0) > 0) return false;
1291
+ return sameStoredValue(prop.value, value);
1292
+ }
1293
+ // ============ Parameter carry-over ============
1294
+ /**
1295
+ * Snapshot every current setting and start reconciling against whatever the
1296
+ * next definition declares. Idempotent per retarget; the snapshot stays until
1297
+ * a cook completes, so a definition that fails to compile loses nothing.
1298
+ */
1299
+ beginParameterCarryOver() {
1300
+ const carried = /* @__PURE__ */ new Map();
1301
+ for (const parameter of this.parameters) {
1302
+ carried.set(`parameter:${parameter.name}`, {
1303
+ name: parameter.name,
1304
+ kind: "parameter",
1305
+ type: carryType(parameter.dataType),
1306
+ // Raw, not resolved: a retarget carries what the author set, and
1307
+ // `parameter.value` now resolves a channel or an expression.
1308
+ value: this.rawParameterValue(parameter.name),
1309
+ expression: this.props[parameter.name]?.expression,
1310
+ channel: this.props[parameter.name]?.channel
1311
+ });
1312
+ }
1313
+ for (const [name, prop] of Object.entries(this.props)) {
1314
+ if (carried.has(`parameter:${name}`)) continue;
1315
+ carried.set(`prop:${name}`, {
1316
+ name,
1317
+ kind: "prop",
1318
+ type: carryType(prop.type),
1319
+ value: prop.value,
1320
+ expression: prop.expression,
1321
+ channel: prop.channel
1322
+ });
1323
+ }
1324
+ this.carryOver = {
1325
+ carried,
1326
+ resolved: /* @__PURE__ */ new Set(),
1327
+ report: { kept: [], defaulted: [], dropped: [], retyped: [] }
1328
+ };
1329
+ this.parametersUsedDuringSetup.clear();
1330
+ }
1331
+ /** Give up on reconciling — the retarget itself failed. Values are left
1332
+ * exactly as they were and nothing is reported as dropped. */
1333
+ abandonParameterCarryOver() {
1334
+ this.carryOver = null;
1335
+ }
1336
+ hasPendingParameterCarryOver() {
1337
+ return this.carryOver !== null;
1338
+ }
1339
+ /**
1340
+ * Reconcile against a declaration list the caller already has, without
1341
+ * waiting for a cook. Runs the declarations through the ordinary `param()` /
1342
+ * `addParm()` path so the node ends up in the same state either way — this
1343
+ * is the same pre-seeding deserialization does, only reported.
1344
+ */
1345
+ applyParameterDeclarations(declarations) {
1346
+ if (!this.carryOver) return null;
1347
+ for (const declaration of declarations) {
1348
+ if (!declaration?.name) continue;
1349
+ if (declaration.kind === "prop") {
1350
+ this.addParm(declaration.name, {
1351
+ value: declaration.defaultValue,
1352
+ type: declaration.type
1353
+ });
1354
+ } else {
1355
+ this.param(declaration.name, declaration.defaultValue, {
1356
+ ...declaration.type ? { type: declaration.type } : {}
1357
+ });
1358
+ }
1359
+ }
1360
+ return this.finishParameterCarryOver();
1361
+ }
1362
+ /**
1363
+ * Close the reconciliation: anything the new definition never declared is
1364
+ * dropped from the node and reported with the value it held.
1365
+ */
1366
+ finishParameterCarryOver() {
1367
+ const carry = this.carryOver;
1368
+ if (!carry) return null;
1369
+ for (const entry of carry.carried.values()) {
1370
+ carry.report.dropped.push({
1371
+ name: entry.name,
1372
+ value: entry.value,
1373
+ ...entry.expression ? { expression: entry.expression } : {}
1374
+ });
1375
+ const index = this.parameters.findIndex((p) => p.name === entry.name);
1376
+ if (index >= 0) {
1377
+ if (this.parameters[index].promoted) this.setParameterPromoted(entry.name, false);
1378
+ const current = this.parameters.findIndex((p) => p.name === entry.name);
1379
+ if (current >= 0) this.parameters.splice(current, 1);
1380
+ }
1381
+ if (!carry.resolved.has(`prop:${entry.name}`)) delete this.props[entry.name];
1382
+ }
1383
+ this.carryOver = null;
1384
+ this.lastCarryOverReport = carry.report;
1385
+ if (carry.report.dropped.length > 0) {
1386
+ this.props = { ...this.props };
1387
+ this.updateTimeDependent();
1388
+ _Node.onPropParamsChanged?.(this.id);
1389
+ }
1390
+ return carry.report;
1391
+ }
1392
+ /** Called from param() for every declaration the new definition makes. */
1393
+ reconcileParameter(parameter, defaultValue, options) {
1394
+ const carry = this.carryOver;
1395
+ if (!carry) return;
1396
+ const key = `parameter:${parameter.name}`;
1397
+ if (carry.resolved.has(key)) return;
1398
+ carry.resolved.add(key);
1399
+ const declared = options.type ?? inferParamType(defaultValue);
1400
+ parameter.dataType = declared;
1401
+ const declaredType = carryType(declared);
1402
+ const crossKey = `prop:${parameter.name}`;
1403
+ const carried = carry.carried.get(key) ?? carry.carried.get(crossKey);
1404
+ if (!carried) {
1405
+ parameter.value = defaultValue;
1406
+ carry.report.defaulted.push(parameter.name);
1407
+ return;
1408
+ }
1409
+ carry.carried.delete(key);
1410
+ carry.carried.delete(crossKey);
1411
+ if (carryTypesMatch(carried.type, declaredType)) {
1412
+ parameter.value = carried.value;
1413
+ if (carried.expression || carried.channel) {
1414
+ this.props[parameter.name] = {
1415
+ ...this.props[parameter.name] ?? {},
1416
+ value: carried.value,
1417
+ ...carried.expression ? { expression: carried.expression } : {},
1418
+ ...carried.channel ? { channel: carried.channel } : {}
1419
+ };
1420
+ carry.resolved.add(crossKey);
1421
+ this.updateTimeDependent();
1422
+ }
1423
+ carry.report.kept.push(parameter.name);
1424
+ } else {
1425
+ parameter.value = defaultValue;
1426
+ carry.report.retyped.push({ name: parameter.name, from: carried.type, to: declaredType });
1427
+ }
1428
+ }
1429
+ /** Called from addParm() for every prop the new definition declares. */
1430
+ reconcileProp(name) {
1431
+ const carry = this.carryOver;
1432
+ if (!carry) return;
1433
+ const key = `prop:${name}`;
1434
+ if (carry.resolved.has(key)) return;
1435
+ carry.resolved.add(key);
1436
+ const prop = this.props[name];
1437
+ const declaredType = carryType(prop?.type);
1438
+ const crossKey = `parameter:${name}`;
1439
+ const carried = carry.carried.get(key) ?? carry.carried.get(crossKey);
1440
+ if (!carried) {
1441
+ carry.report.defaulted.push(name);
1442
+ return;
1443
+ }
1444
+ carry.carried.delete(key);
1445
+ carry.carried.delete(crossKey);
1446
+ if (carryTypesMatch(carried.type, declaredType)) {
1447
+ this.props[name] = {
1448
+ ...prop,
1449
+ value: carried.value,
1450
+ ...carried.expression ? { expression: carried.expression } : {},
1451
+ ...carried.channel ? { channel: carried.channel } : {}
1452
+ };
1453
+ if (carried.expression || carried.channel) this.updateTimeDependent();
1454
+ carry.report.kept.push(name);
1455
+ } else {
1456
+ carry.report.retyped.push({ name, from: carried.type, to: declaredType });
1457
+ }
1458
+ }
1459
+ // ============ Variadic Inputs ============
1460
+ setVariadic(defaultValue = null, options = {}) {
1461
+ this.variadic = true;
1462
+ this.variadicDefault = defaultValue;
1463
+ this.variadicOptions = options;
1464
+ this.syncVariadicPorts();
1465
+ }
1466
+ getVariadicInputs() {
1467
+ return this.inputs.filter((p) => p.variadic && !p.options.hidden);
1468
+ }
1469
+ syncVariadicPorts() {
1470
+ if (!this.variadic) return;
1471
+ const existingPorts = this.inputs.filter((p) => p.variadic);
1472
+ const usedCount = existingPorts.filter(
1473
+ (p) => p.connections.length > 0 || p.value !== null && p.value !== this.variadicDefault
1474
+ ).length;
1475
+ for (let i = 0; i < usedCount + 1; i++) {
1476
+ const port = this.in(`input_${i}`, this.variadicDefault, {
1477
+ ...this.variadicOptions,
1478
+ hidden: false
1479
+ });
1480
+ port.variadic = true;
1481
+ if (!port.onChange) {
1482
+ port.onChange = () => this.onUpdate?.();
1483
+ }
1484
+ }
1485
+ for (let i = usedCount + 1; i < existingPorts.length; i++) {
1486
+ if (existingPorts[i]) existingPorts[i].options.hidden = true;
1487
+ }
1488
+ }
1489
+ // ============ Path System ============
1490
+ /**
1491
+ * Get the full path of this node from the root
1492
+ * e.g., "/effects/blur1"
1493
+ */
1494
+ path() {
1495
+ const segments = [this.id];
1496
+ let current = this.parent;
1497
+ while (current) {
1498
+ segments.unshift(current.id);
1499
+ current = current.parent;
1500
+ }
1501
+ return "/" + segments.join("/");
1502
+ }
1503
+ /**
1504
+ * Get child nodes (for subnet types)
1505
+ * Override in SubnetNode to return actual children
1506
+ */
1507
+ children() {
1508
+ return this._children;
1509
+ }
1510
+ /**
1511
+ * Whether this node can contain children.
1512
+ *
1513
+ * Two sources, and both are legitimate: a subclass that overrides this
1514
+ * (`SubnetNode`), and a definition that declares a container. The class
1515
+ * route came first and stays, because the class nodes are a third authoring
1516
+ * style that is not going anywhere yet.
1517
+ */
1518
+ isNetwork() {
1519
+ return this.declaredContainer === "subnet";
1520
+ }
1521
+ /**
1522
+ * Find a node by relative path from this node
1523
+ * Supports: "child", "../sibling", "./self", "../../parent/child"
1524
+ */
1525
+ node(relativePath) {
1526
+ if (!relativePath) return null;
1527
+ if (relativePath.startsWith("/")) {
1528
+ return this.graph.nodeByPath(relativePath);
1529
+ }
1530
+ const segments = relativePath.split("/").filter((s) => s.length > 0);
1531
+ let current = this;
1532
+ for (const segment of segments) {
1533
+ if (!current) return null;
1534
+ if (segment === ".") {
1535
+ continue;
1536
+ }
1537
+ if (segment === "..") {
1538
+ current = current.parent;
1539
+ continue;
1540
+ }
1541
+ if (current.isNetwork()) {
1542
+ const child = current.children().find((c) => c.id === segment);
1543
+ if (child) {
1544
+ current = child;
1545
+ continue;
1546
+ }
1547
+ }
1548
+ return null;
1549
+ }
1550
+ return current;
1551
+ }
1552
+ /**
1553
+ * Compute relative path from this node to another node
1554
+ */
1555
+ relativePathTo(other) {
1556
+ const thisPath = this.path().split("/").filter((s) => s);
1557
+ const otherPath = other.path().split("/").filter((s) => s);
1558
+ let commonLength = 0;
1559
+ while (commonLength < thisPath.length && commonLength < otherPath.length && thisPath[commonLength] === otherPath[commonLength]) {
1560
+ commonLength++;
1561
+ }
1562
+ const ups = thisPath.length - commonLength;
1563
+ const downs = otherPath.slice(commonLength);
1564
+ const parts = [];
1565
+ for (let i = 0; i < ups; i++) {
1566
+ parts.push("..");
1567
+ }
1568
+ parts.push(...downs);
1569
+ return parts.join("/") || ".";
1570
+ }
1571
+ /**
1572
+ * Get nodes connected to this node's inputs
1573
+ */
1574
+ inputNodes() {
1575
+ const nodes = [];
1576
+ for (const input of this.inputs) {
1577
+ for (const conn of input.connections) {
1578
+ const node = this.graph.getNode(conn.from.nodeId);
1579
+ if (node && !nodes.includes(node)) {
1580
+ nodes.push(node);
1581
+ }
1582
+ }
1583
+ }
1584
+ return nodes;
1585
+ }
1586
+ /**
1587
+ * Get nodes connected to this node's outputs
1588
+ */
1589
+ outputNodes() {
1590
+ const nodes = [];
1591
+ for (const output of this.outputs) {
1592
+ for (const conn of output.connections) {
1593
+ const node = this.graph.getNode(conn.to.nodeId);
1594
+ if (node && !nodes.includes(node)) {
1595
+ nodes.push(node);
1596
+ }
1597
+ }
1598
+ }
1599
+ return nodes;
1600
+ }
1601
+ // ============ Pattern Matching (Globbing) ============
1602
+ /**
1603
+ * Convert a glob pattern to a regular expression
1604
+ * Supports: * (any chars), ? (single char), [abc] (char set), [!abc] (negated set)
1605
+ */
1606
+ globToRegex(pattern) {
1607
+ let regex = "";
1608
+ let i = 0;
1609
+ while (i < pattern.length) {
1610
+ const char = pattern[i];
1611
+ switch (char) {
1612
+ case "*":
1613
+ regex += ".*";
1614
+ break;
1615
+ case "?":
1616
+ regex += ".";
1617
+ break;
1618
+ case "[":
1619
+ let j = i + 1;
1620
+ let classContent = "";
1621
+ if (pattern[j] === "!") {
1622
+ classContent = "^";
1623
+ j++;
1624
+ }
1625
+ while (j < pattern.length && pattern[j] !== "]") {
1626
+ classContent += pattern[j];
1627
+ j++;
1628
+ }
1629
+ regex += "[" + classContent + "]";
1630
+ i = j;
1631
+ break;
1632
+ case ".":
1633
+ case "+":
1634
+ case "^":
1635
+ case "$":
1636
+ case "(":
1637
+ case ")":
1638
+ case "{":
1639
+ case "}":
1640
+ case "|":
1641
+ case "\\":
1642
+ regex += "\\" + char;
1643
+ break;
1644
+ default:
1645
+ regex += char;
1646
+ }
1647
+ i++;
1648
+ }
1649
+ return new RegExp("^" + regex + "$");
1650
+ }
1651
+ /**
1652
+ * Match direct children against a pattern
1653
+ * Pattern syntax:
1654
+ * * - match any characters
1655
+ * ? - match single character
1656
+ * [abc] - match character set
1657
+ * [!abc] - exclude character set
1658
+ * ^pattern - exclude matches (use with space: "* ^backup*")
1659
+ */
1660
+ glob(pattern) {
1661
+ const children = this.children();
1662
+ if (!pattern || pattern === "*") {
1663
+ return [...children];
1664
+ }
1665
+ const parts = pattern.split(/\s+/);
1666
+ const includePatterns = [];
1667
+ const excludePatterns = [];
1668
+ for (const part of parts) {
1669
+ if (part.startsWith("^")) {
1670
+ excludePatterns.push(this.globToRegex(part.slice(1)));
1671
+ } else {
1672
+ includePatterns.push(this.globToRegex(part));
1673
+ }
1674
+ }
1675
+ return children.filter((child) => {
1676
+ const included = includePatterns.length === 0 || includePatterns.some((regex) => regex.test(child.id));
1677
+ const excluded = excludePatterns.some((regex) => regex.test(child.id));
1678
+ return included && !excluded;
1679
+ });
1680
+ }
1681
+ /**
1682
+ * Recursively match all descendants against a pattern
1683
+ * Pattern syntax same as glob(), plus:
1684
+ * ** - match at any depth (prefix with **\/ to find nodes at any level)
1685
+ */
1686
+ recursiveGlob(pattern) {
1687
+ const results = [];
1688
+ const isAnyDepth = pattern.startsWith("**/");
1689
+ const searchPattern = isAnyDepth ? pattern.slice(3) : pattern;
1690
+ const search = (node, depth) => {
1691
+ const children = node.children();
1692
+ for (const child of children) {
1693
+ if (isAnyDepth) {
1694
+ if (this.globToRegex(searchPattern).test(child.id)) {
1695
+ results.push(child);
1696
+ }
1697
+ } else if (depth === 0) {
1698
+ if (this.globToRegex(searchPattern).test(child.id)) {
1699
+ results.push(child);
1700
+ }
1701
+ }
1702
+ if (child.isNetwork()) {
1703
+ search(child, isAnyDepth ? depth : depth - 1);
1704
+ }
1705
+ }
1706
+ };
1707
+ if (pattern === "**") {
1708
+ const collectAll = (node) => {
1709
+ for (const child of node.children()) {
1710
+ results.push(child);
1711
+ if (child.isNetwork()) {
1712
+ collectAll(child);
1713
+ }
1714
+ }
1715
+ };
1716
+ collectAll(this);
1717
+ return results;
1718
+ }
1719
+ const pathDepth = isAnyDepth ? 0 : pattern.split("/").length - 1;
1720
+ search(this, pathDepth);
1721
+ return results;
1722
+ }
1723
+ // ============ Behavior Toggles ============
1724
+ setBypass(value) {
1725
+ this.bypass = value;
1726
+ this.markDirty();
1727
+ }
1728
+ setCook(value) {
1729
+ if (value && this.parent) {
1730
+ this.parent.children().filter((n) => n !== this && n.cook).forEach((n) => n.setCook(false));
1731
+ }
1732
+ this.cook = value;
1733
+ if (value) this.graph.cookingNodes.add(this);
1734
+ else this.graph.cookingNodes.delete(this);
1735
+ this.markDirty();
1736
+ }
1737
+ /**
1738
+ * Set time dependency flag (called by expression compiler)
1739
+ * Time-dependent nodes are marked dirty on each frame
1740
+ */
1741
+ setTimeDependent(value) {
1742
+ this.isTimeDependent = value;
1743
+ }
1744
+ /**
1745
+ * Re-derive time dependency from every binding on the node.
1746
+ *
1747
+ * A keyed parameter is time-dependent for exactly the reason a
1748
+ * time-referencing expression is: its value changes with the frame, so a
1749
+ * per-frame cook has to recook the subgraph it feeds. One key is enough —
1750
+ * the value it holds is still a function of the frame everywhere else.
1751
+ */
1752
+ updateTimeDependent() {
1753
+ const hasTimeRef = Object.values(this.props).some(
1754
+ (prop) => prop.expression && expressionEngine.hasTimeReference(prop.expression) || !isEmptyChannel(prop.channel)
1755
+ );
1756
+ this.setTimeDependent(hasTimeRef);
1757
+ }
1758
+ shouldExecute() {
1759
+ if (this.bypass) return false;
1760
+ if (this.graph.cookingNodes.size > 0) {
1761
+ return this.cook || this.graph.isDownstreamOfCooking(this);
1762
+ }
1763
+ return true;
1764
+ }
1765
+ executeBypass() {
1766
+ this.inputs.forEach((input) => {
1767
+ if (input.portType === "param") {
1768
+ const matchingOutput = this.outputs.find((out) => out.name === input.name || this.outputs.length === 1);
1769
+ if (matchingOutput) matchingOutput.setValue(input.value);
1770
+ }
1771
+ });
1772
+ }
1773
+ // ============ Dirty Tracking ============
1774
+ markDirty() {
1775
+ this.graph.scheduler.markStale(this);
1776
+ }
1777
+ /**
1778
+ * The signal for the cook in flight, or null outside one.
1779
+ *
1780
+ * **Until 2026-09-09 Studio handed every node `new AbortController().signal`
1781
+ * and never aborted it**, so `context.signal` was inert: a node that checked
1782
+ * it — `series-source` does — could not be cancelled, and every long node
1783
+ * ran to completion whether or not its result was still wanted. The
1784
+ * generation check in `execute` then discarded the result, which is the
1785
+ * expensive half in the wrong order: measured on `cloud-volumes`, an
1786
+ * abandoned cook had already encoded and uploaded a 1.55 MB PNG before
1787
+ * anything noticed it was superseded.
1788
+ *
1789
+ * So the signal is real, and a node with expensive work owes itself a check
1790
+ * before committing to it. That is the only place preemption can be cheap.
1791
+ */
1792
+ get cookSignal() {
1793
+ for (const controller of this.cookAborts) {
1794
+ if (!controller.signal.aborted) return controller.signal;
1795
+ }
1796
+ return null;
1797
+ }
1798
+ /**
1799
+ * @internal The signal of the newest cook in flight, aborted or not.
1800
+ *
1801
+ * Unlike `cookSignal`, this keeps answering after the cook is aborted, which
1802
+ * is what a stage started late needs: a call made after the supersede must
1803
+ * see an aborted signal and not start, not see no signal and run unguarded.
1804
+ */
1805
+ get inFlightCookSignal() {
1806
+ let latest = null;
1807
+ for (const controller of this.cookAborts) latest = controller;
1808
+ return latest?.signal ?? null;
1809
+ }
1810
+ /** @internal Scheduler-owned invalidation primitive. */
1811
+ invalidateCook() {
1812
+ this.cookGeneration++;
1813
+ this.abortCooks("superseded");
1814
+ this.setCookState("stale");
1815
+ }
1816
+ /**
1817
+ * @internal Abandon whatever this node is doing.
1818
+ *
1819
+ * Separate from `invalidateCook` because the scheduler needs to stop
1820
+ * background work WITHOUT marking it stale — a preempted instance is not
1821
+ * dirty, it is unfinished, and telling the graph it is stale would put it
1822
+ * straight back in the queue it was just taken out of.
1823
+ */
1824
+ cancelCook(reason) {
1825
+ this.abortCooks(reason);
1826
+ }
1827
+ abortCooks(reason) {
1828
+ for (const controller of this.cookAborts) {
1829
+ if (!controller.signal.aborted) controller.abort(new Error(reason));
1830
+ }
1831
+ }
1832
+ /** @internal Scheduler-owned state transition. */
1833
+ setCookState(state) {
1834
+ if (this.cookState === state) return;
1835
+ this.cookState = state;
1836
+ this.graph.scheduler.notifyNodeStateChange();
1837
+ }
1838
+ /**
1839
+ * What the inputs are, exactly: the same key the early cutoff uses. See
1840
+ * `inputFingerprint.ts` for why this no longer samples. A connected input is
1841
+ * its producers' output versions, which move on every commit that is not
1842
+ * provably the same value — O(1) however large the image. A value set
1843
+ * directly on the port has no producer, so it is hashed in full. A
1844
+ * connection to something that keeps no versions (an annotation) falls back
1845
+ * to hashing every input's value.
1846
+ */
1847
+ calculateInputHash() {
1848
+ return this.inputVersionKey() ?? this.inputs.map((input) => valueFingerprint(input.value)).join("|");
1849
+ }
1850
+ get isDirty() {
1851
+ if (!this.hasExecuted || this.cookState !== "clean") return true;
1852
+ return this.calculateInputHash() !== this.lastInputHash;
1853
+ }
1854
+ /**
1855
+ * The last cook threw, and nothing has changed since it did.
1856
+ *
1857
+ * Separate from `isDirty` on purpose, and this is the second attempt: folding
1858
+ * it into `isDirty` closed the Viewer's cook loop and broke a contract the
1859
+ * scheduler tests already pinned — `requestOutput` retries a failed node,
1860
+ * which is the transient case (a fetch that timed out, a device lost) and is
1861
+ * right. The two callers are not the same thing wearing different names. An
1862
+ * explicit request means *give me this output now* and should retry; the
1863
+ * Viewer's `requestAnimationFrame` pump is a **poll** that was reaching the
1864
+ * same entry point every 100 ms, and a poll asking again on identical inputs
1865
+ * cannot learn anything.
1866
+ *
1867
+ * So the fix belongs at the poll, and the poll needs a question only the node
1868
+ * can answer. Measured 2026-09-08 by MW-OBSERVATORY-ART against a live sketch:
1869
+ * selecting a failing node produced 93 errors in 10 s — 9.3/sec, median gap
1870
+ * 103.7 ms, identical stack every time — each one a full `scheduler.flush()`
1871
+ * re-running the failing execute and re-fetching its inputs over the network,
1872
+ * for as long as the node stayed selected. `markDirty` is not on that path,
1873
+ * which is why auditing its call sites came back clean, and the Viewer's
1874
+ * overlap guard is why it held 61 fps and never looked like a hang.
1875
+ *
1876
+ * `hasExecuted` is deliberately not consulted: a node that throws on its very
1877
+ * first cook never sets it, and that is the reproduction case.
1878
+ */
1879
+ get hasSettledFailure() {
1880
+ if (this.cookState !== "error" || this.failedInputHash === null) return false;
1881
+ return this.calculateInputHash() === this.failedInputHash;
1882
+ }
1883
+ /**
1884
+ * Mark all downstream nodes as dirty (lazy propagation)
1885
+ */
1886
+ markDownstreamDirty() {
1887
+ this.graph.scheduler.markDownstreamStale(this);
1888
+ }
1889
+ /**
1890
+ * Request this node's output - triggers lazy evaluation
1891
+ * Executes dirty upstream dependencies first, then this node
1892
+ */
1893
+ async requestOutput() {
1894
+ if (!this.isDirty) return;
1895
+ if (this.cookState === "clean") this.markDirty();
1896
+ await this.graph.scheduler.flush();
1897
+ }
1898
+ // ============ Lifecycle ============
1899
+ setup() {
1900
+ }
1901
+ setFunction(fn) {
1902
+ this.nodeFunction = fn;
1903
+ this.hasExecuted = false;
1904
+ this.markDirty();
1905
+ }
1906
+ async execute() {
1907
+ const hasExecution = !!this.nodeFunction || this.onCook !== _Node.prototype.onCook;
1908
+ if (!hasExecution) {
1909
+ this.hasExecuted = true;
1910
+ this.setCookState("clean");
1911
+ return;
1912
+ }
1913
+ const needsInitialization = !this.hasExecuted;
1914
+ if (!this.shouldExecute() && !needsInitialization && !this.isDirty) {
1915
+ this.executeBypass();
1916
+ this.setCookState("clean");
1917
+ return;
1918
+ }
1919
+ if (!needsInitialization && !this.isDirty) {
1920
+ return;
1921
+ }
1922
+ this.evaluateAllExpressions();
1923
+ const startTime = performance.now();
1924
+ const startMemory = performance.memory?.usedJSHeapSize ?? 0;
1925
+ const generation = this.cookGeneration;
1926
+ const staging = /* @__PURE__ */ new Map();
1927
+ this.stagingStack.push(staging);
1928
+ const abort = new AbortController();
1929
+ this.cookAborts.add(abort);
1930
+ this.setCookState("cooking");
1931
+ let timeoutId;
1932
+ try {
1933
+ await Promise.race([
1934
+ Promise.resolve(this.onCook()),
1935
+ new Promise((_, reject) => {
1936
+ timeoutId = setTimeout(
1937
+ () => reject(new Error(`Node execution timeout after ${this.executionTimeout}ms`)),
1938
+ this.executionTimeout
1939
+ );
1940
+ })
1941
+ ]);
1942
+ if (generation === this.cookGeneration) {
1943
+ await this.callLifecycleHooks(needsInitialization);
1944
+ for (const [port, value] of staging) {
1945
+ this.commitOutputValue(port, value);
1946
+ }
1947
+ for (const output of this.outputs) {
1948
+ if (!staging.has(output)) this.bumpOutputVersion(output.id);
1949
+ }
1950
+ this.cutoffKey = this.inputVersionKey();
1951
+ this.selfDirty = false;
1952
+ this.error = null;
1953
+ this.hasExecuted = true;
1954
+ this.lastInputHash = this.cutoffKey ?? this.calculateInputHash();
1955
+ this.failedInputHash = null;
1956
+ const carryReport = this.finishParameterCarryOver();
1957
+ if (carryReport) _Node.onParametersRetargeted?.(this.id, carryReport);
1958
+ this.setCookState("clean");
1959
+ } else {
1960
+ this.setCookState("stale");
1961
+ }
1962
+ this.updateCookInfo(startTime, startMemory);
1963
+ } catch (thrown) {
1964
+ const err = asError(thrown);
1965
+ if (generation === this.cookGeneration) {
1966
+ this.cutoffKey = null;
1967
+ this.error = err;
1968
+ this.failedInputHash = this.calculateInputHash();
1969
+ this.setCookState("error");
1970
+ } else {
1971
+ this.setCookState("stale");
1972
+ }
1973
+ this.updateCookInfo(startTime, startMemory);
1974
+ const cancelled = err.message.includes("timeout") || generation !== this.cookGeneration;
1975
+ if (!cancelled) {
1976
+ console.error(`Error executing node ${this.id}: ${err.message}`, err);
1977
+ }
1978
+ } finally {
1979
+ if (timeoutId !== void 0) clearTimeout(timeoutId);
1980
+ const index = this.stagingStack.indexOf(staging);
1981
+ if (index >= 0) this.stagingStack.splice(index, 1);
1982
+ this.cookAborts.delete(abort);
1983
+ }
1984
+ }
1985
+ /**
1986
+ * Override this method in class-based nodes to implement cooking logic.
1987
+ * Base implementation calls nodeFunction if set (for code-based nodes).
1988
+ * Cook timing is handled automatically by execute().
1989
+ */
1990
+ onCook() {
1991
+ if (this.nodeFunction) {
1992
+ return this.nodeFunction(this, this.graph);
1993
+ }
1994
+ }
1995
+ /**
1996
+ * Update cook info after execution
1997
+ * Protected so subclasses can call it for manual cook tracking
1998
+ */
1999
+ updateCookInfo(startTime, startMemory = 0) {
2000
+ const endTime = performance.now();
2001
+ const cookTime = endTime - startTime;
2002
+ this.cookInfo.lastCookTime = cookTime;
2003
+ this.cookInfo.totalCookTime += cookTime;
2004
+ this.cookInfo.cookCount++;
2005
+ this.cookInfo.averageCookTime = this.cookInfo.totalCookTime / this.cookInfo.cookCount;
2006
+ this.cookInfo.lastCookTimestamp = Date.now();
2007
+ const endMemory = performance.memory?.usedJSHeapSize ?? 0;
2008
+ if (startMemory && endMemory) {
2009
+ const memoryUsed = endMemory - startMemory;
2010
+ if (memoryUsed > this.cookInfo.peakMemory) {
2011
+ this.cookInfo.peakMemory = memoryUsed;
2012
+ }
2013
+ }
2014
+ }
2015
+ /**
2016
+ * Start cook timing - returns start time to pass to endCook()
2017
+ * Use for class-based nodes that don't go through execute()
2018
+ */
2019
+ startCook() {
2020
+ return performance.now();
2021
+ }
2022
+ /**
2023
+ * End cook timing - call with the value returned from startCook()
2024
+ */
2025
+ endCook(startTime) {
2026
+ this.updateCookInfo(startTime, 0);
2027
+ }
2028
+ /**
2029
+ * Reset cook statistics
2030
+ */
2031
+ resetCookInfo() {
2032
+ this.cookInfo = {
2033
+ lastCookTime: 0,
2034
+ totalCookTime: 0,
2035
+ cookCount: 0,
2036
+ averageCookTime: 0,
2037
+ lastCookTimestamp: 0,
2038
+ peakMemory: 0
2039
+ };
2040
+ }
2041
+ async callLifecycleHooks(isFirstRun) {
2042
+ const isBrowserAPIError = (err) => {
2043
+ const msg = err?.message || String(err);
2044
+ return msg.includes("document is not defined") || msg.includes("window is not defined") || msg.includes("HTMLCanvasElement") || msg.includes("HTMLImageElement");
2045
+ };
2046
+ if (isFirstRun && this.onSetup) {
2047
+ try {
2048
+ await this.onSetup();
2049
+ } catch (err) {
2050
+ if (!isBrowserAPIError(err)) console.error(`Error in onSetup for ${this.id}:`, err);
2051
+ }
2052
+ }
2053
+ if (this.onUpdate) {
2054
+ try {
2055
+ await this.onUpdate();
2056
+ } catch (err) {
2057
+ if (!isBrowserAPIError(err)) console.error(`Error in onUpdate for ${this.id}:`, err);
2058
+ }
2059
+ }
2060
+ if (this.onRender) {
2061
+ try {
2062
+ await this.onRender();
2063
+ } catch (err) {
2064
+ if (!isBrowserAPIError(err)) console.error(`Error in onRender for ${this.id}:`, err);
2065
+ }
2066
+ }
2067
+ if (isFirstRun && this.onReady) {
2068
+ try {
2069
+ this.onReady();
2070
+ } catch (err) {
2071
+ if (!isBrowserAPIError(err)) console.error(`Error in onReady for ${this.id}:`, err);
2072
+ }
2073
+ }
2074
+ }
2075
+ // ============ Rename ============
2076
+ rename(newId) {
2077
+ if (!newId?.trim()) return this.id;
2078
+ const sanitizedId = newId.trim().replace(/\s+/g, "");
2079
+ if (!sanitizedId) return this.id;
2080
+ const uniqueId = this.graph.generateUniqueNodeId(sanitizedId, this.id);
2081
+ if (this.graph.renameElement(this.id, uniqueId)) this.markDirty();
2082
+ return this.id;
2083
+ }
2084
+ isPreviewValue(value) {
2085
+ return typeof HTMLCanvasElement !== "undefined" && value instanceof HTMLCanvasElement || typeof HTMLImageElement !== "undefined" && value instanceof HTMLImageElement;
2086
+ }
2087
+ // ============ Package Manager ============
2088
+ async require(packageName, version) {
2089
+ if (!this.graph.packageManager) throw new Error("PackageManager not initialized");
2090
+ return this.graph.packageManager.load(packageName, version);
2091
+ }
2092
+ get assets() {
2093
+ return this.graph.assetManager;
2094
+ }
2095
+ // ============ Utilities ============
2096
+ log(...args) {
2097
+ console.log(`[${this.id}]`, ...args);
2098
+ }
2099
+ resetPortTracking() {
2100
+ this.portsUsedDuringSetup.clear();
2101
+ }
2102
+ // ============ State Preservation ============
2103
+ preserveState() {
2104
+ return {
2105
+ inputs: this.inputs.map((p) => ({ id: p.id, name: p.name, value: p.value, connections: p.connections.map((c) => c.id) })),
2106
+ outputs: this.outputs.map((p) => ({ id: p.id, name: p.name, value: p.value, connections: p.connections.map((c) => c.id) })),
2107
+ props: Object.fromEntries(Object.entries(this.props).map(([k, p]) => [k, p.value])),
2108
+ bypass: this.bypass,
2109
+ cook: this.cook
2110
+ };
2111
+ }
2112
+ restoreState(state) {
2113
+ state.inputs?.forEach((s) => {
2114
+ const port = this.inputs.find((p) => p.id === s.id || p.name === s.name);
2115
+ if (port) port.value = s.value;
2116
+ });
2117
+ state.outputs?.forEach((s) => {
2118
+ const port = this.outputs.find((p) => p.id === s.id || p.name === s.name);
2119
+ if (port) port.value = s.value;
2120
+ });
2121
+ if (state.props) {
2122
+ Object.entries(state.props).forEach(([k, v]) => {
2123
+ if (this.props[k]) this.props[k].value = v;
2124
+ });
2125
+ }
2126
+ if (state.bypass !== void 0) this.setBypass(state.bypass);
2127
+ if (state.cook !== void 0) this.setCook(state.cook);
2128
+ }
2129
+ // ============ Port Cleanup ============
2130
+ cleanupUnusedPorts() {
2131
+ const used = this.portsUsedDuringSetup;
2132
+ this.inputs = this.inputs.filter((port) => {
2133
+ if (!used.has(`input_${port.name}`)) {
2134
+ port.connections.forEach((c) => this.graph.disconnect(c.id));
2135
+ this._portMap.delete(port.id);
2136
+ return false;
2137
+ }
2138
+ return true;
2139
+ });
2140
+ this.outputs = this.outputs.filter((port) => {
2141
+ if (!used.has(`output_${port.name}`)) {
2142
+ port.connections.forEach((c) => this.graph.disconnect(c.id));
2143
+ this._portMap.delete(port.id);
2144
+ return false;
2145
+ }
2146
+ return true;
2147
+ });
2148
+ this.portsUsedDuringSetup.clear();
2149
+ }
2150
+ // ============ Serialization ============
2151
+ toJSON() {
2152
+ const fullType = typeToPackagePath(this.type);
2153
+ const result = {
2154
+ id: this.id,
2155
+ type: fullType,
2156
+ position: [this.position.x, this.position.y]
2157
+ };
2158
+ if (!isStandardLibraryNode(fullType) && this.code) {
2159
+ result.code = this.code;
2160
+ }
2161
+ if (this.comment?.trim()) result.comment = this.comment;
2162
+ const props = Object.fromEntries(
2163
+ Object.entries(this.props).map(([k, p]) => {
2164
+ let v = p.value;
2165
+ if (p.type === "color" && isColorValue(v)) {
2166
+ const n = normalizeColor(v);
2167
+ v = [n.r, n.g, n.b, n.a ?? 1];
2168
+ }
2169
+ if (p.expression) {
2170
+ return [k, { value: v, expression: p.expression }];
2171
+ }
2172
+ return [k, v];
2173
+ })
2174
+ );
2175
+ if (Object.keys(props).length > 0) result.props = props;
2176
+ if (this.bypass) result.bypass = true;
2177
+ if (this.cook) result.cook = true;
2178
+ return result;
2179
+ }
2180
+ };
2181
+
2182
+ export {
2183
+ registerNodeMetadata,
2184
+ getNodeDisplayName,
2185
+ registerNodeClasses,
2186
+ packagePathToType,
2187
+ isStandardLibraryNode,
2188
+ getNodeClass,
2189
+ normalizeColor,
2190
+ isColorValue,
2191
+ CORE_TYPES2 as CORE_TYPES,
2192
+ isCoreType,
2193
+ normalizeType,
2194
+ canConnect,
2195
+ Node
2196
+ };
2197
+ //# sourceMappingURL=chunk-CTPLRSWF.js.map