Rewrite core around shared rows and render graphs
Co-authored-by: Heaust Azure <heaust.azure@gmail.com> Amp-Thread-ID: https://ampcode.com/threads/T-01a01380-b478-77d0-84a0-102880a5c5ae
This commit is contained in:
@@ -3,10 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"description": "FXNode frontend for Yawn render graphs",
|
||||
"type": "module",
|
||||
"exports": {
|
||||
".": "./src/index.js",
|
||||
"./catalog": "./src/catalog.js"
|
||||
},
|
||||
"exports": "./src/index.js",
|
||||
"dependencies": {
|
||||
"@yawn/render-graph-ast": "0.1.0"
|
||||
}
|
||||
|
||||
@@ -1,601 +0,0 @@
|
||||
// Converts Yawn's FXNode authoring document into the canonical render-graph AST.
|
||||
import {
|
||||
CATALOG_VERSION,
|
||||
descriptors,
|
||||
GRAPH_ID,
|
||||
nodeDefinitions,
|
||||
socketTypes,
|
||||
} from "./catalog.js";
|
||||
import { createGraphAst } from "@yawn/render-graph-ast";
|
||||
|
||||
class AuthoringGraphError extends Error {
|
||||
constructor(code, details = {}) {
|
||||
super(code);
|
||||
this.name = "AuthoringGraphError";
|
||||
this.code = code;
|
||||
this.details = Object.freeze({ ...details });
|
||||
}
|
||||
}
|
||||
|
||||
const fail = (code, details) => {
|
||||
throw new AuthoringGraphError(code, details);
|
||||
};
|
||||
const object = (value) =>
|
||||
value !== null && typeof value === "object" && !Array.isArray(value);
|
||||
const identifier = (value) =>
|
||||
typeof value === "string" &&
|
||||
/^[A-Za-z][A-Za-z0-9_.-]*$/.test(value) &&
|
||||
new TextEncoder().encode(value).length <= 64;
|
||||
const exactKeys = (value, keys) =>
|
||||
object(value) &&
|
||||
Object.keys(value).length === keys.length &&
|
||||
keys.every((key) => Object.hasOwn(value, key));
|
||||
const finiteJson = (value) =>
|
||||
value === null ||
|
||||
typeof value === "string" ||
|
||||
typeof value === "boolean" ||
|
||||
(typeof value === "number" && Number.isFinite(value)) ||
|
||||
(Array.isArray(value) && value.every(finiteJson)) ||
|
||||
(object(value) && Object.values(value).every(finiteJson));
|
||||
const canonical = (value) =>
|
||||
Array.isArray(value)
|
||||
? value.map(canonical)
|
||||
: object(value)
|
||||
? Object.fromEntries(
|
||||
Object.keys(value)
|
||||
.sort()
|
||||
.map((key) => [key, canonical(value[key])]),
|
||||
)
|
||||
: value;
|
||||
const deepFreeze = (value) => {
|
||||
if (value && typeof value === "object" && !Object.isFrozen(value)) {
|
||||
Object.freeze(value);
|
||||
for (const child of Object.values(value)) deepFreeze(child);
|
||||
}
|
||||
return value;
|
||||
};
|
||||
const sourceMaps = new WeakMap();
|
||||
export const mapAuthoringDiagnostic = (ir, diagnostic) => {
|
||||
const details = diagnostic?.details;
|
||||
const path = [
|
||||
details?.path,
|
||||
diagnostic?.path,
|
||||
details?.field,
|
||||
diagnostic?.field,
|
||||
].find((value) => typeof value === "string");
|
||||
const map = sourceMaps.get(ir);
|
||||
let match;
|
||||
if (path && map)
|
||||
for (const key of Object.keys(map))
|
||||
if (
|
||||
(path === key ||
|
||||
path.startsWith(`${key}.`) ||
|
||||
path.startsWith(`${key}[`)) &&
|
||||
(!match || key.length > match.length)
|
||||
)
|
||||
match = key;
|
||||
return deepFreeze({
|
||||
name: diagnostic?.name,
|
||||
code: diagnostic?.code,
|
||||
message: details?.message ?? diagnostic?.message ?? diagnostic?.code,
|
||||
details: details === undefined ? undefined : structuredClone(details),
|
||||
path,
|
||||
source: match ? structuredClone(map[match]) : undefined,
|
||||
});
|
||||
};
|
||||
|
||||
const mapValuePaths = (paths, path, source, value) => {
|
||||
paths[path] = source;
|
||||
if (Array.isArray(value))
|
||||
value.forEach((child, index) =>
|
||||
mapValuePaths(paths, `${path}[${index}]`, source, child),
|
||||
);
|
||||
else if (object(value))
|
||||
for (const key of Object.keys(value))
|
||||
mapValuePaths(paths, `${path}.${key}`, source, value[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;
|
||||
const bounded = (number) =>
|
||||
Number.isFinite(number) &&
|
||||
(schema.minimum === undefined || number >= schema.minimum) &&
|
||||
(schema.maximum === undefined || number <= schema.maximum);
|
||||
const valid =
|
||||
schema.type === "number"
|
||||
? bounded(value) && (!schema.integer || Number.isSafeInteger(value))
|
||||
: schema.type === "string"
|
||||
? typeof value === "string" &&
|
||||
(!schema.enum || schema.enum.includes(value))
|
||||
: schema.type === "boolean"
|
||||
? typeof value === "boolean"
|
||||
: schema.type === "vector" || schema.type === "color"
|
||||
? Array.isArray(value) &&
|
||||
value.length === (semanticType?.startsWith("vec") ? Number(semanticType.at(-1)) : (schema.type === "vector" ? 3 : 4)) &&
|
||||
value.every(bounded)
|
||||
: 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, { pipelines = {} } = {}) {
|
||||
try {
|
||||
const rootKeys = [
|
||||
"graphId",
|
||||
"catalogVersion",
|
||||
"nodes",
|
||||
"links",
|
||||
"metadata",
|
||||
"version",
|
||||
];
|
||||
if (
|
||||
!exactKeys(raw, rootKeys) ||
|
||||
!Array.isArray(raw.nodes) ||
|
||||
!Array.isArray(raw.links) ||
|
||||
!object(raw.metadata) ||
|
||||
!finiteJson(raw.metadata)
|
||||
)
|
||||
fail("AUTHORING_SHAPE");
|
||||
if (raw.graphId !== GRAPH_ID || raw.catalogVersion !== CATALOG_VERSION)
|
||||
fail("AUTHORING_CATALOG");
|
||||
if (!Number.isSafeInteger(raw.version) || raw.version < 0)
|
||||
fail("AUTHORING_SHAPE");
|
||||
if (!Number.isInteger(revision) || revision < 1 || revision > 0xffffffff)
|
||||
fail("AUTHORING_REVISION");
|
||||
const nodes = new Map(),
|
||||
sockets = new Map(),
|
||||
paths = {};
|
||||
for (let ordinal = 0; ordinal < raw.nodes.length; ordinal++) {
|
||||
const n = raw.nodes[ordinal];
|
||||
if (!object(n) || !identifier(n.id)) fail("AUTHORING_ID", { id: n?.id });
|
||||
if (nodes.has(n.id)) fail("AUTHORING_ID_DUPLICATE", { id: n.id });
|
||||
const descriptor = descriptors[n.typeId],
|
||||
definition = nodeDefinitions[n.typeId];
|
||||
if (!descriptor)
|
||||
fail("AUTHORING_NODE_TYPE", { nodeId: n.id, typeId: n.typeId });
|
||||
const nodeKeys = [
|
||||
"id",
|
||||
"typeId",
|
||||
"typeVersion",
|
||||
"position",
|
||||
"size",
|
||||
"label",
|
||||
"parameters",
|
||||
"sockets",
|
||||
"muted",
|
||||
"collapsed",
|
||||
"extensions",
|
||||
"known",
|
||||
];
|
||||
if (Object.hasOwn(n, "parentId")) nodeKeys.push("parentId");
|
||||
if (
|
||||
!exactKeys(n, nodeKeys) ||
|
||||
n.known !== true ||
|
||||
n.typeVersion !== descriptor.version ||
|
||||
typeof n.muted !== "boolean" ||
|
||||
typeof n.collapsed !== "boolean" ||
|
||||
typeof n.label !== "string" ||
|
||||
!exactKeys(n.position, ["x", "y"]) ||
|
||||
!Number.isFinite(n.position.x) ||
|
||||
!Number.isFinite(n.position.y) ||
|
||||
!exactKeys(n.size, ["x", "y"]) ||
|
||||
!Number.isFinite(n.size.x) ||
|
||||
!Number.isFinite(n.size.y) ||
|
||||
n.size.x <= 0 ||
|
||||
n.size.y <= 0 ||
|
||||
(Object.hasOwn(n, "parentId") && !identifier(n.parentId)) ||
|
||||
!object(n.extensions) ||
|
||||
!finiteJson(n.extensions) ||
|
||||
!Array.isArray(n.sockets) ||
|
||||
!object(n.parameters)
|
||||
)
|
||||
fail("AUTHORING_NODE_INVALID", { nodeId: n.id });
|
||||
const parameterKeys = Object.keys(definition.parameters);
|
||||
if (
|
||||
Object.keys(n.parameters).length !== parameterKeys.length ||
|
||||
!parameterKeys.every((key) => Object.hasOwn(n.parameters, key))
|
||||
)
|
||||
fail("AUTHORING_PARAMETER_SET", { nodeId: n.id });
|
||||
const parameters = Object.fromEntries(
|
||||
parameterKeys.map((key) => [
|
||||
key,
|
||||
parameterValue(
|
||||
n.parameters[key],
|
||||
definition.parameters[key],
|
||||
n.id,
|
||||
key,
|
||||
),
|
||||
]),
|
||||
);
|
||||
if (n.typeId === "bloom_blur")
|
||||
parameters.direction =
|
||||
parameters.direction === "horizontal" ? [1, 0] : [0, 1];
|
||||
if (n.typeId === "frustum_cull") {
|
||||
parameters.camera = parameters.cameraSelection;
|
||||
delete parameters.cameraSelection;
|
||||
}
|
||||
if (n.typeId === "texture") {
|
||||
const extent =
|
||||
parameters.extentMode === "absolute"
|
||||
? {
|
||||
kind: "absolute",
|
||||
width: parameters.absoluteWidth,
|
||||
height: parameters.absoluteHeight,
|
||||
depthOrArrayLayers: parameters.depthOrArrayLayers,
|
||||
}
|
||||
: {
|
||||
kind: "surface_relative",
|
||||
width: {
|
||||
numerator: parameters.relativeWidthNumerator,
|
||||
denominator: parameters.relativeWidthDenominator,
|
||||
},
|
||||
height: {
|
||||
numerator: parameters.relativeHeightNumerator,
|
||||
denominator: parameters.relativeHeightDenominator,
|
||||
},
|
||||
depthOrArrayLayers: parameters.depthOrArrayLayers,
|
||||
};
|
||||
const flat = structuredClone(parameters);
|
||||
Object.keys(parameters).forEach((key) => delete parameters[key]);
|
||||
Object.assign(parameters, {
|
||||
residency: flat.residency,
|
||||
texture: {
|
||||
dimension: flat.dimension,
|
||||
format: flat.format,
|
||||
extent,
|
||||
mipLevelCount: flat.mipLevelCount,
|
||||
sampleCount: Number(flat.sampleCount),
|
||||
viewFormats: flat.viewFormat === "none" ? [] : [flat.viewFormat],
|
||||
},
|
||||
});
|
||||
}
|
||||
const expected = [
|
||||
...Object.keys(descriptor.inputs),
|
||||
...Object.keys(descriptor.outputs),
|
||||
];
|
||||
if (n.sockets.length !== expected.length)
|
||||
fail("AUTHORING_SOCKET_SET", { nodeId: n.id });
|
||||
for (const s of n.sockets) {
|
||||
if (!object(s) || !expected.includes(s.key) || sockets.has(s.id))
|
||||
fail("AUTHORING_SOCKET", { nodeId: n.id, socket: s?.key });
|
||||
const input = descriptor.inputs[s.key],
|
||||
socketDefinition = definition.sockets[s.key],
|
||||
direction = input ? "input" : "output",
|
||||
dataType = socketDefinition.type,
|
||||
socketKeys = [
|
||||
"id",
|
||||
"key",
|
||||
"label",
|
||||
"direction",
|
||||
"dataType",
|
||||
"accepts",
|
||||
"maxIncomingLinks",
|
||||
...(socketDefinition.value ? ["defaultValue"] : []),
|
||||
"visible",
|
||||
];
|
||||
if (
|
||||
!exactKeys(s, socketKeys) ||
|
||||
s.id !== `${n.id}:${s.key}` ||
|
||||
s.label !== socketDefinition.title ||
|
||||
s.direction !== direction ||
|
||||
s.dataType !== dataType ||
|
||||
!Array.isArray(s.accepts) ||
|
||||
s.accepts.length !==
|
||||
(direction === "input"
|
||||
? socketTypes[dataType].acceptsFrom.length
|
||||
: 0) ||
|
||||
!s.accepts.every(
|
||||
(v, i) =>
|
||||
v ===
|
||||
(direction === "input"
|
||||
? socketTypes[dataType].acceptsFrom[i]
|
||||
: undefined),
|
||||
) ||
|
||||
(socketDefinition.value
|
||||
? (() => {
|
||||
try {
|
||||
parameterValue(
|
||||
s.defaultValue,
|
||||
socketDefinition.value,
|
||||
n.id,
|
||||
s.key,
|
||||
input.accepted.types[0],
|
||||
);
|
||||
return false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
})()
|
||||
: s.defaultValue !== undefined) ||
|
||||
s.visible !== socketDefinition.visible ||
|
||||
s.maxIncomingLinks !== socketDefinition.maxIncomingLinks
|
||||
)
|
||||
fail("AUTHORING_SOCKET", { nodeId: n.id, socket: s.key });
|
||||
sockets.set(s.id, {
|
||||
node: n.id,
|
||||
key: s.key,
|
||||
semanticName: (input ?? descriptor.outputs[s.key]).semanticName ?? s.key,
|
||||
direction,
|
||||
semanticType: input
|
||||
? input.accepted.types[0]
|
||||
: descriptor.outputs[s.key].type,
|
||||
authoringType: s.dataType,
|
||||
maxIncomingLinks: s.maxIncomingLinks,
|
||||
defaultValue: socketDefinition.value
|
||||
? structuredClone(s.defaultValue)
|
||||
: undefined,
|
||||
});
|
||||
}
|
||||
if (new Set(n.sockets.map((s) => s.key)).size !== expected.length)
|
||||
fail("AUTHORING_SOCKET_SET", { nodeId: n.id });
|
||||
for (const key of Object.keys(descriptor.inputs)) {
|
||||
const authoredDefault = sockets.get(`${n.id}:${key}`).defaultValue;
|
||||
if (authoredDefault)
|
||||
parameters[`${descriptor.inputs[key].semanticName ?? key}Default`] = parameterValue(
|
||||
authoredDefault,
|
||||
definition.sockets[key].value,
|
||||
n.id,
|
||||
key,
|
||||
descriptor.inputs[key].accepted.types[0],
|
||||
);
|
||||
}
|
||||
nodes.set(n.id, {
|
||||
ordinal,
|
||||
value: {
|
||||
id: n.id,
|
||||
state: n.muted ? "muted" : "enabled",
|
||||
executor: { key: n.typeId, version: descriptor.version },
|
||||
parameters,
|
||||
inputs: {},
|
||||
},
|
||||
});
|
||||
}
|
||||
const incoming = new Map(),
|
||||
linkIds = new Set(),
|
||||
linkSources = new Map();
|
||||
for (let ordinal = 0; ordinal < raw.links.length; ordinal++) {
|
||||
const link = raw.links[ordinal];
|
||||
if (
|
||||
!object(link) ||
|
||||
!identifier(link.id) ||
|
||||
linkIds.has(link.id) ||
|
||||
!exactKeys(link, [
|
||||
"id",
|
||||
"fromNodeId",
|
||||
"fromSocketId",
|
||||
"toNodeId",
|
||||
"toSocketId",
|
||||
"muted",
|
||||
"extensions",
|
||||
]) ||
|
||||
typeof link.muted !== "boolean" ||
|
||||
!object(link.extensions) ||
|
||||
!finiteJson(link.extensions)
|
||||
)
|
||||
fail("AUTHORING_LINK", { linkId: link?.id });
|
||||
linkIds.add(link.id);
|
||||
const from = sockets.get(link.fromSocketId),
|
||||
to = sockets.get(link.toSocketId);
|
||||
if (
|
||||
!from ||
|
||||
!to ||
|
||||
link.fromNodeId !== from.node ||
|
||||
link.toNodeId !== to.node ||
|
||||
from.direction !== "output" ||
|
||||
to.direction !== "input" ||
|
||||
(!link.muted &&
|
||||
(incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
|
||||
)
|
||||
fail(
|
||||
!link.muted &&
|
||||
(incoming.get(link.toSocketId) ?? 0) >=
|
||||
(to?.maxIncomingLinks ?? Infinity)
|
||||
? "AUTHORING_LINK_INCOMING"
|
||||
: "AUTHORING_LINK",
|
||||
!link.muted &&
|
||||
(incoming.get(link.toSocketId) ?? 0) >=
|
||||
(to?.maxIncomingLinks ?? Infinity)
|
||||
? { socketId: link.toSocketId }
|
||||
: { linkId: link.id },
|
||||
);
|
||||
const accepted =
|
||||
descriptors[nodes.get(to.node).value.executor.key].inputs[to.key]
|
||||
.accepted.types;
|
||||
const authoringAccepted =
|
||||
socketTypes[
|
||||
nodeDefinitions[nodes.get(to.node).value.executor.key].sockets[to.key]
|
||||
.type
|
||||
].acceptsFrom;
|
||||
if (
|
||||
!accepted.includes(from.semanticType) ||
|
||||
!authoringAccepted.includes(from.authoringType)
|
||||
)
|
||||
fail("AUTHORING_LINK_TYPE", { linkId: link.id });
|
||||
const linkSource = {
|
||||
kind: "link",
|
||||
linkId: link.id,
|
||||
fromNodeId: link.fromNodeId,
|
||||
fromSocketId: link.fromSocketId,
|
||||
toNodeId: link.toNodeId,
|
||||
toSocketId: link.toSocketId,
|
||||
muted: link.muted,
|
||||
nodeId: to.node,
|
||||
input: to.key,
|
||||
fromSocket: from.key,
|
||||
toSocket: to.key,
|
||||
};
|
||||
linkSources.set(link.id, linkSource);
|
||||
if (!link.muted) {
|
||||
incoming.set(link.toSocketId, (incoming.get(link.toSocketId) ?? 0) + 1);
|
||||
(nodes.get(to.node).value.inputs[to.semanticName] ??= []).push({
|
||||
node: from.node,
|
||||
socket: from.semanticName,
|
||||
});
|
||||
}
|
||||
}
|
||||
const ordered = [...nodes.values()].sort((a, b) =>
|
||||
a.value.id < b.value.id
|
||||
? -1
|
||||
: a.value.id > b.value.id
|
||||
? 1
|
||||
: a.ordinal - b.ordinal,
|
||||
);
|
||||
for (let wireOrdinal = 0; wireOrdinal < ordered.length; wireOrdinal++) {
|
||||
const item = ordered[wireOrdinal];
|
||||
item.value.inputs = Object.fromEntries(
|
||||
Object.keys(descriptors[item.value.executor.key].inputs)
|
||||
.map((key) => descriptors[item.value.executor.key].inputs[key].semanticName ?? key)
|
||||
.filter((semanticName) => Object.hasOwn(item.value.inputs, semanticName))
|
||||
.map((semanticName) => [semanticName, item.value.inputs[semanticName]]),
|
||||
);
|
||||
const base = `nodes[${wireOrdinal}]`;
|
||||
const nodeSource = { kind: "node", nodeId: item.value.id };
|
||||
paths[base] = nodeSource;
|
||||
for (const field of [
|
||||
"id",
|
||||
"state",
|
||||
"executor",
|
||||
"executor.key",
|
||||
"executor.version",
|
||||
])
|
||||
paths[`${base}.${field}`] = nodeSource;
|
||||
paths[`${base}.parameters`] = nodeSource;
|
||||
const parameterSource = (parameter) => ({
|
||||
kind: "parameter",
|
||||
nodeId: item.value.id,
|
||||
parameter,
|
||||
});
|
||||
if (item.value.executor.key === "texture") {
|
||||
const root = `${base}.parameters`;
|
||||
const texture = item.value.parameters.texture;
|
||||
paths[`${root}.residency`] = parameterSource("residency");
|
||||
paths[`${root}.texture`] = nodeSource;
|
||||
paths[`${root}.texture.dimension`] = parameterSource("dimension");
|
||||
paths[`${root}.texture.format`] = parameterSource("format");
|
||||
paths[`${root}.texture.extent`] = parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.kind`] = parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.depthOrArrayLayers`] =
|
||||
parameterSource("depthOrArrayLayers");
|
||||
if (texture.extent.kind === "absolute") {
|
||||
paths[`${root}.texture.extent.width`] =
|
||||
parameterSource("absoluteWidth");
|
||||
paths[`${root}.texture.extent.height`] =
|
||||
parameterSource("absoluteHeight");
|
||||
} else {
|
||||
paths[`${root}.texture.extent.width`] = parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.width.numerator`] = parameterSource(
|
||||
"relativeWidthNumerator",
|
||||
);
|
||||
paths[`${root}.texture.extent.width.denominator`] = parameterSource(
|
||||
"relativeWidthDenominator",
|
||||
);
|
||||
paths[`${root}.texture.extent.height`] =
|
||||
parameterSource("extentMode");
|
||||
paths[`${root}.texture.extent.height.numerator`] = parameterSource(
|
||||
"relativeHeightNumerator",
|
||||
);
|
||||
paths[`${root}.texture.extent.height.denominator`] = parameterSource(
|
||||
"relativeHeightDenominator",
|
||||
);
|
||||
}
|
||||
paths[`${root}.texture.mipLevelCount`] =
|
||||
parameterSource("mipLevelCount");
|
||||
paths[`${root}.texture.sampleCount`] = parameterSource("sampleCount");
|
||||
mapValuePaths(
|
||||
paths,
|
||||
`${root}.texture.viewFormats`,
|
||||
parameterSource("viewFormat"),
|
||||
texture.viewFormats,
|
||||
);
|
||||
} else
|
||||
for (const key of Object.keys(item.value.parameters))
|
||||
mapValuePaths(
|
||||
paths,
|
||||
`${base}.parameters.${key}`,
|
||||
key.endsWith("Default") && Object.hasOwn(descriptors[item.value.executor.key].inputs, key.slice(0, -7))
|
||||
? {
|
||||
kind: "input",
|
||||
nodeId: item.value.id,
|
||||
input: key.slice(0, -7),
|
||||
socketId: `${item.value.id}:${key.slice(0, -7)}`,
|
||||
unconnected: true,
|
||||
}
|
||||
: parameterSource(
|
||||
item.value.executor.key === "frustum_cull" && key === "camera"
|
||||
? "cameraSelection"
|
||||
: key,
|
||||
),
|
||||
item.value.parameters[key],
|
||||
);
|
||||
for (const key of Object.keys(
|
||||
descriptors[item.value.executor.key].inputs,
|
||||
)) {
|
||||
const links = raw.links.filter(
|
||||
(x) =>
|
||||
!x.muted &&
|
||||
x.toNodeId === item.value.id &&
|
||||
sockets.get(x.toSocketId)?.key === key,
|
||||
);
|
||||
const source = linkSources.get(links[0]?.id) ?? {
|
||||
kind: "input",
|
||||
nodeId: item.value.id,
|
||||
input: key,
|
||||
socketId: `${item.value.id}:${key}`,
|
||||
unconnected: true,
|
||||
};
|
||||
const semanticName = descriptors[item.value.executor.key].inputs[key].semanticName ?? key;
|
||||
paths[`${base}.inputs.${semanticName}`] = source;
|
||||
for (const [index, link] of links.entries()) {
|
||||
const linkSource = linkSources.get(link.id);
|
||||
paths[`${base}.inputs.${semanticName}[${index}]`] = linkSource;
|
||||
paths[`${base}.inputs.${semanticName}[${index}].node`] = linkSource;
|
||||
paths[`${base}.inputs.${semanticName}[${index}].socket`] = {
|
||||
kind: "socket",
|
||||
nodeId: link.fromNodeId,
|
||||
socketId: link.fromSocketId,
|
||||
socket: sockets.get(link.fromSocketId).key,
|
||||
linkId: link.id,
|
||||
};
|
||||
}
|
||||
}
|
||||
paths[`${base}.inputs`] = nodeSource;
|
||||
}
|
||||
const ir = createGraphAst({
|
||||
id: GRAPH_ID,
|
||||
revision,
|
||||
pipelines,
|
||||
nodes: ordered.map((item) => item.value),
|
||||
});
|
||||
const graphSource = { kind: "graph", graphId: GRAPH_ID };
|
||||
for (const field of ["version", "id", "revision", "pipelines", "nodes"])
|
||||
paths[field] = graphSource;
|
||||
deepFreeze(paths);
|
||||
sourceMaps.set(ir, paths);
|
||||
return ir;
|
||||
} catch (error) {
|
||||
if (error instanceof AuthoringGraphError) throw error;
|
||||
fail("AUTHORING_SHAPE");
|
||||
}
|
||||
}
|
||||
@@ -1,535 +0,0 @@
|
||||
// Yawn's FXNode contract catalog lives with this addon, not core or the example app.
|
||||
export const GRAPH_ID = "authored_gpu_culling";
|
||||
export const CATALOG_VERSION = 13;
|
||||
const exact = (type) => ({ kind: "exact", types: [type] });
|
||||
const i = (type, minimum = 1, authoringType, defaultPolicy = minimum ? "none" : "parameter_literal", maximum = 1) => ({
|
||||
accepted: typeof type === "string" ? exact(type) : type,
|
||||
cardinality: { minimum, maximum },
|
||||
...(authoringType ? { authoringType } : {}),
|
||||
defaultPolicy,
|
||||
});
|
||||
const o = (type, semanticName) => ({ type, ...(semanticName ? { semanticName } : {}) });
|
||||
const expression = (inputs, outputs) => ({
|
||||
version: 1,
|
||||
execution: "expression",
|
||||
inputs: Object.fromEntries(Object.entries(inputs).map(([name, type]) => [name, i(type, 0)])),
|
||||
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({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
|
||||
or: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
|
||||
not: expression({ operand: "bool" }, { value: "bool" }),
|
||||
xor: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
|
||||
xnor: { ...expression({ inputs: "bool" }, { value: "bool" }), version: 2, inputs: { inputs: i("bool", 0, undefined, "none", 8) } },
|
||||
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: {
|
||||
dimension: "d2",
|
||||
format: "rgba16_float",
|
||||
extent: {
|
||||
kind: "surface_relative",
|
||||
width: { numerator: 1, denominator: 1 },
|
||||
height: { numerator: 1, denominator: 1 },
|
||||
depthOrArrayLayers: 1,
|
||||
},
|
||||
mipLevelCount: 1,
|
||||
sampleCount: 1,
|
||||
viewFormats: [],
|
||||
},
|
||||
};
|
||||
const rasterInputs = () => ({
|
||||
mesh: i("mesh_data"),
|
||||
predicate: i("bool", 0),
|
||||
"input.color": { ...i("texture", 0, undefined, "compiler_texture"), semanticName: "color" },
|
||||
"input.depth": { ...i("texture", 0, undefined, "compiler_texture"), semanticName: "depth" },
|
||||
});
|
||||
const rasterOutputs = () => ({ "output.color": o("texture", "color"), "output.depth": o("texture", "depth") });
|
||||
const rasterParameters = () => ({ depthCompare: "less_equal", depthWriteEnabled: true, clearDepth: 1, clearColor: [0.015, 0.02, 0.03, 1] });
|
||||
const raster = () => ({ version: 2, execution: "render", inputs: rasterInputs(), outputs: rasterOutputs(), parameters: rasterParameters() });
|
||||
export const NODE_TITLE_OVERRIDES = Object.freeze({
|
||||
ground_plane: "Ground Plane",
|
||||
gltf_standard: "glTF Standard",
|
||||
gltf_standard_double_sided: "glTF Standard — Double-Sided",
|
||||
});
|
||||
export const semanticCatalog = Object.freeze({
|
||||
mesh: {
|
||||
version: 2,
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: {
|
||||
mesh: o("mesh_data"),
|
||||
type: o("u32x16"),
|
||||
localAabb: o("local_aabb"),
|
||||
},
|
||||
parameters: {},
|
||||
},
|
||||
texture: {
|
||||
version: 2,
|
||||
execution: "source",
|
||||
inputs: {},
|
||||
outputs: { texture: o("texture") },
|
||||
parameters: {
|
||||
residency: "transient",
|
||||
format: "rgba16_float",
|
||||
dimension: "d2",
|
||||
extentMode: "surface_relative",
|
||||
absoluteWidth: 1,
|
||||
absoluteHeight: 1,
|
||||
relativeWidthNumerator: 1,
|
||||
relativeWidthDenominator: 1,
|
||||
relativeHeightNumerator: 1,
|
||||
relativeHeightDenominator: 1,
|
||||
depthOrArrayLayers: 1,
|
||||
mipLevelCount: 1,
|
||||
sampleCount: "1",
|
||||
viewFormat: "none",
|
||||
},
|
||||
},
|
||||
frustum_cull: {
|
||||
version: 2,
|
||||
execution: "expression",
|
||||
inputs: {
|
||||
mesh: i("mesh_data"),
|
||||
localAabb: i("local_aabb"),
|
||||
},
|
||||
outputs: { isFrustumCulled: o("bool") },
|
||||
parameters: { cameraSelection: "active" },
|
||||
},
|
||||
ground_plane: raster(),
|
||||
gltf_standard: raster(),
|
||||
gltf_standard_double_sided: raster(),
|
||||
...expressionCatalog,
|
||||
fullscreen_copy: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: {},
|
||||
},
|
||||
color_balance: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: { source: i("texture"), colorTarget: i("texture") },
|
||||
outputs: { color: o("texture") },
|
||||
parameters: {
|
||||
mode: "lift_gamma_gain", factor: 1,
|
||||
lift: 0, liftColor: [1, 1, 1, 1], gamma: 1, gammaColor: [1, 1, 1, 1], gain: 1, gainColor: [1, 1, 1, 1],
|
||||
offset: 0, offsetColor: [1, 1, 1, 1], power: 1, powerColor: [1, 1, 1, 1], slope: 1, slopeColor: [1, 1, 1, 1],
|
||||
},
|
||||
},
|
||||
exposure_contrast: {
|
||||
version: 1, execution: "render",
|
||||
inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") },
|
||||
parameters: { exposureStops: 0, contrast: 1, pivot: 0.18, factor: 1 },
|
||||
},
|
||||
saturation: {
|
||||
version: 1, execution: "render",
|
||||
inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") },
|
||||
parameters: { saturation: 1, factor: 1 },
|
||||
},
|
||||
channel_mixer: {
|
||||
version: 1, execution: "render",
|
||||
inputs: { source: i("texture"), colorTarget: i("texture") }, outputs: { color: o("texture") },
|
||||
parameters: { redOutput: [1, 0, 0], greenOutput: [0, 1, 0], blueOutput: [0, 0, 1], factor: 1 },
|
||||
},
|
||||
bloom_extract: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { threshold: 1, knee: 0.5 },
|
||||
},
|
||||
bloom_blur: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { direction: [1, 0], radius: 1 },
|
||||
},
|
||||
bloom_composite: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
bloom: i("texture"),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { intensity: 1 },
|
||||
},
|
||||
luminance_edge: {
|
||||
version: 1,
|
||||
execution: "render",
|
||||
inputs: {
|
||||
source: i("texture"),
|
||||
colorTarget: i("texture"),
|
||||
},
|
||||
outputs: { color: o("texture") },
|
||||
parameters: { strength: 2 },
|
||||
},
|
||||
frame_out: {
|
||||
version: 3,
|
||||
execution: "frame",
|
||||
inputs: { color: i("texture") },
|
||||
outputs: {},
|
||||
parameters: { surfaceFormat: "preferred", hdrEnabled: true, toneMapper: "aces", exposureStops: 0, outputTransfer: "srgb", scaleMode: "stretch", filter: "linear", backgroundColor: [0, 0, 0, 1] },
|
||||
},
|
||||
});
|
||||
const socketColors = [
|
||||
"#d17c7c",
|
||||
"#d19e7c",
|
||||
"#d1c77c",
|
||||
"#9ed17c",
|
||||
"#7cd1a5",
|
||||
"#7ccbd1",
|
||||
"#7c98d1",
|
||||
"#a27cd1",
|
||||
"#d17cb8",
|
||||
];
|
||||
export const socketTypes = Object.fromEntries(
|
||||
[
|
||||
"texture",
|
||||
"mesh_data",
|
||||
"bool", "f32", "u32", "vec2", "vec3", "vec4",
|
||||
"mat2", "mat3", "mat4", "u32x16", "local_aabb",
|
||||
].map((type, index) => [
|
||||
type,
|
||||
{
|
||||
title: type.replaceAll("_", " "),
|
||||
color: socketColors[index % socketColors.length],
|
||||
acceptsFrom: [type],
|
||||
},
|
||||
]),
|
||||
);
|
||||
export const theme = {
|
||||
background: "#151820",
|
||||
grid: "#292e3a",
|
||||
frame: "#30343a80",
|
||||
frameHeader: "#59616c",
|
||||
body: "#292e39",
|
||||
control: "#24272b",
|
||||
controlFill: "#4775b8",
|
||||
controlEditing: "#181a1d",
|
||||
textSelection: "#4775b8",
|
||||
outline: "#0b0d12",
|
||||
text: "#edf1f7",
|
||||
muted: "#969eaa",
|
||||
shadow: "#00000088",
|
||||
nodeSelected: "#ff9f43",
|
||||
nodeActive: "#ffffff",
|
||||
unknownHeader: "#555b64",
|
||||
unknownSocket: "#999999",
|
||||
linkMuted: "#d94b4b",
|
||||
knifeMuted: "#e85b5b",
|
||||
emphasis: "#ffffff",
|
||||
focus: "#f5a623",
|
||||
editOutline: "#666a70",
|
||||
resize: "#8b8e95",
|
||||
muteOverlay: "#14141459",
|
||||
boxSelectionFill: "#f5a6231f",
|
||||
checkerLight: "#aaaaaa",
|
||||
checkerDark: "#777777",
|
||||
widgetBorder: "#111216",
|
||||
rampBorder: "#111111",
|
||||
resourceBackground: "#202228",
|
||||
};
|
||||
export const styles = {
|
||||
source: { header: "#3977a8" },
|
||||
compute: { header: "#725a9b" },
|
||||
expression: { header: "#725a9b" },
|
||||
cpu_preparation: { header: "#8a6d3b" },
|
||||
render: { header: "#426b43" },
|
||||
frame: { header: "#a75d37" },
|
||||
};
|
||||
const socket = (title, direction, type, value = null, capacity = 1) => ({
|
||||
title,
|
||||
direction,
|
||||
type,
|
||||
maxIncomingLinks: direction === "input" ? capacity : 0,
|
||||
visible: true,
|
||||
value,
|
||||
showValue: value !== null,
|
||||
});
|
||||
const tagged = (kind, value) => ({ kind, value: structuredClone(value) });
|
||||
const number = (value, minimum, maximum) => ({
|
||||
type: "number",
|
||||
default: tagged("number", value),
|
||||
...(minimum !== undefined ? { minimum } : {}),
|
||||
...(maximum !== undefined ? { maximum } : {}),
|
||||
});
|
||||
const enumeration = (value, values) => ({
|
||||
type: "string",
|
||||
default: tagged("string", value),
|
||||
enum: values,
|
||||
});
|
||||
const boolean = (value) => ({
|
||||
type: "boolean",
|
||||
default: tagged("boolean", value),
|
||||
});
|
||||
const color = (value, minimum = 0, maximum = 1) => ({
|
||||
type: "color",
|
||||
default: tagged("color", 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 (["ground_plane", "gltf_standard", "gltf_standard_double_sided"].includes(key) && 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"]),
|
||||
format: enumeration("rgba16_float", [
|
||||
"rgba8_unorm",
|
||||
"rgba8_unorm_srgb",
|
||||
"bgra8_unorm",
|
||||
"bgra8_unorm_srgb",
|
||||
"rgba16_float",
|
||||
"r32_float",
|
||||
"depth32_float",
|
||||
]),
|
||||
dimension: enumeration("d2", ["d1", "d2", "d3"]),
|
||||
extentMode: enumeration("surface_relative", [
|
||||
"surface_relative",
|
||||
"absolute",
|
||||
]),
|
||||
absoluteWidth: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
absoluteHeight: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeWidthNumerator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeWidthDenominator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeHeightNumerator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
relativeHeightDenominator: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
depthOrArrayLayers: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
mipLevelCount: { ...number(1, 1, 0xffffffff), integer: true },
|
||||
sampleCount: enumeration("1", ["1", "4"]),
|
||||
viewFormat: enumeration("none", [
|
||||
"none",
|
||||
"rgba8_unorm",
|
||||
"rgba8_unorm_srgb",
|
||||
"bgra8_unorm",
|
||||
"bgra8_unorm_srgb",
|
||||
"rgba16_float",
|
||||
"r32_float",
|
||||
"depth32_float",
|
||||
]),
|
||||
},
|
||||
mesh: {},
|
||||
frustum_cull: { cameraSelection: enumeration("active", ["active"]) },
|
||||
ground_plane: {
|
||||
depthCompare: enumeration("less_equal", [
|
||||
"never",
|
||||
"less",
|
||||
"equal",
|
||||
"less_equal",
|
||||
"greater",
|
||||
"not_equal",
|
||||
"greater_equal",
|
||||
"always",
|
||||
]),
|
||||
depthWriteEnabled: boolean(true),
|
||||
clearDepth: number(1, 0, 1),
|
||||
clearColor: color([0.015, 0.02, 0.03, 1]),
|
||||
},
|
||||
fullscreen_copy: {},
|
||||
color_balance: {
|
||||
mode: enumeration("lift_gamma_gain", ["lift_gamma_gain", "offset_power_slope"]), factor: number(1, 0, 1),
|
||||
lift: number(0, -1, 1), liftColor: color([1, 1, 1, 1], 0, 4), gamma: number(1, 0.01, 4), gammaColor: color([1, 1, 1, 1], 0, 4), gain: number(1, 0, 4), gainColor: color([1, 1, 1, 1], 0, 4),
|
||||
offset: number(0, -1, 1), offsetColor: color([1, 1, 1, 1], 0, 2), power: number(1, 0.01, 4), powerColor: color([1, 1, 1, 1], 0, 4), slope: number(1, 0, 4), slopeColor: color([1, 1, 1, 1], 0, 4),
|
||||
},
|
||||
exposure_contrast: { exposureStops: number(0, -10, 10), contrast: number(1, 0.01, 4), pivot: number(0.18, 0.001, 4), factor: number(1, 0, 1) },
|
||||
saturation: { saturation: number(1, 0, 4), factor: number(1, 0, 1) },
|
||||
channel_mixer: { redOutput: vector([1, 0, 0], -2, 2), greenOutput: vector([0, 1, 0], -2, 2), blueOutput: vector([0, 0, 1], -2, 2), factor: number(1, 0, 1) },
|
||||
bloom_extract: { threshold: number(1, 0, 64), knee: number(0.5, 0, 1) },
|
||||
bloom_blur: {
|
||||
direction: enumeration("horizontal", ["horizontal", "vertical"]),
|
||||
radius: number(1, 1, 16),
|
||||
},
|
||||
bloom_composite: { intensity: number(1, 0, 16) },
|
||||
luminance_edge: { strength: number(2, 0, 16) },
|
||||
frame_out: {
|
||||
surfaceFormat: enumeration("preferred", ["preferred", "rgba8_unorm", "bgra8_unorm", "rgba16_float"]),
|
||||
hdrEnabled: boolean(true),
|
||||
toneMapper: enumeration("aces", ["aces", "reinhard", "none"]),
|
||||
exposureStops: number(0, -10, 10),
|
||||
outputTransfer: enumeration("srgb", ["srgb", "linear"]),
|
||||
scaleMode: enumeration("stretch", ["stretch", "contain", "cover"]),
|
||||
filter: enumeration("linear", ["linear", "nearest"]),
|
||||
backgroundColor: color([0, 0, 0, 1]),
|
||||
},
|
||||
};
|
||||
for (const key of Object.keys(expressionCatalog)) parameterSchemas[key] = {};
|
||||
parameterSchemas.gltf_standard = structuredClone(parameterSchemas.ground_plane);
|
||||
parameterSchemas.gltf_standard_double_sided = structuredClone(parameterSchemas.ground_plane);
|
||||
export const nodeDefinitions = Object.fromEntries(
|
||||
Object.entries(semanticCatalog).map(([key, c]) => {
|
||||
const sockets = {
|
||||
...Object.fromEntries(
|
||||
Object.entries(c.inputs).map(([n, v]) => [
|
||||
n,
|
||||
socket(
|
||||
v.semanticName ?? n,
|
||||
"input",
|
||||
v.authoringType ?? v.accepted.types[0],
|
||||
v.defaultPolicy === "parameter_literal" ? socketDefault(v.accepted.types[0], defaultForInput(key, n, v.accepted.types[0])) : null,
|
||||
v.cardinality.maximum,
|
||||
),
|
||||
]),
|
||||
),
|
||||
...Object.fromEntries(
|
||||
Object.entries(c.outputs).map(([n, v]) => [
|
||||
n,
|
||||
socket(v.semanticName ?? n, "output", v.authoringType ?? v.type),
|
||||
]),
|
||||
),
|
||||
},
|
||||
parameters = parameterSchemas[key];
|
||||
if (
|
||||
!parameters ||
|
||||
Object.keys(parameters).length !== Object.keys(c.parameters).length ||
|
||||
!Object.keys(c.parameters).every((name) =>
|
||||
Object.hasOwn(parameters, name),
|
||||
)
|
||||
)
|
||||
throw new Error(`parameter schema mismatch for ${key}`);
|
||||
return [
|
||||
key,
|
||||
{
|
||||
version: c.version,
|
||||
title: NODE_TITLE_OVERRIDES[key] ?? key.replaceAll("_", " "),
|
||||
behavior: "standard",
|
||||
style: c.execution,
|
||||
parameters,
|
||||
sockets,
|
||||
ui: [
|
||||
...Object.keys(parameters).map((parameter) => ({
|
||||
kind: "parameter",
|
||||
parameter,
|
||||
...(key === "frustum_cull" && parameter === "cameraSelection"
|
||||
? { title: "Camera" }
|
||||
: {}),
|
||||
})),
|
||||
...Object.keys(sockets).map((socket) => ({ kind: "socket", socket })),
|
||||
],
|
||||
muteBypass: [],
|
||||
migrations: [],
|
||||
},
|
||||
];
|
||||
}),
|
||||
);
|
||||
nodeDefinitions.mesh.sockets.localAabb.title = "Local AABB";
|
||||
for (const key of Object.keys(NODE_TITLE_OVERRIDES)) {
|
||||
for (const item of nodeDefinitions[key].ui) {
|
||||
if (item.parameter === "clearColor") item.title = "Initial Color";
|
||||
if (item.parameter === "clearDepth") item.title = "Initial Depth";
|
||||
}
|
||||
}
|
||||
nodeDefinitions.color_balance.ui = [
|
||||
{ kind: "parameter", parameter: "mode" },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
{ title: "Lift", scalar: "lift", color: "liftColor" }, { title: "Gamma", scalar: "gamma", color: "gammaColor" }, { title: "Gain", scalar: "gain", color: "gainColor" },
|
||||
], visibleWhen: { parameter: "mode", equals: "lift_gamma_gain" } },
|
||||
{ kind: "widget", widget: "grading-wheels", bindings: [
|
||||
{ title: "Offset", scalar: "offset", color: "offsetColor" }, { title: "Power", scalar: "power", color: "powerColor" }, { title: "Slope", scalar: "slope", color: "slopeColor" },
|
||||
], visibleWhen: { parameter: "mode", equals: "offset_power_slope" } },
|
||||
{ kind: "parameter", parameter: "factor" },
|
||||
{ kind: "socket", socket: "source" }, { kind: "socket", socket: "colorTarget" }, { kind: "socket", socket: "color" },
|
||||
];
|
||||
nodeDefinitions.frame_out.ui = [
|
||||
{ kind: "text", variant: "section", title: "Canvas Presentation" },
|
||||
{ kind: "parameter", parameter: "surfaceFormat", title: "Surface Format" },
|
||||
{ kind: "text", variant: "section", title: "Display Transform" },
|
||||
{ kind: "parameter", parameter: "hdrEnabled", title: "HDR" },
|
||||
{ kind: "parameter", parameter: "toneMapper", title: "Tone Mapper", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||
{ kind: "parameter", parameter: "exposureStops", title: "Exposure", visibleWhen: { parameter: "hdrEnabled", equals: true } },
|
||||
{ kind: "parameter", parameter: "outputTransfer", title: "Transfer" },
|
||||
{ kind: "parameter", parameter: "scaleMode", title: "Scale" },
|
||||
{ kind: "parameter", parameter: "filter" },
|
||||
{ kind: "parameter", parameter: "backgroundColor", title: "Background", visibleWhen: { parameter: "scaleMode", equals: "contain" } },
|
||||
{ kind: "socket", socket: "color" },
|
||||
];
|
||||
export const fxNodeComposition = Object.freeze({
|
||||
schemaVersion: 2,
|
||||
id: "yawn.render-graph",
|
||||
version: CATALOG_VERSION,
|
||||
compatibility: { wildcardInputTypes: [] },
|
||||
socketTypes,
|
||||
nodeStyles: styles,
|
||||
resources: {},
|
||||
theme,
|
||||
nodes: nodeDefinitions,
|
||||
});
|
||||
export const descriptors = Object.fromEntries(
|
||||
Object.entries(semanticCatalog).map(([key, c]) => [
|
||||
key,
|
||||
{
|
||||
version: c.version,
|
||||
inputs: c.inputs,
|
||||
outputs: c.outputs,
|
||||
parameters: c.parameters,
|
||||
},
|
||||
]),
|
||||
);
|
||||
@@ -1,6 +1,18 @@
|
||||
// FXNode is an optional authoring frontend. Its exporter is intentionally the only
|
||||
// FXNode-aware code that feeds the canonical AST package.
|
||||
export {
|
||||
adaptFxNodeSnapshot,
|
||||
mapAuthoringDiagnostic,
|
||||
} from "./adapter.js";
|
||||
import { createGraphAst } from "@yawn/render-graph-ast";
|
||||
|
||||
/** Exports FXNode nodes with a `pass` payload and links as the canonical pass DAG. */
|
||||
export function adaptFxNodeSnapshot(snapshot, { pipelines = {}, resources = {} } = {}) {
|
||||
if (!Array.isArray(snapshot?.nodes) || !Array.isArray(snapshot?.links)) throw new TypeError("FXNODE_GRAPH");
|
||||
const passes = snapshot.nodes.map(node => {
|
||||
if (!node?.id || !node.pass) throw new TypeError("FXNODE_PASS");
|
||||
return { ...structuredClone(node.pass), id: node.id, after: [...(node.pass.after ?? [])] };
|
||||
});
|
||||
const byId = new Map(passes.map(pass => [pass.id, pass]));
|
||||
for (const link of snapshot.links) {
|
||||
const source = link.fromNodeId ?? link.from?.node;
|
||||
const target = link.toNodeId ?? link.to?.node;
|
||||
if (!byId.has(source) || !byId.has(target)) throw new TypeError("FXNODE_LINK");
|
||||
if (!byId.get(target).after.includes(source)) byId.get(target).after.push(source);
|
||||
}
|
||||
return createGraphAst({ id: snapshot.graphId ?? "fxnode", pipelines, resources, passes });
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user