feat: replace mesh queries with typed predicates
Amp-Thread-ID: https://ampcode.com/threads/T-019f9d91-77c1-7206-a60f-ed6554ce92ab Co-authored-by: Heaust Azure <heaust.azure@gmail.com>
This commit is contained in:
+1
-1
@@ -5,7 +5,7 @@ export class DerivedBvh {
|
||||
let changed = n !== this.count;
|
||||
if (!changed) for (let i = 0; i < n; i++) if (this.identity[i * 2] !== s.instanceSlot[i] || this.identity[i * 2 + 1] !== s.instanceGeneration[i] || this.meshIdentity[i * 2] !== s.instanceMeshSlot[i] || this.meshIdentity[i * 2 + 1] !== s.instanceMeshGeneration[i]) { changed = true; break; }
|
||||
this.count = n; this.identity = new Uint32Array(n * 2); this.meshIdentity = new Uint32Array(n * 2); this.pickable = new Uint8Array(n); this.bounds = new Float32Array(n * 6);
|
||||
for (let i = 0; i < n; i++) { this.identity.set([s.instanceSlot[i], s.instanceGeneration[i]], i * 2); this.meshIdentity.set([s.instanceMeshSlot[i], s.instanceMeshGeneration[i]], i * 2); this.pickable[i] = !!s.instancePickable[i]; this.bounds.set(s.instanceWorldMin.subarray(i * 3, i * 3 + 3), i * 6); this.bounds.set(s.instanceWorldMax.subarray(i * 3, i * 3 + 3), i * 6 + 3); }
|
||||
for (let i = 0; i < n; i++) { this.identity.set([s.instanceSlot[i], s.instanceGeneration[i]], i * 2); this.meshIdentity.set([s.instanceMeshSlot[i], s.instanceMeshGeneration[i]], i * 2); this.pickable[i] = !!(s.instanceType[i * 16] & 1); this.bounds.set(s.instanceWorldMin.subarray(i * 3, i * 3 + 3), i * 6); this.bounds.set(s.instanceWorldMax.subarray(i * 3, i * 3 + 3), i * 6 + 3); }
|
||||
changed ? this.rebuild() : this.refit();
|
||||
}
|
||||
rebuild() {
|
||||
|
||||
@@ -19,7 +19,7 @@ function coalescedUpdate(hint = 0) {
|
||||
addEventListener("message", event => {
|
||||
const m = event.data;
|
||||
try {
|
||||
if (m.type === "init") { reader = new SnapshotReader(m.memory, m.controlPtr); if (m.controlVersion !== 1 || m.schemaVersion !== 1) throw Object.assign(new Error("version"), {code: "PICK_PROTOCOL_MISMATCH"}); postMessage({type: "ready"}); }
|
||||
if (m.type === "init") { reader = new SnapshotReader(m.memory, m.controlPtr); if (m.controlVersion !== 1 || m.schemaVersion !== 2) throw Object.assign(new Error("version"), {code: "PICK_PROTOCOL_MISMATCH"}); postMessage({type: "ready"}); }
|
||||
else if (m.type === "update") coalescedUpdate(m.epoch);
|
||||
else if (m.type === "pick") { if (!ensureEpoch(m.epoch)) { postMessage({type: "pick", request: m.request, stale: true, epoch}); return; } const hits = bvh.pick(m.origin, m.direction, m.maxDistance, m.maxHits); const latest = reader.latest().epoch; postMessage({type: "pick", request: m.request, stale: latest !== m.epoch || epoch !== m.epoch, epoch, hits}); }
|
||||
else if (m.type === "dispose") close();
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
export const SNAPSHOT = Object.freeze({
|
||||
MAGIC: 0x504e5359, BLOB_MAGIC: 0x31534452, VERSION: 1, BYTES: 256,
|
||||
SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 1, INIT: 0, OPEN: 1, FAILED: 2,
|
||||
MAGIC: 0x504e5359, BLOB_MAGIC: 0x32534452, VERSION: 1, BYTES: 256,
|
||||
SLOTS: 3, SLOT_BYTES: 64, SCHEMA: 2, INIT: 0, OPEN: 1, FAILED: 2,
|
||||
CLOSED: 3, FREE: 0, WRITING: 1, READY: 2, READING: 3,
|
||||
});
|
||||
export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshFlags", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceFlags", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instancePickable"];
|
||||
const COMPONENTS = [1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 16, 3, 3, 1];
|
||||
const SCALARS = [1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
export const STREAM_NAMES = ["meshSlot", "meshGeneration", "meshLocalMin", "meshLocalMax", "instanceSlot", "instanceGeneration", "instanceMeshSlot", "instanceMeshGeneration", "instanceModel", "instanceWorldMin", "instanceWorldMax", "instanceType"];
|
||||
const COMPONENTS = [1, 1, 3, 3, 1, 1, 1, 1, 16, 3, 3, 16];
|
||||
const SCALARS = [1, 1, 2, 2, 1, 1, 1, 1, 2, 2, 2, 1];
|
||||
const STRIDES = COMPONENTS.map(n => n * 4);
|
||||
|
||||
export class SnapshotProtocolError extends Error {
|
||||
@@ -70,15 +70,15 @@ export class SnapshotReader {
|
||||
if (slot[0] !== SNAPSHOT.READING || slot[1] !== latest.epoch || slot[2] !== latest.layoutEpoch || slot[5] !== latest.revisionLo || slot[6] !== latest.revisionHi || slot[9] !== SNAPSHOT.SCHEMA || slot[10] !== 64) return null;
|
||||
if (slot.slice(11).some(Boolean)) bad("BAD_SLOT_RESERVED");
|
||||
const ptr = slot[3], bytes = slot[4];
|
||||
if (ptr % 16 || bytes < 512 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT");
|
||||
if (ptr % 16 || bytes < 448 || bytes % 16 || add(ptr, bytes) > this.buffer.byteLength) bad("BAD_SLOT");
|
||||
const u32 = new Uint32Array(this.buffer, ptr, bytes / 4);
|
||||
if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 14 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB");
|
||||
if (u32[0] !== SNAPSHOT.BLOB_MAGIC || u32[1] !== SNAPSHOT.SCHEMA || u32[2] !== 64 || u32[3] !== bytes || u32[4] !== slot[1] || u32[5] !== slot[5] || u32[6] !== slot[6] || u32[7] !== 12 || u32[8] !== 64 || u32[9] !== 32 || u32[10] !== slot[7] || u32[11] !== slot[8] || u32[12] !== 0x01020304 || u32[13] !== 3) bad("BAD_BLOB");
|
||||
if (u32[14] || u32[15]) bad("BAD_BLOB_RESERVED");
|
||||
const ranges = [], streams = {};
|
||||
for (let i = 0; i < 14; i++) {
|
||||
for (let i = 0; i < 12; i++) {
|
||||
const d = 16 + i * 8, semantic = u32[d], scalar = u32[d + 1], offset = u32[d + 2], count = u32[d + 3], components = u32[d + 4], stride = u32[d + 5], width = u32[d + 6], reserved = u32[d + 7];
|
||||
const want = i < 5 ? slot[7] : slot[8];
|
||||
if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 512 || offset % 16) bad("BAD_DESCRIPTOR");
|
||||
const want = i < 4 ? slot[7] : slot[8];
|
||||
if (semantic !== i + 1 || scalar !== SCALARS[i] || count !== want || components !== COMPONENTS[i] || stride !== STRIDES[i] || width !== 4 || reserved || offset < 448 || offset % 16) bad("BAD_DESCRIPTOR");
|
||||
const end = add(offset, mul(stride, count));
|
||||
if (end > bytes) bad("BAD_DESCRIPTOR_RANGE");
|
||||
if (count) ranges.push([offset, end]);
|
||||
|
||||
@@ -94,7 +94,7 @@ const mapValuePaths = (paths, path, source, value) => {
|
||||
mapValuePaths(paths, `${path}.${key}`, source, value[key]);
|
||||
};
|
||||
|
||||
function parameterValue(raw, schema, nodeId, key) {
|
||||
function parameterValue(raw, schema, nodeId, key, semanticType) {
|
||||
if (!exactKeys(raw, ["kind", "value"]) || raw.kind !== schema.type)
|
||||
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
||||
const value = raw.value;
|
||||
@@ -112,13 +112,33 @@ function parameterValue(raw, schema, nodeId, key) {
|
||||
? typeof value === "boolean"
|
||||
: schema.type === "vector" || schema.type === "color"
|
||||
? Array.isArray(value) &&
|
||||
value.length === (schema.type === "vector" ? 3 : 4) &&
|
||||
value.length === (semanticType?.startsWith("vec") ? Number(semanticType.at(-1)) : (schema.type === "vector" ? 3 : 4)) &&
|
||||
value.every(bounded)
|
||||
: schema.type === "json" && finiteJson(value);
|
||||
: schema.type === "json" && finiteJson(value) && validSemanticValue(value, semanticType);
|
||||
if (!valid) fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
|
||||
return canonical(structuredClone(raw.value));
|
||||
}
|
||||
|
||||
function validSemanticValue(value, type) {
|
||||
if (!type) return true;
|
||||
const finiteVector = (candidate, size) =>
|
||||
Array.isArray(candidate) && candidate.length === size && candidate.every(Number.isFinite);
|
||||
const vector = /^vec([24])$/.exec(type);
|
||||
if (vector) return finiteVector(value, Number(vector[1]));
|
||||
if (type === "u32x16")
|
||||
return Array.isArray(value) && value.length === 16 &&
|
||||
value.every((word) => Number.isInteger(word) && word >= 0 && word <= 0xffffffff);
|
||||
if (type === "local_aabb")
|
||||
return exactKeys(value, ["min", "max"]) && finiteVector(value.min, 3) && finiteVector(value.max, 3);
|
||||
const match = /^mat([234])$/.exec(type);
|
||||
if (match) {
|
||||
const size = Number(match[1]);
|
||||
return Array.isArray(value) && value.length === size &&
|
||||
value.every((column) => finiteVector(column, size));
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
try {
|
||||
const rootKeys = [
|
||||
@@ -300,6 +320,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
socketDefinition.value,
|
||||
n.id,
|
||||
s.key,
|
||||
input.accepted.types[0],
|
||||
);
|
||||
return false;
|
||||
} catch {
|
||||
@@ -327,13 +348,16 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
}
|
||||
if (new Set(n.sockets.map((s) => s.key)).size !== expected.length)
|
||||
fail("AUTHORING_SOCKET_SET", { nodeId: n.id });
|
||||
if (n.typeId === "mesh_query") {
|
||||
parameters.visibleDefault = sockets.get(
|
||||
`${n.id}:isVisible`,
|
||||
).defaultValue.value;
|
||||
parameters.frustumCulledDefault = sockets.get(
|
||||
`${n.id}:isFrustumCulled`,
|
||||
).defaultValue.value;
|
||||
for (const key of Object.keys(descriptor.inputs)) {
|
||||
const authoredDefault = sockets.get(`${n.id}:${key}`).defaultValue;
|
||||
if (authoredDefault)
|
||||
parameters[`${key}Default`] = parameterValue(
|
||||
authoredDefault,
|
||||
definition.sockets[key].value,
|
||||
n.id,
|
||||
key,
|
||||
descriptor.inputs[key].accepted.types[0],
|
||||
);
|
||||
}
|
||||
nodes.set(n.id, {
|
||||
ordinal,
|
||||
@@ -507,13 +531,12 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
|
||||
mapValuePaths(
|
||||
paths,
|
||||
`${base}.parameters.${key}`,
|
||||
item.value.executor.key === "mesh_query" && key.endsWith("Default")
|
||||
key.endsWith("Default") && Object.hasOwn(descriptors[item.value.executor.key].inputs, key.slice(0, -7))
|
||||
? {
|
||||
kind: "input",
|
||||
nodeId: item.value.id,
|
||||
input:
|
||||
key === "visibleDefault" ? "isVisible" : "isFrustumCulled",
|
||||
socketId: `${item.value.id}:${key === "visibleDefault" ? "isVisible" : "isFrustumCulled"}`,
|
||||
input: key.slice(0, -7),
|
||||
socketId: `${item.value.id}:${key.slice(0, -7)}`,
|
||||
unconnected: true,
|
||||
}
|
||||
: parameterSource(
|
||||
|
||||
@@ -2,13 +2,14 @@ import { semanticCatalog } from "./catalog.js";
|
||||
|
||||
const GROUPS = Object.freeze([
|
||||
["source", "Source"],
|
||||
["expression", "Expression"],
|
||||
["compute", "Compute"],
|
||||
["cpu_preparation", "CPU preparation"],
|
||||
["render", "Render / post"],
|
||||
["frame", "Frame"],
|
||||
]);
|
||||
|
||||
const title = (typeId) => typeId.replaceAll("_", " ");
|
||||
const title = (typeId) => typeId.replaceAll("_", " ").replace(/\b\w/g, (letter) => letter.toUpperCase());
|
||||
|
||||
/** Application-owned, immutable add-node catalog model. */
|
||||
export const addNodeItems = Object.freeze(
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
export const GRAPH_ID = "authored_gpu_culling";
|
||||
export const CATALOG_VERSION = 7;
|
||||
export const CATALOG_VERSION = 8;
|
||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||
const i = (type, required = true, authoringType) => ({
|
||||
accepted: typeof type === "string" ? exact(type) : type,
|
||||
@@ -7,6 +7,45 @@ const i = (type, required = true, authoringType) => ({
|
||||
...(authoringType ? { authoringType } : {}),
|
||||
});
|
||||
const o = (type) => ({ type });
|
||||
const expression = (inputs, outputs) => ({
|
||||
version: 1,
|
||||
execution: "expression",
|
||||
inputs: Object.fromEntries(Object.entries(inputs).map(([name, type]) => [name, i(type, false)])),
|
||||
outputs: Object.fromEntries(Object.entries(outputs).map(([name, type]) => [name, o(type)])),
|
||||
parameters: {},
|
||||
});
|
||||
const numbered = (prefix, count, type) =>
|
||||
Object.fromEntries(Array.from({ length: count }, (_, index) => [`${prefix}${index}`, type]));
|
||||
const expressionCatalog = {
|
||||
and: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
or: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
not: expression({ operand: "bool" }, { value: "bool" }),
|
||||
xor: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
xnor: expression({ left: "bool", right: "bool" }, { value: "bool" }),
|
||||
greater_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
|
||||
less_than_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
|
||||
equals_f32: expression({ left: "f32", right: "f32" }, { value: "bool" }),
|
||||
greater_than_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }),
|
||||
less_than_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }),
|
||||
equals_u32: expression({ left: "u32", right: "u32" }, { value: "bool" }),
|
||||
separate_vec2: expression({ vector: "vec2" }, { x: "f32", y: "f32" }),
|
||||
combine_vec2: expression({ x: "f32", y: "f32" }, { vector: "vec2" }),
|
||||
separate_vec3: expression({ vector: "vec3" }, { x: "f32", y: "f32", z: "f32" }),
|
||||
combine_vec3: expression({ x: "f32", y: "f32", z: "f32" }, { vector: "vec3" }),
|
||||
separate_vec4: expression({ vector: "vec4" }, { x: "f32", y: "f32", z: "f32", w: "f32" }),
|
||||
combine_vec4: expression({ x: "f32", y: "f32", z: "f32", w: "f32" }, { vector: "vec4" }),
|
||||
separate_mat2: expression({ matrix: "mat2" }, numbered("column", 2, "vec2")),
|
||||
combine_mat2: expression(numbered("column", 2, "vec2"), { matrix: "mat2" }),
|
||||
separate_mat3: expression({ matrix: "mat3" }, numbered("column", 3, "vec3")),
|
||||
combine_mat3: expression(numbered("column", 3, "vec3"), { matrix: "mat3" }),
|
||||
separate_mat4: expression({ matrix: "mat4" }, numbered("column", 4, "vec4")),
|
||||
combine_mat4: expression(numbered("column", 4, "vec4"), { matrix: "mat4" }),
|
||||
separate_u32x16: expression({ value: "u32x16" }, numbered("word", 16, "u32")),
|
||||
combine_u32x16: expression(numbered("word", 16, "u32"), { value: "u32x16" }),
|
||||
separate_u32_bits: expression({ value: "u32" }, numbered("bit", 32, "bool")),
|
||||
combine_u32_bits: expression(numbered("bit", 32, "bool"), { value: "u32" }),
|
||||
separate_local_aabb: expression({ value: "local_aabb" }, { min: "vec3", max: "vec3" }),
|
||||
};
|
||||
const texture = {
|
||||
residency: "transient",
|
||||
texture: {
|
||||
@@ -25,17 +64,13 @@ const texture = {
|
||||
};
|
||||
export const semanticCatalog = Object.freeze({
|
||||
mesh: {
|
||||
version: 1,
|
||||
version: 2,
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: {
|
||||
mesh: o("mesh_data"),
|
||||
localAabbs: o("local_aabb_buffer"),
|
||||
isVisible: {
|
||||
...o("boolean_flag_buffer"),
|
||||
authoringType: "visibility_flag_buffer",
|
||||
},
|
||||
pipelineIndices: o("pipeline_index_stream"),
|
||||
type: o("u32x16"),
|
||||
localAabb: o("local_aabb"),
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
@@ -62,54 +97,28 @@ export const semanticCatalog = Object.freeze({
|
||||
},
|
||||
},
|
||||
frustum_cull: {
|
||||
version: 1,
|
||||
execution: "compute",
|
||||
version: 2,
|
||||
execution: "expression",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
localAabbs: i("local_aabb_buffer"),
|
||||
},
|
||||
outputs: {
|
||||
isFrustumCulled: {
|
||||
...o("boolean_flag_buffer"),
|
||||
authoringType: "frustum_flag_buffer",
|
||||
},
|
||||
localAabb: i("local_aabb"),
|
||||
},
|
||||
outputs: { isFrustumCulled: o("bool") },
|
||||
parameters: { cameraSelection: "active" },
|
||||
},
|
||||
mesh_query: {
|
||||
version: 1,
|
||||
execution: "compute",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
isVisible: i("boolean_flag_buffer", false, "visibility_flag_buffer"),
|
||||
isFrustumCulled: i("boolean_flag_buffer", false, "frustum_flag_buffer"),
|
||||
},
|
||||
outputs: { draws: o("draw_stream") },
|
||||
parameters: {
|
||||
visiblePredicate: "required_true",
|
||||
frustumCulledPredicate: "required_false",
|
||||
},
|
||||
},
|
||||
pipeline_registry: {
|
||||
version: 1,
|
||||
execution: "cpu_preparation",
|
||||
inputs: { pipelineIndices: i("pipeline_index_stream") },
|
||||
outputs: { activation: o("pipeline_activation") },
|
||||
parameters: {},
|
||||
},
|
||||
pipeline: {
|
||||
version: 1,
|
||||
version: 2,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
draws: i("draw_stream"),
|
||||
activation: i("pipeline_activation"),
|
||||
predicate: i("bool", false),
|
||||
colorTarget: i("texture"),
|
||||
depthTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture"), depth: o("texture") },
|
||||
parameters: { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] },
|
||||
},
|
||||
...expressionCatalog,
|
||||
fullscreen_copy: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
@@ -210,13 +219,8 @@ export const socketTypes = Object.fromEntries(
|
||||
[
|
||||
"texture",
|
||||
"mesh_data",
|
||||
"local_aabb_buffer",
|
||||
"boolean_flag_buffer",
|
||||
"pipeline_index_stream",
|
||||
"draw_stream",
|
||||
"pipeline_activation",
|
||||
"visibility_flag_buffer",
|
||||
"frustum_flag_buffer",
|
||||
"bool", "f32", "u32", "vec2", "vec3", "vec4",
|
||||
"mat2", "mat3", "mat4", "u32x16", "local_aabb",
|
||||
].map((type, index) => [
|
||||
type,
|
||||
{
|
||||
@@ -226,11 +230,6 @@ export const socketTypes = Object.fromEntries(
|
||||
},
|
||||
]),
|
||||
);
|
||||
socketTypes.boolean_flag_buffer.acceptsFrom = [
|
||||
"boolean_flag_buffer",
|
||||
"visibility_flag_buffer",
|
||||
"frustum_flag_buffer",
|
||||
];
|
||||
export const theme = {
|
||||
background: "#151820",
|
||||
grid: "#292e3a",
|
||||
@@ -266,6 +265,7 @@ export const theme = {
|
||||
export const styles = {
|
||||
source: { header: "#3977a8" },
|
||||
compute: { header: "#725a9b" },
|
||||
expression: { header: "#725a9b" },
|
||||
cpu_preparation: { header: "#8a6d3b" },
|
||||
render: { header: "#426b43" },
|
||||
frame: { header: "#a75d37" },
|
||||
@@ -283,8 +283,8 @@ const tagged = (kind, value) => ({ kind, value: structuredClone(value) });
|
||||
const number = (value, minimum, maximum) => ({
|
||||
type: "number",
|
||||
default: tagged("number", value),
|
||||
minimum,
|
||||
maximum,
|
||||
...(minimum !== undefined ? { minimum } : {}),
|
||||
...(maximum !== undefined ? { maximum } : {}),
|
||||
});
|
||||
const enumeration = (value, values) => ({
|
||||
type: "string",
|
||||
@@ -302,8 +302,38 @@ const color = (value, minimum = 0, maximum = 1) => ({
|
||||
minimum,
|
||||
maximum,
|
||||
});
|
||||
const vector = (value, minimum, maximum) => ({ type: "vector", default: tagged("vector", value), minimum, maximum });
|
||||
const vector = (value, minimum, maximum) => ({
|
||||
type: "vector", default: tagged("vector", value),
|
||||
...(minimum !== undefined ? { minimum } : {}),
|
||||
...(maximum !== undefined ? { maximum } : {}),
|
||||
});
|
||||
const json = (value) => ({ type: "json", default: tagged("json", value) });
|
||||
const socketDefault = (type, value) => {
|
||||
if (type === "bool") return boolean(value);
|
||||
if (type === "f32") return number(value);
|
||||
if (type === "u32") return { ...number(value, 0, 0xffffffff), integer: true };
|
||||
if (type === "vec3") return vector(value);
|
||||
return json(value);
|
||||
};
|
||||
const zero = (type) => {
|
||||
if (type === "bool") return false;
|
||||
if (type === "f32" || type === "u32") return 0;
|
||||
if (/^vec[234]$/.test(type)) return Array(Number(type.at(-1))).fill(0);
|
||||
if (type === "u32x16") return Array(16).fill(0);
|
||||
if (type === "local_aabb") return { min: [0, 0, 0], max: [0, 0, 0] };
|
||||
const size = Number(type.at(-1));
|
||||
return Array.from({ length: size }, (_, column) =>
|
||||
Array.from({ length: size }, (_, row) => Number(column === row)));
|
||||
};
|
||||
const defaultForInput = (key, name, type) => {
|
||||
if (key === "pipeline" && name === "predicate") return true;
|
||||
if (key === "and") return true;
|
||||
if (/^combine_mat[234]$/.test(key)) {
|
||||
const index = Number(name.replace("column", ""));
|
||||
return zero(type).map((_, row) => Number(index === row));
|
||||
}
|
||||
return zero(type);
|
||||
};
|
||||
const parameterSchemas = {
|
||||
texture: {
|
||||
residency: enumeration("transient", ["transient", "persistent"]),
|
||||
@@ -343,19 +373,6 @@ const parameterSchemas = {
|
||||
},
|
||||
mesh: {},
|
||||
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
|
||||
mesh_query: {
|
||||
visiblePredicate: enumeration("required_true", [
|
||||
"any",
|
||||
"required_true",
|
||||
"required_false",
|
||||
]),
|
||||
frustumCulledPredicate: enumeration("required_false", [
|
||||
"any",
|
||||
"required_true",
|
||||
"required_false",
|
||||
]),
|
||||
},
|
||||
pipeline_registry: {},
|
||||
pipeline: {
|
||||
pipeline: string("gltf_standard"),
|
||||
depthCompare: enumeration("less_equal", [
|
||||
@@ -399,6 +416,7 @@ const parameterSchemas = {
|
||||
backgroundColor: color([0, 0, 0, 1]),
|
||||
},
|
||||
};
|
||||
for (const key of Object.keys(expressionCatalog)) parameterSchemas[key] = {};
|
||||
export const nodeDefinitions = Object.fromEntries(
|
||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||
const sockets = {
|
||||
@@ -409,11 +427,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
n,
|
||||
"input",
|
||||
v.authoringType ?? v.accepted.types[0],
|
||||
key === "mesh_query" && n === "isVisible"
|
||||
? boolean(true)
|
||||
: key === "mesh_query" && n === "isFrustumCulled"
|
||||
? boolean(false)
|
||||
: null,
|
||||
!v.required ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null,
|
||||
),
|
||||
]),
|
||||
),
|
||||
@@ -458,6 +472,7 @@ export const nodeDefinitions = Object.fromEntries(
|
||||
];
|
||||
}),
|
||||
);
|
||||
nodeDefinitions.mesh.sockets.localAabb.title = "Local AABB";
|
||||
nodeDefinitions.color_balance.ui = [
|
||||
{ kind: "parameter", parameter: "mode" },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
|
||||
@@ -3,19 +3,8 @@ import { CATALOG_VERSION, GRAPH_ID, fxNodeComposition } from "./catalog.js";
|
||||
import { prepareBrowserHost } from "./browser-host.js";
|
||||
import { createAddNodeMenu } from "./add-node-menu.js";
|
||||
import { createNodeIdAllocator, spawnRequestedNode } from "./node-spawn.js";
|
||||
import { culling } from "./presets.js";
|
||||
|
||||
const spec = [
|
||||
["hdr", "texture", { x: 40, y: 170 }],
|
||||
["depth", "texture", { x: 40, y: 300 }],
|
||||
["mesh", "mesh", { x: 40, y: 470 }],
|
||||
["cull", "frustum_cull", { x: 540, y: 480 }],
|
||||
["query", "mesh_query", { x: 790, y: 330 }],
|
||||
["registry", "pipeline_registry", { x: 790, y: 620 }],
|
||||
["ground", "pipeline", { x: 1040, y: 290 }],
|
||||
["pbr", "pipeline", { x: 1300, y: 290 }],
|
||||
["pbr_double", "pipeline", { x: 1560, y: 290 }],
|
||||
["frame_out", "frame_out", { x: 1820, y: 250 }],
|
||||
];
|
||||
async function seed(root) {
|
||||
await root.setState({
|
||||
graphId: GRAPH_ID,
|
||||
@@ -24,28 +13,11 @@ async function seed(root) {
|
||||
links: [],
|
||||
metadata: {},
|
||||
});
|
||||
for (const [nodeId, nodeType, position] of spec)
|
||||
await root.dispatch({ type: "node.add", nodeId, nodeType, position });
|
||||
const links = [
|
||||
["mesh", "mesh", "cull", "mesh"],
|
||||
["mesh", "localAabbs", "cull", "localAabbs"],
|
||||
["mesh", "mesh", "query", "mesh"],
|
||||
["mesh", "isVisible", "query", "isVisible"],
|
||||
["cull", "isFrustumCulled", "query", "isFrustumCulled"],
|
||||
["mesh", "pipelineIndices", "registry", "pipelineIndices"],
|
||||
...["ground", "pbr", "pbr_double"].flatMap((pipeline) => [
|
||||
["mesh", "mesh", pipeline, "mesh"],
|
||||
["query", "draws", pipeline, "draws"],
|
||||
["registry", "activation", pipeline, "activation"],
|
||||
]),
|
||||
["hdr", "texture", "ground", "colorTarget"],
|
||||
["depth", "texture", "ground", "depthTarget"],
|
||||
["ground", "color", "pbr", "colorTarget"],
|
||||
["ground", "depth", "pbr", "depthTarget"],
|
||||
["pbr", "color", "pbr_double", "colorTarget"],
|
||||
["pbr", "depth", "pbr_double", "depthTarget"],
|
||||
["pbr_double", "color", "frame_out", "color"],
|
||||
];
|
||||
for (const [index, item] of culling.nodes.entries())
|
||||
await root.dispatch({ type: "node.add", nodeId: item.id, nodeType: item.executor.key,
|
||||
position: { x: 40 + (index % 6) * 280, y: 120 + Math.floor(index / 6) * 260 } });
|
||||
const links = culling.nodes.flatMap((item) => Object.entries(item.inputs).map(([socket, from]) =>
|
||||
[from.node, from.socket, item.id, socket]));
|
||||
for (const [a, as, b, bs] of links) {
|
||||
const id = `${a}_${as}_${b}_${bs}`;
|
||||
await root.dispatch({
|
||||
@@ -61,11 +33,43 @@ async function seed(root) {
|
||||
},
|
||||
});
|
||||
}
|
||||
const authored = await root.getState(),
|
||||
depth = authored.nodes.find((node) => node.id === "depth");
|
||||
depth.parameters.format = { kind: "string", value: "depth32_float" };
|
||||
for (const [id, name] of [["ground", "ground_plane"], ["pbr", "gltf_standard"], ["pbr_double", "gltf_standard_double_sided"]])
|
||||
authored.nodes.find((node) => node.id === id).parameters.pipeline = { kind: "string", value: name };
|
||||
const authored = await root.getState();
|
||||
for (const item of culling.nodes) {
|
||||
const target = authored.nodes.find((candidate) => candidate.id === item.id);
|
||||
if (item.executor.key === "texture") {
|
||||
const texture = item.parameters.texture;
|
||||
const relative = texture.extent.kind === "surface_relative";
|
||||
const values = {
|
||||
residency: item.parameters.residency,
|
||||
format: texture.format,
|
||||
dimension: texture.dimension,
|
||||
extentMode: texture.extent.kind,
|
||||
absoluteWidth: relative ? 1 : texture.extent.width,
|
||||
absoluteHeight: relative ? 1 : texture.extent.height,
|
||||
relativeWidthNumerator: relative ? texture.extent.width.numerator : 1,
|
||||
relativeWidthDenominator: relative ? texture.extent.width.denominator : 1,
|
||||
relativeHeightNumerator: relative ? texture.extent.height.numerator : 1,
|
||||
relativeHeightDenominator: relative ? texture.extent.height.denominator : 1,
|
||||
depthOrArrayLayers: texture.extent.depthOrArrayLayers,
|
||||
mipLevelCount: texture.mipLevelCount,
|
||||
sampleCount: String(texture.sampleCount),
|
||||
viewFormat: texture.viewFormats[0] ?? "none",
|
||||
};
|
||||
for (const [key, value] of Object.entries(values))
|
||||
target.parameters[key].value = structuredClone(value);
|
||||
continue;
|
||||
}
|
||||
for (const [key, value] of Object.entries(item.parameters)) {
|
||||
const input = key.endsWith("Default") ? key.slice(0, -7) : null;
|
||||
if (input) {
|
||||
const socket = target.sockets.find((candidate) => candidate.key === input);
|
||||
if (socket?.defaultValue) socket.defaultValue.value = structuredClone(value);
|
||||
} else {
|
||||
const authoredKey = item.executor.key === "frustum_cull" && key === "camera" ? "cameraSelection" : key;
|
||||
if (target.parameters[authoredKey]) target.parameters[authoredKey].value = structuredClone(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
await root.setState(authored);
|
||||
}
|
||||
export async function createRenderGraphEditor(canvas) {
|
||||
|
||||
@@ -13,16 +13,37 @@ const texture = (format, scale = 1, heightScale = scale) => ({
|
||||
residency: "transient",
|
||||
});
|
||||
const frameOut = (hdr, options = {}) => ({ surfaceFormat: "preferred", hdrEnabled: hdr, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1], ...options });
|
||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1) => [
|
||||
const predicates = (withCulling = false) => {
|
||||
const result = [
|
||||
node("type_words", "separate_u32x16", { valueDefault: Array(16).fill(0) }, { value: input("mesh", "type") }),
|
||||
node("type_bits", "separate_u32_bits", { valueDefault: 0 }, { value: input("type_words", "word0") }),
|
||||
node("ground_class", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit1") }),
|
||||
node("visible_pbr", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit2") }),
|
||||
node("not_double", "not", { operandDefault: false }, { operand: input("type_bits", "bit3") }),
|
||||
node("standard_class", "and", { leftDefault: true, rightDefault: true }, { left: input("visible_pbr", "value"), right: input("not_double", "value") }),
|
||||
node("double_class", "and", { leftDefault: true, rightDefault: true }, { left: input("type_bits", "bit0"), right: input("type_bits", "bit3") }),
|
||||
];
|
||||
if (!withCulling) return { nodes: result, classes: { ground: "ground_class", pbr: "standard_class", pbr_double: "double_class" } };
|
||||
result.push(
|
||||
node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabb: input("mesh", "localAabb") }),
|
||||
node("not_culled", "not", { operandDefault: false }, { operand: input("cull", "isFrustumCulled") }),
|
||||
...[["ground", "ground_class"], ["pbr", "standard_class"], ["pbr_double", "double_class"]].map(([name, classification]) =>
|
||||
node(`${name}_final`, "and", { leftDefault: true, rightDefault: true }, { left: input(classification, "value"), right: input("not_culled", "value") })),
|
||||
);
|
||||
return { nodes: result, classes: { ground: "ground_final", pbr: "pbr_final", pbr_double: "pbr_double_final" } };
|
||||
};
|
||||
const scene = (colorTarget, clearColor = [0.015, 0.02, 0.03, 1], heightScale = 1, withCulling = false) => {
|
||||
const classification = predicates(withCulling);
|
||||
return [
|
||||
node("hdr", "texture", texture("rgba16_float", 1, heightScale)),
|
||||
node("depth", "texture", texture("depth32_float", 1, heightScale)),
|
||||
node("mesh", "mesh"),
|
||||
node("query", "mesh_query", { visiblePredicate: "required_true", visibleDefault: true, frustumCulledPredicate: "any", frustumCulledDefault: false }, { mesh: input("mesh", "mesh"), isVisible: input("mesh", "isVisible") }),
|
||||
node("registry", "pipeline_registry", {}, { pipelineIndices: input("mesh", "pipelineIndices") }),
|
||||
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input(colorTarget, "texture"), depthTarget: input("depth", "texture") }),
|
||||
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
|
||||
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor }, { mesh: input("mesh", "mesh"), draws: input("query", "draws"), activation: input("registry", "activation"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
||||
...classification.nodes,
|
||||
node("ground", "pipeline", { pipeline: "ground_plane", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.ground, "value"), colorTarget: input(colorTarget, "texture"), depthTarget: input("depth", "texture") }),
|
||||
node("pbr", "pipeline", { pipeline: "gltf_standard", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr, "value"), colorTarget: input("ground", "color"), depthTarget: input("ground", "depth") }),
|
||||
node("pbr_double", "pipeline", { pipeline: "gltf_standard_double_sided", depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor, predicateDefault: true }, { mesh: input("mesh", "mesh"), predicate: input(classification.classes.pbr_double, "value"), colorTarget: input("pbr", "color"), depthTarget: input("pbr", "depth") }),
|
||||
];
|
||||
};
|
||||
const graph = (graphId, nodes) => Object.freeze({ schemaVersion: 2, graphId, revision: 1, nodes });
|
||||
const direct = (graphId, clearColor) => graph(graphId, [
|
||||
node("ldr", "texture", texture("rgba8_unorm")),
|
||||
@@ -36,12 +57,7 @@ export const hdr = graph("preset_hdr_fullscreen", [
|
||||
node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") }),
|
||||
]);
|
||||
export const culling = graph("preset_gpu_culling", (() => {
|
||||
const nodes = structuredClone(hdr.nodes);
|
||||
nodes.splice(3, 0, node("cull", "frustum_cull", { camera: "active" }, { mesh: input("mesh", "mesh"), localAabbs: input("mesh", "localAabbs") }));
|
||||
const query = nodes.find((item) => item.id === "query");
|
||||
query.parameters.frustumCulledPredicate = "required_false";
|
||||
query.inputs.isFrustumCulled = input("cull", "isFrustumCulled");
|
||||
return nodes;
|
||||
return [...scene("hdr", undefined, 1, true), node("frame_out", "frame_out", frameOut(true), { color: input("pbr_double", "color") })];
|
||||
})());
|
||||
const postPreset = (graphId, kind) => {
|
||||
const nodes = [...scene("hdr")];
|
||||
|
||||
+21
-16
@@ -1,8 +1,8 @@
|
||||
import { SnapshotReader } from "./render-data-snapshot.js";
|
||||
|
||||
export const VISIBLE = 1;
|
||||
const HEADER_WORDS = 16, SLOT_WORDS = 24, CAPACITY = 1024, SLOT_VERSION = 1;
|
||||
const OP = { IMPORT_GLB: 1, MESH_FLAGS: 2, CREATE_INSTANCE: 3, INSTANCE_FLAGS: 4, INSTANCE_TRANSFORM: 5, DESTROY_INSTANCE: 6, COMPILE_GRAPH: 7, DROP_GRAPH: 8, SWITCH_GRAPH: 9 };
|
||||
const HEADER_WORDS = 16, SLOT_WORDS = 40, CAPACITY = 1024, SLOT_VERSION = 2;
|
||||
const OP = { IMPORT_GLB: 1, MESH_FLAGS: 2, CREATE_INSTANCE: 3, INSTANCE_VISIBLE: 4, INSTANCE_TRANSFORM: 5, DESTROY_INSTANCE: 6, COMPILE_GRAPH: 7, DROP_GRAPH: 8, SWITCH_GRAPH: 9, SET_INSTANCE_TYPE: 10 };
|
||||
const HANDLE_TOKEN = Symbol("renderer handle");
|
||||
|
||||
export class RendererError extends Error {
|
||||
@@ -20,7 +20,7 @@ export class RendererClient {
|
||||
this.#bridge = bridge;
|
||||
this.#worker = bridge.worker;
|
||||
this.#refreshViews();
|
||||
if (Atomics.load(this.#header, 0) !== 0x4e574159 || Atomics.load(this.#header, 1) !== 1 || Atomics.load(this.#header, 2) !== CAPACITY || Atomics.load(this.#header, 3) !== SLOT_WORDS) {
|
||||
if (Atomics.load(this.#header, 0) !== 0x4e574159 || Atomics.load(this.#header, 1) !== 2 || Atomics.load(this.#header, 2) !== CAPACITY || Atomics.load(this.#header, 3) !== SLOT_WORDS) {
|
||||
try { this.#worker?.terminate?.(); } catch { /* best effort */ }
|
||||
try { bridge?.free?.(); } catch { /* best effort */ }
|
||||
throw new RendererError("PROTOCOL_MISMATCH");
|
||||
@@ -70,9 +70,9 @@ export class RendererClient {
|
||||
this.#fail(message.code || "WORKER_FATAL");
|
||||
} else if (message?.type === "snapshot-init") {
|
||||
try {
|
||||
if (message.controlVersion !== 1 || message.schemaVersion !== 1) throw new Error("version");
|
||||
if (message.controlVersion !== 1 || message.schemaVersion !== 2) throw new Error("version");
|
||||
this.#snapshotReader = new SnapshotReader(this.#bridge.memory, message.controlPtr);
|
||||
this.#bvh?.postMessage({type:"init",memory:this.#bridge.memory,controlPtr:message.controlPtr,controlVersion:1,schemaVersion:1});
|
||||
this.#bvh?.postMessage({type:"init",memory:this.#bridge.memory,controlPtr:message.controlPtr,controlVersion:1,schemaVersion:2});
|
||||
} catch { this.#disablePicking("PICK_PROTOCOL_MISMATCH"); }
|
||||
} else if (message?.type === "snapshot-published") {
|
||||
try { this.#snapshotEpoch=this.#snapshotReader?.latest().epoch||0; this.#bvh?.postMessage({type:"update",epoch:this.#snapshotEpoch}); } catch { this.#disablePicking("PICK_PROTOCOL_MISMATCH"); }
|
||||
@@ -157,18 +157,20 @@ export class RendererClient {
|
||||
return promise;
|
||||
}
|
||||
|
||||
#mesh(handle) {
|
||||
#mesh(handle, defaultType = Array(16).fill(0)) {
|
||||
return new Mesh(HANDLE_TOKEN,
|
||||
visible => this.#enqueue(OP.MESH_FLAGS, [...handle, visible ? VISIBLE : 0]),
|
||||
async (transform, visible) => {
|
||||
const result = await this.#enqueue(OP.CREATE_INSTANCE, [...handle, ...floatWords(transform), visible ? VISIBLE : 0]);
|
||||
visible => { validateVisible(visible); return this.#enqueue(OP.MESH_FLAGS, [...handle, visible ? 1 : 0]); },
|
||||
async (transform, {type = defaultType, visible} = {}) => {
|
||||
type = typeWords(type); if (visible !== undefined) { validateVisible(visible); type[0] = (type[0] & ~1) | Number(visible); }
|
||||
const result = await this.#enqueue(OP.CREATE_INSTANCE, [...handle, ...floatWords(transform), ...type]);
|
||||
return this.#instance(result);
|
||||
});
|
||||
}
|
||||
|
||||
#instance(handle) {
|
||||
return new Instance(HANDLE_TOKEN,
|
||||
visible => this.#enqueue(OP.INSTANCE_FLAGS, [...handle, visible ? VISIBLE : 0]),
|
||||
visible => { validateVisible(visible); return this.#enqueue(OP.INSTANCE_VISIBLE, [...handle, visible ? 1 : 0]); },
|
||||
type => this.#enqueue(OP.SET_INSTANCE_TYPE, [...handle, ...typeWords(type)]),
|
||||
transform => this.#enqueue(OP.INSTANCE_TRANSFORM, [...handle, ...floatWords(transform)]),
|
||||
() => this.#enqueue(OP.DESTROY_INSTANCE, [...handle]));
|
||||
}
|
||||
@@ -183,11 +185,9 @@ export class RendererClient {
|
||||
else throw new TypeError("GLB source must be URL, File, or ArrayBuffer");
|
||||
if (this.#disposed) throw new RendererError("DISPOSED");
|
||||
const result = await this.#withPayload(buffer, OP.IMPORT_GLB, [framing === "interior" ? 1 : 0]);
|
||||
return result.meshes.map(handle => this.#mesh(handle));
|
||||
return result.meshes.map(item => this.#mesh(item.handle, item.defaultType));
|
||||
}
|
||||
|
||||
/** Compatibility alias for the original opcode-1 API. */
|
||||
importGlb(source, options) { return this.replaceSceneGlb(source, options); }
|
||||
|
||||
async #withPayload(buffer, opcode, words = []) {
|
||||
if (this.#disposed) throw new RendererError("DISPOSED");
|
||||
@@ -260,6 +260,9 @@ function validateCompiledId(compiledId) {
|
||||
if (!Array.isArray(compiledId) || compiledId.length !== 2 || compiledId.some(word => !Number.isInteger(word) || word < 0 || word > 0xffffffff)) throw new TypeError("compiledId must contain exactly two uint32 values");
|
||||
}
|
||||
|
||||
function validateVisible(value) { if (typeof value !== "boolean") throw new TypeError("visible must be boolean"); }
|
||||
function typeWords(words) { if (!words || words.length !== 16 || [...words].some(x => !Number.isInteger(x) || x < 0 || x > 0xffffffff)) throw new TypeError("type must contain exactly 16 uint32 values"); return Array.from(words, x => x >>> 0); }
|
||||
|
||||
function floatWords(matrix) {
|
||||
if (!matrix || matrix.length !== 16) throw new TypeError("transform must contain 16 numbers");
|
||||
return [...new Int32Array(new Float32Array(matrix).buffer)];
|
||||
@@ -273,19 +276,21 @@ class Mesh {
|
||||
this.#createInstance = createInstance;
|
||||
}
|
||||
setVisible(visible) { return this.#setVisible(visible); }
|
||||
createInstance(transform, visible = true) { return this.#createInstance(transform, visible); }
|
||||
createInstance(transform, options = {}) { return this.#createInstance(transform, options); }
|
||||
}
|
||||
|
||||
class Instance {
|
||||
#setVisible; #setTransform; #destroy; #dead = false;
|
||||
constructor(token, setVisible, setTransform, destroy) {
|
||||
#setVisible; #setType; #setTransform; #destroy; #dead = false;
|
||||
constructor(token, setVisible, setType, setTransform, destroy) {
|
||||
if (token !== HANDLE_TOKEN) throw new TypeError("Instance cannot be constructed directly");
|
||||
this.#setVisible = setVisible;
|
||||
this.#setType = setType;
|
||||
this.#setTransform = setTransform;
|
||||
this.#destroy = destroy;
|
||||
}
|
||||
#live() { if (this.#dead) throw new RendererError("STALE_HANDLE"); }
|
||||
setVisible(visible) { this.#live(); return this.#setVisible(visible); }
|
||||
setType(words) { this.#live(); return this.#setType(words); }
|
||||
setTransform(transform) { this.#live(); return this.#setTransform(transform); }
|
||||
async destroy() { this.#live(); await this.#destroy(); this.#dead = true; }
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user