@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,129 @@
|
|
|
1
|
+
import type { Camera } from "./camera.js";
|
|
2
|
+
import type { Geometry } from "./geometry.js";
|
|
3
|
+
import type { Vec3, Vec4 } from "./values.js";
|
|
4
|
+
/**
|
|
5
|
+
* A scene: geometry, an optional camera, and lights.
|
|
6
|
+
*
|
|
7
|
+
* Marcus asked for this on 2026-09-09, and it arrived as a correction to a
|
|
8
|
+
* worse design. The plan before it proposed a separate `scene` panel beside the
|
|
9
|
+
* Viewer, and then — once that was corrected — a Viewer 3D mode that special
|
|
10
|
+
* cased `geometry`. His version:
|
|
11
|
+
*
|
|
12
|
+
* > *"If we want a 'scene' we could just create a merge node with a 3D viewer,
|
|
13
|
+
* > or perhaps a 'scene' root node with a variadic input? … a 'scene' node with
|
|
14
|
+
* > a camera, lights and geometry attached becomes a '3d scene'."*
|
|
15
|
+
*
|
|
16
|
+
* What that buys is not a feature, it is the removal of a branch. **The Viewer's
|
|
17
|
+
* 3D mode renders exactly one type.** A bare `geometry` output is a scene
|
|
18
|
+
* carrying a default camera and a default light, which is what Houdini itself
|
|
19
|
+
* does — the viewport camera and the headlight exist until you make real ones —
|
|
20
|
+
* so "geometry displays in 3D" is a promotion (`sceneFromGeometry`) rather than
|
|
21
|
+
* a second code path. Every branch that special-cases geometry against scene is
|
|
22
|
+
* a place the two can disagree about framing.
|
|
23
|
+
*
|
|
24
|
+
* The same promotion is why `cascade.geo.Render` can take a scene without
|
|
25
|
+
* breaking a single existing sketch: `geometry` widens to `scene`, so the old
|
|
26
|
+
* wire is still legal and lands on `asScene`.
|
|
27
|
+
*
|
|
28
|
+
* ## Why this is not `cascade.geo.Merge`
|
|
29
|
+
*
|
|
30
|
+
* Merge concatenates points, vertices and primitives and renumbers them, and a
|
|
31
|
+
* camera is not a primitive — `mergeGeometries`' collision rules have nothing
|
|
32
|
+
* to say about one. Two operations that both take N inputs are not the same
|
|
33
|
+
* operation.
|
|
34
|
+
*
|
|
35
|
+
* ## Why the camera is nullable rather than defaulted here
|
|
36
|
+
*
|
|
37
|
+
* A scene with no camera means *"use the viewport's own navigated camera"*, and
|
|
38
|
+
* that is a different statement from *"use a 50 mm camera at the origin"*. It
|
|
39
|
+
* is also Houdini's behaviour, which Marcus confirmed on 2026-09-09 — *"yes to
|
|
40
|
+
* copying the Houdini camera"*: unwired, you fly around freely; wire a
|
|
41
|
+
* `cascade.core.Camera` and the view locks to it and navigation writes back
|
|
42
|
+
* into its parameters. A defaulted camera here would make that distinction
|
|
43
|
+
* unrepresentable, and the viewport would have no way to know whether the
|
|
44
|
+
* camera it was handed was a decision or a fallback.
|
|
45
|
+
*
|
|
46
|
+
* `CAMERA_DEFAULTS` is still where the fallback numbers live; a consumer that
|
|
47
|
+
* genuinely needs a camera object asks for one there, and the null stays
|
|
48
|
+
* meaningful.
|
|
49
|
+
*/
|
|
50
|
+
export interface Scene {
|
|
51
|
+
/**
|
|
52
|
+
* Every geometry in the scene, in input order.
|
|
53
|
+
*
|
|
54
|
+
* A list rather than one merged geometry, because merging is lossy in the way
|
|
55
|
+
* that matters to a viewport: two geometries with different point counts and
|
|
56
|
+
* different style attributes become one after `mergeGeometries`, and the
|
|
57
|
+
* viewport can then no longer draw them with separate styles, report which
|
|
58
|
+
* one is over the item cap, or let one be soloed. Merge is available as a
|
|
59
|
+
* node when someone wants it.
|
|
60
|
+
*/
|
|
61
|
+
readonly geometry: readonly Geometry[];
|
|
62
|
+
/** `null` means the viewport's own navigated camera. See above. */
|
|
63
|
+
readonly camera: Camera | null;
|
|
64
|
+
readonly lights: readonly Light[];
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* A light, following Houdini's `/obj/hlight` where it has an equivalent.
|
|
68
|
+
*
|
|
69
|
+
* Houdini's parameters are `light_type`, `light_color` and `light_intensity`;
|
|
70
|
+
* the names here drop the redundant prefix a Cascade node does not need, since
|
|
71
|
+
* the type already says it is a light. `type` keeps Houdini's own vocabulary,
|
|
72
|
+
* including **`distant`** for what most renderers call a directional light.
|
|
73
|
+
*
|
|
74
|
+
* Nothing consumes lights yet — every piece in the vault is lines, and there is
|
|
75
|
+
* no surface to shade. They are in the type from the start anyway, because
|
|
76
|
+
* adding a field to `Scene` later means every serialized scene and every node
|
|
77
|
+
* that constructs one changes, and the whole point of landing the type first is
|
|
78
|
+
* that it stops changing.
|
|
79
|
+
*/
|
|
80
|
+
export interface Light {
|
|
81
|
+
/** Houdini `light_type`. `ambient` ignores `translate` and `direction`. */
|
|
82
|
+
readonly type: "point" | "distant" | "ambient" | "spot";
|
|
83
|
+
/** World position. Ignored for `distant` and `ambient`. */
|
|
84
|
+
readonly translate: Vec3;
|
|
85
|
+
/** Direction the light points. Used by `distant` and `spot`. */
|
|
86
|
+
readonly direction: Vec3;
|
|
87
|
+
/** Houdini `light_color`, as a `vec4` — colour is four components, always. */
|
|
88
|
+
readonly color: Vec4;
|
|
89
|
+
/** Houdini `light_intensity`. */
|
|
90
|
+
readonly intensity: number;
|
|
91
|
+
/** Cone angle in degrees, `spot` only. Houdini `coneangle`. */
|
|
92
|
+
readonly coneangle: number;
|
|
93
|
+
}
|
|
94
|
+
export declare const LIGHT_DEFAULTS: Light;
|
|
95
|
+
/** A scene with nothing in it. Not a failure — an empty graph produces one. */
|
|
96
|
+
export declare const EMPTY_SCENE: Scene;
|
|
97
|
+
export declare function isLight(value: unknown): value is Light;
|
|
98
|
+
/**
|
|
99
|
+
* Structural, and deliberately not a brand.
|
|
100
|
+
*
|
|
101
|
+
* A scene crosses the host boundary as JSON — Studio panel to worker, project
|
|
102
|
+
* module to runtime — and a branded type would not survive the round trip, so
|
|
103
|
+
* the check has to be answerable from the shape alone. It tests `geometry` and
|
|
104
|
+
* `lights` are arrays and that `camera` is present-and-nullable rather than
|
|
105
|
+
* merely absent, which is what distinguishes a scene from an arbitrary object
|
|
106
|
+
* that happens to carry a `geometry` field.
|
|
107
|
+
*/
|
|
108
|
+
export declare function isScene(value: unknown): value is Scene;
|
|
109
|
+
/** The promotion: a geometry is a scene with the viewport's camera and no lights. */
|
|
110
|
+
export declare function sceneFromGeometry(geometry: Geometry): Scene;
|
|
111
|
+
/**
|
|
112
|
+
* Read a `scene` input, which may have received a `geometry`.
|
|
113
|
+
*
|
|
114
|
+
* `geometry` widens to `scene` in `IMPLICIT_TYPE_CONVERSIONS`, so a node
|
|
115
|
+
* declaring a `scene` input can be handed either. **Every consumer of a scene
|
|
116
|
+
* input must go through this**, and that is the whole contract: the alternative
|
|
117
|
+
* is each consumer testing `isScene` and getting the fallback subtly different,
|
|
118
|
+
* which is how a viewport and a render node end up framing the same graph two
|
|
119
|
+
* ways.
|
|
120
|
+
*
|
|
121
|
+
* Returns `EMPTY_SCENE` for a missing input rather than throwing, because an
|
|
122
|
+
* unwired viewport is a normal state and not an error — and for anything that
|
|
123
|
+
* is neither a scene nor a geometry, because a viewport that throws on a
|
|
124
|
+
* surprising value is a viewport that goes black while the graph is fine. The
|
|
125
|
+
* connection rules are what stop a wrong type arriving; this is the floor under
|
|
126
|
+
* them, not the check.
|
|
127
|
+
*/
|
|
128
|
+
export declare function asScene(value: unknown): Scene;
|
|
129
|
+
//# sourceMappingURL=scene.d.ts.map
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
export const LIGHT_DEFAULTS = {
|
|
2
|
+
type: "point",
|
|
3
|
+
translate: [0, 0, 0],
|
|
4
|
+
direction: [0, 0, -1],
|
|
5
|
+
color: [1, 1, 1, 1],
|
|
6
|
+
intensity: 1,
|
|
7
|
+
coneangle: 45,
|
|
8
|
+
};
|
|
9
|
+
/** A scene with nothing in it. Not a failure — an empty graph produces one. */
|
|
10
|
+
export const EMPTY_SCENE = { geometry: [], camera: null, lights: [] };
|
|
11
|
+
export function isLight(value) {
|
|
12
|
+
if (typeof value !== "object" || value === null)
|
|
13
|
+
return false;
|
|
14
|
+
const light = value;
|
|
15
|
+
return ((light.type === "point" ||
|
|
16
|
+
light.type === "distant" ||
|
|
17
|
+
light.type === "ambient" ||
|
|
18
|
+
light.type === "spot") &&
|
|
19
|
+
Array.isArray(light.translate) &&
|
|
20
|
+
Array.isArray(light.direction) &&
|
|
21
|
+
Array.isArray(light.color) &&
|
|
22
|
+
typeof light.intensity === "number");
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* Structural, and deliberately not a brand.
|
|
26
|
+
*
|
|
27
|
+
* A scene crosses the host boundary as JSON — Studio panel to worker, project
|
|
28
|
+
* module to runtime — and a branded type would not survive the round trip, so
|
|
29
|
+
* the check has to be answerable from the shape alone. It tests `geometry` and
|
|
30
|
+
* `lights` are arrays and that `camera` is present-and-nullable rather than
|
|
31
|
+
* merely absent, which is what distinguishes a scene from an arbitrary object
|
|
32
|
+
* that happens to carry a `geometry` field.
|
|
33
|
+
*/
|
|
34
|
+
export function isScene(value) {
|
|
35
|
+
if (typeof value !== "object" || value === null)
|
|
36
|
+
return false;
|
|
37
|
+
const scene = value;
|
|
38
|
+
return (Array.isArray(scene.geometry) &&
|
|
39
|
+
Array.isArray(scene.lights) &&
|
|
40
|
+
"camera" in scene &&
|
|
41
|
+
(scene.camera === null || typeof scene.camera === "object"));
|
|
42
|
+
}
|
|
43
|
+
/** The promotion: a geometry is a scene with the viewport's camera and no lights. */
|
|
44
|
+
export function sceneFromGeometry(geometry) {
|
|
45
|
+
return { geometry: [geometry], camera: null, lights: [] };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* Read a `scene` input, which may have received a `geometry`.
|
|
49
|
+
*
|
|
50
|
+
* `geometry` widens to `scene` in `IMPLICIT_TYPE_CONVERSIONS`, so a node
|
|
51
|
+
* declaring a `scene` input can be handed either. **Every consumer of a scene
|
|
52
|
+
* input must go through this**, and that is the whole contract: the alternative
|
|
53
|
+
* is each consumer testing `isScene` and getting the fallback subtly different,
|
|
54
|
+
* which is how a viewport and a render node end up framing the same graph two
|
|
55
|
+
* ways.
|
|
56
|
+
*
|
|
57
|
+
* Returns `EMPTY_SCENE` for a missing input rather than throwing, because an
|
|
58
|
+
* unwired viewport is a normal state and not an error — and for anything that
|
|
59
|
+
* is neither a scene nor a geometry, because a viewport that throws on a
|
|
60
|
+
* surprising value is a viewport that goes black while the graph is fine. The
|
|
61
|
+
* connection rules are what stop a wrong type arriving; this is the floor under
|
|
62
|
+
* them, not the check.
|
|
63
|
+
*/
|
|
64
|
+
export function asScene(value) {
|
|
65
|
+
if (value === undefined || value === null)
|
|
66
|
+
return EMPTY_SCENE;
|
|
67
|
+
if (isScene(value))
|
|
68
|
+
return value;
|
|
69
|
+
if (typeof value === "object" && value.kind === "geometry")
|
|
70
|
+
return sceneFromGeometry(value);
|
|
71
|
+
return EMPTY_SCENE;
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=scene.js.map
|
package/dist/contracts/schema.js
CHANGED
|
@@ -36,7 +36,27 @@ const dataProperties = {
|
|
|
36
36
|
step: { type: "number", exclusiveMinimum: 0 },
|
|
37
37
|
accept: { type: "array", items: { type: "string", minLength: 1 } },
|
|
38
38
|
control,
|
|
39
|
-
|
|
39
|
+
// A bare value, or `{ value, label, disabled? }`. Both forms, because a
|
|
40
|
+
// bare value means "the value is its own label" and is what every
|
|
41
|
+
// definition written before the labelled form meant.
|
|
42
|
+
options: {
|
|
43
|
+
type: "array",
|
|
44
|
+
items: {
|
|
45
|
+
anyOf: [
|
|
46
|
+
{ not: { type: "object" } },
|
|
47
|
+
{
|
|
48
|
+
type: "object",
|
|
49
|
+
properties: {
|
|
50
|
+
value: {},
|
|
51
|
+
label: { type: "string", minLength: 1 },
|
|
52
|
+
disabled: { type: "boolean" },
|
|
53
|
+
},
|
|
54
|
+
required: ["value", "label"],
|
|
55
|
+
additionalProperties: false,
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
},
|
|
40
60
|
};
|
|
41
61
|
const data = object(dataProperties, ["kind", "type"]);
|
|
42
62
|
const output = object({ kind: dataProperties.kind, type: dataProperties.type, description }, ["kind", "type"]);
|
|
@@ -47,6 +67,8 @@ const prop = object({
|
|
|
47
67
|
// Props only, not inputs: an input's value comes from whatever is wired to
|
|
48
68
|
// it, so a default expression there would be describing the wrong thing.
|
|
49
69
|
expression: { type: "string", minLength: 1 },
|
|
70
|
+
// A button that fires a named host action instead of an editable field.
|
|
71
|
+
action: { type: "string", minLength: 1 },
|
|
50
72
|
}, ["type", "default"]);
|
|
51
73
|
function environmentCapabilities(runsOn, capabilities) {
|
|
52
74
|
return {
|
|
@@ -60,7 +82,7 @@ export const nodeDefinitionSchema = {
|
|
|
60
82
|
...object({
|
|
61
83
|
apiVersion: { const: 1 },
|
|
62
84
|
runsOn: { enum: ["portable", "browser", "server"] },
|
|
63
|
-
container: {
|
|
85
|
+
container: { enum: ["subnet", "feedback"] },
|
|
64
86
|
capabilities: {
|
|
65
87
|
type: "array",
|
|
66
88
|
items: { type: "string" },
|
|
@@ -74,7 +96,7 @@ export const nodeDefinitionSchema = {
|
|
|
74
96
|
props: record(prop),
|
|
75
97
|
}, ["apiVersion", "runsOn"]),
|
|
76
98
|
allOf: [
|
|
77
|
-
environmentCapabilities("portable", ["assets", "media"]),
|
|
99
|
+
environmentCapabilities("portable", ["assets", "media", "gpu"]),
|
|
78
100
|
environmentCapabilities("browser", ["assets", "media", "gpu"]),
|
|
79
101
|
environmentCapabilities("server", [
|
|
80
102
|
"files",
|
|
@@ -82,6 +104,7 @@ export const nodeDefinitionSchema = {
|
|
|
82
104
|
"media",
|
|
83
105
|
"python",
|
|
84
106
|
"shell",
|
|
107
|
+
"gpu",
|
|
85
108
|
]),
|
|
86
109
|
],
|
|
87
110
|
};
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { CascadePreset } from "./document.js";
|
|
2
|
+
/**
|
|
3
|
+
* A series: one system, many instances.
|
|
4
|
+
*
|
|
5
|
+
* **A series is a graph plus an ordered list of parameter sets, and nothing
|
|
6
|
+
* else.** Plan: `Work/Projects/Cascade/plans/PLAN series.md`. The definition is
|
|
7
|
+
* deliberately this thin, because the alternative is the trap the plan names —
|
|
8
|
+
* *the series must not become a second document format.* If it grows its own
|
|
9
|
+
* copy of node state it forks from the graph and the two disagree.
|
|
10
|
+
*
|
|
11
|
+
* So there is no boundary in it. Houdini marks a region because the varying
|
|
12
|
+
* thing flows through the region as geometry; here the varying thing is a
|
|
13
|
+
* sparse map of parameter overrides keyed by node id, so the affected subgraph
|
|
14
|
+
* is **derived** from those overrides by the dependency graph that already
|
|
15
|
+
* exists. Nothing is marked, so nothing can be marked wrongly. That is also
|
|
16
|
+
* why `Block Begin`/`Block End` are not copied: they exist to serve feedback,
|
|
17
|
+
* a series has none, and the form without the feedback is all cost.
|
|
18
|
+
*/
|
|
19
|
+
export interface CascadeSeries {
|
|
20
|
+
readonly version: 1;
|
|
21
|
+
/**
|
|
22
|
+
* Ordered, and ordered on purpose.
|
|
23
|
+
*
|
|
24
|
+
* Independent instances do not need an order, which makes a bag tempting.
|
|
25
|
+
* But feedback — instance N reading N-1 — needs a sequence, and retrofitting
|
|
26
|
+
* order onto something built unordered is the expensive kind of change.
|
|
27
|
+
* Ordered costs nothing today. Marcus, 2026-09-08: *"feedback will at some
|
|
28
|
+
* point be useful but maybe not this series."*
|
|
29
|
+
*/
|
|
30
|
+
readonly instances: readonly CascadeSeriesInstance[];
|
|
31
|
+
/** Which output the series collects, one value per instance. */
|
|
32
|
+
readonly collect: SeriesCollection;
|
|
33
|
+
}
|
|
34
|
+
export interface CascadeSeriesInstance {
|
|
35
|
+
/**
|
|
36
|
+
* **The record's own id, never its index in the array.**
|
|
37
|
+
*
|
|
38
|
+
* The decision the plan says to make before anything is built. A set reorders
|
|
39
|
+
* the moment a filter changes or data arrives, so a judgement pinned to a
|
|
40
|
+
* position — a favourite, an override, a note — silently becomes a different
|
|
41
|
+
* picture. For the Observatory this is the moment's id; for a wedge, the
|
|
42
|
+
* parameter tuple; for a hand-written list, whatever the author typed.
|
|
43
|
+
*/
|
|
44
|
+
readonly id: string;
|
|
45
|
+
/** This instance's overrides. `CascadePreset` already is a parameter set. */
|
|
46
|
+
readonly parameters: CascadePreset;
|
|
47
|
+
/** For display. Never an address — see `id`. */
|
|
48
|
+
readonly label?: string;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Either a graph output by name, or any node's output port.
|
|
52
|
+
*
|
|
53
|
+
* Both, for the same reason subnets stay an ordinary tool rather than the
|
|
54
|
+
* mechanism: requiring a `cascade.core.Output` node would mean restructuring an
|
|
55
|
+
* existing graph before it could be a series, and `cloud-volumes` is exactly
|
|
56
|
+
* that case.
|
|
57
|
+
*/
|
|
58
|
+
export type SeriesCollection = {
|
|
59
|
+
readonly graphOutput: string;
|
|
60
|
+
} | {
|
|
61
|
+
readonly nodeId: string;
|
|
62
|
+
readonly output: string;
|
|
63
|
+
};
|
|
64
|
+
/** One instance's result. `status` is the run's, so a failed instance is
|
|
65
|
+
* reported rather than dropped — a series that silently renders 9,998 of
|
|
66
|
+
* 10,000 is worse than one that says which two failed. */
|
|
67
|
+
export interface SeriesInstanceResult {
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly index: number;
|
|
70
|
+
readonly status: "completed" | "cancelled" | "failed";
|
|
71
|
+
readonly value: unknown;
|
|
72
|
+
readonly diagnostics: readonly unknown[];
|
|
73
|
+
readonly durationMs: number;
|
|
74
|
+
}
|
|
75
|
+
//# sourceMappingURL=series.d.ts.map
|
|
@@ -13,16 +13,16 @@ const CONTROLS = new Set(["number", "slider", "range", "int", "boolean", "text",
|
|
|
13
13
|
* capability belongs to.
|
|
14
14
|
*/
|
|
15
15
|
export const CAPABILITIES_BY_ENVIRONMENT = {
|
|
16
|
-
portable: ["assets", "media"],
|
|
16
|
+
portable: ["assets", "media", "gpu"],
|
|
17
17
|
browser: ["assets", "media", "gpu"],
|
|
18
|
-
server: ["files", "assets", "media", "python", "shell"],
|
|
18
|
+
server: ["files", "assets", "media", "python", "shell", "gpu"],
|
|
19
19
|
};
|
|
20
20
|
const ALLOWED = CAPABILITIES_BY_ENVIRONMENT;
|
|
21
21
|
const fields = (names) => new Set(names.split(" "));
|
|
22
22
|
const TOP_FIELDS = fields("apiVersion runsOn container capabilities label description icon inputs outputs props");
|
|
23
23
|
const DATA_FIELDS = fields("kind type default description variadic min max step accept control options");
|
|
24
24
|
const OUTPUT_FIELDS = fields("kind type description");
|
|
25
|
-
const PROP_FIELDS = fields("type default label description min max step accept control options expression");
|
|
25
|
+
const PROP_FIELDS = fields("type default label description min max step accept control options expression action");
|
|
26
26
|
function diagnostic(code, message, path) {
|
|
27
27
|
return { phase: "definition", code: `definition/${code}`, message, path };
|
|
28
28
|
}
|
|
@@ -90,8 +90,10 @@ export function validateNodeDefinition(value) {
|
|
|
90
90
|
add("api-version", "apiVersion must be 1", "apiVersion");
|
|
91
91
|
if (!Object.prototype.hasOwnProperty.call(ALLOWED, String(environment)))
|
|
92
92
|
add("environment", "runsOn must be portable, browser, or server", "runsOn");
|
|
93
|
-
if (value.container !== undefined &&
|
|
94
|
-
|
|
93
|
+
if (value.container !== undefined &&
|
|
94
|
+
value.container !== "subnet" &&
|
|
95
|
+
value.container !== "feedback")
|
|
96
|
+
add("invalid-container", "container must be subnet or feedback", "container");
|
|
95
97
|
const allowed = ALLOWED[environment] ?? [];
|
|
96
98
|
if (value.capabilities !== undefined && !Array.isArray(value.capabilities))
|
|
97
99
|
add("invalid-capabilities", "capabilities must be an array", "capabilities");
|
|
@@ -170,6 +172,22 @@ export function validateNodeDefinition(value) {
|
|
|
170
172
|
add("invalid-control", `${String(raw.control)} is not a Cascade control`, `${path}.control`);
|
|
171
173
|
if ("options" in raw && raw.control !== "select")
|
|
172
174
|
add("options-without-select", "options require the select control", `${path}.options`);
|
|
175
|
+
// A labelled option is `{ value, label }`. Checked because the failure
|
|
176
|
+
// is cosmetic and silent: a malformed entry reaches the control and
|
|
177
|
+
// renders as "[object Object]" rather than erroring, and the labelled
|
|
178
|
+
// form exists precisely because a lost label lost a cost warning.
|
|
179
|
+
if (Array.isArray(raw.options))
|
|
180
|
+
for (const [index, option] of raw.options.entries()) {
|
|
181
|
+
if (option === null || typeof option !== "object")
|
|
182
|
+
continue;
|
|
183
|
+
const entry = option;
|
|
184
|
+
if (!("value" in entry))
|
|
185
|
+
add("invalid-option", "a labelled option needs a value", `${path}.options[${index}]`);
|
|
186
|
+
if (typeof entry.label !== "string" || !entry.label)
|
|
187
|
+
add("invalid-option", "a labelled option needs a non-empty label", `${path}.options[${index}].label`);
|
|
188
|
+
if ("disabled" in entry && typeof entry.disabled !== "boolean")
|
|
189
|
+
add("invalid-option", "disabled must be a boolean", `${path}.options[${index}].disabled`);
|
|
190
|
+
}
|
|
173
191
|
const type = raw.type;
|
|
174
192
|
if (!validType(type)) {
|
|
175
193
|
add("invalid-type", `${String(type)} is not a Cascade type`, `${path}.type`);
|
|
@@ -1,6 +1,8 @@
|
|
|
1
|
+
import type { Camera } from "./camera.js";
|
|
1
2
|
import type { Color } from "./color.js";
|
|
3
|
+
import type { Light, Scene } from "./scene.js";
|
|
2
4
|
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"];
|
|
5
|
+
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", "scene", "light", "asset", "array", "object", "any"];
|
|
4
6
|
export type CoreType = (typeof CORE_TYPES)[number];
|
|
5
7
|
export type NamespacedType = `${string}.${string}`;
|
|
6
8
|
export type CascadeType = CoreType | NamespacedType;
|
|
@@ -94,7 +96,7 @@ export interface AssetRef {
|
|
|
94
96
|
readonly mediaType?: string;
|
|
95
97
|
}
|
|
96
98
|
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;
|
|
99
|
+
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 "scene" ? Scene : T extends "light" ? Light : 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
100
|
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
101
|
export {};
|
|
100
102
|
//# sourceMappingURL=values.d.ts.map
|
package/dist/contracts/values.js
CHANGED
|
@@ -21,6 +21,9 @@ export const CORE_TYPES = [
|
|
|
21
21
|
"mesh",
|
|
22
22
|
"rects",
|
|
23
23
|
"color",
|
|
24
|
+
"camera",
|
|
25
|
+
"scene",
|
|
26
|
+
"light",
|
|
24
27
|
"asset",
|
|
25
28
|
"array",
|
|
26
29
|
"object",
|
|
@@ -38,6 +41,13 @@ export const IMPLICIT_TYPE_CONVERSIONS = {
|
|
|
38
41
|
vec2i: ["vec2"],
|
|
39
42
|
vec3i: ["vec3"],
|
|
40
43
|
vec4i: ["vec4"],
|
|
44
|
+
/**
|
|
45
|
+
* A geometry *is* a scene — one with the viewport's own camera and no lights.
|
|
46
|
+
* So the wire is legal and nothing is lost, which is the widening rule this
|
|
47
|
+
* table already follows. Every consumer of a `scene` input reads it through
|
|
48
|
+
* `asScene`, which performs the promotion; see `scene.ts`.
|
|
49
|
+
*/
|
|
50
|
+
geometry: ["scene"],
|
|
41
51
|
vec4: ["color"],
|
|
42
52
|
color: ["vec4"],
|
|
43
53
|
};
|
package/dist/gpu.d.ts
CHANGED
|
@@ -69,3 +69,20 @@ export declare const ColorWrite: {
|
|
|
69
69
|
readonly ALPHA: 8;
|
|
70
70
|
readonly ALL: 15;
|
|
71
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
CHANGED
|
@@ -43,11 +43,71 @@ var ColorWrite = {
|
|
|
43
43
|
ALPHA: 8,
|
|
44
44
|
ALL: 15
|
|
45
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
|
+
}
|
|
46
105
|
export {
|
|
47
106
|
BufferUsage,
|
|
48
107
|
ColorWrite,
|
|
49
108
|
MapMode,
|
|
50
109
|
ShaderStage,
|
|
51
|
-
TextureUsage
|
|
110
|
+
TextureUsage,
|
|
111
|
+
readTexture
|
|
52
112
|
};
|
|
53
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-De1cIZ9N.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
|
-
<link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-
|
|
11
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-BDJsc2Ks.js">
|
|
11
|
+
<link rel="stylesheet" crossorigin href="/assets/index-ilgbOu6G.css">
|
|
12
12
|
</head>
|
|
13
13
|
<body>
|
|
14
14
|
<div id="app"></div>
|
package/dist/io.d.ts
CHANGED
|
@@ -1,19 +1,5 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* The interop rests on one rule: an image port carries a project-relative PATH,
|
|
5
|
-
* never pixels. A Python node writes a file and emits its path; a WebGL node
|
|
6
|
-
* reads that path as a texture, renders, writes its result back, and emits a
|
|
7
|
-
* path of the same kind. Neither knows or cares which side produced its input,
|
|
8
|
-
* which is what makes the two interchangeable — and it lets the heavy stages
|
|
9
|
-
* stay in Python while the interactive ones run in the page.
|
|
10
|
-
*
|
|
11
|
-
* The counterpart on the server side is the project's own exec bridge
|
|
12
|
-
* (`runStage`), which is per-project because the stages are.
|
|
13
|
-
*
|
|
14
|
-
* This file is compiled into any node module that imports 'cascade/io'; it is
|
|
15
|
-
* not published to npm and has no dependencies.
|
|
16
|
-
*/
|
|
1
|
+
/** Shared `cascade/io` helpers. Studio uses project HTTP routes; other hosts
|
|
2
|
+
* install a bridge. Image ports carry paths rather than ambient pixel buffers. */
|
|
17
3
|
export interface MediaOptions {
|
|
18
4
|
/** Ask for a resized copy. Never upscales past the file's own resolution. */
|
|
19
5
|
width?: number;
|
|
@@ -45,6 +31,8 @@ export interface IoBridge {
|
|
|
45
31
|
read(path: string, options?: MediaOptions): Promise<Uint8Array>;
|
|
46
32
|
/** Write bytes into the project cache. Returns the stored project-relative path. */
|
|
47
33
|
write(path: string, data: Uint8Array): Promise<string>;
|
|
34
|
+
/** Host-owned display URL, valid until the host releases its stored asset. */
|
|
35
|
+
url?(path: string, options?: MediaOptions): string;
|
|
48
36
|
}
|
|
49
37
|
/** Install the local transport. Returns a disposer, so a test can take it down. */
|
|
50
38
|
export declare function installIoBridge(bridge: IoBridge | null): () => void;
|
package/dist/io.js
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
// server/src/runtime/io.ts
|
|
2
2
|
function mediaUrl(path, options = {}) {
|
|
3
3
|
if (/^(https?:|data:|blob:)/.test(path)) return path;
|
|
4
|
+
const bridge = localIo();
|
|
5
|
+
if (bridge?.url) return bridge.url(path, options);
|
|
4
6
|
const clean = path.replace(/^\.?\//, "");
|
|
5
7
|
const encoded = clean.split("/").map(encodeURIComponent).join("/");
|
|
6
8
|
const params = new URLSearchParams();
|
|
@@ -40,17 +42,33 @@ async function loadBytes(path, options = { raw: true }) {
|
|
|
40
42
|
async function loadImage(path, options = { raw: true }) {
|
|
41
43
|
const image = new Image();
|
|
42
44
|
const bridge = localIo();
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
45
|
+
let temporaryUrl;
|
|
46
|
+
try {
|
|
47
|
+
if (bridge?.url) {
|
|
48
|
+
image.crossOrigin = "anonymous";
|
|
49
|
+
image.src = bridge.url(path, options);
|
|
50
|
+
} else if (bridge) {
|
|
51
|
+
const bytes = await loadBytes(path, options);
|
|
52
|
+
if (typeof document === "undefined") {
|
|
53
|
+
image.src = new Uint8Array(bytes);
|
|
54
|
+
} else {
|
|
55
|
+
temporaryUrl = URL.createObjectURL(new Blob([bytes]));
|
|
56
|
+
image.src = temporaryUrl;
|
|
57
|
+
}
|
|
58
|
+
} else {
|
|
59
|
+
image.crossOrigin = "anonymous";
|
|
60
|
+
image.src = mediaUrl(path, options);
|
|
61
|
+
}
|
|
62
|
+
await image.decode();
|
|
63
|
+
return image;
|
|
64
|
+
} finally {
|
|
65
|
+
if (temporaryUrl) URL.revokeObjectURL(temporaryUrl);
|
|
48
66
|
}
|
|
49
|
-
await image.decode();
|
|
50
|
-
return image;
|
|
51
67
|
}
|
|
52
68
|
async function loadBitmap(path, options = { raw: true }) {
|
|
53
|
-
|
|
69
|
+
const bridge = localIo();
|
|
70
|
+
if (bridge?.url) return createImageBitmap(await loadImage(path, options));
|
|
71
|
+
if (bridge) return createImageBitmap(new Blob([await loadBytes(path, options)]));
|
|
54
72
|
const response = await fetch(mediaUrl(path, options));
|
|
55
73
|
if (!response.ok) throw new Error(`cascade/io: cannot load ${path} (${response.status})`);
|
|
56
74
|
return createImageBitmap(await response.blob());
|