72 lines
5.4 KiB
TypeScript
72 lines
5.4 KiB
TypeScript
import { GEOMETRY_NODE_GRAPH_SCHEMA, type GeometryNodeDataType, type GeometryNodeGraphIR, type GeometryNodeIR, type GeometryNodeLinkIR, type GeometryNodeSocketIR } from "./geometry-nodes";
|
|
import type { WebEngineEditCommand } from "./web-engine";
|
|
|
|
const clone = <T,>(value: T): T => JSON.parse(JSON.stringify(value)) as T;
|
|
|
|
const socket = (id: string, name: string, direction: "INPUT" | "OUTPUT", dataType: GeometryNodeDataType, defaultValue?: boolean | number | string | number[]): GeometryNodeSocketIR => ({ id, name, direction, dataType, ...(defaultValue === undefined ? {} : { defaultValue }) });
|
|
|
|
const nodeSockets = (type: string): GeometryNodeSocketIR[] => {
|
|
if (type === "NodeGroupInput") return [socket("Geometry", "Geometry", "OUTPUT", "GEOMETRY")];
|
|
if (type === "NodeGroupOutput") return [socket("Geometry", "Geometry", "INPUT", "GEOMETRY")];
|
|
if (type === "GeometryNodeTransform") return [socket("Geometry", "Geometry", "INPUT", "GEOMETRY"), socket("Translation", "Translation", "INPUT", "VECTOR", [0, 0, 0]), socket("Rotation", "Rotation", "INPUT", "VECTOR", [0, 0, 0]), socket("Scale", "Scale", "INPUT", "VECTOR", [1, 1, 1]), socket("Geometry", "Geometry", "OUTPUT", "GEOMETRY")];
|
|
if (type === "GeometryNodeSetPosition") return [socket("Geometry", "Geometry", "INPUT", "GEOMETRY"), socket("Offset", "Offset", "INPUT", "VECTOR", [0, 0, 0]), socket("Position", "Position", "INPUT", "VECTOR", [0, 0, 0]), socket("Geometry", "Geometry", "OUTPUT", "GEOMETRY")];
|
|
if (type === "GeometryNodeJoinGeometry") return [socket("Geometry", "Geometry", "INPUT", "GEOMETRY"), socket("Geometry", "Geometry", "OUTPUT", "GEOMETRY")];
|
|
if (type === "GeometryNodeRealizeInstances") return [socket("Geometry", "Geometry", "INPUT", "GEOMETRY"), socket("Geometry", "Geometry", "OUTPUT", "GEOMETRY")];
|
|
if (type === "FunctionNodeInputInt") return [socket("Value", "Value", "OUTPUT", "INT", 0)];
|
|
if (type === "FunctionNodeInputVector") return [socket("Vector", "Vector", "OUTPUT", "VECTOR", [0, 0, 0])];
|
|
if (type === "ShaderNodeValue") return [socket("Value", "Value", "OUTPUT", "FLOAT", 0)];
|
|
if (type === "ShaderNodeMath") return [socket("Value", "Value", "INPUT", "FLOAT", 0), socket("Value_001", "Value", "INPUT", "FLOAT", 0), socket("Value", "Value", "OUTPUT", "FLOAT")];
|
|
return [];
|
|
};
|
|
|
|
function uniqueId(base: string, ids: ReadonlySet<string>): string {
|
|
let id = base;
|
|
let suffix = 2;
|
|
while (ids.has(id)) id = `${base}-${suffix++}`;
|
|
return id;
|
|
}
|
|
|
|
/** Workbench constructors for validated Geometry Node graph transactions. */
|
|
export const geometryNodeCommands = Object.freeze({
|
|
createGraph(meshId: string, name = "Web Geometry") : GeometryNodeGraphIR {
|
|
const input = { id: "group-input", type: "NodeGroupInput", name: "Group Input", sockets: [socket("Geometry", "Geometry", "OUTPUT", "GEOMETRY")] };
|
|
const output = { id: "group-output", type: "NodeGroupOutput", name: "Group Output", sockets: [socket("Geometry", "Geometry", "INPUT", "GEOMETRY")] };
|
|
return { schemaVersion: GEOMETRY_NODE_GRAPH_SCHEMA, id: `geometry-graph:${meshId}`, name, interfaceInputs: [socket("Geometry", "Geometry", "INPUT", "GEOMETRY")], interfaceOutputs: [socket("Geometry", "Geometry", "OUTPUT", "GEOMETRY")], nodes: [input, output], links: [{ fromNodeId: input.id, fromSocketId: "Geometry", toNodeId: output.id, toSocketId: "Geometry" }] };
|
|
},
|
|
addNode(graph: GeometryNodeGraphIR, type: string, name = type): GeometryNodeGraphIR {
|
|
const next = clone(graph);
|
|
const ids = new Set(next.nodes.map((node) => node.id));
|
|
const id = uniqueId(`node-${type.replace(/[^a-z0-9]+/gi, "-").toLowerCase()}`, ids);
|
|
const node: GeometryNodeIR = { id, type, name: name.trim() || type, sockets: nodeSockets(type) };
|
|
next.nodes.push(node);
|
|
return next;
|
|
},
|
|
removeNode(graph: GeometryNodeGraphIR, nodeId: string): GeometryNodeGraphIR {
|
|
const next = clone(graph);
|
|
next.nodes = next.nodes.filter((node) => node.id !== nodeId);
|
|
next.links = next.links.filter((link) => link.fromNodeId !== nodeId && link.toNodeId !== nodeId);
|
|
return next;
|
|
},
|
|
connect(graph: GeometryNodeGraphIR, link: GeometryNodeLinkIR): GeometryNodeGraphIR {
|
|
const next = clone(graph);
|
|
next.links = next.links.filter((item) => !(item.toNodeId === link.toNodeId && item.toSocketId === link.toSocketId));
|
|
if (!next.links.some((item) => item.fromNodeId === link.fromNodeId && item.fromSocketId === link.fromSocketId && item.toNodeId === link.toNodeId && item.toSocketId === link.toSocketId)) next.links.push(link);
|
|
return next;
|
|
},
|
|
disconnect(graph: GeometryNodeGraphIR, link: GeometryNodeLinkIR): GeometryNodeGraphIR {
|
|
const next = clone(graph);
|
|
next.links = next.links.filter((item) => !(item.fromNodeId === link.fromNodeId && item.fromSocketId === link.fromSocketId && item.toNodeId === link.toNodeId && item.toSocketId === link.toSocketId));
|
|
return next;
|
|
},
|
|
setSocketValue(graph: GeometryNodeGraphIR, nodeId: string, socketId: string, value: GeometryNodeSocketIR["defaultValue"]): GeometryNodeGraphIR {
|
|
const next = clone(graph);
|
|
const node = next.nodes.find((candidate) => candidate.id === nodeId);
|
|
const target = node?.sockets.find((candidate) => candidate.id === socketId && candidate.direction === "INPUT");
|
|
if (target) target.defaultValue = value;
|
|
return next;
|
|
},
|
|
setGraph(meshId: string, graph: GeometryNodeGraphIR): Extract<WebEngineEditCommand, { type: "setGeometryNodeGraph" }> {
|
|
return { type: "setGeometryNodeGraph", meshId, graph };
|
|
},
|
|
} as const);
|