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