feat: make group input/output node work
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m11s
🚀 Lint & Test & Deploy / quality (pull_request) Successful in 2m7s
🚀 Lint & Test & Deploy / test-unit (pull_request) Successful in 32s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Successful in 1m50s
🚀 Lint & Test & Deploy / deploy (pull_request) Successful in 1m56s
📊 Benchmark the Runtime / benchmark (pull_request) Successful in 1m11s
🚀 Lint & Test & Deploy / quality (pull_request) Successful in 2m7s
🚀 Lint & Test & Deploy / test-unit (pull_request) Successful in 32s
🚀 Lint & Test & Deploy / test-e2e (pull_request) Successful in 1m50s
🚀 Lint & Test & Deploy / deploy (pull_request) Successful in 1m56s
This commit is contained in:
@@ -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<number, NodeInstance>();
|
||||
@@ -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;
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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}`;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
@@ -114,6 +128,7 @@
|
||||
bind:this={graphState.wrapper}
|
||||
class="graph-wrapper"
|
||||
style="height: 100%"
|
||||
class:is-inside-group={graph.isInsideGroup}
|
||||
class:is-panning={graphState.isPanning}
|
||||
class:is-hovering={graphState.hoveredNodeId !== -1}
|
||||
aria-label="Graph"
|
||||
@@ -121,11 +136,13 @@
|
||||
tabindex="0"
|
||||
bind:clientWidth={graphState.width}
|
||||
bind:clientHeight={graphState.height}
|
||||
style:--padding-right="{safePadding?.right || 0}px"
|
||||
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
||||
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
||||
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
|
||||
{...fileDropEvents.getEventListenerProps()}
|
||||
>
|
||||
<div class="shadow"></div>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/wasm,application/json"
|
||||
@@ -140,6 +157,9 @@
|
||||
<button class="exit-group" onclick={() => graphState.exitGroupNode()}>
|
||||
↑ Exit Group
|
||||
</button>
|
||||
<p class="group-name absolute">
|
||||
Group <b>{getGroupName()}</b>
|
||||
</p>
|
||||
{/if}
|
||||
|
||||
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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,6 +76,7 @@
|
||||
{/if}
|
||||
{nodeType?.meta?.title || node.type?.split('/').pop()}
|
||||
</div>
|
||||
{#if rightBump}
|
||||
<div
|
||||
class="target"
|
||||
role="button"
|
||||
@@ -80,6 +84,7 @@
|
||||
onmousedown={handleMouseDown}
|
||||
>
|
||||
</div>
|
||||
{/if}
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 100 100"
|
||||
|
||||
@@ -29,14 +29,27 @@
|
||||
function handleMouseDown(ev: MouseEvent) {
|
||||
ev.preventDefault();
|
||||
ev.stopPropagation();
|
||||
|
||||
if (node.type === '__internal/group/input') {
|
||||
const outputIndex = Object.entries(nodeType?.inputs ?? {}).findIndex(([key]) => 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 @@
|
||||
<div
|
||||
class="wrapper"
|
||||
data-node-type={node.type}
|
||||
class:is-group-input={node.type === '__internal/group/input'}
|
||||
data-node-input={id}
|
||||
style:height="{height}px"
|
||||
style:--socket-color={hoverColor}
|
||||
@@ -130,6 +146,11 @@
|
||||
transform: translateY(-50%) translateX(-50%);
|
||||
}
|
||||
|
||||
.is-group-input .target {
|
||||
right: 0px;
|
||||
transform: translateY(-50%) translateX(50%);
|
||||
}
|
||||
|
||||
.possible-socket .target::before {
|
||||
content: "";
|
||||
position: absolute;
|
||||
|
||||
@@ -1,5 +1,8 @@
|
||||
export const debugNode = {
|
||||
id: '__internal/debug/instance',
|
||||
meta: {
|
||||
title: 'Debug'
|
||||
},
|
||||
inputs: {
|
||||
input: {
|
||||
type: '*'
|
||||
|
||||
@@ -96,6 +96,4 @@
|
||||
bind:value={store}
|
||||
type={nodeDefinition}
|
||||
/>
|
||||
{:else}
|
||||
<p class="mx-4 mt-4">Node has no settings</p>
|
||||
{/if}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
</script>
|
||||
|
||||
{#if node && !isGroupInstance}
|
||||
<div class='{node?"border-l-2 pl-3.5!":""} bg-layer-2 flex items-center h-[70px] border-b-1 border-l-selected border-b-outline pl-4'>
|
||||
<h3>Node Settings</h3>
|
||||
</div>
|
||||
|
||||
{#if node}
|
||||
{#key node.id}
|
||||
{#if isGroupInstance && activeGroup}
|
||||
<div class="group-settings">
|
||||
<label for="group-name">Group name</label>
|
||||
<input
|
||||
id="group-name"
|
||||
type="text"
|
||||
placeholder="Group {activeGroup.id}"
|
||||
value={groupName}
|
||||
oninput={handleRename}
|
||||
/>
|
||||
</div>
|
||||
{:else if node}
|
||||
<ActiveNodeSelected {manager} bind:node />
|
||||
{/if}
|
||||
{/key}
|
||||
{:else}
|
||||
<p class="mx-4 mt-4">No node selected</p>
|
||||
{/if}
|
||||
|
||||
{#if manager?.graph.groups.length}
|
||||
<div class="group-actions">
|
||||
<button onclick={() => manager.removeUnusedGroups()}>
|
||||
Remove unused groups
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.group-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.group-settings label {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.group-settings input {
|
||||
background: var(--color-layer-1);
|
||||
border: 1px solid var(--color-outline);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-size: 0.9em;
|
||||
padding: 0.4em 0.6em;
|
||||
}
|
||||
|
||||
.group-settings input:focus {
|
||||
outline: 1px solid var(--color-active);
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
padding: 0.75em 1em;
|
||||
border-top: 1px solid var(--color-outline);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.group-actions button {
|
||||
width: 100%;
|
||||
padding: 0.4em 0.8em;
|
||||
background: var(--color-layer-1);
|
||||
border: 1px solid var(--color-outline);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-actions button:hover {
|
||||
border-color: var(--color-active);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
<script lang="ts">
|
||||
import type { GraphManager } from '$lib/graph-interface/graph-manager.svelte';
|
||||
import type { NodeInstance } from '@nodarium/types';
|
||||
|
||||
type Props = {
|
||||
manager: GraphManager;
|
||||
node?: NodeInstance;
|
||||
};
|
||||
|
||||
const { manager, node = $bindable() }: Props = $props();
|
||||
|
||||
const activeGroup = $derived.by(() => {
|
||||
console.log('isInsideGroup', manager?.isInsideGroup);
|
||||
if (manager?.isInsideGroup) {
|
||||
const activeGroupId = manager.graphStack?.at(-1)?.groupId;
|
||||
console.log('activeGroupId', activeGroupId);
|
||||
if (activeGroupId !== undefined) {
|
||||
return manager.getGroup(activeGroupId);
|
||||
}
|
||||
}
|
||||
|
||||
if (node?.type === '__internal/group/instance') {
|
||||
return manager.getGroup(node.props?.groupId as number);
|
||||
}
|
||||
});
|
||||
|
||||
const groupName = $derived(activeGroup?.name ?? '');
|
||||
function handleRename(e: Event) {
|
||||
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;
|
||||
});
|
||||
</script>
|
||||
|
||||
{#if activeGroup || hasUnusedGroups}
|
||||
<div class='{node?"border-l-2 pl-3.5!":""} bg-layer-2 flex items-center h-[70px] border-b-1 border-l-selected border-b-outline pl-4'>
|
||||
<h3>Group Settings</h3>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if activeGroup}
|
||||
{#key activeGroup.id}
|
||||
<div class="group-settings">
|
||||
<label for="group-name">Group name</label>
|
||||
<input
|
||||
id="group-name"
|
||||
type="text"
|
||||
placeholder="Group {activeGroup.id}"
|
||||
value={groupName}
|
||||
oninput={handleRename}
|
||||
/>
|
||||
</div>
|
||||
{/key}
|
||||
{/if}
|
||||
|
||||
{#if hasUnusedGroups}
|
||||
<div class="group-actions">
|
||||
<button onclick={() => manager.removeUnusedGroups()}>
|
||||
Remove ({hasUnusedGroups}) orphaned groups
|
||||
</button>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.group-settings {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 0.4em;
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.group-settings label {
|
||||
font-size: 0.8em;
|
||||
opacity: 0.7;
|
||||
}
|
||||
|
||||
.group-settings input {
|
||||
background: var(--color-layer-1);
|
||||
border: 1px solid var(--color-outline);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-size: 0.9em;
|
||||
padding: 0.4em 0.6em;
|
||||
}
|
||||
|
||||
.group-settings input:focus {
|
||||
outline: 1px solid var(--color-active);
|
||||
}
|
||||
|
||||
.group-actions {
|
||||
padding: 0.75em 1em;
|
||||
border-bottom: 1px solid var(--color-outline);
|
||||
margin-top: auto;
|
||||
}
|
||||
|
||||
.group-actions button {
|
||||
width: 100%;
|
||||
padding: 0.4em 0.8em;
|
||||
background: var(--color-layer-1);
|
||||
border: 1px solid var(--color-outline);
|
||||
border-radius: 4px;
|
||||
color: var(--color-text);
|
||||
font-family: var(--font-family);
|
||||
font-size: 0.85em;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.group-actions button:hover {
|
||||
border-color: var(--color-active);
|
||||
}
|
||||
</style>
|
||||
@@ -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"
|
||||
>
|
||||
<span class="block h-[1px]"></span>
|
||||
<NestedSettings
|
||||
id="graph-settings"
|
||||
type={graphSettingTypes}
|
||||
bind:value={graphSettings}
|
||||
/>
|
||||
<ActiveNodeSettings {manager} bind:node={activeNode} />
|
||||
<GroupSettings {manager} bind:node={activeNode} />
|
||||
</Panel>
|
||||
<Panel
|
||||
id="changelog"
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
<script module lang="ts">
|
||||
import { SvelteMap } from 'svelte/reactivity';
|
||||
const cache = new SvelteMap<string, Record<string, boolean>>();
|
||||
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||
const cache = new Map<string, Record<string, boolean>>();
|
||||
|
||||
function getStore(root: string): Record<string, boolean> {
|
||||
if (!cache.has(root)) {
|
||||
|
||||
Reference in New Issue
Block a user