From 9c9a7b8c6781a71e88538024af450f8bdbceb228 Mon Sep 17 00:00:00 2001 From: Max Richter Date: Sun, 3 May 2026 18:23:40 +0200 Subject: [PATCH] feat: initial group entering ui --- .../graph-interface/graph-manager.svelte.ts | 102 ++++++++++++++++-- .../lib/graph-interface/graph-state.svelte.ts | 17 ++- .../lib/graph-interface/graph/Graph.svelte | 26 +++++ app/src/lib/graph-interface/keymaps.ts | 4 + app/src/lib/graph-templates/grid.ts | 3 +- app/src/lib/graph-templates/tree.ts | 3 +- 6 files changed, 141 insertions(+), 14 deletions(-) diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index d123933..af30f10 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -76,6 +76,13 @@ export class GraphManager extends EventEmitter<{ edges = $state([]); + graphStack: { + rootGraph: Graph; + groupId: number; + nodeId: number; + cameraPosition: [number, number, number]; + }[] = $state([]); + settingTypes: Record = {}; settings = $state>(); @@ -90,9 +97,25 @@ export class GraphManager extends EventEmitter<{ }); history: HistoryManager = new HistoryManager(); + + private serializeFullGraph(): Graph { + if (this.graphStack.length === 0) return this.serialize(); + let merged = this.serialize(); + for (let i = this.graphStack.length - 1; i >= 0; i--) { + const { rootGraph, groupId } = this.graphStack[i]; + merged = { + ...rootGraph, + groups: rootGraph.groups.map(g => + g.id === groupId ? { ...g, nodes: merged.nodes, edges: merged.edges } : g + ) + }; + } + return merged; + } + execute = throttle(() => { if (this.loaded === false) return; - this.emit('result', this.serialize()); + this.emit('result', this.serializeFullGraph()); }, 10); constructor(public registry: NodeRegistry) { @@ -290,13 +313,11 @@ export class GraphManager extends EventEmitter<{ private _init(graph: Graph) { const nodes = new SvelteMap( graph.nodes.map((node) => { - const nodeType = this.registry.getNode(node.type); - const n = node as NodeInstance; - if (nodeType) { - n.state = { - type: nodeType - }; - } + const n = { ...node } as NodeInstance; + const registryType = this.registry.getNode(node.type); + n.state = registryType ? { type: registryType } : {}; + const resolvedType = this.getNodeType(n); + if (resolvedType) n.state = { type: resolvedType }; return [node.id, n]; }) ); @@ -436,6 +457,31 @@ export class GraphManager extends EventEmitter<{ } getNodeType(node: NodeInstance) { + if (node.type === '__internal/group/input') { + const groupId = this.graphStack.at(-1)?.groupId; + const group = groupId !== undefined ? this.getGroup(groupId) : undefined; + if (!group) return node.state.type; + return { + id: '__internal/group/input' as NodeId, + outputs: Object.values(group.inputs ?? {}).map(i => i.type), + execute: (x: Int32Array) => x + } as NodeDefinition; + } + + if (node.type === '__internal/group/output') { + const groupId = this.graphStack.at(-1)?.groupId; + const group = groupId !== undefined ? this.getGroup(groupId) : undefined; + if (!group) return node.state.type; + return { + id: '__internal/group/output' as NodeId, + inputs: Object.fromEntries( + (group.outputs ?? []).map((o, i) => [`out_${i}`, { type: o.type, label: o.label }]) + ), + outputs: [], + execute: (x: Int32Array) => x + } as NodeDefinition; + } + // Construct the group inputs on the fly if (node.type === '__internal/group/instance') { const groupDefinition = this.getGroup(node.props?.groupId as number); @@ -576,6 +622,41 @@ export class GraphManager extends EventEmitter<{ return this.graph.groups.find(g => g.id === id); } + get isInsideGroup() { + return this.graphStack.length > 0; + } + + enterGroup(nodeId: number, cameraPosition: [number, number, number]): boolean { + const groupNode = this.getNode(nodeId); + if (!groupNode || groupNode.type !== '__internal/group/instance') return false; + const groupId = groupNode.props?.groupId as number; + const group = this.getGroup(groupId); + if (!group) return false; + + this.graphStack.push({ rootGraph: this.serialize(), groupId, nodeId, cameraPosition }); + this.graph = { ...this.graph, nodes: group.nodes, edges: group.edges }; + this._init(this.graph); + this.history.reset(); + return true; + } + + exitGroup(): { camera: [number, number, number]; nodeId: number } | false { + if (!this.graphStack.length) return false; + const { rootGraph, groupId, nodeId, cameraPosition } = this.graphStack.pop()!; + const internalState = this.serialize(); + const updatedRoot = { + ...rootGraph, + groups: rootGraph.groups.map(g => + g.id === groupId ? { ...g, nodes: internalState.nodes, edges: internalState.edges } : g + ) + }; + this.graph = updatedRoot; + this._init(updatedRoot); + this.history.reset(); + this.save(); + return { camera: cameraPosition, nodeId }; + } + createNodeId() { const ids = [ ...this.nodes.keys(), @@ -900,8 +981,9 @@ export class GraphManager extends EventEmitter<{ return; } - this.emit('save', state); - logger.log('saving graphs', state); + const fullState = this.graphStack.length > 0 ? this.serializeFullGraph() : state; + this.emit('save', fullState); + logger.log('saving graphs', fullState); } getParentsOfNode(node: NodeInstance) { diff --git a/app/src/lib/graph-interface/graph-state.svelte.ts b/app/src/lib/graph-interface/graph-state.svelte.ts index ce5b24e..1d5a908 100644 --- a/app/src/lib/graph-interface/graph-state.svelte.ts +++ b/app/src/lib/graph-interface/graph-state.svelte.ts @@ -388,8 +388,21 @@ export class GraphState { enterGroupNode() { if (this.activeNodeId === -1) return; - const selectedNode = this.graph.getNode(this.activeNodeId); - if (!selectedNode || selectedNode.type.startsWith('__internal/group/instance')) return; + const node = this.graph.getNode(this.activeNodeId); + if (!node || node.type !== '__internal/group/instance') return; + const ok = this.graph.enterGroup(this.activeNodeId, [...this.cameraPosition]); + if (ok) { + this.activeNodeId = -1; + this.clearSelection(); + } + } + + exitGroupNode() { + const result = this.graph.exitGroup(); + if (!result) return; + this.cameraPosition = result.camera; + this.activeNodeId = -1; + this.clearSelection(); } getSocketPosition( diff --git a/app/src/lib/graph-interface/graph/Graph.svelte b/app/src/lib/graph-interface/graph/Graph.svelte index bbe40e1..e5e48cb 100644 --- a/app/src/lib/graph-interface/graph/Graph.svelte +++ b/app/src/lib/graph-interface/graph/Graph.svelte @@ -169,6 +169,12 @@ {/if} {#if graph.status === 'idle'} + {#if graph.isInsideGroup} + + + + {/if} + {#if graphState.addMenuPosition} { + if (graph.isInsideGroup) { + graphState.exitGroupNode(); + return; + } graphState.activeNodeId = -1; graphState.clearSelection(); graphState.edgeEndPosition = null; diff --git a/app/src/lib/graph-templates/grid.ts b/app/src/lib/graph-templates/grid.ts index bc759e6..7510243 100644 --- a/app/src/lib/graph-templates/grid.ts +++ b/app/src/lib/graph-templates/grid.ts @@ -4,7 +4,8 @@ export function grid(width: number, height: number) { const graph: Graph = { id: Math.floor(Math.random() * 100000), edges: [], - nodes: [] + nodes: [], + groups: [] }; const amount = width * height; diff --git a/app/src/lib/graph-templates/tree.ts b/app/src/lib/graph-templates/tree.ts index e382a1c..6c39992 100644 --- a/app/src/lib/graph-templates/tree.ts +++ b/app/src/lib/graph-templates/tree.ts @@ -47,6 +47,7 @@ export function tree(depth: number): Graph { return { id: Math.floor(Math.random() * 100000), nodes, - edges + edges, + groups: [] }; }