@field/cascade 0.4.0 → 0.5.2
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 +18 -0
- package/ARCHITECTURE.md +53 -7
- package/CHANGELOG.md +68 -0
- package/CONTRIBUTING.md +33 -0
- package/DESIGN.md +9 -6
- package/README.md +80 -4
- package/dist/assets/{CodeEditor-BNOJIDzL.js → CodeEditor-BHej8kM9.js} +7 -7
- package/dist/assets/{CodePanel-2wp2G0Fj.js → CodePanel-CT3eirK8.js} +2 -2
- package/dist/assets/{CustomNodeDialog-DNqaVBqA.js → CustomNodeDialog-BG0m7_Rv.js} +1 -1
- package/dist/assets/{ProjectSettings-DcdYF2E1.js → ProjectSettings-BxoLWJG6.js} +2 -2
- package/dist/assets/{SettingsDialog-D2xVvyiR.js → SettingsDialog-r9PzZ22z.js} +1 -1
- package/dist/assets/{VersionHistoryDialog-DW288ykF.js → VersionHistoryDialog-B6zUV6Dy.js} +1 -1
- package/dist/assets/{core-B1y38i95.js → core-wwx5881P.js} +2 -2
- package/dist/assets/{cssMode-BLucnaln.js → cssMode-DY5T2Or0.js} +1 -1
- package/dist/assets/definitions-BgCvRHyR.js +1 -0
- package/dist/assets/{editor.api-B3Et3Pph.js → editor.api-mkORHEMm.js} +1 -1
- package/dist/assets/{freemarker2-DVlPoano.js → freemarker2-CIWlk0sw.js} +1 -1
- package/dist/assets/geo-CIxgs0oE.js +1 -0
- package/dist/assets/{handlebars-B5FZBAMR.js → handlebars-t4jbj3Br.js} +1 -1
- package/dist/assets/{html-FodD8KbR.js → html-Ctx7UOlY.js} +1 -1
- package/dist/assets/{htmlMode-CsG57VCD.js → htmlMode-BKNZxVw7.js} +1 -1
- package/dist/assets/{image-B3F-nq25.js → image-BCBFz0gD.js} +3 -3
- package/dist/assets/index-CeqTfMbr.js +212 -0
- package/dist/assets/{index-Y2ock1cw.css → index-ilgbOu6G.css} +1 -1
- package/dist/assets/{javascript-B3gonF_H.js → javascript-Da_TJBzH.js} +1 -1
- package/dist/assets/{jsonMode-Y3w1ahZg.js → jsonMode-Ftt5d1nV.js} +1 -1
- package/dist/assets/{liquid-CTpWXIYO.js → liquid-D5kWYG57.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-CODBdKEZ.js → lspLanguageFeatures-B-93LC_L.js} +1 -1
- package/dist/assets/{mdx-CDAQJZIz.js → mdx-DGT3C_qs.js} +1 -1
- package/dist/assets/nodeTypeUtils-BDJsc2Ks.js +1 -0
- package/dist/assets/pop-BJGN5Ac4.js +1 -0
- package/dist/assets/{python-LOIlxJU-.js → python-Bz3Eew5S.js} +1 -1
- package/dist/assets/{razor-CllPQSHo.js → razor-CFDdKpKI.js} +1 -1
- package/dist/assets/{tsMode-CO28ut8t.js → tsMode-BaCINWBI.js} +1 -1
- package/dist/assets/{typescript-ruU9wKQ_.js → typescript-BoOWspVs.js} +1 -1
- package/dist/assets/{workers-Bc1Ywp4-.js → workers-dSWyDxsQ.js} +1 -1
- package/dist/assets/{xml-DoXP2Dh1.js → xml-eFi1_vYt.js} +1 -1
- package/dist/assets/{yaml-D9YPyGiJ.js → yaml-a7xrJHxm.js} +1 -1
- package/dist/cli/index.js +16266 -12653
- package/dist/cli/runtime/io.ts +31 -25
- package/dist/contracts/definition.d.ts +21 -2
- package/dist/contracts/index.d.ts +2 -0
- package/dist/contracts/index.js +2 -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 +1 -1
- package/dist/contracts/validate.js +4 -2
- package/dist/contracts/values.d.ts +3 -2
- package/dist/contracts/values.js +9 -0
- 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/index.d.ts +3 -2
- package/dist/runtime/builtins/core/index.js +6 -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/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/index.d.ts +1 -0
- package/dist/runtime/index.js +1 -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 +221 -3
- package/dist/runtime/scene/index.d.ts +50 -0
- package/dist/runtime/scene/index.js +94 -0
- package/dist/studio/panel.d.ts +68 -1
- package/doc/HEADLESS_GPU.md +4 -4
- package/doc/NODE_REFERENCE.md +372 -1
- package/doc/PROJECT_AUTHORING.md +16 -4
- package/doc/WEB_PLAYER.md +131 -0
- package/package.json +16 -2
- package/dist/assets/geo-DhyFdBXX.js +0 -1
- package/dist/assets/index-CC6YQw2n.js +0 -212
- package/dist/assets/nodeTypeUtils-BBA1JqHx.js +0 -1
package/dist/cli/runtime/io.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
|
|
|
18
4
|
export interface MediaOptions {
|
|
19
5
|
/** Ask for a resized copy. Never upscales past the file's own resolution. */
|
|
@@ -30,6 +16,8 @@ export interface MediaOptions {
|
|
|
30
16
|
/** Build the URL that serves a project file to the page. */
|
|
31
17
|
export function mediaUrl(path: string, options: MediaOptions = {}): string {
|
|
32
18
|
if (/^(https?:|data:|blob:)/.test(path)) return path;
|
|
19
|
+
const bridge = localIo();
|
|
20
|
+
if (bridge?.url) return bridge.url(path, options);
|
|
33
21
|
const clean = path.replace(/^\.?\//, '');
|
|
34
22
|
const encoded = clean.split('/').map(encodeURIComponent).join('/');
|
|
35
23
|
const params = new URLSearchParams();
|
|
@@ -61,6 +49,8 @@ export interface IoBridge {
|
|
|
61
49
|
read(path: string, options?: MediaOptions): Promise<Uint8Array>;
|
|
62
50
|
/** Write bytes into the project cache. Returns the stored project-relative path. */
|
|
63
51
|
write(path: string, data: Uint8Array): Promise<string>;
|
|
52
|
+
/** Host-owned display URL, valid until the host releases its stored asset. */
|
|
53
|
+
url?(path: string, options?: MediaOptions): string;
|
|
64
54
|
}
|
|
65
55
|
|
|
66
56
|
const IO_BRIDGE_KEY = '__cascadeIoBridge';
|
|
@@ -108,20 +98,36 @@ export async function loadBytes(path: string, options: MediaOptions = { raw: tru
|
|
|
108
98
|
export async function loadImage(path: string, options: MediaOptions = { raw: true }): Promise<HTMLImageElement> {
|
|
109
99
|
const image = new Image();
|
|
110
100
|
const bridge = localIo();
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
(
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
101
|
+
let temporaryUrl: string | undefined;
|
|
102
|
+
try {
|
|
103
|
+
if (bridge?.url) {
|
|
104
|
+
image.crossOrigin = 'anonymous';
|
|
105
|
+
image.src = bridge.url(path, options);
|
|
106
|
+
} else if (bridge) {
|
|
107
|
+
const bytes = await loadBytes(path, options);
|
|
108
|
+
if (typeof document === 'undefined') {
|
|
109
|
+
// The Node canvas Image decoder accepts bytes, not browser Blob URLs.
|
|
110
|
+
(image as unknown as { src: unknown }).src = new Uint8Array(bytes);
|
|
111
|
+
} else {
|
|
112
|
+
temporaryUrl = URL.createObjectURL(new Blob([bytes]));
|
|
113
|
+
image.src = temporaryUrl;
|
|
114
|
+
}
|
|
115
|
+
} else {
|
|
116
|
+
image.crossOrigin = 'anonymous';
|
|
117
|
+
image.src = mediaUrl(path, options);
|
|
118
|
+
}
|
|
119
|
+
await image.decode();
|
|
120
|
+
return image;
|
|
121
|
+
} finally {
|
|
122
|
+
if (temporaryUrl) URL.revokeObjectURL(temporaryUrl);
|
|
117
123
|
}
|
|
118
|
-
await image.decode();
|
|
119
|
-
return image;
|
|
120
124
|
}
|
|
121
125
|
|
|
122
126
|
/** Same, as an ImageBitmap — the right input for `texImage2D`. */
|
|
123
127
|
export async function loadBitmap(path: string, options: MediaOptions = { raw: true }): Promise<ImageBitmap> {
|
|
124
|
-
|
|
128
|
+
const bridge = localIo();
|
|
129
|
+
if (bridge?.url) return createImageBitmap(await loadImage(path, options));
|
|
130
|
+
if (bridge) return createImageBitmap(new Blob([await loadBytes(path, options)]));
|
|
125
131
|
const response = await fetch(mediaUrl(path, options));
|
|
126
132
|
if (!response.ok) throw new Error(`cascade/io: cannot load ${path} (${response.status})`);
|
|
127
133
|
return createImageBitmap(await response.blob());
|
|
@@ -146,8 +146,27 @@ export type AnyPropDefinition = {
|
|
|
146
146
|
export interface NodeDefinitionForEnvironment<E extends RuntimeEnvironment> {
|
|
147
147
|
readonly apiVersion: 1;
|
|
148
148
|
readonly runsOn: E;
|
|
149
|
-
/**
|
|
150
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Declares that authored nodes may use this node as their parent.
|
|
151
|
+
*
|
|
152
|
+
* `subnet` is a **grouping**: the children are ordinary nodes in the one flat
|
|
153
|
+
* graph, cooked once each in the usual dependency order, and the container is
|
|
154
|
+
* a boundary for display and for `cascade.core.Input`/`Output` to bind to.
|
|
155
|
+
*
|
|
156
|
+
* `feedback` is an **evaluation construct**: the children are removed from the
|
|
157
|
+
* main order and re-run once per step by the container, with the previous
|
|
158
|
+
* step's result entering through `cascade.core.Previous`. It exists because a
|
|
159
|
+
* simulation is the one thing a pure DAG cannot express — which is why Houdini
|
|
160
|
+
* made its Solver SOP special too — and Marcus asked for it on 2026-09-09,
|
|
161
|
+
* wanting to dive into a particle node and see its forces as nodes.
|
|
162
|
+
*
|
|
163
|
+
* The distinction is load-bearing for correctness, not just for scheduling: a
|
|
164
|
+
* `subnet` child's output is read once and is a function of the graph, while a
|
|
165
|
+
* `feedback` child's output is read N times and is a function of the graph
|
|
166
|
+
* *and* the step. Anything that caches on a node's inputs has to know which
|
|
167
|
+
* kind it is looking at.
|
|
168
|
+
*/
|
|
169
|
+
readonly container?: "subnet" | "feedback";
|
|
151
170
|
readonly capabilities?: readonly CapabilityForEnvironment<E>[];
|
|
152
171
|
readonly label?: string;
|
|
153
172
|
readonly description?: string;
|
|
@@ -5,6 +5,8 @@ export * from "./color.js";
|
|
|
5
5
|
export * from "./definition.js";
|
|
6
6
|
export * from "./document.js";
|
|
7
7
|
export * from "./geometry.js";
|
|
8
|
+
export * from "./particles.js";
|
|
9
|
+
export * from "./scene.js";
|
|
8
10
|
export * from "./series.js";
|
|
9
11
|
export * from "./validate.js";
|
|
10
12
|
export * from "./values.js";
|
package/dist/contracts/index.js
CHANGED
|
@@ -5,6 +5,8 @@ export * from "./color.js";
|
|
|
5
5
|
export * from "./definition.js";
|
|
6
6
|
export * from "./document.js";
|
|
7
7
|
export * from "./geometry.js";
|
|
8
|
+
export * from "./particles.js";
|
|
9
|
+
export * from "./scene.js";
|
|
8
10
|
export * from "./series.js";
|
|
9
11
|
export * from "./validate.js";
|
|
10
12
|
export * from "./values.js";
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type AttributeLevel, type AttributeStorage, type Geometry } from "./geometry.js";
|
|
2
|
+
/**
|
|
3
|
+
* The attribute contract a particle system reads and writes.
|
|
4
|
+
*
|
|
5
|
+
* Step 1 of `PLAN particles.md`, and it is deliberately first: **renaming an
|
|
6
|
+
* attribute after three operators read it is the expensive change.** So the
|
|
7
|
+
* names, levels and storage are fixed before any node exists, and nothing here
|
|
8
|
+
* is a node — this file declares a vocabulary and the checks that hold it.
|
|
9
|
+
*
|
|
10
|
+
* Houdini's names throughout, per the standing rule. Where Houdini and Cascade
|
|
11
|
+
* disagree the reason is written down beside the entry rather than left as a
|
|
12
|
+
* preference.
|
|
13
|
+
*
|
|
14
|
+
* **A particle system is geometry.** There is no particle type: points with
|
|
15
|
+
* these attributes on them are what a solver reads and what `CopyToPoints`,
|
|
16
|
+
* `SvgExport` and every other geometry node already understand. That is the
|
|
17
|
+
* whole reason `PLAN geometry` retires `points` to an alias — four names for
|
|
18
|
+
* one concept, and a fifth for particles would be worse.
|
|
19
|
+
*/
|
|
20
|
+
export interface ParticleAttributeSpec {
|
|
21
|
+
readonly name: string;
|
|
22
|
+
readonly level: AttributeLevel;
|
|
23
|
+
readonly storage: AttributeStorage;
|
|
24
|
+
/** Components. `P` is 2 or 3, so it is stated as a range rather than a size. */
|
|
25
|
+
readonly size: number | readonly [number, number];
|
|
26
|
+
/** Whether a solver may run without it. */
|
|
27
|
+
readonly required: boolean;
|
|
28
|
+
readonly units?: "seconds" | "turns";
|
|
29
|
+
readonly note: string;
|
|
30
|
+
}
|
|
31
|
+
export declare const PARTICLE_ATTRIBUTES: readonly ParticleAttributeSpec[];
|
|
32
|
+
export declare function particleAttribute(name: string): ParticleAttributeSpec | undefined;
|
|
33
|
+
/** The attributes a solver cannot run without. */
|
|
34
|
+
export declare function requiredParticleAttributes(): readonly ParticleAttributeSpec[];
|
|
35
|
+
export interface ParticleContractViolation {
|
|
36
|
+
readonly attribute: string;
|
|
37
|
+
readonly reason: string;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Whether a geometry carries the attributes a solver reads, in the shapes it
|
|
41
|
+
* reads them in.
|
|
42
|
+
*
|
|
43
|
+
* Returns every violation rather than the first, because the answer a caller
|
|
44
|
+
* wants is *what is wrong with this geometry* and not *what is the first thing
|
|
45
|
+
* wrong with it* — a source node missing three attributes should say so once.
|
|
46
|
+
*
|
|
47
|
+
* Deliberately not a throw: `cascade check` wants to report this statically
|
|
48
|
+
* alongside its other findings, and a solver wants to fail with a sentence. The
|
|
49
|
+
* shared thing between them is the list.
|
|
50
|
+
*/
|
|
51
|
+
export declare function checkParticleContract(geometry: Geometry): readonly ParticleContractViolation[];
|
|
52
|
+
/** True when this geometry could be stepped by a solver. */
|
|
53
|
+
export declare function isParticleGeometry(geometry: Geometry): boolean;
|
|
54
|
+
//# sourceMappingURL=particles.d.ts.map
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import { POSITION_ATTRIBUTE, } from "./geometry.js";
|
|
2
|
+
/**
|
|
3
|
+
* Position. Already `POSITION_ATTRIBUTE`, and `f64` because the Python bridge
|
|
4
|
+
* writes points as float64 and narrowing would add a lossy step to a boundary
|
|
5
|
+
* that is lossless today.
|
|
6
|
+
*/
|
|
7
|
+
const P = {
|
|
8
|
+
name: POSITION_ATTRIBUTE,
|
|
9
|
+
level: "point",
|
|
10
|
+
storage: "f64",
|
|
11
|
+
size: [2, 3],
|
|
12
|
+
required: true,
|
|
13
|
+
note: "Position. Shared with every other geometry node, not a particle attribute.",
|
|
14
|
+
};
|
|
15
|
+
export const PARTICLE_ATTRIBUTES = Object.freeze([
|
|
16
|
+
P,
|
|
17
|
+
{
|
|
18
|
+
name: "v",
|
|
19
|
+
level: "point",
|
|
20
|
+
storage: "f32",
|
|
21
|
+
size: [2, 3],
|
|
22
|
+
required: true,
|
|
23
|
+
note: "Velocity, in units per second. f32 rather than f64: a velocity is not a coordinate, so the precision argument for P does not carry over.",
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "age",
|
|
27
|
+
level: "point",
|
|
28
|
+
storage: "f32",
|
|
29
|
+
size: 1,
|
|
30
|
+
required: true,
|
|
31
|
+
units: "seconds",
|
|
32
|
+
// The one place this departs from the sketch it is modelled on.
|
|
33
|
+
note: "Seconds, not frames. A frame count changes meaning with fps, and a graph whose particles die at a different point when rendered at 60 is not deterministic in any useful sense.",
|
|
34
|
+
},
|
|
35
|
+
{
|
|
36
|
+
name: "life",
|
|
37
|
+
level: "point",
|
|
38
|
+
storage: "f32",
|
|
39
|
+
size: 1,
|
|
40
|
+
required: true,
|
|
41
|
+
units: "seconds",
|
|
42
|
+
note: "Life expectancy in seconds. A particle dies when age >= life.",
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
name: "id",
|
|
46
|
+
level: "point",
|
|
47
|
+
storage: "i32",
|
|
48
|
+
size: 1,
|
|
49
|
+
required: true,
|
|
50
|
+
note: "Stable across frames and across kills. Mandatory: a particle array reorders on every kill, so anything durable and per-particle must key on this rather than on an index.",
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
name: "nextid",
|
|
54
|
+
level: "detail",
|
|
55
|
+
storage: "i32",
|
|
56
|
+
size: 1,
|
|
57
|
+
required: true,
|
|
58
|
+
note: "The next id to hand out. Houdini's own mechanism for keeping id monotonic, and what stops an id being reused after a kill.",
|
|
59
|
+
},
|
|
60
|
+
{
|
|
61
|
+
name: "Cd",
|
|
62
|
+
level: "point",
|
|
63
|
+
storage: "f32",
|
|
64
|
+
size: 4,
|
|
65
|
+
required: false,
|
|
66
|
+
note: "Colour. Owned by the style vocabulary in PLAN geometry, which refuses a Cd that is a string or numeric with any size but four.",
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
name: "pscale",
|
|
70
|
+
level: "point",
|
|
71
|
+
storage: "f32",
|
|
72
|
+
size: 1,
|
|
73
|
+
required: false,
|
|
74
|
+
note: "Not a POP attribute in Houdini's sense. Writing it is what makes CopyToPoints scale its instances, which is how oriented marks get drawn without a new node.",
|
|
75
|
+
},
|
|
76
|
+
{
|
|
77
|
+
name: "N",
|
|
78
|
+
level: "point",
|
|
79
|
+
storage: "f32",
|
|
80
|
+
size: [2, 3],
|
|
81
|
+
required: false,
|
|
82
|
+
note: "Orientation, read by CopyToPoints alongside pscale.",
|
|
83
|
+
},
|
|
84
|
+
]);
|
|
85
|
+
const BY_NAME = new Map(PARTICLE_ATTRIBUTES.map((spec) => [spec.name, spec]));
|
|
86
|
+
export function particleAttribute(name) {
|
|
87
|
+
return BY_NAME.get(name);
|
|
88
|
+
}
|
|
89
|
+
/** The attributes a solver cannot run without. */
|
|
90
|
+
export function requiredParticleAttributes() {
|
|
91
|
+
return PARTICLE_ATTRIBUTES.filter((spec) => spec.required);
|
|
92
|
+
}
|
|
93
|
+
function sizeMatches(spec, size) {
|
|
94
|
+
return typeof spec.size === "number"
|
|
95
|
+
? size === spec.size
|
|
96
|
+
: size >= spec.size[0] && size <= spec.size[1];
|
|
97
|
+
}
|
|
98
|
+
function describeSize(spec) {
|
|
99
|
+
return typeof spec.size === "number" ? String(spec.size) : `${spec.size[0]} or ${spec.size[1]}`;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Whether a geometry carries the attributes a solver reads, in the shapes it
|
|
103
|
+
* reads them in.
|
|
104
|
+
*
|
|
105
|
+
* Returns every violation rather than the first, because the answer a caller
|
|
106
|
+
* wants is *what is wrong with this geometry* and not *what is the first thing
|
|
107
|
+
* wrong with it* — a source node missing three attributes should say so once.
|
|
108
|
+
*
|
|
109
|
+
* Deliberately not a throw: `cascade check` wants to report this statically
|
|
110
|
+
* alongside its other findings, and a solver wants to fail with a sentence. The
|
|
111
|
+
* shared thing between them is the list.
|
|
112
|
+
*/
|
|
113
|
+
export function checkParticleContract(geometry) {
|
|
114
|
+
const violations = [];
|
|
115
|
+
for (const spec of PARTICLE_ATTRIBUTES) {
|
|
116
|
+
if (spec.level === "detail") {
|
|
117
|
+
const value = geometry.detail[spec.name];
|
|
118
|
+
if (value === undefined) {
|
|
119
|
+
if (spec.required) {
|
|
120
|
+
violations.push({
|
|
121
|
+
attribute: spec.name,
|
|
122
|
+
reason: `missing detail attribute ${spec.name} — ${spec.note}`,
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
continue;
|
|
126
|
+
}
|
|
127
|
+
if (typeof value !== "number") {
|
|
128
|
+
violations.push({
|
|
129
|
+
attribute: spec.name,
|
|
130
|
+
reason: `detail ${spec.name} must be a number, not ${typeof value}`,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
continue;
|
|
134
|
+
}
|
|
135
|
+
const attribute = geometry.point[spec.name];
|
|
136
|
+
if (attribute === undefined) {
|
|
137
|
+
if (spec.required) {
|
|
138
|
+
violations.push({
|
|
139
|
+
attribute: spec.name,
|
|
140
|
+
reason: `missing point attribute ${spec.name} — ${spec.note}`,
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
// A string where a number belongs is the failure that would otherwise
|
|
146
|
+
// reach arithmetic and produce NaN silently.
|
|
147
|
+
if (attribute.storage === "string") {
|
|
148
|
+
violations.push({
|
|
149
|
+
attribute: spec.name,
|
|
150
|
+
reason: `${spec.name} is a string attribute; a solver reads it as ${spec.storage}`,
|
|
151
|
+
});
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (!sizeMatches(spec, attribute.size)) {
|
|
155
|
+
violations.push({
|
|
156
|
+
attribute: spec.name,
|
|
157
|
+
reason: `${spec.name} has size ${attribute.size}; expected ${describeSize(spec)}`,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return violations;
|
|
162
|
+
}
|
|
163
|
+
/** True when this geometry could be stepped by a solver. */
|
|
164
|
+
export function isParticleGeometry(geometry) {
|
|
165
|
+
return checkParticleContract(geometry).length === 0;
|
|
166
|
+
}
|
|
167
|
+
//# sourceMappingURL=particles.js.map
|
|
@@ -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
|
@@ -82,7 +82,7 @@ export const nodeDefinitionSchema = {
|
|
|
82
82
|
...object({
|
|
83
83
|
apiVersion: { const: 1 },
|
|
84
84
|
runsOn: { enum: ["portable", "browser", "server"] },
|
|
85
|
-
container: {
|
|
85
|
+
container: { enum: ["subnet", "feedback"] },
|
|
86
86
|
capabilities: {
|
|
87
87
|
type: "array",
|
|
88
88
|
items: { type: "string" },
|
|
@@ -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");
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import type { Camera } from "./camera.js";
|
|
2
2
|
import type { Color } from "./color.js";
|
|
3
|
+
import type { Light, Scene } from "./scene.js";
|
|
3
4
|
import type { Geometry, GeometryFileRef, GeometryJson, GeometrySerialized } from "./geometry.js";
|
|
4
|
-
export declare const CORE_TYPES: readonly ["float", "int", "bool", "string", "vec2", "vec3", "vec4", "vec2i", "vec3i", "vec4i", "mat2", "mat3", "mat4", "image", "texture", "geometry", "points", "lines", "polyline", "mesh", "rects", "color", "camera", "asset", "array", "object", "any"];
|
|
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"];
|
|
5
6
|
export type CoreType = (typeof CORE_TYPES)[number];
|
|
6
7
|
export type NamespacedType = `${string}.${string}`;
|
|
7
8
|
export type CascadeType = CoreType | NamespacedType;
|
|
@@ -95,7 +96,7 @@ export interface AssetRef {
|
|
|
95
96
|
readonly mediaType?: string;
|
|
96
97
|
}
|
|
97
98
|
export type SerializableValue = JsonValue | AssetRef | ImageRef | GeometryJson | GeometryFileRef;
|
|
98
|
-
export type ValueForType<T extends CascadeType> = T extends "float" | "int" ? number : T extends "bool" ? boolean : T extends "string" ? string : T extends "vec2" ? Vec2 : T extends "vec3" ? Vec3 : T extends "vec4" ? Vec4 : T extends "vec2i" ? Vec2i : T extends "vec3i" ? Vec3i : T extends "vec4i" ? Vec4i : T extends "mat2" ? Mat2 : T extends "mat3" ? Mat3 : T extends "mat4" ? Mat4 : T extends "color" ? Color : T extends "camera" ? Camera : T extends "image" ? ImageRef : T extends "texture" ? TextureHandle : T extends "geometry" ? Geometry : T extends "points" ? PointSet : T extends "lines" ? LineSegments : T extends "polyline" ? Polyline : T extends "mesh" ? Mesh : T extends "rects" ? Rects : T extends "asset" ? AssetRef : T extends "array" ? readonly unknown[] : T extends "object" ? Readonly<Record<string, unknown>> : unknown;
|
|
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;
|
|
99
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>;
|
|
100
101
|
export {};
|
|
101
102
|
//# sourceMappingURL=values.d.ts.map
|
package/dist/contracts/values.js
CHANGED
|
@@ -22,6 +22,8 @@ export const CORE_TYPES = [
|
|
|
22
22
|
"rects",
|
|
23
23
|
"color",
|
|
24
24
|
"camera",
|
|
25
|
+
"scene",
|
|
26
|
+
"light",
|
|
25
27
|
"asset",
|
|
26
28
|
"array",
|
|
27
29
|
"object",
|
|
@@ -39,6 +41,13 @@ export const IMPLICIT_TYPE_CONVERSIONS = {
|
|
|
39
41
|
vec2i: ["vec2"],
|
|
40
42
|
vec3i: ["vec3"],
|
|
41
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"],
|
|
42
51
|
vec4: ["color"],
|
|
43
52
|
color: ["vec4"],
|
|
44
53
|
};
|
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-CeqTfMbr.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>
|