feat: implement node sockets ui
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m22s
🚀 Lint & Test & Deploy / quality (pull_request) Failing after 1m6s
🚀 Lint & Test & Deploy / test-unit (pull_request) Failing after 43s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Successful in 2m5s
🚀 Lint & Test & Deploy / deploy (pull_request) Has been skipped

This commit is contained in:
2026-05-05 21:07:57 +02:00
parent ed11195327
commit 7f082ad8f6
12 changed files with 331 additions and 40 deletions
@@ -51,7 +51,7 @@ export class GraphManager extends EventEmitter<{
id: number;
nodes: SerializedNode[];
edges: SerializedEdge[];
cameraPosition: [number, number, number];
nodeId: number; // group instance node id that was entered to reach the next level
}[] = $state([]);
// ID of the currently active group, or null when at the root graph.
@@ -642,11 +642,11 @@ export class GraphManager extends EventEmitter<{
isInsideGroup = $derived(this.currentGroupId !== null);
enterGroup(nodeId: number, cameraPosition: [number, number, number]): boolean {
enterGroup(nodeId: number): boolean {
const groupNode = this.getNode(nodeId);
if (!groupNode || groupNode.type !== '__internal/group/instance') return false;
log.log('entering group', { nodeId, cameraPosition });
log.log('entering group', { nodeId });
const groupId = groupNode.props?.groupId as number;
const group = this.getGroup(groupId);
@@ -657,7 +657,7 @@ export class GraphManager extends EventEmitter<{
id: this.currentGroupId ?? this.id,
nodes: [...this.nodes.values()].map(n => serializeNode(n)),
edges: [...this.edges.values()].map(e => serializeEdge(e)),
cameraPosition
nodeId
});
this.currentGroupId = groupId;
@@ -685,6 +685,8 @@ export class GraphManager extends EventEmitter<{
this.history.reset();
this.execute();
this.save();
return { nodeId: parent.nodeId };
}
createNodeId() {
@@ -365,7 +365,7 @@ export class GraphState {
if (this.activeNodeId === -1) return;
const node = this.graph.getNode(this.activeNodeId);
if (!node || node.type !== '__internal/group/instance') return;
const ok = this.graph.enterGroup(this.activeNodeId, [...this.cameraPosition]);
const ok = this.graph.enterGroup(this.activeNodeId);
if (ok) {
this.activeNodeId = -1;
this.clearSelection();
@@ -375,7 +375,6 @@ export class GraphState {
exitGroupNode() {
const result = this.graph.exitGroup();
if (!result) return;
this.cameraPosition = result.camera;
this.activeNodeId = result.nodeId;
this.clearSelection();
}
+9 -1
View File
@@ -2,7 +2,7 @@ type Color = { hue: number; saturation: number; lightness: number };
export class ColorGenerator {
private colors: Map<string, Color> = new Map();
private lightnessLevels = [10, 60];
// private lightnessLevels = [10, 60];
constructor(predefined: Record<string, Color>) {
for (const [id, colorStr] of Object.entries(predefined)) {
@@ -10,6 +10,14 @@ export class ColorGenerator {
}
}
public getColors() {
return Object.fromEntries(
this.colors.entries().map(([key, col]) => {
return [key, this.colorToHsl(col)];
})
);
}
public getColor(id: string): string {
if (this.colors.has(id)) {
return this.colorToHsl(this.colors.get(id)!);
@@ -26,6 +26,13 @@
const rightBump = $derived(
!!nodeType?.outputs?.length && node.type !== '__internal/group/input'
);
const cornerBottom = $derived(
node.type === '__internal/group/input'
? (Object.keys(nodeType?.inputs ?? {}).length ? 0 : 10)
: node.type === '__internal/group/output'
? (nodeType?.outputs?.length ? 0 : 10)
: 0
);
const aspectRatio = 0.25;
@@ -35,6 +42,7 @@
height: 34,
y: 49,
cornerTop,
cornerBottom,
rightBump,
aspectRatio
})
@@ -45,6 +53,7 @@
height: 40,
y: 49,
cornerTop,
cornerBottom,
rightBump,
aspectRatio
})
+78 -23
View File
@@ -1,26 +1,26 @@
<script lang="ts">
import type { GraphManager } from '$lib/graph-interface/graph-manager.svelte';
import { GraphState } from '$lib/graph-interface/graph-state.svelte';
import type { NodeInstance } from '@nodarium/types';
import InputSelect from '../../../../../packages/ui/src/lib/inputs/InputSelect.svelte';
import { SocketTable } from '@nodarium/ui';
import UnusedGroupsPanel from './UnusedGroupsPanel.svelte';
type Props = {
manager: GraphManager;
graphState: GraphState;
node?: NodeInstance;
};
const { manager, node = $bindable() }: Props = $props();
const { manager, graphState, node = $bindable() }: Props = $props();
const activeGroup = $derived.by(() => {
if (node?.type === '__internal/group/instance') {
return manager.getGroup(node.props?.groupId as number);
let group = manager.getGroup(node.props?.groupId as number);
if (group) return group;
}
if (manager?.isInsideGroup) {
const activeGroupId = manager.parentStack?.at(-1)?.id;
if (activeGroupId !== undefined) {
return manager.getGroup(activeGroupId);
}
if (manager?.isInsideGroup && manager.currentGroupId !== null) {
return manager.getGroup(manager.currentGroupId);
}
});
@@ -29,6 +29,47 @@
const name = (e.target as HTMLInputElement).value;
if (activeGroup) manager.renameGroup(activeGroup.id, name);
}
function handleRemoveInput(key: string) {
if (!activeGroup) return;
const group = manager.getGroup(activeGroup?.id);
const inputs = $state.snapshot(group?.inputs ?? {});
delete inputs[key];
activeGroup.inputs = inputs;
manager.nodes = manager.nodes;
manager.save();
}
const types = $derived(
Array.from(
new Set(
manager?.registry
? manager.registry.getAllNodes()
.flatMap(n =>
Object.values(n.inputs ?? {})
.map(v => v.type)
)
: []
)
)
);
let outputType = $derived(activeGroup?.outputs?.[0]?.type ?? 'unknown');
$effect(() => {
if (!activeGroup) return;
const group = manager.getGroup(activeGroup?.id);
const outputs = $state.snapshot(group?.outputs ?? []);
if (outputs?.[0]?.type === outputType) return;
activeGroup.outputs = [
{
label: outputs[0]?.label ?? 'Output',
type: outputType
}
];
manager.nodes = manager.nodes;
manager.save();
});
</script>
{#if activeGroup}
@@ -39,7 +80,7 @@
{#if activeGroup}
{#key activeGroup.id}
<div class="group-settings">
<div class="p-4 group-settings">
<label for="group-name">Group name</label>
<input
id="group-name"
@@ -51,18 +92,33 @@
<label for="group-name">Group Inputs</label>
<div>
{#each Object.keys(activeGroup?.inputs ?? {}) as key (key)}
<div class="flex">
<InputSelect
value={activeGroup.inputs?.[key].type}
options={['seed', 'float', 'boolean']}
/>
<input type="text" placeholder="Input {key}" />
<button>
🥊
</button>
</div>
{/each}
<SocketTable
{types}
onremove={handleRemoveInput}
bind:inputs={activeGroup.inputs}
colors={graphState?.colors?.getColors()}
/>
</div>
<label for="group-name mb-2">Group output</label>
<div class="flex bg-layer-2 rounded-sm outline outline-outline w-min">
<span
style:background={graphState?.colors?.getColor(outputType)}
class="block opacity-50 min-w-2 ml-2 w-2 h-2 my-auto rounded-sm"
></span>
<select
class="text-[0.9em] shrink-0 px-2 py-1 border-outline"
bind:value={outputType}
>
{#each types as type (type)}
<option>
<span
style="background: {graphState?.colors?.getColor(type)}; width: 5px; height: 5px; border-radius: 5px;"
></span>
{type}
</option>
{/each}
</select>
</div>
</div>
{/key}
@@ -77,10 +133,9 @@
display: flex;
flex-direction: column;
gap: 0.4em;
padding: 1em;
}
.group-settings label {
label {
font-size: 0.8em;
opacity: 0.7;
}
@@ -48,7 +48,7 @@
</script>
{#if unusedTree.length}
<div class="panel">
<div class="panel p-4">
<div class="header">
<span>Unused groups</span>
<button class="remove-all" onclick={() => manager.removeUnusedGroups()}>
@@ -78,8 +78,9 @@
<style>
.panel {
border-top: 1px solid var(--color-outline);
margin-top: -1px;
border-bottom: 1px solid var(--color-outline);
padding: 0.75em 1em;
}
.header {
+1 -1
View File
@@ -349,7 +349,7 @@
/>
{#key activeNode}
<ActiveNodeSettings {manager} bind:node={activeNode} />
<GroupSettings {manager} bind:node={activeNode} />
<GroupSettings graphState={graphInterface?.state} {manager} bind:node={activeNode} />
{/key}
</Panel>
<Panel