@field/cascade 0.7.1 → 0.10.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 (117) hide show
  1. package/AGENTS.md +17 -1
  2. package/CHANGELOG.md +260 -0
  3. package/DESIGN.md +14 -0
  4. package/README.md +41 -25
  5. package/dist/assets/CodeEditor-DSZBpzdy.js +623 -0
  6. package/dist/assets/CodePanel-sY6sZa7B.js +2 -0
  7. package/dist/assets/CustomNodeDialog-BRKnqZhp.js +1 -0
  8. package/dist/assets/{ExportDialog-BJu2Bpvv.js → ExportDialog-C7dVGua4.js} +2 -2
  9. package/dist/assets/OpenGraphDialog-ZsgL-iwr.js +1 -0
  10. package/dist/assets/ProjectSettings-Cw17nNe3.js +2 -0
  11. package/dist/assets/SettingsDialog-p25h3tj0.js +1 -0
  12. package/dist/assets/VersionHistoryDialog-DzFLUotB.js +3 -0
  13. package/dist/assets/{core-DlkL34JF.js → core-BICrEt0y.js} +8 -8
  14. package/dist/assets/{cssMode-DaSSFZYi.js → cssMode-D7TXw57I.js} +1 -1
  15. package/dist/assets/definitions-DUww2Rt-.js +1 -0
  16. package/dist/assets/{editor.api-CHUMRaMm.js → editor.api-DvkCO3ko.js} +1 -1
  17. package/dist/assets/{freemarker2-DQ604q2A.js → freemarker2-BA2lxsF1.js} +1 -1
  18. package/dist/assets/geo-C2RHOVHC.js +1 -0
  19. package/dist/assets/{handlebars-09pcWz8x.js → handlebars-BcDiyPd8.js} +1 -1
  20. package/dist/assets/{html-DtIKrZiD.js → html-C7O4TyhX.js} +1 -1
  21. package/dist/assets/{htmlMode-DgJ_4m1_.js → htmlMode-WaDGF3Mj.js} +1 -1
  22. package/dist/assets/{image-CtUE4_I-.js → image-DR4oeyJB.js} +3 -3
  23. package/dist/assets/index-7vae6YFC.css +1 -0
  24. package/dist/assets/index-DyvDC6pS.js +219 -0
  25. package/dist/assets/index-client-DqYmKstU.js +1 -0
  26. package/dist/assets/{javascript-kbIBYYGo.js → javascript-CpIEK7tz.js} +1 -1
  27. package/dist/assets/{jsonMode-DblCG64t.js → jsonMode-B2FltW0f.js} +1 -1
  28. package/dist/assets/legacy-CTEeXeqF.js +2 -0
  29. package/dist/assets/{liquid-D-qdTGhQ.js → liquid-CN6FFHZE.js} +1 -1
  30. package/dist/assets/{lspLanguageFeatures-Bc6azcFQ.js → lspLanguageFeatures-QhV9XtLp.js} +1 -1
  31. package/dist/assets/math-DaDEgWKt.js +1 -0
  32. package/dist/assets/{mdx-TXg4xsyD.js → mdx-DE89pafV.js} +1 -1
  33. package/dist/assets/pop-pLH7L0PW.js +1 -0
  34. package/dist/assets/{python-C4SGyXJW.js → python-ByTllTB9.js} +1 -1
  35. package/dist/assets/{razor-BZ-lEgY9.js → razor-USYlRRCE.js} +1 -1
  36. package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
  37. package/dist/assets/{tsMode-BoaO1D7x.js → tsMode-BXpN8iUz.js} +1 -1
  38. package/dist/assets/{typescript-D87dYk9k.js → typescript-DInBjGQQ.js} +1 -1
  39. package/dist/assets/{workers-ZrST7klJ.js → workers-C7Ye5B7D.js} +1 -1
  40. package/dist/assets/{xml-DT6r4F7G.js → xml-U0Uocqyo.js} +1 -1
  41. package/dist/assets/{yaml-DxmQ8XYb.js → yaml-CBw_PVYf.js} +1 -1
  42. package/dist/cli/index.js +20070 -3487
  43. package/dist/cli/runtime/stage.ts +70 -10
  44. package/dist/contracts/bundle.d.ts +75 -0
  45. package/dist/contracts/bundle.js +112 -0
  46. package/dist/contracts/definition.d.ts +67 -4
  47. package/dist/contracts/document.d.ts +29 -0
  48. package/dist/contracts/editor.d.ts +172 -0
  49. package/dist/contracts/editor.js +2 -0
  50. package/dist/contracts/index.d.ts +2 -0
  51. package/dist/contracts/index.js +2 -0
  52. package/dist/contracts/validate.js +59 -3
  53. package/dist/contracts/values.d.ts +3 -3
  54. package/dist/contracts/values.js +1 -0
  55. package/dist/index.html +6 -4
  56. package/dist/player/app.js +17 -5
  57. package/dist/player/editor.js +1894 -0
  58. package/dist/player/editorEngine.js +477 -0
  59. package/dist/runtime/builtins/core/bundle.d.ts +70 -0
  60. package/dist/runtime/builtins/core/bundle.js +81 -0
  61. package/dist/runtime/builtins/core/index.d.ts +3 -2
  62. package/dist/runtime/builtins/core/index.js +6 -2
  63. package/dist/runtime/builtins/core/routing.d.ts +27 -0
  64. package/dist/runtime/builtins/core/routing.js +28 -0
  65. package/dist/runtime/edit/document.d.ts +94 -0
  66. package/dist/runtime/edit/document.js +194 -0
  67. package/dist/runtime/edit/graphEditor.d.ts +223 -0
  68. package/dist/runtime/edit/graphEditor.js +893 -0
  69. package/dist/runtime/edit/index.d.ts +12 -0
  70. package/dist/runtime/edit/index.js +6 -0
  71. package/dist/runtime/edit/layout.d.ts +50 -0
  72. package/dist/runtime/edit/layout.js +126 -0
  73. package/dist/runtime/edit/operations.d.ts +65 -0
  74. package/dist/runtime/edit/operations.js +57 -0
  75. package/dist/runtime/edit/parameters.d.ts +75 -0
  76. package/dist/runtime/edit/parameters.js +284 -0
  77. package/dist/runtime/edit/read.d.ts +35 -0
  78. package/dist/runtime/edit/read.js +65 -0
  79. package/dist/runtime/editor/index.d.ts +104 -0
  80. package/dist/runtime/editor/index.js +439 -0
  81. package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
  82. package/dist/runtime/expressions/bundlePaths.js +55 -0
  83. package/dist/runtime/expressions/index.d.ts +2 -0
  84. package/dist/runtime/expressions/index.js +1 -0
  85. package/dist/runtime/index.d.ts +2 -0
  86. package/dist/runtime/index.js +2 -0
  87. package/dist/runtime/params/animator.d.ts +5 -0
  88. package/dist/runtime/params/animator.js +16 -0
  89. package/dist/runtime/runtime.js +238 -12
  90. package/dist/runtime/seed.d.ts +29 -0
  91. package/dist/runtime/seed.js +52 -0
  92. package/dist/runtime/types.d.ts +12 -0
  93. package/dist/stage.js +40 -9
  94. package/dist/studio/panel.d.ts +2 -0
  95. package/doc/CLI_REFERENCE.md +100 -0
  96. package/doc/EDITOR_EXPORT.md +226 -0
  97. package/doc/HEADLESS_GPU.md +27 -0
  98. package/doc/MCP.md +53 -0
  99. package/doc/NODE_REFERENCE.md +66 -0
  100. package/doc/PROJECT_AUTHORING.md +137 -0
  101. package/package.json +15 -6
  102. package/dist/assets/CodeEditor-D-N0C2jT.js +0 -623
  103. package/dist/assets/CodePanel-EN411mp5.js +0 -2
  104. package/dist/assets/CustomNodeDialog-6z2n27VV.js +0 -1
  105. package/dist/assets/OpenGraphDialog-fMZjzIR3.js +0 -1
  106. package/dist/assets/ProjectSettings-BQ8PGNqo.js +0 -2
  107. package/dist/assets/SettingsDialog-IbYLm8Bx.js +0 -1
  108. package/dist/assets/VersionHistoryDialog-Bgh-q5Jg.js +0 -3
  109. package/dist/assets/definitions-CQBdsJYJ.js +0 -1
  110. package/dist/assets/geo-DcmMAcqM.js +0 -1
  111. package/dist/assets/index-CnylT98t.js +0 -212
  112. package/dist/assets/index-client-BoJgsN9M.js +0 -1
  113. package/dist/assets/index-ilgbOu6G.css +0 -1
  114. package/dist/assets/legacy-CAj2_7QE.js +0 -2
  115. package/dist/assets/math-CY11tkoN.js +0 -1
  116. package/dist/assets/pop-Dr5-ARqQ.js +0 -1
  117. package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
@@ -0,0 +1,35 @@
1
+ /**
2
+ * A graph as an agent reads it: nodes with their stored parameters, wires in
3
+ * the same `a.out -> b.in` spelling the CLI takes, and each subnet's ports.
4
+ *
5
+ * Read from the document alone, so it answers instantly and shows exactly
6
+ * what is stored — not a cooked value, and not a default the file does not
7
+ * contain. Use `cascade inspect` for definitions.
8
+ */
9
+ import { type EditableDocument } from "./document.js";
10
+ import type { Point } from "./layout.js";
11
+ export interface GraphReadNode {
12
+ readonly id: string;
13
+ readonly module: string;
14
+ readonly parent: string | null;
15
+ readonly position: Point;
16
+ readonly comment?: string;
17
+ readonly props?: Readonly<Record<string, unknown>>;
18
+ readonly inputs?: unknown;
19
+ /** For a subnet: its ports, named `input_<n>` / `output_<n>`, with the label each carries. */
20
+ readonly ports?: {
21
+ readonly inputs: readonly string[];
22
+ readonly outputs: readonly string[];
23
+ };
24
+ }
25
+ export interface GraphRead {
26
+ readonly name: string | null;
27
+ readonly nodes: readonly GraphReadNode[];
28
+ readonly wires: readonly string[];
29
+ /** Ids of the top-level nodes, in file order: the graph's first screen. */
30
+ readonly top: readonly string[];
31
+ }
32
+ export declare function readGraph(document: EditableDocument, options?: {
33
+ in?: string;
34
+ }): GraphRead;
35
+ //# sourceMappingURL=read.d.ts.map
@@ -0,0 +1,65 @@
1
+ /**
2
+ * A graph as an agent reads it: nodes with their stored parameters, wires in
3
+ * the same `a.out -> b.in` spelling the CLI takes, and each subnet's ports.
4
+ *
5
+ * Read from the document alone, so it answers instantly and shows exactly
6
+ * what is stored — not a cooked value, and not a default the file does not
7
+ * contain. Use `cascade inspect` for definitions.
8
+ */
9
+ import { moduleOf, parentOf, positionOf, readWire, subnetPorts, wireLabel, SUBNET } from "./document.js";
10
+ export function readGraph(document, options = {}) {
11
+ const scope = options.in ?? null;
12
+ const inScope = new Set(scope === null ? document.nodes.map((node) => node.id) : descendants(document, scope));
13
+ const nodes = document.nodes.filter((node) => inScope.has(node.id)).map((node) => {
14
+ const children = document.nodes.filter((child) => parentOf(child) === node.id);
15
+ const labelled = (prefix, indexProp, nameProp) => (port) => {
16
+ const boundary = children.find((child) => Number(child.props?.[indexProp]) === port.index && moduleOf(child) === `cascade.core.${prefix}`);
17
+ const label = boundary?.comment || boundary?.props?.[nameProp];
18
+ return label ? `${port.name} (${label})` : port.name;
19
+ };
20
+ const ports = moduleOf(node) === SUBNET ? subnetPorts(children) : null;
21
+ return {
22
+ id: node.id,
23
+ module: moduleOf(node),
24
+ parent: parentOf(node),
25
+ position: positionOf(node),
26
+ ...(typeof node.comment === "string" && node.comment ? { comment: node.comment } : {}),
27
+ ...(node.props && Object.keys(node.props).length ? { props: node.props } : {}),
28
+ ...(node.inputs !== undefined ? { inputs: node.inputs } : {}),
29
+ ...(ports ? {
30
+ ports: {
31
+ inputs: ports.inputs.map(labelled("Input", "inputIndex", "inputName")),
32
+ outputs: ports.outputs.map(labelled("Output", "outputIndex", "outputName")),
33
+ },
34
+ } : {}),
35
+ };
36
+ });
37
+ const wires = (document.connections ?? [])
38
+ .map(readWire)
39
+ .filter((wire) => wire && inScope.has(wire.from.node) && inScope.has(wire.to.node))
40
+ .map((wire) => wireLabel(wire));
41
+ const metadata = document.metadata;
42
+ return {
43
+ name: typeof metadata?.name === "string" ? metadata.name : null,
44
+ nodes,
45
+ wires,
46
+ top: nodes.filter((node) => node.parent === scope).map((node) => node.id),
47
+ };
48
+ }
49
+ function descendants(document, id) {
50
+ if (!document.nodes.some((node) => node.id === id))
51
+ throw new Error(`No node "${id}"`);
52
+ const result = [];
53
+ const queue = [id];
54
+ while (queue.length) {
55
+ const current = queue.shift();
56
+ for (const child of document.nodes) {
57
+ if (parentOf(child) === current) {
58
+ result.push(child.id);
59
+ queue.push(child.id);
60
+ }
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ //# sourceMappingURL=read.js.map
@@ -0,0 +1,104 @@
1
+ /**
2
+ * Published parameters, export subgraphs and scene files.
3
+ *
4
+ * Pure functions over a `CascadeDocument`, shared by `cascade export`, the
5
+ * exported editor and Studio, so the three agree on what a scene means. Nothing
6
+ * here touches a filesystem, a DOM or a network; hosts supply files.
7
+ */
8
+ import { type CascadeDocument, type CascadeDocumentConnection, type CascadeScene, type EditorDeclaration, type JsonValue, type PublishedParameter, type SceneFile, type VisibleWhen } from "@field/cascade/contracts";
9
+ export interface EditorDiagnostic {
10
+ readonly severity: "error" | "warning";
11
+ readonly code: string;
12
+ readonly message: string;
13
+ }
14
+ export declare const SCENE_FORMAT = "cascade.scene";
15
+ /** Node ids at both ends of a connection, in either document spelling. */
16
+ export declare function connectionEnds(connection: CascadeDocumentConnection): {
17
+ source: string;
18
+ sourcePort?: string;
19
+ target: string;
20
+ targetPort?: string;
21
+ };
22
+ /** The editor declaration, or an error saying the graph has none. */
23
+ export declare function requireEditor(document: CascadeDocument): EditorDeclaration;
24
+ /** Static checks of the declaration against the document's own nodes. */
25
+ export declare function validateEditor(document: CascadeDocument): EditorDiagnostic[];
26
+ /**
27
+ * The nodes an export ships: everything the published outputs and parameters
28
+ * depend on, and nothing else. Frozen sources are kept as leaves, so nothing
29
+ * upstream of one is reached. A node inside a subnet brings its ancestors; a
30
+ * subnet brings its contents, which define what its ports carry.
31
+ */
32
+ export declare function selectEditorSubgraph(document: CascadeDocument): Set<string>;
33
+ /**
34
+ * The compatibility version a scene records: the editor's id, its parameter
35
+ * interface and the structure of the exported subgraph. Values are left out,
36
+ * so tuning a prop in Studio does not orphan the scenes made against it, while
37
+ * rewiring or renaming a published parameter does.
38
+ */
39
+ export declare function editorGraphVersion(document: CascadeDocument): string;
40
+ /** The value a published parameter currently holds in the document. */
41
+ export declare function publishedValues(document: CascadeDocument): Record<string, JsonValue | undefined>;
42
+ export interface CreateSceneOptions {
43
+ readonly name?: string;
44
+ /** Values to record; parameters not given take the document's current value. */
45
+ readonly values?: Readonly<Record<string, JsonValue | undefined>>;
46
+ readonly files?: Readonly<Record<string, SceneFile>>;
47
+ readonly savedAt?: string;
48
+ }
49
+ /** A scene holding published values only: no props, nodes or project data. */
50
+ export declare function createScene(document: CascadeDocument, options?: CreateSceneOptions): CascadeScene;
51
+ /** Parse and shape-check a scene file. Throws on anything that is not one. */
52
+ export declare function parseScene(input: unknown): CascadeScene;
53
+ export interface ApplySceneOptions {
54
+ /**
55
+ * Turn a scene file into the prop value the host can read (an asset ref).
56
+ * Undefined keeps the document's value and reports why.
57
+ */
58
+ readonly resolveFile?: (parameter: PublishedParameter, file: SceneFile) => JsonValue | undefined;
59
+ }
60
+ export interface AppliedScene {
61
+ readonly document: CascadeDocument;
62
+ /** Published id to the value now in the document, after coercion. */
63
+ readonly values: Readonly<Record<string, JsonValue | undefined>>;
64
+ readonly warnings: readonly string[];
65
+ }
66
+ export interface ResolvedScene {
67
+ /** Coerced values for the parameters the scene sets, file parameters excluded. */
68
+ readonly values: Readonly<Record<string, JsonValue>>;
69
+ /** Files the scene carries for known file parameters. */
70
+ readonly files: Readonly<Record<string, SceneFile>>;
71
+ readonly warnings: readonly string[];
72
+ }
73
+ /**
74
+ * What a scene means for this document, before anything is written.
75
+ *
76
+ * Degrades rather than refuses: another graph id or version is a warning,
77
+ * unknown parameters are ignored with a warning, a value of the wrong kind is
78
+ * skipped with a warning, and parameters the scene does not mention are left
79
+ * out, so they keep the graph's default.
80
+ */
81
+ export declare function resolveScene(document: CascadeDocument, sceneInput: CascadeScene | unknown): ResolvedScene;
82
+ /** Write a scene into a copy of the document, by the rules of `resolveScene`. */
83
+ export declare function applyScene(document: CascadeDocument, sceneInput: CascadeScene | unknown, options?: ApplySceneOptions): AppliedScene;
84
+ /** A copy of the document with published parameters set by id. Unknown ids are ignored. */
85
+ export declare function setPublishedValues(document: CascadeDocument, updates: Readonly<Record<string, JsonValue | undefined>>): CascadeDocument;
86
+ /** A parameter's visibility conditions as a list. */
87
+ export declare function conditionsOf(parameter: PublishedParameter): readonly VisibleWhen[];
88
+ /** Whether a parameter is shown, given every parameter's current value. */
89
+ export declare function isVisible(parameter: PublishedParameter, values: Readonly<Record<string, unknown>>): boolean;
90
+ /** Coerce a scene value to what the parameter's control accepts. */
91
+ export declare function coerceValue(parameter: PublishedParameter, value: unknown, current?: unknown): {
92
+ value?: JsonValue;
93
+ warning?: string;
94
+ };
95
+ /**
96
+ * The control a parameter shows: its declaration, else the prop's type when
97
+ * the host knows it, else the shape of its current value.
98
+ */
99
+ export declare function controlOf(parameter: PublishedParameter, current?: unknown, type?: string): string;
100
+ /** Build a scene file record; embeds bytes only when the parameter allows it. */
101
+ export declare function sceneFile(parameter: PublishedParameter, bytes: Uint8Array, name: string, mediaType?: string): Promise<SceneFile>;
102
+ export declare function toBase64(bytes: Uint8Array): string;
103
+ export declare function fromBase64(text: string): Uint8Array;
104
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1,439 @@
1
+ /**
2
+ * Published parameters, export subgraphs and scene files.
3
+ *
4
+ * Pure functions over a `CascadeDocument`, shared by `cascade export`, the
5
+ * exported editor and Studio, so the three agree on what a scene means. Nothing
6
+ * here touches a filesystem, a DOM or a network; hosts supply files.
7
+ */
8
+ import { SCENE_EMBED_LIMIT, } from "@field/cascade/contracts";
9
+ export const SCENE_FORMAT = "cascade.scene";
10
+ /** Web platform globals present in browsers and Node 22, typed here because the runtime builds without DOM types. */
11
+ const platform = globalThis;
12
+ /** Node ids at both ends of a connection, in either document spelling. */
13
+ export function connectionEnds(connection) {
14
+ if (Array.isArray(connection)) {
15
+ const [source, target] = connection;
16
+ return { source: source[0], sourcePort: source[2], target: target[0], targetPort: target[2] };
17
+ }
18
+ const object = connection;
19
+ return {
20
+ source: object.source.nodeId,
21
+ sourcePort: object.source.outputName,
22
+ target: object.target.nodeId,
23
+ targetPort: object.target.inputName,
24
+ };
25
+ }
26
+ /** The editor declaration, or an error saying the graph has none. */
27
+ export function requireEditor(document) {
28
+ const editor = document.editor;
29
+ if (!editor || typeof editor !== "object" || typeof editor.id !== "string" || !editor.id)
30
+ throw new Error('This graph declares no editor. Add an "editor" section with an id (see doc/EDITOR_EXPORT.md).');
31
+ return editor;
32
+ }
33
+ /** Static checks of the declaration against the document's own nodes. */
34
+ export function validateEditor(document) {
35
+ const out = [];
36
+ const error = (code, message) => out.push({ severity: "error", code, message });
37
+ const warn = (code, message) => out.push({ severity: "warning", code, message });
38
+ const editor = document.editor;
39
+ if (!editor)
40
+ return [{ severity: "error", code: "editor/missing", message: "The graph has no editor section" }];
41
+ if (!/^[a-z0-9][a-z0-9._-]*$/i.test(editor.id ?? ""))
42
+ error("editor/id", `editor.id must be a plain identifier, got ${JSON.stringify(editor.id)}`);
43
+ const nodes = new Map(document.nodes.map((node) => [node.id, node]));
44
+ const ids = new Set();
45
+ for (const parameter of editor.parameters ?? []) {
46
+ if (ids.has(parameter.id))
47
+ error("editor/duplicate-parameter", `Parameter id ${parameter.id} is used twice`);
48
+ ids.add(parameter.id);
49
+ if (!nodes.has(parameter.node))
50
+ error("editor/parameter-node", `Parameter ${parameter.id} targets missing node ${parameter.node}`);
51
+ }
52
+ for (const parameter of editor.parameters ?? [])
53
+ for (const condition of conditionsOf(parameter))
54
+ if (!ids.has(condition.param))
55
+ warn("editor/visible-when", `Parameter ${parameter.id} is shown when unknown parameter ${condition.param} matches`);
56
+ const outputIds = new Set();
57
+ let views = 0;
58
+ for (const output of editor.outputs ?? []) {
59
+ if (outputIds.has(output.id))
60
+ error("editor/duplicate-output", `Output id ${output.id} is used twice`);
61
+ outputIds.add(output.id);
62
+ if (!nodes.has(output.node))
63
+ error("editor/output-node", `Output ${output.id} reads missing node ${output.node}`);
64
+ if (output.view)
65
+ views++;
66
+ for (const id of output.parameters ?? [])
67
+ if (!ids.has(id))
68
+ error("editor/output-parameter", `Output ${output.id} shows unknown parameter ${id}`);
69
+ }
70
+ if (!(editor.outputs ?? []).length)
71
+ error("editor/no-outputs", "The editor declares no outputs");
72
+ if (views > 1)
73
+ warn("editor/views", "More than one output sets view; the first is shown");
74
+ for (const preset of editor.presets ?? [])
75
+ for (const key of Object.keys(preset.values ?? {}))
76
+ if (!ids.has(key))
77
+ warn("editor/preset-parameter", `Preset ${preset.id} sets unknown parameter ${key}`);
78
+ for (const id of editor.peek ?? [])
79
+ if (!ids.has(id))
80
+ warn("editor/peek-parameter", `peek names unknown parameter ${id}`);
81
+ for (const id of Object.keys(editor.draft ?? {}))
82
+ if (!ids.has(id))
83
+ warn("editor/draft-parameter", `draft sets unknown parameter ${id}`);
84
+ for (const frozen of editor.freeze ?? [])
85
+ if (!nodes.has(frozen.node))
86
+ error("editor/freeze-node", `Frozen source ${frozen.node} is not in the graph`);
87
+ return out;
88
+ }
89
+ /**
90
+ * The nodes an export ships: everything the published outputs and parameters
91
+ * depend on, and nothing else. Frozen sources are kept as leaves, so nothing
92
+ * upstream of one is reached. A node inside a subnet brings its ancestors; a
93
+ * subnet brings its contents, which define what its ports carry.
94
+ */
95
+ export function selectEditorSubgraph(document) {
96
+ const editor = requireEditor(document);
97
+ const frozen = new Set((editor.freeze ?? []).map((item) => item.node));
98
+ const children = new Map();
99
+ const parents = new Map();
100
+ for (const node of document.nodes) {
101
+ if (!node.parent)
102
+ continue;
103
+ parents.set(node.id, node.parent);
104
+ children.set(node.parent, [...(children.get(node.parent) ?? []), node.id]);
105
+ }
106
+ const upstream = new Map();
107
+ for (const connection of document.connections ?? []) {
108
+ const { source, target } = connectionEnds(connection);
109
+ upstream.set(target, [...(upstream.get(target) ?? []), source]);
110
+ }
111
+ const known = new Set(document.nodes.map((node) => node.id));
112
+ const selected = new Set();
113
+ const queue = [
114
+ ...(editor.outputs ?? []).map((output) => output.node),
115
+ ...(editor.parameters ?? []).map((parameter) => parameter.node),
116
+ ].filter((id) => known.has(id));
117
+ while (queue.length) {
118
+ const id = queue.pop();
119
+ if (selected.has(id))
120
+ continue;
121
+ selected.add(id);
122
+ const parent = parents.get(id);
123
+ if (parent)
124
+ queue.push(parent);
125
+ if (frozen.has(id))
126
+ continue;
127
+ queue.push(...(children.get(id) ?? []), ...(upstream.get(id) ?? []));
128
+ }
129
+ return selected;
130
+ }
131
+ /**
132
+ * The compatibility version a scene records: the editor's id, its parameter
133
+ * interface and the structure of the exported subgraph. Values are left out,
134
+ * so tuning a prop in Studio does not orphan the scenes made against it, while
135
+ * rewiring or renaming a published parameter does.
136
+ */
137
+ export function editorGraphVersion(document) {
138
+ const editor = requireEditor(document);
139
+ const selected = selectEditorSubgraph(document);
140
+ const frozen = new Set((editor.freeze ?? []).map((item) => item.node));
141
+ const nodes = document.nodes
142
+ .filter((node) => selected.has(node.id))
143
+ .map((node) => [node.id, frozen.has(node.id) ? "frozen" : moduleOf(node), node.parent ?? ""])
144
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0])));
145
+ const connections = (document.connections ?? [])
146
+ .map(connectionEnds)
147
+ .filter((end) => selected.has(end.source) && selected.has(end.target) && !frozen.has(end.target))
148
+ // A frozen source's port is renamed when it is replaced, so it does not count.
149
+ .map((end) => `${end.source}.${frozen.has(end.source) ? "" : end.sourcePort ?? ""}>${end.target}.${end.targetPort ?? ""}`)
150
+ .sort();
151
+ const parameters = (editor.parameters ?? [])
152
+ .map((parameter) => [parameter.id, parameter.node, parameter.prop, controlOf(parameter)])
153
+ .sort((a, b) => String(a[0]).localeCompare(String(b[0])));
154
+ return `h${hash(JSON.stringify([editor.id, parameters, nodes, connections]))}`;
155
+ }
156
+ /** The value a published parameter currently holds in the document. */
157
+ export function publishedValues(document) {
158
+ const editor = requireEditor(document);
159
+ const nodes = new Map(document.nodes.map((node) => [node.id, node]));
160
+ const values = {};
161
+ for (const parameter of editor.parameters ?? []) {
162
+ const stored = propsOf(nodes.get(parameter.node))[parameter.prop];
163
+ values[parameter.id] = (isBinding(stored) ? stored.value : stored);
164
+ }
165
+ return values;
166
+ }
167
+ /** A scene holding published values only: no props, nodes or project data. */
168
+ export function createScene(document, options = {}) {
169
+ const editor = requireEditor(document);
170
+ const current = publishedValues(document);
171
+ const values = {};
172
+ const files = {};
173
+ for (const parameter of editor.parameters ?? []) {
174
+ if (controlOf(parameter) === "file") {
175
+ const file = options.files?.[parameter.id];
176
+ if (file)
177
+ files[parameter.id] = parameter.embed === "never" ? withoutData(file) : file;
178
+ continue;
179
+ }
180
+ const value = options.values && parameter.id in options.values ? options.values[parameter.id] : current[parameter.id];
181
+ if (value !== undefined)
182
+ values[parameter.id] = value;
183
+ }
184
+ return {
185
+ format: SCENE_FORMAT,
186
+ version: 1,
187
+ graph: { id: editor.id, version: editorGraphVersion(document) },
188
+ ...(options.name ? { name: options.name } : {}),
189
+ values,
190
+ ...(Object.keys(files).length ? { files } : {}),
191
+ ...(options.savedAt ? { savedAt: options.savedAt } : {}),
192
+ };
193
+ }
194
+ /** Parse and shape-check a scene file. Throws on anything that is not one. */
195
+ export function parseScene(input) {
196
+ const value = typeof input === "string" ? JSON.parse(input) : input;
197
+ if (!value || typeof value !== "object" || Array.isArray(value))
198
+ throw new Error("A scene must be a JSON object");
199
+ const scene = value;
200
+ if (scene.format !== SCENE_FORMAT)
201
+ throw new Error(`Not a Cascade scene (format ${JSON.stringify(scene.format)})`);
202
+ if (scene.version !== 1)
203
+ throw new Error(`Unsupported scene version ${JSON.stringify(scene.version)}`);
204
+ if (!scene.graph || typeof scene.graph.id !== "string")
205
+ throw new Error("A scene must name its graph");
206
+ if (!scene.values || typeof scene.values !== "object" || Array.isArray(scene.values))
207
+ throw new Error("A scene must carry a values object");
208
+ for (const [id, file] of Object.entries(scene.files ?? {})) {
209
+ if (!file || typeof file.name !== "string" || typeof file.sha256 !== "string" || typeof file.size !== "number")
210
+ throw new Error(`Scene file ${id} needs a name, a size and a sha256`);
211
+ if (file.data !== undefined && typeof file.data !== "string")
212
+ throw new Error(`Scene file ${id} data must be base64`);
213
+ }
214
+ return scene;
215
+ }
216
+ /**
217
+ * What a scene means for this document, before anything is written.
218
+ *
219
+ * Degrades rather than refuses: another graph id or version is a warning,
220
+ * unknown parameters are ignored with a warning, a value of the wrong kind is
221
+ * skipped with a warning, and parameters the scene does not mention are left
222
+ * out, so they keep the graph's default.
223
+ */
224
+ export function resolveScene(document, sceneInput) {
225
+ const scene = parseScene(sceneInput);
226
+ const editor = requireEditor(document);
227
+ const warnings = [];
228
+ if (scene.graph.id !== editor.id)
229
+ warnings.push(`Scene was made for "${scene.graph.id}", not "${editor.id}"; matching parameters are applied`);
230
+ else if (scene.graph.version && scene.graph.version !== editorGraphVersion(document))
231
+ warnings.push("Scene was saved against another version of this graph; matching parameters are applied");
232
+ const parameters = new Map((editor.parameters ?? []).map((parameter) => [parameter.id, parameter]));
233
+ for (const id of Object.keys(scene.values))
234
+ if (!parameters.has(id))
235
+ warnings.push(`Unknown parameter "${id}" ignored`);
236
+ for (const id of Object.keys(scene.files ?? {}))
237
+ if (!parameters.has(id))
238
+ warnings.push(`Unknown file parameter "${id}" ignored`);
239
+ const current = publishedValues(document);
240
+ const values = {};
241
+ const files = {};
242
+ for (const parameter of parameters.values()) {
243
+ if (controlOf(parameter, current[parameter.id]) === "file") {
244
+ const file = scene.files?.[parameter.id];
245
+ if (file)
246
+ files[parameter.id] = file;
247
+ continue;
248
+ }
249
+ if (!(parameter.id in scene.values))
250
+ continue;
251
+ const coerced = coerceValue(parameter, scene.values[parameter.id], current[parameter.id]);
252
+ if (coerced.warning)
253
+ warnings.push(coerced.warning);
254
+ if (coerced.value !== undefined)
255
+ values[parameter.id] = coerced.value;
256
+ }
257
+ return { values, files, warnings };
258
+ }
259
+ /** Write a scene into a copy of the document, by the rules of `resolveScene`. */
260
+ export function applyScene(document, sceneInput, options = {}) {
261
+ const editor = requireEditor(document);
262
+ const resolved = resolveScene(document, sceneInput);
263
+ const warnings = [...resolved.warnings];
264
+ const updates = { ...resolved.values };
265
+ for (const parameter of editor.parameters ?? []) {
266
+ const file = resolved.files[parameter.id];
267
+ if (!file)
268
+ continue;
269
+ const value = options.resolveFile?.(parameter, file);
270
+ if (value === undefined)
271
+ warnings.push(`${labelOf(parameter)}: "${file.name}" is not available here; the graph's file is kept`);
272
+ else
273
+ updates[parameter.id] = value;
274
+ }
275
+ const result = setPublishedValues(document, updates);
276
+ return { document: result, values: publishedValues(result), warnings };
277
+ }
278
+ /** A copy of the document with published parameters set by id. Unknown ids are ignored. */
279
+ export function setPublishedValues(document, updates) {
280
+ const editor = requireEditor(document);
281
+ const writes = new Map();
282
+ for (const parameter of editor.parameters ?? []) {
283
+ if (!(parameter.id in updates) || updates[parameter.id] === undefined)
284
+ continue;
285
+ const props = writes.get(parameter.node) ?? {};
286
+ props[parameter.prop] = updates[parameter.id];
287
+ writes.set(parameter.node, props);
288
+ }
289
+ const nodes = document.nodes.map((node) => {
290
+ const props = writes.get(node.id);
291
+ if (!props)
292
+ return node;
293
+ const merged = { ...propsOf(node) };
294
+ for (const [name, value] of Object.entries(props)) {
295
+ const stored = merged[name];
296
+ merged[name] = isBinding(stored) ? { ...stored, value } : value;
297
+ }
298
+ return { ...node, props: merged };
299
+ });
300
+ return { ...document, nodes };
301
+ }
302
+ /** A parameter's visibility conditions as a list. */
303
+ export function conditionsOf(parameter) {
304
+ const condition = parameter.visibleWhen;
305
+ if (!condition)
306
+ return [];
307
+ return Array.isArray(condition) ? condition : [condition];
308
+ }
309
+ /** Whether a parameter is shown, given every parameter's current value. */
310
+ export function isVisible(parameter, values) {
311
+ return conditionsOf(parameter).every((condition) => {
312
+ const value = values[condition.param];
313
+ if (condition.in && !condition.in.some((item) => Object.is(item, value)))
314
+ return false;
315
+ if (condition.not && condition.not.some((item) => Object.is(item, value)))
316
+ return false;
317
+ if ("equals" in condition && !Object.is(condition.equals, value))
318
+ return false;
319
+ return true;
320
+ });
321
+ }
322
+ /** Coerce a scene value to what the parameter's control accepts. */
323
+ export function coerceValue(parameter, value, current) {
324
+ const label = labelOf(parameter);
325
+ const reject = (what) => ({ warning: `${label}: ${what}; kept the graph's value` });
326
+ if (value === undefined)
327
+ return {};
328
+ const control = controlOf(parameter, current);
329
+ switch (control) {
330
+ case "number":
331
+ case "slider":
332
+ return typeof value === "number" && Number.isFinite(value) ? { value } : reject("expected a number");
333
+ case "int":
334
+ case "seed":
335
+ return typeof value === "number" && Number.isFinite(value) ? { value: Math.round(value) } : reject("expected a whole number");
336
+ case "bool":
337
+ return typeof value === "boolean" ? { value } : reject("expected true or false");
338
+ case "text":
339
+ case "textarea":
340
+ return typeof value === "string" ? { value } : reject("expected text");
341
+ case "select": {
342
+ const options = (parameter.options ?? []).map((option) => option !== null && typeof option === "object" && "value" in option ? option.value : option);
343
+ return options.some((option) => Object.is(option, value))
344
+ ? { value: value }
345
+ : reject(`${JSON.stringify(value)} is not one of its options`);
346
+ }
347
+ case "color":
348
+ return typeof value === "string" || (Array.isArray(value) && value.every((part) => typeof part === "number"))
349
+ ? { value: value }
350
+ : reject("expected a colour");
351
+ case "vec2":
352
+ return Array.isArray(value) && value.length === 2 && value.every((part) => typeof part === "number" && Number.isFinite(part))
353
+ ? { value: value }
354
+ : reject("expected two numbers");
355
+ default:
356
+ return { value: value };
357
+ }
358
+ }
359
+ /**
360
+ * The control a parameter shows: its declaration, else the prop's type when
361
+ * the host knows it, else the shape of its current value.
362
+ */
363
+ export function controlOf(parameter, current, type) {
364
+ if (parameter.control)
365
+ return parameter.control;
366
+ if (parameter.options?.length)
367
+ return "select";
368
+ const byType = {
369
+ float: parameter.min !== undefined && parameter.max !== undefined ? "slider" : "number",
370
+ int: "int", bool: "bool", string: "text", color: "color", vec2: "vec2", vec2i: "vec2", image: "file", asset: "file",
371
+ };
372
+ if (type && byType[type])
373
+ return byType[type];
374
+ if (typeof current === "boolean")
375
+ return "bool";
376
+ if (typeof current === "string")
377
+ return "text";
378
+ if (Array.isArray(current) && current.length === 2)
379
+ return "vec2";
380
+ if (current && typeof current === "object" && "path" in current)
381
+ return "file";
382
+ return typeof current === "number" ? "number" : "any";
383
+ }
384
+ /** Build a scene file record; embeds bytes only when the parameter allows it. */
385
+ export async function sceneFile(parameter, bytes, name, mediaType) {
386
+ const digest = await platform.crypto.subtle.digest("SHA-256", bytes);
387
+ const sha256 = Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
388
+ const embed = parameter.embed !== "never" && bytes.byteLength <= SCENE_EMBED_LIMIT;
389
+ return {
390
+ name,
391
+ ...(mediaType ? { mediaType } : {}),
392
+ size: bytes.byteLength,
393
+ sha256,
394
+ ...(embed ? { data: toBase64(bytes) } : {}),
395
+ };
396
+ }
397
+ export function toBase64(bytes) {
398
+ let binary = "";
399
+ for (let index = 0; index < bytes.length; index += 0x8000)
400
+ binary += String.fromCharCode(...bytes.subarray(index, index + 0x8000));
401
+ return platform.btoa(binary);
402
+ }
403
+ export function fromBase64(text) {
404
+ const binary = platform.atob(text);
405
+ const bytes = new Uint8Array(binary.length);
406
+ for (let index = 0; index < binary.length; index++)
407
+ bytes[index] = binary.charCodeAt(index);
408
+ return bytes;
409
+ }
410
+ function withoutData(file) {
411
+ const { data: _data, ...rest } = file;
412
+ return rest;
413
+ }
414
+ function labelOf(parameter) {
415
+ return parameter.label ?? parameter.id;
416
+ }
417
+ function moduleOf(node) {
418
+ return node.module ?? node.type ?? "";
419
+ }
420
+ function propsOf(node) {
421
+ const props = node?.props;
422
+ return props && typeof props === "object" ? props : {};
423
+ }
424
+ function isBinding(value) {
425
+ if (!value || typeof value !== "object" || Array.isArray(value) || !("value" in value))
426
+ return false;
427
+ return Object.keys(value).every((key) => key === "value" || key === "expression" || key === "channel");
428
+ }
429
+ /** 64-bit FNV-1a, as 16 hex digits. A compatibility tag, not a security hash. */
430
+ function hash(text) {
431
+ let h = 0xcbf29ce484222325n;
432
+ const prime = 0x100000001b3n;
433
+ for (let index = 0; index < text.length; index++) {
434
+ h ^= BigInt(text.charCodeAt(index));
435
+ h = (h * prime) & 0xffffffffffffffffn;
436
+ }
437
+ return h.toString(16).padStart(16, "0");
438
+ }
439
+ //# sourceMappingURL=index.js.map