@field/cascade 0.8.0 → 0.11.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/AGENTS.md +36 -3
- package/CHANGELOG.md +401 -0
- package/DESIGN.md +14 -0
- package/README.md +41 -26
- package/dist/assets/CodeEditor-4O7FQ__R.js +623 -0
- package/dist/assets/CodePanel-BU-autmq.js +2 -0
- package/dist/assets/{CustomNodeDialog-BQbEhIwL.js → CustomNodeDialog-BRpDfOV5.js} +1 -1
- package/dist/assets/OpenGraphDialog-BwbRlC0X.js +1 -0
- package/dist/assets/ProjectSettings-DLOXLxcA.js +2 -0
- package/dist/assets/SettingsDialog-Bz8JxISH.js +1 -0
- package/dist/assets/{VersionHistoryDialog-D6VBzSQ0.js → VersionHistoryDialog-D5Z0nJ5-.js} +2 -2
- package/dist/assets/{core-BInLG4gw.js → core-Yqi0Ni61.js} +8 -8
- package/dist/assets/{cssMode-DfR2qmYG.js → cssMode-CxUzc4vp.js} +1 -1
- package/dist/assets/definitions-GreEMXLV.js +1 -0
- package/dist/assets/{editor.api-Ck8_vgQV.js → editor.api-BNFb25CX.js} +1 -1
- package/dist/assets/{freemarker2-fVn10Wn3.js → freemarker2-0ZDnWxeM.js} +1 -1
- package/dist/assets/geo-u0n_jOsD.js +1 -0
- package/dist/assets/{handlebars-BCSchLT2.js → handlebars-Cy18MoaD.js} +1 -1
- package/dist/assets/{html-CVpSv_k9.js → html-B-2Dx7nq.js} +1 -1
- package/dist/assets/{htmlMode-Da4ESTv0.js → htmlMode-DUAc9B_d.js} +1 -1
- package/dist/assets/{image-BLn--oSF.js → image-cAa-Nu9O.js} +3 -3
- package/dist/assets/index-5ZtDAuy2.js +220 -0
- package/dist/assets/index-7vae6YFC.css +1 -0
- package/dist/assets/index-client-DqYmKstU.js +1 -0
- package/dist/assets/{javascript-BpX0Gvcf.js → javascript-DeQsDqP5.js} +1 -1
- package/dist/assets/{jsonMode-CsLqaou7.js → jsonMode-BiFwJVSk.js} +1 -1
- package/dist/assets/legacy-CTEeXeqF.js +2 -0
- package/dist/assets/{liquid-efUz51Wf.js → liquid-BLoHH68a.js} +1 -1
- package/dist/assets/{lspLanguageFeatures-DCi5-K0n.js → lspLanguageFeatures-CNFgm1Kv.js} +1 -1
- package/dist/assets/math-DioIQOPJ.js +1 -0
- package/dist/assets/{mdx-DP_sjGVy.js → mdx-CFXQVNfs.js} +1 -1
- package/dist/assets/pop-DC_TQG9t.js +1 -0
- package/dist/assets/{python-aAuP3LUi.js → python-Cf7S7p2E.js} +1 -1
- package/dist/assets/{razor-7M6sMmC5.js → razor-CMHBcG-g.js} +1 -1
- package/dist/assets/studioScenes-8sq9FfFg.js +1 -0
- package/dist/assets/{tsMode-Bf4CnoYK.js → tsMode-BEih0GDj.js} +1 -1
- package/dist/assets/{typescript-ddhYBGyT.js → typescript-COEKYD_a.js} +1 -1
- package/dist/assets/{workers-X5la8DDT.js → workers-BNhAhMNn.js} +1 -1
- package/dist/assets/{xml-BzbXa3Ps.js → xml-CPiMpnPl.js} +1 -1
- package/dist/assets/{yaml-CXs-VzxL.js → yaml-Bgp1ITLR.js} +1 -1
- package/dist/cli/Graph-ZJJTRQTM.js +2975 -0
- package/dist/cli/chunk-2KXB47FP.js +115 -0
- package/dist/cli/chunk-35D343DC.js +1447 -0
- package/dist/cli/chunk-36P5K3SD.js +631 -0
- package/dist/cli/chunk-3JTJHF7V.js +267 -0
- package/dist/cli/chunk-4UDFYF5G.js +132 -0
- package/dist/cli/chunk-53NMBJAO.js +35 -0
- package/dist/cli/chunk-A4OAMOZH.js +310 -0
- package/dist/cli/chunk-BIJO5OFS.js +71 -0
- package/dist/cli/chunk-BP7UKF36.js +376 -0
- package/dist/cli/chunk-CKS2FQWR.js +89 -0
- package/dist/cli/chunk-CO22XHNG.js +593 -0
- package/dist/cli/chunk-D6BUOHUG.js +634 -0
- package/dist/cli/chunk-DEQSEFFH.js +153 -0
- package/dist/cli/chunk-DUIUFVU2.js +241 -0
- package/dist/cli/chunk-EUIAC3U3.js +3006 -0
- package/dist/cli/chunk-FCDXJ6EK.js +2208 -0
- package/dist/cli/chunk-GFS6W3C5.js +308 -0
- package/dist/cli/chunk-HFHWYL55.js +139 -0
- package/dist/cli/chunk-HSNOBIVT.js +263 -0
- package/dist/cli/chunk-ICH7N5PN.js +26 -0
- package/dist/cli/chunk-J7VK7MNF.js +418 -0
- package/dist/cli/chunk-KH4MUBKF.js +1551 -0
- package/dist/cli/chunk-MIIEFUGG.js +623 -0
- package/dist/cli/chunk-OWWBYLHC.js +136 -0
- package/dist/cli/chunk-POMMNKZZ.js +672 -0
- package/dist/cli/chunk-QBBKRNGY.js +90 -0
- package/dist/cli/chunk-QQPA2ZHH.js +273 -0
- package/dist/cli/chunk-RLCAJBVR.js +224 -0
- package/dist/cli/chunk-RQZ4SX5R.js +1125 -0
- package/dist/cli/chunk-RUU3YWXU.js +99 -0
- package/dist/cli/chunk-RXDIMXN5.js +98 -0
- package/dist/cli/chunk-SGJAET2L.js +197 -0
- package/dist/cli/chunk-VVGSLVSF.js +151 -0
- package/dist/cli/chunk-WD62JIP6.js +118 -0
- package/dist/cli/chunk-WIH4TEIS.js +104 -0
- package/dist/cli/chunk-WKURDVYP.js +1119 -0
- package/dist/cli/chunk-WPUDGEFY.js +20 -0
- package/dist/cli/chunk-XWDSKG7N.js +1301 -0
- package/dist/cli/chunk-YAT5Y25U.js +121 -0
- package/dist/cli/chunk-ZL4JTG5Y.js +37 -0
- package/dist/cli/cliCommands-UQSFGEHF.js +3464 -0
- package/dist/cli/commands-Q23M26QM.js +1292 -0
- package/dist/cli/definitions-HF2FTHYU.js +32 -0
- package/dist/cli/doctorCommand-B7BPQMN6.js +76 -0
- package/dist/cli/edit-JHCPTQFT.js +28 -0
- package/dist/cli/editorCommands-UIF5YLUZ.js +33 -0
- package/dist/cli/encode-HAUZTYPT.js +17 -0
- package/dist/cli/frames-LSAUBZ7P.js +16 -0
- package/dist/cli/geo-4RNK3QVN.js +43 -0
- package/dist/cli/graphCommands-6BMAKGQE.js +184 -0
- package/dist/cli/graphFile-VPSQ6OF7.js +286 -0
- package/dist/cli/headlessCanvas-5XLDZT7M.js +15 -0
- package/dist/cli/helpCommand-3VRE3KVO.js +155 -0
- package/dist/cli/index.js +106 -14207
- package/dist/cli/math-ZRMLKU2D.js +16 -0
- package/dist/cli/mcp-ZLEEMYGF.js +164 -0
- package/dist/cli/parity-KINARCP2.js +328 -0
- package/dist/cli/pop-USDFXB7H.js +39 -0
- package/dist/cli/project-K3QIPGBB.js +12 -0
- package/dist/cli/projectDefinition-DMTZN2NE.js +27 -0
- package/dist/cli/projectRuntime-Z5PJOH47.js +48 -0
- package/dist/cli/registerStandardNodes-S2VLMF6J.js +3081 -0
- package/dist/cli/render-YURAPPUI.js +436 -0
- package/dist/cli/runCommand-PI764LEG.js +198 -0
- package/dist/cli/runner-RA6UKEPD.js +37 -0
- package/dist/cli/runtime/image.ts +277 -0
- package/dist/cli/runtime/stage.ts +74 -11
- package/dist/cli/stageRunner-KUNIMQKJ.js +10 -0
- package/dist/cli/verbs-RCE3K4VQ.js +19 -0
- package/dist/cli/webBuild-GT4PZT5D.js +37 -0
- package/dist/contracts/bundle.d.ts +75 -0
- package/dist/contracts/bundle.js +112 -0
- package/dist/contracts/definition.d.ts +67 -4
- package/dist/contracts/document.d.ts +29 -0
- package/dist/contracts/documentFormat.d.ts +18 -0
- package/dist/contracts/documentFormat.js +195 -0
- package/dist/contracts/editor.d.ts +189 -0
- package/dist/contracts/editor.js +2 -0
- package/dist/contracts/index.d.ts +3 -0
- package/dist/contracts/index.js +3 -0
- package/dist/contracts/validate.js +59 -3
- package/dist/contracts/values.d.ts +3 -3
- package/dist/contracts/values.js +1 -0
- package/dist/image.d.ts +141 -0
- package/dist/image.js +127 -0
- package/dist/index.html +4 -4
- package/dist/player/app.js +17 -5
- package/dist/player/editor.js +1919 -0
- package/dist/player/editorEngine.js +477 -0
- package/dist/runtime/builtins/core/bundle.d.ts +70 -0
- package/dist/runtime/builtins/core/bundle.js +81 -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/routing.d.ts +27 -0
- package/dist/runtime/builtins/core/routing.js +28 -0
- package/dist/runtime/builtins/headless.d.ts +19 -0
- package/dist/runtime/builtins/headless.js +25 -0
- package/dist/runtime/builtins/image/contactSheet.d.ts +226 -0
- package/dist/runtime/builtins/image/contactSheet.js +199 -0
- package/dist/runtime/builtins/image/index.d.ts +9 -0
- package/dist/runtime/builtins/image/index.js +9 -0
- package/dist/runtime/edit/document.d.ts +94 -0
- package/dist/runtime/edit/document.js +194 -0
- package/dist/runtime/edit/graphEditor.d.ts +223 -0
- package/dist/runtime/edit/graphEditor.js +893 -0
- package/dist/runtime/edit/index.d.ts +12 -0
- package/dist/runtime/edit/index.js +6 -0
- package/dist/runtime/edit/layout.d.ts +50 -0
- package/dist/runtime/edit/layout.js +126 -0
- package/dist/runtime/edit/operations.d.ts +65 -0
- package/dist/runtime/edit/operations.js +57 -0
- package/dist/runtime/edit/parameters.d.ts +75 -0
- package/dist/runtime/edit/parameters.js +284 -0
- package/dist/runtime/edit/read.d.ts +35 -0
- package/dist/runtime/edit/read.js +65 -0
- package/dist/runtime/editor/index.d.ts +104 -0
- package/dist/runtime/editor/index.js +439 -0
- package/dist/runtime/expressions/bundlePaths.d.ts +37 -0
- package/dist/runtime/expressions/bundlePaths.js +55 -0
- package/dist/runtime/expressions/index.d.ts +2 -0
- package/dist/runtime/expressions/index.js +1 -0
- package/dist/runtime/index.d.ts +2 -0
- package/dist/runtime/index.js +2 -0
- package/dist/runtime/params/animator.d.ts +5 -0
- package/dist/runtime/params/animator.js +16 -0
- package/dist/runtime/runtime.js +238 -12
- package/dist/runtime/seed.d.ts +29 -0
- package/dist/runtime/seed.js +52 -0
- package/dist/runtime/types.d.ts +12 -0
- package/dist/stage.js +43 -9
- package/dist/studio/panel.d.ts +2 -0
- package/doc/CLI_REFERENCE.md +171 -0
- package/doc/EDITOR_EXPORT.md +235 -0
- package/doc/HEADLESS_GPU.md +27 -0
- package/doc/MCP.md +69 -0
- package/doc/NODE_REFERENCE.md +208 -0
- package/doc/PROJECT_AUTHORING.md +258 -1
- package/package.json +29 -8
- package/dist/assets/CodeEditor-BEk-kIBd.js +0 -623
- package/dist/assets/CodePanel-CeSdjXkg.js +0 -2
- package/dist/assets/ExportDialog-CjTbbTZJ.js +0 -358
- package/dist/assets/ExportDialog-DOq0flMV.css +0 -1
- package/dist/assets/OpenGraphDialog-DCwH9itM.js +0 -1
- package/dist/assets/ProjectSettings-Dg7M5xt9.js +0 -2
- package/dist/assets/SettingsDialog-e7GgsjB9.js +0 -1
- package/dist/assets/definitions-DZpOjWKR.js +0 -1
- package/dist/assets/geo-D2VPjAdt.js +0 -1
- package/dist/assets/index-CMiBdEuT.js +0 -212
- package/dist/assets/index-DMIuml9a.css +0 -1
- package/dist/assets/index-client-BI2H82FJ.js +0 -1
- package/dist/assets/legacy-ZA7textG.js +0 -2
- package/dist/assets/math-B6hpxPJO.js +0 -1
- package/dist/assets/pop-CYUIjT6x.js +0 -1
- package/dist/assets/projectSettingsApi-C1WvIwfP.js +0 -1
|
@@ -19,9 +19,9 @@ export const CAPABILITIES_BY_ENVIRONMENT = {
|
|
|
19
19
|
};
|
|
20
20
|
const ALLOWED = CAPABILITIES_BY_ENVIRONMENT;
|
|
21
21
|
const fields = (names) => new Set(names.split(" "));
|
|
22
|
-
const TOP_FIELDS = fields("apiVersion runsOn container capabilities label description icon inputs outputs props");
|
|
23
|
-
const DATA_FIELDS = fields("kind type default description variadic min max step accept control options");
|
|
24
|
-
const OUTPUT_FIELDS = fields("kind type description");
|
|
22
|
+
const TOP_FIELDS = fields("apiVersion runsOn container capabilities source label description icon inputs outputs props");
|
|
23
|
+
const DATA_FIELDS = fields("kind type default description variadic min max step accept control options schema fields");
|
|
24
|
+
const OUTPUT_FIELDS = fields("kind type description schema fields");
|
|
25
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 };
|
|
@@ -94,6 +94,8 @@ export function validateNodeDefinition(value) {
|
|
|
94
94
|
value.container !== "subnet" &&
|
|
95
95
|
value.container !== "feedback")
|
|
96
96
|
add("invalid-container", "container must be subnet or feedback", "container");
|
|
97
|
+
if (value.source !== undefined && value.source !== "live")
|
|
98
|
+
add("invalid-source", "source must be \"live\" when present", "source");
|
|
97
99
|
const allowed = ALLOWED[environment] ?? [];
|
|
98
100
|
if (value.capabilities !== undefined && !Array.isArray(value.capabilities))
|
|
99
101
|
add("invalid-capabilities", "capabilities must be an array", "capabilities");
|
|
@@ -193,6 +195,8 @@ export function validateNodeDefinition(value) {
|
|
|
193
195
|
add("invalid-type", `${String(type)} is not a Cascade type`, `${path}.type`);
|
|
194
196
|
continue;
|
|
195
197
|
}
|
|
198
|
+
if (section !== "props")
|
|
199
|
+
result.push(...bundleDiagnostics(raw, type, section, path));
|
|
196
200
|
if (type === "texture" && environment !== "browser")
|
|
197
201
|
add("texture-not-portable", "texture is browser-only", `${path}.type`);
|
|
198
202
|
if (type === "texture" && "default" in raw)
|
|
@@ -229,6 +233,58 @@ export function validateNodeDefinition(value) {
|
|
|
229
233
|
}
|
|
230
234
|
return result;
|
|
231
235
|
}
|
|
236
|
+
/** `schema` and `fields` on a port: bundle-only, and well-formed. */
|
|
237
|
+
function bundleDiagnostics(raw, type, section, path) {
|
|
238
|
+
const result = [];
|
|
239
|
+
const add = (code, message, at) => result.push(diagnostic(code, message, at));
|
|
240
|
+
for (const key of ["schema", "fields"])
|
|
241
|
+
if (key in raw && type !== "bundle")
|
|
242
|
+
add("bundle-metadata", `${key} is only valid on a bundle port`, `${path}.${key}`);
|
|
243
|
+
if (type !== "bundle")
|
|
244
|
+
return result;
|
|
245
|
+
if ("schema" in raw && (typeof raw.schema !== "string" || !IDENTIFIER.test(raw.schema)))
|
|
246
|
+
add("invalid-schema", "schema must be an identifier", `${path}.schema`);
|
|
247
|
+
if (!("fields" in raw))
|
|
248
|
+
return result;
|
|
249
|
+
const declared = raw.fields;
|
|
250
|
+
if (section === "inputs") {
|
|
251
|
+
if (!Array.isArray(declared)) {
|
|
252
|
+
add("invalid-fields", "an input's fields list the field names it reads", `${path}.fields`);
|
|
253
|
+
return result;
|
|
254
|
+
}
|
|
255
|
+
const seen = new Set();
|
|
256
|
+
for (const [index, field] of declared.entries()) {
|
|
257
|
+
if (typeof field !== "string" || !IDENTIFIER.test(field))
|
|
258
|
+
add("invalid-field", `${String(field)} is not a valid field name`, `${path}.fields[${index}]`);
|
|
259
|
+
else if (seen.has(field))
|
|
260
|
+
add("duplicate-field", `${field} is listed twice`, `${path}.fields[${index}]`);
|
|
261
|
+
else
|
|
262
|
+
seen.add(field);
|
|
263
|
+
}
|
|
264
|
+
return result;
|
|
265
|
+
}
|
|
266
|
+
if (!isRecord(declared)) {
|
|
267
|
+
add("invalid-fields", "an output's fields map each field name to its type", `${path}.fields`);
|
|
268
|
+
return result;
|
|
269
|
+
}
|
|
270
|
+
for (const [field, definition] of Object.entries(declared)) {
|
|
271
|
+
const at = `${path}.fields.${field}`;
|
|
272
|
+
if (!IDENTIFIER.test(field) || RESERVED.has(field))
|
|
273
|
+
add("invalid-field", `${field} is not a valid field name`, at);
|
|
274
|
+
const fieldType = typeof definition === "string"
|
|
275
|
+
? definition
|
|
276
|
+
: isRecord(definition) ? definition.type : undefined;
|
|
277
|
+
if (isRecord(definition))
|
|
278
|
+
for (const key of Object.keys(definition))
|
|
279
|
+
if (key !== "type" && key !== "description")
|
|
280
|
+
add("unknown-field", `${at}.${key} is not allowed`, `${at}.${key}`);
|
|
281
|
+
if (!validType(fieldType))
|
|
282
|
+
add("invalid-type", `${String(fieldType)} is not a Cascade type`, at);
|
|
283
|
+
else if (fieldType === "texture")
|
|
284
|
+
add("texture-not-portable", "a bundle field cannot hold a texture", at);
|
|
285
|
+
}
|
|
286
|
+
return result;
|
|
287
|
+
}
|
|
232
288
|
export function assertNodeDefinition(value) {
|
|
233
289
|
const diagnostics = validateNodeDefinition(value);
|
|
234
290
|
if (diagnostics.length)
|
|
@@ -2,7 +2,7 @@ import type { Camera } from "./camera.js";
|
|
|
2
2
|
import type { Color } from "./color.js";
|
|
3
3
|
import type { Light, Scene } from "./scene.js";
|
|
4
4
|
import type { Geometry, GeometryFileRef, GeometryJson, GeometrySerialized } from "./geometry.js";
|
|
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
|
+
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", "bundle", "any"];
|
|
6
6
|
export type CoreType = (typeof CORE_TYPES)[number];
|
|
7
7
|
export type NamespacedType = `${string}.${string}`;
|
|
8
8
|
export type CascadeType = CoreType | NamespacedType;
|
|
@@ -96,7 +96,7 @@ export interface AssetRef {
|
|
|
96
96
|
readonly mediaType?: string;
|
|
97
97
|
}
|
|
98
98
|
export type SerializableValue = JsonValue | AssetRef | ImageRef | GeometryJson | GeometryFileRef;
|
|
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;
|
|
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
|
+
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" | "bundle" ? Readonly<Record<string, unknown>> : unknown;
|
|
100
|
+
export type SerializableValueForType<T extends CascadeType> = T extends "texture" ? never : T extends "geometry" ? GeometrySerialized : T extends "array" ? readonly JsonValue[] : T extends "object" | "bundle" ? Readonly<Record<string, JsonValue>> : T extends "any" | NamespacedType ? JsonValue : ValueForType<T>;
|
|
101
101
|
export {};
|
|
102
102
|
//# sourceMappingURL=values.d.ts.map
|
package/dist/contracts/values.js
CHANGED
package/dist/image.d.ts
ADDED
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `cascade/image` — the one image API a node body uses, in every host.
|
|
3
|
+
*
|
|
4
|
+
* A node that draws used to have two honest choices and one dishonest one.
|
|
5
|
+
* `new OffscreenCanvas` worked in Studio and under `cascade run` (the CLI
|
|
6
|
+
* installs Skia's canvas as that global), but the bytes in and out went
|
|
7
|
+
* through `cascade/io`'s project paths, and fonts had no portable door at all.
|
|
8
|
+
* So the nodes that needed fonts or files reached for `@napi-rs/canvas` and
|
|
9
|
+
* `node:fs` directly, declared `runsOn: 'server'`, and could never be exported
|
|
10
|
+
* as a browser editor — the commons plotter-social job was blocked on exactly
|
|
11
|
+
* that ("Cascade has no browser image contract").
|
|
12
|
+
*
|
|
13
|
+
* The contract, in full:
|
|
14
|
+
*
|
|
15
|
+
* - **Surface.** `createCanvas(w, h)` is an `OffscreenCanvas`: the browser's
|
|
16
|
+
* own in a page or a worker, Skia's (`@napi-rs/canvas`, installed by the
|
|
17
|
+
* headless host) under `cascade run`/`render`/MCP. `context2d(canvas)` asks
|
|
18
|
+
* for a CPU-backed 2D context (`willReadFrequently`), which is what keeps
|
|
19
|
+
* Chrome on the same software Skia rasteriser the headless host uses, and
|
|
20
|
+
* is the right hint anyway for nodes that read pixels back.
|
|
21
|
+
* - **Pixels in.** `readImage(context, ref)` reads an `ImageRef`, `AssetRef`
|
|
22
|
+
* or project path through the node's declared `assets` capability and
|
|
23
|
+
* decodes it with `decodeImage` (`createImageBitmap` in both hosts).
|
|
24
|
+
* - **Pixels out.** `writeImage(context, canvas, name)` encodes PNG through
|
|
25
|
+
* `encodeImage` and stores it with `assets.write`, returning the `ImageRef`
|
|
26
|
+
* to set on an `image` output. The host decides where it lands
|
|
27
|
+
* (`.cascade-cache/<name>.<hash>.png` headless, an in-memory generated
|
|
28
|
+
* asset in an exported editor), so a node never names a path.
|
|
29
|
+
* - **Fonts.** `registerFont(family, bytes)` makes a face drawable by
|
|
30
|
+
* `ctx.font` in this host: `FontFace` added to the page's or worker's font
|
|
31
|
+
* set in a browser, Skia's `GlobalFonts` headless. Bytes come from the same
|
|
32
|
+
* `assets` capability, so a font is a project file like any other.
|
|
33
|
+
* - **Refusal, not fallback.** No canvas in this host, or no `assets`
|
|
34
|
+
* capability declared, is an error with one sentence saying which, never a
|
|
35
|
+
* silent blank image.
|
|
36
|
+
*
|
|
37
|
+
* A node using it declares `runsOn: 'portable'` and `capabilities: ['assets']`
|
|
38
|
+
* and imports nothing host-specific (`node:*`, `@napi-rs/canvas`, `document`).
|
|
39
|
+
* Then the same body cooks in Studio, in `cascade run`, and in an exported
|
|
40
|
+
* editor's worker.
|
|
41
|
+
*
|
|
42
|
+
* The headless host installs its font door under a global key, like
|
|
43
|
+
* `cascade/io`'s bridge, because the node's copy of this module and the CLI's
|
|
44
|
+
* copy are not always the same module instance.
|
|
45
|
+
*/
|
|
46
|
+
/** An image port value. Mirrors Cascade's core `ImageRef`. */
|
|
47
|
+
export interface ImageRef {
|
|
48
|
+
path: string;
|
|
49
|
+
size: [number, number];
|
|
50
|
+
channels: 'r' | 'a' | 'rg' | 'rgb' | 'rgba';
|
|
51
|
+
depth: 'u8' | 'u16' | 'f16' | 'f32';
|
|
52
|
+
space: 'srgb' | 'linear';
|
|
53
|
+
}
|
|
54
|
+
/** Anything naming stored bytes: an `ImageRef`, an `AssetRef`, or a project path. */
|
|
55
|
+
export type ImageSourceRef = string | {
|
|
56
|
+
readonly path: string;
|
|
57
|
+
};
|
|
58
|
+
/** The part of the `assets` capability these helpers use. */
|
|
59
|
+
export interface ImageAssets {
|
|
60
|
+
read(ref: {
|
|
61
|
+
path: string;
|
|
62
|
+
}, options: {
|
|
63
|
+
signal: unknown;
|
|
64
|
+
}): Promise<Uint8Array>;
|
|
65
|
+
write(data: Uint8Array, metadata: {
|
|
66
|
+
mediaType?: string;
|
|
67
|
+
suggestedName?: string;
|
|
68
|
+
}, options: {
|
|
69
|
+
signal: unknown;
|
|
70
|
+
}): Promise<{
|
|
71
|
+
path: string;
|
|
72
|
+
mediaType?: string;
|
|
73
|
+
}>;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* The slice of a definition-v1 `NodeExecutionContext` these helpers need, so
|
|
77
|
+
* a node passes its own `context`. Typed loosely on purpose: a node that did
|
|
78
|
+
* not declare `assets` still type-checks here and gets a sentence at run time
|
|
79
|
+
* naming the missing declaration rather than a type error about generics.
|
|
80
|
+
*/
|
|
81
|
+
export interface ImageNodeContext {
|
|
82
|
+
readonly capabilities: object;
|
|
83
|
+
readonly signal?: unknown;
|
|
84
|
+
}
|
|
85
|
+
export type ImageType = 'image/png' | 'image/webp' | 'image/jpeg';
|
|
86
|
+
/** What a host gives `registerFont`. A browser realm needs none. */
|
|
87
|
+
export interface ImageHost {
|
|
88
|
+
/** For a `--verbose` line and for errors. */
|
|
89
|
+
readonly renderer: string;
|
|
90
|
+
registerFont(family: string, bytes: Uint8Array, descriptors: FontDescriptors): void | Promise<void>;
|
|
91
|
+
}
|
|
92
|
+
export interface FontDescriptors {
|
|
93
|
+
/** CSS weight, `'400'`, `'bold'`. Headless reads the weight from the file. */
|
|
94
|
+
weight?: string;
|
|
95
|
+
/** `'normal'` or `'italic'`. Headless reads the style from the file. */
|
|
96
|
+
style?: string;
|
|
97
|
+
}
|
|
98
|
+
/** Install the headless host's font door. Returns the disposer. */
|
|
99
|
+
export declare function installImageHost(host: ImageHost | null): () => void;
|
|
100
|
+
/** Whether this host can draw. Never throws. */
|
|
101
|
+
export declare function canvasAvailable(): boolean;
|
|
102
|
+
/** A drawing surface of whole pixels, at least 1x1. */
|
|
103
|
+
export declare function createCanvas(width: number, height: number): OffscreenCanvas;
|
|
104
|
+
/**
|
|
105
|
+
* The canvas's 2D context, CPU-backed. `willReadFrequently` keeps a browser
|
|
106
|
+
* off the GPU rasteriser, so a node draws with the same software Skia in Chrome
|
|
107
|
+
* as headless, and `getImageData` does not stall on a readback.
|
|
108
|
+
*/
|
|
109
|
+
export declare function context2d(canvas: OffscreenCanvas): OffscreenCanvasRenderingContext2D;
|
|
110
|
+
/** `createCanvas` and `context2d` in one: `const { canvas, context } = surface(w, h)`. */
|
|
111
|
+
export declare function surface(width: number, height: number): {
|
|
112
|
+
canvas: OffscreenCanvas;
|
|
113
|
+
context: OffscreenCanvasRenderingContext2D;
|
|
114
|
+
};
|
|
115
|
+
/** Encoded bytes (PNG, JPEG, WebP) to something `drawImage` accepts. */
|
|
116
|
+
export declare function decodeImage(bytes: Uint8Array | ArrayBuffer | Blob): Promise<ImageBitmap>;
|
|
117
|
+
/** A canvas's pixels as encoded bytes, PNG unless asked otherwise. */
|
|
118
|
+
export declare function encodeImage(canvas: OffscreenCanvas, options?: {
|
|
119
|
+
type?: ImageType;
|
|
120
|
+
quality?: number;
|
|
121
|
+
}): Promise<Uint8Array>;
|
|
122
|
+
/**
|
|
123
|
+
* Make `family` drawable by `ctx.font` here. Idempotent per family and bytes.
|
|
124
|
+
* A browser realm (page or worker) uses `FontFace`; the headless host
|
|
125
|
+
* registers with Skia. A host with neither refuses.
|
|
126
|
+
*/
|
|
127
|
+
export declare function registerFont(family: string, bytes: Uint8Array | ArrayBuffer, descriptors?: FontDescriptors): Promise<void>;
|
|
128
|
+
/** A stored file's bytes through the node's `assets` capability: an image, a font, anything. */
|
|
129
|
+
export declare function readBytes(context: ImageNodeContext, ref: ImageSourceRef): Promise<Uint8Array>;
|
|
130
|
+
/** Read and decode an image input, an asset, or a project file. */
|
|
131
|
+
export declare function readImage(context: ImageNodeContext, ref: ImageSourceRef): Promise<ImageBitmap>;
|
|
132
|
+
/**
|
|
133
|
+
* Encode a canvas and store it; set the result on an `image` output. `name`
|
|
134
|
+
* is a suggestion (`context.nodeId + '.png'` keeps instances apart); the host
|
|
135
|
+
* picks the stored path.
|
|
136
|
+
*/
|
|
137
|
+
export declare function writeImage(context: ImageNodeContext, canvas: OffscreenCanvas, name: string, options?: {
|
|
138
|
+
type?: ImageType;
|
|
139
|
+
quality?: number;
|
|
140
|
+
space?: 'srgb' | 'linear';
|
|
141
|
+
}): Promise<ImageRef>;
|
package/dist/image.js
ADDED
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
// server/src/runtime/image.ts
|
|
2
|
+
var HOST_KEY = "__cascadeImageHost";
|
|
3
|
+
var NO_CANVAS = 'cascade/image: this host has no canvas. In a browser this needs OffscreenCanvas; headless, Cascade installs @napi-rs/canvas before a run (install it with "npm install @napi-rs/canvas" if it is missing).';
|
|
4
|
+
function installImageHost(host) {
|
|
5
|
+
const globals = globalThis;
|
|
6
|
+
const previous = globals[HOST_KEY];
|
|
7
|
+
globals[HOST_KEY] = host ?? void 0;
|
|
8
|
+
return () => {
|
|
9
|
+
globals[HOST_KEY] = previous;
|
|
10
|
+
};
|
|
11
|
+
}
|
|
12
|
+
function installedHost() {
|
|
13
|
+
const host = globalThis[HOST_KEY];
|
|
14
|
+
return host && typeof host.registerFont === "function" ? host : null;
|
|
15
|
+
}
|
|
16
|
+
function canvasConstructor() {
|
|
17
|
+
return globalThis.OffscreenCanvas;
|
|
18
|
+
}
|
|
19
|
+
function canvasAvailable() {
|
|
20
|
+
return typeof canvasConstructor() === "function";
|
|
21
|
+
}
|
|
22
|
+
function createCanvas(width, height) {
|
|
23
|
+
const Canvas = canvasConstructor();
|
|
24
|
+
if (!Canvas) throw new Error(NO_CANVAS);
|
|
25
|
+
return new Canvas(Math.max(1, Math.round(width)), Math.max(1, Math.round(height)));
|
|
26
|
+
}
|
|
27
|
+
function context2d(canvas) {
|
|
28
|
+
const context = canvas.getContext("2d", { willReadFrequently: true });
|
|
29
|
+
if (!context) throw new Error("cascade/image: the canvas has no 2D context");
|
|
30
|
+
return context;
|
|
31
|
+
}
|
|
32
|
+
function surface(width, height) {
|
|
33
|
+
const canvas = createCanvas(width, height);
|
|
34
|
+
return { canvas, context: context2d(canvas) };
|
|
35
|
+
}
|
|
36
|
+
async function decodeImage(bytes) {
|
|
37
|
+
const create = globalThis.createImageBitmap;
|
|
38
|
+
if (typeof create !== "function") throw new Error(NO_CANVAS);
|
|
39
|
+
const blob = bytes instanceof Blob ? bytes : new Blob([bytes]);
|
|
40
|
+
const image = await create(blob);
|
|
41
|
+
if (!image?.width || !image?.height) throw new Error("cascade/image: could not decode the image data");
|
|
42
|
+
return image;
|
|
43
|
+
}
|
|
44
|
+
async function encodeImage(canvas, options = {}) {
|
|
45
|
+
const type = options.type ?? "image/png";
|
|
46
|
+
if (typeof canvas.convertToBlob !== "function") throw new Error("cascade/image: this canvas cannot encode (no convertToBlob)");
|
|
47
|
+
const blob = await canvas.convertToBlob({ type, ...options.quality === void 0 ? {} : { quality: options.quality } });
|
|
48
|
+
return new Uint8Array(await blob.arrayBuffer());
|
|
49
|
+
}
|
|
50
|
+
var registered = /* @__PURE__ */ new Map();
|
|
51
|
+
function fingerprint(bytes) {
|
|
52
|
+
let hash = 2166136261;
|
|
53
|
+
for (let index = 0; index < bytes.length; index += 1) {
|
|
54
|
+
hash ^= bytes[index];
|
|
55
|
+
hash = Math.imul(hash, 16777619);
|
|
56
|
+
}
|
|
57
|
+
return `${bytes.length}:${(hash >>> 0).toString(16)}`;
|
|
58
|
+
}
|
|
59
|
+
async function registerFont(family, bytes, descriptors = {}) {
|
|
60
|
+
if (!family.trim()) throw new Error("cascade/image: registerFont needs a family name");
|
|
61
|
+
const data = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
|
|
62
|
+
if (!data.byteLength) throw new Error(`cascade/image: the font for ${family} is empty`);
|
|
63
|
+
const host = installedHost();
|
|
64
|
+
const globals = globalThis;
|
|
65
|
+
const fonts = host ? null : globals.fonts ?? globals.document?.fonts;
|
|
66
|
+
const realm = host ? `headless:${host.renderer}` : fonts ?? globalThis;
|
|
67
|
+
const key = `${family}\0${descriptors.weight ?? ""}\0${descriptors.style ?? ""}\0${fingerprint(data)}`;
|
|
68
|
+
const seen = registered.get(realm) ?? /* @__PURE__ */ new Set();
|
|
69
|
+
if (seen.has(key)) return;
|
|
70
|
+
if (host) {
|
|
71
|
+
await host.registerFont(family, data, descriptors);
|
|
72
|
+
} else if (typeof globals.FontFace === "function" && fonts) {
|
|
73
|
+
const face = new globals.FontFace(family, data.slice().buffer, descriptors);
|
|
74
|
+
fonts.add(await face.load());
|
|
75
|
+
} else {
|
|
76
|
+
throw new Error("cascade/image: this host cannot register fonts (no FontFace and no headless canvas host)");
|
|
77
|
+
}
|
|
78
|
+
seen.add(key);
|
|
79
|
+
registered.set(realm, seen);
|
|
80
|
+
}
|
|
81
|
+
function assetsOf(context) {
|
|
82
|
+
const assets = context.capabilities.assets;
|
|
83
|
+
if (!assets || typeof assets.read !== "function" || typeof assets.write !== "function") {
|
|
84
|
+
throw new Error("cascade/image: this node reads or writes images, so its definition must declare capabilities: ['assets']");
|
|
85
|
+
}
|
|
86
|
+
return assets;
|
|
87
|
+
}
|
|
88
|
+
function refOf(ref) {
|
|
89
|
+
const path = typeof ref === "string" ? ref : ref?.path;
|
|
90
|
+
if (typeof path !== "string" || !path) throw new Error("cascade/image: no image to read (an empty path or an unset image input)");
|
|
91
|
+
return { path };
|
|
92
|
+
}
|
|
93
|
+
var NEVER = { aborted: false, addEventListener() {
|
|
94
|
+
}, removeEventListener() {
|
|
95
|
+
} };
|
|
96
|
+
async function readBytes(context, ref) {
|
|
97
|
+
return assetsOf(context).read(refOf(ref), { signal: context.signal ?? NEVER });
|
|
98
|
+
}
|
|
99
|
+
async function readImage(context, ref) {
|
|
100
|
+
return decodeImage(await readBytes(context, ref));
|
|
101
|
+
}
|
|
102
|
+
async function writeImage(context, canvas, name, options = {}) {
|
|
103
|
+
const type = options.type ?? "image/png";
|
|
104
|
+
const bytes = await encodeImage(canvas, { type, ...options.quality === void 0 ? {} : { quality: options.quality } });
|
|
105
|
+
const stored = await assetsOf(context).write(bytes, { mediaType: type, suggestedName: name }, { signal: context.signal ?? NEVER });
|
|
106
|
+
return {
|
|
107
|
+
path: stored.path,
|
|
108
|
+
size: [canvas.width, canvas.height],
|
|
109
|
+
channels: "rgba",
|
|
110
|
+
depth: "u8",
|
|
111
|
+
space: options.space ?? "srgb"
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
export {
|
|
115
|
+
canvasAvailable,
|
|
116
|
+
context2d,
|
|
117
|
+
createCanvas,
|
|
118
|
+
decodeImage,
|
|
119
|
+
encodeImage,
|
|
120
|
+
installImageHost,
|
|
121
|
+
readBytes,
|
|
122
|
+
readImage,
|
|
123
|
+
registerFont,
|
|
124
|
+
surface,
|
|
125
|
+
writeImage
|
|
126
|
+
};
|
|
127
|
+
//# sourceMappingURL=image.js.map
|
package/dist/index.html
CHANGED
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
<!-- Pinterest browser extension: no Save buttons over canvases and previews -->
|
|
7
7
|
<meta name="pinterest" content="nopin">
|
|
8
8
|
<title>Cascade</title>
|
|
9
|
-
<script type="module" crossorigin src="/assets/index-
|
|
10
|
-
<link rel="modulepreload" crossorigin href="/assets/legacy-
|
|
11
|
-
<link rel="modulepreload" crossorigin href="/assets/index-client-
|
|
9
|
+
<script type="module" crossorigin src="/assets/index-5ZtDAuy2.js"></script>
|
|
10
|
+
<link rel="modulepreload" crossorigin href="/assets/legacy-CTEeXeqF.js">
|
|
11
|
+
<link rel="modulepreload" crossorigin href="/assets/index-client-DqYmKstU.js">
|
|
12
12
|
<link rel="modulepreload" crossorigin href="/assets/nodeTypeUtils-BDJsc2Ks.js">
|
|
13
|
-
<link rel="stylesheet" crossorigin href="/assets/index-
|
|
13
|
+
<link rel="stylesheet" crossorigin href="/assets/index-7vae6YFC.css">
|
|
14
14
|
</head>
|
|
15
15
|
<body>
|
|
16
16
|
<div id="app"></div>
|
package/dist/player/app.js
CHANGED
|
@@ -474,7 +474,7 @@ var BrowserPlayer = class {
|
|
|
474
474
|
}
|
|
475
475
|
if (typeof value === "string") {
|
|
476
476
|
return new Blob([value], {
|
|
477
|
-
type: value
|
|
477
|
+
type: isSvgText(value) ? "image/svg+xml" : "text/plain;charset=utf-8"
|
|
478
478
|
});
|
|
479
479
|
}
|
|
480
480
|
if ((isGeometry(value) || isScene(value)) && this.displayedElement instanceof HTMLCanvasElement)
|
|
@@ -572,7 +572,7 @@ var BrowserPlayer = class {
|
|
|
572
572
|
let next;
|
|
573
573
|
let nextRelease;
|
|
574
574
|
const asset = isAssetRef(value) ? value : void 0;
|
|
575
|
-
if (asset ||
|
|
575
|
+
if (asset || isSvgText(value)) {
|
|
576
576
|
let url;
|
|
577
577
|
if (asset) {
|
|
578
578
|
const lease = await this.context.assets.resolveUrl(asset);
|
|
@@ -692,13 +692,13 @@ function createUi(hostDocument, root) {
|
|
|
692
692
|
root.append(toolbar, error, output);
|
|
693
693
|
return { play, frame, error, output };
|
|
694
694
|
}
|
|
695
|
-
function renderSceneCanvas(hostDocument, scene, width, height) {
|
|
695
|
+
function renderSceneCanvas(hostDocument, scene, width, height, stroke = [1, 1, 1, 1]) {
|
|
696
696
|
let canvas;
|
|
697
697
|
const camera = scene.camera ?? fitCamera(scene, width, height);
|
|
698
698
|
renderScene(scene.geometry, {
|
|
699
699
|
camera,
|
|
700
700
|
size: [width, height],
|
|
701
|
-
stroke: [
|
|
701
|
+
stroke: [...stroke],
|
|
702
702
|
strokeWidth: 1,
|
|
703
703
|
opacity: 1,
|
|
704
704
|
pointRadius: 1,
|
|
@@ -753,7 +753,7 @@ function preferredOutput(result) {
|
|
|
753
753
|
for (const [nodeId, outputs] of result.outputs) {
|
|
754
754
|
for (const [port, value] of outputs) {
|
|
755
755
|
fallback = { nodeId, port };
|
|
756
|
-
if (isAssetRef(value) || isScene(value) || isGeometry(value) ||
|
|
756
|
+
if (isAssetRef(value) || isScene(value) || isGeometry(value) || isSvgText(value))
|
|
757
757
|
preferred = { nodeId, port };
|
|
758
758
|
}
|
|
759
759
|
}
|
|
@@ -805,6 +805,18 @@ function requireFinite(value, label) {
|
|
|
805
805
|
function asError(value) {
|
|
806
806
|
return value instanceof Error ? value : new Error(String(value));
|
|
807
807
|
}
|
|
808
|
+
function isSvgText(value) {
|
|
809
|
+
return typeof value === "string" && /^\s*(?:<\?xml[^>]*>\s*)?(?:<!--[\s\S]*?-->\s*|<!DOCTYPE[^>]*>\s*)*<svg[\s>]/i.test(value.slice(0, 2e3));
|
|
810
|
+
}
|
|
808
811
|
export {
|
|
812
|
+
asError,
|
|
813
|
+
canvasBlob,
|
|
814
|
+
cleanupAll,
|
|
815
|
+
isAssetRef,
|
|
816
|
+
isGeometry,
|
|
817
|
+
isImageAsset,
|
|
818
|
+
isSvgText,
|
|
819
|
+
jsonReplacer,
|
|
820
|
+
renderSceneCanvas,
|
|
809
821
|
startPlayer
|
|
810
822
|
};
|