diff --git a/app/src/lib/graph-interface/graph-manager.svelte.ts b/app/src/lib/graph-interface/graph-manager.svelte.ts index 75dfb85..9ec0566 100644 --- a/app/src/lib/graph-interface/graph-manager.svelte.ts +++ b/app/src/lib/graph-interface/graph-manager.svelte.ts @@ -69,7 +69,7 @@ export class GraphManager extends EventEmitter<{ status = $state<'loading' | 'idle' | 'error'>(); loaded = false; - graph: Graph = { id: 0, nodes: [], edges: [], groups: [] }; + graph: Graph = $state({ id: 0, nodes: [], edges: [], groups: [] }); id = $state(0); nodes = new SvelteMap(); @@ -495,9 +495,24 @@ export class GraphManager extends EventEmitter<{ const groupId = this.graphStack.at(-1)?.groupId; const group = groupId !== undefined ? this.getGroup(groupId) : undefined; if (!group) return node.state.type; + + const groupInputs: NodeDefinition['inputs'] = Object.fromEntries( + Object.values(group?.inputs || {}).map((o, i) => { + return [ + `in_${i}`, + { + ...o, + external: true + } + ]; + }) || [] + ); return { id: '__internal/group/input' as NodeId, - outputs: Object.values(group.inputs ?? {}).map(i => i.type), + meta: { + title: 'Group Input' + }, + inputs: groupInputs, execute: (x: Int32Array) => x } as NodeDefinition; } @@ -514,6 +529,9 @@ export class GraphManager extends EventEmitter<{ i ) => [`out_${i}`, { type: o.type, label: o.label, external: true }]) ), + meta: { + title: 'Group Output' + }, outputs: [], execute: (x: Int32Array) => x } as NodeDefinition; @@ -1007,7 +1025,9 @@ export class GraphManager extends EventEmitter<{ const toType = this.getNodeType(to); // check if socket types match - const fromSocketType = fromType?.outputs?.[fromSocket]; + const fromSocketType = from.type === '__internal/group/input' + ? fromType?.inputs?.[Object.keys(fromType?.inputs || {})[fromSocket]].type + : fromType?.outputs?.[fromSocket]; const toSocketType = [toType?.inputs?.[toSocket]?.type]; if (toType?.inputs?.[toSocket]?.accepts) { toSocketType.push(...(toType?.inputs?.[toSocket]?.accepts || [])); @@ -1179,7 +1199,9 @@ export class GraphManager extends EventEmitter<{ } }); - const ownType = nodeType.outputs?.[index]; + const ownType = node.type === '__internal/group/input' + ? nodeType.inputs?.[Object.keys(nodeType?.inputs || {})[index]].type + : nodeType.outputs?.[index]; for (const node of nodes) { const inputs = this.getNodeType(node)?.inputs; diff --git a/app/src/lib/graph-interface/graph-state.svelte.ts b/app/src/lib/graph-interface/graph-state.svelte.ts index 37e820b..1146e99 100644 --- a/app/src/lib/graph-interface/graph-state.svelte.ts +++ b/app/src/lib/graph-interface/graph-state.svelte.ts @@ -384,6 +384,13 @@ export class GraphState { node: NodeInstance, index: string | number ): [number, number] { + if (node.type === '__internal/group/input' && typeof index === 'number') { + return [ + (node?.state?.x ?? node.position[0]) + 20, + (node?.state?.y ?? node.position[1]) + 2.5 + 5 * index + 5 + ]; + } + if (typeof index === 'number') { return [ (node?.state?.x ?? node.position[0]) + 20, diff --git a/app/src/lib/graph-interface/graph/Graph.svelte b/app/src/lib/graph-interface/graph/Graph.svelte index e2e6744..352791c 100644 --- a/app/src/lib/graph-interface/graph/Graph.svelte +++ b/app/src/lib/graph-interface/graph/Graph.svelte @@ -100,8 +100,22 @@ if (typeof index === 'string') { return nodeType?.inputs?.[index].type || 'unknown'; } + + if (node.type === '__internal/group/input') { + const key = Object.keys(nodeType?.inputs || {})[index]; + return nodeType?.inputs?.[key].type || 'unknown'; + } + 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}`; + } + } keymap.handleKeyboardEvent(ev)} onmousedown={(ev) => mouseEvents.handleMouseDown(ev)} oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)} {...fileDropEvents.getEventListenerProps()} > +
graphState.exitGroupNode()}> ↑ Exit Group +

+ Group {getGroupName()} +

{/if} @@ -250,6 +270,15 @@ height: 100%; } + .group-name { + position: absolute; + left: calc(50% - var(--padding-right) / 2); + transition: left 0.3s ease; + top: 12px; + transform: translateX(-50%); + z-index: 1000; + } + .exit-group { position: absolute; top: 12px; @@ -280,6 +309,22 @@ cursor: pointer; } + .shadow { + position: absolute; + top: -5px; + left: -5px; + right: calc(var(--padding-right) - 5px); + bottom: -5px; + z-index: 1; + transition: box-shadow 0.3s ease; + box-shadow: 0 0 0px 0px var(--color-layer-2) inset; + pointer-events: none; + } + + .is-inside-group .shadow { + box-shadow: 0 0 0px 8px var(--color-layer-2) inset; + } + .is-panning { cursor: grab; } diff --git a/app/src/lib/graph-interface/helpers/nodeHelpers.ts b/app/src/lib/graph-interface/helpers/nodeHelpers.ts index 57bc166..7e1c149 100644 --- a/app/src/lib/graph-interface/helpers/nodeHelpers.ts +++ b/app/src/lib/graph-interface/helpers/nodeHelpers.ts @@ -1,6 +1,10 @@ import type { NodeDefinition } from '@nodarium/types'; export function getParameterHeight(node: NodeDefinition, inputKey: string) { + if (node.id === '__internal/group/input') { + return 50; + } + const input = node.inputs?.[inputKey]; if (!input) { return 0; diff --git a/app/src/lib/graph-interface/node/NodeHeader.svelte b/app/src/lib/graph-interface/node/NodeHeader.svelte index 9bb56a2..a292c74 100644 --- a/app/src/lib/graph-interface/node/NodeHeader.svelte +++ b/app/src/lib/graph-interface/node/NodeHeader.svelte @@ -23,7 +23,10 @@ const cornerTop = 10; const nodeType = $derived(graph.getNodeType(node)); - const rightBump = $derived(!!nodeType?.outputs?.length); + const rightBump = $derived( + !!nodeType?.outputs?.length && node.type !== '__internal/group/input' + ); + const aspectRatio = 0.25; const path = $derived( @@ -73,13 +76,15 @@ {/if} {nodeType?.meta?.title || node.type?.split('/').pop()} -
-
+ {#if rightBump} +
+
+ {/if} key === id); + graphState.setDownSocket({ + node, + index: outputIndex, + position: graphState.getSocketPosition(node, outputIndex) + }); + } else { + graphState.setDownSocket({ + node, + index: id, + position: graphState.getSocketPosition(node, id) + }); + } } - const leftBump = $derived(nodeType.inputs?.[id].internal !== true); + const leftBump = $derived( + nodeType.inputs?.[id].internal !== true && node.type !== '__internal/group/input' + ); + const rightBump = $derived(node.type === '__internal/group/input'); const cornerBottom = $derived(isLast ? 5 : 0); const aspectRatio = 0.5; @@ -46,6 +59,7 @@ height: 2000 / height, y: 50.5, cornerBottom, + rightBump, leftBump, aspectRatio }) @@ -55,6 +69,7 @@ depth: 7, height: 2200 / height, y: 50.5, + rightBump, cornerBottom, leftBump, aspectRatio @@ -76,6 +91,7 @@
-{:else} -

Node has no settings

{/if} diff --git a/app/src/lib/sidebar/panels/ActiveNodeSettings.svelte b/app/src/lib/sidebar/panels/ActiveNodeSettings.svelte index 49d1da4..e5ece84 100644 --- a/app/src/lib/sidebar/panels/ActiveNodeSettings.svelte +++ b/app/src/lib/sidebar/panels/ActiveNodeSettings.svelte @@ -11,97 +11,11 @@ let { manager, node = $bindable() }: Props = $props(); const isGroupInstance = $derived(node?.type === '__internal/group/instance'); - const activeGroup = $derived( - isGroupInstance ? manager.getGroup(node!.props?.groupId as number) : undefined - ); - - const groupName = $derived(activeGroup?.name ?? ''); - - function handleRename(e: Event) { - const name = (e.target as HTMLInputElement).value; - if (activeGroup) manager.renameGroup(activeGroup.id, name); - } -
-

Node Settings

-
- -{#if node} - {#key node.id} - {#if isGroupInstance && activeGroup} -
- - -
- {:else if node} - - {/if} - {/key} -{:else} -

No node selected

-{/if} - -{#if manager?.graph.groups.length} -
- +{#if node && !isGroupInstance} +
+

Node Settings

+ {/if} - - diff --git a/app/src/lib/sidebar/panels/GroupSettings.svelte b/app/src/lib/sidebar/panels/GroupSettings.svelte new file mode 100644 index 0000000..c285adb --- /dev/null +++ b/app/src/lib/sidebar/panels/GroupSettings.svelte @@ -0,0 +1,120 @@ + + +{#if activeGroup || hasUnusedGroups} +
+

Group Settings

+
+{/if} + +{#if activeGroup} + {#key activeGroup.id} +
+ + +
+ {/key} +{/if} + +{#if hasUnusedGroups} +
+ +
+{/if} + + diff --git a/app/src/routes/+page.svelte b/app/src/routes/+page.svelte index ccc4fb7..677b908 100644 --- a/app/src/routes/+page.svelte +++ b/app/src/routes/+page.svelte @@ -21,6 +21,7 @@ import Changelog from '$lib/sidebar/panels/Changelog.svelte'; import ExportSettings from '$lib/sidebar/panels/ExportSettings.svelte'; import GraphSource from '$lib/sidebar/panels/GraphSource.svelte'; + import GroupSettings from '$lib/sidebar/panels/GroupSettings.svelte'; import Keymap from '$lib/sidebar/panels/Keymap.svelte'; import { panelState } from '$lib/sidebar/PanelState.svelte'; import Sidebar from '$lib/sidebar/Sidebar.svelte'; @@ -258,7 +259,7 @@ graph={pm.graph} bind:this={graphInterface} registry={nodeRegistry} - safePadding={{ right: sidebarOpen ? 330 : undefined }} + safePadding={{ right: sidebarOpen ? 320 : undefined }} backgroundType={appSettings.value.nodeInterface.backgroundType} snapToGrid={appSettings.value.nodeInterface.snapToGrid} bind:activeNode @@ -336,12 +337,14 @@ title="Graph Settings" icon="i-[custom--graph] bg-blue-400" > + + - import { SvelteMap } from 'svelte/reactivity'; - const cache = new SvelteMap>(); + // eslint-disable-next-line svelte/prefer-svelte-reactivity + const cache = new Map>(); function getStore(root: string): Record { if (!cache.has(root)) {