import type { CompositorGraphIR, CompositorLinkIR, CompositorNodeIR, CompositorNodeType, CompositorResourceIR } from "./compositor"; function clone(graph: CompositorGraphIR): CompositorGraphIR { return structuredClone(graph); } /** Commands and pure graph edits for the Compositor canvas. */ export const compositorCommands = Object.freeze({ evaluate(graph: CompositorGraphIR): CompositorGraphIR { return structuredClone(graph); }, setNodeGraph(sceneId: string, graph: CompositorGraphIR) { return { type: "setCompositorNodeGraph", sceneId, graph } as const; }, createGraph(id: string, name: string): CompositorGraphIR { const composite: CompositorNodeIR = { id: "node-composite", type: "COMPOSITE", name: "Composite", properties: {}, location: { x: 620, y: 80 }, }; return { schemaVersion: 1, id, name, outputNodeId: composite.id, nodes: [composite], links: [], resources: [] }; }, addNode(graph: CompositorGraphIR, type: CompositorNodeType, name: string, location?: { x: number; y: number }): CompositorGraphIR { const next = clone(graph); let index = 1; let id = `node-${type.toLowerCase().replaceAll("_", "-")}-${index}`; while (next.nodes.some((node) => node.id === id)) id = `node-${type.toLowerCase().replaceAll("_", "-")}-${++index}`; const node: CompositorNodeIR = { id, type, name, properties: defaultProperties(type), location: location ?? { x: 90 + (next.nodes.length % 4) * 210, y: 60 + Math.floor(next.nodes.length / 4) * 150 }, }; next.nodes.push(node); return next; }, removeNode(graph: CompositorGraphIR, nodeId: string): CompositorGraphIR { const next = clone(graph); if (nodeId === next.outputNodeId) return graph; next.nodes = next.nodes.filter((node) => node.id !== nodeId); next.links = next.links.filter((link) => link.fromNodeId !== nodeId && link.toNodeId !== nodeId); return next; }, moveNode(graph: CompositorGraphIR, nodeId: string, location: { x: number; y: number }): CompositorGraphIR { const next = clone(graph); const node = next.nodes.find((candidate) => candidate.id === nodeId); if (node) node.location = { x: location.x, y: location.y }; return next; }, connect(graph: CompositorGraphIR, link: CompositorLinkIR): CompositorGraphIR { const next = clone(graph); if (!next.nodes.some((node) => node.id === link.fromNodeId) || !next.nodes.some((node) => node.id === link.toNodeId)) return graph; next.links = next.links.filter((candidate) => !(candidate.toNodeId === link.toNodeId && candidate.toSocket === link.toSocket)); next.links.push({ ...link }); return next; }, disconnect(graph: CompositorGraphIR, toNodeId: string, toSocket: string): CompositorGraphIR { const next = clone(graph); next.links = next.links.filter((link) => !(link.toNodeId === toNodeId && link.toSocket === toSocket)); return next; }, setProperty(graph: CompositorGraphIR, nodeId: string, key: string, value: unknown): CompositorGraphIR { const next = clone(graph); const node = next.nodes.find((candidate) => candidate.id === nodeId); if (node) node.properties = { ...node.properties, [key]: value }; return next; }, upsertResource(graph: CompositorGraphIR, resource: CompositorResourceIR): CompositorGraphIR { const next = clone(graph); const index = next.resources.findIndex((candidate) => candidate.id === resource.id); if (index >= 0) next.resources[index] = { ...resource }; else next.resources.push({ ...resource }); return next; }, removeResource(graph: CompositorGraphIR, resourceId: string): CompositorGraphIR { const next = clone(graph); next.resources = next.resources.filter((resource) => resource.id !== resourceId); return next; }, setOutput(graph: CompositorGraphIR, nodeId: string): CompositorGraphIR { const next = clone(graph); if (next.nodes.find((node) => node.id === nodeId)?.type !== "COMPOSITE") return graph; next.outputNodeId = nodeId; return next; }, }); export function defaultProperties(type: CompositorNodeType): Record { switch (type) { case "CONSTANT_COLOR": return { color: [0.2, 0.45, 0.8, 1] }; case "EXPOSURE": return { exposure: 0 }; case "INVERT": return {}; case "BLUR": return { radius: 4 }; case "MIX": return { factor: 0.5 }; case "TRANSFORM": return { translateX: 0, translateY: 0, scaleX: 1, scaleY: 1 }; case "VIEWER": return {}; default: return {}; } }