Files
workinf_Blender_Wasm/web/protocol/compositor-commands.ts
mes123456 d0b6d2a164 feat(web): close remaining editor, layout and persistence gaps
- Area headers get a per-area editor switcher backed by a shared
  AreaEditorContext; Preferences can hide it.
- Animation camera area is a real navigable viewport aligned to the
  scene camera; Graph Editor adds channel selection, keyframe drag/
  insert/delete and interpolation.
- Geometry Nodes node locations round-trip through the native writer
  and reader, so saved graphs keep their layout.
- Texture Paint can make linked library images local via a new native
  makeImageLocal command before packing strokes.
- Compositor IMAGE and Render Layers nodes now persist to Main and
  execute against packed images / the last PBR render.
- Spreadsheet row cap raised to 250k; Outliner gains Ctrl+A and arrow/
  Home/End navigation.

Co-Authored-By: Claude Code <noreply@anthropic.com>
2026-11-13 07:47:10 +08:00

97 lines
4.4 KiB
TypeScript

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<string, unknown> {
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 {};
}
}