@field/cascade 0.3.1 → 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 +51 -1
- package/CONTRIBUTING.md +71 -0
- package/DESIGN.md +19 -13
- package/README.md +42 -11
- package/dist/assets/{CodeEditor-D22fVi-4.js → CodeEditor-BNOJIDzL.js} +4 -4
- package/dist/assets/{CodePanel-DPNEYfcR.js → CodePanel-2wp2G0Fj.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DeHCAvgb.js → CustomNodeDialog-DNqaVBqA.js} +1 -1
- package/dist/assets/{ProjectSettings-DWpVtWEx.js → ProjectSettings-DcdYF2E1.js} +1 -1
- package/dist/assets/{SettingsDialog-za00IS3L.js → SettingsDialog-D2xVvyiR.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DTMGhI0_.js → VersionHistoryDialog-DW288ykF.js} +1 -1
- package/dist/assets/{core-JsyKMO1k.js → core-B1y38i95.js} +1 -1
- package/dist/assets/{cssMode-emdRheFK.js → cssMode-BLucnaln.js} +1 -1
- package/dist/assets/{editor.api-xHX-fMSo.js → editor.api-B3Et3Pph.js} +1 -1
- package/dist/assets/{freemarker2-C3M8_Gs3.js → freemarker2-DVlPoano.js} +1 -1
- package/dist/assets/geo-DhyFdBXX.js +1 -0
- package/dist/assets/{handlebars-CBMurtFk.js → handlebars-B5FZBAMR.js} +1 -1
- package/dist/assets/{html-xbLQwknw.js → html-FodD8KbR.js} +1 -1
- package/dist/assets/{htmlMode-2wKdDTon.js → htmlMode-CsG57VCD.js} +1 -1
- package/dist/assets/{image-i1j6nWKe.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-DJaOo_2I.js → javascript-B3gonF_H.js} +1 -1
- package/dist/assets/{jsonMode-Bheas8_0.js → jsonMode-Y3w1ahZg.js} +1 -1
- package/dist/assets/{liquid-BCj_BGbM.js → liquid-CTpWXIYO.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BfIsx_xz.js → lspLanguageFeatures-CODBdKEZ.js} +1 -1
- package/dist/assets/{mdx-DwiQDiVO.js → mdx-CDAQJZIz.js} +1 -1
- package/dist/assets/{python-BjrB5nqj.js → python-LOIlxJU-.js} +1 -1
- package/dist/assets/{razor-CfoBmGDs.js → razor-CllPQSHo.js} +1 -1
- package/dist/assets/{tsMode-BcEoMSxp.js → tsMode-CO28ut8t.js} +1 -1
- package/dist/assets/{typescript-DIg7TEmQ.js → typescript-ruU9wKQ_.js} +1 -1
- package/dist/assets/{workers-vXYuwtPw.js → workers-Bc1Ywp4-.js} +1 -1
- package/dist/assets/{xml-DexLQIQy.js → xml-DoXP2Dh1.js} +1 -1
- package/dist/assets/{yaml-DUnD_1dE.js → yaml-D9YPyGiJ.js} +1 -1
- package/dist/cli/index.js +999 -185
- package/dist/cli/runtime/gpu.ts +71 -0
- package/dist/contracts/camera.d.ts +70 -0
- package/dist/contracts/camera.js +27 -0
- package/dist/contracts/capabilities.d.ts +6 -6
- package/dist/contracts/definition.d.ts +41 -2
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/schema.js +25 -2
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +19 -3
- package/dist/contracts/values.d.ts +3 -2
- package/dist/contracts/values.js +1 -0
- package/dist/gpu.d.ts +17 -0
- package/dist/gpu.js +61 -1
- 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.js +63 -6
- package/dist/runtime/series/index.d.ts +41 -0
- package/dist/runtime/series/index.js +167 -0
- package/dist/runtime/types.d.ts +1 -1
- package/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +36 -0
- package/doc/PROJECT_AUTHORING.md +106 -20
- package/package.json +10 -2
- package/dist/assets/geo-BAR6lQp5.js +0 -1
- package/dist/assets/index-BDV5xGBZ.css +0 -1
- package/dist/assets/index-CMcH8AgP.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(
|
|
@@ -814,9 +865,9 @@ var init_validate = __esm({
|
|
|
814
865
|
CORE = new Set(CORE_TYPES);
|
|
815
866
|
CONTROLS = /* @__PURE__ */ new Set(["number", "slider", "range", "int", "boolean", "text", "textarea", "select", "vector", "matrix", "color", "image", "asset"]);
|
|
816
867
|
CAPABILITIES_BY_ENVIRONMENT = {
|
|
817
|
-
portable: ["assets", "media"],
|
|
868
|
+
portable: ["assets", "media", "gpu"],
|
|
818
869
|
browser: ["assets", "media", "gpu"],
|
|
819
|
-
server: ["files", "assets", "media", "python", "shell"]
|
|
870
|
+
server: ["files", "assets", "media", "python", "shell", "gpu"]
|
|
820
871
|
};
|
|
821
872
|
ALLOWED = CAPABILITIES_BY_ENVIRONMENT;
|
|
822
873
|
fields = (names) => new Set(names.split(" "));
|
|
@@ -828,7 +879,7 @@ var init_validate = __esm({
|
|
|
828
879
|
);
|
|
829
880
|
OUTPUT_FIELDS = fields("kind type description");
|
|
830
881
|
PROP_FIELDS = fields(
|
|
831
|
-
"type default label description min max step accept control options expression"
|
|
882
|
+
"type default label description min max step accept control options expression action"
|
|
832
883
|
);
|
|
833
884
|
}
|
|
834
885
|
});
|
|
@@ -838,11 +889,13 @@ var init_src = __esm({
|
|
|
838
889
|
"packages/contracts/src/index.ts"() {
|
|
839
890
|
"use strict";
|
|
840
891
|
init_animation();
|
|
892
|
+
init_camera();
|
|
841
893
|
init_capabilities();
|
|
842
894
|
init_color();
|
|
843
895
|
init_definition();
|
|
844
896
|
init_document();
|
|
845
897
|
init_geometry();
|
|
898
|
+
init_series();
|
|
846
899
|
init_validate();
|
|
847
900
|
init_values();
|
|
848
901
|
}
|
|
@@ -2886,7 +2939,7 @@ var init_panels = __esm({
|
|
|
2886
2939
|
});
|
|
2887
2940
|
|
|
2888
2941
|
// server/src/pythonWorker.ts
|
|
2889
|
-
import { spawn as
|
|
2942
|
+
import { spawn as spawn3 } from "child_process";
|
|
2890
2943
|
import readline from "readline";
|
|
2891
2944
|
var REQUEST_TIMEOUT_MS, PythonWorker;
|
|
2892
2945
|
var init_pythonWorker = __esm({
|
|
@@ -2906,7 +2959,7 @@ var init_pythonWorker = __esm({
|
|
|
2906
2959
|
ensureStarted() {
|
|
2907
2960
|
if (this.proc && !this.proc.killed) return this.proc;
|
|
2908
2961
|
const scriptPath = this.project.resolve(this.entrypoint);
|
|
2909
|
-
const proc =
|
|
2962
|
+
const proc = spawn3(this.python, [scriptPath], { cwd: this.project.root, env: this.env });
|
|
2910
2963
|
readline.createInterface({ input: proc.stdout }).on("line", (line2) => this.handleLine(line2));
|
|
2911
2964
|
proc.stderr.on("data", (chunk) => console.error("[python-worker]", chunk.toString().trimEnd()));
|
|
2912
2965
|
proc.on("exit", (code) => {
|
|
@@ -3001,8 +3054,8 @@ function createProjectRequestBoundary(options) {
|
|
|
3001
3054
|
function createBrowserCapabilityBoundary(options, label, header) {
|
|
3002
3055
|
const origins = new Set(options.trustedOrigins.map((origin) => validateOrigin(origin, label)));
|
|
3003
3056
|
const authorities = allowedAuthorities(options, origins);
|
|
3004
|
-
const
|
|
3005
|
-
const expectedBytes = Buffer.from(
|
|
3057
|
+
const capability2 = randomBytes(32).toString("base64url");
|
|
3058
|
+
const expectedBytes = Buffer.from(capability2);
|
|
3006
3059
|
return {
|
|
3007
3060
|
guardOrigin(req, res, next) {
|
|
3008
3061
|
const origin = req.get("Origin");
|
|
@@ -3022,7 +3075,7 @@ function createBrowserCapabilityBoundary(options, label, header) {
|
|
|
3022
3075
|
issueCapability(_req, res) {
|
|
3023
3076
|
res.setHeader("Cache-Control", "no-store, private");
|
|
3024
3077
|
res.setHeader("Pragma", "no-cache");
|
|
3025
|
-
res.json({ capability });
|
|
3078
|
+
res.json({ capability: capability2 });
|
|
3026
3079
|
},
|
|
3027
3080
|
requireCapability(req, res, next) {
|
|
3028
3081
|
const supplied = req.get(header);
|
|
@@ -3040,12 +3093,36 @@ function allowedAuthorities(options, origins) {
|
|
|
3040
3093
|
if (isLoopbackHost(options.host)) {
|
|
3041
3094
|
authorities.add(`127.0.0.1:${options.port}`);
|
|
3042
3095
|
authorities.add(`localhost:${options.port}`);
|
|
3043
|
-
} else if (options.
|
|
3096
|
+
} else if (!isWildcardHost(options.host)) {
|
|
3044
3097
|
authorities.add(authority(options.host, options.port));
|
|
3045
3098
|
}
|
|
3046
|
-
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
|
+
}
|
|
3047
3105
|
return authorities;
|
|
3048
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
|
+
}
|
|
3049
3126
|
function authority(host, port) {
|
|
3050
3127
|
const normalized = host.trim().toLowerCase().replace(/^\[|\]$/g, "");
|
|
3051
3128
|
if (!normalized || /[\s/?#@]/.test(normalized)) throw new Error(`Invalid trusted host: ${host}`);
|
|
@@ -3403,7 +3480,7 @@ var init_allowlist = __esm({
|
|
|
3403
3480
|
});
|
|
3404
3481
|
|
|
3405
3482
|
// server/src/agent/session.ts
|
|
3406
|
-
import { spawn as
|
|
3483
|
+
import { spawn as spawn4 } from "node:child_process";
|
|
3407
3484
|
function childEnvironment() {
|
|
3408
3485
|
const env = { ...process.env };
|
|
3409
3486
|
for (const key2 of Object.keys(env)) {
|
|
@@ -3633,7 +3710,7 @@ var init_session = __esm({
|
|
|
3633
3710
|
const resumed = Boolean(state.sessionId);
|
|
3634
3711
|
const args = argsFor(request.agent, request.prompt, state.sessionId, agentExtraArgs(this.project.root, request.agent));
|
|
3635
3712
|
run.emit({ type: "started", agent: request.agent, sketch: request.sketch, resumed });
|
|
3636
|
-
const child =
|
|
3713
|
+
const child = spawn4(executable, args, {
|
|
3637
3714
|
cwd: this.project.root,
|
|
3638
3715
|
env: childEnvironment(),
|
|
3639
3716
|
shell: false,
|
|
@@ -4297,7 +4374,27 @@ function startServer(project, opts = {}) {
|
|
|
4297
4374
|
const trustedHosts = Object.freeze([...opts.trustedHosts ?? []]);
|
|
4298
4375
|
const defaultOrigins = [
|
|
4299
4376
|
...isLoopbackHost(HOST) ? [`http://127.0.0.1:${PORT}`, `http://localhost:${PORT}`] : [],
|
|
4300
|
-
|
|
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
|
+
})
|
|
4301
4398
|
];
|
|
4302
4399
|
const security = Object.freeze({
|
|
4303
4400
|
host: HOST,
|
|
@@ -4356,7 +4453,7 @@ function startServer(project, opts = {}) {
|
|
|
4356
4453
|
server.listen(PORT, HOST, () => {
|
|
4357
4454
|
console.log(`\u{1F680} Cascade running on http://${HOST}:${PORT}`);
|
|
4358
4455
|
console.log(`\u{1F4C1} Project: ${project.root}${project.isGitRepo ? " (git)" : " (not a git repo yet)"}`);
|
|
4359
|
-
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)`);
|
|
4360
4457
|
});
|
|
4361
4458
|
return server;
|
|
4362
4459
|
}
|
|
@@ -4455,10 +4552,11 @@ Read \`node_modules/cascade/AGENTS.md\` for the framework itself. What follows i
|
|
|
4455
4552
|
|
|
4456
4553
|
## Read this first
|
|
4457
4554
|
|
|
4458
|
-
\`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.
|
|
4459
4556
|
|
|
4460
4557
|
## Common commands
|
|
4461
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.
|
|
4462
4560
|
- \`npm run check\` \u2014 type-check custom nodes.
|
|
4463
4561
|
- \`npm run check:graph\` \u2014 statically check the graph and node definitions.
|
|
4464
4562
|
- \`npm run validate\` \u2014 validate \`index.cascade\`.
|
|
@@ -4471,7 +4569,7 @@ both the explicit bind address and the exact trusted browser hostname.
|
|
|
4471
4569
|
|
|
4472
4570
|
## The \`.cascade\` document format
|
|
4473
4571
|
|
|
4474
|
-
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.
|
|
4475
4573
|
|
|
4476
4574
|
\`\`\`json
|
|
4477
4575
|
{
|
|
@@ -4479,28 +4577,25 @@ Hand-editing this file is normal and expected. The shape is not what you would g
|
|
|
4479
4577
|
"metadata": { "name": "My Sketch" },
|
|
4480
4578
|
"nodes": [
|
|
4481
4579
|
{
|
|
4482
|
-
"id": "
|
|
4483
|
-
"module": "project.
|
|
4580
|
+
"id": "multiply",
|
|
4581
|
+
"module": "project.Multiply",
|
|
4484
4582
|
"position": [240, 200],
|
|
4485
4583
|
"source": "project",
|
|
4486
|
-
"
|
|
4487
|
-
"props": { "angle": { "value": 0, "expression": "$T * 40" } }
|
|
4584
|
+
"props": { "factor": { "value": 2, "expression": "2 + sin($T)" } }
|
|
4488
4585
|
}
|
|
4489
4586
|
],
|
|
4490
|
-
"connections": [
|
|
4491
|
-
[["oscillator", 0, "value"], ["logo-1024", 0, "angle"]]
|
|
4492
|
-
],
|
|
4587
|
+
"connections": [],
|
|
4493
4588
|
"annotations": []
|
|
4494
4589
|
}
|
|
4495
4590
|
\`\`\`
|
|
4496
4591
|
|
|
4497
4592
|
Five things that are easy to get wrong:
|
|
4498
4593
|
|
|
4499
|
-
-
|
|
4594
|
+
- **Use \`module\` for the module identity and \`"source": "project"\` for project modules.** Unresolved modules need investigation even when validation only warns.
|
|
4500
4595
|
- **\`position\` is \`[x, y]\`**, an array, not \`{x, y}\`.
|
|
4501
|
-
-
|
|
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.
|
|
4502
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.
|
|
4503
|
-
- **
|
|
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.
|
|
4504
4599
|
|
|
4505
4600
|
## Before writing a node, check whether you need one
|
|
4506
4601
|
|
|
@@ -4529,25 +4624,22 @@ import type { NodeDefinition, NodeExecutionContext } from 'cascade/contracts';
|
|
|
4529
4624
|
|
|
4530
4625
|
export const definition = {
|
|
4531
4626
|
apiVersion: 1,
|
|
4532
|
-
label: '
|
|
4627
|
+
label: 'Multiply',
|
|
4533
4628
|
icon: 'Circle', // a Lucide icon name
|
|
4534
4629
|
runsOn: 'portable', // 'portable' | 'browser' | 'server'
|
|
4535
4630
|
inputs: {
|
|
4536
|
-
value: { kind: 'data', type: 'float', default:
|
|
4631
|
+
value: { kind: 'data', type: 'float', default: 3 },
|
|
4537
4632
|
},
|
|
4538
4633
|
outputs: {
|
|
4539
|
-
|
|
4634
|
+
result: { kind: 'data', type: 'float' },
|
|
4540
4635
|
},
|
|
4541
4636
|
props: {
|
|
4542
|
-
|
|
4637
|
+
factor: { type: 'float', default: 2, min: 0, max: 10, step: 0.1 },
|
|
4543
4638
|
},
|
|
4544
4639
|
} as const satisfies NodeDefinition;
|
|
4545
4640
|
|
|
4546
4641
|
export function execute(context: NodeExecutionContext<typeof definition>) {
|
|
4547
|
-
|
|
4548
|
-
const { size } = context.props;
|
|
4549
|
-
// ... work ...
|
|
4550
|
-
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);
|
|
4551
4643
|
}
|
|
4552
4644
|
\`\`\`
|
|
4553
4645
|
|
|
@@ -4555,7 +4647,7 @@ The declaration is the point: ports, types, props and the execution locus are re
|
|
|
4555
4647
|
|
|
4556
4648
|
### Use the vector types
|
|
4557
4649
|
|
|
4558
|
-
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.
|
|
4559
4651
|
|
|
4560
4652
|
\`\`\`ts
|
|
4561
4653
|
// Wrong
|
|
@@ -4567,22 +4659,26 @@ props: { offset: { type: 'vec2', default: [0, 0] } }
|
|
|
4567
4659
|
|
|
4568
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.
|
|
4569
4661
|
|
|
4570
|
-
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.
|
|
4571
4665
|
|
|
4572
|
-
|
|
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.
|
|
4573
4667
|
|
|
4574
|
-
|
|
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.
|
|
4575
4669
|
|
|
4576
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.
|
|
4577
4671
|
|
|
4578
4672
|
## Rendering
|
|
4579
4673
|
|
|
4580
|
-
|
|
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.
|
|
4581
4675
|
|
|
4582
|
-
|
|
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.
|
|
4583
4677
|
|
|
4584
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.
|
|
4585
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
|
+
|
|
4586
4682
|
## Finding out what exists
|
|
4587
4683
|
|
|
4588
4684
|
- \`node_modules/cascade/doc/PROJECT_AUTHORING.md\` \u2014 project layout and the node contract.
|
|
@@ -4591,11 +4687,11 @@ Use \`browser\` only for code that genuinely needs the page, such as the DOM or
|
|
|
4591
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.)
|
|
4592
4688
|
- The **Definition panel** in Studio shows, for any selected node, where it is defined and what it declares.
|
|
4593
4689
|
|
|
4594
|
-
##
|
|
4690
|
+
## Verification and shared development
|
|
4595
4691
|
|
|
4596
|
-
|
|
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.
|
|
4597
4693
|
|
|
4598
|
-
|
|
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.
|
|
4599
4695
|
`);
|
|
4600
4696
|
return directory;
|
|
4601
4697
|
}
|
|
@@ -4669,7 +4765,7 @@ function parseStudioArgs(args) {
|
|
|
4669
4765
|
if (!value || value.startsWith("--")) throw new Error(`${name} requires a value`);
|
|
4670
4766
|
if (name === "--host") host = value;
|
|
4671
4767
|
else if (name === "--trusted-host") {
|
|
4672
|
-
|
|
4768
|
+
trustedAuthority(value, 1);
|
|
4673
4769
|
trustedHosts.push(value);
|
|
4674
4770
|
} else {
|
|
4675
4771
|
port = Number(value);
|
|
@@ -4682,8 +4778,8 @@ function parseStudioArgs(args) {
|
|
|
4682
4778
|
if (argument.startsWith("-")) throw new Error(`unknown option: ${argument}`);
|
|
4683
4779
|
positional.push(argument);
|
|
4684
4780
|
}
|
|
4685
|
-
if (trustedHosts.length > 0 &&
|
|
4686
|
-
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");
|
|
4687
4783
|
}
|
|
4688
4784
|
if (host && !isLoopbackHost(host)) {
|
|
4689
4785
|
if (host === "0.0.0.0" || host === "::" || host === "[::]") {
|
|
@@ -4735,7 +4831,7 @@ async function runStudioCli(args) {
|
|
|
4735
4831
|
const address = server.address();
|
|
4736
4832
|
const port2 = typeof address === "object" && address ? address.port : 3030;
|
|
4737
4833
|
const browserHost = trustedHosts[0] ?? (host && !isLoopbackHost(host) ? host : "localhost");
|
|
4738
|
-
const url = `http://${
|
|
4834
|
+
const url = `http://${trustedAuthority(browserHost, port2)}`;
|
|
4739
4835
|
if (!noOpen) {
|
|
4740
4836
|
const opener = process.platform === "darwin" ? "open" : process.platform === "win32" ? "start" : "xdg-open";
|
|
4741
4837
|
execFile5(opener, [url], (error) => {
|
|
@@ -5639,6 +5735,34 @@ function split(path23) {
|
|
|
5639
5735
|
return String(path23 ?? "").split("/").filter((segment) => segment.length > 0 && segment !== ".");
|
|
5640
5736
|
}
|
|
5641
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
|
+
|
|
5642
5766
|
// src/nodes/Node.ts
|
|
5643
5767
|
function inferParamType(value) {
|
|
5644
5768
|
if (typeof value === "number") return Number.isInteger(value) ? "int" : "float";
|
|
@@ -5745,9 +5869,38 @@ var Node = class _Node {
|
|
|
5745
5869
|
// Execution state
|
|
5746
5870
|
this.hasExecuted = false;
|
|
5747
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;
|
|
5748
5875
|
this.cookState = "stale";
|
|
5749
5876
|
this.cookGeneration = 0;
|
|
5750
|
-
|
|
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 = [];
|
|
5751
5904
|
this.executionTimeout = 3e4;
|
|
5752
5905
|
// Cook info - performance tracking
|
|
5753
5906
|
this.cookInfo = {
|
|
@@ -5893,8 +6046,9 @@ var Node = class _Node {
|
|
|
5893
6046
|
return port && "defaultValue" in port ? port : null;
|
|
5894
6047
|
}
|
|
5895
6048
|
setOutputValue(port, value) {
|
|
5896
|
-
|
|
5897
|
-
|
|
6049
|
+
const staging = this.stagingStack[this.stagingStack.length - 1];
|
|
6050
|
+
if (staging) {
|
|
6051
|
+
staging.set(port, value);
|
|
5898
6052
|
return;
|
|
5899
6053
|
}
|
|
5900
6054
|
this.commitOutputValue(port, value);
|
|
@@ -6849,6 +7003,35 @@ var Node = class _Node {
|
|
|
6849
7003
|
if (!this.hasExecuted || this.cookState !== "clean") return true;
|
|
6850
7004
|
return this.calculateInputHash() !== this.lastInputHash;
|
|
6851
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
|
+
}
|
|
6852
7035
|
/**
|
|
6853
7036
|
* Mark all downstream nodes as dirty (lazy propagation)
|
|
6854
7037
|
*/
|
|
@@ -6892,7 +7075,8 @@ var Node = class _Node {
|
|
|
6892
7075
|
const startTime = performance.now();
|
|
6893
7076
|
const startMemory = performance.memory?.usedJSHeapSize ?? 0;
|
|
6894
7077
|
const generation = this.cookGeneration;
|
|
6895
|
-
|
|
7078
|
+
const staging = /* @__PURE__ */ new Map();
|
|
7079
|
+
this.stagingStack.push(staging);
|
|
6896
7080
|
this.setCookState("cooking");
|
|
6897
7081
|
let timeoutId;
|
|
6898
7082
|
try {
|
|
@@ -6907,12 +7091,13 @@ var Node = class _Node {
|
|
|
6907
7091
|
]);
|
|
6908
7092
|
if (generation === this.cookGeneration) {
|
|
6909
7093
|
await this.callLifecycleHooks(needsInitialization);
|
|
6910
|
-
for (const [port, value] of
|
|
7094
|
+
for (const [port, value] of staging) {
|
|
6911
7095
|
this.commitOutputValue(port, value);
|
|
6912
7096
|
}
|
|
6913
7097
|
this.error = null;
|
|
6914
7098
|
this.hasExecuted = true;
|
|
6915
7099
|
this.lastInputHash = this.calculateInputHash();
|
|
7100
|
+
this.failedInputHash = null;
|
|
6916
7101
|
const carryReport = this.finishParameterCarryOver();
|
|
6917
7102
|
if (carryReport) _Node.onParametersRetargeted?.(this.id, carryReport);
|
|
6918
7103
|
this.setCookState("clean");
|
|
@@ -6920,20 +7105,23 @@ var Node = class _Node {
|
|
|
6920
7105
|
this.setCookState("stale");
|
|
6921
7106
|
}
|
|
6922
7107
|
this.updateCookInfo(startTime, startMemory);
|
|
6923
|
-
} catch (
|
|
7108
|
+
} catch (thrown) {
|
|
7109
|
+
const err = asError(thrown);
|
|
6924
7110
|
if (generation === this.cookGeneration) {
|
|
6925
7111
|
this.error = err;
|
|
7112
|
+
this.failedInputHash = this.calculateInputHash();
|
|
6926
7113
|
this.setCookState("error");
|
|
6927
7114
|
} else {
|
|
6928
7115
|
this.setCookState("stale");
|
|
6929
7116
|
}
|
|
6930
7117
|
this.updateCookInfo(startTime, startMemory);
|
|
6931
|
-
if (!err.message
|
|
6932
|
-
console.error(`Error executing node ${this.id}
|
|
7118
|
+
if (!err.message.includes("timeout")) {
|
|
7119
|
+
console.error(`Error executing node ${this.id}: ${err.message}`, err);
|
|
6933
7120
|
}
|
|
6934
7121
|
} finally {
|
|
6935
7122
|
if (timeoutId !== void 0) clearTimeout(timeoutId);
|
|
6936
|
-
|
|
7123
|
+
const index = this.stagingStack.indexOf(staging);
|
|
7124
|
+
if (index >= 0) this.stagingStack.splice(index, 1);
|
|
6937
7125
|
}
|
|
6938
7126
|
}
|
|
6939
7127
|
/**
|
|
@@ -8141,11 +8329,20 @@ function parameterOptions(definition) {
|
|
|
8141
8329
|
return {
|
|
8142
8330
|
...definition.label ? { label: definition.label } : {},
|
|
8143
8331
|
...definition.expression ? { defaultExpression: definition.expression } : {},
|
|
8332
|
+
...definition.action ? { action: definition.action } : {},
|
|
8144
8333
|
...definition.min === void 0 ? {} : { min: definition.min },
|
|
8145
8334
|
...definition.max === void 0 ? {} : { max: definition.max },
|
|
8146
8335
|
...definition.step === void 0 ? {} : { step: definition.step },
|
|
8147
8336
|
...definition.options ? {
|
|
8148
|
-
|
|
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) })
|
|
8149
8346
|
} : {},
|
|
8150
8347
|
promotable: false
|
|
8151
8348
|
};
|
|
@@ -8277,86 +8474,238 @@ var browserAssetCapability = {
|
|
|
8277
8474
|
}
|
|
8278
8475
|
};
|
|
8279
8476
|
|
|
8280
|
-
// src/nodes/definition/
|
|
8281
|
-
var
|
|
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";
|
|
8282
8578
|
var MISSING_DEVICE = "The gpu capability was used before its device was ready \u2014 the host must await ensure() before a cook.";
|
|
8283
|
-
|
|
8284
|
-
|
|
8285
|
-
|
|
8286
|
-
|
|
8287
|
-
|
|
8288
|
-
let
|
|
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;
|
|
8289
8585
|
let starting = null;
|
|
8290
|
-
let
|
|
8586
|
+
let disposal = null;
|
|
8587
|
+
let disposed = false;
|
|
8291
8588
|
const caches = /* @__PURE__ */ new Map();
|
|
8589
|
+
const retiring = /* @__PURE__ */ new Set();
|
|
8590
|
+
const retirementErrors = [];
|
|
8292
8591
|
function dropCaches() {
|
|
8293
|
-
|
|
8294
|
-
|
|
8295
|
-
}
|
|
8592
|
+
const errors = [];
|
|
8593
|
+
const scopes = [...caches.values()];
|
|
8296
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
|
+
});
|
|
8297
8644
|
}
|
|
8298
8645
|
async function start() {
|
|
8299
|
-
const
|
|
8300
|
-
|
|
8301
|
-
|
|
8302
|
-
|
|
8303
|
-
|
|
8304
|
-
|
|
8305
|
-
|
|
8306
|
-
|
|
8307
|
-
|
|
8308
|
-
|
|
8309
|
-
|
|
8310
|
-
|
|
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
|
|
8311
8658
|
};
|
|
8312
|
-
|
|
8313
|
-
|
|
8314
|
-
|
|
8315
|
-
|
|
8316
|
-
|
|
8317
|
-
|
|
8318
|
-
|
|
8319
|
-
});
|
|
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);
|
|
8320
8666
|
}
|
|
8321
8667
|
const host = {
|
|
8322
8668
|
get device() {
|
|
8323
|
-
if (!
|
|
8324
|
-
return device;
|
|
8669
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8670
|
+
return active.device;
|
|
8325
8671
|
},
|
|
8326
8672
|
get adapterInfo() {
|
|
8327
|
-
if (!
|
|
8328
|
-
return
|
|
8673
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8674
|
+
return active.info;
|
|
8329
8675
|
},
|
|
8330
8676
|
get limits() {
|
|
8331
|
-
return limits;
|
|
8677
|
+
return active?.limits ?? {};
|
|
8332
8678
|
},
|
|
8333
8679
|
cache(key2, create, destroy) {
|
|
8334
|
-
return host.forNode(
|
|
8680
|
+
return host.forNode(HOST_SCOPE).cache(key2, create, destroy);
|
|
8335
8681
|
},
|
|
8336
8682
|
async ensure() {
|
|
8337
|
-
if (
|
|
8338
|
-
if (
|
|
8339
|
-
|
|
8340
|
-
|
|
8341
|
-
|
|
8342
|
-
|
|
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
|
+
}
|
|
8343
8692
|
await starting;
|
|
8344
8693
|
},
|
|
8345
8694
|
forNode(nodeId) {
|
|
8346
8695
|
return {
|
|
8347
8696
|
get device() {
|
|
8348
|
-
if (!
|
|
8349
|
-
return device;
|
|
8697
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8698
|
+
return active.device;
|
|
8350
8699
|
},
|
|
8351
8700
|
get adapterInfo() {
|
|
8352
|
-
if (!
|
|
8353
|
-
return
|
|
8701
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8702
|
+
return active.info;
|
|
8354
8703
|
},
|
|
8355
8704
|
get limits() {
|
|
8356
|
-
return limits;
|
|
8705
|
+
return active?.limits ?? {};
|
|
8357
8706
|
},
|
|
8358
8707
|
cache(key2, create, destroy) {
|
|
8359
|
-
if (!
|
|
8708
|
+
if (!active) throw new Error(MISSING_DEVICE);
|
|
8360
8709
|
let entries = caches.get(nodeId);
|
|
8361
8710
|
if (!entries) {
|
|
8362
8711
|
entries = /* @__PURE__ */ new Map();
|
|
@@ -8364,37 +8713,84 @@ function createStudioGpuCapability() {
|
|
|
8364
8713
|
}
|
|
8365
8714
|
const existing = entries.get(key2);
|
|
8366
8715
|
if (existing) return existing.value;
|
|
8367
|
-
const value = create(device);
|
|
8368
|
-
entries.set(key2, {
|
|
8716
|
+
const value = create(active.device);
|
|
8717
|
+
entries.set(key2, {
|
|
8718
|
+
value,
|
|
8719
|
+
destroy
|
|
8720
|
+
});
|
|
8369
8721
|
return value;
|
|
8370
8722
|
}
|
|
8371
8723
|
};
|
|
8372
8724
|
},
|
|
8373
|
-
|
|
8374
|
-
|
|
8375
|
-
|
|
8376
|
-
device?.destroy();
|
|
8377
|
-
device = null;
|
|
8378
|
-
starting = null;
|
|
8725
|
+
dispose() {
|
|
8726
|
+
disposal ??= performDispose();
|
|
8727
|
+
return disposal;
|
|
8379
8728
|
}
|
|
8380
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
|
+
}
|
|
8381
8750
|
return host;
|
|
8382
8751
|
}
|
|
8383
|
-
function
|
|
8752
|
+
function readGpuLimits(source) {
|
|
8384
8753
|
if (!source || typeof source !== "object") return {};
|
|
8385
|
-
const out = {};
|
|
8386
|
-
for (const key2 of limitNames(source)) {
|
|
8387
|
-
const value = source[key2];
|
|
8388
|
-
if (typeof value === "number") out[key2] = value;
|
|
8389
|
-
}
|
|
8390
|
-
return out;
|
|
8391
|
-
}
|
|
8392
|
-
function limitNames(source) {
|
|
8393
8754
|
const names = new Set(Object.keys(source));
|
|
8394
8755
|
for (let cursor = Object.getPrototypeOf(source); cursor && cursor !== Object.prototype; cursor = Object.getPrototypeOf(cursor)) {
|
|
8395
|
-
for (const key2 of Object.getOwnPropertyNames(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;
|
|
8396
8764
|
}
|
|
8397
|
-
return
|
|
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
|
+
});
|
|
8398
8794
|
}
|
|
8399
8795
|
|
|
8400
8796
|
// src/nodes/definition/projectDefinition.ts
|
|
@@ -8405,8 +8801,31 @@ var studioCapabilities = {
|
|
|
8405
8801
|
// reports `runtime/missing-capability` and the adapter throws "requires the
|
|
8406
8802
|
// Studio gpu capability", both naming what is missing. A stub that threw on
|
|
8407
8803
|
// first use would name nothing.
|
|
8408
|
-
...gpuCapabilityIfAvailable()
|
|
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
|
|
8409
8825
|
};
|
|
8826
|
+
void shellRouteAvailable().then((available) => {
|
|
8827
|
+
if (!available) delete studioCapabilities.shell;
|
|
8828
|
+
});
|
|
8410
8829
|
function gpuCapabilityIfAvailable() {
|
|
8411
8830
|
const gpu = createStudioGpuCapability();
|
|
8412
8831
|
return gpu ? { gpu } : {};
|
|
@@ -9809,7 +10228,8 @@ var Graph = class _Graph {
|
|
|
9809
10228
|
value = propData;
|
|
9810
10229
|
}
|
|
9811
10230
|
let normalizedValue = value;
|
|
9812
|
-
|
|
10231
|
+
const declaredColor = node.props[key2]?.type === "color";
|
|
10232
|
+
if (isColorValue(value) && (declaredColor || !Array.isArray(value))) {
|
|
9813
10233
|
const normalized = normalizeColor(value);
|
|
9814
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 };
|
|
9815
10235
|
}
|
|
@@ -10081,6 +10501,81 @@ var CascadeRuntimeError = class extends Error {
|
|
|
10081
10501
|
}
|
|
10082
10502
|
};
|
|
10083
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
|
+
|
|
10084
10579
|
// packages/runtime/src/geometry/attributes.ts
|
|
10085
10580
|
init_src();
|
|
10086
10581
|
function geometryError(code, message2) {
|
|
@@ -10967,6 +11462,134 @@ function utf8Bytes(value) {
|
|
|
10967
11462
|
// packages/runtime/src/runtime.ts
|
|
10968
11463
|
init_src();
|
|
10969
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
|
+
|
|
10970
11593
|
// packages/runtime/src/builtins/core/random.ts
|
|
10971
11594
|
var randomDefinition = {
|
|
10972
11595
|
apiVersion: 1,
|
|
@@ -11246,7 +11869,8 @@ var coreNodeRegistrations = Object.freeze([
|
|
|
11246
11869
|
nullRegistration,
|
|
11247
11870
|
selectRegistration,
|
|
11248
11871
|
randomRegistration,
|
|
11249
|
-
remapRegistration
|
|
11872
|
+
remapRegistration,
|
|
11873
|
+
cameraRegistration
|
|
11250
11874
|
]);
|
|
11251
11875
|
var registrationsById = new Map(
|
|
11252
11876
|
coreNodeRegistrations.map((registration) => [registration.moduleId, registration])
|
|
@@ -11596,10 +12220,12 @@ function builtinNodeRegistration(moduleId2) {
|
|
|
11596
12220
|
// packages/runtime/src/runtime.ts
|
|
11597
12221
|
var PREFLIGHT_WARNING_CODES = /* @__PURE__ */ new Set([
|
|
11598
12222
|
"runtime/preflight-environment",
|
|
11599
|
-
"runtime/stray-params"
|
|
12223
|
+
"runtime/stray-params",
|
|
12224
|
+
"runtime/unknown-props"
|
|
11600
12225
|
]);
|
|
11601
12226
|
var RUN_TOLERATED_CODES = /* @__PURE__ */ new Set([
|
|
11602
|
-
"runtime/stray-params"
|
|
12227
|
+
"runtime/stray-params",
|
|
12228
|
+
"runtime/unknown-props"
|
|
11603
12229
|
]);
|
|
11604
12230
|
var MutableAbortSignal = class {
|
|
11605
12231
|
constructor(report) {
|
|
@@ -12306,14 +12932,20 @@ var Graph2 = class {
|
|
|
12306
12932
|
]
|
|
12307
12933
|
)
|
|
12308
12934
|
);
|
|
12309
|
-
const
|
|
12310
|
-
|
|
12311
|
-
|
|
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([
|
|
12312
12943
|
name,
|
|
12313
|
-
name === "shell" ? bindShellToRun(this.host.capabilities.shell, signal) :
|
|
12314
|
-
])
|
|
12315
|
-
|
|
12316
|
-
|
|
12944
|
+
name === "shell" ? bindShellToRun(this.host.capabilities.shell, signal) : installed
|
|
12945
|
+
]);
|
|
12946
|
+
}
|
|
12947
|
+
}
|
|
12948
|
+
const capabilities = Object.freeze(Object.fromEntries(capabilityEntries));
|
|
12317
12949
|
const props = Object.freeze({ ...node.props });
|
|
12318
12950
|
const progress = {
|
|
12319
12951
|
report: (event) => this.emit({ type: "node:progress", runId, nodeId: node.id, ...event })
|
|
@@ -12376,6 +13008,12 @@ var Graph2 = class {
|
|
|
12376
13008
|
const diagnostics = [];
|
|
12377
13009
|
const push = (code, nodeId, message2) => diagnostics.push({ phase: "preflight", code, path: nodeId, message: message2 });
|
|
12378
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
|
+
);
|
|
12379
13017
|
if (node.strayParams.length)
|
|
12380
13018
|
push(
|
|
12381
13019
|
"runtime/stray-params",
|
|
@@ -12414,12 +13052,12 @@ var Graph2 = class {
|
|
|
12414
13052
|
`${node.moduleId} cannot send textures over the server bridge`
|
|
12415
13053
|
);
|
|
12416
13054
|
} else
|
|
12417
|
-
for (const
|
|
12418
|
-
if (!this.host.capabilities[
|
|
13055
|
+
for (const capability2 of node.definition.capabilities ?? [])
|
|
13056
|
+
if (!this.host.capabilities[capability2])
|
|
12419
13057
|
push(
|
|
12420
13058
|
"runtime/missing-capability",
|
|
12421
13059
|
node.id,
|
|
12422
|
-
`Missing capability ${
|
|
13060
|
+
`Missing capability ${capability2} for ${node.moduleId} (node ${node.id}) \u2014 the ${this.host.environment} host ${this.hostProvides()}`
|
|
12423
13061
|
);
|
|
12424
13062
|
}
|
|
12425
13063
|
return diagnostics;
|
|
@@ -12691,6 +13329,11 @@ function materialize(authored, registration) {
|
|
|
12691
13329
|
false,
|
|
12692
13330
|
`${authored.id}.${name}`
|
|
12693
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
|
+
);
|
|
12694
13337
|
const bindings = {};
|
|
12695
13338
|
const storedProps = authored.props ?? {};
|
|
12696
13339
|
for (const [name, definition] of Object.entries(
|
|
@@ -12736,9 +13379,16 @@ function materialize(authored, registration) {
|
|
|
12736
13379
|
props,
|
|
12737
13380
|
bindings,
|
|
12738
13381
|
strayParams: strayParams(authored, registration),
|
|
13382
|
+
unknownProps: unknownProps(authored, registration),
|
|
12739
13383
|
outputs: /* @__PURE__ */ new Map()
|
|
12740
13384
|
};
|
|
12741
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
|
+
}
|
|
12742
13392
|
function strayParams(authored, registration) {
|
|
12743
13393
|
const definition = registration.definition;
|
|
12744
13394
|
const declared = /* @__PURE__ */ new Set([
|
|
@@ -13184,12 +13834,16 @@ function validateNodeModuleArchitecture(source, fileName, ts3) {
|
|
|
13184
13834
|
ts3.ScriptKind.TS
|
|
13185
13835
|
);
|
|
13186
13836
|
const diagnostics = [];
|
|
13837
|
+
const exportsDefinition = file.statements.some(
|
|
13838
|
+
(statement) => ts3.isVariableStatement(statement) && isDefinition(statement)
|
|
13839
|
+
);
|
|
13187
13840
|
for (const statement of file.statements) {
|
|
13188
13841
|
if (ts3.isVariableStatement(statement) && !isDefinition(statement)) {
|
|
13842
|
+
const superseded = supersededByDefinition(statement);
|
|
13189
13843
|
add(
|
|
13190
13844
|
statement,
|
|
13191
13845
|
"module-state",
|
|
13192
|
-
"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"
|
|
13193
13847
|
);
|
|
13194
13848
|
} else if (ts3.isClassDeclaration(statement) || ts3.isEnumDeclaration(statement) || ts3.isModuleDeclaration(statement)) {
|
|
13195
13849
|
add(
|
|
@@ -13241,6 +13895,13 @@ function validateNodeModuleArchitecture(source, fileName, ts3) {
|
|
|
13241
13895
|
const parent = node.parent;
|
|
13242
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);
|
|
13243
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
|
+
}
|
|
13244
13905
|
function isDefinition(statement) {
|
|
13245
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";
|
|
13246
13907
|
}
|
|
@@ -13258,15 +13919,15 @@ function validateNodeModuleArchitecture(source, fileName, ts3) {
|
|
|
13258
13919
|
}
|
|
13259
13920
|
function importsSiblingNode(fileName, specifier) {
|
|
13260
13921
|
if (!specifier.startsWith(".")) return false;
|
|
13261
|
-
const file =
|
|
13922
|
+
const file = normalize2(fileName).split("/");
|
|
13262
13923
|
const nodesIndex = file.lastIndexOf("nodes");
|
|
13263
13924
|
if (nodesIndex < 0 || !file[nodesIndex + 1]) return false;
|
|
13264
13925
|
const sourceNode = file[nodesIndex + 1];
|
|
13265
|
-
const resolved =
|
|
13926
|
+
const resolved = normalize2([...file.slice(0, -1), ...specifier.split("/")].join("/")).split("/");
|
|
13266
13927
|
const resolvedNodesIndex = resolved.lastIndexOf("nodes");
|
|
13267
13928
|
return resolvedNodesIndex >= 0 && Boolean(resolved[resolvedNodesIndex + 1]) && resolved[resolvedNodesIndex + 1] !== sourceNode;
|
|
13268
13929
|
}
|
|
13269
|
-
function
|
|
13930
|
+
function normalize2(value) {
|
|
13270
13931
|
const output = [];
|
|
13271
13932
|
for (const part of value.split("\\").join("/").split("/")) {
|
|
13272
13933
|
if (!part || part === ".") continue;
|
|
@@ -13381,6 +14042,41 @@ function imagePath(value) {
|
|
|
13381
14042
|
return null;
|
|
13382
14043
|
}
|
|
13383
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
|
+
|
|
13384
14080
|
// src/cli/projectRuntime.ts
|
|
13385
14081
|
function createProjectHost(file, registrations) {
|
|
13386
14082
|
const project = new ProjectRoot(path9.dirname(path9.resolve(file)));
|
|
@@ -13391,7 +14087,8 @@ function createProjectHost(file, registrations) {
|
|
|
13391
14087
|
// `cascade.geo.SvgExport` declares it — so an export graph passed both
|
|
13392
14088
|
// `validate` and `check` and then refused to run. See ./nodeAssets.ts
|
|
13393
14089
|
// for where the file lands and why.
|
|
13394
|
-
assets: createNodeAssetCapability(project)
|
|
14090
|
+
assets: createNodeAssetCapability(project),
|
|
14091
|
+
gpu: createDawnGpuHost()
|
|
13395
14092
|
});
|
|
13396
14093
|
}
|
|
13397
14094
|
async function preflightAgainstRunHost(file, registrations, document2) {
|
|
@@ -13409,9 +14106,9 @@ async function preflightAgainstRunHost(file, registrations, document2) {
|
|
|
13409
14106
|
try {
|
|
13410
14107
|
const graph = await runtime.load(document2);
|
|
13411
14108
|
if (checkable) diagnostics = graph.preflight();
|
|
13412
|
-
await graph.dispose();
|
|
13413
14109
|
} finally {
|
|
13414
14110
|
await runtime.dispose();
|
|
14111
|
+
await disposeProjectGpu(host);
|
|
13415
14112
|
}
|
|
13416
14113
|
if (!diagnostics.length) return;
|
|
13417
14114
|
const { errors, warnings } = classifyPreflight(diagnostics);
|
|
@@ -13425,7 +14122,7 @@ async function preflightAgainstRunHost(file, registrations, document2) {
|
|
|
13425
14122
|
}
|
|
13426
14123
|
function reportPreflightWarnings(host, warnings) {
|
|
13427
14124
|
const elsewhere = warnings.filter((item) => item.code === "runtime/preflight-environment");
|
|
13428
|
-
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");
|
|
13429
14126
|
if (elsewhere.length) {
|
|
13430
14127
|
console.warn([
|
|
13431
14128
|
`Not checkable here \u2014 ${elsewhere.length === 1 ? "one node" : `${elsewhere.length} nodes`} in this graph target${elsewhere.length === 1 ? "s" : ""} another host:`,
|
|
@@ -13435,7 +14132,7 @@ function reportPreflightWarnings(host, warnings) {
|
|
|
13435
14132
|
}
|
|
13436
14133
|
if (lossy.length) {
|
|
13437
14134
|
console.warn([
|
|
13438
|
-
`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`}:`,
|
|
13439
14136
|
...lossy.map(line),
|
|
13440
14137
|
` This graph will run, on the defaults, and look like it worked.`
|
|
13441
14138
|
].join("\n"));
|
|
@@ -13456,8 +14153,8 @@ function capabilityAdvice(host, diagnostics) {
|
|
|
13456
14153
|
const hostCapabilities = CAPABILITIES_BY_ENVIRONMENT[host.environment] ?? [];
|
|
13457
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.`);
|
|
13458
14155
|
}
|
|
13459
|
-
function environmentOwning(
|
|
13460
|
-
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);
|
|
13461
14158
|
return owners.length ? owners.join("/") : "unknown";
|
|
13462
14159
|
}
|
|
13463
14160
|
function reportUnresolved(prepared) {
|
|
@@ -13507,7 +14204,7 @@ async function runDeterministicProjectGraph(file, document2, entryNode) {
|
|
|
13507
14204
|
graph = await runtime.load(document2);
|
|
13508
14205
|
reportPreflightWarnings(
|
|
13509
14206
|
host,
|
|
13510
|
-
classifyPreflight(graph.preflight()).warnings.filter((item) => item.code === "runtime/stray-params")
|
|
14207
|
+
classifyPreflight(graph.preflight()).warnings.filter((item) => item.code === "runtime/stray-params" || item.code === "runtime/unknown-props")
|
|
13511
14208
|
);
|
|
13512
14209
|
const result = await graph.run(entryNode ? { target: { kind: "node", nodeId: entryNode } } : {});
|
|
13513
14210
|
if (result.status !== "completed") {
|
|
@@ -13515,8 +14212,11 @@ async function runDeterministicProjectGraph(file, document2, entryNode) {
|
|
|
13515
14212
|
}
|
|
13516
14213
|
return true;
|
|
13517
14214
|
} finally {
|
|
13518
|
-
|
|
13519
|
-
|
|
14215
|
+
try {
|
|
14216
|
+
await runtime.dispose();
|
|
14217
|
+
} finally {
|
|
14218
|
+
await disposeProjectGpu(host);
|
|
14219
|
+
}
|
|
13520
14220
|
}
|
|
13521
14221
|
}
|
|
13522
14222
|
async function renderDeterministicProjectFrames(file, document2, options) {
|
|
@@ -13534,15 +14234,18 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
13534
14234
|
host,
|
|
13535
14235
|
nodes: prepared.registrations
|
|
13536
14236
|
});
|
|
13537
|
-
|
|
13538
|
-
reportPreflightWarnings(host, classifyPreflight(graph.preflight()).warnings);
|
|
13539
|
-
const outDirectory = project.resolve(options.out);
|
|
13540
|
-
await fs7.mkdir(outDirectory, { recursive: true });
|
|
14237
|
+
let graph;
|
|
13541
14238
|
const width = sequenceWidth(options.end);
|
|
13542
14239
|
const frames = [];
|
|
13543
14240
|
const files = [];
|
|
13544
14241
|
let aborted = false;
|
|
14242
|
+
let gpu;
|
|
14243
|
+
const started = performance.now();
|
|
13545
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 });
|
|
13546
14249
|
for (const frame of frameRange(options.start, options.end, options.step ?? 1)) {
|
|
13547
14250
|
if (options.signal?.aborted) {
|
|
13548
14251
|
aborted = true;
|
|
@@ -13574,11 +14277,21 @@ async function renderDeterministicProjectFrames(file, document2, options) {
|
|
|
13574
14277
|
frames.push(frame);
|
|
13575
14278
|
if (options.verbose) console.log(` frame ${frame}`);
|
|
13576
14279
|
}
|
|
14280
|
+
try {
|
|
14281
|
+
gpu = { renderer: "dawn", adapter: host.capabilities.gpu.adapterInfo };
|
|
14282
|
+
} catch {
|
|
14283
|
+
}
|
|
13577
14284
|
} finally {
|
|
13578
|
-
|
|
13579
|
-
|
|
14285
|
+
try {
|
|
14286
|
+
await runtime.dispose();
|
|
14287
|
+
} finally {
|
|
14288
|
+
await disposeProjectGpu(host);
|
|
14289
|
+
}
|
|
13580
14290
|
}
|
|
13581
|
-
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();
|
|
13582
14295
|
}
|
|
13583
14296
|
function imageOutputs(graph, entryNode) {
|
|
13584
14297
|
const found = [];
|
|
@@ -13644,6 +14357,8 @@ async function prepare(file, document2, compileExecutors) {
|
|
|
13644
14357
|
const mentionsDefinition = /\bexport\s+const\s+definition\b/.test(source);
|
|
13645
14358
|
const extracted = extractNodeDefinition(source, moduleFile, ts2);
|
|
13646
14359
|
if (!extracted.ok) {
|
|
14360
|
+
const reExport = reExportedDefinition(source);
|
|
14361
|
+
if (reExport) throw new Error(formatDiagnostic(reExport(moduleFile)));
|
|
13647
14362
|
if (!mentionsDefinition && extracted.diagnostics.every((item) => item.code === "definition/missing-export")) {
|
|
13648
14363
|
registrations.push(savedRegistration(id, node));
|
|
13649
14364
|
continue;
|
|
@@ -13683,6 +14398,18 @@ async function projectModuleFile(root, node, id) {
|
|
|
13683
14398
|
tried.push(candidate);
|
|
13684
14399
|
return { file: await exists(candidate) ? candidate : null, tried };
|
|
13685
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
|
+
}
|
|
13686
14413
|
function unresolvedModule(id, node, tried, root) {
|
|
13687
14414
|
const relative = tried.map((candidate) => path9.relative(root, candidate) || candidate);
|
|
13688
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`;
|
|
@@ -17367,28 +18094,50 @@ async function runGraph(options) {
|
|
|
17367
18094
|
if (verbose) console.warn(canvasError.message);
|
|
17368
18095
|
}
|
|
17369
18096
|
const frameSpec = frames ? parseFrameSpec(frames) : null;
|
|
18097
|
+
let hostReleased = false;
|
|
17370
18098
|
const releaseHost = () => {
|
|
17371
|
-
|
|
17372
|
-
|
|
17373
|
-
|
|
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];
|
|
17374
18116
|
};
|
|
17375
|
-
|
|
17376
|
-
|
|
17377
|
-
|
|
17378
|
-
|
|
17379
|
-
|
|
17380
|
-
|
|
17381
|
-
|
|
17382
|
-
|
|
17383
|
-
|
|
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");
|
|
17384
18135
|
releaseHost();
|
|
17385
|
-
console.log(`Rendered ${rendered.frames.length} frames, ${rendered.files.length} files -> ${out ?? "renders"}/`);
|
|
17386
18136
|
return;
|
|
17387
18137
|
}
|
|
17388
|
-
}
|
|
17389
|
-
if (verbose) console.log("Graph execution completed");
|
|
18138
|
+
} catch (error) {
|
|
17390
18139
|
releaseHost();
|
|
17391
|
-
|
|
18140
|
+
throw error;
|
|
17392
18141
|
}
|
|
17393
18142
|
let activeGraph;
|
|
17394
18143
|
try {
|
|
@@ -17498,14 +18247,60 @@ async function runGraph(options) {
|
|
|
17498
18247
|
} finally {
|
|
17499
18248
|
activeGraph?.scheduler?.dispose?.();
|
|
17500
18249
|
releaseHost();
|
|
17501
|
-
setProjectModuleCompiler(null);
|
|
17502
|
-
setEmbeddedCompiler(null);
|
|
17503
18250
|
}
|
|
17504
18251
|
}
|
|
17505
18252
|
|
|
17506
18253
|
// src/cli/index.ts
|
|
17507
18254
|
import * as fs22 from "fs/promises";
|
|
17508
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
|
|
17509
18304
|
var VERSION = JSON.parse(readFileSync(new URL("../../package.json", import.meta.url), "utf8")).version;
|
|
17510
18305
|
async function main() {
|
|
17511
18306
|
const args = process.argv.slice(2);
|
|
@@ -17550,6 +18345,8 @@ Options:
|
|
|
17550
18345
|
--frames <range> Render a frame sequence: 1-100, 1-100x2 (step), or 42
|
|
17551
18346
|
--fps <number> Frame rate to evaluate the range at
|
|
17552
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)
|
|
17553
18350
|
--validate-only Validate only (same as 'validate' command)
|
|
17554
18351
|
--verbose, -v Show verbose output
|
|
17555
18352
|
--version Show version
|
|
@@ -17627,7 +18424,24 @@ image output nothing downstream consumes, or per output of --entry-node.
|
|
|
17627
18424
|
}
|
|
17628
18425
|
const out = valueOf("--out");
|
|
17629
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
|
+
}
|
|
17630
18443
|
try {
|
|
18444
|
+
if (options.json) console.log = console.error;
|
|
17631
18445
|
await runGraph(options);
|
|
17632
18446
|
} catch (error) {
|
|
17633
18447
|
console.error(`Error: ${error.message}`);
|