Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
9c9a7b8c67
|
@@ -19,7 +19,7 @@ import EventEmitter from './helpers/EventEmitter';
|
||||
import { HistoryManager } from './history-manager';
|
||||
|
||||
const logger = createLogger('graph-manager');
|
||||
// logger.mute();
|
||||
logger.mute();
|
||||
|
||||
const remoteRegistry = new RemoteNodeRegistry('');
|
||||
|
||||
@@ -73,21 +73,15 @@ export class GraphManager extends EventEmitter<{
|
||||
id = $state(0);
|
||||
|
||||
nodes = new SvelteMap<number, NodeInstance>();
|
||||
nodeArray = $derived(Array.from(this.nodes.values()));
|
||||
|
||||
edges = $state<Edge[]>([]);
|
||||
|
||||
// Plain array — NOT $state. rootGraph items are plain-serialized (safe for structuredClone).
|
||||
// savedNodes/savedEdges hold live reactive references so reactivity is preserved on exit.
|
||||
graphStack: {
|
||||
rootGraph: Graph;
|
||||
savedNodes: Map<number, NodeInstance>;
|
||||
savedEdges: Edge[];
|
||||
outerGraph: Graph;
|
||||
groupId: number;
|
||||
nodeId: number;
|
||||
cameraPosition: [number, number, number];
|
||||
}[] = [];
|
||||
}[] = $state([]);
|
||||
|
||||
settingTypes: Record<string, NodeInput> = {};
|
||||
settings = $state<Record<string, unknown>>();
|
||||
@@ -292,28 +286,6 @@ export class GraphManager extends EventEmitter<{
|
||||
});
|
||||
}
|
||||
|
||||
tryConnectToDebugNode(nodeId: number) {
|
||||
const node = this.nodes.get(nodeId);
|
||||
if (!node) return;
|
||||
if (node.type.endsWith('/debug')) return;
|
||||
if (!node.state.type?.outputs?.length) return;
|
||||
let debugNode = this.nodes.values().find(n => n.type.endsWith('/debug'));
|
||||
|
||||
if (!debugNode) {
|
||||
debugNode = this.createNode({
|
||||
type: '__internal/node/debug',
|
||||
position: [node.position[0] + 30, node.position[1]],
|
||||
props: {}
|
||||
});
|
||||
}
|
||||
|
||||
if (debugNode) {
|
||||
this.createEdge(node, 0, debugNode, 'input');
|
||||
}
|
||||
|
||||
return debugNode;
|
||||
}
|
||||
|
||||
getEdgesBetweenNodes(nodes: NodeInstance[]): [number, number, number, string][] {
|
||||
const edges = [];
|
||||
for (const node of nodes) {
|
||||
@@ -341,7 +313,7 @@ export class GraphManager extends EventEmitter<{
|
||||
private _init(graph: Graph) {
|
||||
const nodes = new SvelteMap(
|
||||
graph.nodes.map((node) => {
|
||||
const n = node as NodeInstance;
|
||||
const n = { ...node } as NodeInstance;
|
||||
const registryType = this.registry.getNode(node.type);
|
||||
n.state = registryType ? { type: registryType } : {};
|
||||
const resolvedType = this.getNodeType(n);
|
||||
@@ -485,11 +457,6 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
|
||||
getNodeType(node: NodeInstance) {
|
||||
if (!node) {
|
||||
console.trace('failed to get node type');
|
||||
return;
|
||||
}
|
||||
|
||||
if (node.type === '__internal/group/input') {
|
||||
const groupId = this.graphStack.at(-1)?.groupId;
|
||||
const group = groupId !== undefined ? this.getGroup(groupId) : undefined;
|
||||
@@ -508,10 +475,7 @@ export class GraphManager extends EventEmitter<{
|
||||
return {
|
||||
id: '__internal/group/output' as NodeId,
|
||||
inputs: Object.fromEntries(
|
||||
(group.outputs ?? []).map((
|
||||
o,
|
||||
i
|
||||
) => [`out_${i}`, { type: o.type, label: o.label, external: true }])
|
||||
(group.outputs ?? []).map((o, i) => [`out_${i}`, { type: o.type, label: o.label }])
|
||||
),
|
||||
outputs: [],
|
||||
execute: (x: Int32Array) => x
|
||||
@@ -528,15 +492,15 @@ export class GraphManager extends EventEmitter<{
|
||||
}
|
||||
|
||||
const inputs = {
|
||||
...(node.state.type?.inputs || {}),
|
||||
...groupDefinition?.inputs,
|
||||
'groupId': {
|
||||
type: 'select',
|
||||
label: '',
|
||||
value: node.props?.groupId,
|
||||
internal: true,
|
||||
options: this.graph.groups.map(g => g.id)
|
||||
}
|
||||
},
|
||||
...(node.state.type?.inputs || {}),
|
||||
...groupDefinition?.inputs
|
||||
};
|
||||
|
||||
const groupType = {
|
||||
@@ -658,7 +622,9 @@ export class GraphManager extends EventEmitter<{
|
||||
return this.graph.groups.find(g => g.id === id);
|
||||
}
|
||||
|
||||
isInsideGroup = $state(false);
|
||||
get isInsideGroup() {
|
||||
return this.graphStack.length > 0;
|
||||
}
|
||||
|
||||
enterGroup(nodeId: number, cameraPosition: [number, number, number]): boolean {
|
||||
const groupNode = this.getNode(nodeId);
|
||||
@@ -667,45 +633,26 @@ export class GraphManager extends EventEmitter<{
|
||||
const group = this.getGroup(groupId);
|
||||
if (!group) return false;
|
||||
|
||||
this.graphStack.push({
|
||||
rootGraph: this.serialize(),
|
||||
savedNodes: new Map(this.nodes),
|
||||
savedEdges: [...this.edges],
|
||||
outerGraph: this.graph,
|
||||
groupId,
|
||||
nodeId,
|
||||
cameraPosition
|
||||
});
|
||||
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();
|
||||
this.isInsideGroup = true;
|
||||
return true;
|
||||
}
|
||||
|
||||
exitGroup(): { camera: [number, number, number]; nodeId: number } | false {
|
||||
if (!this.graphStack.length) return false;
|
||||
const { savedNodes, savedEdges, outerGraph, groupId, nodeId, cameraPosition } = this.graphStack.pop()!;
|
||||
const { rootGraph, groupId, nodeId, cameraPosition } = this.graphStack.pop()!;
|
||||
const internalState = this.serialize();
|
||||
|
||||
// Restore live reactive nodes and edges so drag-reactivity is preserved
|
||||
this.nodes.clear();
|
||||
for (const [id, node] of savedNodes) {
|
||||
this.nodes.set(id, node);
|
||||
}
|
||||
this.edges = savedEdges;
|
||||
|
||||
// Patch the group definition with the edited internal graph
|
||||
this.graph = {
|
||||
...outerGraph,
|
||||
groups: (outerGraph.groups ?? []).map(g =>
|
||||
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.isInsideGroup = this.graphStack.length > 0;
|
||||
this.execute();
|
||||
this.save();
|
||||
return { camera: cameraPosition, nodeId };
|
||||
}
|
||||
@@ -717,8 +664,6 @@ export class GraphManager extends EventEmitter<{
|
||||
...this.graph.groups.flatMap(g => g.nodes.map(n => n.id))
|
||||
];
|
||||
|
||||
console.log('CREATE NODE ID', ids);
|
||||
|
||||
let id = 0;
|
||||
while (ids.includes(id)) {
|
||||
id++;
|
||||
@@ -859,7 +804,7 @@ export class GraphManager extends EventEmitter<{
|
||||
return [groupInputNode.id, 0, edge[2].id, edge[3]];
|
||||
// Going out to the group
|
||||
} else if (!ids.has(edge[2].id)) {
|
||||
return [edge[0].id, edge[1], groupOutputNode.id, 'out_0'];
|
||||
return [edge[0].id, edge[1], groupOutputNode.id, 'Out'];
|
||||
}
|
||||
return [edge[0].id, edge[1], edge[2].id, edge[3]];
|
||||
}) as [number, number, number, string][];
|
||||
|
||||
@@ -152,6 +152,10 @@ export class GraphState {
|
||||
this.edges.delete(edgeId);
|
||||
}
|
||||
|
||||
getEdgeData() {
|
||||
return this.edges;
|
||||
}
|
||||
|
||||
updateNodePosition(node: NodeInstance) {
|
||||
if (
|
||||
node.state.x === node.position[0]
|
||||
@@ -186,6 +190,29 @@ export class GraphState {
|
||||
return 1;
|
||||
}
|
||||
|
||||
tryConnectToDebugNode(nodeId: number) {
|
||||
const node = this.graph.nodes.get(nodeId);
|
||||
if (!node) return;
|
||||
if (node.type.endsWith('/debug')) return;
|
||||
if (!node.state.type?.outputs?.length) return;
|
||||
for (const _node of this.graph.nodes.values()) {
|
||||
if (_node.type.endsWith('/debug')) {
|
||||
this.graph.createEdge(node, 0, _node, 'input');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const debugNode = this.graph.createNode({
|
||||
type: '__internal/node/debug',
|
||||
position: [node.position[0] + 30, node.position[1]],
|
||||
props: {}
|
||||
});
|
||||
|
||||
if (debugNode) {
|
||||
this.graph.createEdge(node, 0, debugNode, 'input');
|
||||
}
|
||||
}
|
||||
|
||||
copyNodes() {
|
||||
if (this.activeNodeId === -1 && !this.selectedNodes?.size) {
|
||||
return;
|
||||
@@ -335,8 +362,7 @@ export class GraphState {
|
||||
for (const node of this.graph.nodes.values()) {
|
||||
const x = node.position[0];
|
||||
const y = node.position[1];
|
||||
const nodeType = this.graph.getNodeType(node);
|
||||
const height = nodeType ? getNodeHeight(nodeType) : 20;
|
||||
const height = getNodeHeight(this.graph.getNodeType(node)!);
|
||||
if (downX > x && downX < x + 20 && downY > y && downY < y + height) {
|
||||
clickedNodeId = node.id;
|
||||
break;
|
||||
@@ -348,7 +374,6 @@ export class GraphState {
|
||||
}
|
||||
|
||||
isNodeInView(node: NodeInstance) {
|
||||
if (!node) return false;
|
||||
const height = getNodeHeight(this.graph.getNodeType(node)!);
|
||||
const width = 20;
|
||||
return node.position[0] > this.cameraBounds[0] - width
|
||||
|
||||
@@ -95,9 +95,8 @@
|
||||
graphState.addMenuPosition = null;
|
||||
}
|
||||
|
||||
function getSocketType(node: NodeInstance, index: number | string): string {
|
||||
function getSocketType(node: NodeInstance, index: number | string, e: unknown): string {
|
||||
const nodeType = graph.getNodeType(node);
|
||||
console.log({ nodeType, index });
|
||||
if (typeof index === 'string') {
|
||||
return nodeType?.inputs?.[index].type || 'unknown';
|
||||
}
|
||||
@@ -172,9 +171,7 @@
|
||||
{#if graph.status === 'idle'}
|
||||
{#if graph.isInsideGroup}
|
||||
<HTML transform={false}>
|
||||
<button class="exit-group" onclick={() => graphState.exitGroupNode()}>
|
||||
↑ Exit Group
|
||||
</button>
|
||||
<button class="exit-group" onclick={() => graphState.exitGroupNode()}>↑ Exit Group</button>
|
||||
</HTML>
|
||||
{/if}
|
||||
|
||||
@@ -191,8 +188,8 @@
|
||||
{#if graphState.activeSocket}
|
||||
<EdgeEl
|
||||
z={graphState.cameraPosition[2]}
|
||||
inputType={getSocketType(graphState.activeSocket.node, graphState.activeSocket.index)}
|
||||
outputType={getSocketType(graphState.activeSocket.node, graphState.activeSocket.index)}
|
||||
inputType={getSocketType(graphState.activeSocket.node, graphState.activeSocket.index, 'c')}
|
||||
outputType={getSocketType(graphState.activeSocket.node, graphState.activeSocket.index, 'd')}
|
||||
x1={graphState.activeSocket.position[0]}
|
||||
y1={graphState.activeSocket.position[1]}
|
||||
x2={graphState.edgeEndPosition?.[0] ?? graphState.mousePosition[0]}
|
||||
@@ -205,8 +202,8 @@
|
||||
<EdgeEl
|
||||
id={graph.getEdgeId(edge)}
|
||||
z={graphState.cameraPosition[2]}
|
||||
inputType={getSocketType(edge[0], edge[1])}
|
||||
outputType={getSocketType(edge[2], edge[3])}
|
||||
inputType={getSocketType(edge[0], edge[1], 'a')}
|
||||
outputType={getSocketType(edge[2], edge[3], 'b')}
|
||||
{x1}
|
||||
{y1}
|
||||
{x2}
|
||||
@@ -225,10 +222,10 @@
|
||||
style:transform={`scale(${graphState.cameraPosition[2] * 0.1})`}
|
||||
class:hovering-sockets={graphState.activeSocket}
|
||||
>
|
||||
{#each graph.nodeArray as node, index (node.id)}
|
||||
{#each graph.getAllNodes() as node (node.id)}
|
||||
<NodeEl
|
||||
bind:node={graph.nodeArray[index]}
|
||||
inView={node ? graphState.isNodeInView(node) : false}
|
||||
{node}
|
||||
inView={graphState.isNodeInView(node)}
|
||||
/>
|
||||
{/each}
|
||||
</div>
|
||||
|
||||
@@ -190,7 +190,7 @@ export class MouseEventManager {
|
||||
// if we clicked on a node
|
||||
if (clickedNodeId !== -1) {
|
||||
if (event.ctrlKey && event.shiftKey) {
|
||||
this.graph.tryConnectToDebugNode(clickedNodeId);
|
||||
this.state.tryConnectToDebugNode(clickedNodeId);
|
||||
return;
|
||||
}
|
||||
if (this.state.activeNodeId === -1) {
|
||||
|
||||
@@ -25,12 +25,15 @@ export function getParameterHeight(node: NodeDefinition, inputKey: string) {
|
||||
|
||||
const nodeHeightCache: Record<string, number> = {};
|
||||
export function getNodeHeight(node: NodeDefinition) {
|
||||
if (!node || !('inputs' in node)) {
|
||||
return 5;
|
||||
if (!node) {
|
||||
console.trace('Node is undefined', node);
|
||||
}
|
||||
if (node.id in nodeHeightCache) {
|
||||
return nodeHeightCache[node.id];
|
||||
}
|
||||
if (!node?.inputs) {
|
||||
return 5;
|
||||
}
|
||||
let height = 5;
|
||||
|
||||
for (const key in node.inputs) {
|
||||
|
||||
@@ -68,7 +68,6 @@ export function setupKeymaps(keymap: Keymap, graph: GraphManager, graphState: Gr
|
||||
|
||||
keymap.addShortcut({
|
||||
key: 'Tab',
|
||||
preventDefault: true,
|
||||
description: 'Enter selected node group',
|
||||
callback: () => graphState.enterGroupNode()
|
||||
});
|
||||
|
||||
@@ -34,14 +34,14 @@
|
||||
|
||||
const sectionHeights = $derived(
|
||||
Object
|
||||
.keys(nodeType?.inputs || {})
|
||||
.keys(nodeType.inputs || {})
|
||||
.map(key => getParameterHeight(nodeType, key) / 10)
|
||||
.filter(b => !!b)
|
||||
);
|
||||
|
||||
let meshRef: Mesh | undefined = $state();
|
||||
|
||||
const height = $derived(nodeType ? getNodeHeight(nodeType) : 20);
|
||||
const height = $derived(getNodeHeight(nodeType));
|
||||
|
||||
const zoom = $derived(graphState.cameraPosition[2]);
|
||||
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
|
||||
let { node = $bindable(), input, id, isLast }: Props = $props();
|
||||
|
||||
let nodeType = $derived(graph.getNodeType(node)!);
|
||||
const nodeType = $derived(graph.getNodeType(node)!);
|
||||
|
||||
const inputType = $derived(nodeType.inputs?.[id]);
|
||||
|
||||
|
||||
@@ -159,7 +159,7 @@ export class MemoryRuntimeExecutor implements RuntimeExecutor {
|
||||
// Only load non-virtual types (virtual nodes are resolved locally)
|
||||
const nonVirtualTypes = graph.nodes
|
||||
.map(node => node.type)
|
||||
.filter(t => !t.startsWith('__internal/'));
|
||||
.filter(t => !t.startsWith('__virtual/'));
|
||||
await this.registry.load(nonVirtualTypes as any);
|
||||
|
||||
const typeMap = new Map<string, NodeDefinition>();
|
||||
|
||||
Reference in New Issue
Block a user