@field/cascade 0.3.1 → 0.5.1
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 +32 -9
- package/ARCHITECTURE.md +75 -13
- package/CHANGELOG.md +98 -1
- package/CONTRIBUTING.md +104 -0
- package/DESIGN.md +24 -15
- package/README.md +121 -14
- package/dist/assets/{CodeEditor-D22fVi-4.js → CodeEditor-dOrU3aQg.js} +7 -7
- package/dist/assets/{CodePanel-DPNEYfcR.js → CodePanel-Bqy85qen.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DeHCAvgb.js → CustomNodeDialog-DkZlOgjs.js} +1 -1
- package/dist/assets/{ProjectSettings-DWpVtWEx.js → ProjectSettings-D192mr6g.js} +2 -2
- package/dist/assets/{SettingsDialog-za00IS3L.js → SettingsDialog-BTeeIE3T.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DTMGhI0_.js → VersionHistoryDialog-TZQKkn3Y.js} +1 -1
- package/dist/assets/{core-JsyKMO1k.js → core-DwbcGL0o.js} +2 -2
- package/dist/assets/{cssMode-emdRheFK.js → cssMode-Dcjn356x.js} +1 -1
- package/dist/assets/definitions-C_SY6Bhg.js +1 -0
- package/dist/assets/{editor.api-xHX-fMSo.js → editor.api-D7Q83606.js} +1 -1
- package/dist/assets/{freemarker2-C3M8_Gs3.js → freemarker2-DCCiHlV6.js} +1 -1
- package/dist/assets/geo-CT-wtvaE.js +1 -0
- package/dist/assets/{handlebars-CBMurtFk.js → handlebars-BCxC8xu1.js} +1 -1
- package/dist/assets/{html-xbLQwknw.js → html-COwepzSd.js} +1 -1
- package/dist/assets/{htmlMode-2wKdDTon.js → htmlMode-Dpg7H0rx.js} +1 -1
- package/dist/assets/{image-i1j6nWKe.js → image-CMfMMxlC.js} +3 -3
- package/dist/assets/index-De1cIZ9N.js +212 -0
- package/dist/assets/index-ilgbOu6G.css +1 -0
- package/dist/assets/{javascript-DJaOo_2I.js → javascript-CSwWxfLr.js} +1 -1
- package/dist/assets/{jsonMode-Bheas8_0.js → jsonMode-BXRriTpj.js} +1 -1
- package/dist/assets/{liquid-BCj_BGbM.js → liquid-Cmkfyogc.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-BfIsx_xz.js → lspLanguageFeatures-xH9JbZGO.js} +1 -1
- package/dist/assets/{mdx-DwiQDiVO.js → mdx-BzrNjNvK.js} +1 -1
- package/dist/assets/nodeTypeUtils-BDJsc2Ks.js +1 -0
- package/dist/assets/pop-BdKEHslJ.js +1 -0
- package/dist/assets/{python-BjrB5nqj.js → python-D4E9jbx7.js} +1 -1
- package/dist/assets/{razor-CfoBmGDs.js → razor-B3x9cIEM.js} +1 -1
- package/dist/assets/{tsMode-BcEoMSxp.js → tsMode-dhQYPNMZ.js} +1 -1
- package/dist/assets/{typescript-DIg7TEmQ.js → typescript-CITkDxAE.js} +1 -1
- package/dist/assets/{workers-vXYuwtPw.js → workers-BQvF9bbO.js} +1 -1
- package/dist/assets/{xml-DexLQIQy.js → xml-DvHlU3A8.js} +1 -1
- package/dist/assets/{yaml-DUnD_1dE.js → yaml-uznsLgs8.js} +1 -1
- package/dist/cli/index.js +16465 -12096
- package/dist/cli/runtime/gpu.ts +71 -0
- package/dist/cli/runtime/io.ts +31 -25
- 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 +62 -4
- package/dist/contracts/index.d.ts +4 -0
- package/dist/contracts/index.js +4 -0
- package/dist/contracts/particles.d.ts +54 -0
- package/dist/contracts/particles.js +167 -0
- package/dist/contracts/scene.d.ts +129 -0
- package/dist/contracts/scene.js +73 -0
- package/dist/contracts/schema.js +26 -3
- package/dist/contracts/series.d.ts +75 -0
- package/dist/contracts/series.js +2 -0
- package/dist/contracts/validate.js +23 -5
- package/dist/contracts/values.d.ts +4 -2
- package/dist/contracts/values.js +10 -0
- package/dist/gpu.d.ts +17 -0
- package/dist/gpu.js +61 -1
- package/dist/index.html +3 -3
- package/dist/io.d.ts +4 -16
- package/dist/io.js +26 -8
- package/dist/player/app.js +810 -0
- package/dist/player/embed.d.ts +4 -0
- package/dist/player/embed.js +77 -0
- package/dist/player/types.d.ts +35 -0
- 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 +4 -2
- package/dist/runtime/builtins/core/index.js +8 -2
- package/dist/runtime/builtins/core/structural.d.ts +168 -0
- package/dist/runtime/builtins/core/structural.js +175 -0
- package/dist/runtime/builtins/core/time.d.ts +165 -0
- package/dist/runtime/builtins/core/time.js +110 -0
- package/dist/runtime/builtins/geo/definitions.d.ts +247 -0
- package/dist/runtime/builtins/geo/definitions.js +70 -0
- package/dist/runtime/builtins/geo/index.d.ts +2 -1
- package/dist/runtime/builtins/geo/index.js +3 -1
- package/dist/runtime/builtins/geo/render.d.ts +87 -0
- package/dist/runtime/builtins/geo/render.js +87 -0
- package/dist/runtime/builtins/geo/svgExport.d.ts +11 -0
- package/dist/runtime/builtins/geo/svgExport.js +10 -0
- package/dist/runtime/builtins/index.d.ts +5 -2
- package/dist/runtime/builtins/index.js +13 -3
- package/dist/runtime/builtins/pop/definitions.d.ts +2120 -0
- package/dist/runtime/builtins/pop/definitions.js +815 -0
- package/dist/runtime/builtins/pop/index.d.ts +2 -0
- package/dist/runtime/builtins/pop/index.js +2 -0
- package/dist/runtime/builtins/scene/definitions.d.ts +356 -0
- package/dist/runtime/builtins/scene/definitions.js +224 -0
- package/dist/runtime/builtins/scene/index.d.ts +2 -0
- package/dist/runtime/builtins/scene/index.js +2 -0
- 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/geometry/render.d.ts +109 -0
- package/dist/runtime/geometry/render.js +145 -0
- package/dist/runtime/geometry/svg.d.ts +29 -0
- package/dist/runtime/geometry/svg.js +11 -5
- package/dist/runtime/gpu.d.ts +33 -0
- package/dist/runtime/gpu.js +218 -0
- package/dist/runtime/index.d.ts +4 -0
- package/dist/runtime/index.js +4 -0
- package/dist/runtime/pop/cache.d.ts +27 -0
- package/dist/runtime/pop/cache.js +123 -0
- package/dist/runtime/pop/forces.d.ts +202 -0
- package/dist/runtime/pop/forces.js +519 -0
- package/dist/runtime/pop/index.d.ts +5 -0
- package/dist/runtime/pop/index.js +5 -0
- package/dist/runtime/pop/state.d.ts +136 -0
- package/dist/runtime/pop/state.js +283 -0
- package/dist/runtime/pop/trail.d.ts +62 -0
- package/dist/runtime/pop/trail.js +142 -0
- package/dist/runtime/runtime.js +284 -9
- package/dist/runtime/scene/index.d.ts +50 -0
- package/dist/runtime/scene/index.js +94 -0
- 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/dist/studio/panel.d.ts +68 -1
- package/doc/HEADLESS_GPU.md +160 -0
- package/doc/NODE_REFERENCE.md +408 -1
- package/doc/PROJECT_AUTHORING.md +118 -20
- package/doc/WEB_PLAYER.md +131 -0
- package/package.json +25 -3
- 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/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
|
@@ -0,0 +1,234 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Everything derived from a `camera`, so that no renderer derives it twice.
|
|
3
|
+
*
|
|
4
|
+
* The type stores intent — a transform, a lens, a frame — and every consumer
|
|
5
|
+
* wants something else: a rasteriser wants matrices, a raymarcher wants the
|
|
6
|
+
* basis vectors directly, and a UI wants the angle in degrees. Deriving those
|
|
7
|
+
* here is the same argument as `resolvePropBinding`: one implementation, or the
|
|
8
|
+
* second one disagrees with the first about handedness in a way that takes an
|
|
9
|
+
* afternoon to find.
|
|
10
|
+
*
|
|
11
|
+
* Houdini's conventions throughout: the camera looks down its own **-Z** with
|
|
12
|
+
* **+Y** up, rotation is degrees applied in **XYZ** order, and the vertical
|
|
13
|
+
* aperture is derived from the horizontal one and the frame shape rather than
|
|
14
|
+
* stored.
|
|
15
|
+
*
|
|
16
|
+
* **Matching a frame is one number, never two.** Both angles come from the same
|
|
17
|
+
* `focal`, `aperture` and resolution ratio, so a `focal` chosen to reproduce a
|
|
18
|
+
* known horizontal angle reproduces the vertical one at the same time. Worked
|
|
19
|
+
* example, from converting `cloud-volumes` off its hand-rolled projection on
|
|
20
|
+
* 2026-09-08: that sketch pinned its **vertical** half-tangent at `0.36` and
|
|
21
|
+
* scaled the horizontal by aspect, which on a 1098×1512 page gave a 29.3°
|
|
22
|
+
* horizontal angle; `focal = (aperture / 2) / 0.261 ≈ 79.2 mm` reproduces it,
|
|
23
|
+
* and the vertical lands back on `0.36` without being asked. Which is also the
|
|
24
|
+
* reason that sketch's `fit` parameter had to go — it backed the camera off to
|
|
25
|
+
* win back horizontal coverage a portrait page had lost, and that is the same
|
|
26
|
+
* compensation this module already does in the projection.
|
|
27
|
+
*/
|
|
28
|
+
const DEG = Math.PI / 180;
|
|
29
|
+
function cross(a, b) {
|
|
30
|
+
return [
|
|
31
|
+
a[1] * b[2] - a[2] * b[1],
|
|
32
|
+
a[2] * b[0] - a[0] * b[2],
|
|
33
|
+
a[0] * b[1] - a[1] * b[0],
|
|
34
|
+
];
|
|
35
|
+
}
|
|
36
|
+
function normalize(v) {
|
|
37
|
+
const length = Math.hypot(v[0], v[1], v[2]);
|
|
38
|
+
if (length === 0)
|
|
39
|
+
return [0, 0, 0];
|
|
40
|
+
return [v[0] / length, v[1] / length, v[2] / length];
|
|
41
|
+
}
|
|
42
|
+
function subtract(a, b) {
|
|
43
|
+
return [a[0] - b[0], a[1] - b[1], a[2] - b[2]];
|
|
44
|
+
}
|
|
45
|
+
function dot(a, b) {
|
|
46
|
+
return a[0] * b[0] + a[1] * b[1] + a[2] * b[2];
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The rotation the camera needs to look at a point, as Houdini's look-at does.
|
|
50
|
+
*
|
|
51
|
+
* Returned as degrees in XYZ order so it can be written straight into
|
|
52
|
+
* `camera.rotate` — which is what makes look-at an override of the transform
|
|
53
|
+
* rather than a second, parallel way to orient a camera.
|
|
54
|
+
*
|
|
55
|
+
* **`up` chooses the roll, and only the roll.** Yaw and pitch are fixed the
|
|
56
|
+
* moment a direction is given; all an up vector can still decide is the
|
|
57
|
+
* rotation about that direction. The default `[0, 1, 0]` is Houdini's own
|
|
58
|
+
* `upvector` default and gives roll zero for any horizontal up, which is why
|
|
59
|
+
* every ordinary orbit never needs to think about it.
|
|
60
|
+
*
|
|
61
|
+
* Written first with `roll = 0` and the parameter ignored, which was wrong in
|
|
62
|
+
* the way this codebase keeps finding: the node exposes `up` as an input, so a
|
|
63
|
+
* value set there did nothing at all and nothing said so.
|
|
64
|
+
*/
|
|
65
|
+
export function lookAtRotation(from, to, up = [0, 1, 0]) {
|
|
66
|
+
const forward = normalize(subtract(to, from));
|
|
67
|
+
if (forward[0] === 0 && forward[1] === 0 && forward[2] === 0)
|
|
68
|
+
return [0, 0, 0];
|
|
69
|
+
// Pitch first, because yaw is measured in the horizontal plane and the
|
|
70
|
+
// horizontal length is what is left of the vector once pitch is taken out.
|
|
71
|
+
const horizontal = Math.hypot(forward[0], forward[2]);
|
|
72
|
+
const pitch = Math.atan2(forward[1], horizontal);
|
|
73
|
+
// Looking down -Z at rest, so a forward of (0, 0, -1) must give a yaw of 0.
|
|
74
|
+
const yaw = Math.atan2(-forward[0], -forward[2]);
|
|
75
|
+
return [pitch / DEG, yaw / DEG, rollTowards(forward, pitch, yaw, up) / DEG];
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* The rotation about `forward` that takes the world-up-locked up vector onto
|
|
79
|
+
* the requested one.
|
|
80
|
+
*
|
|
81
|
+
* Only the component of `up` perpendicular to `forward` can be honoured — the
|
|
82
|
+
* parallel component is asking the camera to look along its own up, which is
|
|
83
|
+
* not a rotation. So it is projected out, and an up parallel to forward leaves
|
|
84
|
+
* nothing to aim at and returns zero rather than a NaN.
|
|
85
|
+
*/
|
|
86
|
+
function rollTowards(forward, pitch, yaw, up) {
|
|
87
|
+
const desired = normalize([
|
|
88
|
+
up[0] - forward[0] * dot(up, forward),
|
|
89
|
+
up[1] - forward[1] * dot(up, forward),
|
|
90
|
+
up[2] - forward[2] * dot(up, forward),
|
|
91
|
+
]);
|
|
92
|
+
if (desired[0] === 0 && desired[1] === 0 && desired[2] === 0)
|
|
93
|
+
return 0;
|
|
94
|
+
// The up this camera has with no roll, taken from the basis rather than
|
|
95
|
+
// rebuilt, so the two cannot drift apart.
|
|
96
|
+
const locked = cameraBasis({
|
|
97
|
+
...CAMERA_ROTATION_ONLY,
|
|
98
|
+
rotate: [pitch / DEG, yaw / DEG, 0],
|
|
99
|
+
}).up;
|
|
100
|
+
// `cross(desired, locked)` rather than the other way round: a positive
|
|
101
|
+
// `rotate.z` carries the camera's up toward its own -X, so the angle that
|
|
102
|
+
// takes `locked` onto `desired` runs the opposite way.
|
|
103
|
+
return Math.atan2(dot(cross(desired, locked), forward), dot(locked, desired));
|
|
104
|
+
}
|
|
105
|
+
/** Only `rotate` is read by `cameraBasis`, so the rest is filler rather than a
|
|
106
|
+
* claim about any real camera. */
|
|
107
|
+
const CAMERA_ROTATION_ONLY = {
|
|
108
|
+
translate: [0, 0, 0],
|
|
109
|
+
focal: 50,
|
|
110
|
+
aperture: 41.4214,
|
|
111
|
+
near: 0.001,
|
|
112
|
+
far: 10000,
|
|
113
|
+
resolution: [1, 1],
|
|
114
|
+
aspect: 1,
|
|
115
|
+
projection: "perspective",
|
|
116
|
+
orthowidth: 2,
|
|
117
|
+
};
|
|
118
|
+
/** The camera's axes, applying Houdini's XYZ rotation order to a camera that
|
|
119
|
+
* looks down -Z at rest. */
|
|
120
|
+
export function cameraBasis(camera) {
|
|
121
|
+
const [rx, ry, rz] = camera.rotate;
|
|
122
|
+
const sx = Math.sin(rx * DEG);
|
|
123
|
+
const cx = Math.cos(rx * DEG);
|
|
124
|
+
const sy = Math.sin(ry * DEG);
|
|
125
|
+
const cy = Math.cos(ry * DEG);
|
|
126
|
+
const sz = Math.sin(rz * DEG);
|
|
127
|
+
const cz = Math.cos(rz * DEG);
|
|
128
|
+
/**
|
|
129
|
+
* `R = Ry * Rx * Rz` on a column vector: yaw about world +Y, then pitch
|
|
130
|
+
* about the camera's own right, then roll about its own view direction.
|
|
131
|
+
*
|
|
132
|
+
* This is the third order tried and the reason is worth writing down, because
|
|
133
|
+
* two of them are indistinguishable by almost every test. With `rotate.z` at
|
|
134
|
+
* zero all three agree exactly — so yaw, pitch, look-at and orthonormality
|
|
135
|
+
* passed under each, and only the roll separates them. Under `Rz * Ry * Rx`,
|
|
136
|
+
* written first, `rotate.z` turns the camera about the WORLD z axis, which
|
|
137
|
+
* looks like a roll for as long as the camera faces -Z and stops being one
|
|
138
|
+
* the moment it does not. Under `Rx * Ry * Rz` the yaw stops being a yaw
|
|
139
|
+
* about world up, which is what an orbit needs. This order is the one every
|
|
140
|
+
* camera rig uses, and it is what banking a camera means.
|
|
141
|
+
*/
|
|
142
|
+
const m00 = cy * cz + sy * sx * sz;
|
|
143
|
+
const m01 = -cy * sz + sy * sx * cz;
|
|
144
|
+
const m02 = sy * cx;
|
|
145
|
+
const m10 = cx * sz;
|
|
146
|
+
const m11 = cx * cz;
|
|
147
|
+
const m12 = -sx;
|
|
148
|
+
const m20 = -sy * cz + cy * sx * sz;
|
|
149
|
+
const m21 = sy * sz + cy * sx * cz;
|
|
150
|
+
const m22 = cy * cx;
|
|
151
|
+
const right = [m00, m10, m20];
|
|
152
|
+
const up = [m01, m11, m21];
|
|
153
|
+
// Its own -Z, which is the whole convention in one line.
|
|
154
|
+
const forward = [-m02, -m12, -m22];
|
|
155
|
+
return { eye: camera.translate, forward, right, up };
|
|
156
|
+
}
|
|
157
|
+
/**
|
|
158
|
+
* The horizontal field of view in degrees, from the lens rather than stored.
|
|
159
|
+
*
|
|
160
|
+
* Houdini's defaults of 50 mm and 41.4214 mm are exactly 45°, which is the
|
|
161
|
+
* check worth having in a test: it is the one value that proves the formula and
|
|
162
|
+
* the defaults agree.
|
|
163
|
+
*/
|
|
164
|
+
export function horizontalFov(camera) {
|
|
165
|
+
if (camera.focal === 0)
|
|
166
|
+
return 0;
|
|
167
|
+
return (2 * Math.atan(camera.aperture / (2 * camera.focal))) / DEG;
|
|
168
|
+
}
|
|
169
|
+
/**
|
|
170
|
+
* The VERTICAL field of view in degrees.
|
|
171
|
+
*
|
|
172
|
+
* Houdini does not store a vertical aperture; it derives one from the
|
|
173
|
+
* horizontal aperture, the resolution ratio and the pixel aspect. Which is why
|
|
174
|
+
* `resolution` is on the camera at all — without it there is no vertical angle
|
|
175
|
+
* to be had.
|
|
176
|
+
*/
|
|
177
|
+
export function verticalFov(camera) {
|
|
178
|
+
if (camera.focal === 0)
|
|
179
|
+
return 0;
|
|
180
|
+
const [width, height] = camera.resolution;
|
|
181
|
+
if (width === 0 || camera.aspect === 0)
|
|
182
|
+
return 0;
|
|
183
|
+
const verticalAperture = (camera.aperture * height) / (width * camera.aspect);
|
|
184
|
+
return (2 * Math.atan(verticalAperture / (2 * camera.focal))) / DEG;
|
|
185
|
+
}
|
|
186
|
+
/** Frame aspect — the shape of the picture, pixel aspect included. Distinct
|
|
187
|
+
* from `camera.aspect`, which is the aspect of one pixel. */
|
|
188
|
+
export function frameAspect(camera) {
|
|
189
|
+
const [width, height] = camera.resolution;
|
|
190
|
+
if (height === 0)
|
|
191
|
+
return 1;
|
|
192
|
+
return (width * camera.aspect) / height;
|
|
193
|
+
}
|
|
194
|
+
/** Column-major, GLSL convention, matching `MATRIX_TYPES` in Studio: element 0
|
|
195
|
+
* starts the first COLUMN. */
|
|
196
|
+
export function viewMatrix(camera) {
|
|
197
|
+
const { eye, forward, right, up } = cameraBasis(camera);
|
|
198
|
+
// The view matrix is the inverse of the camera's transform, and for a
|
|
199
|
+
// rotation that inverse is the transpose — so the basis vectors become rows.
|
|
200
|
+
const back = [-forward[0], -forward[1], -forward[2]];
|
|
201
|
+
return [
|
|
202
|
+
right[0], up[0], back[0], 0,
|
|
203
|
+
right[1], up[1], back[1], 0,
|
|
204
|
+
right[2], up[2], back[2], 0,
|
|
205
|
+
-(right[0] * eye[0] + right[1] * eye[1] + right[2] * eye[2]),
|
|
206
|
+
-(up[0] * eye[0] + up[1] * eye[1] + up[2] * eye[2]),
|
|
207
|
+
-(back[0] * eye[0] + back[1] * eye[1] + back[2] * eye[2]),
|
|
208
|
+
1,
|
|
209
|
+
];
|
|
210
|
+
}
|
|
211
|
+
/** Column-major, and orthographic when the camera says so — `orthowidth` is a
|
|
212
|
+
* width in world units, so the height comes from the frame aspect. */
|
|
213
|
+
export function projectionMatrix(camera) {
|
|
214
|
+
const { near, far } = camera;
|
|
215
|
+
const aspect = frameAspect(camera);
|
|
216
|
+
if (camera.projection === "orthographic") {
|
|
217
|
+
const halfWidth = camera.orthowidth / 2;
|
|
218
|
+
const halfHeight = aspect === 0 ? halfWidth : halfWidth / aspect;
|
|
219
|
+
return [
|
|
220
|
+
1 / halfWidth, 0, 0, 0,
|
|
221
|
+
0, 1 / halfHeight, 0, 0,
|
|
222
|
+
0, 0, -2 / (far - near), 0,
|
|
223
|
+
0, 0, -(far + near) / (far - near), 1,
|
|
224
|
+
];
|
|
225
|
+
}
|
|
226
|
+
const focalScale = (2 * camera.focal) / camera.aperture;
|
|
227
|
+
return [
|
|
228
|
+
focalScale, 0, 0, 0,
|
|
229
|
+
0, focalScale * aspect, 0, 0,
|
|
230
|
+
0, 0, -(far + near) / (far - near), -1,
|
|
231
|
+
0, 0, (-2 * far * near) / (far - near), 0,
|
|
232
|
+
];
|
|
233
|
+
}
|
|
234
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -16,9 +16,24 @@ const AMBIENT_STATE = new Set([
|
|
|
16
16
|
export function validateNodeModuleArchitecture(source, fileName, ts) {
|
|
17
17
|
const file = ts.createSourceFile(fileName, source, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
|
|
18
18
|
const diagnostics = [];
|
|
19
|
+
const exportsDefinition = file.statements.some((statement) => ts.isVariableStatement(statement) && isDefinition(statement));
|
|
19
20
|
for (const statement of file.statements) {
|
|
20
21
|
if (ts.isVariableStatement(statement) && !isDefinition(statement)) {
|
|
21
|
-
|
|
22
|
+
// `icon` and `runsOn` get their own sentence, because the general
|
|
23
|
+
// message sends you looking for the wrong thing. A dynamic node declares
|
|
24
|
+
// them at module level and must — they are read by a pass over the
|
|
25
|
+
// source. A definition-v1 node declares them *inside* the definition,
|
|
26
|
+
// and the moment it does, the module-level copies become exactly the
|
|
27
|
+
// module state this rule forbids.
|
|
28
|
+
//
|
|
29
|
+
// Found the hard way on 2026-09-08: the first literal stub written for a
|
|
30
|
+
// converted shared node failed here, and the message talks about pure
|
|
31
|
+
// functions and capabilities while the actual fix is to move two lines
|
|
32
|
+
// into the object above them. That is not guessable from the error.
|
|
33
|
+
const superseded = supersededByDefinition(statement);
|
|
34
|
+
add(statement, "module-state", superseded
|
|
35
|
+
? `\`${superseded}\` belongs inside the definition, not beside it — 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.`
|
|
36
|
+
: "Node modules may not declare module-level values; use a pure function or pass state through ports, props, and declared capabilities");
|
|
22
37
|
}
|
|
23
38
|
else if (ts.isClassDeclaration(statement) ||
|
|
24
39
|
ts.isEnumDeclaration(statement) ||
|
|
@@ -62,6 +77,26 @@ export function validateNodeModuleArchitecture(source, fileName, ts) {
|
|
|
62
77
|
(ts.isMethodDeclaration(parent) && parent.name === node) ||
|
|
63
78
|
(ts.isPropertyDeclaration(parent) && parent.name === node));
|
|
64
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* The name of a declaration a `definition` has taken over, or null.
|
|
82
|
+
*
|
|
83
|
+
* Only reported when the file actually exports a definition: without one,
|
|
84
|
+
* `export const icon` is the correct and only way to state it, and telling
|
|
85
|
+
* somebody to move it into an object that does not exist would be worse than
|
|
86
|
+
* the general message.
|
|
87
|
+
*/
|
|
88
|
+
function supersededByDefinition(statement) {
|
|
89
|
+
if (!exportsDefinition)
|
|
90
|
+
return null;
|
|
91
|
+
const declaration = statement.declarationList.declarations[0];
|
|
92
|
+
if (statement.declarationList.declarations.length !== 1)
|
|
93
|
+
return null;
|
|
94
|
+
if (!declaration || !ts.isIdentifier(declaration.name))
|
|
95
|
+
return null;
|
|
96
|
+
return declaration.name.text === "icon" || declaration.name.text === "runsOn"
|
|
97
|
+
? declaration.name.text
|
|
98
|
+
: null;
|
|
99
|
+
}
|
|
65
100
|
function isDefinition(statement) {
|
|
66
101
|
return Boolean(statement.declarationList.flags & ts.NodeFlags.Const) &&
|
|
67
102
|
Boolean(statement.modifiers?.some((item) => item.kind === ts.SyntaxKind.ExportKeyword)) &&
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { type Camera, type Color, type Geometry } from "@field/cascade/contracts";
|
|
2
|
+
/**
|
|
3
|
+
* Geometry to a raster, through the camera the render node was given.
|
|
4
|
+
*
|
|
5
|
+
* Step 2 of `PLAN viewport.md`, and the half that matters for correctness. The
|
|
6
|
+
* plan's central rule: **a scene view and a render node must agree, and the
|
|
7
|
+
* only way to guarantee that is both going through `packages/runtime/src/camera`.**
|
|
8
|
+
* If a viewport implements its own matrices, the two disagree about framing and
|
|
9
|
+
* every composition made in one is wrong in the other.
|
|
10
|
+
*
|
|
11
|
+
* So this file owns no projection maths at all. `viewMatrix` and
|
|
12
|
+
* `projectionMatrix` came from the camera module this morning, with Houdini's
|
|
13
|
+
* conventions already in them — down -Z, +Y up, `focal` and `aperture` rather
|
|
14
|
+
* than a field of view. All that happens here is the multiply and the raster.
|
|
15
|
+
*
|
|
16
|
+
* **Wireframe and points, deliberately, and not because shading is hard.** Every
|
|
17
|
+
* piece in this vault is lines: `cloud-volumes` is a volume, `particle-type` is
|
|
18
|
+
* trails, `cloud-plots` is marks. A shaded mode needs normals and a light and
|
|
19
|
+
* nothing here has a surface to put them on, so it would be a week spent on a
|
|
20
|
+
* mode with no caller.
|
|
21
|
+
*/
|
|
22
|
+
export interface RenderOptions {
|
|
23
|
+
readonly camera: Camera;
|
|
24
|
+
/** Pixels. Independent of the camera's own `resolution`, which describes the
|
|
25
|
+
* frame the projection assumes — a preview renders small and frames the
|
|
26
|
+
* same. */
|
|
27
|
+
readonly size: readonly [number, number];
|
|
28
|
+
readonly background?: Color;
|
|
29
|
+
/** Fallback stroke for a primitive with no `Cd`. */
|
|
30
|
+
readonly stroke: Color;
|
|
31
|
+
readonly strokeWidth: number;
|
|
32
|
+
readonly opacity: number;
|
|
33
|
+
/** Radius in pixels for a point with no primitive to belong to. */
|
|
34
|
+
readonly pointRadius: number;
|
|
35
|
+
/** Draw unprimitived points at all. A trail system has thousands and they
|
|
36
|
+
* are usually noise beside the strokes. */
|
|
37
|
+
readonly drawPoints: boolean;
|
|
38
|
+
/** How to make a canvas. The caller has a host; this file does not. */
|
|
39
|
+
readonly surface: SurfaceFactory;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The canvas surface this needs, declared rather than imported.
|
|
43
|
+
*
|
|
44
|
+
* `packages/runtime` compiles with `lib: ["ES2022"]` and no DOM, deliberately:
|
|
45
|
+
* it is the host-agnostic half of the system and a `dom` lib would let a DOM
|
|
46
|
+
* assumption into it by accident. So rather than widening that, this states the
|
|
47
|
+
* dozen members it actually uses. Both hosts satisfy it — the browser natively,
|
|
48
|
+
* the headless host through @napi-rs/canvas — and the narrow shape is also the
|
|
49
|
+
* documentation of what a future host would have to provide.
|
|
50
|
+
*/
|
|
51
|
+
export interface DrawingSurface {
|
|
52
|
+
readonly width: number;
|
|
53
|
+
readonly height: number;
|
|
54
|
+
getContext(kind: "2d"): DrawingContext | null;
|
|
55
|
+
/** How the raster leaves: both hosts return a PNG blob, and the asset
|
|
56
|
+
* capability takes bytes. */
|
|
57
|
+
convertToBlob(options?: {
|
|
58
|
+
type?: string;
|
|
59
|
+
}): Promise<{
|
|
60
|
+
arrayBuffer(): Promise<ArrayBuffer>;
|
|
61
|
+
}>;
|
|
62
|
+
}
|
|
63
|
+
export interface DrawingContext {
|
|
64
|
+
fillStyle: string;
|
|
65
|
+
strokeStyle: string;
|
|
66
|
+
lineWidth: number;
|
|
67
|
+
lineCap: string;
|
|
68
|
+
lineJoin: string;
|
|
69
|
+
globalAlpha: number;
|
|
70
|
+
fillRect(x: number, y: number, width: number, height: number): void;
|
|
71
|
+
beginPath(): void;
|
|
72
|
+
moveTo(x: number, y: number): void;
|
|
73
|
+
lineTo(x: number, y: number): void;
|
|
74
|
+
arc(x: number, y: number, radius: number, from: number, to: number): void;
|
|
75
|
+
stroke(): void;
|
|
76
|
+
fill(): void;
|
|
77
|
+
}
|
|
78
|
+
/**
|
|
79
|
+
* Where a surface comes from, injected rather than reached for.
|
|
80
|
+
*
|
|
81
|
+
* The first version called `new OffscreenCanvas(...)` directly and it was the
|
|
82
|
+
* wrong structure twice over. `packages/runtime` is the host-agnostic half of
|
|
83
|
+
* the system, so a global is exactly the assumption it should not make — and
|
|
84
|
+
* the test environment proved it immediately with *"OffscreenCanvas is not
|
|
85
|
+
* defined"*, because the global is installed by the CLI host and by the
|
|
86
|
+
* browser, not by node.
|
|
87
|
+
*
|
|
88
|
+
* So the host assumption lives in the node that has a host, and this file
|
|
89
|
+
* takes a factory. Which also makes the renderer testable, and a renderer that
|
|
90
|
+
* cannot be tested without a browser is the thing `PLAN viewport` argues
|
|
91
|
+
* against building.
|
|
92
|
+
*/
|
|
93
|
+
export type SurfaceFactory = (width: number, height: number) => DrawingSurface;
|
|
94
|
+
/**
|
|
95
|
+
* A scene onto one surface: the background once, then every geometry through
|
|
96
|
+
* the same camera.
|
|
97
|
+
*
|
|
98
|
+
* Separate from `renderGeometry` only in that it draws N of them. The surface,
|
|
99
|
+
* the background and the matrices are set up once, which is the reason this is
|
|
100
|
+
* a function rather than a loop at the call site — a loop there would produce a
|
|
101
|
+
* surface per geometry and composite them, and compositing straight alpha over
|
|
102
|
+
* straight alpha darkens every overlap.
|
|
103
|
+
*/
|
|
104
|
+
export declare function renderScene(geometry: readonly Geometry[], options: RenderOptions): DrawingSurface;
|
|
105
|
+
/** One geometry through a camera, on its own surface. */
|
|
106
|
+
export declare function renderGeometry(geometry: Geometry, options: RenderOptions): DrawingSurface;
|
|
107
|
+
/** The raster as PNG bytes, for the asset capability. */
|
|
108
|
+
export declare function renderToPng(surface: DrawingSurface): Promise<Uint8Array>;
|
|
109
|
+
//# sourceMappingURL=render.d.ts.map
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import { POSITION_ATTRIBUTE, } from "@field/cascade/contracts";
|
|
2
|
+
import { projectionMatrix, viewMatrix } from "../camera/index.js";
|
|
3
|
+
import { numericAttribute, readElement } from "./attributes.js";
|
|
4
|
+
/**
|
|
5
|
+
* A canvas the host already has.
|
|
6
|
+
*
|
|
7
|
+
* Both hosts supply `OffscreenCanvas` — the browser natively, the headless host
|
|
8
|
+
* through @napi-rs/canvas — which is why the node is `portable` with no
|
|
9
|
+
* capability for drawing and only needs `assets` to write the result.
|
|
10
|
+
*/
|
|
11
|
+
function css(colour) {
|
|
12
|
+
const [r, g, b, a = 1] = colour;
|
|
13
|
+
return `rgba(${Math.round(r * 255)}, ${Math.round(g * 255)}, ${Math.round(b * 255)}, ${a})`;
|
|
14
|
+
}
|
|
15
|
+
/** Column-major, GLSL convention, matching the camera module's own output. */
|
|
16
|
+
function multiply(matrix, x, y, z) {
|
|
17
|
+
return [0, 1, 2, 3].map((row) => matrix[row] * x + matrix[4 + row] * y + matrix[8 + row] * z + matrix[12 + row]);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* A scene onto one surface: the background once, then every geometry through
|
|
21
|
+
* the same camera.
|
|
22
|
+
*
|
|
23
|
+
* Separate from `renderGeometry` only in that it draws N of them. The surface,
|
|
24
|
+
* the background and the matrices are set up once, which is the reason this is
|
|
25
|
+
* a function rather than a loop at the call site — a loop there would produce a
|
|
26
|
+
* surface per geometry and composite them, and compositing straight alpha over
|
|
27
|
+
* straight alpha darkens every overlap.
|
|
28
|
+
*/
|
|
29
|
+
export function renderScene(geometry, options) {
|
|
30
|
+
const [width, height] = options.size;
|
|
31
|
+
const canvas = options.surface(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
32
|
+
const context = canvas.getContext("2d");
|
|
33
|
+
if (!context)
|
|
34
|
+
throw new Error("no 2D context on this surface");
|
|
35
|
+
if (options.background && options.background[3] > 0) {
|
|
36
|
+
context.fillStyle = css(options.background);
|
|
37
|
+
context.fillRect(0, 0, canvas.width, canvas.height);
|
|
38
|
+
}
|
|
39
|
+
for (const one of geometry)
|
|
40
|
+
drawGeometry(context, canvas, one, options);
|
|
41
|
+
return canvas;
|
|
42
|
+
}
|
|
43
|
+
/** One geometry through a camera, on its own surface. */
|
|
44
|
+
export function renderGeometry(geometry, options) {
|
|
45
|
+
return renderScene([geometry], options);
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Draw one geometry onto a context that is already sized and cleared.
|
|
49
|
+
*
|
|
50
|
+
* The matrices are rebuilt per geometry rather than hoisted, and that is
|
|
51
|
+
* deliberate: it costs two small matrix builds per geometry and it keeps this
|
|
52
|
+
* function a pure function of its arguments, so a caller cannot pass a view
|
|
53
|
+
* matrix that disagrees with the camera it also passed.
|
|
54
|
+
*/
|
|
55
|
+
function drawGeometry(context, canvas, geometry, options) {
|
|
56
|
+
const view = viewMatrix(options.camera);
|
|
57
|
+
const projection = projectionMatrix(options.camera);
|
|
58
|
+
const position = geometry.point[POSITION_ATTRIBUTE];
|
|
59
|
+
if (!position || position.storage === "string")
|
|
60
|
+
return;
|
|
61
|
+
const size = position.size;
|
|
62
|
+
const points = position.data;
|
|
63
|
+
const projected = [];
|
|
64
|
+
for (let index = 0; index < geometry.pointCount; index += 1) {
|
|
65
|
+
const world = multiply(view, Number(points[index * size]), Number(points[index * size + 1]), size >= 3 ? Number(points[index * size + 2]) : 0);
|
|
66
|
+
const clip = multiply(projection, world[0], world[1], world[2]);
|
|
67
|
+
// The perspective divide. `w` is kept so a segment with one vertex behind
|
|
68
|
+
// the camera can be dropped rather than drawn mirrored — which is what
|
|
69
|
+
// dividing by a negative w produces, and it looks like a plausible line.
|
|
70
|
+
const w = clip[3];
|
|
71
|
+
projected.push({
|
|
72
|
+
x: (clip[0] / w + 1) * 0.5 * canvas.width,
|
|
73
|
+
y: (1 - (clip[1] / w + 1) * 0.5) * canvas.height,
|
|
74
|
+
w,
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
// Validated to the sizes the style vocabulary declares, so a `Cd` with three
|
|
78
|
+
// components is refused rather than read as a colour with a missing channel.
|
|
79
|
+
// That rule is PLAN geometry's and SvgExport already enforces it; a renderer
|
|
80
|
+
// that was laxer would draw what the exporter refuses.
|
|
81
|
+
const colours = numericAttribute(geometry.primitive.Cd, "primitive.Cd", [4]);
|
|
82
|
+
const widths = numericAttribute(geometry.primitive.width, "primitive.width", [1]);
|
|
83
|
+
context.lineCap = "round";
|
|
84
|
+
context.lineJoin = "round";
|
|
85
|
+
context.globalAlpha = options.opacity;
|
|
86
|
+
const { vertexPoints, offsets, closed } = geometry.topology;
|
|
87
|
+
const drawn = new Set();
|
|
88
|
+
for (let primitive = 0; primitive < geometry.primitiveCount; primitive += 1) {
|
|
89
|
+
const from = offsets[primitive];
|
|
90
|
+
const to = offsets[primitive + 1];
|
|
91
|
+
if (to - from < 2)
|
|
92
|
+
continue;
|
|
93
|
+
context.strokeStyle = colours
|
|
94
|
+
? css(readElement(colours, primitive))
|
|
95
|
+
: css(options.stroke);
|
|
96
|
+
context.lineWidth = widths ? readElement(widths, primitive)[0] : options.strokeWidth;
|
|
97
|
+
context.beginPath();
|
|
98
|
+
let open = false;
|
|
99
|
+
for (let vertex = from; vertex < to; vertex += 1) {
|
|
100
|
+
const point = vertexPoints[vertex];
|
|
101
|
+
drawn.add(point);
|
|
102
|
+
const at = projected[point];
|
|
103
|
+
if (!at || at.w <= 0) {
|
|
104
|
+
// Behind the camera: break the run rather than joining across it. A
|
|
105
|
+
// line from an on-screen vertex to a mirrored one is the failure this
|
|
106
|
+
// avoids, and it draws as a plausible stroke to the wrong place.
|
|
107
|
+
open = false;
|
|
108
|
+
continue;
|
|
109
|
+
}
|
|
110
|
+
if (open)
|
|
111
|
+
context.lineTo(at.x, at.y);
|
|
112
|
+
else {
|
|
113
|
+
context.moveTo(at.x, at.y);
|
|
114
|
+
open = true;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (closed[primitive] === 1 && open) {
|
|
118
|
+
const first = projected[vertexPoints[from]];
|
|
119
|
+
if (first && first.w > 0)
|
|
120
|
+
context.lineTo(first.x, first.y);
|
|
121
|
+
}
|
|
122
|
+
context.stroke();
|
|
123
|
+
}
|
|
124
|
+
if (options.drawPoints && options.pointRadius > 0) {
|
|
125
|
+
context.fillStyle = css(options.stroke);
|
|
126
|
+
for (let index = 0; index < projected.length; index += 1) {
|
|
127
|
+
// Only points no primitive claimed. A trail system has thousands of
|
|
128
|
+
// vertices and drawing a dot on every one buries the strokes.
|
|
129
|
+
if (drawn.has(index))
|
|
130
|
+
continue;
|
|
131
|
+
const at = projected[index];
|
|
132
|
+
if (at.w <= 0)
|
|
133
|
+
continue;
|
|
134
|
+
context.beginPath();
|
|
135
|
+
context.arc(at.x, at.y, options.pointRadius, 0, Math.PI * 2);
|
|
136
|
+
context.fill();
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
/** The raster as PNG bytes, for the asset capability. */
|
|
141
|
+
export async function renderToPng(surface) {
|
|
142
|
+
const blob = await surface.convertToBlob({ type: "image/png" });
|
|
143
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
144
|
+
}
|
|
145
|
+
//# sourceMappingURL=render.js.map
|
|
@@ -53,6 +53,35 @@ export interface SvgExportOptions {
|
|
|
53
53
|
readonly height?: number;
|
|
54
54
|
/** Added around the derived bounds, in geometry units. */
|
|
55
55
|
readonly margin?: number;
|
|
56
|
+
/**
|
|
57
|
+
* An explicit world rectangle, `[minX, minY, maxX, maxY]`, replacing the
|
|
58
|
+
* derived bounds.
|
|
59
|
+
*
|
|
60
|
+
* **A frame sequence needs this and cannot work without it.** The bounds are
|
|
61
|
+
* derived per export, so in an animation every frame gets its own viewBox —
|
|
62
|
+
* and as the drawing grows the content appears to scale and drift, which
|
|
63
|
+
* reads as a camera move nobody asked for. Rendering 110 frames of a
|
|
64
|
+
* particle system is what surfaced it: the early frames are sparse, so their
|
|
65
|
+
* bounds are tight, so the first strokes fill the screen and then zoom out
|
|
66
|
+
* for the rest of the sequence.
|
|
67
|
+
*
|
|
68
|
+
* `margin` still applies, so a fixed rectangle can breathe the same way a
|
|
69
|
+
* derived one does.
|
|
70
|
+
*/
|
|
71
|
+
readonly bounds?: readonly [number, number, number, number];
|
|
72
|
+
/**
|
|
73
|
+
* A ground behind the drawing, or absent for a transparent document.
|
|
74
|
+
*
|
|
75
|
+
* Absent by default, because plotter work wants paper and a filled rectangle
|
|
76
|
+
* is a rectangle a pen would draw. It exists for the other case: a piece
|
|
77
|
+
* framed on screen has a ground, and every preview of one was otherwise
|
|
78
|
+
* hand-editing a `<rect>` into the file afterwards — which is the sort of
|
|
79
|
+
* step that says a prop is missing.
|
|
80
|
+
*
|
|
81
|
+
* Emitted at the viewBox rather than at the document size, so it fills the
|
|
82
|
+
* frame however the drawing is fitted inside it.
|
|
83
|
+
*/
|
|
84
|
+
readonly background?: Color;
|
|
56
85
|
/** Fallback stroke colour for a primitive with no `Cd`. */
|
|
57
86
|
readonly stroke?: Color;
|
|
58
87
|
/** Fallback stroke width for a primitive with no `width`. */
|
|
@@ -7,11 +7,14 @@ const BLACK = [0, 0, 0, 1];
|
|
|
7
7
|
export function geometryToSvg(geometry, options = {}) {
|
|
8
8
|
validateOptions(options);
|
|
9
9
|
const margin = options.margin ?? 0;
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
const
|
|
13
|
-
const
|
|
14
|
-
const
|
|
10
|
+
// An explicit rectangle wins over the geometry's own, because the caller
|
|
11
|
+
// stating a frame is a stronger signal than the content filling one.
|
|
12
|
+
const explicit = options.bounds;
|
|
13
|
+
const bounds = explicit ? undefined : geometryBounds(geometry);
|
|
14
|
+
const minX = explicit ? explicit[0] - margin : bounds === undefined ? 0 : bounds.min[0] - margin;
|
|
15
|
+
const minY = explicit ? explicit[1] - margin : bounds === undefined ? 0 : bounds.min[1] - margin;
|
|
16
|
+
const maxX = explicit ? explicit[2] + margin : bounds === undefined ? 1 : bounds.max[0] + margin;
|
|
17
|
+
const maxY = explicit ? explicit[3] + margin : bounds === undefined ? 1 : bounds.max[1] + margin;
|
|
15
18
|
const spanX = Math.max(maxX - minX, Number.EPSILON);
|
|
16
19
|
const spanY = Math.max(maxY - minY, Number.EPSILON);
|
|
17
20
|
const width = options.width && options.width > 0 ? options.width : spanX;
|
|
@@ -91,6 +94,9 @@ export function geometryToSvg(geometry, options = {}) {
|
|
|
91
94
|
}
|
|
92
95
|
const root = [
|
|
93
96
|
`<svg xmlns="http://www.w3.org/2000/svg" width="${number(width)}" height="${number(height)}" viewBox="${number(minX)} ${number(-maxY)} ${number(spanX)} ${number(spanY)}">`,
|
|
97
|
+
...(options.background
|
|
98
|
+
? [indent(`<rect x="${number(minX)}" y="${number(-maxY)}" width="${number(spanX)}" height="${number(spanY)}" fill="${Color.toHex(options.background)}" />`, 1)]
|
|
99
|
+
: []),
|
|
94
100
|
indent("<!-- Geometry is +Y up; SVG is +Y down. This is the one flip. -->", 1),
|
|
95
101
|
indent(`<g transform="scale(1,-1)" fill="${filled ? Color.toHex(fill) : "none"}"${filled && Color.alpha(fill) < 1
|
|
96
102
|
? ` fill-opacity="${number(Color.alpha(fill))}"`
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import type { GpuCapability } from "@field/cascade/contracts";
|
|
2
|
+
export interface GpuHostAcquisition {
|
|
3
|
+
readonly device: GpuCapability["device"];
|
|
4
|
+
readonly info?: Partial<GpuCapability["adapterInfo"]>;
|
|
5
|
+
/** May be a getter-backed `GPUSupportedLimits`; normalized by the host. */
|
|
6
|
+
readonly limits: unknown;
|
|
7
|
+
/**
|
|
8
|
+
* Release provider-owned references after the device is no longer usable.
|
|
9
|
+
* Native providers need this in addition to `GPUDevice.destroy()` so their
|
|
10
|
+
* bindings can stop background work and allow the process to exit.
|
|
11
|
+
*/
|
|
12
|
+
readonly release?: () => void | Promise<void>;
|
|
13
|
+
}
|
|
14
|
+
export interface GpuHostOptions {
|
|
15
|
+
readonly acquire: () => Promise<GpuHostAcquisition>;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* A host-owned GPU device with page/process-wide and per-node cache scopes.
|
|
19
|
+
*
|
|
20
|
+
* The runtime can await `ensure()` before a cook while node implementations
|
|
21
|
+
* continue to receive the synchronous `GpuCapability` contract. Ownership
|
|
22
|
+
* remains with the injecting host: runtimes and graphs never dispose it.
|
|
23
|
+
*/
|
|
24
|
+
export interface GpuHost extends GpuCapability {
|
|
25
|
+
ensure(): Promise<void>;
|
|
26
|
+
forNode(nodeId: string): GpuCapability;
|
|
27
|
+
dispose(): Promise<void>;
|
|
28
|
+
}
|
|
29
|
+
export declare function isGpuHost(value: unknown): value is GpuHost;
|
|
30
|
+
export declare function createGpuHost(options: GpuHostOptions): GpuHost;
|
|
31
|
+
/** Getter-backed supported-limits objects cannot be normalized with spread. */
|
|
32
|
+
export declare function readGpuLimits(source: unknown): Record<string, number>;
|
|
33
|
+
//# sourceMappingURL=gpu.d.ts.map
|