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:
+267
-41
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user