feat(web): add an interactive Compositor node canvas

Author Compositor graphs in the web UI with React Flow and execute
them locally. The native writer materializes RGB/Exposure/Invert/Blur/
Mix/Alpha Over/Transform/Viewer/Composite nodes into a real
CompositorNodeTree, the reader returns node locations and more node
types, and save/reopen keeps nodes, links, positions and properties.

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
mes123456
2026-11-13 05:35:30 +08:00
parent 4efb4b778d
commit ef50fdd988
18 changed files with 933 additions and 19 deletions

View File

@@ -67,6 +67,8 @@ export interface CompositorNodeIR {
name: string;
blenderType?: string;
properties: Record<string, unknown>;
/** Canvas position in Blender node-space units. */
location?: { x: number; y: number };
}
export interface CompositorLinkIR {
@@ -279,7 +281,11 @@ export function parseCompositorGraph(value: unknown): CompositorGraphIR {
const id = text(item.id, `nodes[${index}].id`);
if (nodeIds.has(id)) throw new CompositorValidationError("COMPOSITOR_GRAPH_INVALID", `Duplicate node ${id}`);
nodeIds.add(id);
const node = { id, type: item.type as CompositorNodeType, name: text(item.name, `nodes[${index}].name`), properties: properties(item.properties ?? {}, index), blenderType: item.blenderType === undefined ? undefined : text(item.blenderType, `nodes[${index}].blenderType`) };
const node: CompositorNodeIR = { id, type: item.type as CompositorNodeType, name: text(item.name, `nodes[${index}].name`), properties: properties(item.properties ?? {}, index), blenderType: item.blenderType === undefined ? undefined : text(item.blenderType, `nodes[${index}].blenderType`) };
if (item.location !== undefined) {
if (!record(item.location)) throw new CompositorValidationError("COMPOSITOR_GRAPH_INVALID", `nodes[${index}].location is invalid`);
node.location = { x: finite(item.location.x, `nodes[${index}].location.x`, -1_000_000, 1_000_000), y: finite(item.location.y, `nodes[${index}].location.y`, -1_000_000, 1_000_000) };
}
validateNodeProperties(node, index);
return node;
});