diff --git a/app/src/lib/graph-interface/components/GroupBreadcrumps.svelte b/app/src/lib/graph-interface/components/GroupBreadcrumps.svelte new file mode 100644 index 0000000..8601bf2 --- /dev/null +++ b/app/src/lib/graph-interface/components/GroupBreadcrumps.svelte @@ -0,0 +1,64 @@ + + +
+ +{#if graph.isInsideGroup} +
+ + {#each graph.graphStack as group (group.groupId)} + + + {/each} +
+{/if} + + diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index e588e7a..b4b0713 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -1,3 +1,4 @@ +import { clone } from '$lib/helpers'; import throttle from '$lib/helpers/throttle'; import { RemoteNodeRegistry } from '$lib/node-registry/index'; import type { @@ -16,6 +17,12 @@ import { fastHashString } from '@nodarium/utils'; import { createLogger } from '@nodarium/utils'; import { SvelteMap, SvelteSet } from 'svelte/reactivity'; import EventEmitter from './helpers/EventEmitter'; +import { + areEdgesEqual, + areSocketsCompatible, + serializeEdge, + serializeNode +} from './helpers/nodeHelpers'; import { HistoryManager } from './history-manager'; const logger = createLogger('graph-manager'); @@ -23,41 +30,6 @@ logger.mute(); const remoteRegistry = new RemoteNodeRegistry(''); -const clone = 'structuredClone' in globalThis - ? globalThis.structuredClone - : (args: unknown) => JSON.parse(JSON.stringify(args)); - -function areSocketsCompatible( - output: string | undefined, - inputs: string | (string | undefined)[] | undefined -) { - if (output === '*') return true; - if (Array.isArray(inputs) && output) { - return inputs.includes('*') || inputs.includes(output); - } - return inputs === output; -} - -function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) { - if (firstEdge[0].id !== secondEdge[0].id) { - return false; - } - - if (firstEdge[1] !== secondEdge[1]) { - return false; - } - - if (firstEdge[2].id !== secondEdge[2].id) { - return false; - } - - if (firstEdge[3] !== secondEdge[3]) { - return false; - } - - return true; -} - export class GraphManager extends EventEmitter<{ save: Graph; result: unknown; @@ -129,26 +101,11 @@ export class GraphManager extends EventEmitter<{ } serialize(): Graph { - const nodes = Array.from(this.nodes.values()).map((node) => ({ - id: node.id, - position: [...node.position], - type: node.type, - props: node.props - })) as NodeInstance[]; - const edges = this.edges.map((edge) => [ - edge[0].id, - edge[1], - edge[2].id, - edge[3] - ]) as Graph['edges']; + const nodes = Array.from(this.nodes.values()).map((node) => serializeNode(node)); + const edges = this.edges.map((edge) => serializeEdge(edge)); const groups = this.graph.groups?.map((group) => { - const groupNodes = group.nodes.map((node) => ({ - id: node.id, - position: [...node.position], - type: node.type, - props: node.props - })) as NodeInstance[]; + const groupNodes = group.nodes.map((node) => serializeNode(node)); return { id: group.id, @@ -814,12 +771,39 @@ export class GraphManager extends EventEmitter<{ return nodes; } + getUnusedGroups() { + const usedGroupIds = new SvelteSet(); + const queue: number[] = []; + + for (const node of this.nodes.values()) { + if (node.type === '__internal/group/instance' && node.props?.groupId !== undefined) { + queue.push(node.props.groupId as number); + } + } + + while (queue.length) { + const groupId = queue.pop()!; + if (usedGroupIds.has(groupId)) continue; + usedGroupIds.add(groupId); + const group = this.getGroup(groupId); + if (!group) continue; + for (const node of group.nodes) { + if (node.type === '__internal/group/instance' && node.props?.groupId !== undefined) { + const childId = node.props.groupId as number; + if (!usedGroupIds.has(childId)) queue.push(childId); + } + } + } + + return this.graph.groups.filter(g => !usedGroupIds.has(g.id)); + } + removeUnusedGroups() { - const usedGroups = new SvelteSet(this.getAllNodes().map(n => n.props?.groupId)); - const unusedGroupAmount = this.graph.groups.length - usedGroups.size; - this.graph.groups = this.graph.groups.filter(g => usedGroups.has(g.id)); + const unused = this.getUnusedGroups(); + const unusedIds = new SvelteSet(unused.map(g => g.id)); + this.graph.groups = this.graph.groups.filter(g => !unusedIds.has(g.id)); this.save(); - return unusedGroupAmount; + return unused.length; } groupNodes(nodeIds: number[]) { @@ -863,10 +847,14 @@ export class GraphManager extends EventEmitter<{ inputs[`input_${i}`] = input as NodeInput; }); - const outputs = [groupOutputs.values().next().value!].map((edge, i) => ({ - label: `Output ${i}`, - type: edge[2].state.type?.inputs?.[edge[3]].type || '*' - })); + const outputs = []; + if (groupOutputs.size) { + const edge = groupOutputs.values().next().value!; + outputs.push({ + label: `Output`, + type: edge[2].state.type?.inputs?.[edge[3]].type || '*' + }); + } const groupPosition = [0, 0] as [number, number]; const bounds: Box = { minX: Infinity, maxX: -Infinity, minY: Infinity, maxY: -Infinity }; diff --git a/app/src/lib/graph-interface/graph/Graph.svelte b/app/src/lib/graph-interface/graph/Graph.svelte index d724edf..f66defd 100644 --- a/app/src/lib/graph-interface/graph/Graph.svelte +++ b/app/src/lib/graph-interface/graph/Graph.svelte @@ -7,6 +7,7 @@ import AddMenu from '../components/AddMenu.svelte'; import BoxSelection from '../components/BoxSelection.svelte'; import Camera from '../components/Camera.svelte'; + import GroupBreadcrumps from '../components/GroupBreadcrumps.svelte'; import HelpView from '../components/HelpView.svelte'; import Debug from '../debug/Debug.svelte'; import EdgeEl from '../edges/Edge.svelte'; @@ -109,13 +110,15 @@ return nodeType?.outputs?.[index] || 'unknown'; } - function getGroupName() { - const groupId = graph.graphStack.at(-1)?.groupId; - if (groupId !== undefined) { - const group = graph.getGroup(groupId); - return group?.name || `Group#${groupId}`; + let groupSize = 0; + $effect(() => { + if (graph.graph.groups.length > groupSize) { + groupSize = graph.graph.groups.length; } - } + if (graph.graph.groups.length < groupSize) { + console.error('We have lost a group!'); + } + }); mouseEvents.handleContextMenu(ev)} {...fileDropEvents.getEventListenerProps()} > -
- {#if graph.isInsideGroup} - -

- Group {getGroupName()} -

- {/if} + = {}; export function getNodeHeight(node: NodeDefinition) { if (!node || !('inputs' in node)) { @@ -45,3 +64,34 @@ export function getNodeHeight(node: NodeDefinition) { nodeHeightCache[node.id] = height; return height; } + +export function areSocketsCompatible( + output: string | undefined, + inputs: string | (string | undefined)[] | undefined +) { + if (output === '*') return true; + if (Array.isArray(inputs) && output) { + return inputs.includes('*') || inputs.includes(output); + } + return inputs === output; +} + +export function areEdgesEqual(firstEdge: Edge, secondEdge: Edge) { + if (firstEdge[0].id !== secondEdge[0].id) { + return false; + } + + if (firstEdge[1] !== secondEdge[1]) { + return false; + } + + if (firstEdge[2].id !== secondEdge[2].id) { + return false; + } + + if (firstEdge[3] !== secondEdge[3]) { + return false; + } + + return true; +} diff --git a/app/src/lib/sidebar/panels/GroupSettings.svelte b/app/src/lib/sidebar/panels/GroupSettings.svelte index 88f2182..5ffff60 100644 --- a/app/src/lib/sidebar/panels/GroupSettings.svelte +++ b/app/src/lib/sidebar/panels/GroupSettings.svelte @@ -2,6 +2,7 @@ import type { GraphManager } from '$lib/graph-interface/graph-manager.svelte'; import type { NodeInstance } from '@nodarium/types'; import InputSelect from '../../../../../packages/ui/src/lib/inputs/InputSelect.svelte'; + import UnusedGroupsPanel from './UnusedGroupsPanel.svelte'; type Props = { manager: GraphManager; @@ -28,18 +29,9 @@ const name = (e.target as HTMLInputElement).value; if (activeGroup) manager.renameGroup(activeGroup.id, name); } - - const hasUnusedGroups = $derived.by(() => { - if (!manager) return false; - if (manager.isInsideGroup) return false; - if (manager.graph.groups.length === 0) return false; - return manager.graph.groups.filter(g => { - return !manager.graph.nodes.find(n => n.props?.groupId === g.id); - }).length; - }); -{#if activeGroup || hasUnusedGroups} +{#if activeGroup}

Group Settings

@@ -76,12 +68,8 @@ {/key} {/if} -{#if hasUnusedGroups} -
- -
+{#if manager && !manager.isInsideGroup} + {/if} diff --git a/app/src/lib/sidebar/panels/UnusedGroupsPanel.svelte b/app/src/lib/sidebar/panels/UnusedGroupsPanel.svelte new file mode 100644 index 0000000..d2f2799 --- /dev/null +++ b/app/src/lib/sidebar/panels/UnusedGroupsPanel.svelte @@ -0,0 +1,134 @@ + + +{#if unusedTree.length} +
+
+ Unused groups + +
+ +
    + {#snippet treeNode(node: GroupNode)} +
  • + {node.group.name || `Group #${node.group.id}`} + {#if node.children.length} +
      + {#each node.children as child (child.group.id)} + {@render treeNode(child)} + {/each} +
    + {/if} +
  • + {/snippet} + {#each unusedTree as node (node.group.id)} + {@render treeNode(node)} + {/each} +
+
+{/if} + + diff --git a/packages/types/src/index.ts b/packages/types/src/index.ts index 374cc5d..31cb6bb 100644 --- a/packages/types/src/index.ts +++ b/packages/types/src/index.ts @@ -8,6 +8,7 @@ export type { NodeDefinition, NodeId, NodeInstance, + SerializedEdge, SerializedNode, Socket } from './types'; diff --git a/packages/types/src/types.ts b/packages/types/src/types.ts index 878f9b5..2a5f7fe 100644 --- a/packages/types/src/types.ts +++ b/packages/types/src/types.ts @@ -76,6 +76,10 @@ export type Socket = { export type Edge = [NodeInstance, number, NodeInstance, string]; +const SerializedEdgeSchema = z.tuple([z.number(), z.number(), z.number(), z.string()]); + +export type SerializedEdge = z.infer; + export const GroupSchema = z.object({ id: z.number(), name: z.string().optional(), @@ -100,7 +104,7 @@ export const GraphSchema = z.object({ .optional(), settings: z.record(z.string(), z.any()).optional(), nodes: z.array(NodeSchema), - edges: z.array(z.tuple([z.number(), z.number(), z.number(), z.string()])), + edges: z.array(SerializedEdgeSchema), groups: z.array(GroupSchema) });