@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.
Files changed (74) hide show
  1. package/AGENTS.md +14 -9
  2. package/ARCHITECTURE.md +22 -6
  3. package/CHANGELOG.md +51 -1
  4. package/CONTRIBUTING.md +71 -0
  5. package/DESIGN.md +19 -13
  6. package/README.md +42 -11
  7. package/dist/assets/{CodeEditor-D22fVi-4.js → CodeEditor-BNOJIDzL.js} +4 -4
  8. package/dist/assets/{CodePanel-DPNEYfcR.js → CodePanel-2wp2G0Fj.js} +2 -2
  9. package/dist/assets/{CustomNodeDialog-DeHCAvgb.js → CustomNodeDialog-DNqaVBqA.js} +1 -1
  10. package/dist/assets/{ProjectSettings-DWpVtWEx.js → ProjectSettings-DcdYF2E1.js} +1 -1
  11. package/dist/assets/{SettingsDialog-za00IS3L.js → SettingsDialog-D2xVvyiR.js} +1 -1
  12. package/dist/assets/{VersionHistoryDialog-DTMGhI0_.js → VersionHistoryDialog-DW288ykF.js} +1 -1
  13. package/dist/assets/{core-JsyKMO1k.js → core-B1y38i95.js} +1 -1
  14. package/dist/assets/{cssMode-emdRheFK.js → cssMode-BLucnaln.js} +1 -1
  15. package/dist/assets/{editor.api-xHX-fMSo.js → editor.api-B3Et3Pph.js} +1 -1
  16. package/dist/assets/{freemarker2-C3M8_Gs3.js → freemarker2-DVlPoano.js} +1 -1
  17. package/dist/assets/geo-DhyFdBXX.js +1 -0
  18. package/dist/assets/{handlebars-CBMurtFk.js → handlebars-B5FZBAMR.js} +1 -1
  19. package/dist/assets/{html-xbLQwknw.js → html-FodD8KbR.js} +1 -1
  20. package/dist/assets/{htmlMode-2wKdDTon.js → htmlMode-CsG57VCD.js} +1 -1
  21. package/dist/assets/{image-i1j6nWKe.js → image-B3F-nq25.js} +1 -1
  22. package/dist/assets/index-CC6YQw2n.js +212 -0
  23. package/dist/assets/index-Y2ock1cw.css +1 -0
  24. package/dist/assets/{javascript-DJaOo_2I.js → javascript-B3gonF_H.js} +1 -1
  25. package/dist/assets/{jsonMode-Bheas8_0.js → jsonMode-Y3w1ahZg.js} +1 -1
  26. package/dist/assets/{liquid-BCj_BGbM.js → liquid-CTpWXIYO.js} +1 -1
  27. package/dist/assets/{lspLanguageFeatures-BfIsx_xz.js → lspLanguageFeatures-CODBdKEZ.js} +1 -1
  28. package/dist/assets/{mdx-DwiQDiVO.js → mdx-CDAQJZIz.js} +1 -1
  29. package/dist/assets/{python-BjrB5nqj.js → python-LOIlxJU-.js} +1 -1
  30. package/dist/assets/{razor-CfoBmGDs.js → razor-CllPQSHo.js} +1 -1
  31. package/dist/assets/{tsMode-BcEoMSxp.js → tsMode-CO28ut8t.js} +1 -1
  32. package/dist/assets/{typescript-DIg7TEmQ.js → typescript-ruU9wKQ_.js} +1 -1
  33. package/dist/assets/{workers-vXYuwtPw.js → workers-Bc1Ywp4-.js} +1 -1
  34. package/dist/assets/{xml-DexLQIQy.js → xml-DoXP2Dh1.js} +1 -1
  35. package/dist/assets/{yaml-DUnD_1dE.js → yaml-D9YPyGiJ.js} +1 -1
  36. package/dist/cli/index.js +999 -185
  37. package/dist/cli/runtime/gpu.ts +71 -0
  38. package/dist/contracts/camera.d.ts +70 -0
  39. package/dist/contracts/camera.js +27 -0
  40. package/dist/contracts/capabilities.d.ts +6 -6
  41. package/dist/contracts/definition.d.ts +41 -2
  42. package/dist/contracts/index.d.ts +2 -0
  43. package/dist/contracts/index.js +2 -0
  44. package/dist/contracts/schema.js +25 -2
  45. package/dist/contracts/series.d.ts +75 -0
  46. package/dist/contracts/series.js +2 -0
  47. package/dist/contracts/validate.js +19 -3
  48. package/dist/contracts/values.d.ts +3 -2
  49. package/dist/contracts/values.js +1 -0
  50. package/dist/gpu.d.ts +17 -0
  51. package/dist/gpu.js +61 -1
  52. package/dist/index.html +2 -2
  53. package/dist/runtime/builtins/core/camera.d.ts +252 -0
  54. package/dist/runtime/builtins/core/camera.js +149 -0
  55. package/dist/runtime/builtins/core/index.d.ts +2 -1
  56. package/dist/runtime/builtins/core/index.js +3 -1
  57. package/dist/runtime/camera/index.d.ts +60 -0
  58. package/dist/runtime/camera/index.js +234 -0
  59. package/dist/runtime/definition/architecture.js +36 -1
  60. package/dist/runtime/gpu.d.ts +33 -0
  61. package/dist/runtime/gpu.js +218 -0
  62. package/dist/runtime/index.d.ts +3 -0
  63. package/dist/runtime/index.js +3 -0
  64. package/dist/runtime/runtime.js +63 -6
  65. package/dist/runtime/series/index.d.ts +41 -0
  66. package/dist/runtime/series/index.js +167 -0
  67. package/dist/runtime/types.d.ts +1 -1
  68. package/doc/HEADLESS_GPU.md +160 -0
  69. package/doc/NODE_REFERENCE.md +36 -0
  70. package/doc/PROJECT_AUTHORING.md +106 -20
  71. package/package.json +10 -2
  72. package/dist/assets/geo-BAR6lQp5.js +0 -1
  73. package/dist/assets/index-BDV5xGBZ.css +0 -1
  74. package/dist/assets/index-CMcH8AgP.js +0 -212
@@ -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
- add(statement, "module-state", "Node modules may not declare module-level values; use a pure function or pass state through ports, props, and declared capabilities");
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,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
@@ -0,0 +1,218 @@
1
+ const HOST_SCOPE = "\u0000host";
2
+ const MISSING_DEVICE = "The gpu capability was used before its device was ready — the host must await ensure() before a cook.";
3
+ const DISPOSED = "The gpu capability has been disposed.";
4
+ export function isGpuHost(value) {
5
+ return (!!value &&
6
+ typeof value.ensure === "function" &&
7
+ typeof value.forNode === "function" &&
8
+ typeof value.dispose === "function");
9
+ }
10
+ export function createGpuHost(options) {
11
+ let active = null;
12
+ let starting = null;
13
+ let disposal = null;
14
+ let disposed = false;
15
+ const caches = new Map();
16
+ const retiring = new Set();
17
+ const retirementErrors = [];
18
+ function dropCaches() {
19
+ const errors = [];
20
+ const scopes = [...caches.values()];
21
+ caches.clear();
22
+ for (const entries of scopes) {
23
+ for (const entry of entries.values()) {
24
+ try {
25
+ entry.destroy?.(entry.value);
26
+ }
27
+ catch (error) {
28
+ errors.push(error);
29
+ }
30
+ }
31
+ }
32
+ return errors;
33
+ }
34
+ async function release(acquired, destroyDevice) {
35
+ if (acquired.released)
36
+ return [];
37
+ acquired.released = true;
38
+ const errors = [];
39
+ if (destroyDevice) {
40
+ try {
41
+ acquired.device.destroy();
42
+ }
43
+ catch (error) {
44
+ errors.push(error);
45
+ }
46
+ }
47
+ try {
48
+ await acquired.release?.();
49
+ }
50
+ catch (error) {
51
+ errors.push(error);
52
+ }
53
+ return errors;
54
+ }
55
+ function throwCleanupErrors(errors) {
56
+ if (errors.length === 1)
57
+ throw errors[0];
58
+ if (errors.length > 1) {
59
+ const combined = new Error("GPU host cleanup failed");
60
+ Object.assign(combined, { errors });
61
+ throw combined;
62
+ }
63
+ }
64
+ function watchLoss(acquired) {
65
+ void acquired.device.lost.then(() => {
66
+ if (disposed || active !== acquired)
67
+ return;
68
+ active = null;
69
+ retirementErrors.push(...dropCaches());
70
+ const cleanup = release(acquired, false).then((errors) => {
71
+ retirementErrors.push(...errors);
72
+ });
73
+ retiring.add(cleanup);
74
+ void cleanup.finally(() => retiring.delete(cleanup));
75
+ }).catch(() => {
76
+ // A rejected loss notification cannot be acted on safely. The active
77
+ // device remains usable until WebGPU reports otherwise or the owner
78
+ // disposes the host.
79
+ });
80
+ }
81
+ async function start() {
82
+ const result = await options.acquire();
83
+ const acquired = {
84
+ device: result.device,
85
+ info: {
86
+ vendor: result.info?.vendor ?? "",
87
+ architecture: result.info?.architecture ?? "",
88
+ device: result.info?.device ?? "",
89
+ description: result.info?.description ?? "",
90
+ },
91
+ limits: readGpuLimits(result.limits),
92
+ release: result.release,
93
+ released: false,
94
+ };
95
+ if (disposed) {
96
+ const errors = await release(acquired, true);
97
+ throwCleanupErrors(errors);
98
+ throw new Error(DISPOSED);
99
+ }
100
+ active = acquired;
101
+ watchLoss(acquired);
102
+ }
103
+ const host = {
104
+ get device() {
105
+ if (!active)
106
+ throw new Error(MISSING_DEVICE);
107
+ return active.device;
108
+ },
109
+ get adapterInfo() {
110
+ if (!active)
111
+ throw new Error(MISSING_DEVICE);
112
+ return active.info;
113
+ },
114
+ get limits() {
115
+ return active?.limits ?? {};
116
+ },
117
+ cache(key, create, destroy) {
118
+ return host.forNode(HOST_SCOPE).cache(key, create, destroy);
119
+ },
120
+ async ensure() {
121
+ if (disposed)
122
+ throw new Error(DISPOSED);
123
+ if (active)
124
+ return;
125
+ if (!starting) {
126
+ const attempt = start();
127
+ const tracked = attempt.finally(() => {
128
+ if (starting === tracked)
129
+ starting = null;
130
+ });
131
+ starting = tracked;
132
+ }
133
+ await starting;
134
+ },
135
+ forNode(nodeId) {
136
+ return {
137
+ get device() {
138
+ if (!active)
139
+ throw new Error(MISSING_DEVICE);
140
+ return active.device;
141
+ },
142
+ get adapterInfo() {
143
+ if (!active)
144
+ throw new Error(MISSING_DEVICE);
145
+ return active.info;
146
+ },
147
+ get limits() {
148
+ return active?.limits ?? {};
149
+ },
150
+ cache(key, create, destroy) {
151
+ if (!active)
152
+ throw new Error(MISSING_DEVICE);
153
+ let entries = caches.get(nodeId);
154
+ if (!entries) {
155
+ entries = new Map();
156
+ caches.set(nodeId, entries);
157
+ }
158
+ const existing = entries.get(key);
159
+ if (existing)
160
+ return existing.value;
161
+ const value = create(active.device);
162
+ entries.set(key, {
163
+ value,
164
+ destroy: destroy,
165
+ });
166
+ return value;
167
+ },
168
+ };
169
+ },
170
+ dispose() {
171
+ disposal ??= performDispose();
172
+ return disposal;
173
+ },
174
+ };
175
+ async function performDispose() {
176
+ disposed = true;
177
+ const errors = dropCaches();
178
+ const pending = starting;
179
+ if (pending) {
180
+ try {
181
+ await pending;
182
+ }
183
+ catch (error) {
184
+ if (!(error instanceof Error && error.message === DISPOSED))
185
+ errors.push(error);
186
+ }
187
+ }
188
+ const acquired = active;
189
+ active = null;
190
+ if (acquired)
191
+ errors.push(...(await release(acquired, true)));
192
+ await Promise.all([...retiring]);
193
+ errors.push(...retirementErrors);
194
+ retirementErrors.length = 0;
195
+ throwCleanupErrors(errors);
196
+ }
197
+ return host;
198
+ }
199
+ /** Getter-backed supported-limits objects cannot be normalized with spread. */
200
+ export function readGpuLimits(source) {
201
+ if (!source || typeof source !== "object")
202
+ return {};
203
+ const names = new Set(Object.keys(source));
204
+ for (let cursor = Object.getPrototypeOf(source); cursor && cursor !== Object.prototype; cursor = Object.getPrototypeOf(cursor)) {
205
+ for (const key of Object.getOwnPropertyNames(cursor)) {
206
+ if (key !== "constructor")
207
+ names.add(key);
208
+ }
209
+ }
210
+ const limits = {};
211
+ for (const key of names) {
212
+ const value = source[key];
213
+ if (typeof value === "number")
214
+ limits[key] = value;
215
+ }
216
+ return limits;
217
+ }
218
+ //# sourceMappingURL=gpu.js.map
@@ -1,7 +1,10 @@
1
1
  export { CascadeRuntimeError } from "./error.js";
2
2
  export * from "./animation/index.js";
3
+ export * from "./camera/index.js";
4
+ export * from "./series/index.js";
3
5
  export * from "./expressions/index.js";
4
6
  export * from "./geometry/index.js";
7
+ export * from "./gpu.js";
5
8
  export * from "./params/index.js";
6
9
  export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
7
10
  export type * from "./types.js";
@@ -1,7 +1,10 @@
1
1
  export { CascadeRuntimeError } from "./error.js";
2
2
  export * from "./animation/index.js";
3
+ export * from "./camera/index.js";
4
+ export * from "./series/index.js";
3
5
  export * from "./expressions/index.js";
4
6
  export * from "./geometry/index.js";
7
+ export * from "./gpu.js";
5
8
  export * from "./params/index.js";
6
9
  export { createRuntime, PREFLIGHT_WARNING_CODES } from "./runtime.js";
7
10
  //# sourceMappingURL=index.js.map