feat: initial group entering ui
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m7s
🚀 Lint & Test & Deploy / quality (pull_request) Failing after 45s
🚀 Lint & Test & Deploy / test-unit (pull_request) Failing after 30s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Failing after 33s
🚀 Lint & Test & Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
2026-05-03 18:23:40 +02:00
parent 7499b80789
commit 9c9a7b8c67
6 changed files with 141 additions and 14 deletions
@@ -76,6 +76,13 @@ export class GraphManager extends EventEmitter<{
edges = $state<Edge[]>([]); edges = $state<Edge[]>([]);
graphStack: {
rootGraph: Graph;
groupId: number;
nodeId: number;
cameraPosition: [number, number, number];
}[] = $state([]);
settingTypes: Record<string, NodeInput> = {}; settingTypes: Record<string, NodeInput> = {};
settings = $state<Record<string, unknown>>(); settings = $state<Record<string, unknown>>();
@@ -90,9 +97,25 @@ export class GraphManager extends EventEmitter<{
}); });
history: HistoryManager = new HistoryManager(); 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(() => { execute = throttle(() => {
if (this.loaded === false) return; if (this.loaded === false) return;
this.emit('result', this.serialize()); this.emit('result', this.serializeFullGraph());
}, 10); }, 10);
constructor(public registry: NodeRegistry) { constructor(public registry: NodeRegistry) {
@@ -290,13 +313,11 @@ export class GraphManager extends EventEmitter<{
private _init(graph: Graph) { private _init(graph: Graph) {
const nodes = new SvelteMap( const nodes = new SvelteMap(
graph.nodes.map((node) => { graph.nodes.map((node) => {
const nodeType = this.registry.getNode(node.type); const n = { ...node } as NodeInstance;
const n = node as NodeInstance; const registryType = this.registry.getNode(node.type);
if (nodeType) { n.state = registryType ? { type: registryType } : {};
n.state = { const resolvedType = this.getNodeType(n);
type: nodeType if (resolvedType) n.state = { type: resolvedType };
};
}
return [node.id, n]; return [node.id, n];
}) })
); );
@@ -436,6 +457,31 @@ export class GraphManager extends EventEmitter<{
} }
getNodeType(node: NodeInstance) { 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 // Construct the group inputs on the fly
if (node.type === '__internal/group/instance') { if (node.type === '__internal/group/instance') {
const groupDefinition = this.getGroup(node.props?.groupId as number); 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); 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() { createNodeId() {
const ids = [ const ids = [
...this.nodes.keys(), ...this.nodes.keys(),
@@ -900,8 +981,9 @@ export class GraphManager extends EventEmitter<{
return; return;
} }
this.emit('save', state); const fullState = this.graphStack.length > 0 ? this.serializeFullGraph() : state;
logger.log('saving graphs', state); this.emit('save', fullState);
logger.log('saving graphs', fullState);
} }
getParentsOfNode(node: NodeInstance) { getParentsOfNode(node: NodeInstance) {
@@ -388,8 +388,21 @@ export class GraphState {
enterGroupNode() { enterGroupNode() {
if (this.activeNodeId === -1) return; if (this.activeNodeId === -1) return;
const selectedNode = this.graph.getNode(this.activeNodeId); const node = this.graph.getNode(this.activeNodeId);
if (!selectedNode || selectedNode.type.startsWith('__internal/group/instance')) return; 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( getSocketPosition(
@@ -169,6 +169,12 @@
{/if} {/if}
{#if graph.status === 'idle'} {#if graph.status === 'idle'}
{#if graph.isInsideGroup}
<HTML transform={false}>
<button class="exit-group" onclick={() => graphState.exitGroupNode()}> Exit Group</button>
</HTML>
{/if}
{#if graphState.addMenuPosition} {#if graphState.addMenuPosition}
<AddMenu <AddMenu
onnode={handleNodeCreation} onnode={handleNodeCreation}
@@ -244,6 +250,26 @@
height: 100%; height: 100%;
} }
:global(.exit-group) {
position: fixed;
top: 12px;
left: 50%;
transform: translateX(-50%);
z-index: 1000;
padding: 4px 12px;
background: var(--color-layer-2);
border: 1px solid var(--stroke);
border-radius: 4px;
color: inherit;
font-size: 0.85em;
cursor: pointer;
opacity: 0.85;
}
:global(.exit-group:hover) {
opacity: 1;
}
.wrapper { .wrapper {
position: absolute; position: absolute;
z-index: 100; z-index: 100;
+4
View File
@@ -47,6 +47,10 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
key: 'Escape', key: 'Escape',
description: 'Deselect nodes', description: 'Deselect nodes',
callback: () => { callback: () => {
if (graph.isInsideGroup) {
graphState.exitGroupNode();
return;
}
graphState.activeNodeId = -1; graphState.activeNodeId = -1;
graphState.clearSelection(); graphState.clearSelection();
graphState.edgeEndPosition = null; graphState.edgeEndPosition = null;
+2 -1
View File
@@ -4,7 +4,8 @@ export function grid(width: number, height: number) {
const graph: Graph = { const graph: Graph = {
id: Math.floor(Math.random() * 100000), id: Math.floor(Math.random() * 100000),
edges: [], edges: [],
nodes: [] nodes: [],
groups: []
}; };
const amount = width * height; const amount = width * height;
+2 -1
View File
@@ -47,6 +47,7 @@ export function tree(depth: number): Graph {
return { return {
id: Math.floor(Math.random() * 100000), id: Math.floor(Math.random() * 100000),
nodes, nodes,
edges edges,
groups: []
}; };
} }