refactor: redesign render graph nodes

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:
Amp
2026-07-28 13:00:29 +00:00
co-authored by heaust
parent fc8c16daec
commit edc7c8ef29
23 changed files with 5229 additions and 1810 deletions
+267 -41
View File
@@ -56,14 +56,20 @@ const sourceMaps = new WeakMap();
export const getSourceMap = (ir) => sourceMaps.get(ir);
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 path = [
details?.path,
diagnostic?.path,
details?.field,
diagnostic?.field,
].find((value) => typeof value === "string");
const map = getSourceMap(ir);
let match;
if (path && map)
for (const key of Object.keys(map))
if (
(path === key || path.startsWith(`${key}.`) || path.startsWith(`${key}[`)) &&
(path === key ||
path.startsWith(`${key}.`) ||
path.startsWith(`${key}[`)) &&
(!match || key.length > match.length)
)
match = key;
@@ -80,33 +86,49 @@ export const mapAuthoringDiagnostic = (ir, diagnostic) => {
const mapValuePaths = (paths, path, source, value) => {
paths[path] = source;
if (Array.isArray(value))
value.forEach((child, index) => mapValuePaths(paths, `${path}[${index}]`, source, child));
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) {
const expected = schema.type === "json" ? "json" : schema.type;
if (
!exactKeys(raw, ["kind", "value"]) ||
raw.kind !== expected ||
!finiteJson(raw.value)
)
fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
if (
(expected === "number" && typeof raw.value !== "number") ||
(expected === "string" && typeof raw.value !== "string") ||
(expected === "boolean" && typeof raw.value !== "boolean") ||
(expected === "json" && !finiteJson(raw.value))
)
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 === (schema.type === "vector" ? 3 : 4) &&
value.every(bounded)
: schema.type === "json" && finiteJson(value);
if (!valid) fail("AUTHORING_PARAMETER", { nodeId, parameter: key });
return canonical(structuredClone(raw.value));
}
export function adaptFxNodeSnapshot(raw, revision = 1) {
try {
const rootKeys = ["graphId", "catalogVersion", "nodes", "links", "metadata", "version"];
const rootKeys = [
"graphId",
"catalogVersion",
"nodes",
"links",
"metadata",
"version",
];
if (
!exactKeys(raw, rootKeys) ||
!Array.isArray(raw.nodes) ||
@@ -117,9 +139,7 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
fail("AUTHORING_SHAPE");
if (raw.graphId !== GRAPH_ID || raw.catalogVersion !== CATALOG_VERSION)
fail("AUTHORING_CATALOG");
if (
!Number.isSafeInteger(raw.version) || raw.version < 0
)
if (!Number.isSafeInteger(raw.version) || raw.version < 0)
fail("AUTHORING_SHAPE");
if (!Number.isInteger(revision) || revision < 1 || revision > 0xffffffff)
fail("AUTHORING_REVISION");
@@ -134,7 +154,20 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
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"];
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) ||
@@ -143,10 +176,17 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
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 ||
!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) ||
!object(n.extensions) ||
!finiteJson(n.extensions) ||
!Array.isArray(n.sockets) ||
!object(n.parameters)
)
@@ -168,6 +208,48 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
),
]),
);
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),
@@ -181,17 +263,49 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
socketDefinition = definition.sockets[s.key],
direction = input ? "input" : "output",
dataType = socketDefinition.type,
socketKeys = ["id", "key", "label", "direction", "dataType", "accepts", "maxIncomingLinks", ...(socketDefinition.value ? ["defaultValue"] : []), "visible"];
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)) ||
!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
? !exactKeys(s.defaultValue, ["kind", "value"]) || !finiteJson(s.defaultValue.value)
? (() => {
try {
parameterValue(
s.defaultValue,
socketDefinition.value,
n.id,
s.key,
);
return false;
} catch {
return true;
}
})()
: s.defaultValue !== undefined) ||
s.visible !== socketDefinition.visible ||
s.maxIncomingLinks !== socketDefinition.maxIncomingLinks
@@ -206,10 +320,21 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
: 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 });
if (n.typeId === "mesh_query") {
parameters.visibleDefault = sockets.get(
`${n.id}:isVisible`,
).defaultValue.value;
parameters.frustumCulledDefault = sockets.get(
`${n.id}:isFrustumCulled`,
).defaultValue.value;
}
nodes.set(n.id, {
ordinal,
value: {
@@ -230,8 +355,18 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
!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)
!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);
@@ -244,21 +379,33 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
link.toNodeId !== to.node ||
from.direction !== "output" ||
to.direction !== "input" ||
(!link.muted && (incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
(!link.muted &&
(incoming.get(link.toSocketId) ?? 0) >= to.maxIncomingLinks)
)
fail(
!link.muted && (incoming.get(link.toSocketId) ?? 0) >= (to?.maxIncomingLinks ?? Infinity)
!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)
!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))
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",
@@ -299,13 +446,92 @@ export function adaptFxNodeSnapshot(raw, revision = 1) {
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"])
for (const field of [
"id",
"state",
"executor",
"executor.key",
"executor.version",
])
paths[`${base}.${field}`] = nodeSource;
paths[`${base}.parameters`] = nodeSource;
for (const key of Object.keys(item.value.parameters))
mapValuePaths(paths, `${base}.parameters.${key}`, { kind: "parameter", nodeId: item.value.id, parameter: key }, item.value.parameters[key]);
for (const key of Object.keys(descriptors[item.value.executor.key].inputs)) {
const link = raw.links.find((x) => !x.muted && x.toNodeId === item.value.id && sockets.get(x.toSocketId)?.key === key);
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}`,
item.value.executor.key === "mesh_query" && key.endsWith("Default")
? {
kind: "input",
nodeId: item.value.id,
input:
key === "visibleDefault" ? "isVisible" : "isFrustumCulled",
socketId: `${item.value.id}:${key === "visibleDefault" ? "isVisible" : "isFrustumCulled"}`,
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 link = raw.links.find(
(x) =>
!x.muted &&
x.toNodeId === item.value.id &&
sockets.get(x.toSocketId)?.key === key,
);
const source = linkSources.get(link?.id) ?? {
kind: "input",
nodeId: item.value.id,