feat: add node group breadcrumbs
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
<script lang="ts">
|
||||
import { getGraphManager } from '../graph-state.svelte';
|
||||
const graph = getGraphManager();
|
||||
|
||||
function getGroupName(groupId: number) {
|
||||
const group = graph.getGroup(groupId);
|
||||
return group?.name || `Group#${groupId}`;
|
||||
}
|
||||
|
||||
function exitToGroup(groupId?: number) {
|
||||
while (graph.graphStack.length > 0 && graph.graphStack.at(-1)?.groupId !== groupId) {
|
||||
graph.exitGroup();
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="shadow" class:is-inside-group={graph.isInsideGroup}></div>
|
||||
|
||||
{#if graph.isInsideGroup}
|
||||
<div class="group-name flex gap-1 items-center">
|
||||
<button
|
||||
class="bg-layer-2 opacity-75 hover:opacity-100 cursor-pointer rounded-sm p-1 px-2"
|
||||
onclick={() => exitToGroup()}
|
||||
>
|
||||
Root
|
||||
</button>
|
||||
{#each graph.graphStack as group (group.groupId)}
|
||||
<span class="i-[tabler--arrow-right]"></span>
|
||||
<button
|
||||
class="bg-layer-2 opacity-75 hover:opacity-100 cursor-pointer rounded-sm p-1 px-2"
|
||||
onclick={() => exitToGroup(group.groupId)}
|
||||
>
|
||||
{getGroupName(group.groupId)}
|
||||
</button>
|
||||
{/each}
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<style>
|
||||
.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;
|
||||
}
|
||||
|
||||
.shadow.is-inside-group {
|
||||
box-shadow: 0 0 0px 8px var(--color-layer-2) inset;
|
||||
}
|
||||
|
||||
.group-name {
|
||||
position: absolute;
|
||||
left: calc(50% - var(--padding-right) / 2);
|
||||
transition: left 0.3s ease;
|
||||
top: 12px;
|
||||
transform: translateX(-50%);
|
||||
z-index: 1000;
|
||||
}
|
||||
</style>
|
||||
@@ -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<number>();
|
||||
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 };
|
||||
|
||||
@@ -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!');
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<svelte:window
|
||||
@@ -142,7 +145,6 @@
|
||||
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
|
||||
{...fileDropEvents.getEventListenerProps()}
|
||||
>
|
||||
<div class="shadow"></div>
|
||||
<input
|
||||
type="file"
|
||||
accept="application/wasm,application/json"
|
||||
@@ -153,17 +155,7 @@
|
||||
/>
|
||||
<label for="drop-zone"></label>
|
||||
|
||||
{#if graph.isInsideGroup}
|
||||
<button
|
||||
class="exit-group flex items-center gap-1 p-1 text-sm px-2 bg-layer-2"
|
||||
onclick={() => graphState.exitGroupNode()}
|
||||
>
|
||||
<span class="i-[tabler--arrow-left]"></span>Exit Group
|
||||
</button>
|
||||
<p class="group-name absolute">
|
||||
Group <b>{getGroupName()}</b>
|
||||
</p>
|
||||
{/if}
|
||||
<GroupBreadcrumps />
|
||||
|
||||
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
|
||||
<Camera
|
||||
@@ -273,30 +265,6 @@
|
||||
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;
|
||||
left: 12px;
|
||||
z-index: 10;
|
||||
border: 1px solid var(--stroke);
|
||||
border-radius: 4px;
|
||||
cursor: pointer;
|
||||
opacity: 0.85;
|
||||
}
|
||||
|
||||
.exit-group:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.wrapper {
|
||||
position: absolute;
|
||||
z-index: 100;
|
||||
@@ -308,22 +276,6 @@
|
||||
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,4 +1,10 @@
|
||||
import type { NodeDefinition } from '@nodarium/types';
|
||||
import type {
|
||||
Edge,
|
||||
NodeDefinition,
|
||||
NodeInstance,
|
||||
SerializedEdge,
|
||||
SerializedNode
|
||||
} from '@nodarium/types';
|
||||
|
||||
export function getParameterHeight(node: NodeDefinition, inputKey: string) {
|
||||
if (node.id === '__internal/group/input') {
|
||||
@@ -27,6 +33,19 @@ export function getParameterHeight(node: NodeDefinition, inputKey: string) {
|
||||
return 50;
|
||||
}
|
||||
|
||||
export function serializeNode(node: SerializedNode | NodeInstance): SerializedNode {
|
||||
return {
|
||||
id: node.id,
|
||||
position: [...node.position],
|
||||
type: node.type,
|
||||
props: node.props
|
||||
};
|
||||
}
|
||||
|
||||
export function serializeEdge(edge: Edge): SerializedEdge {
|
||||
return [edge[0].id, edge[1], edge[2].id, edge[3]];
|
||||
}
|
||||
|
||||
const nodeHeightCache: Record<string, number> = {};
|
||||
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;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user