@field/cascade 0.3.0 → 0.4.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +14 -9
- package/ARCHITECTURE.md +22 -6
- package/CHANGELOG.md +69 -1
- package/CONTRIBUTING.md +71 -0
- package/DESIGN.md +19 -13
- package/README.md +42 -11
- package/dist/assets/{CodeEditor-BADFbTJS.js → CodeEditor-BNOJIDzL.js} +4 -4
- package/dist/assets/{CodePanel-faK6PRfz.js → CodePanel-2wp2G0Fj.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DhlKuviy.js → CustomNodeDialog-DNqaVBqA.js} +1 -1
- package/dist/assets/{ProjectSettings-D6a6kjqR.js → ProjectSettings-DcdYF2E1.js} +1 -1
- package/dist/assets/{SettingsDialog-DJZdIvLC.js → SettingsDialog-D2xVvyiR.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-C1X3HOhc.js → VersionHistoryDialog-DW288ykF.js} +1 -1
- package/dist/assets/{core-CiuiuWZi.js → core-B1y38i95.js} +1 -1
- package/dist/assets/{cssMode-D-NwZSMJ.js → cssMode-BLucnaln.js} +1 -1
- package/dist/assets/{editor.api-DT3WEvBd.js → editor.api-B3Et3Pph.js} +1 -1
- package/dist/assets/{freemarker2-eKNYQaiz.js → freemarker2-DVlPoano.js} +1 -1
- package/dist/assets/geo-DhyFdBXX.js +1 -0
- package/dist/assets/{handlebars-C2w1ggK2.js → handlebars-B5FZBAMR.js} +1 -1
- package/dist/assets/{html-DQb41jyo.js → html-FodD8KbR.js} +1 -1
- package/dist/assets/{htmlMode-IhNlh6Lv.js → htmlMode-CsG57VCD.js} +1 -1
- package/dist/assets/{image-DMCXBY-_.js → image-B3F-nq25.js} +1 -1
- package/dist/assets/index-CC6YQw2n.js +212 -0
- package/dist/assets/index-Y2ock1cw.css +1 -0
- package/dist/assets/{javascript-BIT6jMLC.js → javascript-B3gonF_H.js} +1 -1
- package/dist/assets/{jsonMode-CNfyB1ro.js → jsonMode-Y3w1ahZg.js} +1 -1
- package/dist/assets/{liquid-CmSmJmOG.js → liquid-CTpWXIYO.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BqMm3uMP.js → lspLanguageFeatures-CODBdKEZ.js} +1 -1
- package/dist/assets/{mdx-6r6YgOFw.js → mdx-CDAQJZIz.js} +1 -1
- package/dist/assets/{python-x1H0TQuI.js → python-LOIlxJU-.js} +1 -1
- package/dist/assets/{razor-Dzl1vZFm.js → razor-CllPQSHo.js} +1 -1
- package/dist/assets/{tsMode-n6DMMe3a.js → tsMode-CO28ut8t.js} +1 -1
- package/dist/assets/{typescript-ziEz65xN.js → typescript-ruU9wKQ_.js} +1 -1
- package/dist/assets/{workers-D-qUdp-r.js → workers-Bc1Ywp4-.js} +1 -1
- package/dist/assets/{xml-DMZjt5lE.js → xml-DoXP2Dh1.js} +1 -1
- package/dist/assets/{yaml-DFe5qZSO.js → yaml-D9YPyGiJ.js} +1 -1
- package/dist/cli/index.js +1240 -158
- package/dist/cli/runtime/gpu.ts +147 -0
- package/dist/contracts/camera.d.ts +70 -0
- package/dist/contracts/camera.js +27 -0
- package/dist/contracts/capabilities.d.ts +69 -6
- package/dist/contracts/definition.d.ts +61 -2
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -0
- package/dist/contracts/schema.js +33 -7
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +29 -4
- package/dist/contracts/values.d.ts +3 -2
- package/dist/contracts/values.js +1 -0
- package/dist/gpu.d.ts +88 -0
- package/dist/gpu.js +113 -0
- package/dist/index.html +2 -2
- package/dist/runtime/builtins/core/camera.d.ts +252 -0
- package/dist/runtime/builtins/core/camera.js +149 -0
- package/dist/runtime/builtins/core/index.d.ts +2 -1
- package/dist/runtime/builtins/core/index.js +3 -1
- package/dist/runtime/camera/index.d.ts +60 -0
- package/dist/runtime/camera/index.js +234 -0
- package/dist/runtime/definition/architecture.js +36 -1
- package/dist/runtime/gpu.d.ts +33 -0
- package/dist/runtime/gpu.js +218 -0
- package/dist/runtime/index.d.ts +3 -0
- package/dist/runtime/index.js +3 -0
- package/dist/runtime/runtime.d.ts +23 -3
- package/dist/runtime/runtime.js +181 -30
- package/dist/runtime/series/index.d.ts +41 -0
- package/dist/runtime/series/index.js +167 -0
- package/dist/runtime/types.d.ts +3 -3
- package/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +36 -0
- package/doc/PROJECT_AUTHORING.md +106 -20
- package/package.json +14 -2
- package/dist/assets/geo-DUvVbLnx.js +0 -1
- package/dist/assets/index-BDV5xGBZ.css +0 -1
- package/dist/assets/index-HTCN44kj.js +0 -212
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import type { Camera } from "./camera.js";
|
|
1
2
|
import type { Color } from "./color.js";
|
|
2
3
|
import type { Geometry, GeometryFileRef, GeometryJson, GeometrySerialized } from "./geometry.js";
|
|
3
|
-
export declare const CORE_TYPES: readonly ["float", "int", "bool", "string", "vec2", "vec3", "vec4", "vec2i", "vec3i", "vec4i", "mat2", "mat3", "mat4", "image", "texture", "geometry", "points", "lines", "polyline", "mesh", "rects", "color", "asset", "array", "object", "any"];
|
|
4
|
+
export declare const CORE_TYPES: readonly ["float", "int", "bool", "string", "vec2", "vec3", "vec4", "vec2i", "vec3i", "vec4i", "mat2", "mat3", "mat4", "image", "texture", "geometry", "points", "lines", "polyline", "mesh", "rects", "color", "camera", "asset", "array", "object", "any"];
|
|
4
5
|
export type CoreType = (typeof CORE_TYPES)[number];
|
|
5
6
|
export type NamespacedType = `${string}.${string}`;
|
|
6
7
|
export type CascadeType = CoreType | NamespacedType;
|
|
@@ -94,7 +95,7 @@ export interface AssetRef {
|
|
|
94
95
|
readonly mediaType?: string;
|
|
95
96
|
}
|
|
96
97
|
export type SerializableValue = JsonValue | AssetRef | ImageRef | GeometryJson | GeometryFileRef;
|
|
97
|
-
export type ValueForType<T extends CascadeType> = T extends "float" | "int" ? number : T extends "bool" ? boolean : T extends "string" ? string : T extends "vec2" ? Vec2 : T extends "vec3" ? Vec3 : T extends "vec4" ? Vec4 : T extends "vec2i" ? Vec2i : T extends "vec3i" ? Vec3i : T extends "vec4i" ? Vec4i : T extends "mat2" ? Mat2 : T extends "mat3" ? Mat3 : T extends "mat4" ? Mat4 : T extends "color" ? Color : T extends "image" ? ImageRef : T extends "texture" ? TextureHandle : T extends "geometry" ? Geometry : T extends "points" ? PointSet : T extends "lines" ? LineSegments : T extends "polyline" ? Polyline : T extends "mesh" ? Mesh : T extends "rects" ? Rects : T extends "asset" ? AssetRef : T extends "array" ? readonly unknown[] : T extends "object" ? Readonly<Record<string, unknown>> : unknown;
|
|
98
|
+
export type ValueForType<T extends CascadeType> = T extends "float" | "int" ? number : T extends "bool" ? boolean : T extends "string" ? string : T extends "vec2" ? Vec2 : T extends "vec3" ? Vec3 : T extends "vec4" ? Vec4 : T extends "vec2i" ? Vec2i : T extends "vec3i" ? Vec3i : T extends "vec4i" ? Vec4i : T extends "mat2" ? Mat2 : T extends "mat3" ? Mat3 : T extends "mat4" ? Mat4 : T extends "color" ? Color : T extends "camera" ? Camera : T extends "image" ? ImageRef : T extends "texture" ? TextureHandle : T extends "geometry" ? Geometry : T extends "points" ? PointSet : T extends "lines" ? LineSegments : T extends "polyline" ? Polyline : T extends "mesh" ? Mesh : T extends "rects" ? Rects : T extends "asset" ? AssetRef : T extends "array" ? readonly unknown[] : T extends "object" ? Readonly<Record<string, unknown>> : unknown;
|
|
98
99
|
export type SerializableValueForType<T extends CascadeType> = T extends "texture" ? never : T extends "geometry" ? GeometrySerialized : T extends "array" ? readonly JsonValue[] : T extends "object" ? Readonly<Record<string, JsonValue>> : T extends "any" | NamespacedType ? JsonValue : ValueForType<T>;
|
|
99
100
|
export {};
|
|
100
101
|
//# sourceMappingURL=values.d.ts.map
|
package/dist/contracts/values.js
CHANGED
package/dist/gpu.d.ts
ADDED
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cascade/gpu` — WebGPU's flag namespaces as plain values.
|
|
3
|
+
*
|
|
4
|
+
* Small, and it was load-bearing. TypeScript's DOM library declares the WebGPU
|
|
5
|
+
* *interfaces* but not the constant objects, so the only way to reach
|
|
6
|
+
* `GPUTextureUsage.RENDER_ATTACHMENT` was off `globalThis` — which
|
|
7
|
+
* `architecture/ambient-state` correctly forbids inside a node module, because
|
|
8
|
+
* an ambient read makes a node's inputs invisible to the static reader that
|
|
9
|
+
* lets `cascade check` say anything without cooking.
|
|
10
|
+
*
|
|
11
|
+
* So a sketch had to choose between a rule and a working shader, and it chose
|
|
12
|
+
* a project library file to hide the read in. Both stay honest now: the rule
|
|
13
|
+
* keeps its teeth and a node imports these instead. That is the whole reason
|
|
14
|
+
* this file exists, and it is why it had to land with the capability rather
|
|
15
|
+
* than after it — a rule whose only workaround is removed at the same moment
|
|
16
|
+
* is a rule; one enforced a release earlier is a nuisance.
|
|
17
|
+
*
|
|
18
|
+
* The values are fixed by the WebGPU specification, not by an implementation,
|
|
19
|
+
* which is what makes writing them down safe rather than a guess. They are
|
|
20
|
+
* checked against a real browser's own globals in `tests/gpu-usage-flags.test.ts`
|
|
21
|
+
* — the numbers below were verified against Chrome rather than recalled.
|
|
22
|
+
*/
|
|
23
|
+
/** `GPUBufferUsage`. */
|
|
24
|
+
export declare const BufferUsage: {
|
|
25
|
+
readonly MAP_READ: 1;
|
|
26
|
+
readonly MAP_WRITE: 2;
|
|
27
|
+
readonly COPY_SRC: 4;
|
|
28
|
+
readonly COPY_DST: 8;
|
|
29
|
+
readonly INDEX: 16;
|
|
30
|
+
readonly VERTEX: 32;
|
|
31
|
+
readonly UNIFORM: 64;
|
|
32
|
+
readonly STORAGE: 128;
|
|
33
|
+
readonly INDIRECT: 256;
|
|
34
|
+
readonly QUERY_RESOLVE: 512;
|
|
35
|
+
};
|
|
36
|
+
/** `GPUTextureUsage`. */
|
|
37
|
+
export declare const TextureUsage: {
|
|
38
|
+
readonly COPY_SRC: 1;
|
|
39
|
+
readonly COPY_DST: 2;
|
|
40
|
+
readonly TEXTURE_BINDING: 4;
|
|
41
|
+
readonly STORAGE_BINDING: 8;
|
|
42
|
+
readonly RENDER_ATTACHMENT: 16;
|
|
43
|
+
/**
|
|
44
|
+
* Chrome 147 has this and it is newer than the rest — verified present
|
|
45
|
+
* 2026-09-08 alongside the others, but do not assume every implementation
|
|
46
|
+
* carries it. Included rather than omitted deliberately: leaving it out
|
|
47
|
+
* would send a node that wants it back to `globalThis`, which is the exact
|
|
48
|
+
* ambient read this file exists to remove, so an honest constant with a
|
|
49
|
+
* caveat beats an omission that forces a rule violation.
|
|
50
|
+
*/
|
|
51
|
+
readonly TRANSIENT_ATTACHMENT: 32;
|
|
52
|
+
};
|
|
53
|
+
/** `GPUShaderStage`, for a bind group layout entry's `visibility`. */
|
|
54
|
+
export declare const ShaderStage: {
|
|
55
|
+
readonly VERTEX: 1;
|
|
56
|
+
readonly FRAGMENT: 2;
|
|
57
|
+
readonly COMPUTE: 4;
|
|
58
|
+
};
|
|
59
|
+
/** `GPUMapMode`, for `buffer.mapAsync`. */
|
|
60
|
+
export declare const MapMode: {
|
|
61
|
+
readonly READ: 1;
|
|
62
|
+
readonly WRITE: 2;
|
|
63
|
+
};
|
|
64
|
+
/** `GPUColorWrite`, for a colour target's `writeMask`. */
|
|
65
|
+
export declare const ColorWrite: {
|
|
66
|
+
readonly RED: 1;
|
|
67
|
+
readonly GREEN: 2;
|
|
68
|
+
readonly BLUE: 4;
|
|
69
|
+
readonly ALPHA: 8;
|
|
70
|
+
readonly ALL: 15;
|
|
71
|
+
};
|
|
72
|
+
/** Read one single-sample rgba8unorm 2D texture into owned, tightly packed
|
|
73
|
+
* top-left RGBA bytes. No colour conversion, PNG encoding or graph transport.
|
|
74
|
+
* The caller owns the source texture and must give it COPY_SRC usage.
|
|
75
|
+
*/
|
|
76
|
+
export declare function readTexture(device: GPUDevice, texture: GPUTexture, options?: {
|
|
77
|
+
signal?: {
|
|
78
|
+
readonly aborted: boolean;
|
|
79
|
+
addEventListener(type: 'abort', listener: () => void, options?: {
|
|
80
|
+
once?: boolean;
|
|
81
|
+
}): void;
|
|
82
|
+
removeEventListener(type: 'abort', listener: () => void): void;
|
|
83
|
+
};
|
|
84
|
+
}): Promise<{
|
|
85
|
+
data: Uint8Array<ArrayBuffer>;
|
|
86
|
+
width: number;
|
|
87
|
+
height: number;
|
|
88
|
+
}>;
|
package/dist/gpu.js
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
// server/src/runtime/gpu.ts
|
|
2
|
+
var BufferUsage = {
|
|
3
|
+
MAP_READ: 1,
|
|
4
|
+
MAP_WRITE: 2,
|
|
5
|
+
COPY_SRC: 4,
|
|
6
|
+
COPY_DST: 8,
|
|
7
|
+
INDEX: 16,
|
|
8
|
+
VERTEX: 32,
|
|
9
|
+
UNIFORM: 64,
|
|
10
|
+
STORAGE: 128,
|
|
11
|
+
INDIRECT: 256,
|
|
12
|
+
QUERY_RESOLVE: 512
|
|
13
|
+
};
|
|
14
|
+
var TextureUsage = {
|
|
15
|
+
COPY_SRC: 1,
|
|
16
|
+
COPY_DST: 2,
|
|
17
|
+
TEXTURE_BINDING: 4,
|
|
18
|
+
STORAGE_BINDING: 8,
|
|
19
|
+
RENDER_ATTACHMENT: 16,
|
|
20
|
+
/**
|
|
21
|
+
* Chrome 147 has this and it is newer than the rest — verified present
|
|
22
|
+
* 2026-09-08 alongside the others, but do not assume every implementation
|
|
23
|
+
* carries it. Included rather than omitted deliberately: leaving it out
|
|
24
|
+
* would send a node that wants it back to `globalThis`, which is the exact
|
|
25
|
+
* ambient read this file exists to remove, so an honest constant with a
|
|
26
|
+
* caveat beats an omission that forces a rule violation.
|
|
27
|
+
*/
|
|
28
|
+
TRANSIENT_ATTACHMENT: 32
|
|
29
|
+
};
|
|
30
|
+
var ShaderStage = {
|
|
31
|
+
VERTEX: 1,
|
|
32
|
+
FRAGMENT: 2,
|
|
33
|
+
COMPUTE: 4
|
|
34
|
+
};
|
|
35
|
+
var MapMode = {
|
|
36
|
+
READ: 1,
|
|
37
|
+
WRITE: 2
|
|
38
|
+
};
|
|
39
|
+
var ColorWrite = {
|
|
40
|
+
RED: 1,
|
|
41
|
+
GREEN: 2,
|
|
42
|
+
BLUE: 4,
|
|
43
|
+
ALPHA: 8,
|
|
44
|
+
ALL: 15
|
|
45
|
+
};
|
|
46
|
+
async function readTexture(device, texture, options = {}) {
|
|
47
|
+
const { signal } = options;
|
|
48
|
+
if (signal?.aborted) throw new Error("GPU readback aborted");
|
|
49
|
+
if (texture.format !== "rgba8unorm" || texture.dimension !== "2d" || texture.depthOrArrayLayers !== 1 || texture.sampleCount !== 1 || !(texture.usage & TextureUsage.COPY_SRC)) {
|
|
50
|
+
throw new Error("GPU readback requires a single-sample rgba8unorm 2D texture with COPY_SRC usage");
|
|
51
|
+
}
|
|
52
|
+
const { width, height } = texture;
|
|
53
|
+
const rowBytes = width * 4;
|
|
54
|
+
const bytesPerRow = Math.ceil(rowBytes / 256) * 256;
|
|
55
|
+
let buffer;
|
|
56
|
+
let mapped = false;
|
|
57
|
+
let abort;
|
|
58
|
+
let errorScopeOpen = false;
|
|
59
|
+
try {
|
|
60
|
+
device.pushErrorScope("validation");
|
|
61
|
+
errorScopeOpen = true;
|
|
62
|
+
buffer = device.createBuffer({
|
|
63
|
+
label: "Cascade RGBA readback",
|
|
64
|
+
size: bytesPerRow * height,
|
|
65
|
+
usage: BufferUsage.COPY_DST | BufferUsage.MAP_READ
|
|
66
|
+
});
|
|
67
|
+
const encoder = device.createCommandEncoder();
|
|
68
|
+
encoder.copyTextureToBuffer({ texture }, { buffer, bytesPerRow }, [width, height]);
|
|
69
|
+
device.queue.submit([encoder.finish()]);
|
|
70
|
+
const validation = device.popErrorScope();
|
|
71
|
+
errorScopeOpen = false;
|
|
72
|
+
const completion = Promise.allSettled([buffer.mapAsync(MapMode.READ), validation]);
|
|
73
|
+
let completed;
|
|
74
|
+
if (signal) {
|
|
75
|
+
completed = await Promise.race([completion, new Promise((_, reject) => {
|
|
76
|
+
abort = () => reject(new Error("GPU readback aborted"));
|
|
77
|
+
signal.addEventListener("abort", abort, { once: true });
|
|
78
|
+
if (signal.aborted) abort();
|
|
79
|
+
})]);
|
|
80
|
+
} else completed = await completion;
|
|
81
|
+
const mappingResult = completed[0];
|
|
82
|
+
const validationResult = completed[1];
|
|
83
|
+
if (validationResult.status === "rejected") throw validationResult.reason;
|
|
84
|
+
const validationError = validationResult.value;
|
|
85
|
+
if (validationError) throw new Error(`GPU readback validation failed: ${validationError.message}`);
|
|
86
|
+
if (mappingResult.status === "rejected") throw mappingResult.reason;
|
|
87
|
+
mapped = true;
|
|
88
|
+
if (signal?.aborted) throw new Error("GPU readback aborted");
|
|
89
|
+
const source = new Uint8Array(buffer.getMappedRange());
|
|
90
|
+
const data = new Uint8Array(rowBytes * height);
|
|
91
|
+
for (let y = 0; y < height; y++) data.set(source.subarray(y * bytesPerRow, y * bytesPerRow + rowBytes), y * rowBytes);
|
|
92
|
+
return { data, width, height };
|
|
93
|
+
} finally {
|
|
94
|
+
if (abort) signal?.removeEventListener("abort", abort);
|
|
95
|
+
if (errorScopeOpen) {
|
|
96
|
+
try {
|
|
97
|
+
await device.popErrorScope();
|
|
98
|
+
} catch {
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
if (mapped) buffer?.unmap();
|
|
102
|
+
buffer?.destroy();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
export {
|
|
106
|
+
BufferUsage,
|
|
107
|
+
ColorWrite,
|
|
108
|
+
MapMode,
|
|
109
|
+
ShaderStage,
|
|
110
|
+
TextureUsage,
|
|
111
|
+
readTexture
|
|
112
|
+
};
|
|
113
|
+
//# sourceMappingURL=gpu.js.map
|
package/dist/index.html
CHANGED
|
@@ -4,11 +4,11 @@
|
|
|
4
4
|
<meta charset="UTF-8">
|
|
5
5
|
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
6
6
|
<title>Cascade</title>
|
|
7
|
-
<script type="module" crossorigin src="/assets/index-
|
|
7
|
+
<script type="module" crossorigin src="/assets/index-CC6YQw2n.js"></script>
|
|
8
8
|
<link rel="modulepreload" crossorigin href="/assets/legacy-CAj2_7QE.js">
|
|
9
9
|
<link rel="modulepreload" crossorigin href="/assets/index-client-BoJgsN9M.js">
|
|
10
10
|
<link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-BBA1JqHx.js">
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Y2ock1cw.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="app"></div>
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import { type NodeExecutionContext } from "@field/cascade/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* `cascade.core.Camera` — Houdini's `/obj/cam`, as a node.
|
|
4
|
+
*
|
|
5
|
+
* Marcus, 2026-09-08: *"I think it just requires a camera. camera should be a
|
|
6
|
+
* core node."* So a renderer takes a camera on a port rather than carrying its
|
|
7
|
+
* own copy of the same eight parameters, which is what `volume-render` was
|
|
8
|
+
* doing.
|
|
9
|
+
*
|
|
10
|
+
* The split between inputs and props is the usual one and it matters here:
|
|
11
|
+
* **what a camera move animates is an input** — position, orientation, the
|
|
12
|
+
* look-at target, the lens — and **what defines the frame is a prop**, because
|
|
13
|
+
* clipping planes, resolution and projection are set once and left. `focal` is
|
|
14
|
+
* an input for exactly this reason: a focus pull is a camera move.
|
|
15
|
+
*/
|
|
16
|
+
export declare const cameraDefinition: {
|
|
17
|
+
readonly apiVersion: 1;
|
|
18
|
+
readonly label: "Camera";
|
|
19
|
+
readonly description: "A camera, following Houdini's /obj/cam.";
|
|
20
|
+
readonly icon: "Video";
|
|
21
|
+
readonly runsOn: "portable";
|
|
22
|
+
readonly inputs: {
|
|
23
|
+
readonly translate: {
|
|
24
|
+
readonly kind: "data";
|
|
25
|
+
readonly type: "vec3";
|
|
26
|
+
readonly default: readonly [0, 0, 5];
|
|
27
|
+
readonly description: "World position.";
|
|
28
|
+
};
|
|
29
|
+
readonly rotate: {
|
|
30
|
+
readonly kind: "data";
|
|
31
|
+
readonly type: "vec3";
|
|
32
|
+
readonly default: readonly [0, 0, 0];
|
|
33
|
+
readonly description: "Degrees, XYZ order. The camera looks down its own -Z.";
|
|
34
|
+
};
|
|
35
|
+
readonly focal: {
|
|
36
|
+
readonly kind: "data";
|
|
37
|
+
readonly type: "float";
|
|
38
|
+
readonly default: 50;
|
|
39
|
+
readonly min: 1;
|
|
40
|
+
readonly max: 300;
|
|
41
|
+
readonly description: "Focal length in millimetres.";
|
|
42
|
+
};
|
|
43
|
+
readonly lookat: {
|
|
44
|
+
readonly kind: "data";
|
|
45
|
+
readonly type: "vec3";
|
|
46
|
+
readonly default: readonly [0, 0, 0];
|
|
47
|
+
readonly description: "Point to aim at. Used only when Look At is on, and it replaces Rotate.";
|
|
48
|
+
};
|
|
49
|
+
readonly up: {
|
|
50
|
+
readonly kind: "data";
|
|
51
|
+
readonly type: "vec3";
|
|
52
|
+
readonly default: readonly [0, 1, 0];
|
|
53
|
+
readonly description: "Up vector for Look At.";
|
|
54
|
+
};
|
|
55
|
+
readonly spin: {
|
|
56
|
+
readonly kind: "data";
|
|
57
|
+
readonly type: "float";
|
|
58
|
+
readonly default: 0;
|
|
59
|
+
readonly description: "Degrees added to the Y rotation, after Look At. For turntables.";
|
|
60
|
+
};
|
|
61
|
+
};
|
|
62
|
+
readonly outputs: {
|
|
63
|
+
readonly camera: {
|
|
64
|
+
readonly kind: "data";
|
|
65
|
+
readonly type: "camera";
|
|
66
|
+
};
|
|
67
|
+
};
|
|
68
|
+
readonly props: {
|
|
69
|
+
readonly lookAt: {
|
|
70
|
+
readonly type: "bool";
|
|
71
|
+
readonly default: false;
|
|
72
|
+
readonly label: "Look At";
|
|
73
|
+
readonly description: "Aim at the Look At point instead of using Rotate.";
|
|
74
|
+
};
|
|
75
|
+
readonly aperture: {
|
|
76
|
+
readonly type: "float";
|
|
77
|
+
readonly default: 41.4214;
|
|
78
|
+
readonly min: 1;
|
|
79
|
+
readonly max: 200;
|
|
80
|
+
readonly label: "Aperture";
|
|
81
|
+
readonly description: "Horizontal aperture in millimetres. With the default focal length this is a 45 degree view.";
|
|
82
|
+
};
|
|
83
|
+
readonly resolution: {
|
|
84
|
+
readonly type: "vec2i";
|
|
85
|
+
readonly default: readonly [1280, 720];
|
|
86
|
+
readonly label: "Resolution";
|
|
87
|
+
readonly description: "Frame size in pixels. The vertical field of view is derived from this.";
|
|
88
|
+
};
|
|
89
|
+
readonly aspect: {
|
|
90
|
+
readonly type: "float";
|
|
91
|
+
readonly default: 1;
|
|
92
|
+
readonly min: 0.1;
|
|
93
|
+
readonly max: 10;
|
|
94
|
+
readonly label: "Pixel Aspect";
|
|
95
|
+
readonly description: "The aspect of one pixel, not of the frame.";
|
|
96
|
+
};
|
|
97
|
+
readonly projection: {
|
|
98
|
+
readonly type: "string";
|
|
99
|
+
readonly default: "perspective";
|
|
100
|
+
readonly label: "Projection";
|
|
101
|
+
readonly control: "select";
|
|
102
|
+
readonly options: readonly [{
|
|
103
|
+
readonly value: "perspective";
|
|
104
|
+
readonly label: "Perspective";
|
|
105
|
+
}, {
|
|
106
|
+
readonly value: "orthographic";
|
|
107
|
+
readonly label: "Orthographic";
|
|
108
|
+
}];
|
|
109
|
+
};
|
|
110
|
+
readonly orthowidth: {
|
|
111
|
+
readonly type: "float";
|
|
112
|
+
readonly default: 2;
|
|
113
|
+
readonly min: 0.01;
|
|
114
|
+
readonly label: "Ortho Width";
|
|
115
|
+
readonly description: "Width of the orthographic frame in world units.";
|
|
116
|
+
};
|
|
117
|
+
readonly near: {
|
|
118
|
+
readonly type: "float";
|
|
119
|
+
readonly default: 0.001;
|
|
120
|
+
readonly min: 0;
|
|
121
|
+
readonly label: "Near Clip";
|
|
122
|
+
};
|
|
123
|
+
readonly far: {
|
|
124
|
+
readonly type: "float";
|
|
125
|
+
readonly default: 10000;
|
|
126
|
+
readonly min: 0;
|
|
127
|
+
readonly label: "Far Clip";
|
|
128
|
+
};
|
|
129
|
+
};
|
|
130
|
+
};
|
|
131
|
+
export declare function executeCamera(context: NodeExecutionContext<typeof cameraDefinition>): void;
|
|
132
|
+
export declare const cameraRegistration: {
|
|
133
|
+
kind: "definition-v1";
|
|
134
|
+
moduleId: string;
|
|
135
|
+
definition: {
|
|
136
|
+
readonly apiVersion: 1;
|
|
137
|
+
readonly label: "Camera";
|
|
138
|
+
readonly description: "A camera, following Houdini's /obj/cam.";
|
|
139
|
+
readonly icon: "Video";
|
|
140
|
+
readonly runsOn: "portable";
|
|
141
|
+
readonly inputs: {
|
|
142
|
+
readonly translate: {
|
|
143
|
+
readonly kind: "data";
|
|
144
|
+
readonly type: "vec3";
|
|
145
|
+
readonly default: readonly [0, 0, 5];
|
|
146
|
+
readonly description: "World position.";
|
|
147
|
+
};
|
|
148
|
+
readonly rotate: {
|
|
149
|
+
readonly kind: "data";
|
|
150
|
+
readonly type: "vec3";
|
|
151
|
+
readonly default: readonly [0, 0, 0];
|
|
152
|
+
readonly description: "Degrees, XYZ order. The camera looks down its own -Z.";
|
|
153
|
+
};
|
|
154
|
+
readonly focal: {
|
|
155
|
+
readonly kind: "data";
|
|
156
|
+
readonly type: "float";
|
|
157
|
+
readonly default: 50;
|
|
158
|
+
readonly min: 1;
|
|
159
|
+
readonly max: 300;
|
|
160
|
+
readonly description: "Focal length in millimetres.";
|
|
161
|
+
};
|
|
162
|
+
readonly lookat: {
|
|
163
|
+
readonly kind: "data";
|
|
164
|
+
readonly type: "vec3";
|
|
165
|
+
readonly default: readonly [0, 0, 0];
|
|
166
|
+
readonly description: "Point to aim at. Used only when Look At is on, and it replaces Rotate.";
|
|
167
|
+
};
|
|
168
|
+
readonly up: {
|
|
169
|
+
readonly kind: "data";
|
|
170
|
+
readonly type: "vec3";
|
|
171
|
+
readonly default: readonly [0, 1, 0];
|
|
172
|
+
readonly description: "Up vector for Look At.";
|
|
173
|
+
};
|
|
174
|
+
readonly spin: {
|
|
175
|
+
readonly kind: "data";
|
|
176
|
+
readonly type: "float";
|
|
177
|
+
readonly default: 0;
|
|
178
|
+
readonly description: "Degrees added to the Y rotation, after Look At. For turntables.";
|
|
179
|
+
};
|
|
180
|
+
};
|
|
181
|
+
readonly outputs: {
|
|
182
|
+
readonly camera: {
|
|
183
|
+
readonly kind: "data";
|
|
184
|
+
readonly type: "camera";
|
|
185
|
+
};
|
|
186
|
+
};
|
|
187
|
+
readonly props: {
|
|
188
|
+
readonly lookAt: {
|
|
189
|
+
readonly type: "bool";
|
|
190
|
+
readonly default: false;
|
|
191
|
+
readonly label: "Look At";
|
|
192
|
+
readonly description: "Aim at the Look At point instead of using Rotate.";
|
|
193
|
+
};
|
|
194
|
+
readonly aperture: {
|
|
195
|
+
readonly type: "float";
|
|
196
|
+
readonly default: 41.4214;
|
|
197
|
+
readonly min: 1;
|
|
198
|
+
readonly max: 200;
|
|
199
|
+
readonly label: "Aperture";
|
|
200
|
+
readonly description: "Horizontal aperture in millimetres. With the default focal length this is a 45 degree view.";
|
|
201
|
+
};
|
|
202
|
+
readonly resolution: {
|
|
203
|
+
readonly type: "vec2i";
|
|
204
|
+
readonly default: readonly [1280, 720];
|
|
205
|
+
readonly label: "Resolution";
|
|
206
|
+
readonly description: "Frame size in pixels. The vertical field of view is derived from this.";
|
|
207
|
+
};
|
|
208
|
+
readonly aspect: {
|
|
209
|
+
readonly type: "float";
|
|
210
|
+
readonly default: 1;
|
|
211
|
+
readonly min: 0.1;
|
|
212
|
+
readonly max: 10;
|
|
213
|
+
readonly label: "Pixel Aspect";
|
|
214
|
+
readonly description: "The aspect of one pixel, not of the frame.";
|
|
215
|
+
};
|
|
216
|
+
readonly projection: {
|
|
217
|
+
readonly type: "string";
|
|
218
|
+
readonly default: "perspective";
|
|
219
|
+
readonly label: "Projection";
|
|
220
|
+
readonly control: "select";
|
|
221
|
+
readonly options: readonly [{
|
|
222
|
+
readonly value: "perspective";
|
|
223
|
+
readonly label: "Perspective";
|
|
224
|
+
}, {
|
|
225
|
+
readonly value: "orthographic";
|
|
226
|
+
readonly label: "Orthographic";
|
|
227
|
+
}];
|
|
228
|
+
};
|
|
229
|
+
readonly orthowidth: {
|
|
230
|
+
readonly type: "float";
|
|
231
|
+
readonly default: 2;
|
|
232
|
+
readonly min: 0.01;
|
|
233
|
+
readonly label: "Ortho Width";
|
|
234
|
+
readonly description: "Width of the orthographic frame in world units.";
|
|
235
|
+
};
|
|
236
|
+
readonly near: {
|
|
237
|
+
readonly type: "float";
|
|
238
|
+
readonly default: 0.001;
|
|
239
|
+
readonly min: 0;
|
|
240
|
+
readonly label: "Near Clip";
|
|
241
|
+
};
|
|
242
|
+
readonly far: {
|
|
243
|
+
readonly type: "float";
|
|
244
|
+
readonly default: 10000;
|
|
245
|
+
readonly min: 0;
|
|
246
|
+
readonly label: "Far Clip";
|
|
247
|
+
};
|
|
248
|
+
};
|
|
249
|
+
};
|
|
250
|
+
loadExecute: () => Promise<typeof executeCamera>;
|
|
251
|
+
};
|
|
252
|
+
//# sourceMappingURL=camera.d.ts.map
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
import { CAMERA_DEFAULTS } from "@field/cascade/contracts";
|
|
2
|
+
import { lookAtRotation } from "../../camera/index.js";
|
|
3
|
+
/**
|
|
4
|
+
* `cascade.core.Camera` — Houdini's `/obj/cam`, as a node.
|
|
5
|
+
*
|
|
6
|
+
* Marcus, 2026-09-08: *"I think it just requires a camera. camera should be a
|
|
7
|
+
* core node."* So a renderer takes a camera on a port rather than carrying its
|
|
8
|
+
* own copy of the same eight parameters, which is what `volume-render` was
|
|
9
|
+
* doing.
|
|
10
|
+
*
|
|
11
|
+
* The split between inputs and props is the usual one and it matters here:
|
|
12
|
+
* **what a camera move animates is an input** — position, orientation, the
|
|
13
|
+
* look-at target, the lens — and **what defines the frame is a prop**, because
|
|
14
|
+
* clipping planes, resolution and projection are set once and left. `focal` is
|
|
15
|
+
* an input for exactly this reason: a focus pull is a camera move.
|
|
16
|
+
*/
|
|
17
|
+
export const cameraDefinition = {
|
|
18
|
+
apiVersion: 1,
|
|
19
|
+
label: "Camera",
|
|
20
|
+
description: "A camera, following Houdini's /obj/cam.",
|
|
21
|
+
icon: "Video",
|
|
22
|
+
runsOn: "portable",
|
|
23
|
+
inputs: {
|
|
24
|
+
translate: {
|
|
25
|
+
kind: "data",
|
|
26
|
+
type: "vec3",
|
|
27
|
+
default: [0, 0, 5],
|
|
28
|
+
description: "World position.",
|
|
29
|
+
},
|
|
30
|
+
rotate: {
|
|
31
|
+
kind: "data",
|
|
32
|
+
type: "vec3",
|
|
33
|
+
default: [0, 0, 0],
|
|
34
|
+
description: "Degrees, XYZ order. The camera looks down its own -Z.",
|
|
35
|
+
},
|
|
36
|
+
focal: {
|
|
37
|
+
kind: "data",
|
|
38
|
+
type: "float",
|
|
39
|
+
default: 50,
|
|
40
|
+
min: 1,
|
|
41
|
+
max: 300,
|
|
42
|
+
description: "Focal length in millimetres.",
|
|
43
|
+
},
|
|
44
|
+
lookat: {
|
|
45
|
+
kind: "data",
|
|
46
|
+
type: "vec3",
|
|
47
|
+
default: [0, 0, 0],
|
|
48
|
+
description: "Point to aim at. Used only when Look At is on, and it replaces Rotate.",
|
|
49
|
+
},
|
|
50
|
+
up: {
|
|
51
|
+
kind: "data",
|
|
52
|
+
type: "vec3",
|
|
53
|
+
default: [0, 1, 0],
|
|
54
|
+
description: "Up vector for Look At.",
|
|
55
|
+
},
|
|
56
|
+
spin: {
|
|
57
|
+
kind: "data",
|
|
58
|
+
type: "float",
|
|
59
|
+
default: 0,
|
|
60
|
+
description: "Degrees added to the Y rotation, after Look At. For turntables.",
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
outputs: {
|
|
64
|
+
camera: { kind: "data", type: "camera" },
|
|
65
|
+
},
|
|
66
|
+
props: {
|
|
67
|
+
lookAt: {
|
|
68
|
+
type: "bool",
|
|
69
|
+
default: false,
|
|
70
|
+
label: "Look At",
|
|
71
|
+
description: "Aim at the Look At point instead of using Rotate.",
|
|
72
|
+
},
|
|
73
|
+
aperture: {
|
|
74
|
+
type: "float",
|
|
75
|
+
default: 41.4214,
|
|
76
|
+
min: 1,
|
|
77
|
+
max: 200,
|
|
78
|
+
label: "Aperture",
|
|
79
|
+
description: "Horizontal aperture in millimetres. With the default focal length this is a 45 degree view.",
|
|
80
|
+
},
|
|
81
|
+
resolution: {
|
|
82
|
+
type: "vec2i",
|
|
83
|
+
default: [1280, 720],
|
|
84
|
+
label: "Resolution",
|
|
85
|
+
description: "Frame size in pixels. The vertical field of view is derived from this.",
|
|
86
|
+
},
|
|
87
|
+
aspect: {
|
|
88
|
+
type: "float",
|
|
89
|
+
default: 1,
|
|
90
|
+
min: 0.1,
|
|
91
|
+
max: 10,
|
|
92
|
+
label: "Pixel Aspect",
|
|
93
|
+
description: "The aspect of one pixel, not of the frame.",
|
|
94
|
+
},
|
|
95
|
+
projection: {
|
|
96
|
+
type: "string",
|
|
97
|
+
default: "perspective",
|
|
98
|
+
label: "Projection",
|
|
99
|
+
control: "select",
|
|
100
|
+
options: [
|
|
101
|
+
{ value: "perspective", label: "Perspective" },
|
|
102
|
+
{ value: "orthographic", label: "Orthographic" },
|
|
103
|
+
],
|
|
104
|
+
},
|
|
105
|
+
orthowidth: {
|
|
106
|
+
type: "float",
|
|
107
|
+
default: 2,
|
|
108
|
+
min: 0.01,
|
|
109
|
+
label: "Ortho Width",
|
|
110
|
+
description: "Width of the orthographic frame in world units.",
|
|
111
|
+
},
|
|
112
|
+
near: { type: "float", default: 0.001, min: 0, label: "Near Clip" },
|
|
113
|
+
far: { type: "float", default: 10000, min: 0, label: "Far Clip" },
|
|
114
|
+
},
|
|
115
|
+
};
|
|
116
|
+
export function executeCamera(context) {
|
|
117
|
+
const { translate, rotate, focal, lookat, up, spin } = context.inputs;
|
|
118
|
+
const props = context.props;
|
|
119
|
+
const aimed = props.lookAt
|
|
120
|
+
? lookAtRotation(translate, lookat, up)
|
|
121
|
+
: rotate;
|
|
122
|
+
// Spin is applied after look-at rather than folded into it, so a turntable
|
|
123
|
+
// keeps pointing at its subject while it goes round. Degrees, because
|
|
124
|
+
// `rotate` is degrees and adding a different unit to it would be a trap.
|
|
125
|
+
const oriented = spin === 0
|
|
126
|
+
? aimed
|
|
127
|
+
: [aimed[0], aimed[1] + spin, aimed[2]];
|
|
128
|
+
const camera = {
|
|
129
|
+
...CAMERA_DEFAULTS,
|
|
130
|
+
translate: translate,
|
|
131
|
+
rotate: oriented,
|
|
132
|
+
focal,
|
|
133
|
+
aperture: props.aperture,
|
|
134
|
+
resolution: props.resolution,
|
|
135
|
+
aspect: props.aspect,
|
|
136
|
+
projection: props.projection === "orthographic" ? "orthographic" : "perspective",
|
|
137
|
+
orthowidth: props.orthowidth,
|
|
138
|
+
near: props.near,
|
|
139
|
+
far: props.far,
|
|
140
|
+
};
|
|
141
|
+
context.outputs.camera.set(camera);
|
|
142
|
+
}
|
|
143
|
+
export const cameraRegistration = {
|
|
144
|
+
kind: "definition-v1",
|
|
145
|
+
moduleId: "cascade.core.Camera",
|
|
146
|
+
definition: cameraDefinition,
|
|
147
|
+
loadExecute: async () => executeCamera,
|
|
148
|
+
};
|
|
149
|
+
//# sourceMappingURL=camera.js.map
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import type { DefinitionNodeRegistration } from "../../types.js";
|
|
2
|
+
import { cameraRegistration } from "./camera.js";
|
|
2
3
|
import { randomRegistration } from "./random.js";
|
|
3
4
|
import { remapRegistration } from "./remap.js";
|
|
4
5
|
import { mergeRegistration, nullRegistration, selectRegistration, switchRegistration } from "./routing.js";
|
|
5
6
|
import { inputRegistration, outputRegistration, subnetRegistration } from "./structural.js";
|
|
6
|
-
export { inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, };
|
|
7
|
+
export { cameraRegistration, inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, };
|
|
7
8
|
export declare const coreNodeRegistrations: readonly DefinitionNodeRegistration[];
|
|
8
9
|
export declare function coreNodeRegistration(moduleId: string): DefinitionNodeRegistration | undefined;
|
|
9
10
|
//# sourceMappingURL=index.d.ts.map
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
import { cameraRegistration } from "./camera.js";
|
|
1
2
|
import { randomRegistration } from "./random.js";
|
|
2
3
|
import { remapRegistration } from "./remap.js";
|
|
3
4
|
import { mergeRegistration, nullRegistration, selectRegistration, switchRegistration } from "./routing.js";
|
|
4
5
|
import { inputRegistration, outputRegistration, subnetRegistration } from "./structural.js";
|
|
5
|
-
export { inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, };
|
|
6
|
+
export { cameraRegistration, inputRegistration, mergeRegistration, nullRegistration, outputRegistration, randomRegistration, remapRegistration, selectRegistration, subnetRegistration, switchRegistration, };
|
|
6
7
|
export const coreNodeRegistrations = Object.freeze([
|
|
7
8
|
subnetRegistration,
|
|
8
9
|
inputRegistration,
|
|
@@ -13,6 +14,7 @@ export const coreNodeRegistrations = Object.freeze([
|
|
|
13
14
|
selectRegistration,
|
|
14
15
|
randomRegistration,
|
|
15
16
|
remapRegistration,
|
|
17
|
+
cameraRegistration,
|
|
16
18
|
]);
|
|
17
19
|
const registrationsById = new Map(coreNodeRegistrations.map((registration) => [registration.moduleId, registration]));
|
|
18
20
|
export function coreNodeRegistration(moduleId) {
|