feat: initial node groups #44
@@ -7,11 +7,16 @@
|
|||||||
return group?.name || `Group#${groupId}`;
|
return group?.name || `Group#${groupId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
function exitToGroup(groupId?: number) {
|
function exitToGroup(targetId?: number) {
|
||||||
while (graph.graphStack.length > 0 && graph.graphStack.at(-1)?.groupId !== groupId) {
|
while (graph.currentGroupId !== (targetId ?? null)) {
|
||||||
graph.exitGroup();
|
graph.exitGroup();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Intermediate groups: parent stack entries that are groups (not the root graph).
|
||||||
|
const intermediateGroups = $derived(
|
||||||
|
graph.parentStack.filter(e => e.id !== graph.id)
|
||||||
|
);
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<div class="shadow" class:is-inside-group={graph.isInsideGroup}></div>
|
<div class="shadow" class:is-inside-group={graph.isInsideGroup}></div>
|
||||||
@@ -24,15 +29,23 @@
|
|||||||
>
|
>
|
||||||
Root
|
Root
|
||||||
</button>
|
</button>
|
||||||
{#each graph.graphStack as group (group.groupId)}
|
|
||||||
|
{#each intermediateGroups as entry (entry.id)}
|
||||||
<span class="i-[tabler--arrow-right]"></span>
|
<span class="i-[tabler--arrow-right]"></span>
|
||||||
<button
|
<button
|
||||||
class="bg-layer-2 opacity-75 hover:opacity-100 cursor-pointer rounded-sm p-1 px-2"
|
class="bg-layer-2 opacity-75 hover:opacity-100 cursor-pointer rounded-sm p-1 px-2"
|
||||||
onclick={() => exitToGroup(group.groupId)}
|
onclick={() => exitToGroup(entry.id)}
|
||||||
>
|
>
|
||||||
{getGroupName(group.groupId)}
|
{getGroupName(entry.id)}
|
||||||
</button>
|
</button>
|
||||||
{/each}
|
{/each}
|
||||||
|
|
||||||
|
<span class="i-[tabler--arrow-right]"></span>
|
||||||
|
<button
|
||||||
|
class="bg-layer-2 opacity-100 cursor-pointer rounded-sm p-1 px-2"
|
||||||
|
>
|
||||||
|
{getGroupName(graph.currentGroupId!)}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
|
|||||||
@@ -11,6 +11,8 @@ import type {
|
|||||||
NodeInput,
|
NodeInput,
|
||||||
NodeInstance,
|
NodeInstance,
|
||||||
NodeRegistry,
|
NodeRegistry,
|
||||||
|
SerializedEdge,
|
||||||
|
SerializedNode,
|
||||||
Socket
|
Socket
|
||||||
} from '@nodarium/types';
|
} from '@nodarium/types';
|
||||||
import { fastHashString } from '@nodarium/utils';
|
import { fastHashString } from '@nodarium/utils';
|
||||||
@@ -25,8 +27,8 @@ import {
|
|||||||
} from './helpers/nodeHelpers';
|
} from './helpers/nodeHelpers';
|
||||||
import { HistoryManager } from './history-manager';
|
import { HistoryManager } from './history-manager';
|
||||||
|
|
||||||
const logger = createLogger('graph-manager');
|
const log = createLogger('graph-manager');
|
||||||
logger.mute();
|
log.mute();
|
||||||
|
|
||||||
const remoteRegistry = new RemoteNodeRegistry('');
|
const remoteRegistry = new RemoteNodeRegistry('');
|
||||||
|
|
||||||
@@ -42,24 +44,26 @@ export class GraphManager extends EventEmitter<{
|
|||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
graph: Graph = $state({ id: 0, nodes: [], edges: [], groups: [] });
|
graph: Graph = $state({ id: 0, nodes: [], edges: [], groups: [] });
|
||||||
id = $state(0);
|
|
||||||
|
|
||||||
nodes = new SvelteMap<number, NodeInstance>();
|
// Snapshots of parent levels we navigated away from. Empty at root.
|
||||||
nodeArray = $derived(Array.from(this.nodes.values()));
|
// Entry i has the saved state of depth i (0 = root graph, 1 = first group, …).
|
||||||
|
parentStack: {
|
||||||
edges = $state<Edge[]>([]);
|
id: number;
|
||||||
|
nodes: SerializedNode[];
|
||||||
// Plain array — NOT $state. rootGraph items are plain-serialized (safe for structuredClone).
|
edges: SerializedEdge[];
|
||||||
// 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];
|
cameraPosition: [number, number, number];
|
||||||
}[] = [];
|
}[] = $state([]);
|
||||||
|
|
||||||
|
// ID of the currently active group, or null when at the root graph.
|
||||||
|
currentGroupId = $state<number | null>(null);
|
||||||
|
|
||||||
|
// Graph Data
|
||||||
|
id = $state(0);
|
||||||
|
nodes = new SvelteMap<number, NodeInstance>();
|
||||||
|
edges = $state<Edge[]>([]);
|
||||||
|
groups: GroupDefinition[] = $state([]);
|
||||||
|
|
||||||
|
nodeArray = $derived(Array.from(this.nodes.values()));
|
||||||
|
|
||||||
settingTypes: Record<string, NodeInput> = {};
|
settingTypes: Record<string, NodeInput> = {};
|
||||||
settings = $state<Record<string, unknown>>();
|
settings = $state<Record<string, unknown>>();
|
||||||
@@ -76,24 +80,9 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
history: HistoryManager = new HistoryManager();
|
history: HistoryManager = new HistoryManager();
|
||||||
|
|
||||||
public 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.serializeFullGraph());
|
this.emit('result', this.serialize());
|
||||||
}, 10);
|
}, 10);
|
||||||
|
|
||||||
constructor(public registry: NodeRegistry) {
|
constructor(public registry: NodeRegistry) {
|
||||||
@@ -101,32 +90,44 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
serialize(): Graph {
|
serialize(): Graph {
|
||||||
const nodes = Array.from(this.nodes.values()).map((node) => serializeNode(node));
|
const nodes =
|
||||||
const edges = this.edges.map((edge) => serializeEdge(edge));
|
(this.parentStack.length === 0 ? Array.from(this.nodes.values()) : this.parentStack[0].nodes)
|
||||||
|
.map(n => serializeNode(n));
|
||||||
const groups = this.graph.groups?.map((group) => {
|
const edges =
|
||||||
const groupNodes = group.nodes.map((node) => serializeNode(node));
|
(this.parentStack.length === 0 ? Array.from(this.edges.values()) : this.parentStack[0].edges)
|
||||||
|
.map(e => serializeEdge(e));
|
||||||
|
|
||||||
|
const groups = this.groups?.map((group) => {
|
||||||
|
const isCurrentActive = this.currentGroupId === group.id;
|
||||||
|
const stackState = this.parentStack.find((s) => s.id === group.id);
|
||||||
|
const groupNodes =
|
||||||
|
(isCurrentActive ? [...this.nodes.values()] : stackState?.nodes ?? group.nodes).map(
|
||||||
|
n => serializeNode(n)
|
||||||
|
);
|
||||||
|
const groupEdges =
|
||||||
|
(isCurrentActive ? [...this.edges.values()] : stackState?.edges ?? group.edges).map(
|
||||||
|
e => serializeEdge(e)
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
id: group.id,
|
id: group.id,
|
||||||
name: group.name,
|
name: group.name,
|
||||||
inputs: group.inputs,
|
inputs: group.inputs,
|
||||||
outputs: group.outputs,
|
outputs: group.outputs,
|
||||||
nodes: groupNodes,
|
nodes: groupNodes,
|
||||||
edges: group.edges
|
edges: groupEdges
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
const serialized = {
|
const serialized = $state.snapshot({
|
||||||
id: this.graph.id,
|
id: this.id,
|
||||||
settings: $state.snapshot(this.settings),
|
settings: this.settings,
|
||||||
meta: $state.snapshot(this.graph.meta),
|
meta: this.graph.meta,
|
||||||
groups,
|
groups,
|
||||||
nodes,
|
nodes,
|
||||||
edges
|
edges
|
||||||
};
|
});
|
||||||
logger.log('serializing graph', serialized);
|
log.log('serializing graph', serialized);
|
||||||
return clone($state.snapshot(serialized));
|
return clone(serialized);
|
||||||
}
|
}
|
||||||
|
|
||||||
private lastSettingsHash = 0;
|
private lastSettingsHash = 0;
|
||||||
@@ -190,7 +191,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
);
|
);
|
||||||
|
|
||||||
if (!bestInputEntry || bestOutputIdx === -1) {
|
if (!bestInputEntry || bestOutputIdx === -1) {
|
||||||
logger.error('Could not find compatible sockets for drop');
|
log.error('Could not find compatible sockets for drop');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,21 +297,22 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return edges;
|
return edges;
|
||||||
}
|
}
|
||||||
|
|
||||||
private _init(graph: Graph) {
|
private _init(
|
||||||
const nodes = new SvelteMap(
|
graph: { nodes: SerializedNode[]; edges: SerializedEdge[] }
|
||||||
graph.nodes.map((node) => {
|
) {
|
||||||
const n = node as NodeInstance;
|
this.nodes.clear();
|
||||||
const registryType = this.registry.getNode(node.type);
|
for (const node of graph.nodes) {
|
||||||
n.state = registryType ? { type: registryType } : {};
|
const n = $state(node) as NodeInstance;
|
||||||
const resolvedType = this.getNodeType(n);
|
const registryType = this.registry.getNode(node.type);
|
||||||
if (resolvedType) n.state = { type: resolvedType };
|
n.state = registryType ? { type: registryType } : {};
|
||||||
return [node.id, n];
|
const resolvedType = this.getNodeType(n);
|
||||||
})
|
if (resolvedType) n.state = { type: resolvedType };
|
||||||
);
|
this.nodes.set(n.id, n);
|
||||||
|
}
|
||||||
|
|
||||||
this.edges = graph.edges.map((edge) => {
|
this.edges = graph.edges.map((edge) => {
|
||||||
const from = nodes.get(edge[0]);
|
const from = this.nodes.get(edge[0]);
|
||||||
const to = nodes.get(edge[2]);
|
const to = this.nodes.get(edge[2]);
|
||||||
if (!from || !to) {
|
if (!from || !to) {
|
||||||
throw new Error('Edge references non-existing node');
|
throw new Error('Edge references non-existing node');
|
||||||
}
|
}
|
||||||
@@ -321,11 +323,6 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return [from, edge[1], to, edge[3]] as Edge;
|
return [from, edge[1], to, edge[3]] as Edge;
|
||||||
});
|
});
|
||||||
|
|
||||||
this.nodes.clear();
|
|
||||||
for (const [id, node] of nodes) {
|
|
||||||
this.nodes.set(id, node);
|
|
||||||
}
|
|
||||||
|
|
||||||
this.execute();
|
this.execute();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -334,11 +331,12 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
this.loaded = false;
|
this.loaded = false;
|
||||||
graph.groups ??= [];
|
graph.groups ??= [];
|
||||||
|
this.groups = graph.groups;
|
||||||
this.graph = graph;
|
this.graph = graph;
|
||||||
this.status = 'loading';
|
this.status = 'loading';
|
||||||
this.id = graph.id;
|
this.id = graph.id;
|
||||||
|
|
||||||
logger.info(
|
log.info(
|
||||||
'loading graph',
|
'loading graph',
|
||||||
{ nodes: graph.nodes, edges: graph.edges, id: graph.id }
|
{ nodes: graph.nodes, edges: graph.edges, id: graph.id }
|
||||||
);
|
);
|
||||||
@@ -353,8 +351,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
.filter(n => n && 'type' in n)
|
.filter(n => n && 'type' in n)
|
||||||
.map((n) => n.type)
|
.map((n) => n.type)
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
.filter(n => !n.startsWith('__internal/'));
|
|
||||||
|
|
||||||
await this.registry.load(nodeIds);
|
await this.registry.load(nodeIds);
|
||||||
|
|
||||||
@@ -374,20 +371,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info('loaded node types', this.registry.getAllNodes());
|
log.info('loaded node types', this.registry.getAllNodes());
|
||||||
|
|
||||||
for (const node of this.graph.nodes) {
|
|
||||||
const nodeType = this.registry.getNode(node.type);
|
|
||||||
if (!nodeType && !node.type.startsWith('__internal/')) {
|
|
||||||
logger.error(`Node type not found: ${node.type}`);
|
|
||||||
this.status = 'error';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// Turn into runtime node
|
|
||||||
const n = node as NodeInstance;
|
|
||||||
n.state = {};
|
|
||||||
n.state.type = nodeType;
|
|
||||||
}
|
|
||||||
|
|
||||||
// load settings
|
// load settings
|
||||||
const settingTypes: Record<
|
const settingTypes: Record<
|
||||||
@@ -418,18 +402,18 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.parentStack = [];
|
||||||
|
this.currentGroupId = null;
|
||||||
|
|
||||||
this.settings = settingValues;
|
this.settings = settingValues;
|
||||||
this.emit('settings', { types: settingTypes, values: settingValues });
|
this.emit('settings', { types: settingTypes, values: settingValues });
|
||||||
|
|
||||||
this.history.reset();
|
this.history.reset();
|
||||||
this._init(this.graph);
|
this._init(graph);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
|
|
||||||
this.status = 'idle';
|
this.status = 'idle';
|
||||||
|
|
||||||
this.loaded = true;
|
this.loaded = true;
|
||||||
logger.log(`Graph loaded in ${performance.now() - a}ms`);
|
log.log(`Graph loaded in ${performance.now() - a}ms`);
|
||||||
setTimeout(() => this.execute(), 100);
|
setTimeout(() => this.execute(), 100);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -449,19 +433,15 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === '__internal/group/input') {
|
if (node.type === '__internal/group/input') {
|
||||||
const groupId = this.graphStack.at(-1)?.groupId;
|
const group = this.currentGroupId !== null ? this.getGroup(this.currentGroupId) : undefined;
|
||||||
const group = groupId !== undefined ? this.getGroup(groupId) : undefined;
|
|
||||||
if (!group) return node.state.type;
|
if (!group) return node.state.type;
|
||||||
|
|
||||||
const groupInputs: NodeDefinition['inputs'] = Object.fromEntries(
|
const groupInputs: NodeDefinition['inputs'] = Object.fromEntries(
|
||||||
Object.values(group?.inputs || {}).map((o, i) => {
|
Object.values(group?.inputs || {}).map((o, i) => {
|
||||||
return [
|
return [`in_${i}`, {
|
||||||
`in_${i}`,
|
...o,
|
||||||
{
|
external: true
|
||||||
...o,
|
}];
|
||||||
external: true
|
|
||||||
}
|
|
||||||
];
|
|
||||||
}) || []
|
}) || []
|
||||||
);
|
);
|
||||||
return {
|
return {
|
||||||
@@ -475,8 +455,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (node.type === '__internal/group/output') {
|
if (node.type === '__internal/group/output') {
|
||||||
const groupId = this.graphStack.at(-1)?.groupId;
|
const group = this.currentGroupId !== null ? this.getGroup(this.currentGroupId) : undefined;
|
||||||
const group = groupId !== undefined ? this.getGroup(groupId) : undefined;
|
|
||||||
if (!group) return node.state.type;
|
if (!group) return node.state.type;
|
||||||
return {
|
return {
|
||||||
id: '__internal/group/output' as NodeId,
|
id: '__internal/group/output' as NodeId,
|
||||||
@@ -499,7 +478,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const groupDefinition = this.getGroup(node.props?.groupId as number);
|
const groupDefinition = this.getGroup(node.props?.groupId as number);
|
||||||
|
|
||||||
if (!groupDefinition) {
|
if (!groupDefinition) {
|
||||||
logger.error(`Group not found: ${node.props?.groupId}`);
|
log.error(`Group not found: ${node.props?.groupId}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -508,18 +487,24 @@ export class GraphManager extends EventEmitter<{
|
|||||||
...groupDefinition?.inputs
|
...groupDefinition?.inputs
|
||||||
};
|
};
|
||||||
|
|
||||||
// This is to make sure the the groupId is always first
|
|
||||||
delete defaultInputs['groupId'];
|
delete defaultInputs['groupId'];
|
||||||
|
|
||||||
const inputs = {
|
const inputs = {
|
||||||
'groupId': {
|
'groupId': {
|
||||||
type: 'select',
|
type: 'select',
|
||||||
label: '',
|
label: '',
|
||||||
value: node.props?.groupId,
|
value: node.props?.groupId,
|
||||||
internal: true,
|
internal: true,
|
||||||
options: this.graph.groups.map((g) => ({
|
options: this.groups.map((g) => ({
|
||||||
value: g.id,
|
value: g.id,
|
||||||
label: g.name || `Group#${g.id}`
|
label: g.name || `Group#${g.id}`
|
||||||
}))
|
})).filter((g) => {
|
||||||
|
const activeIds = new SvelteSet([
|
||||||
|
...this.parentStack.filter(e => e.id !== this.id).map(e => e.id),
|
||||||
|
...(this.currentGroupId !== null ? [this.currentGroupId] : [])
|
||||||
|
]);
|
||||||
|
return !activeIds.has(g.value);
|
||||||
|
})
|
||||||
},
|
},
|
||||||
...defaultInputs
|
...defaultInputs
|
||||||
};
|
};
|
||||||
@@ -599,6 +584,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
|
removeNode(node: NodeInstance, { restoreEdges = false } = {}) {
|
||||||
|
log.log('removing node', { id: node.id, type: node.type, restoreEdges });
|
||||||
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
|
const edgesToNode = this.edges.filter((edge) => edge[2].id === node.id);
|
||||||
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
|
const edgesFromNode = this.edges.filter((edge) => edge[0].id === node.id);
|
||||||
for (const edge of [...edgesToNode, ...edgesFromNode]) {
|
for (const edge of [...edgesToNode, ...edgesFromNode]) {
|
||||||
@@ -643,82 +629,69 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
getGroup(id: number) {
|
getGroup(id: number) {
|
||||||
return this.graph.groups.find(g => g.id === id);
|
return this.groups.find(g => g.id === id);
|
||||||
}
|
}
|
||||||
|
|
||||||
renameGroup(groupId: number, name: string) {
|
renameGroup(groupId: number, name: string) {
|
||||||
|
log.log('renaming group', { groupId, name });
|
||||||
const group = this.getGroup(groupId);
|
const group = this.getGroup(groupId);
|
||||||
if (!group) return;
|
if (!group) return;
|
||||||
group.name = name;
|
group.name = name;
|
||||||
this.save();
|
this.save();
|
||||||
}
|
}
|
||||||
|
|
||||||
isInsideGroup = $state(false);
|
isInsideGroup = $derived(this.currentGroupId !== null);
|
||||||
|
|
||||||
enterGroup(nodeId: number, cameraPosition: [number, number, number]): boolean {
|
enterGroup(nodeId: number, cameraPosition: [number, number, number]): boolean {
|
||||||
const groupNode = this.getNode(nodeId);
|
const groupNode = this.getNode(nodeId);
|
||||||
if (!groupNode || groupNode.type !== '__internal/group/instance') return false;
|
if (!groupNode || groupNode.type !== '__internal/group/instance') return false;
|
||||||
|
|
||||||
|
log.log('entering group', { nodeId, cameraPosition });
|
||||||
|
|
||||||
const groupId = groupNode.props?.groupId as number;
|
const groupId = groupNode.props?.groupId as number;
|
||||||
const group = this.getGroup(groupId);
|
const group = this.getGroup(groupId);
|
||||||
if (!group) return false;
|
if (!group) return false;
|
||||||
|
|
||||||
this.graphStack.push({
|
// Snapshot current level and push it onto the parent stack.
|
||||||
rootGraph: this.serialize(),
|
this.parentStack.push({
|
||||||
savedNodes: new SvelteMap(this.nodes),
|
id: this.currentGroupId ?? this.id,
|
||||||
savedEdges: [...this.edges],
|
nodes: [...this.nodes.values()].map(n => serializeNode(n)),
|
||||||
outerGraph: this.graph,
|
edges: [...this.edges.values()].map(e => serializeEdge(e)),
|
||||||
groupId,
|
|
||||||
nodeId,
|
|
||||||
cameraPosition
|
cameraPosition
|
||||||
});
|
});
|
||||||
this.graph = { ...this.graph, nodes: group.nodes, edges: group.edges };
|
this.currentGroupId = groupId;
|
||||||
this._init(this.graph);
|
|
||||||
|
log.log('entered group', { groupId, depth: this.parentStack.length });
|
||||||
this.history.reset();
|
this.history.reset();
|
||||||
this.isInsideGroup = true;
|
this._init(group);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
exitGroup(): { camera: [number, number, number]; nodeId: number } | false {
|
exitGroup() {
|
||||||
if (!this.graphStack.length) return false;
|
log.log('exiting group', { depth: this.parentStack.length });
|
||||||
const { savedNodes, savedEdges, outerGraph, groupId, nodeId, cameraPosition } = this.graphStack
|
if (this.parentStack.length === 0) return;
|
||||||
.pop()!;
|
|
||||||
const internalState = this.serialize();
|
|
||||||
|
|
||||||
// Clear stale DOM/mesh refs so the remounting components register fresh ones.
|
// Persist live edits back to the GroupDefinition.
|
||||||
// The $effect guards in NodeHTML/Node only set these when undefined, so without
|
const group = this.getGroup(this.currentGroupId!);
|
||||||
// this clear they'd keep pointing to the detached elements from before group entry.
|
if (group) {
|
||||||
for (const node of savedNodes.values()) {
|
group.nodes = [...this.nodes.values()].map(n => serializeNode(n));
|
||||||
node.state.ref = undefined;
|
group.edges = [...this.edges.values()].map(e => serializeEdge(e));
|
||||||
node.state.mesh = undefined;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Restore live reactive nodes and edges so drag-reactivity is preserved
|
const parent = this.parentStack.pop()!;
|
||||||
this.nodes.clear();
|
this.currentGroupId = this.parentStack.length === 0 ? null : parent.id;
|
||||||
for (const [id, node] of savedNodes) {
|
this._init(parent);
|
||||||
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 =>
|
|
||||||
g.id === groupId ? { ...g, nodes: internalState.nodes, edges: internalState.edges } : g
|
|
||||||
)
|
|
||||||
};
|
|
||||||
|
|
||||||
this.history.reset();
|
this.history.reset();
|
||||||
this.isInsideGroup = this.graphStack.length > 0;
|
|
||||||
this.execute();
|
this.execute();
|
||||||
this.save();
|
this.save();
|
||||||
return { camera: cameraPosition, nodeId };
|
|
||||||
}
|
}
|
||||||
|
|
||||||
createNodeId() {
|
createNodeId() {
|
||||||
const ids = [
|
const ids = [
|
||||||
...this.nodes.keys(),
|
...this.nodes.keys(),
|
||||||
...this.graph.groups.map(g => g.id),
|
...this.groups.map(g => g.id),
|
||||||
...this.graph.groups.flatMap(g => g.nodes.map(n => n.id))
|
...this.groups.flatMap(g => g.nodes.map(n => n.id))
|
||||||
];
|
];
|
||||||
|
|
||||||
let id = 0;
|
let id = 0;
|
||||||
@@ -775,12 +748,32 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const usedGroupIds = new SvelteSet<number>();
|
const usedGroupIds = new SvelteSet<number>();
|
||||||
const queue: number[] = [];
|
const queue: number[] = [];
|
||||||
|
|
||||||
for (const node of this.nodes.values()) {
|
// Seed from root-level nodes so outer groups aren't treated as unused when inside a group.
|
||||||
|
const rootNodes = this.parentStack.length > 0
|
||||||
|
? this.parentStack[0].nodes
|
||||||
|
: [...this.nodes.values()];
|
||||||
|
|
||||||
|
for (const node of rootNodes) {
|
||||||
if (node.type === '__internal/group/instance' && node.props?.groupId !== undefined) {
|
if (node.type === '__internal/group/instance' && node.props?.groupId !== undefined) {
|
||||||
queue.push(node.props.groupId as number);
|
queue.push(node.props.groupId as number);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Also seed from live nodes (may contain new group instances created inside a group).
|
||||||
|
if (this.currentGroupId !== null) {
|
||||||
|
for (const node of this.nodes.values()) {
|
||||||
|
if (node.type === '__internal/group/instance' && node.props?.groupId !== undefined) {
|
||||||
|
queue.push(node.props.groupId as number);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Every group on the navigation path is used by definition.
|
||||||
|
for (const entry of this.parentStack) {
|
||||||
|
if (entry.id !== this.id) usedGroupIds.add(entry.id);
|
||||||
|
}
|
||||||
|
if (this.currentGroupId !== null) usedGroupIds.add(this.currentGroupId);
|
||||||
|
|
||||||
while (queue.length) {
|
while (queue.length) {
|
||||||
const groupId = queue.pop()!;
|
const groupId = queue.pop()!;
|
||||||
if (usedGroupIds.has(groupId)) continue;
|
if (usedGroupIds.has(groupId)) continue;
|
||||||
@@ -795,13 +788,13 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return this.graph.groups.filter(g => !usedGroupIds.has(g.id));
|
return this.groups.filter(g => !usedGroupIds.has(g.id));
|
||||||
}
|
}
|
||||||
|
|
||||||
removeUnusedGroups() {
|
removeUnusedGroups() {
|
||||||
const unused = this.getUnusedGroups();
|
const unused = this.getUnusedGroups();
|
||||||
const unusedIds = new SvelteSet(unused.map(g => g.id));
|
const unusedIds = new SvelteSet(unused.map(g => g.id));
|
||||||
this.graph.groups = this.graph.groups.filter(g => !unusedIds.has(g.id));
|
this.groups = this.groups.filter(g => !unusedIds.has(g.id));
|
||||||
this.save();
|
this.save();
|
||||||
return unused.length;
|
return unused.length;
|
||||||
}
|
}
|
||||||
@@ -816,7 +809,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
if (!nodes.length) return;
|
if (!nodes.length) return;
|
||||||
|
|
||||||
logger.log(`Grouping ${nodes.length} nodes`, { nodes });
|
log.log(`Grouping ${nodes.length} nodes`, { nodes });
|
||||||
|
|
||||||
const ids = new SvelteSet(nodes.map(n => n.id));
|
const ids = new SvelteSet(nodes.map(n => n.id));
|
||||||
|
|
||||||
@@ -877,8 +870,8 @@ export class GraphManager extends EventEmitter<{
|
|||||||
// Allocate all needed IDs up front so sequential calls never collide.
|
// Allocate all needed IDs up front so sequential calls never collide.
|
||||||
const usedIds = new SvelteSet<number>([
|
const usedIds = new SvelteSet<number>([
|
||||||
...this.nodes.keys(),
|
...this.nodes.keys(),
|
||||||
...this.graph.groups.map(g => g.id),
|
...this.groups.map(g => g.id),
|
||||||
...this.graph.groups.flatMap(g => g.nodes.map(n => n.id))
|
...this.groups.flatMap(g => g.nodes.map(n => n.id))
|
||||||
]);
|
]);
|
||||||
const nextId = () => {
|
const nextId = () => {
|
||||||
let id = 0;
|
let id = 0;
|
||||||
@@ -925,7 +918,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
};
|
};
|
||||||
|
|
||||||
// Push before createNode so createNodeId() inside sees the allocated IDs.
|
// Push before createNode so createNodeId() inside sees the allocated IDs.
|
||||||
this.graph.groups.push(groupDefinition);
|
this.groups.push(groupDefinition);
|
||||||
|
|
||||||
const groupNode = this.createNode({
|
const groupNode = this.createNode({
|
||||||
type: '__internal/group/instance',
|
type: '__internal/group/instance',
|
||||||
@@ -972,7 +965,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}) {
|
}) {
|
||||||
const nodeType = this.registry.getNode(type);
|
const nodeType = this.registry.getNode(type);
|
||||||
if (!nodeType && !type.startsWith('__internal/')) {
|
if (!nodeType && !type.startsWith('__internal/')) {
|
||||||
logger.error(`Node type not found: ${type}`);
|
log.error(`Node type not found: ${type}`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -984,6 +977,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
props
|
props
|
||||||
});
|
});
|
||||||
|
|
||||||
|
log.log('creating node', { id: node.id, type, position, props });
|
||||||
this.nodes.set(node.id, node);
|
this.nodes.set(node.id, node);
|
||||||
|
|
||||||
this.save();
|
this.save();
|
||||||
@@ -1005,7 +999,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket
|
(e) => e[0].id === from.id && e[1] === fromSocket && e[3] === toSocket
|
||||||
);
|
);
|
||||||
if (existingEdge) {
|
if (existingEdge) {
|
||||||
logger.error('Edge already exists', existingEdge);
|
log.error('Edge already exists', existingEdge);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1022,7 +1016,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
if (!areSocketsCompatible(fromSocketType, toSocketType)) {
|
||||||
logger.error(
|
log.error(
|
||||||
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`
|
`Socket types do not match: ${fromSocketType} !== ${toSocketType}`
|
||||||
);
|
);
|
||||||
return;
|
return;
|
||||||
@@ -1037,6 +1031,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
|
|
||||||
const edge = [from, fromSocket, to, toSocket] as Edge;
|
const edge = [from, fromSocket, to, toSocket] as Edge;
|
||||||
|
|
||||||
|
log.log('creating edge', { from: from.id, fromSocket, to: to.id, toSocket });
|
||||||
this.edges.push(edge);
|
this.edges.push(edge);
|
||||||
|
|
||||||
from.state.children = from.state.children || [];
|
from.state.children = from.state.children || [];
|
||||||
@@ -1053,6 +1048,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
undo() {
|
undo() {
|
||||||
|
log.log('undo');
|
||||||
const nextState = this.history.undo();
|
const nextState = this.history.undo();
|
||||||
if (nextState) {
|
if (nextState) {
|
||||||
this._init(nextState);
|
this._init(nextState);
|
||||||
@@ -1061,6 +1057,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
redo() {
|
redo() {
|
||||||
|
log.log('redo');
|
||||||
const nextState = this.history.redo();
|
const nextState = this.history.redo();
|
||||||
if (nextState) {
|
if (nextState) {
|
||||||
this._init(nextState);
|
this._init(nextState);
|
||||||
@@ -1088,9 +1085,9 @@ export class GraphManager extends EventEmitter<{
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fullState = this.graphStack.length > 0 ? this.serializeFullGraph() : state;
|
const fullState = this.serialize();
|
||||||
this.emit('save', fullState);
|
this.emit('save', fullState);
|
||||||
logger.log('saving graphs', fullState);
|
log.log('saving graphs', fullState);
|
||||||
}
|
}
|
||||||
|
|
||||||
getParentsOfNode(node: NodeInstance) {
|
getParentsOfNode(node: NodeInstance) {
|
||||||
@@ -1098,7 +1095,7 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const stack = node.state?.parents?.slice(0);
|
const stack = node.state?.parents?.slice(0);
|
||||||
while (stack?.length) {
|
while (stack?.length) {
|
||||||
if (parents.length > 1000000) {
|
if (parents.length > 1000000) {
|
||||||
logger.warn('Infinite loop detected');
|
log.warn('Infinite loop detected');
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
const parent = stack.pop();
|
const parent = stack.pop();
|
||||||
@@ -1215,6 +1212,12 @@ export class GraphManager extends EventEmitter<{
|
|||||||
edge: Edge,
|
edge: Edge,
|
||||||
{ applyDeletion = true }: { applyDeletion?: boolean } = {}
|
{ applyDeletion = true }: { applyDeletion?: boolean } = {}
|
||||||
) {
|
) {
|
||||||
|
log.log('removing edge', {
|
||||||
|
from: edge[0].id,
|
||||||
|
fromSocket: edge[1],
|
||||||
|
to: edge[2].id,
|
||||||
|
toSocket: edge[3]
|
||||||
|
});
|
||||||
const id0 = edge[0].id;
|
const id0 = edge[0].id;
|
||||||
const sid0 = edge[1];
|
const sid0 = edge[1];
|
||||||
const id2 = edge[2].id;
|
const id2 = edge[2].id;
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ describe('enterGroupNode', () => {
|
|||||||
const { manager, state } = createFixture();
|
const { manager, state } = createFixture();
|
||||||
state.activeNodeId = -1;
|
state.activeNodeId = -1;
|
||||||
state.enterGroupNode();
|
state.enterGroupNode();
|
||||||
expect(manager.graphStack.length).toBe(0);
|
expect(manager.parentStack.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('does nothing when the active node is not a group instance', () => {
|
it('does nothing when the active node is not a group instance', () => {
|
||||||
@@ -106,7 +106,7 @@ describe('enterGroupNode', () => {
|
|||||||
assert.isDefined(node);
|
assert.isDefined(node);
|
||||||
state.activeNodeId = node!.id;
|
state.activeNodeId = node!.id;
|
||||||
state.enterGroupNode();
|
state.enterGroupNode();
|
||||||
expect(manager.graphStack.length).toBe(0);
|
expect(manager.parentStack.length).toBe(0);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('enters the group, pushes graphStack, and clears UI state', () => {
|
it('enters the group, pushes graphStack, and clears UI state', () => {
|
||||||
@@ -123,7 +123,7 @@ describe('enterGroupNode', () => {
|
|||||||
|
|
||||||
state.enterGroupNode();
|
state.enterGroupNode();
|
||||||
|
|
||||||
expect(manager.graphStack.length).toBe(1);
|
expect(manager.parentStack.length).toBe(1);
|
||||||
expect(state.activeNodeId).toBe(-1);
|
expect(state.activeNodeId).toBe(-1);
|
||||||
expect(state.selectedNodes.size).toBe(0);
|
expect(state.selectedNodes.size).toBe(0);
|
||||||
expect(manager.isInsideGroup).toBe(true);
|
expect(manager.isInsideGroup).toBe(true);
|
||||||
@@ -135,7 +135,7 @@ describe('exitGroupNode', () => {
|
|||||||
const { manager, state } = createFixture();
|
const { manager, state } = createFixture();
|
||||||
const before = [...state.cameraPosition];
|
const before = [...state.cameraPosition];
|
||||||
state.exitGroupNode();
|
state.exitGroupNode();
|
||||||
expect(manager.graphStack.length).toBe(0);
|
expect(manager.parentStack.length).toBe(0);
|
||||||
expect(state.cameraPosition).toEqual(before);
|
expect(state.cameraPosition).toEqual(before);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
<script lang="ts">
|
<script lang="ts">
|
||||||
import { createKeyMap } from '$lib/helpers/createKeyMap';
|
import { createKeyMap } from '$lib/helpers/createKeyMap';
|
||||||
import type { Graph, NodeInstance, NodeRegistry } from '@nodarium/types';
|
import type { Graph, NodeInstance, NodeRegistry } from '@nodarium/types';
|
||||||
|
import { onMount } from 'svelte';
|
||||||
import { GraphManager } from '../graph-manager.svelte';
|
import { GraphManager } from '../graph-manager.svelte';
|
||||||
import { GraphState, setGraphManager, setGraphState } from '../graph-state.svelte';
|
import { GraphState, setGraphManager, setGraphState } from '../graph-state.svelte';
|
||||||
import { setupKeymaps } from '../keymaps';
|
import { setupKeymaps } from '../keymaps';
|
||||||
@@ -83,8 +84,8 @@
|
|||||||
|
|
||||||
manager.on('save', (save) => onsave?.(save));
|
manager.on('save', (save) => onsave?.(save));
|
||||||
|
|
||||||
$effect(() => {
|
onMount(() => {
|
||||||
if (graph && (manager.status !== 'idle' || manager.graph.id !== graph.id)) {
|
if (graph) {
|
||||||
manager.load(graph);
|
manager.load(graph);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -42,8 +42,12 @@ export function serializeNode(node: SerializedNode | NodeInstance): SerializedNo
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function serializeEdge(edge: Edge): SerializedEdge {
|
export function serializeEdge(edge: SerializedEdge | Edge): SerializedEdge {
|
||||||
return [edge[0].id, edge[1], edge[2].id, edge[3]];
|
if (typeof edge[0] === 'number' && typeof edge[2] === 'number') {
|
||||||
|
return [edge[0], edge[1], edge[2], edge[3]];
|
||||||
|
}
|
||||||
|
const e = edge as Edge;
|
||||||
|
return [e[0].id, e[1], e[2].id, e[3]];
|
||||||
}
|
}
|
||||||
|
|
||||||
const nodeHeightCache: Record<string, number> = {};
|
const nodeHeightCache: Record<string, number> = {};
|
||||||
|
|||||||
@@ -21,6 +21,26 @@ log.mute();
|
|||||||
export function expandGroups(graph: Graph): Graph {
|
export function expandGroups(graph: Graph): Graph {
|
||||||
if (!graph.groups || graph.groups.length === 0) return graph;
|
if (!graph.groups || graph.groups.length === 0) return graph;
|
||||||
|
|
||||||
|
function groupContainsSelf(groupId: number, visited = new Set<number>()): boolean {
|
||||||
|
if (visited.has(groupId)) return true;
|
||||||
|
visited.add(groupId);
|
||||||
|
const group = graph.groups!.find(g => g.id === groupId);
|
||||||
|
if (!group) return false;
|
||||||
|
for (const n of group.nodes) {
|
||||||
|
if (n.type === '__internal/group/instance') {
|
||||||
|
const nestedId = n.props?.groupId as number | undefined;
|
||||||
|
if (nestedId !== undefined && groupContainsSelf(nestedId, visited)) return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const group of graph.groups) {
|
||||||
|
if (groupContainsSelf(group.id)) {
|
||||||
|
throw new Error(`Circular group reference: group ${group.id} contains itself`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const nodes = [...graph.nodes];
|
const nodes = [...graph.nodes];
|
||||||
let edges = [...graph.edges];
|
let edges = [...graph.edges];
|
||||||
|
|
||||||
@@ -57,10 +77,16 @@ export function expandGroups(graph: Graph): Graph {
|
|||||||
const newEdges: Graph['edges'] = [];
|
const newEdges: Graph['edges'] = [];
|
||||||
|
|
||||||
// external_source → [inputBoundary →] internal_target
|
// external_source → [inputBoundary →] internal_target
|
||||||
|
//
|
||||||
|
// External socket names are "input_N" where N equals the input boundary's
|
||||||
|
// output index. Match each external edge only to the internal edges that
|
||||||
|
// originate from that specific output slot — not a cartesian product of all.
|
||||||
if (inputBoundary) {
|
if (inputBoundary) {
|
||||||
const fromInput = group.edges.filter(e => e[0] === inputBoundary.id);
|
const fromInput = group.edges.filter(e => e[0] === inputBoundary.id);
|
||||||
for (const extEdge of incomingExternal) {
|
for (const extEdge of incomingExternal) {
|
||||||
for (const intEdge of fromInput) {
|
const inputIndex = parseInt((extEdge[3] as string).replace('input_', ''), 10);
|
||||||
|
const matchingIntEdges = fromInput.filter(e => e[1] === inputIndex);
|
||||||
|
for (const intEdge of matchingIntEdges) {
|
||||||
const toId = idMap.get(intEdge[2]);
|
const toId = idMap.get(intEdge[2]);
|
||||||
if (toId !== undefined) newEdges.push([extEdge[0], extEdge[1], toId, intEdge[3]]);
|
if (toId !== undefined) newEdges.push([extEdge[0], extEdge[1], toId, intEdge[3]]);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,16 +12,16 @@
|
|||||||
const { manager, node = $bindable() }: Props = $props();
|
const { manager, node = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const activeGroup = $derived.by(() => {
|
const activeGroup = $derived.by(() => {
|
||||||
|
if (node?.type === '__internal/group/instance') {
|
||||||
|
return manager.getGroup(node.props?.groupId as number);
|
||||||
|
}
|
||||||
|
|
||||||
if (manager?.isInsideGroup) {
|
if (manager?.isInsideGroup) {
|
||||||
const activeGroupId = manager.graphStack?.at(-1)?.groupId;
|
const activeGroupId = manager.parentStack?.at(-1)?.id;
|
||||||
if (activeGroupId !== undefined) {
|
if (activeGroupId !== undefined) {
|
||||||
return manager.getGroup(activeGroupId);
|
return manager.getGroup(activeGroupId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (node?.type === '__internal/group/instance') {
|
|
||||||
return manager.getGroup(node.props?.groupId as number);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const groupName = $derived(activeGroup?.name ?? '');
|
const groupName = $derived(activeGroup?.name ?? '');
|
||||||
|
|||||||
+20
-16
@@ -95,7 +95,7 @@
|
|||||||
randomSeed: { type: 'boolean', value: false }
|
randomSeed: { type: 'boolean', value: false }
|
||||||
});
|
});
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (graphSettings && graphSettingTypes) {
|
if (graphSettings && graphSettingTypes && manager?.loaded) {
|
||||||
manager?.setSettings($state.snapshot(graphSettings));
|
manager?.setSettings($state.snapshot(graphSettings));
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
@@ -255,20 +255,22 @@
|
|||||||
</Grid.Cell>
|
</Grid.Cell>
|
||||||
<Grid.Cell>
|
<Grid.Cell>
|
||||||
{#if pm.graph}
|
{#if pm.graph}
|
||||||
<GraphInterface
|
{#key pm.graph.id}
|
||||||
graph={pm.graph}
|
<GraphInterface
|
||||||
bind:this={graphInterface}
|
graph={pm.graph}
|
||||||
registry={nodeRegistry}
|
bind:this={graphInterface}
|
||||||
safePadding={{ right: sidebarOpen ? 321 : undefined }}
|
registry={nodeRegistry}
|
||||||
backgroundType={appSettings.value.nodeInterface.backgroundType}
|
safePadding={{ right: sidebarOpen ? 321 : undefined }}
|
||||||
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
|
backgroundType={appSettings.value.nodeInterface.backgroundType}
|
||||||
bind:activeNode
|
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
|
||||||
bind:showHelp={appSettings.value.nodeInterface.showHelp}
|
bind:activeNode
|
||||||
bind:settings={graphSettings}
|
bind:showHelp={appSettings.value.nodeInterface.showHelp}
|
||||||
bind:settingTypes={graphSettingTypes}
|
bind:settings={graphSettings}
|
||||||
onsave={(g) => pm.saveGraph(g)}
|
bind:settingTypes={graphSettingTypes}
|
||||||
onresult={(result) => handleUpdate(result as Graph)}
|
onsave={(g) => pm.saveGraph(g)}
|
||||||
/>
|
onresult={(result) => handleUpdate(result as Graph)}
|
||||||
|
/>
|
||||||
|
{/key}
|
||||||
{/if}
|
{/if}
|
||||||
<Sidebar bind:open={sidebarOpen}>
|
<Sidebar bind:open={sidebarOpen}>
|
||||||
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
<Panel id="general" title="General" icon="i-[tabler--settings]">
|
||||||
@@ -322,7 +324,9 @@
|
|||||||
hidden={!appSettings.value.debug.advancedMode}
|
hidden={!appSettings.value.debug.advancedMode}
|
||||||
icon="i-[tabler--code]"
|
icon="i-[tabler--code]"
|
||||||
>
|
>
|
||||||
<GraphSource graph={manager?.serializeFullGraph()} />
|
{#if manager?.status === 'idle'}
|
||||||
|
<GraphSource graph={manager.serialize()} />
|
||||||
|
{/if}
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel
|
<Panel
|
||||||
id="benchmark"
|
id="benchmark"
|
||||||
|
|||||||
Reference in New Issue
Block a user