@field/cascade 0.3.0 → 0.4.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 +14 -9
- package/ARCHITECTURE.md +22 -6
- package/CHANGELOG.md +69 -1
- package/CONTRIBUTING.md +71 -0
- package/DESIGN.md +19 -13
- package/README.md +42 -11
- package/dist/assets/{CodeEditor-BADFbTJS.js → CodeEditor-BNOJIDzL.js} +4 -4
- package/dist/assets/{CodePanel-faK6PRfz.js → CodePanel-2wp2G0Fj.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DhlKuviy.js → CustomNodeDialog-DNqaVBqA.js} +1 -1
- package/dist/assets/{ProjectSettings-D6a6kjqR.js → ProjectSettings-DcdYF2E1.js} +1 -1
- package/dist/assets/{SettingsDialog-DJZdIvLC.js → SettingsDialog-D2xVvyiR.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-C1X3HOhc.js → VersionHistoryDialog-DW288ykF.js} +1 -1
- package/dist/assets/{core-CiuiuWZi.js → core-B1y38i95.js} +1 -1
- package/dist/assets/{cssMode-D-NwZSMJ.js → cssMode-BLucnaln.js} +1 -1
- package/dist/assets/{editor.api-DT3WEvBd.js → editor.api-B3Et3Pph.js} +1 -1
- package/dist/assets/{freemarker2-eKNYQaiz.js → freemarker2-DVlPoano.js} +1 -1
- package/dist/assets/geo-DhyFdBXX.js +1 -0
- package/dist/assets/{handlebars-C2w1ggK2.js → handlebars-B5FZBAMR.js} +1 -1
- package/dist/assets/{html-DQb41jyo.js → html-FodD8KbR.js} +1 -1
- package/dist/assets/{htmlMode-IhNlh6Lv.js → htmlMode-CsG57VCD.js} +1 -1
- package/dist/assets/{image-DMCXBY-_.js → image-B3F-nq25.js} +1 -1
- package/dist/assets/index-CC6YQw2n.js +212 -0
- package/dist/assets/index-Y2ock1cw.css +1 -0
- package/dist/assets/{javascript-BIT6jMLC.js → javascript-B3gonF_H.js} +1 -1
- package/dist/assets/{jsonMode-CNfyB1ro.js → jsonMode-Y3w1ahZg.js} +1 -1
- package/dist/assets/{liquid-CmSmJmOG.js → liquid-CTpWXIYO.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BqMm3uMP.js → lspLanguageFeatures-CODBdKEZ.js} +1 -1
- package/dist/assets/{mdx-6r6YgOFw.js → mdx-CDAQJZIz.js} +1 -1
- package/dist/assets/{python-x1H0TQuI.js → python-LOIlxJU-.js} +1 -1
- package/dist/assets/{razor-Dzl1vZFm.js → razor-CllPQSHo.js} +1 -1
- package/dist/assets/{tsMode-n6DMMe3a.js → tsMode-CO28ut8t.js} +1 -1
- package/dist/assets/{typescript-ziEz65xN.js → typescript-ruU9wKQ_.js} +1 -1
- package/dist/assets/{workers-D-qUdp-r.js → workers-Bc1Ywp4-.js} +1 -1
- package/dist/assets/{xml-DMZjt5lE.js → xml-DoXP2Dh1.js} +1 -1
- package/dist/assets/{yaml-DFe5qZSO.js → yaml-D9YPyGiJ.js} +1 -1
- package/dist/cli/index.js +1240 -158
- package/dist/cli/runtime/gpu.ts +147 -0
- package/dist/contracts/camera.d.ts +70 -0
- package/dist/contracts/camera.js +27 -0
- package/dist/contracts/capabilities.d.ts +69 -6
- package/dist/contracts/definition.d.ts +61 -2
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/schema.js +33 -7
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +29 -4
- package/dist/contracts/values.d.ts +3 -2
- package/dist/contracts/values.js +1 -0
- package/dist/gpu.d.ts +88 -0
- package/dist/gpu.js +113 -0
- package/dist/index.html +2 -2
- package/dist/runtime/builtins/core/camera.d.ts +252 -0
- package/dist/runtime/builtins/core/camera.js +149 -0
- package/dist/runtime/builtins/core/index.d.ts +2 -1
- package/dist/runtime/builtins/core/index.js +3 -1
- package/dist/runtime/camera/index.d.ts +60 -0
- package/dist/runtime/camera/index.js +234 -0
- package/dist/runtime/definition/architecture.js +36 -1
- package/dist/runtime/gpu.d.ts +33 -0
- package/dist/runtime/gpu.js +218 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/runtime.d.ts +23 -3
- package/dist/runtime/runtime.js +181 -30
- package/dist/runtime/series/index.d.ts +41 -0
- package/dist/runtime/series/index.js +167 -0
- package/dist/runtime/types.d.ts +3 -3
- package/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +36 -0
- package/doc/PROJECT_AUTHORING.md +106 -20
- package/package.json +14 -2
- package/dist/assets/geo-DUvVbLnx.js +0 -1
- package/dist/assets/index-BDV5xGBZ.css +0 -1
- package/dist/assets/index-HTCN44kj.js +0 -212
package/dist/cli/index.js
CHANGED
|
@@ -21,6 +21,26 @@ var init_animation = __esm({
|
|
|
21
21
|
}
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
+
// packages/contracts/src/camera.ts
|
|
25
|
+
var CAMERA_DEFAULTS;
|
|
26
|
+
var init_camera = __esm({
|
|
27
|
+
"packages/contracts/src/camera.ts"() {
|
|
28
|
+
"use strict";
|
|
29
|
+
CAMERA_DEFAULTS = Object.freeze({
|
|
30
|
+
translate: [0, 0, 0],
|
|
31
|
+
rotate: [0, 0, 0],
|
|
32
|
+
focal: 50,
|
|
33
|
+
aperture: 41.4214,
|
|
34
|
+
near: 1e-3,
|
|
35
|
+
far: 1e4,
|
|
36
|
+
resolution: [1280, 720],
|
|
37
|
+
aspect: 1,
|
|
38
|
+
projection: "perspective",
|
|
39
|
+
orthowidth: 2
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
|
|
24
44
|
// packages/contracts/src/capabilities.ts
|
|
25
45
|
var init_capabilities = __esm({
|
|
26
46
|
"packages/contracts/src/capabilities.ts"() {
|
|
@@ -540,6 +560,13 @@ var init_geometry = __esm({
|
|
|
540
560
|
}
|
|
541
561
|
});
|
|
542
562
|
|
|
563
|
+
// packages/contracts/src/series.ts
|
|
564
|
+
var init_series = __esm({
|
|
565
|
+
"packages/contracts/src/series.ts"() {
|
|
566
|
+
"use strict";
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
543
570
|
// packages/contracts/src/values.ts
|
|
544
571
|
function normalizeCascadeType(type) {
|
|
545
572
|
if (!type) return "any";
|
|
@@ -577,6 +604,7 @@ var init_values = __esm({
|
|
|
577
604
|
"mesh",
|
|
578
605
|
"rects",
|
|
579
606
|
"color",
|
|
607
|
+
"camera",
|
|
580
608
|
"asset",
|
|
581
609
|
"array",
|
|
582
610
|
"object",
|
|
@@ -670,17 +698,17 @@ function validateNodeDefinition(value) {
|
|
|
670
698
|
"capabilities"
|
|
671
699
|
);
|
|
672
700
|
const seen = /* @__PURE__ */ new Set();
|
|
673
|
-
for (const [index,
|
|
701
|
+
for (const [index, capability2] of (Array.isArray(value.capabilities) ? value.capabilities : []).entries()) {
|
|
674
702
|
const path23 = `capabilities.${index}`;
|
|
675
|
-
if (!seen.has(
|
|
703
|
+
if (!seen.has(capability2) && !allowed.includes(capability2))
|
|
676
704
|
add(
|
|
677
705
|
"capability-not-allowed",
|
|
678
|
-
`${String(
|
|
706
|
+
`${String(capability2)} is not available on ${String(environment)}`,
|
|
679
707
|
path23
|
|
680
708
|
);
|
|
681
|
-
if (seen.has(
|
|
682
|
-
add("duplicate-capability", `${String(
|
|
683
|
-
seen.add(
|
|
709
|
+
if (seen.has(capability2))
|
|
710
|
+
add("duplicate-capability", `${String(capability2)} is duplicated`, path23);
|
|
711
|
+
seen.add(capability2);
|
|
684
712
|
}
|
|
685
713
|
const inward = /* @__PURE__ */ new Map();
|
|
686
714
|
const outward = /* @__PURE__ */ new Map();
|
|
@@ -743,6 +771,29 @@ function validateNodeDefinition(value) {
|
|
|
743
771
|
"options require the select control",
|
|
744
772
|
`${path23}.options`
|
|
745
773
|
);
|
|
774
|
+
if (Array.isArray(raw.options))
|
|
775
|
+
for (const [index, option] of raw.options.entries()) {
|
|
776
|
+
if (option === null || typeof option !== "object") continue;
|
|
777
|
+
const entry = option;
|
|
778
|
+
if (!("value" in entry))
|
|
779
|
+
add(
|
|
780
|
+
"invalid-option",
|
|
781
|
+
"a labelled option needs a value",
|
|
782
|
+
`${path23}.options[${index}]`
|
|
783
|
+
);
|
|
784
|
+
if (typeof entry.label !== "string" || !entry.label)
|
|
785
|
+
add(
|
|
786
|
+
"invalid-option",
|
|
787
|
+
"a labelled option needs a non-empty label",
|
|
788
|
+
`${path23}.options[${index}].label`
|
|
789
|
+
);
|
|
790
|
+
if ("disabled" in entry && typeof entry.disabled !== "boolean")
|
|
791
|
+
add(
|
|
792
|
+
"invalid-option",
|
|
793
|
+
"disabled must be a boolean",
|
|
794
|
+
`${path23}.options[${index}].disabled`
|
|
795
|
+
);
|
|
796
|
+
}
|
|
746
797
|
const type = raw.type;
|
|
747
798
|
if (!validType(type)) {
|
|
748
799
|
add(
|
|
@@ -782,6 +833,20 @@ function validateNodeDefinition(value) {
|
|
|
782
833
|
);
|
|
783
834
|
if ("step" in raw && (typeof raw.step !== "number" || !Number.isFinite(raw.step) || raw.step <= 0))
|
|
784
835
|
add("invalid-step", "step must be greater than zero", `${path23}.step`);
|
|
836
|
+
if (section === "props" && "expression" in raw && (typeof raw.expression !== "string" || raw.expression.length === 0))
|
|
837
|
+
add(
|
|
838
|
+
"invalid-expression",
|
|
839
|
+
"expression must be a non-empty string",
|
|
840
|
+
`${path23}.expression`
|
|
841
|
+
);
|
|
842
|
+
for (const field of ["min", "max"]) {
|
|
843
|
+
if (field in raw && (typeof raw[field] !== "number" || !Number.isFinite(raw[field])))
|
|
844
|
+
add(
|
|
845
|
+
`invalid-${field}`,
|
|
846
|
+
`${field} must be a finite number`,
|
|
847
|
+
`${path23}.${field}`
|
|
848
|
+
);
|
|
849
|
+
}
|
|
785
850
|
if (typeof raw.min === "number" && typeof raw.max === "number" && raw.min > raw.max)
|
|
786
851
|
add("invalid-range", "min must not exceed max", path23);
|
|
787
852
|
}
|
|
@@ -800,9 +865,9 @@ var init_validate = __esm({
|
|
|
800
865
|
CORE = new Set(CORE_TYPES);
|
|
801
866
|
CONTROLS = /* @__PURE__ */ new Set(["number", "slider", "range", "int", "boolean", "text", "textarea", "select", "vector", "matrix", "color", "image", "asset"]);
|
|
802
867
|
CAPABILITIES_BY_ENVIRONMENT = {
|
|
803
|
-
portable: ["assets", "media"],
|
|
804
|
-
browser: ["assets", "media", "
|
|
805
|
-
server: ["files", "assets", "media", "python", "shell"]
|
|
868
|
+
portable: ["assets", "media", "gpu"],
|
|
869
|
+
browser: ["assets", "media", "gpu"],
|
|
870
|
+
server: ["files", "assets", "media", "python", "shell", "gpu"]
|
|
806
871
|
};
|
|
807
872
|
ALLOWED = CAPABILITIES_BY_ENVIRONMENT;
|
|
808
873
|
fields = (names) => new Set(names.split(" "));
|
|
@@ -814,7 +879,7 @@ var init_validate = __esm({
|
|
|
814
879
|
);
|
|
815
880
|
OUTPUT_FIELDS = fields("kind type description");
|
|
816
881
|
PROP_FIELDS = fields(
|
|
817
|
-
"type default label description min max step accept control options"
|
|
882
|
+
"type default label description min max step accept control options expression action"
|
|
818
883
|
);
|
|
819
884
|
}
|
|
820
885
|
});
|
|
@@ -824,11 +889,13 @@ var init_src = __esm({
|
|
|
824
889
|
"packages/contracts/src/index.ts"() {
|
|
825
890
|
"use strict";
|
|
826
891
|
init_animation();
|
|
892
|
+
init_camera();
|
|
827
893
|
init_capabilities();
|
|
828
894
|
init_color();
|
|
829
895
|
init_definition();
|
|
830
896
|
init_document();
|
|
831
897
|
init_geometry();
|
|
898
|
+
init_series();
|
|
832
899
|
init_validate();
|
|
833
900
|
init_values();
|
|
834
901
|
}
|
|
@@ -2872,7 +2939,7 @@ var init_panels = __esm({
|
|
|
2872
2939
|
});
|
|
2873
2940
|
|
|
2874
2941
|
// server/src/pythonWorker.ts
|
|
2875
|
-
import { spawn as
|
|
2942
|
+
import { spawn as spawn3 } from "child_process";
|
|
2876
2943
|
import readline from "readline";
|
|
2877
2944
|
var REQUEST_TIMEOUT_MS, PythonWorker;
|
|
2878
2945
|
var init_pythonWorker = __esm({
|
|
@@ -2892,7 +2959,7 @@ var init_pythonWorker = __esm({
|
|
|
2892
2959
|
ensureStarted() {
|
|
2893
2960
|
if (this.proc && !this.proc.killed) return this.proc;
|
|
2894
2961
|
const scriptPath = this.project.resolve(this.entrypoint);
|
|
2895
|
-
const proc =
|
|
2962
|
+
const proc = spawn3(this.python, [scriptPath], { cwd: this.project.root, env: this.env });
|
|
2896
2963
|
readline.createInterface({ input: proc.stdout }).on("line", (line2) => this.handleLine(line2));
|
|
2897
2964
|
proc.stderr.on("data", (chunk) => console.error("[python-worker]", chunk.toString().trimEnd()));
|
|
2898
2965
|
proc.on("exit", (code) => {
|
|
@@ -2987,8 +3054,8 @@ function createProjectRequestBoundary(options) {
|
|
|
2987
3054
|
function createBrowserCapabilityBoundary(options, label, header) {
|
|
2988
3055
|
const origins = new Set(options.trustedOrigins.map((origin) => validateOrigin(origin, label)));
|
|
2989
3056
|
const authorities = allowedAuthorities(options, origins);
|
|
2990
|
-
const
|
|
2991
|
-
const expectedBytes = Buffer.from(
|
|
3057
|
+
const capability2 = randomBytes(32).toString("base64url");
|
|
3058
|
+
const expectedBytes = Buffer.from(capability2);
|
|
2992
3059
|
return {
|
|
2993
3060
|
guardOrigin(req, res, next) {
|
|
2994
3061
|
const origin = req.get("Origin");
|
|
@@ -3008,7 +3075,7 @@ function createBrowserCapabilityBoundary(options, label, header) {
|
|
|
3008
3075
|
issueCapability(_req, res) {
|
|
3009
3076
|
res.setHeader("Cache-Control", "no-store, private");
|
|
3010
3077
|
res.setHeader("Pragma", "no-cache");
|
|
3011
|
-
res.json({ capability });
|
|
3078
|
+
res.json({ capability: capability2 });
|
|
3012
3079
|
},
|
|
3013
3080
|
requireCapability(req, res, next) {
|
|
3014
3081
|
const supplied = req.get(header);
|
|
@@ -3026,12 +3093,36 @@ function allowedAuthorities(options, origins) {
|
|
|
3026
3093
|
if (isLoopbackHost(options.host)) {
|
|
3027
3094
|
authorities.add(`127.0.0.1:${options.port}`);
|
|
3028
3095
|
authorities.add(`localhost:${options.port}`);
|
|
3029
|
-
} else if (options.
|
|
3096
|
+
} else if (!isWildcardHost(options.host)) {
|
|
3030
3097
|
authorities.add(authority(options.host, options.port));
|
|
3031
3098
|
}
|
|
3032
|
-
for (const host of options.trustedHosts)
|
|
3099
|
+
for (const host of options.trustedHosts) {
|
|
3100
|
+
const value = trustedAuthority(host, options.port);
|
|
3101
|
+
authorities.add(value);
|
|
3102
|
+
const bare = value.replace(/:(?:80|443)$/, "");
|
|
3103
|
+
if (bare !== value) authorities.add(bare);
|
|
3104
|
+
}
|
|
3033
3105
|
return authorities;
|
|
3034
3106
|
}
|
|
3107
|
+
function isWildcardHost(host) {
|
|
3108
|
+
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
3109
|
+
return normalized === "0.0.0.0" || normalized === "::" || normalized === "*";
|
|
3110
|
+
}
|
|
3111
|
+
function trustedAuthority(value, defaultPort) {
|
|
3112
|
+
const trimmed = value.trim().toLowerCase();
|
|
3113
|
+
const bracketed = /^\[([^\]]+)\]:(\d+)$/.exec(trimmed);
|
|
3114
|
+
if (bracketed) return authority(`[${bracketed[1]}]`, checkedPort(bracketed[2]));
|
|
3115
|
+
const hostAndPort = /^([^:]+):(\d+)$/.exec(trimmed);
|
|
3116
|
+
if (hostAndPort) return authority(hostAndPort[1], checkedPort(hostAndPort[2]));
|
|
3117
|
+
return authority(trimmed, defaultPort);
|
|
3118
|
+
}
|
|
3119
|
+
function checkedPort(raw) {
|
|
3120
|
+
const port = Number(raw);
|
|
3121
|
+
if (!Number.isInteger(port) || port < 1 || port > 65535) {
|
|
3122
|
+
throw new Error(`Invalid trusted host port: ${raw}`);
|
|
3123
|
+
}
|
|
3124
|
+
return port;
|
|
3125
|
+
}
|
|
3035
3126
|
function authority(host, port) {
|
|
3036
3127
|
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
3037
3128
|
if (!normalized || /[\s/?#@]/.test(normalized)) throw new Error(`Invalid trusted host: ${host}`);
|
|
@@ -3389,7 +3480,7 @@ var init_allowlist = __esm({
|
|
|
3389
3480
|
});
|
|
3390
3481
|
|
|
3391
3482
|
// server/src/agent/session.ts
|
|
3392
|
-
import { spawn as
|
|
3483
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
3393
3484
|
function childEnvironment() {
|
|
3394
3485
|
const env = { ...process.env };
|
|
3395
3486
|
for (const key2 of Object.keys(env)) {
|
|
@@ -3619,7 +3710,7 @@ var init_session = __esm({
|
|
|
3619
3710
|
const resumed = Boolean(state.sessionId);
|
|
3620
3711
|
const args = argsFor(request.agent, request.prompt, state.sessionId, agentExtraArgs(this.project.root, request.agent));
|
|
3621
3712
|
run.emit({ type: "started", agent: request.agent, sketch: request.sketch, resumed });
|
|
3622
|
-
const child =
|
|
3713
|
+
const child = spawn4(executable, args, {
|
|
3623
3714
|
cwd: this.project.root,
|
|
3624
3715
|
env: childEnvironment(),
|
|
3625
3716
|
shell: false,
|
|
@@ -4283,7 +4374,27 @@ function startServer(project, opts = {}) {
|
|
|
4283
4374
|
const trustedHosts = Object.freeze([...opts.trustedHosts ?? []]);
|
|
4284
4375
|
const defaultOrigins = [
|
|
4285
4376
|
...isLoopbackHost(HOST) ? [`http://127.0.0.1:${PORT}`, `http://localhost:${PORT}`] : [],
|
|
4286
|
-
|
|
4377
|
+
// Both schemes for a trusted host, because the server cannot know whether
|
|
4378
|
+
// something is terminating TLS in front of it.
|
|
4379
|
+
//
|
|
4380
|
+
// This was `http://` only, and behind a TLS proxy a browser sends
|
|
4381
|
+
// `Origin: https://name:port` — so the Host check passed and the **Origin
|
|
4382
|
+
// check refused it**, which 403s every POST and PUT while GETs without an
|
|
4383
|
+
// Origin header sail through. That is a Studio that loads, renders and
|
|
4384
|
+
// then silently cannot save. Measured before fixing: `httpsOrigin: 403`,
|
|
4385
|
+
// `httpOrigin: 200` on the same server.
|
|
4386
|
+
//
|
|
4387
|
+
// Not a widening: the Host allowlist already decides which authorities are
|
|
4388
|
+
// acceptable, and this only says the same authority may arrive over either
|
|
4389
|
+
// scheme.
|
|
4390
|
+
...trustedHosts.flatMap((host) => {
|
|
4391
|
+
const value = trustedAuthority(host, PORT);
|
|
4392
|
+
const bare = value.replace(/:(?:80|443)$/, "");
|
|
4393
|
+
return [.../* @__PURE__ */ new Set([value, bare])].flatMap((authority3) => [
|
|
4394
|
+
`http://${authority3}`,
|
|
4395
|
+
`https://${authority3}`
|
|
4396
|
+
]);
|
|
4397
|
+
})
|
|
4287
4398
|
];
|
|
4288
4399
|
const security = Object.freeze({
|
|
4289
4400
|
host: HOST,
|
|
@@ -4342,7 +4453,7 @@ function startServer(project, opts = {}) {
|
|
|
4342
4453
|
server.listen(PORT, HOST, () => {
|
|
4343
4454
|
console.log(`\u{1F680} Cascade running on http://${HOST}:${PORT}`);
|
|
4344
4455
|
console.log(`\u{1F4C1} Project: ${project.root}${project.isGitRepo ? " (git)" : " (not a git repo yet)"}`);
|
|
4345
|
-
if (trustedHosts.length > 0) console.log(`\u{1F510} Trusted Studio URL: http://${
|
|
4456
|
+
if (trustedHosts.length > 0) console.log(`\u{1F510} Trusted Studio URL: http://${trustedAuthority(trustedHosts[0], PORT)} (all reachable VPN/interface peers are trusted)`);
|
|
4346
4457
|
});
|
|
4347
4458
|
return server;
|
|
4348
4459
|
}
|
|
@@ -4441,10 +4552,11 @@ Read \`node_modules/cascade/AGENTS.md\` for the framework itself. What follows i
|
|
|
4441
4552
|
|
|
4442
4553
|
## Read this first
|
|
4443
4554
|
|
|
4444
|
-
\`node_modules/cascade/doc/NODE_REFERENCE.md\` \u2014 every built-in node with its inputs, props and outputs, generated from the definitions themselves.
|
|
4555
|
+
\`node_modules/cascade/doc/NODE_REFERENCE.md\` \u2014 every built-in node with its inputs, props and outputs, generated from the definitions themselves. Read it before creating a wrapper for an existing node. Time-based parameter changes usually need an expression or channel.
|
|
4445
4556
|
|
|
4446
4557
|
## Common commands
|
|
4447
4558
|
|
|
4559
|
+
- Run \`npm install\` in this directory first. Scripts use the project's installed Cascade version; use \`npx --no-install cascade\` for direct local CLI commands.
|
|
4448
4560
|
- \`npm run check\` \u2014 type-check custom nodes.
|
|
4449
4561
|
- \`npm run check:graph\` \u2014 statically check the graph and node definitions.
|
|
4450
4562
|
- \`npm run validate\` \u2014 validate \`index.cascade\`.
|
|
@@ -4457,7 +4569,7 @@ both the explicit bind address and the exact trusted browser hostname.
|
|
|
4457
4569
|
|
|
4458
4570
|
## The \`.cascade\` document format
|
|
4459
4571
|
|
|
4460
|
-
Hand-editing
|
|
4572
|
+
Hand-editing is supported. Preserve metadata, source fields, IDs, and valid connection endpoints, then run the static checks. This example uses the node below; save it at \`nodes/Multiply/index.ts\` before running the graph.
|
|
4461
4573
|
|
|
4462
4574
|
\`\`\`json
|
|
4463
4575
|
{
|
|
@@ -4465,28 +4577,25 @@ Hand-editing this file is normal and expected. The shape is not what you would g
|
|
|
4465
4577
|
"metadata": { "name": "My Sketch" },
|
|
4466
4578
|
"nodes": [
|
|
4467
4579
|
{
|
|
4468
|
-
"id": "
|
|
4469
|
-
"module": "project.
|
|
4580
|
+
"id": "multiply",
|
|
4581
|
+
"module": "project.Multiply",
|
|
4470
4582
|
"position": [240, 200],
|
|
4471
4583
|
"source": "project",
|
|
4472
|
-
"
|
|
4473
|
-
"props": { "angle": { "value": 0, "expression": "$T * 40" } }
|
|
4584
|
+
"props": { "factor": { "value": 2, "expression": "2 + sin($T)" } }
|
|
4474
4585
|
}
|
|
4475
4586
|
],
|
|
4476
|
-
"connections": [
|
|
4477
|
-
[["oscillator", 0, "value"], ["logo-1024", 0, "angle"]]
|
|
4478
|
-
],
|
|
4587
|
+
"connections": [],
|
|
4479
4588
|
"annotations": []
|
|
4480
4589
|
}
|
|
4481
4590
|
\`\`\`
|
|
4482
4591
|
|
|
4483
4592
|
Five things that are easy to get wrong:
|
|
4484
4593
|
|
|
4485
|
-
-
|
|
4594
|
+
- **Use \`module\` for the module identity and \`"source": "project"\` for project modules.** Unresolved modules need investigation even when validation only warns.
|
|
4486
4595
|
- **\`position\` is \`[x, y]\`**, an array, not \`{x, y}\`.
|
|
4487
|
-
-
|
|
4596
|
+
- **Definition-v1 stored parameter values belong under \`props\`.** A leftover \`params\` array is not read by the deterministic runtime; \`runtime/stray-params\` warns about stranded values.
|
|
4488
4597
|
- **A connection is a pair of triples**: \`[[fromNode, portIndex, portName], [toNode, portIndex, portName]]\`. Direction comes from *position in the pair* \u2014 first is the source. The middle number is the **port index**, not a direction.
|
|
4489
|
-
- **
|
|
4598
|
+
- **A prop is a bare value or \`{ value, expression?, channel? }\`.** Channels take precedence over expressions, then stored values. Save the binding, not just its value at the playhead.
|
|
4490
4599
|
|
|
4491
4600
|
## Before writing a node, check whether you need one
|
|
4492
4601
|
|
|
@@ -4515,25 +4624,22 @@ import type { NodeDefinition, NodeExecutionContext } from 'cascade/contracts';
|
|
|
4515
4624
|
|
|
4516
4625
|
export const definition = {
|
|
4517
4626
|
apiVersion: 1,
|
|
4518
|
-
label: '
|
|
4627
|
+
label: 'Multiply',
|
|
4519
4628
|
icon: 'Circle', // a Lucide icon name
|
|
4520
4629
|
runsOn: 'portable', // 'portable' | 'browser' | 'server'
|
|
4521
4630
|
inputs: {
|
|
4522
|
-
value: { kind: 'data', type: 'float', default:
|
|
4631
|
+
value: { kind: 'data', type: 'float', default: 3 },
|
|
4523
4632
|
},
|
|
4524
4633
|
outputs: {
|
|
4525
|
-
|
|
4634
|
+
result: { kind: 'data', type: 'float' },
|
|
4526
4635
|
},
|
|
4527
4636
|
props: {
|
|
4528
|
-
|
|
4637
|
+
factor: { type: 'float', default: 2, min: 0, max: 10, step: 0.1 },
|
|
4529
4638
|
},
|
|
4530
4639
|
} as const satisfies NodeDefinition;
|
|
4531
4640
|
|
|
4532
4641
|
export function execute(context: NodeExecutionContext<typeof definition>) {
|
|
4533
|
-
|
|
4534
|
-
const { size } = context.props;
|
|
4535
|
-
// ... work ...
|
|
4536
|
-
context.outputs.image.set({ path, size: [size, size], channels: 'rgb', depth: 'u8', space: 'srgb' });
|
|
4642
|
+
context.outputs.result.set(context.inputs.value * context.props.factor);
|
|
4537
4643
|
}
|
|
4538
4644
|
\`\`\`
|
|
4539
4645
|
|
|
@@ -4541,7 +4647,7 @@ The declaration is the point: ports, types, props and the execution locus are re
|
|
|
4541
4647
|
|
|
4542
4648
|
### Use the vector types
|
|
4543
4649
|
|
|
4544
|
-
Anything with an x and a y is **one** \`vec2\`, not two floats. Same for \`vec3\` and \`vec4\`, with \`vec2i\`, \`vec3i\` and \`vec4i\` for integer counts and pixel sizes.
|
|
4650
|
+
Anything with an x and a y is **one** \`vec2\`, not two floats. Same for \`vec3\` and \`vec4\`, with \`vec2i\`, \`vec3i\` and \`vec4i\` for integer counts and pixel sizes. Positions, offsets, scales, and resolutions use vector ports; colours use the core \`color\` type.
|
|
4545
4651
|
|
|
4546
4652
|
\`\`\`ts
|
|
4547
4653
|
// Wrong
|
|
@@ -4553,22 +4659,26 @@ props: { offset: { type: 'vec2', default: [0, 0] } }
|
|
|
4553
4659
|
|
|
4554
4660
|
Not a style preference. Two floats that are really one vector cannot be connected to a \`vec2\` output, get two rows in the Inspector instead of one control, need two keyframes to animate one movement, and let a graph carry an x without its y. The type system knows what a \`vec2\` is; it cannot know that \`offset_x\` and \`offset_y\` belong together.
|
|
4555
4661
|
|
|
4556
|
-
Split
|
|
4662
|
+
Split only when the components differ in kind. Vector props support \`min\`, \`max\`, and \`step\`, with one range across their components.
|
|
4663
|
+
|
|
4664
|
+
Studio and a compatible CLI host cook this style. **Studio** builds ports from the compiled definition and calls \`execute\` with a real \`NodeExecutionContext\`; **\`cascade run\`** uses the deterministic runtime when the node's execution locus and capabilities are supported. A browser-only declaration still prevents a Node-host run.
|
|
4557
4665
|
|
|
4558
|
-
|
|
4666
|
+
The **dynamic** style \u2014 \`execute(node, graph)\` declaring ports with \`node.in\`, \`node.param\`, and \`node.out\` \u2014 remains supported by Studio and the CLI compatibility engine. It cannot provide the same static checks. Studio may mix both styles; the CLI rejects mixed graphs. Convert deliberately with saved-value and output comparisons.
|
|
4559
4667
|
|
|
4560
|
-
|
|
4668
|
+
Definition-v1 props support default \`expression\` values. An explicitly saved value overrides that default, even when equal to the numeric default. Both fully definition-v1 and supported dynamic graphs can render sequences with \`cascade run index.cascade --frames 1-100\`, subject to host capabilities.
|
|
4561
4669
|
|
|
4562
4670
|
Either way the module is a real ES module and the loader needs the \`execute\` export. Top-level side effects do not belong in it.
|
|
4563
4671
|
|
|
4564
4672
|
## Rendering
|
|
4565
4673
|
|
|
4566
|
-
|
|
4674
|
+
Prefer Canvas 2D, WebGL, and WebGPU for interactive rendering. Use Python or another server stage for libraries and tools that require it. Transport, serialization, and cold starts can affect parameter-drag latency; persistent workers can avoid repeated process/model startup.
|
|
4567
4675
|
|
|
4568
|
-
|
|
4676
|
+
Definition-v1 image nodes can use \`saveImage(canvas, cachePath(context.nodeId, '.png'))\` from \`cascade/io\`. The instance ID provides a cache namespace, not graph access. Final artefacts may instead use an explicit filename. Optional Skia supplies Canvas2D APIs; optional Dawn separately supplies WebGPU. Neither overrides a browser-only declaration or supplies the DOM.
|
|
4569
4677
|
|
|
4570
4678
|
Use \`browser\` only for code that genuinely needs the page, such as the DOM or WebGL. Prefer \`new OffscreenCanvas(width, height)\` to \`document.createElement('canvas')\` when both hosts can run the same node.
|
|
4571
4679
|
|
|
4680
|
+
WebGPU nodes declare \`capabilities: ['gpu']\` and use \`context.capabilities.gpu\` for the shared device and per-node cache. Portable GPU nodes run in Studio or the optional Dawn CLI host. Import usage constants and explicit RGBA8 \`readTexture\` from \`cascade/gpu\`, then encode through the image/IO host. Graph texture exchange remains future work. For agent image feedback use \`cascade run index.cascade --frames 1 --json --timeout 60000\`; see \`node_modules/cascade/doc/HEADLESS_GPU.md\` for limitations.
|
|
4681
|
+
|
|
4572
4682
|
## Finding out what exists
|
|
4573
4683
|
|
|
4574
4684
|
- \`node_modules/cascade/doc/PROJECT_AUTHORING.md\` \u2014 project layout and the node contract.
|
|
@@ -4577,11 +4687,11 @@ Use \`browser\` only for code that genuinely needs the page, such as the DOM or
|
|
|
4577
4687
|
- \`node_modules/cascade/dist/runtime/builtins/\` \u2014 the built-in node definitions, as the shipped type declarations. (\`packages/\` is not in the published \`files\` list, so an installed copy has no source tree.)
|
|
4578
4688
|
- The **Definition panel** in Studio shows, for any selected node, where it is defined and what it declares.
|
|
4579
4689
|
|
|
4580
|
-
##
|
|
4690
|
+
## Verification and shared development
|
|
4581
4691
|
|
|
4582
|
-
|
|
4692
|
+
Cascade's build refreshes \`dist/\`, which running Studio servers may serve. Coordinate a core rebuild with anyone using that checkout; routine sketch edits do not require rebuilding Cascade.
|
|
4583
4693
|
|
|
4584
|
-
|
|
4694
|
+
Use \`npm run check:graph\` for static checks and \`npm run run\` for a compatible headless graph. A browser-only node may pass static checks with an environment warning and still be refused by the CLI; verify it in Studio instead. Compare actual outputs after edits and distinguish recorded measurements from tests run in the current session.
|
|
4585
4695
|
`);
|
|
4586
4696
|
return directory;
|
|
4587
4697
|
}
|
|
@@ -4655,7 +4765,7 @@ function parseStudioArgs(args) {
|
|
|
4655
4765
|
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
4656
4766
|
if (name === "--host") host = value;
|
|
4657
4767
|
else if (name === "--trusted-host") {
|
|
4658
|
-
|
|
4768
|
+
trustedAuthority(value, 1);
|
|
4659
4769
|
trustedHosts.push(value);
|
|
4660
4770
|
} else {
|
|
4661
4771
|
port = Number(value);
|
|
@@ -4668,8 +4778,8 @@ function parseStudioArgs(args) {
|
|
|
4668
4778
|
if (argument.startsWith("-")) throw new Error(`unknown option: ${argument}`);
|
|
4669
4779
|
positional.push(argument);
|
|
4670
4780
|
}
|
|
4671
|
-
if (trustedHosts.length > 0 &&
|
|
4672
|
-
throw new Error("--trusted-host requires an explicit
|
|
4781
|
+
if (trustedHosts.length > 0 && !host) {
|
|
4782
|
+
throw new Error("--trusted-host requires an explicit --host bind address, loopback or otherwise");
|
|
4673
4783
|
}
|
|
4674
4784
|
if (host && !isLoopbackHost(host)) {
|
|
4675
4785
|
if (host === "0.0.0.0" || host === "::" || host === "[::]") {
|
|
@@ -4721,7 +4831,7 @@ async function runStudioCli(args) {
|
|
|
4721
4831
|
const address = server.address();
|
|
4722
4832
|
const port2 = typeof address === "object" && address ? address.port : 3030;
|
|
4723
4833
|
const browserHost = trustedHosts[0] ?? (host && !isLoopbackHost(host) ? host : "localhost");
|
|
4724
|
-
const url = `http://${
|
|
4834
|
+
const url = `http://${trustedAuthority(browserHost, port2)}`;
|
|
4725
4835
|
if (!noOpen) {
|
|
4726
4836
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4727
4837
|
execFile5(opener, [url], (error) => {
|
|
@@ -5625,6 +5735,34 @@ function split(path23) {
|
|
|
5625
5735
|
return String(path23 ?? "").split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
5626
5736
|
}
|
|
5627
5737
|
|
|
5738
|
+
// src/utils/thrownError.ts
|
|
5739
|
+
function asError(thrown) {
|
|
5740
|
+
if (thrown instanceof Error) return thrown;
|
|
5741
|
+
const described = describeThrown(thrown);
|
|
5742
|
+
const error = new Error(described);
|
|
5743
|
+
error.cause = thrown;
|
|
5744
|
+
return error;
|
|
5745
|
+
}
|
|
5746
|
+
function describeThrown(thrown) {
|
|
5747
|
+
if (thrown === null) return "null was thrown";
|
|
5748
|
+
if (thrown === void 0) return "undefined was thrown";
|
|
5749
|
+
if (typeof thrown === "string") return thrown || "an empty string was thrown";
|
|
5750
|
+
if (typeof thrown !== "object") return `${String(thrown)} was thrown`;
|
|
5751
|
+
const value = thrown;
|
|
5752
|
+
const kind = typeof value.name === "string" && value.name ? value.name : value.constructor?.name || "object";
|
|
5753
|
+
const parts = [];
|
|
5754
|
+
if (typeof value.message === "string" && value.message) parts.push(value.message);
|
|
5755
|
+
if (typeof value.reason === "string" && value.reason) parts.push(`reason: ${value.reason}`);
|
|
5756
|
+
if (!parts.length) {
|
|
5757
|
+
try {
|
|
5758
|
+
const json6 = JSON.stringify(thrown);
|
|
5759
|
+
if (json6 && json6 !== "{}") parts.push(json6);
|
|
5760
|
+
} catch {
|
|
5761
|
+
}
|
|
5762
|
+
}
|
|
5763
|
+
return parts.length ? `${kind}: ${parts.join(", ")}` : `${kind} was thrown with no message`;
|
|
5764
|
+
}
|
|
5765
|
+
|
|
5628
5766
|
// src/nodes/Node.ts
|
|
5629
5767
|
function inferParamType(value) {
|
|
5630
5768
|
if (typeof value === "number") return Number.isInteger(value) ? "int" : "float";
|
|
@@ -5731,9 +5869,38 @@ var Node = class _Node {
|
|
|
5731
5869
|
// Execution state
|
|
5732
5870
|
this.hasExecuted = false;
|
|
5733
5871
|
this.lastInputHash = "";
|
|
5872
|
+
/** The inputs an execute threw on, or null if the last cook did not throw.
|
|
5873
|
+
* Read only by `isDirty` — see the comment there for why it exists. */
|
|
5874
|
+
this.failedInputHash = null;
|
|
5734
5875
|
this.cookState = "stale";
|
|
5735
5876
|
this.cookGeneration = 0;
|
|
5736
|
-
|
|
5877
|
+
/**
|
|
5878
|
+
* Output values staged by the cook currently running, one map per cook.
|
|
5879
|
+
*
|
|
5880
|
+
* A stack rather than a field, because it was a field and that was a race.
|
|
5881
|
+
* `stagedOutputs` was instance state created per cook, iterated under a
|
|
5882
|
+
* `generation === this.cookGeneration` guard, and nulled in `finally` — but
|
|
5883
|
+
* `cookGeneration` only advances in `invalidateCook()`, and `cook()` has no
|
|
5884
|
+
* re-entrancy guard. So two overlapping cooks of the same node with no
|
|
5885
|
+
* invalidation between them both ran at the same generation: the second's
|
|
5886
|
+
* `finally` nulled the field, the first resumed, passed the generation check
|
|
5887
|
+
* because nothing had invalidated it, and iterated `null` —
|
|
5888
|
+
* `stagedOutputs is not iterable`.
|
|
5889
|
+
*
|
|
5890
|
+
* Worse than the crash, and the reason this is a correctness fix rather than
|
|
5891
|
+
* a robustness one: the second cook's `new Map()` **replaced the first's
|
|
5892
|
+
* mid-flight**, so the first cook's staged writes were lost whenever the
|
|
5893
|
+
* timing did not happen to produce the throw.
|
|
5894
|
+
*
|
|
5895
|
+
* The underlying fault is that per-cook state lived on the instance, and the
|
|
5896
|
+
* thing protecting it (the generation) was not the thing destroying it (the
|
|
5897
|
+
* `finally`) — two mechanisms guarding one field, neither aware of the other.
|
|
5898
|
+
* Each cook now owns its own map, holds it locally, and removes it **by
|
|
5899
|
+
* identity**, so an interleaving cannot make one cook drop another's.
|
|
5900
|
+
*
|
|
5901
|
+
* Diagnosed by MW-OBSERVATORY-ART, 2026-09-08.
|
|
5902
|
+
*/
|
|
5903
|
+
this.stagingStack = [];
|
|
5737
5904
|
this.executionTimeout = 3e4;
|
|
5738
5905
|
// Cook info - performance tracking
|
|
5739
5906
|
this.cookInfo = {
|
|
@@ -5750,6 +5917,17 @@ var Node = class _Node {
|
|
|
5750
5917
|
this.portsUsedDuringSetup = /* @__PURE__ */ new Set();
|
|
5751
5918
|
// Port index map for O(1) lookups (portId -> port)
|
|
5752
5919
|
this._portMap = /* @__PURE__ */ new Map();
|
|
5920
|
+
// ============ Props System ============
|
|
5921
|
+
/**
|
|
5922
|
+
* Add a parameter to this node
|
|
5923
|
+
* Short alias for defineProp - use in setup()
|
|
5924
|
+
*/
|
|
5925
|
+
/** Props whose declared default expression has already been applied — see
|
|
5926
|
+
* `bindParameterProp`, which must not reinstate one the author deleted. */
|
|
5927
|
+
this.defaultExpressionsApplied = /* @__PURE__ */ new Set();
|
|
5928
|
+
/** Props whose plain stored value explicitly overrides a declared default
|
|
5929
|
+
* expression. The marker survives even when that value equals the default. */
|
|
5930
|
+
this.plainPropOverrides = /* @__PURE__ */ new Set();
|
|
5753
5931
|
this.id = id;
|
|
5754
5932
|
this.type = type;
|
|
5755
5933
|
this.graph = graph;
|
|
@@ -5868,8 +6046,9 @@ var Node = class _Node {
|
|
|
5868
6046
|
return port && "defaultValue" in port ? port : null;
|
|
5869
6047
|
}
|
|
5870
6048
|
setOutputValue(port, value) {
|
|
5871
|
-
|
|
5872
|
-
|
|
6049
|
+
const staging = this.stagingStack[this.stagingStack.length - 1];
|
|
6050
|
+
if (staging) {
|
|
6051
|
+
staging.set(port, value);
|
|
5873
6052
|
return;
|
|
5874
6053
|
}
|
|
5875
6054
|
this.commitOutputValue(port, value);
|
|
@@ -5884,11 +6063,6 @@ var Node = class _Node {
|
|
|
5884
6063
|
targetPort.onChange?.(value);
|
|
5885
6064
|
}
|
|
5886
6065
|
}
|
|
5887
|
-
// ============ Props System ============
|
|
5888
|
-
/**
|
|
5889
|
-
* Add a parameter to this node
|
|
5890
|
-
* Short alias for defineProp - use in setup()
|
|
5891
|
-
*/
|
|
5892
6066
|
addParm(name, config) {
|
|
5893
6067
|
this.props[name] = config;
|
|
5894
6068
|
this.propsDeclaredByCode.add(name);
|
|
@@ -5989,6 +6163,9 @@ var Node = class _Node {
|
|
|
5989
6163
|
deleteExpression: () => {
|
|
5990
6164
|
const { expression: _, expressionError: __, ...rest } = self.props[name];
|
|
5991
6165
|
self.props[name] = rest;
|
|
6166
|
+
if (self.defaultExpressionsApplied.has(name)) {
|
|
6167
|
+
self.plainPropOverrides.add(name);
|
|
6168
|
+
}
|
|
5992
6169
|
self.updateTimeDependent();
|
|
5993
6170
|
self.markDirty();
|
|
5994
6171
|
},
|
|
@@ -6068,6 +6245,12 @@ var Node = class _Node {
|
|
|
6068
6245
|
...channel ? { channel } : {}
|
|
6069
6246
|
};
|
|
6070
6247
|
}
|
|
6248
|
+
preservePlainProp(name) {
|
|
6249
|
+
this.plainPropOverrides.add(name);
|
|
6250
|
+
}
|
|
6251
|
+
preservesPlainProp(name) {
|
|
6252
|
+
return this.plainPropOverrides.has(name) && this.defaultExpressionsApplied.has(name);
|
|
6253
|
+
}
|
|
6071
6254
|
/** Read a channel back off disk. Bad data yields no channel, never a throw. */
|
|
6072
6255
|
restorePropChannel(name, data) {
|
|
6073
6256
|
if (!this.props[name]) return;
|
|
@@ -6215,6 +6398,14 @@ var Node = class _Node {
|
|
|
6215
6398
|
this.props[name] = prop;
|
|
6216
6399
|
this.props = { ...this.props };
|
|
6217
6400
|
}
|
|
6401
|
+
if (options.defaultExpression && !this.defaultExpressionsApplied.has(name)) {
|
|
6402
|
+
this.defaultExpressionsApplied.add(name);
|
|
6403
|
+
if (!this.plainPropOverrides.has(name) && !prop.expression) {
|
|
6404
|
+
prop.expression = options.defaultExpression;
|
|
6405
|
+
this.props = { ...this.props };
|
|
6406
|
+
this.updateTimeDependent();
|
|
6407
|
+
}
|
|
6408
|
+
}
|
|
6218
6409
|
prop.fromParameter = name;
|
|
6219
6410
|
prop.hidden = true;
|
|
6220
6411
|
const control = paramControlType(parameter.dataType);
|
|
@@ -6812,6 +7003,35 @@ var Node = class _Node {
|
|
|
6812
7003
|
if (!this.hasExecuted || this.cookState !== "clean") return true;
|
|
6813
7004
|
return this.calculateInputHash() !== this.lastInputHash;
|
|
6814
7005
|
}
|
|
7006
|
+
/**
|
|
7007
|
+
* The last cook threw, and nothing has changed since it did.
|
|
7008
|
+
*
|
|
7009
|
+
* Separate from `isDirty` on purpose, and this is the second attempt: folding
|
|
7010
|
+
* it into `isDirty` closed the Viewer's cook loop and broke a contract the
|
|
7011
|
+
* scheduler tests already pinned — `requestOutput` retries a failed node,
|
|
7012
|
+
* which is the transient case (a fetch that timed out, a device lost) and is
|
|
7013
|
+
* right. The two callers are not the same thing wearing different names. An
|
|
7014
|
+
* explicit request means *give me this output now* and should retry; the
|
|
7015
|
+
* Viewer's `requestAnimationFrame` pump is a **poll** that was reaching the
|
|
7016
|
+
* same entry point every 100 ms, and a poll asking again on identical inputs
|
|
7017
|
+
* cannot learn anything.
|
|
7018
|
+
*
|
|
7019
|
+
* So the fix belongs at the poll, and the poll needs a question only the node
|
|
7020
|
+
* can answer. Measured 2026-09-08 by MW-OBSERVATORY-ART against a live sketch:
|
|
7021
|
+
* selecting a failing node produced 93 errors in 10 s — 9.3/sec, median gap
|
|
7022
|
+
* 103.7 ms, identical stack every time — each one a full `scheduler.flush()`
|
|
7023
|
+
* re-running the failing execute and re-fetching its inputs over the network,
|
|
7024
|
+
* for as long as the node stayed selected. `markDirty` is not on that path,
|
|
7025
|
+
* which is why auditing its call sites came back clean, and the Viewer's
|
|
7026
|
+
* overlap guard is why it held 61 fps and never looked like a hang.
|
|
7027
|
+
*
|
|
7028
|
+
* `hasExecuted` is deliberately not consulted: a node that throws on its very
|
|
7029
|
+
* first cook never sets it, and that is the reproduction case.
|
|
7030
|
+
*/
|
|
7031
|
+
get hasSettledFailure() {
|
|
7032
|
+
if (this.cookState !== "error" || this.failedInputHash === null) return false;
|
|
7033
|
+
return this.calculateInputHash() === this.failedInputHash;
|
|
7034
|
+
}
|
|
6815
7035
|
/**
|
|
6816
7036
|
* Mark all downstream nodes as dirty (lazy propagation)
|
|
6817
7037
|
*/
|
|
@@ -6855,7 +7075,8 @@ var Node = class _Node {
|
|
|
6855
7075
|
const startTime = performance.now();
|
|
6856
7076
|
const startMemory = performance.memory?.usedJSHeapSize ?? 0;
|
|
6857
7077
|
const generation = this.cookGeneration;
|
|
6858
|
-
|
|
7078
|
+
const staging = /* @__PURE__ */ new Map();
|
|
7079
|
+
this.stagingStack.push(staging);
|
|
6859
7080
|
this.setCookState("cooking");
|
|
6860
7081
|
let timeoutId;
|
|
6861
7082
|
try {
|
|
@@ -6870,12 +7091,13 @@ var Node = class _Node {
|
|
|
6870
7091
|
]);
|
|
6871
7092
|
if (generation === this.cookGeneration) {
|
|
6872
7093
|
await this.callLifecycleHooks(needsInitialization);
|
|
6873
|
-
for (const [port, value] of
|
|
7094
|
+
for (const [port, value] of staging) {
|
|
6874
7095
|
this.commitOutputValue(port, value);
|
|
6875
7096
|
}
|
|
6876
7097
|
this.error = null;
|
|
6877
7098
|
this.hasExecuted = true;
|
|
6878
7099
|
this.lastInputHash = this.calculateInputHash();
|
|
7100
|
+
this.failedInputHash = null;
|
|
6879
7101
|
const carryReport = this.finishParameterCarryOver();
|
|
6880
7102
|
if (carryReport) _Node.onParametersRetargeted?.(this.id, carryReport);
|
|
6881
7103
|
this.setCookState("clean");
|
|
@@ -6883,20 +7105,23 @@ var Node = class _Node {
|
|
|
6883
7105
|
this.setCookState("stale");
|
|
6884
7106
|
}
|
|
6885
7107
|
this.updateCookInfo(startTime, startMemory);
|
|
6886
|
-
} catch (
|
|
7108
|
+
} catch (thrown) {
|
|
7109
|
+
const err = asError(thrown);
|
|
6887
7110
|
if (generation === this.cookGeneration) {
|
|
6888
7111
|
this.error = err;
|
|
7112
|
+
this.failedInputHash = this.calculateInputHash();
|
|
6889
7113
|
this.setCookState("error");
|
|
6890
7114
|
} else {
|
|
6891
7115
|
this.setCookState("stale");
|
|
6892
7116
|
}
|
|
6893
7117
|
this.updateCookInfo(startTime, startMemory);
|
|
6894
|
-
if (!err.message
|
|
6895
|
-
console.error(`Error executing node ${this.id}
|
|
7118
|
+
if (!err.message.includes("timeout")) {
|
|
7119
|
+
console.error(`Error executing node ${this.id}: ${err.message}`, err);
|
|
6896
7120
|
}
|
|
6897
7121
|
} finally {
|
|
6898
7122
|
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
6899
|
-
|
|
7123
|
+
const index = this.stagingStack.indexOf(staging);
|
|
7124
|
+
if (index >= 0) this.stagingStack.splice(index, 1);
|
|
6900
7125
|
}
|
|
6901
7126
|
}
|
|
6902
7127
|
/**
|
|
@@ -8103,15 +8328,28 @@ var silentProgress = { report: () => {
|
|
|
8103
8328
|
function parameterOptions(definition) {
|
|
8104
8329
|
return {
|
|
8105
8330
|
...definition.label ? { label: definition.label } : {},
|
|
8331
|
+
...definition.expression ? { defaultExpression: definition.expression } : {},
|
|
8332
|
+
...definition.action ? { action: definition.action } : {},
|
|
8106
8333
|
...definition.min === void 0 ? {} : { min: definition.min },
|
|
8107
8334
|
...definition.max === void 0 ? {} : { max: definition.max },
|
|
8108
8335
|
...definition.step === void 0 ? {} : { step: definition.step },
|
|
8109
8336
|
...definition.options ? {
|
|
8110
|
-
|
|
8337
|
+
// A declared label wins; a bare value is its own label, which is what
|
|
8338
|
+
// every definition written before the labelled form meant. Stringifying
|
|
8339
|
+
// unconditionally is what turned "Magnific — costs money" into
|
|
8340
|
+
// "magnific" and threw away the only notice that it bills per call.
|
|
8341
|
+
choices: definition.options.map((option) => option !== null && typeof option === "object" && "value" in option ? {
|
|
8342
|
+
value: option.value,
|
|
8343
|
+
label: String(option.label ?? option.value),
|
|
8344
|
+
...option.disabled ? { disabled: true } : {}
|
|
8345
|
+
} : { value: option, label: String(option) })
|
|
8111
8346
|
} : {},
|
|
8112
8347
|
promotable: false
|
|
8113
8348
|
};
|
|
8114
8349
|
}
|
|
8350
|
+
function asGpuHost(value) {
|
|
8351
|
+
return value && typeof value.ensure === "function" && typeof value.forNode === "function" ? value : null;
|
|
8352
|
+
}
|
|
8115
8353
|
function attachDefinition(node, registration, capabilities) {
|
|
8116
8354
|
const definition = registration.definition;
|
|
8117
8355
|
const pendingTriggers = /* @__PURE__ */ new Map();
|
|
@@ -8186,12 +8424,18 @@ function attachDefinition(node, registration, capabilities) {
|
|
|
8186
8424
|
if (!port) throw new Error(`Missing output ${registration.moduleId}.${name}`);
|
|
8187
8425
|
return [name, output.kind === "trigger" ? { trigger: (payload) => port.trigger(payload) } : { set: (value) => port.setValue(value) }];
|
|
8188
8426
|
}));
|
|
8427
|
+
let nodeCapabilities = capabilities;
|
|
8428
|
+
const gpuHost = (definition.capabilities ?? []).includes("gpu") ? asGpuHost(capabilities.gpu) : null;
|
|
8429
|
+
if (gpuHost) {
|
|
8430
|
+
await gpuHost.ensure();
|
|
8431
|
+
nodeCapabilities = { ...capabilities, gpu: gpuHost.forNode(node.id) };
|
|
8432
|
+
}
|
|
8189
8433
|
const context = {
|
|
8190
8434
|
nodeId: node.id,
|
|
8191
8435
|
inputs: inputValues,
|
|
8192
8436
|
outputs,
|
|
8193
8437
|
props: Object.fromEntries(node.parameters.map((parameter) => [parameter.name, parameter.value])),
|
|
8194
|
-
capabilities,
|
|
8438
|
+
capabilities: nodeCapabilities,
|
|
8195
8439
|
signal: new AbortController().signal,
|
|
8196
8440
|
progress: silentProgress
|
|
8197
8441
|
};
|
|
@@ -8230,8 +8474,362 @@ var browserAssetCapability = {
|
|
|
8230
8474
|
}
|
|
8231
8475
|
};
|
|
8232
8476
|
|
|
8477
|
+
// src/nodes/definition/browserShellCapability.ts
|
|
8478
|
+
var capability = null;
|
|
8479
|
+
function forgetShellCapability() {
|
|
8480
|
+
capability = null;
|
|
8481
|
+
}
|
|
8482
|
+
async function token() {
|
|
8483
|
+
if (capability) return capability;
|
|
8484
|
+
const response = await fetch("/api/shell/capability", { cache: "no-store" });
|
|
8485
|
+
if (!response.ok) {
|
|
8486
|
+
throw new Error(
|
|
8487
|
+
response.status === 404 ? "The shell capability is disabled on this server. It is available on a loopback server, or one started with an explicit trusted host." : `Could not obtain the shell capability (${response.status})`
|
|
8488
|
+
);
|
|
8489
|
+
}
|
|
8490
|
+
const { capability: issued } = await response.json();
|
|
8491
|
+
if (typeof issued !== "string" || !issued) {
|
|
8492
|
+
throw new Error("Server issued no shell capability");
|
|
8493
|
+
}
|
|
8494
|
+
capability = issued;
|
|
8495
|
+
return issued;
|
|
8496
|
+
}
|
|
8497
|
+
function domSignal(signal) {
|
|
8498
|
+
if (!signal) return void 0;
|
|
8499
|
+
const controller = new AbortController();
|
|
8500
|
+
if (signal.aborted) controller.abort(signal.reason);
|
|
8501
|
+
else signal.addEventListener("abort", () => controller.abort(signal.reason), { once: true });
|
|
8502
|
+
return controller.signal;
|
|
8503
|
+
}
|
|
8504
|
+
async function post(body, signal) {
|
|
8505
|
+
const bridged = domSignal(signal);
|
|
8506
|
+
const send = async (issued) => fetch("/api/shell", {
|
|
8507
|
+
method: "POST",
|
|
8508
|
+
headers: {
|
|
8509
|
+
"Content-Type": "application/json",
|
|
8510
|
+
"X-Cascade-Shell-Capability": issued
|
|
8511
|
+
},
|
|
8512
|
+
body: JSON.stringify(body),
|
|
8513
|
+
...bridged ? { signal: bridged } : {}
|
|
8514
|
+
});
|
|
8515
|
+
let response = await send(await token());
|
|
8516
|
+
if (response.status === 403) {
|
|
8517
|
+
capability = null;
|
|
8518
|
+
response = await send(await token());
|
|
8519
|
+
}
|
|
8520
|
+
return response;
|
|
8521
|
+
}
|
|
8522
|
+
function requestBody(command, args, options) {
|
|
8523
|
+
const { signal: _signal, ...rest } = options ?? {};
|
|
8524
|
+
return {
|
|
8525
|
+
command,
|
|
8526
|
+
...args ? { args } : {},
|
|
8527
|
+
...Object.keys(rest).length > 0 ? { options: rest } : {}
|
|
8528
|
+
};
|
|
8529
|
+
}
|
|
8530
|
+
async function fail2(response) {
|
|
8531
|
+
let payload = null;
|
|
8532
|
+
try {
|
|
8533
|
+
payload = await response.json();
|
|
8534
|
+
} catch {
|
|
8535
|
+
}
|
|
8536
|
+
const message2 = typeof payload?.error === "string" ? payload.error : `Shell request failed (${response.status})`;
|
|
8537
|
+
if (payload?.code === "COMMAND_NOT_ALLOWED") {
|
|
8538
|
+
throw new Error(`${message2}. Add it under Allowed Commands in Project Settings.`);
|
|
8539
|
+
}
|
|
8540
|
+
throw new Error(message2);
|
|
8541
|
+
}
|
|
8542
|
+
var browserShellCapability = {
|
|
8543
|
+
async run(command, args, options) {
|
|
8544
|
+
const response = await post(requestBody(command, args, options), options?.signal);
|
|
8545
|
+
if (!response.ok) await fail2(response);
|
|
8546
|
+
const { ok: _ok, ...result } = await response.json();
|
|
8547
|
+
return result;
|
|
8548
|
+
},
|
|
8549
|
+
async runJson(command, args, options) {
|
|
8550
|
+
const result = await this.run(command, args, options);
|
|
8551
|
+
try {
|
|
8552
|
+
return JSON.parse(result.stdout);
|
|
8553
|
+
} catch {
|
|
8554
|
+
throw new Error(`${command} did not print JSON`);
|
|
8555
|
+
}
|
|
8556
|
+
}
|
|
8557
|
+
};
|
|
8558
|
+
async function shellRouteAvailable() {
|
|
8559
|
+
const forget = () => {
|
|
8560
|
+
forgetShellCapability();
|
|
8561
|
+
return false;
|
|
8562
|
+
};
|
|
8563
|
+
if (typeof fetch !== "function") return forget();
|
|
8564
|
+
try {
|
|
8565
|
+
const response = await fetch("/api/shell/capability", { cache: "no-store" });
|
|
8566
|
+
if (!response.ok) return forget();
|
|
8567
|
+
const { capability: issued } = await response.json();
|
|
8568
|
+
if (typeof issued !== "string" || !issued) return forget();
|
|
8569
|
+
capability = issued;
|
|
8570
|
+
return true;
|
|
8571
|
+
} catch {
|
|
8572
|
+
return forget();
|
|
8573
|
+
}
|
|
8574
|
+
}
|
|
8575
|
+
|
|
8576
|
+
// packages/runtime/src/gpu.ts
|
|
8577
|
+
var HOST_SCOPE = "\0host";
|
|
8578
|
+
var MISSING_DEVICE = "The gpu capability was used before its device was ready \u2014 the host must await ensure() before a cook.";
|
|
8579
|
+
var DISPOSED = "The gpu capability has been disposed.";
|
|
8580
|
+
function isGpuHost(value) {
|
|
8581
|
+
return !!value && typeof value.ensure === "function" && typeof value.forNode === "function" && typeof value.dispose === "function";
|
|
8582
|
+
}
|
|
8583
|
+
function createGpuHost(options) {
|
|
8584
|
+
let active = null;
|
|
8585
|
+
let starting = null;
|
|
8586
|
+
let disposal = null;
|
|
8587
|
+
let disposed = false;
|
|
8588
|
+
const caches = /* @__PURE__ */ new Map();
|
|
8589
|
+
const retiring = /* @__PURE__ */ new Set();
|
|
8590
|
+
const retirementErrors = [];
|
|
8591
|
+
function dropCaches() {
|
|
8592
|
+
const errors = [];
|
|
8593
|
+
const scopes = [...caches.values()];
|
|
8594
|
+
caches.clear();
|
|
8595
|
+
for (const entries of scopes) {
|
|
8596
|
+
for (const entry of entries.values()) {
|
|
8597
|
+
try {
|
|
8598
|
+
entry.destroy?.(entry.value);
|
|
8599
|
+
} catch (error) {
|
|
8600
|
+
errors.push(error);
|
|
8601
|
+
}
|
|
8602
|
+
}
|
|
8603
|
+
}
|
|
8604
|
+
return errors;
|
|
8605
|
+
}
|
|
8606
|
+
async function release(acquired, destroyDevice) {
|
|
8607
|
+
if (acquired.released) return [];
|
|
8608
|
+
acquired.released = true;
|
|
8609
|
+
const errors = [];
|
|
8610
|
+
if (destroyDevice) {
|
|
8611
|
+
try {
|
|
8612
|
+
acquired.device.destroy();
|
|
8613
|
+
} catch (error) {
|
|
8614
|
+
errors.push(error);
|
|
8615
|
+
}
|
|
8616
|
+
}
|
|
8617
|
+
try {
|
|
8618
|
+
await acquired.release?.();
|
|
8619
|
+
} catch (error) {
|
|
8620
|
+
errors.push(error);
|
|
8621
|
+
}
|
|
8622
|
+
return errors;
|
|
8623
|
+
}
|
|
8624
|
+
function throwCleanupErrors(errors) {
|
|
8625
|
+
if (errors.length === 1) throw errors[0];
|
|
8626
|
+
if (errors.length > 1) {
|
|
8627
|
+
const combined = new Error("GPU host cleanup failed");
|
|
8628
|
+
Object.assign(combined, { errors });
|
|
8629
|
+
throw combined;
|
|
8630
|
+
}
|
|
8631
|
+
}
|
|
8632
|
+
function watchLoss(acquired) {
|
|
8633
|
+
void acquired.device.lost.then(() => {
|
|
8634
|
+
if (disposed || active !== acquired) return;
|
|
8635
|
+
active = null;
|
|
8636
|
+
retirementErrors.push(...dropCaches());
|
|
8637
|
+
const cleanup = release(acquired, false).then((errors) => {
|
|
8638
|
+
retirementErrors.push(...errors);
|
|
8639
|
+
});
|
|
8640
|
+
retiring.add(cleanup);
|
|
8641
|
+
void cleanup.finally(() => retiring.delete(cleanup));
|
|
8642
|
+
}).catch(() => {
|
|
8643
|
+
});
|
|
8644
|
+
}
|
|
8645
|
+
async function start() {
|
|
8646
|
+
const result = await options.acquire();
|
|
8647
|
+
const acquired = {
|
|
8648
|
+
device: result.device,
|
|
8649
|
+
info: {
|
|
8650
|
+
vendor: result.info?.vendor ?? "",
|
|
8651
|
+
architecture: result.info?.architecture ?? "",
|
|
8652
|
+
device: result.info?.device ?? "",
|
|
8653
|
+
description: result.info?.description ?? ""
|
|
8654
|
+
},
|
|
8655
|
+
limits: readGpuLimits(result.limits),
|
|
8656
|
+
release: result.release,
|
|
8657
|
+
released: false
|
|
8658
|
+
};
|
|
8659
|
+
if (disposed) {
|
|
8660
|
+
const errors = await release(acquired, true);
|
|
8661
|
+
throwCleanupErrors(errors);
|
|
8662
|
+
throw new Error(DISPOSED);
|
|
8663
|
+
}
|
|
8664
|
+
active = acquired;
|
|
8665
|
+
watchLoss(acquired);
|
|
8666
|
+
}
|
|
8667
|
+
const host = {
|
|
8668
|
+
get device() {
|
|
8669
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8670
|
+
return active.device;
|
|
8671
|
+
},
|
|
8672
|
+
get adapterInfo() {
|
|
8673
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8674
|
+
return active.info;
|
|
8675
|
+
},
|
|
8676
|
+
get limits() {
|
|
8677
|
+
return active?.limits ?? {};
|
|
8678
|
+
},
|
|
8679
|
+
cache(key2, create, destroy) {
|
|
8680
|
+
return host.forNode(HOST_SCOPE).cache(key2, create, destroy);
|
|
8681
|
+
},
|
|
8682
|
+
async ensure() {
|
|
8683
|
+
if (disposed) throw new Error(DISPOSED);
|
|
8684
|
+
if (active) return;
|
|
8685
|
+
if (!starting) {
|
|
8686
|
+
const attempt = start();
|
|
8687
|
+
const tracked = attempt.finally(() => {
|
|
8688
|
+
if (starting === tracked) starting = null;
|
|
8689
|
+
});
|
|
8690
|
+
starting = tracked;
|
|
8691
|
+
}
|
|
8692
|
+
await starting;
|
|
8693
|
+
},
|
|
8694
|
+
forNode(nodeId) {
|
|
8695
|
+
return {
|
|
8696
|
+
get device() {
|
|
8697
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8698
|
+
return active.device;
|
|
8699
|
+
},
|
|
8700
|
+
get adapterInfo() {
|
|
8701
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8702
|
+
return active.info;
|
|
8703
|
+
},
|
|
8704
|
+
get limits() {
|
|
8705
|
+
return active?.limits ?? {};
|
|
8706
|
+
},
|
|
8707
|
+
cache(key2, create, destroy) {
|
|
8708
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8709
|
+
let entries = caches.get(nodeId);
|
|
8710
|
+
if (!entries) {
|
|
8711
|
+
entries = /* @__PURE__ */ new Map();
|
|
8712
|
+
caches.set(nodeId, entries);
|
|
8713
|
+
}
|
|
8714
|
+
const existing = entries.get(key2);
|
|
8715
|
+
if (existing) return existing.value;
|
|
8716
|
+
const value = create(active.device);
|
|
8717
|
+
entries.set(key2, {
|
|
8718
|
+
value,
|
|
8719
|
+
destroy
|
|
8720
|
+
});
|
|
8721
|
+
return value;
|
|
8722
|
+
}
|
|
8723
|
+
};
|
|
8724
|
+
},
|
|
8725
|
+
dispose() {
|
|
8726
|
+
disposal ??= performDispose();
|
|
8727
|
+
return disposal;
|
|
8728
|
+
}
|
|
8729
|
+
};
|
|
8730
|
+
async function performDispose() {
|
|
8731
|
+
disposed = true;
|
|
8732
|
+
const errors = dropCaches();
|
|
8733
|
+
const pending = starting;
|
|
8734
|
+
if (pending) {
|
|
8735
|
+
try {
|
|
8736
|
+
await pending;
|
|
8737
|
+
} catch (error) {
|
|
8738
|
+
if (!(error instanceof Error && error.message === DISPOSED))
|
|
8739
|
+
errors.push(error);
|
|
8740
|
+
}
|
|
8741
|
+
}
|
|
8742
|
+
const acquired = active;
|
|
8743
|
+
active = null;
|
|
8744
|
+
if (acquired) errors.push(...await release(acquired, true));
|
|
8745
|
+
await Promise.all([...retiring]);
|
|
8746
|
+
errors.push(...retirementErrors);
|
|
8747
|
+
retirementErrors.length = 0;
|
|
8748
|
+
throwCleanupErrors(errors);
|
|
8749
|
+
}
|
|
8750
|
+
return host;
|
|
8751
|
+
}
|
|
8752
|
+
function readGpuLimits(source) {
|
|
8753
|
+
if (!source || typeof source !== "object") return {};
|
|
8754
|
+
const names = new Set(Object.keys(source));
|
|
8755
|
+
for (let cursor = Object.getPrototypeOf(source); cursor && cursor !== Object.prototype; cursor = Object.getPrototypeOf(cursor)) {
|
|
8756
|
+
for (const key2 of Object.getOwnPropertyNames(cursor)) {
|
|
8757
|
+
if (key2 !== "constructor") names.add(key2);
|
|
8758
|
+
}
|
|
8759
|
+
}
|
|
8760
|
+
const limits = {};
|
|
8761
|
+
for (const key2 of names) {
|
|
8762
|
+
const value = source[key2];
|
|
8763
|
+
if (typeof value === "number") limits[key2] = value;
|
|
8764
|
+
}
|
|
8765
|
+
return limits;
|
|
8766
|
+
}
|
|
8767
|
+
|
|
8768
|
+
// src/nodes/definition/gpuCapability.ts
|
|
8769
|
+
function createStudioGpuCapability() {
|
|
8770
|
+
const gpu = globalThis.navigator?.gpu;
|
|
8771
|
+
if (!gpu) return void 0;
|
|
8772
|
+
return createGpuHost({
|
|
8773
|
+
async acquire() {
|
|
8774
|
+
const adapter = await gpu.requestAdapter();
|
|
8775
|
+
if (!adapter) {
|
|
8776
|
+
throw new Error(
|
|
8777
|
+
"No WebGPU adapter is available \u2014 the browser reports WebGPU but the system offers no GPU to render on."
|
|
8778
|
+
);
|
|
8779
|
+
}
|
|
8780
|
+
const device = await adapter.requestDevice();
|
|
8781
|
+
const info = adapter.info;
|
|
8782
|
+
return {
|
|
8783
|
+
device,
|
|
8784
|
+
info: {
|
|
8785
|
+
vendor: info?.vendor ?? "",
|
|
8786
|
+
architecture: info?.architecture ?? "",
|
|
8787
|
+
device: info?.device ?? "",
|
|
8788
|
+
description: info?.description ?? ""
|
|
8789
|
+
},
|
|
8790
|
+
limits: device.limits ?? adapter.limits
|
|
8791
|
+
};
|
|
8792
|
+
}
|
|
8793
|
+
});
|
|
8794
|
+
}
|
|
8795
|
+
|
|
8233
8796
|
// src/nodes/definition/projectDefinition.ts
|
|
8234
|
-
var studioCapabilities = {
|
|
8797
|
+
var studioCapabilities = {
|
|
8798
|
+
assets: browserAssetCapability,
|
|
8799
|
+
// Installed only where the browser has WebGPU, and absent rather than
|
|
8800
|
+
// stubbed otherwise. That is what makes the failure a sentence: the preflight
|
|
8801
|
+
// reports `runtime/missing-capability` and the adapter throws "requires the
|
|
8802
|
+
// Studio gpu capability", both naming what is missing. A stub that threw on
|
|
8803
|
+
// first use would name nothing.
|
|
8804
|
+
...gpuCapabilityIfAvailable(),
|
|
8805
|
+
/**
|
|
8806
|
+
* Shell is present up front and withdrawn if the route turns out not to be
|
|
8807
|
+
* there, which is the opposite way round from gpu and worth the sentence.
|
|
8808
|
+
*
|
|
8809
|
+
* `navigator.gpu` is a synchronous check, so gpu can be absent from the map
|
|
8810
|
+
* before anything reads it. Whether the shell route exists takes a fetch, and
|
|
8811
|
+
* this object is a module-level const read by three default parameters — so
|
|
8812
|
+
* the choice is between delaying Studio's boot on a request, racing the
|
|
8813
|
+
* probe, or installing and withdrawing. Installing is the one where the
|
|
8814
|
+
* common case (a loopback server, where the route is always there) has no
|
|
8815
|
+
* race at all and no cost.
|
|
8816
|
+
*
|
|
8817
|
+
* What it costs: on a server that does NOT offer the route, a cook in the
|
|
8818
|
+
* first few milliseconds after boot sees the capability present and fails at
|
|
8819
|
+
* the fetch instead of at the gate. That failure still names itself — *"the
|
|
8820
|
+
* shell capability is disabled on this server"* — so it is a worse-shaped
|
|
8821
|
+
* error rather than a silent one, and once the probe lands the preflight is
|
|
8822
|
+
* correct from then on.
|
|
8823
|
+
*/
|
|
8824
|
+
shell: browserShellCapability
|
|
8825
|
+
};
|
|
8826
|
+
void shellRouteAvailable().then((available) => {
|
|
8827
|
+
if (!available) delete studioCapabilities.shell;
|
|
8828
|
+
});
|
|
8829
|
+
function gpuCapabilityIfAvailable() {
|
|
8830
|
+
const gpu = createStudioGpuCapability();
|
|
8831
|
+
return gpu ? { gpu } : {};
|
|
8832
|
+
}
|
|
8235
8833
|
function declaresDefinitionV1(value) {
|
|
8236
8834
|
return typeof value === "object" && value !== null && value.apiVersion === 1;
|
|
8237
8835
|
}
|
|
@@ -9429,7 +10027,9 @@ var Graph = class _Graph {
|
|
|
9429
10027
|
result.parent = node.parent.id;
|
|
9430
10028
|
}
|
|
9431
10029
|
const rawParameterValue = (p) => node.rawParameterValue(p.name);
|
|
9432
|
-
const changedParameters = (node.parameters ?? []).filter(
|
|
10030
|
+
const changedParameters = (node.parameters ?? []).filter(
|
|
10031
|
+
(p) => p.promoted || node.preservesPlainProp(p.name) || JSON.stringify(rawParameterValue(p)) !== JSON.stringify(p.defaultValue)
|
|
10032
|
+
);
|
|
9433
10033
|
const parameters = changedParameters.filter((p) => p.documentField !== "props").map((p) => p.promoted ? { name: p.name, value: rawParameterValue(p), promoted: true } : { name: p.name, value: rawParameterValue(p) });
|
|
9434
10034
|
if (parameters.length > 0) {
|
|
9435
10035
|
result.params = parameters;
|
|
@@ -9628,7 +10228,8 @@ var Graph = class _Graph {
|
|
|
9628
10228
|
value = propData;
|
|
9629
10229
|
}
|
|
9630
10230
|
let normalizedValue = value;
|
|
9631
|
-
|
|
10231
|
+
const declaredColor = node.props[key2]?.type === "color";
|
|
10232
|
+
if (isColorValue(value) && (declaredColor || !Array.isArray(value))) {
|
|
9632
10233
|
const normalized = normalizeColor(value);
|
|
9633
10234
|
normalizedValue = normalized.a !== void 0 && normalized.a !== 1 ? { r: normalized.r, g: normalized.g, b: normalized.b, a: normalized.a } : { r: normalized.r, g: normalized.g, b: normalized.b };
|
|
9634
10235
|
}
|
|
@@ -9644,6 +10245,12 @@ var Graph = class _Graph {
|
|
|
9644
10245
|
}
|
|
9645
10246
|
if (expression && node.props[key2]) {
|
|
9646
10247
|
node.props[key2].expression = expression;
|
|
10248
|
+
} else {
|
|
10249
|
+
node.preservePlainProp(key2);
|
|
10250
|
+
}
|
|
10251
|
+
if (!expression && node.props[key2]?.expression) {
|
|
10252
|
+
delete node.props[key2].expression;
|
|
10253
|
+
node.props[key2] = { ...node.props[key2] };
|
|
9647
10254
|
}
|
|
9648
10255
|
if (propData?.channel && node.props[key2]) {
|
|
9649
10256
|
node.restorePropChannel(key2, propData.channel);
|
|
@@ -9894,6 +10501,81 @@ var CascadeRuntimeError = class extends Error {
|
|
|
9894
10501
|
}
|
|
9895
10502
|
};
|
|
9896
10503
|
|
|
10504
|
+
// packages/runtime/src/camera/index.ts
|
|
10505
|
+
var DEG = Math.PI / 180;
|
|
10506
|
+
function cross(a, b) {
|
|
10507
|
+
return [
|
|
10508
|
+
a[1] * b[2] - a[2] * b[1],
|
|
10509
|
+
a[2] * b[0] - a[0] * b[2],
|
|
10510
|
+
a[0] * b[1] - a[1] * b[0]
|
|
10511
|
+
];
|
|
10512
|
+
}
|
|
10513
|
+
function normalize(v) {
|
|
10514
|
+
const length = Math.hypot(v[0], v[1], v[2]);
|
|
10515
|
+
if (length === 0) return [0, 0, 0];
|
|
10516
|
+
return [v[0] / length, v[1] / length, v[2] / length];
|
|
10517
|
+
}
|
|
10518
|
+
function subtract(a, b) {
|
|
10519
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
10520
|
+
}
|
|
10521
|
+
function dot(a, b) {
|
|
10522
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
10523
|
+
}
|
|
10524
|
+
function lookAtRotation(from, to, up = [0, 1, 0]) {
|
|
10525
|
+
const forward = normalize(subtract(to, from));
|
|
10526
|
+
if (forward[0] === 0 && forward[1] === 0 && forward[2] === 0) return [0, 0, 0];
|
|
10527
|
+
const horizontal = Math.hypot(forward[0], forward[2]);
|
|
10528
|
+
const pitch = Math.atan2(forward[1], horizontal);
|
|
10529
|
+
const yaw = Math.atan2(-forward[0], -forward[2]);
|
|
10530
|
+
return [pitch / DEG, yaw / DEG, rollTowards(forward, pitch, yaw, up) / DEG];
|
|
10531
|
+
}
|
|
10532
|
+
function rollTowards(forward, pitch, yaw, up) {
|
|
10533
|
+
const desired = normalize([
|
|
10534
|
+
up[0] - forward[0] * dot(up, forward),
|
|
10535
|
+
up[1] - forward[1] * dot(up, forward),
|
|
10536
|
+
up[2] - forward[2] * dot(up, forward)
|
|
10537
|
+
]);
|
|
10538
|
+
if (desired[0] === 0 && desired[1] === 0 && desired[2] === 0) return 0;
|
|
10539
|
+
const locked = cameraBasis({
|
|
10540
|
+
...CAMERA_ROTATION_ONLY,
|
|
10541
|
+
rotate: [pitch / DEG, yaw / DEG, 0]
|
|
10542
|
+
}).up;
|
|
10543
|
+
return Math.atan2(dot(cross(desired, locked), forward), dot(locked, desired));
|
|
10544
|
+
}
|
|
10545
|
+
var CAMERA_ROTATION_ONLY = {
|
|
10546
|
+
translate: [0, 0, 0],
|
|
10547
|
+
focal: 50,
|
|
10548
|
+
aperture: 41.4214,
|
|
10549
|
+
near: 1e-3,
|
|
10550
|
+
far: 1e4,
|
|
10551
|
+
resolution: [1, 1],
|
|
10552
|
+
aspect: 1,
|
|
10553
|
+
projection: "perspective",
|
|
10554
|
+
orthowidth: 2
|
|
10555
|
+
};
|
|
10556
|
+
function cameraBasis(camera) {
|
|
10557
|
+
const [rx, ry, rz] = camera.rotate;
|
|
10558
|
+
const sx = Math.sin(rx * DEG);
|
|
10559
|
+
const cx = Math.cos(rx * DEG);
|
|
10560
|
+
const sy = Math.sin(ry * DEG);
|
|
10561
|
+
const cy = Math.cos(ry * DEG);
|
|
10562
|
+
const sz = Math.sin(rz * DEG);
|
|
10563
|
+
const cz = Math.cos(rz * DEG);
|
|
10564
|
+
const m00 = cy * cz + sy * sx * sz;
|
|
10565
|
+
const m01 = -cy * sz + sy * sx * cz;
|
|
10566
|
+
const m02 = sy * cx;
|
|
10567
|
+
const m10 = cx * sz;
|
|
10568
|
+
const m11 = cx * cz;
|
|
10569
|
+
const m12 = -sx;
|
|
10570
|
+
const m20 = -sy * cz + cy * sx * sz;
|
|
10571
|
+
const m21 = sy * sz + cy * sx * cz;
|
|
10572
|
+
const m22 = cy * cx;
|
|
10573
|
+
const right = [m00, m10, m20];
|
|
10574
|
+
const up = [m01, m11, m21];
|
|
10575
|
+
const forward = [-m02, -m12, -m22];
|
|
10576
|
+
return { eye: camera.translate, forward, right, up };
|
|
10577
|
+
}
|
|
10578
|
+
|
|
9897
10579
|
// packages/runtime/src/geometry/attributes.ts
|
|
9898
10580
|
init_src();
|
|
9899
10581
|
function geometryError(code, message2) {
|
|
@@ -10780,6 +11462,134 @@ function utf8Bytes(value) {
|
|
|
10780
11462
|
// packages/runtime/src/runtime.ts
|
|
10781
11463
|
init_src();
|
|
10782
11464
|
|
|
11465
|
+
// packages/runtime/src/builtins/core/camera.ts
|
|
11466
|
+
init_src();
|
|
11467
|
+
var cameraDefinition = {
|
|
11468
|
+
apiVersion: 1,
|
|
11469
|
+
label: "Camera",
|
|
11470
|
+
description: "A camera, following Houdini's /obj/cam.",
|
|
11471
|
+
icon: "Video",
|
|
11472
|
+
runsOn: "portable",
|
|
11473
|
+
inputs: {
|
|
11474
|
+
translate: {
|
|
11475
|
+
kind: "data",
|
|
11476
|
+
type: "vec3",
|
|
11477
|
+
default: [0, 0, 5],
|
|
11478
|
+
description: "World position."
|
|
11479
|
+
},
|
|
11480
|
+
rotate: {
|
|
11481
|
+
kind: "data",
|
|
11482
|
+
type: "vec3",
|
|
11483
|
+
default: [0, 0, 0],
|
|
11484
|
+
description: "Degrees, XYZ order. The camera looks down its own -Z."
|
|
11485
|
+
},
|
|
11486
|
+
focal: {
|
|
11487
|
+
kind: "data",
|
|
11488
|
+
type: "float",
|
|
11489
|
+
default: 50,
|
|
11490
|
+
min: 1,
|
|
11491
|
+
max: 300,
|
|
11492
|
+
description: "Focal length in millimetres."
|
|
11493
|
+
},
|
|
11494
|
+
lookat: {
|
|
11495
|
+
kind: "data",
|
|
11496
|
+
type: "vec3",
|
|
11497
|
+
default: [0, 0, 0],
|
|
11498
|
+
description: "Point to aim at. Used only when Look At is on, and it replaces Rotate."
|
|
11499
|
+
},
|
|
11500
|
+
up: {
|
|
11501
|
+
kind: "data",
|
|
11502
|
+
type: "vec3",
|
|
11503
|
+
default: [0, 1, 0],
|
|
11504
|
+
description: "Up vector for Look At."
|
|
11505
|
+
},
|
|
11506
|
+
spin: {
|
|
11507
|
+
kind: "data",
|
|
11508
|
+
type: "float",
|
|
11509
|
+
default: 0,
|
|
11510
|
+
description: "Degrees added to the Y rotation, after Look At. For turntables."
|
|
11511
|
+
}
|
|
11512
|
+
},
|
|
11513
|
+
outputs: {
|
|
11514
|
+
camera: { kind: "data", type: "camera" }
|
|
11515
|
+
},
|
|
11516
|
+
props: {
|
|
11517
|
+
lookAt: {
|
|
11518
|
+
type: "bool",
|
|
11519
|
+
default: false,
|
|
11520
|
+
label: "Look At",
|
|
11521
|
+
description: "Aim at the Look At point instead of using Rotate."
|
|
11522
|
+
},
|
|
11523
|
+
aperture: {
|
|
11524
|
+
type: "float",
|
|
11525
|
+
default: 41.4214,
|
|
11526
|
+
min: 1,
|
|
11527
|
+
max: 200,
|
|
11528
|
+
label: "Aperture",
|
|
11529
|
+
description: "Horizontal aperture in millimetres. With the default focal length this is a 45 degree view."
|
|
11530
|
+
},
|
|
11531
|
+
resolution: {
|
|
11532
|
+
type: "vec2i",
|
|
11533
|
+
default: [1280, 720],
|
|
11534
|
+
label: "Resolution",
|
|
11535
|
+
description: "Frame size in pixels. The vertical field of view is derived from this."
|
|
11536
|
+
},
|
|
11537
|
+
aspect: {
|
|
11538
|
+
type: "float",
|
|
11539
|
+
default: 1,
|
|
11540
|
+
min: 0.1,
|
|
11541
|
+
max: 10,
|
|
11542
|
+
label: "Pixel Aspect",
|
|
11543
|
+
description: "The aspect of one pixel, not of the frame."
|
|
11544
|
+
},
|
|
11545
|
+
projection: {
|
|
11546
|
+
type: "string",
|
|
11547
|
+
default: "perspective",
|
|
11548
|
+
label: "Projection",
|
|
11549
|
+
control: "select",
|
|
11550
|
+
options: [
|
|
11551
|
+
{ value: "perspective", label: "Perspective" },
|
|
11552
|
+
{ value: "orthographic", label: "Orthographic" }
|
|
11553
|
+
]
|
|
11554
|
+
},
|
|
11555
|
+
orthowidth: {
|
|
11556
|
+
type: "float",
|
|
11557
|
+
default: 2,
|
|
11558
|
+
min: 0.01,
|
|
11559
|
+
label: "Ortho Width",
|
|
11560
|
+
description: "Width of the orthographic frame in world units."
|
|
11561
|
+
},
|
|
11562
|
+
near: { type: "float", default: 1e-3, min: 0, label: "Near Clip" },
|
|
11563
|
+
far: { type: "float", default: 1e4, min: 0, label: "Far Clip" }
|
|
11564
|
+
}
|
|
11565
|
+
};
|
|
11566
|
+
function executeCamera(context) {
|
|
11567
|
+
const { translate, rotate, focal, lookat, up, spin } = context.inputs;
|
|
11568
|
+
const props = context.props;
|
|
11569
|
+
const aimed = props.lookAt ? lookAtRotation(translate, lookat, up) : rotate;
|
|
11570
|
+
const oriented = spin === 0 ? aimed : [aimed[0], aimed[1] + spin, aimed[2]];
|
|
11571
|
+
const camera = {
|
|
11572
|
+
...CAMERA_DEFAULTS,
|
|
11573
|
+
translate,
|
|
11574
|
+
rotate: oriented,
|
|
11575
|
+
focal,
|
|
11576
|
+
aperture: props.aperture,
|
|
11577
|
+
resolution: props.resolution,
|
|
11578
|
+
aspect: props.aspect,
|
|
11579
|
+
projection: props.projection === "orthographic" ? "orthographic" : "perspective",
|
|
11580
|
+
orthowidth: props.orthowidth,
|
|
11581
|
+
near: props.near,
|
|
11582
|
+
far: props.far
|
|
11583
|
+
};
|
|
11584
|
+
context.outputs.camera.set(camera);
|
|
11585
|
+
}
|
|
11586
|
+
var cameraRegistration = {
|
|
11587
|
+
kind: "definition-v1",
|
|
11588
|
+
moduleId: "cascade.core.Camera",
|
|
11589
|
+
definition: cameraDefinition,
|
|
11590
|
+
loadExecute: async () => executeCamera
|
|
11591
|
+
};
|
|
11592
|
+
|
|
10783
11593
|
// packages/runtime/src/builtins/core/random.ts
|
|
10784
11594
|
var randomDefinition = {
|
|
10785
11595
|
apiVersion: 1,
|
|
@@ -11059,7 +11869,8 @@ var coreNodeRegistrations = Object.freeze([
|
|
|
11059
11869
|
nullRegistration,
|
|
11060
11870
|
selectRegistration,
|
|
11061
11871
|
randomRegistration,
|
|
11062
|
-
remapRegistration
|
|
11872
|
+
remapRegistration,
|
|
11873
|
+
cameraRegistration
|
|
11063
11874
|
]);
|
|
11064
11875
|
var registrationsById = new Map(
|
|
11065
11876
|
coreNodeRegistrations.map((registration) => [registration.moduleId, registration])
|
|
@@ -11409,7 +12220,12 @@ function builtinNodeRegistration(moduleId2) {
|
|
|
11409
12220
|
// packages/runtime/src/runtime.ts
|
|
11410
12221
|
var PREFLIGHT_WARNING_CODES = /* @__PURE__ */ new Set([
|
|
11411
12222
|
"runtime/preflight-environment",
|
|
11412
|
-
"runtime/stray-params"
|
|
12223
|
+
"runtime/stray-params",
|
|
12224
|
+
"runtime/unknown-props"
|
|
12225
|
+
]);
|
|
12226
|
+
var RUN_TOLERATED_CODES = /* @__PURE__ */ new Set([
|
|
12227
|
+
"runtime/stray-params",
|
|
12228
|
+
"runtime/unknown-props"
|
|
11413
12229
|
]);
|
|
11414
12230
|
var MutableAbortSignal = class {
|
|
11415
12231
|
constructor(report) {
|
|
@@ -11559,6 +12375,46 @@ var Graph2 = class {
|
|
|
11559
12375
|
this.frame = MIN_FRAME;
|
|
11560
12376
|
this.fps = DEFAULT_FPS;
|
|
11561
12377
|
this.byId = new Map(nodes.map((node) => [node.id, node]));
|
|
12378
|
+
const byInput = /* @__PURE__ */ new Map();
|
|
12379
|
+
const byNode = /* @__PURE__ */ new Map();
|
|
12380
|
+
const connectedInputs = /* @__PURE__ */ new Set();
|
|
12381
|
+
const connectedOutputs = /* @__PURE__ */ new Set();
|
|
12382
|
+
for (const connection of connections) {
|
|
12383
|
+
connectedInputs.add(
|
|
12384
|
+
`${connection.target.nodeId}\0${connection.target.inputName}`
|
|
12385
|
+
);
|
|
12386
|
+
connectedOutputs.add(
|
|
12387
|
+
`${connection.source.nodeId}\0${connection.source.outputName}`
|
|
12388
|
+
);
|
|
12389
|
+
if (connection.kind !== "data") continue;
|
|
12390
|
+
const inputKey = `${connection.target.nodeId}\0${connection.target.inputName}`;
|
|
12391
|
+
const inputConnections = byInput.get(inputKey) ?? [];
|
|
12392
|
+
inputConnections.push(connection);
|
|
12393
|
+
byInput.set(inputKey, inputConnections);
|
|
12394
|
+
const nodeConnections = byNode.get(connection.target.nodeId) ?? [];
|
|
12395
|
+
nodeConnections.push(connection);
|
|
12396
|
+
byNode.set(connection.target.nodeId, nodeConnections);
|
|
12397
|
+
}
|
|
12398
|
+
this.dataConnectionsByInput = new Map(
|
|
12399
|
+
[...byInput].map(([key2, inputConnections]) => [
|
|
12400
|
+
key2,
|
|
12401
|
+
Object.freeze(
|
|
12402
|
+
inputConnections.sort(
|
|
12403
|
+
(first, second) => (first.variadicIndex ?? first.order) - (second.variadicIndex ?? second.order)
|
|
12404
|
+
)
|
|
12405
|
+
)
|
|
12406
|
+
])
|
|
12407
|
+
);
|
|
12408
|
+
this.dataConnectionsByNode = new Map(
|
|
12409
|
+
[...byNode].map(([key2, nodeConnections]) => [
|
|
12410
|
+
key2,
|
|
12411
|
+
Object.freeze(
|
|
12412
|
+
nodeConnections.sort((first, second) => first.order - second.order)
|
|
12413
|
+
)
|
|
12414
|
+
])
|
|
12415
|
+
);
|
|
12416
|
+
this.connectedInputs = connectedInputs;
|
|
12417
|
+
this.connectedOutputs = connectedOutputs;
|
|
11562
12418
|
this.graphInputs = graphBoundaries(nodes, "cascade.core.Input", "inputName");
|
|
11563
12419
|
this.graphOutputs = graphBoundaries(nodes, "cascade.core.Output", "outputName");
|
|
11564
12420
|
this.authoredAnnotations = Object.freeze(
|
|
@@ -11568,9 +12424,7 @@ var Graph2 = class {
|
|
|
11568
12424
|
nodes,
|
|
11569
12425
|
node: (id) => this.byId.get(id),
|
|
11570
12426
|
inputNode: (node, index) => {
|
|
11571
|
-
const feeding = this.
|
|
11572
|
-
(connection) => connection.kind === "data" && connection.target.nodeId === node.id
|
|
11573
|
-
).sort((a, b) => a.order - b.order);
|
|
12427
|
+
const feeding = this.dataConnectionsByNode.get(node.id) ?? [];
|
|
11574
12428
|
const source = feeding[index]?.source.nodeId;
|
|
11575
12429
|
return (source ? this.byId.get(source) : void 0) ?? null;
|
|
11576
12430
|
},
|
|
@@ -11614,9 +12468,7 @@ var Graph2 = class {
|
|
|
11614
12468
|
{
|
|
11615
12469
|
kind: definition.kind,
|
|
11616
12470
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
11617
|
-
connected: this.
|
|
11618
|
-
(connection) => connection.target.nodeId === node.id && connection.target.inputName === name
|
|
11619
|
-
),
|
|
12471
|
+
connected: this.connectedInputs.has(`${node.id}\0${name}`),
|
|
11620
12472
|
...definition.kind === "data" ? { value: snapshot(node.inputs[name]) } : {}
|
|
11621
12473
|
}
|
|
11622
12474
|
]
|
|
@@ -11629,9 +12481,7 @@ var Graph2 = class {
|
|
|
11629
12481
|
{
|
|
11630
12482
|
kind: definition.kind,
|
|
11631
12483
|
type: definition.kind === "trigger" ? "trigger" : definition.type,
|
|
11632
|
-
connected: this.
|
|
11633
|
-
(connection) => connection.source.nodeId === node.id && connection.source.outputName === name
|
|
11634
|
-
),
|
|
12484
|
+
connected: this.connectedOutputs.has(`${node.id}\0${name}`),
|
|
11635
12485
|
...definition.kind === "data" && node.outputs.has(name) ? { value: snapshot(node.outputs.get(name)) } : {}
|
|
11636
12486
|
}
|
|
11637
12487
|
]
|
|
@@ -11981,11 +12831,7 @@ var Graph2 = class {
|
|
|
11981
12831
|
const index = integerProp(node.props.inputIndex);
|
|
11982
12832
|
return [name, this.dataInputValue(parent, `input_${index}`)];
|
|
11983
12833
|
}
|
|
11984
|
-
const connections = this.
|
|
11985
|
-
(item) => item.kind === "data" && item.target.nodeId === node.id && item.target.inputName === name
|
|
11986
|
-
).sort(
|
|
11987
|
-
(first, second) => (first.variadicIndex ?? first.order) - (second.variadicIndex ?? second.order)
|
|
11988
|
-
);
|
|
12834
|
+
const connections = this.dataConnectionsByInput.get(`${node.id}\0${name}`) ?? [];
|
|
11989
12835
|
if (definition.variadic) {
|
|
11990
12836
|
const values = connections.map(
|
|
11991
12837
|
(connection2) => this.requireNode(connection2.source.nodeId).outputs.get(
|
|
@@ -12086,14 +12932,20 @@ var Graph2 = class {
|
|
|
12086
12932
|
]
|
|
12087
12933
|
)
|
|
12088
12934
|
);
|
|
12089
|
-
const
|
|
12090
|
-
|
|
12091
|
-
|
|
12935
|
+
const capabilityEntries = [];
|
|
12936
|
+
for (const name of node.definition.capabilities ?? []) {
|
|
12937
|
+
const installed = this.host.capabilities[name];
|
|
12938
|
+
if (name === "gpu" && isGpuHost(installed)) {
|
|
12939
|
+
await installed.ensure();
|
|
12940
|
+
capabilityEntries.push([name, installed.forNode(node.id)]);
|
|
12941
|
+
} else {
|
|
12942
|
+
capabilityEntries.push([
|
|
12092
12943
|
name,
|
|
12093
|
-
name === "shell" ? bindShellToRun(this.host.capabilities.shell, signal) :
|
|
12094
|
-
])
|
|
12095
|
-
|
|
12096
|
-
|
|
12944
|
+
name === "shell" ? bindShellToRun(this.host.capabilities.shell, signal) : installed
|
|
12945
|
+
]);
|
|
12946
|
+
}
|
|
12947
|
+
}
|
|
12948
|
+
const capabilities = Object.freeze(Object.fromEntries(capabilityEntries));
|
|
12097
12949
|
const props = Object.freeze({ ...node.props });
|
|
12098
12950
|
const progress = {
|
|
12099
12951
|
report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event })
|
|
@@ -12156,6 +13008,12 @@ var Graph2 = class {
|
|
|
12156
13008
|
const diagnostics = [];
|
|
12157
13009
|
const push = (code, nodeId, message2) => diagnostics.push({ phase: "preflight", code, path: nodeId, message: message2 });
|
|
12158
13010
|
for (const node of nodes) {
|
|
13011
|
+
if (node.unknownProps.length)
|
|
13012
|
+
push(
|
|
13013
|
+
"runtime/unknown-props",
|
|
13014
|
+
node.id,
|
|
13015
|
+
`${node.moduleId} stores ${node.unknownProps.join(", ")} under "props", and its definition declares no such prop \u2014 ${node.unknownProps.length === 1 ? "that value was" : "those values were"} dropped on load. A prop renamed or removed leaves its value behind; a name that never existed is a typo`
|
|
13016
|
+
);
|
|
12159
13017
|
if (node.strayParams.length)
|
|
12160
13018
|
push(
|
|
12161
13019
|
"runtime/stray-params",
|
|
@@ -12194,12 +13052,12 @@ var Graph2 = class {
|
|
|
12194
13052
|
`${node.moduleId} cannot send textures over the server bridge`
|
|
12195
13053
|
);
|
|
12196
13054
|
} else
|
|
12197
|
-
for (const
|
|
12198
|
-
if (!this.host.capabilities[
|
|
13055
|
+
for (const capability2 of node.definition.capabilities ?? [])
|
|
13056
|
+
if (!this.host.capabilities[capability2])
|
|
12199
13057
|
push(
|
|
12200
13058
|
"runtime/missing-capability",
|
|
12201
13059
|
node.id,
|
|
12202
|
-
`Missing capability ${
|
|
13060
|
+
`Missing capability ${capability2} for ${node.moduleId} (node ${node.id}) \u2014 the ${this.host.environment} host ${this.hostProvides()}`
|
|
12203
13061
|
);
|
|
12204
13062
|
}
|
|
12205
13063
|
return diagnostics;
|
|
@@ -12215,7 +13073,7 @@ var Graph2 = class {
|
|
|
12215
13073
|
}
|
|
12216
13074
|
assertPreflight(nodes) {
|
|
12217
13075
|
const [first] = this.preflightDiagnostics(nodes).filter(
|
|
12218
|
-
(diagnostic2) => !
|
|
13076
|
+
(diagnostic2) => !RUN_TOLERATED_CODES.has(diagnostic2.code)
|
|
12219
13077
|
);
|
|
12220
13078
|
if (first) throw new CascadeRuntimeError(first.code, [first]);
|
|
12221
13079
|
}
|
|
@@ -12229,8 +13087,9 @@ var Graph2 = class {
|
|
|
12229
13087
|
}
|
|
12230
13088
|
runSelection(target) {
|
|
12231
13089
|
if (target.kind === "all") {
|
|
13090
|
+
const gatedDataDescendants = this.gatedDataDescendants();
|
|
12232
13091
|
const initial = this.nodes.filter(
|
|
12233
|
-
(node) => !isGated(node) && !
|
|
13092
|
+
(node) => !isGated(node) && !gatedDataDescendants.has(node.id)
|
|
12234
13093
|
);
|
|
12235
13094
|
const ids = new Set(initial.map((node) => node.id));
|
|
12236
13095
|
let changed = true;
|
|
@@ -12278,8 +13137,24 @@ var Graph2 = class {
|
|
|
12278
13137
|
}
|
|
12279
13138
|
return this.nodes.filter((node) => ids.has(node.id));
|
|
12280
13139
|
}
|
|
12281
|
-
|
|
12282
|
-
|
|
13140
|
+
gatedDataDescendants() {
|
|
13141
|
+
const outgoing = /* @__PURE__ */ new Map();
|
|
13142
|
+
for (const dependency of dataDependencies(this.nodes, this.connections)) {
|
|
13143
|
+
const targets = outgoing.get(dependency.sourceId) ?? [];
|
|
13144
|
+
targets.push(dependency.targetId);
|
|
13145
|
+
outgoing.set(dependency.sourceId, targets);
|
|
13146
|
+
}
|
|
13147
|
+
const queue = this.nodes.filter(isGated).map((node) => node.id);
|
|
13148
|
+
const descendants = new Set(queue);
|
|
13149
|
+
let queueIndex = 0;
|
|
13150
|
+
while (queueIndex < queue.length) {
|
|
13151
|
+
for (const targetId of outgoing.get(queue[queueIndex++]) ?? []) {
|
|
13152
|
+
if (descendants.has(targetId)) continue;
|
|
13153
|
+
descendants.add(targetId);
|
|
13154
|
+
queue.push(targetId);
|
|
13155
|
+
}
|
|
13156
|
+
}
|
|
13157
|
+
return descendants;
|
|
12283
13158
|
}
|
|
12284
13159
|
triggerDataAncestors(nodeId) {
|
|
12285
13160
|
const dependencies = this.dataClosure(nodeId).filter(
|
|
@@ -12454,8 +13329,23 @@ function materialize(authored, registration) {
|
|
|
12454
13329
|
false,
|
|
12455
13330
|
`${authored.id}.${name}`
|
|
12456
13331
|
);
|
|
13332
|
+
if (Array.isArray(authored.props))
|
|
13333
|
+
misuse(
|
|
13334
|
+
"runtime/props-not-a-map",
|
|
13335
|
+
`Node ${authored.id} stores props as a list. "props" is a map of name to value; "params" was the list. Every value in it is being dropped, silently, and the graph would run on its defaults`
|
|
13336
|
+
);
|
|
12457
13337
|
const bindings = {};
|
|
12458
|
-
|
|
13338
|
+
const storedProps = authored.props ?? {};
|
|
13339
|
+
for (const [name, definition] of Object.entries(
|
|
13340
|
+
registration.definition.props ?? {}
|
|
13341
|
+
)) {
|
|
13342
|
+
if (!definition.expression || name in storedProps) continue;
|
|
13343
|
+
bindings[name] = {
|
|
13344
|
+
value: props[name],
|
|
13345
|
+
expression: definition.expression
|
|
13346
|
+
};
|
|
13347
|
+
}
|
|
13348
|
+
for (const [name, saved] of Object.entries(storedProps)) {
|
|
12459
13349
|
const definition = registration.definition.props?.[name];
|
|
12460
13350
|
if (!definition) continue;
|
|
12461
13351
|
const object = saved && typeof saved === "object" && "value" in saved ? saved : null;
|
|
@@ -12466,7 +13356,10 @@ function materialize(authored, registration) {
|
|
|
12466
13356
|
`${authored.id}.${name}`
|
|
12467
13357
|
);
|
|
12468
13358
|
props[name] = value;
|
|
12469
|
-
if (!object)
|
|
13359
|
+
if (!object) {
|
|
13360
|
+
delete bindings[name];
|
|
13361
|
+
continue;
|
|
13362
|
+
}
|
|
12470
13363
|
const channel = deserializeChannel(object.channel);
|
|
12471
13364
|
const expression = typeof object.expression === "string" && object.expression ? object.expression : void 0;
|
|
12472
13365
|
const binding = {
|
|
@@ -12486,9 +13379,16 @@ function materialize(authored, registration) {
|
|
|
12486
13379
|
props,
|
|
12487
13380
|
bindings,
|
|
12488
13381
|
strayParams: strayParams(authored, registration),
|
|
13382
|
+
unknownProps: unknownProps(authored, registration),
|
|
12489
13383
|
outputs: /* @__PURE__ */ new Map()
|
|
12490
13384
|
};
|
|
12491
13385
|
}
|
|
13386
|
+
function unknownProps(authored, registration) {
|
|
13387
|
+
const props = authored.props;
|
|
13388
|
+
if (!props || Array.isArray(props)) return [];
|
|
13389
|
+
const declared = registration.definition.props ?? {};
|
|
13390
|
+
return Object.keys(props).filter((name) => !(name in declared));
|
|
13391
|
+
}
|
|
12492
13392
|
function strayParams(authored, registration) {
|
|
12493
13393
|
const definition = registration.definition;
|
|
12494
13394
|
const declared = /* @__PURE__ */ new Set([
|
|
@@ -12727,6 +13627,7 @@ function validateTopology(nodes, connections) {
|
|
|
12727
13627
|
topological(nodes, connections);
|
|
12728
13628
|
}
|
|
12729
13629
|
function topological(nodes, connections) {
|
|
13630
|
+
const byId = new Map(nodes.map((node) => [node.id, node]));
|
|
12730
13631
|
const indegree = new Map(nodes.map((node) => [node.id, 0]));
|
|
12731
13632
|
const outgoing = new Map(nodes.map((node) => [node.id, []]));
|
|
12732
13633
|
for (const dependency of graphDependencies(nodes, connections)) {
|
|
@@ -12738,13 +13639,14 @@ function topological(nodes, connections) {
|
|
|
12738
13639
|
}
|
|
12739
13640
|
const queue = nodes.filter((node) => indegree.get(node.id) === 0);
|
|
12740
13641
|
const result = [];
|
|
12741
|
-
|
|
12742
|
-
|
|
13642
|
+
let queueIndex = 0;
|
|
13643
|
+
while (queueIndex < queue.length) {
|
|
13644
|
+
const node = queue[queueIndex++];
|
|
12743
13645
|
result.push(node);
|
|
12744
13646
|
for (const id of outgoing.get(node.id)) {
|
|
12745
13647
|
indegree.set(id, indegree.get(id) - 1);
|
|
12746
13648
|
if (indegree.get(id) === 0)
|
|
12747
|
-
queue.push(
|
|
13649
|
+
queue.push(byId.get(id));
|
|
12748
13650
|
}
|
|
12749
13651
|
}
|
|
12750
13652
|
if (result.length !== nodes.length)
|
|
@@ -12932,12 +13834,16 @@ function validateNodeModuleArchitecture(source, fileName, ts3) {
|
|
|
12932
13834
|
ts3.ScriptKind.TS
|
|
12933
13835
|
);
|
|
12934
13836
|
const diagnostics = [];
|
|
13837
|
+
const exportsDefinition = file.statements.some(
|
|
13838
|
+
(statement) => ts3.isVariableStatement(statement) && isDefinition(statement)
|
|
13839
|
+
);
|
|
12935
13840
|
for (const statement of file.statements) {
|
|
12936
13841
|
if (ts3.isVariableStatement(statement) && !isDefinition(statement)) {
|
|
13842
|
+
const superseded = supersededByDefinition(statement);
|
|
12937
13843
|
add(
|
|
12938
13844
|
statement,
|
|
12939
13845
|
"module-state",
|
|
12940
|
-
"Node modules may not declare module-level values; use a pure function or pass state through ports, props, and declared capabilities"
|
|
13846
|
+
superseded ? `\`${superseded}\` belongs inside the definition, not beside it \u2014 a definition-v1 node declares it as \`${superseded}: ...\` in \`export const definition\`, and a module-level copy is the module state this rule forbids. A dynamic node without a definition still declares it here.` : "Node modules may not declare module-level values; use a pure function or pass state through ports, props, and declared capabilities"
|
|
12941
13847
|
);
|
|
12942
13848
|
} else if (ts3.isClassDeclaration(statement) || ts3.isEnumDeclaration(statement) || ts3.isModuleDeclaration(statement)) {
|
|
12943
13849
|
add(
|
|
@@ -12989,6 +13895,13 @@ function validateNodeModuleArchitecture(source, fileName, ts3) {
|
|
|
12989
13895
|
const parent = node.parent;
|
|
12990
13896
|
return !(ts3.isPropertyAccessExpression(parent) && parent.name === node || ts3.isPropertyAssignment(parent) && parent.name === node || ts3.isMethodDeclaration(parent) && parent.name === node || ts3.isPropertyDeclaration(parent) && parent.name === node);
|
|
12991
13897
|
}
|
|
13898
|
+
function supersededByDefinition(statement) {
|
|
13899
|
+
if (!exportsDefinition) return null;
|
|
13900
|
+
const declaration = statement.declarationList.declarations[0];
|
|
13901
|
+
if (statement.declarationList.declarations.length !== 1) return null;
|
|
13902
|
+
if (!declaration || !ts3.isIdentifier(declaration.name)) return null;
|
|
13903
|
+
return declaration.name.text === "icon" || declaration.name.text === "runsOn" ? declaration.name.text : null;
|
|
13904
|
+
}
|
|
12992
13905
|
function isDefinition(statement) {
|
|
12993
13906
|
return Boolean(statement.declarationList.flags & ts3.NodeFlags.Const) && Boolean(statement.modifiers?.some((item) => item.kind === ts3.SyntaxKind.ExportKeyword)) && statement.declarationList.declarations.length === 1 && ts3.isIdentifier(statement.declarationList.declarations[0].name) && statement.declarationList.declarations[0].name.text === "definition";
|
|
12994
13907
|
}
|
|
@@ -13006,15 +13919,15 @@ function validateNodeModuleArchitecture(source, fileName, ts3) {
|
|
|
13006
13919
|
}
|
|
13007
13920
|
function importsSiblingNode(fileName, specifier) {
|
|
13008
13921
|
if (!specifier.startsWith(".")) return false;
|
|
13009
|
-
const file =
|
|
13922
|
+
const file = normalize2(fileName).split("/");
|
|
13010
13923
|
const nodesIndex = file.lastIndexOf("nodes");
|
|
13011
13924
|
if (nodesIndex < 0 || !file[nodesIndex + 1]) return false;
|
|
13012
13925
|
const sourceNode = file[nodesIndex + 1];
|
|
13013
|
-
const resolved =
|
|
13926
|
+
const resolved = normalize2([...file.slice(0, -1), ...specifier.split("/")].join("/")).split("/");
|
|
13014
13927
|
const resolvedNodesIndex = resolved.lastIndexOf("nodes");
|
|
13015
13928
|
return resolvedNodesIndex >= 0 && Boolean(resolved[resolvedNodesIndex + 1]) && resolved[resolvedNodesIndex + 1] !== sourceNode;
|
|
13016
13929
|
}
|
|
13017
|
-
function
|
|
13930
|
+
function normalize2(value) {
|
|
13018
13931
|
const output = [];
|
|
13019
13932
|
for (const part of value.split("\\").join("/").split("/")) {
|
|
13020
13933
|
if (!part || part === ".") continue;
|
|
@@ -13129,6 +14042,41 @@ function imagePath(value) {
|
|
|
13129
14042
|
return null;
|
|
13130
14043
|
}
|
|
13131
14044
|
|
|
14045
|
+
// src/cli/headlessGpu.ts
|
|
14046
|
+
var MISSING_GPU_MESSAGE = 'Cascade cannot load the optional Dawn WebGPU renderer. Install "webgpu@0.6.0" on a supported platform and run again.';
|
|
14047
|
+
function createDawnGpuHost() {
|
|
14048
|
+
return createGpuHost({
|
|
14049
|
+
async acquire() {
|
|
14050
|
+
let dawn;
|
|
14051
|
+
try {
|
|
14052
|
+
dawn = await import("webgpu");
|
|
14053
|
+
} catch (cause) {
|
|
14054
|
+
throw Object.assign(new Error(MISSING_GPU_MESSAGE), { cause });
|
|
14055
|
+
}
|
|
14056
|
+
let gpu = dawn.create([]);
|
|
14057
|
+
let device;
|
|
14058
|
+
try {
|
|
14059
|
+
const adapter = await gpu.requestAdapter();
|
|
14060
|
+
if (!adapter) throw new Error("Dawn found no WebGPU adapter. Check GPU drivers and backend availability; headless rendering still needs a hardware or explicitly configured software adapter.");
|
|
14061
|
+
device = await adapter.requestDevice();
|
|
14062
|
+
const info = adapter.info;
|
|
14063
|
+
return { device, info: {
|
|
14064
|
+
vendor: info.vendor,
|
|
14065
|
+
architecture: info.architecture,
|
|
14066
|
+
device: info.device,
|
|
14067
|
+
description: info.description
|
|
14068
|
+
}, limits: device.limits, release: () => {
|
|
14069
|
+
gpu = void 0;
|
|
14070
|
+
} };
|
|
14071
|
+
} catch (error) {
|
|
14072
|
+
device?.destroy();
|
|
14073
|
+
gpu = void 0;
|
|
14074
|
+
throw error;
|
|
14075
|
+
}
|
|
14076
|
+
}
|
|
14077
|
+
});
|
|
14078
|
+
}
|
|
14079
|
+
|
|
13132
14080
|
// src/cli/projectRuntime.ts
|
|
13133
14081
|
function createProjectHost(file, registrations) {
|
|
13134
14082
|
const project = new ProjectRoot(path9.dirname(path9.resolve(file)));
|
|
@@ -13139,7 +14087,8 @@ function createProjectHost(file, registrations) {
|
|
|
13139
14087
|
// `cascade.geo.SvgExport` declares it — so an export graph passed both
|
|
13140
14088
|
// `validate` and `check` and then refused to run. See ./nodeAssets.ts
|
|
13141
14089
|
// for where the file lands and why.
|
|
13142
|
-
assets: createNodeAssetCapability(project)
|
|
14090
|
+
assets: createNodeAssetCapability(project),
|
|
14091
|
+
gpu: createDawnGpuHost()
|
|
13143
14092
|
});
|
|
13144
14093
|
}
|
|
13145
14094
|
async function preflightAgainstRunHost(file, registrations, document2) {
|
|
@@ -13157,14 +14106,23 @@ async function preflightAgainstRunHost(file, registrations, document2) {
|
|
|
13157
14106
|
try {
|
|
13158
14107
|
const graph = await runtime.load(document2);
|
|
13159
14108
|
if (checkable) diagnostics = graph.preflight();
|
|
13160
|
-
await graph.dispose();
|
|
13161
14109
|
} finally {
|
|
13162
14110
|
await runtime.dispose();
|
|
14111
|
+
await disposeProjectGpu(host);
|
|
13163
14112
|
}
|
|
13164
14113
|
if (!diagnostics.length) return;
|
|
13165
14114
|
const { errors, warnings } = classifyPreflight(diagnostics);
|
|
14115
|
+
reportPreflightWarnings(host, warnings);
|
|
14116
|
+
if (!errors.length) return;
|
|
14117
|
+
throw new Error([
|
|
14118
|
+
`This graph cannot run on the ${host.environment} host that "cascade run" uses:`,
|
|
14119
|
+
...errors.map(line),
|
|
14120
|
+
...capabilityAdvice(host, errors)
|
|
14121
|
+
].join("\n"));
|
|
14122
|
+
}
|
|
14123
|
+
function reportPreflightWarnings(host, warnings) {
|
|
13166
14124
|
const elsewhere = warnings.filter((item) => item.code === "runtime/preflight-environment");
|
|
13167
|
-
const lossy = warnings.filter((item) => item.code === "runtime/stray-params");
|
|
14125
|
+
const lossy = warnings.filter((item) => item.code === "runtime/stray-params" || item.code === "runtime/unknown-props");
|
|
13168
14126
|
if (elsewhere.length) {
|
|
13169
14127
|
console.warn([
|
|
13170
14128
|
`Not checkable here \u2014 ${elsewhere.length === 1 ? "one node" : `${elsewhere.length} nodes`} in this graph target${elsewhere.length === 1 ? "s" : ""} another host:`,
|
|
@@ -13174,17 +14132,11 @@ async function preflightAgainstRunHost(file, registrations, document2) {
|
|
|
13174
14132
|
}
|
|
13175
14133
|
if (lossy.length) {
|
|
13176
14134
|
console.warn([
|
|
13177
|
-
`Stored values were dropped on load \u2014 ${lossy.length === 1 ? "one node" : `${lossy.length} nodes`}
|
|
14135
|
+
`Stored values were dropped on load \u2014 ${lossy.length === 1 ? "one node" : `${lossy.length} nodes`}:`,
|
|
13178
14136
|
...lossy.map(line),
|
|
13179
14137
|
` This graph will run, on the defaults, and look like it worked.`
|
|
13180
14138
|
].join("\n"));
|
|
13181
14139
|
}
|
|
13182
|
-
if (!errors.length) return;
|
|
13183
|
-
throw new Error([
|
|
13184
|
-
`This graph cannot run on the ${host.environment} host that "cascade run" uses:`,
|
|
13185
|
-
...errors.map(line),
|
|
13186
|
-
...capabilityAdvice(host, errors)
|
|
13187
|
-
].join("\n"));
|
|
13188
14140
|
}
|
|
13189
14141
|
function line(item) {
|
|
13190
14142
|
return ` ${item.code}${item.path ? ` [${item.path}]` : ""}: ${item.message}`;
|
|
@@ -13201,8 +14153,8 @@ function capabilityAdvice(host, diagnostics) {
|
|
|
13201
14153
|
const hostCapabilities = CAPABILITIES_BY_ENVIRONMENT[host.environment] ?? [];
|
|
13202
14154
|
return missing.map((name) => hostCapabilities.includes(name) ? ` ${name}: declared in @cascade/contracts but not installed by the host \u2014 drop it from the definition, or install it where the host is built (createProjectHost, src/cli/projectRuntime.ts).` : ` ${name}: a ${environmentOwning(name)} capability. The CLI runs the ${host.environment} host, so no CLI run can supply it.`);
|
|
13203
14155
|
}
|
|
13204
|
-
function environmentOwning(
|
|
13205
|
-
const owners = Object.entries(CAPABILITIES_BY_ENVIRONMENT).filter(([, names]) => names.includes(
|
|
14156
|
+
function environmentOwning(capability2) {
|
|
14157
|
+
const owners = Object.entries(CAPABILITIES_BY_ENVIRONMENT).filter(([, names]) => names.includes(capability2)).map(([environment]) => environment);
|
|
13206
14158
|
return owners.length ? owners.join("/") : "unknown";
|
|
13207
14159
|
}
|
|
13208
14160
|
function reportUnresolved(prepared) {
|
|
@@ -13242,21 +14194,34 @@ async function runDeterministicProjectGraph(file, document2, entryNode) {
|
|
|
13242
14194
|
if (deterministicCount !== nodes.length) {
|
|
13243
14195
|
throw new Error("Mixed deterministic and dynamic graphs are not executable until the bounded legacy adapter is implemented");
|
|
13244
14196
|
}
|
|
14197
|
+
const host = createProjectHost(file, prepared.registrations);
|
|
13245
14198
|
const runtime = createRuntime({
|
|
13246
|
-
host
|
|
14199
|
+
host,
|
|
13247
14200
|
nodes: prepared.registrations
|
|
13248
14201
|
});
|
|
13249
|
-
|
|
13250
|
-
|
|
13251
|
-
|
|
13252
|
-
|
|
13253
|
-
|
|
13254
|
-
|
|
14202
|
+
let graph;
|
|
14203
|
+
try {
|
|
14204
|
+
graph = await runtime.load(document2);
|
|
14205
|
+
reportPreflightWarnings(
|
|
14206
|
+
host,
|
|
14207
|
+
classifyPreflight(graph.preflight()).warnings.filter((item) => item.code === "runtime/stray-params" || item.code === "runtime/unknown-props")
|
|
14208
|
+
);
|
|
14209
|
+
const result = await graph.run(entryNode ? { target: { kind: "node", nodeId: entryNode } } : {});
|
|
14210
|
+
if (result.status !== "completed") {
|
|
14211
|
+
throw new Error(result.diagnostics.map((item) => item.message).join("\n") || `Graph ${result.status}`);
|
|
14212
|
+
}
|
|
14213
|
+
return true;
|
|
14214
|
+
} finally {
|
|
14215
|
+
try {
|
|
14216
|
+
await runtime.dispose();
|
|
14217
|
+
} finally {
|
|
14218
|
+
await disposeProjectGpu(host);
|
|
14219
|
+
}
|
|
13255
14220
|
}
|
|
13256
|
-
return true;
|
|
13257
14221
|
}
|
|
13258
14222
|
async function renderDeterministicProjectFrames(file, document2, options) {
|
|
13259
14223
|
const prepared = await prepare(file, document2, true);
|
|
14224
|
+
reportUnresolved(prepared);
|
|
13260
14225
|
const nodes = Array.isArray(document2?.nodes) ? document2.nodes : [];
|
|
13261
14226
|
const deterministicCount = nodes.filter((node) => prepared.deterministic.has(moduleId(node))).length;
|
|
13262
14227
|
if (!nodes.length || deterministicCount === 0) return null;
|
|
@@ -13264,18 +14229,23 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
13264
14229
|
throw new Error("Mixed deterministic and dynamic graphs are not executable until the bounded legacy adapter is implemented");
|
|
13265
14230
|
}
|
|
13266
14231
|
const project = new ProjectRoot(path9.dirname(path9.resolve(file)));
|
|
14232
|
+
const host = createProjectHost(file, prepared.registrations);
|
|
13267
14233
|
const runtime = createRuntime({
|
|
13268
|
-
host
|
|
14234
|
+
host,
|
|
13269
14235
|
nodes: prepared.registrations
|
|
13270
14236
|
});
|
|
13271
|
-
|
|
13272
|
-
const outDirectory = project.resolve(options.out);
|
|
13273
|
-
await fs7.mkdir(outDirectory, { recursive: true });
|
|
14237
|
+
let graph;
|
|
13274
14238
|
const width = sequenceWidth(options.end);
|
|
13275
14239
|
const frames = [];
|
|
13276
14240
|
const files = [];
|
|
13277
14241
|
let aborted = false;
|
|
14242
|
+
let gpu;
|
|
14243
|
+
const started = performance.now();
|
|
13278
14244
|
try {
|
|
14245
|
+
graph = await runtime.load(document2);
|
|
14246
|
+
reportPreflightWarnings(host, classifyPreflight(graph.preflight()).warnings);
|
|
14247
|
+
const outDirectory = project.resolve(options.out);
|
|
14248
|
+
await fs7.mkdir(outDirectory, { recursive: true });
|
|
13279
14249
|
for (const frame of frameRange(options.start, options.end, options.step ?? 1)) {
|
|
13280
14250
|
if (options.signal?.aborted) {
|
|
13281
14251
|
aborted = true;
|
|
@@ -13307,11 +14277,21 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
13307
14277
|
frames.push(frame);
|
|
13308
14278
|
if (options.verbose) console.log(` frame ${frame}`);
|
|
13309
14279
|
}
|
|
14280
|
+
try {
|
|
14281
|
+
gpu = { renderer: "dawn", adapter: host.capabilities.gpu.adapterInfo };
|
|
14282
|
+
} catch {
|
|
14283
|
+
}
|
|
13310
14284
|
} finally {
|
|
13311
|
-
|
|
13312
|
-
|
|
14285
|
+
try {
|
|
14286
|
+
await runtime.dispose();
|
|
14287
|
+
} finally {
|
|
14288
|
+
await disposeProjectGpu(host);
|
|
14289
|
+
}
|
|
13313
14290
|
}
|
|
13314
|
-
return { frames, files, aborted };
|
|
14291
|
+
return { frames, files, aborted, fps: graph.getFps(), durationMs: performance.now() - started, ...gpu ? { gpu } : {} };
|
|
14292
|
+
}
|
|
14293
|
+
async function disposeProjectGpu(host) {
|
|
14294
|
+
await host.capabilities.gpu?.dispose();
|
|
13315
14295
|
}
|
|
13316
14296
|
function imageOutputs(graph, entryNode) {
|
|
13317
14297
|
const found = [];
|
|
@@ -13328,6 +14308,7 @@ function imageOutputs(graph, entryNode) {
|
|
|
13328
14308
|
}
|
|
13329
14309
|
async function inspectProjectGraph(file, document2) {
|
|
13330
14310
|
const prepared = await prepare(file, document2, false);
|
|
14311
|
+
reportUnresolved(prepared);
|
|
13331
14312
|
const registrations = new Map(
|
|
13332
14313
|
[...builtinNodeRegistrations, ...prepared.registrations].map((item) => [item.moduleId, item])
|
|
13333
14314
|
);
|
|
@@ -13376,6 +14357,8 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
13376
14357
|
const mentionsDefinition = /\bexport\s+const\s+definition\b/.test(source);
|
|
13377
14358
|
const extracted = extractNodeDefinition(source, moduleFile, ts2);
|
|
13378
14359
|
if (!extracted.ok) {
|
|
14360
|
+
const reExport = reExportedDefinition(source);
|
|
14361
|
+
if (reExport) throw new Error(formatDiagnostic(reExport(moduleFile)));
|
|
13379
14362
|
if (!mentionsDefinition && extracted.diagnostics.every((item) => item.code === "definition/missing-export")) {
|
|
13380
14363
|
registrations.push(savedRegistration(id, node));
|
|
13381
14364
|
continue;
|
|
@@ -13415,6 +14398,18 @@ async function projectModuleFile(root, node, id) {
|
|
|
13415
14398
|
tried.push(candidate);
|
|
13416
14399
|
return { file: await exists(candidate) ? candidate : null, tried };
|
|
13417
14400
|
}
|
|
14401
|
+
function reExportedDefinition(source) {
|
|
14402
|
+
const named = /\bexport\s*\{[^}]*\bdefinition\b[^}]*\}\s*from\s*['"]/.test(source);
|
|
14403
|
+
const star = /\bexport\s*\*\s*from\s*['"]/.test(source);
|
|
14404
|
+
if (!named && !star) return null;
|
|
14405
|
+
const what = named ? "this module re-exports `definition` from another file" : "this module has a star re-export and no `definition` of its own";
|
|
14406
|
+
return (file) => ({
|
|
14407
|
+
phase: "prepare",
|
|
14408
|
+
code: "definition/re-exported",
|
|
14409
|
+
file,
|
|
14410
|
+
message: `${what}, and a definition cannot arrive that way \u2014 it is read statically from this file. Write the literal here, or keep the stub for \`execute\` only and leave this module dynamic. Without this the node loads as dynamic and every command reports success.`
|
|
14411
|
+
});
|
|
14412
|
+
}
|
|
13418
14413
|
function unresolvedModule(id, node, tried, root) {
|
|
13419
14414
|
const relative = tried.map((candidate) => path9.relative(root, candidate) || candidate);
|
|
13420
14415
|
const where = relative.length ? `looked for ${relative.join(" and ")}` : `no project file is possible for this module id \u2014 a project module is named "project.<dir>" or carries a source file`;
|
|
@@ -13422,7 +14417,7 @@ function unresolvedModule(id, node, tried, root) {
|
|
|
13422
14417
|
phase: "prepare",
|
|
13423
14418
|
code: "project/module-not-found",
|
|
13424
14419
|
path: node.id,
|
|
13425
|
-
message: `${id} has no module file \u2014 ${where}.
|
|
14420
|
+
message: `${id} has no module file \u2014 ${where}. Only known legacy modules can use saved-port compatibility, for inspection only; unknown and explicit project modules are rejected`
|
|
13426
14421
|
};
|
|
13427
14422
|
}
|
|
13428
14423
|
async function compileExecute(projectRoot, moduleFile) {
|
|
@@ -17099,28 +18094,50 @@ async function runGraph(options) {
|
|
|
17099
18094
|
if (verbose) console.warn(canvasError.message);
|
|
17100
18095
|
}
|
|
17101
18096
|
const frameSpec = frames ? parseFrameSpec(frames) : null;
|
|
18097
|
+
let hostReleased = false;
|
|
17102
18098
|
const releaseHost = () => {
|
|
17103
|
-
|
|
17104
|
-
|
|
17105
|
-
|
|
18099
|
+
if (hostReleased) return;
|
|
18100
|
+
hostReleased = true;
|
|
18101
|
+
const errors = [];
|
|
18102
|
+
for (const dispose of [
|
|
18103
|
+
disposeStageBridge,
|
|
18104
|
+
disposeIoBridge,
|
|
18105
|
+
canvasHost ? () => canvasHost.dispose() : void 0,
|
|
18106
|
+
() => setProjectModuleCompiler(null),
|
|
18107
|
+
() => setEmbeddedCompiler(null)
|
|
18108
|
+
]) {
|
|
18109
|
+
try {
|
|
18110
|
+
dispose?.();
|
|
18111
|
+
} catch (error) {
|
|
18112
|
+
errors.push(error);
|
|
18113
|
+
}
|
|
18114
|
+
}
|
|
18115
|
+
if (errors.length) throw errors[0];
|
|
17106
18116
|
};
|
|
17107
|
-
|
|
17108
|
-
|
|
17109
|
-
|
|
17110
|
-
|
|
17111
|
-
|
|
17112
|
-
|
|
17113
|
-
|
|
17114
|
-
|
|
17115
|
-
|
|
18117
|
+
try {
|
|
18118
|
+
if (frameSpec) {
|
|
18119
|
+
const rendered = await renderDeterministicProjectFrames(file, graphData, {
|
|
18120
|
+
...frameSpec,
|
|
18121
|
+
...fps === void 0 ? {} : { fps },
|
|
18122
|
+
out: out ?? "renders",
|
|
18123
|
+
...entryNode ? { entryNode } : {},
|
|
18124
|
+
...verbose ? { verbose } : {}
|
|
18125
|
+
});
|
|
18126
|
+
if (rendered) {
|
|
18127
|
+
releaseHost();
|
|
18128
|
+
if (options.json) process.stdout.write(JSON.stringify({ status: "completed", graph: path13.resolve(file), ...rendered }) + "\n");
|
|
18129
|
+
else console.log(`Rendered ${rendered.frames.length} frames, ${rendered.files.length} files -> ${out ?? "renders"}/`);
|
|
18130
|
+
return;
|
|
18131
|
+
}
|
|
18132
|
+
if (options.json) throw new Error("--json rendering requires a fully definition-v1 graph");
|
|
18133
|
+
} else if (await runDeterministicProjectGraph(file, graphData, entryNode)) {
|
|
18134
|
+
if (verbose) console.log("Graph execution completed");
|
|
17116
18135
|
releaseHost();
|
|
17117
|
-
console.log(`Rendered ${rendered.frames.length} frames, ${rendered.files.length} files -> ${out ?? "renders"}/`);
|
|
17118
18136
|
return;
|
|
17119
18137
|
}
|
|
17120
|
-
}
|
|
17121
|
-
if (verbose) console.log("Graph execution completed");
|
|
18138
|
+
} catch (error) {
|
|
17122
18139
|
releaseHost();
|
|
17123
|
-
|
|
18140
|
+
throw error;
|
|
17124
18141
|
}
|
|
17125
18142
|
let activeGraph;
|
|
17126
18143
|
try {
|
|
@@ -17230,14 +18247,60 @@ async function runGraph(options) {
|
|
|
17230
18247
|
} finally {
|
|
17231
18248
|
activeGraph?.scheduler?.dispose?.();
|
|
17232
18249
|
releaseHost();
|
|
17233
|
-
setProjectModuleCompiler(null);
|
|
17234
|
-
setEmbeddedCompiler(null);
|
|
17235
18250
|
}
|
|
17236
18251
|
}
|
|
17237
18252
|
|
|
17238
18253
|
// src/cli/index.ts
|
|
17239
18254
|
import * as fs22 from "fs/promises";
|
|
17240
18255
|
import { readFileSync } from "node:fs";
|
|
18256
|
+
|
|
18257
|
+
// src/cli/supervise.ts
|
|
18258
|
+
import { spawn as spawn2 } from "node:child_process";
|
|
18259
|
+
function superviseProcess(command, args, timeoutMs) {
|
|
18260
|
+
return new Promise((resolve2, reject) => {
|
|
18261
|
+
const child = spawn2(command, args, { stdio: "inherit", detached: process.platform !== "win32" });
|
|
18262
|
+
let timedOut = false;
|
|
18263
|
+
let cancelled = false;
|
|
18264
|
+
let grace;
|
|
18265
|
+
const kill = (signal) => {
|
|
18266
|
+
if (!child.pid) return;
|
|
18267
|
+
try {
|
|
18268
|
+
if (process.platform === "win32") child.kill(signal);
|
|
18269
|
+
else process.kill(-child.pid, signal);
|
|
18270
|
+
} catch (error) {
|
|
18271
|
+
if (error.code !== "ESRCH") throw error;
|
|
18272
|
+
}
|
|
18273
|
+
};
|
|
18274
|
+
const cancel = () => {
|
|
18275
|
+
cancelled = true;
|
|
18276
|
+
kill("SIGTERM");
|
|
18277
|
+
grace ??= setTimeout(() => kill("SIGKILL"), 1e3);
|
|
18278
|
+
};
|
|
18279
|
+
process.on("SIGINT", cancel);
|
|
18280
|
+
process.on("SIGTERM", cancel);
|
|
18281
|
+
const timeout = setTimeout(() => {
|
|
18282
|
+
timedOut = true;
|
|
18283
|
+
console.error(`Render timed out after ${timeoutMs} ms`);
|
|
18284
|
+
kill("SIGKILL");
|
|
18285
|
+
}, timeoutMs);
|
|
18286
|
+
const cleanup = () => {
|
|
18287
|
+
clearTimeout(timeout);
|
|
18288
|
+
clearTimeout(grace);
|
|
18289
|
+
process.off("SIGINT", cancel);
|
|
18290
|
+
process.off("SIGTERM", cancel);
|
|
18291
|
+
};
|
|
18292
|
+
child.once("error", (error) => {
|
|
18293
|
+
cleanup();
|
|
18294
|
+
reject(error);
|
|
18295
|
+
});
|
|
18296
|
+
child.once("close", (code) => {
|
|
18297
|
+
cleanup();
|
|
18298
|
+
resolve2(timedOut ? 124 : cancelled ? 130 : code ?? 1);
|
|
18299
|
+
});
|
|
18300
|
+
});
|
|
18301
|
+
}
|
|
18302
|
+
|
|
18303
|
+
// src/cli/index.ts
|
|
17241
18304
|
var VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
17242
18305
|
async function main() {
|
|
17243
18306
|
const args = process.argv.slice(2);
|
|
@@ -17282,6 +18345,8 @@ Options:
|
|
|
17282
18345
|
--frames <range> Render a frame sequence: 1-100, 1-100x2 (step), or 42
|
|
17283
18346
|
--fps <number> Frame rate to evaluate the range at
|
|
17284
18347
|
--out <dir> Sequence directory, project-relative (default: renders)
|
|
18348
|
+
--json Return a JSON render manifest (requires run --frames)
|
|
18349
|
+
--timeout <ms> Bound a run in a supervised child process (exit 124 on timeout)
|
|
17285
18350
|
--validate-only Validate only (same as 'validate' command)
|
|
17286
18351
|
--verbose, -v Show verbose output
|
|
17287
18352
|
--version Show version
|
|
@@ -17359,7 +18424,24 @@ image output nothing downstream consumes, or per output of --entry-node.
|
|
|
17359
18424
|
}
|
|
17360
18425
|
const out = valueOf("--out");
|
|
17361
18426
|
if (out) options.out = out;
|
|
18427
|
+
if (args.includes("--json")) {
|
|
18428
|
+
if (command !== "run" || !options.frames) throw new Error("--json requires run --frames (use --frames 1 for a still)");
|
|
18429
|
+
options.json = true;
|
|
18430
|
+
}
|
|
18431
|
+
const timeout = valueOf("--timeout");
|
|
18432
|
+
if (timeout !== void 0) {
|
|
18433
|
+
const ms = Number(timeout);
|
|
18434
|
+
if (command !== "run" || !Number.isSafeInteger(ms) || ms <= 0 || ms > 2147483647) {
|
|
18435
|
+
throw new Error("--timeout requires run and a positive integer number of milliseconds up to 2147483647");
|
|
18436
|
+
}
|
|
18437
|
+
const index = args.indexOf("--timeout");
|
|
18438
|
+
const childArgs = [...args.slice(0, index), ...args.slice(index + 2)];
|
|
18439
|
+
if (childArgs.includes("--timeout")) throw new Error("--timeout may only be specified once");
|
|
18440
|
+
process.exitCode = await superviseProcess(process.execPath, [...process.execArgv, process.argv[1], ...childArgs], ms);
|
|
18441
|
+
return;
|
|
18442
|
+
}
|
|
17362
18443
|
try {
|
|
18444
|
+
if (options.json) console.log = console.error;
|
|
17363
18445
|
await runGraph(options);
|
|
17364
18446
|
} catch (error) {
|
|
17365
18447
|
console.error(`Error: ${error.message}`);
|