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'>();
|
status = $state<'loading' | 'idle' | 'error'>();
|
||||||
loaded = false;
|
loaded = false;
|
||||||
|
|
||||||
graph: Graph = { id: 0, nodes: [], edges: [], groups: [] };
|
graph: Graph = $state({ id: 0, nodes: [], edges: [], groups: [] });
|
||||||
id = $state(0);
|
id = $state(0);
|
||||||
|
|
||||||
nodes = new SvelteMap<number, NodeInstance>();
|
nodes = new SvelteMap<number, NodeInstance>();
|
||||||
@@ -495,9 +495,24 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const groupId = this.graphStack.at(-1)?.groupId;
|
const groupId = this.graphStack.at(-1)?.groupId;
|
||||||
const group = groupId !== undefined ? this.getGroup(groupId) : undefined;
|
const group = groupId !== undefined ? this.getGroup(groupId) : undefined;
|
||||||
if (!group) return node.state.type;
|
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 {
|
return {
|
||||||
id: '__internal/group/input' as NodeId,
|
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
|
execute: (x: Int32Array) => x
|
||||||
} as NodeDefinition;
|
} as NodeDefinition;
|
||||||
}
|
}
|
||||||
@@ -514,6 +529,9 @@ export class GraphManager extends EventEmitter<{
|
|||||||
i
|
i
|
||||||
) => [`out_${i}`, { type: o.type, label: o.label, external: true }])
|
) => [`out_${i}`, { type: o.type, label: o.label, external: true }])
|
||||||
),
|
),
|
||||||
|
meta: {
|
||||||
|
title: 'Group Output'
|
||||||
|
},
|
||||||
outputs: [],
|
outputs: [],
|
||||||
execute: (x: Int32Array) => x
|
execute: (x: Int32Array) => x
|
||||||
} as NodeDefinition;
|
} as NodeDefinition;
|
||||||
@@ -1007,7 +1025,9 @@ export class GraphManager extends EventEmitter<{
|
|||||||
const toType = this.getNodeType(to);
|
const toType = this.getNodeType(to);
|
||||||
|
|
||||||
// check if socket types match
|
// 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];
|
const toSocketType = [toType?.inputs?.[toSocket]?.type];
|
||||||
if (toType?.inputs?.[toSocket]?.accepts) {
|
if (toType?.inputs?.[toSocket]?.accepts) {
|
||||||
toSocketType.push(...(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) {
|
for (const node of nodes) {
|
||||||
const inputs = this.getNodeType(node)?.inputs;
|
const inputs = this.getNodeType(node)?.inputs;
|
||||||
|
|||||||
@@ -384,6 +384,13 @@ export class GraphState {
|
|||||||
node: NodeInstance,
|
node: NodeInstance,
|
||||||
index: string | number
|
index: string | number
|
||||||
): [number, 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') {
|
if (typeof index === 'number') {
|
||||||
return [
|
return [
|
||||||
(node?.state?.x ?? node.position[0]) + 20,
|
(node?.state?.x ?? node.position[0]) + 20,
|
||||||
|
|||||||
@@ -100,8 +100,22 @@
|
|||||||
if (typeof index === 'string') {
|
if (typeof index === 'string') {
|
||||||
return nodeType?.inputs?.[index].type || 'unknown';
|
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';
|
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>
|
</script>
|
||||||
|
|
||||||
<svelte:window
|
<svelte:window
|
||||||
@@ -114,6 +128,7 @@
|
|||||||
bind:this={graphState.wrapper}
|
bind:this={graphState.wrapper}
|
||||||
class="graph-wrapper"
|
class="graph-wrapper"
|
||||||
style="height: 100%"
|
style="height: 100%"
|
||||||
|
class:is-inside-group={graph.isInsideGroup}
|
||||||
class:is-panning={graphState.isPanning}
|
class:is-panning={graphState.isPanning}
|
||||||
class:is-hovering={graphState.hoveredNodeId !== -1}
|
class:is-hovering={graphState.hoveredNodeId !== -1}
|
||||||
aria-label="Graph"
|
aria-label="Graph"
|
||||||
@@ -121,11 +136,13 @@
|
|||||||
tabindex="0"
|
tabindex="0"
|
||||||
bind:clientWidth={graphState.width}
|
bind:clientWidth={graphState.width}
|
||||||
bind:clientHeight={graphState.height}
|
bind:clientHeight={graphState.height}
|
||||||
|
style:--padding-right="{safePadding?.right || 0}px"
|
||||||
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
onkeydown={(ev) => keymap.handleKeyboardEvent(ev)}
|
||||||
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
onmousedown={(ev) => mouseEvents.handleMouseDown(ev)}
|
||||||
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
|
oncontextmenu={(ev) => mouseEvents.handleContextMenu(ev)}
|
||||||
{...fileDropEvents.getEventListenerProps()}
|
{...fileDropEvents.getEventListenerProps()}
|
||||||
>
|
>
|
||||||
|
<div class="shadow"></div>
|
||||||
<input
|
<input
|
||||||
type="file"
|
type="file"
|
||||||
accept="application/wasm,application/json"
|
accept="application/wasm,application/json"
|
||||||
@@ -140,6 +157,9 @@
|
|||||||
<button class="exit-group" onclick={() => graphState.exitGroupNode()}>
|
<button class="exit-group" onclick={() => graphState.exitGroupNode()}>
|
||||||
↑ Exit Group
|
↑ Exit Group
|
||||||
</button>
|
</button>
|
||||||
|
<p class="group-name absolute">
|
||||||
|
Group <b>{getGroupName()}</b>
|
||||||
|
</p>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
|
<Canvas shadows={false} renderMode="on-demand" colorManagementEnabled={false}>
|
||||||
@@ -250,6 +270,15 @@
|
|||||||
height: 100%;
|
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 {
|
.exit-group {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
top: 12px;
|
top: 12px;
|
||||||
@@ -280,6 +309,22 @@
|
|||||||
cursor: pointer;
|
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 {
|
.is-panning {
|
||||||
cursor: grab;
|
cursor: grab;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
import type { NodeDefinition } from '@nodarium/types';
|
import type { NodeDefinition } from '@nodarium/types';
|
||||||
|
|
||||||
export function getParameterHeight(node: NodeDefinition, inputKey: string) {
|
export function getParameterHeight(node: NodeDefinition, inputKey: string) {
|
||||||
|
if (node.id === '__internal/group/input') {
|
||||||
|
return 50;
|
||||||
|
}
|
||||||
|
|
||||||
const input = node.inputs?.[inputKey];
|
const input = node.inputs?.[inputKey];
|
||||||
if (!input) {
|
if (!input) {
|
||||||
return 0;
|
return 0;
|
||||||
|
|||||||
@@ -23,7 +23,10 @@
|
|||||||
|
|
||||||
const cornerTop = 10;
|
const cornerTop = 10;
|
||||||
const nodeType = $derived(graph.getNodeType(node));
|
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 aspectRatio = 0.25;
|
||||||
|
|
||||||
const path = $derived(
|
const path = $derived(
|
||||||
@@ -73,6 +76,7 @@
|
|||||||
{/if}
|
{/if}
|
||||||
{nodeType?.meta?.title || node.type?.split('/').pop()}
|
{nodeType?.meta?.title || node.type?.split('/').pop()}
|
||||||
</div>
|
</div>
|
||||||
|
{#if rightBump}
|
||||||
<div
|
<div
|
||||||
class="target"
|
class="target"
|
||||||
role="button"
|
role="button"
|
||||||
@@ -80,6 +84,7 @@
|
|||||||
onmousedown={handleMouseDown}
|
onmousedown={handleMouseDown}
|
||||||
>
|
>
|
||||||
</div>
|
</div>
|
||||||
|
{/if}
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 100 100"
|
viewBox="0 0 100 100"
|
||||||
|
|||||||
@@ -29,14 +29,27 @@
|
|||||||
function handleMouseDown(ev: MouseEvent) {
|
function handleMouseDown(ev: MouseEvent) {
|
||||||
ev.preventDefault();
|
ev.preventDefault();
|
||||||
ev.stopPropagation();
|
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({
|
graphState.setDownSocket({
|
||||||
node,
|
node,
|
||||||
index: id,
|
index: id,
|
||||||
position: graphState.getSocketPosition(node, 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 cornerBottom = $derived(isLast ? 5 : 0);
|
||||||
const aspectRatio = 0.5;
|
const aspectRatio = 0.5;
|
||||||
|
|
||||||
@@ -46,6 +59,7 @@
|
|||||||
height: 2000 / height,
|
height: 2000 / height,
|
||||||
y: 50.5,
|
y: 50.5,
|
||||||
cornerBottom,
|
cornerBottom,
|
||||||
|
rightBump,
|
||||||
leftBump,
|
leftBump,
|
||||||
aspectRatio
|
aspectRatio
|
||||||
})
|
})
|
||||||
@@ -55,6 +69,7 @@
|
|||||||
depth: 7,
|
depth: 7,
|
||||||
height: 2200 / height,
|
height: 2200 / height,
|
||||||
y: 50.5,
|
y: 50.5,
|
||||||
|
rightBump,
|
||||||
cornerBottom,
|
cornerBottom,
|
||||||
leftBump,
|
leftBump,
|
||||||
aspectRatio
|
aspectRatio
|
||||||
@@ -76,6 +91,7 @@
|
|||||||
<div
|
<div
|
||||||
class="wrapper"
|
class="wrapper"
|
||||||
data-node-type={node.type}
|
data-node-type={node.type}
|
||||||
|
class:is-group-input={node.type === '__internal/group/input'}
|
||||||
data-node-input={id}
|
data-node-input={id}
|
||||||
style:height="{height}px"
|
style:height="{height}px"
|
||||||
style:--socket-color={hoverColor}
|
style:--socket-color={hoverColor}
|
||||||
@@ -130,6 +146,11 @@
|
|||||||
transform: translateY(-50%) translateX(-50%);
|
transform: translateY(-50%) translateX(-50%);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.is-group-input .target {
|
||||||
|
right: 0px;
|
||||||
|
transform: translateY(-50%) translateX(50%);
|
||||||
|
}
|
||||||
|
|
||||||
.possible-socket .target::before {
|
.possible-socket .target::before {
|
||||||
content: "";
|
content: "";
|
||||||
position: absolute;
|
position: absolute;
|
||||||
|
|||||||
@@ -1,5 +1,8 @@
|
|||||||
export const debugNode = {
|
export const debugNode = {
|
||||||
id: '__internal/debug/instance',
|
id: '__internal/debug/instance',
|
||||||
|
meta: {
|
||||||
|
title: 'Debug'
|
||||||
|
},
|
||||||
inputs: {
|
inputs: {
|
||||||
input: {
|
input: {
|
||||||
type: '*'
|
type: '*'
|
||||||
|
|||||||
@@ -96,6 +96,4 @@
|
|||||||
bind:value={store}
|
bind:value={store}
|
||||||
type={nodeDefinition}
|
type={nodeDefinition}
|
||||||
/>
|
/>
|
||||||
{:else}
|
|
||||||
<p class="mx-4 mt-4">Node has no settings</p>
|
|
||||||
{/if}
|
{/if}
|
||||||
|
|||||||
@@ -11,97 +11,11 @@
|
|||||||
let { manager, node = $bindable() }: Props = $props();
|
let { manager, node = $bindable() }: Props = $props();
|
||||||
|
|
||||||
const isGroupInstance = $derived(node?.type === '__internal/group/instance');
|
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>
|
</script>
|
||||||
|
|
||||||
<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'>
|
{#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>
|
<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>
|
</div>
|
||||||
{:else if node}
|
|
||||||
<ActiveNodeSelected {manager} bind:node />
|
<ActiveNodeSelected {manager} bind:node />
|
||||||
{/if}
|
|
||||||
{/key}
|
|
||||||
{:else}
|
|
||||||
<p class="mx-4 mt-4">No node selected</p>
|
|
||||||
{/if}
|
{/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 Changelog from '$lib/sidebar/panels/Changelog.svelte';
|
||||||
import ExportSettings from '$lib/sidebar/panels/ExportSettings.svelte';
|
import ExportSettings from '$lib/sidebar/panels/ExportSettings.svelte';
|
||||||
import GraphSource from '$lib/sidebar/panels/GraphSource.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 Keymap from '$lib/sidebar/panels/Keymap.svelte';
|
||||||
import { panelState } from '$lib/sidebar/PanelState.svelte';
|
import { panelState } from '$lib/sidebar/PanelState.svelte';
|
||||||
import Sidebar from '$lib/sidebar/Sidebar.svelte';
|
import Sidebar from '$lib/sidebar/Sidebar.svelte';
|
||||||
@@ -258,7 +259,7 @@
|
|||||||
graph={pm.graph}
|
graph={pm.graph}
|
||||||
bind:this={graphInterface}
|
bind:this={graphInterface}
|
||||||
registry={nodeRegistry}
|
registry={nodeRegistry}
|
||||||
safePadding={{ right: sidebarOpen ? 330 : undefined }}
|
safePadding={{ right: sidebarOpen ? 320 : undefined }}
|
||||||
backgroundType={appSettings.value.nodeInterface.backgroundType}
|
backgroundType={appSettings.value.nodeInterface.backgroundType}
|
||||||
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
|
snapToGrid={appSettings.value.nodeInterface.snapToGrid}
|
||||||
bind:activeNode
|
bind:activeNode
|
||||||
@@ -336,12 +337,14 @@
|
|||||||
title="Graph Settings"
|
title="Graph Settings"
|
||||||
icon="i-[custom--graph] bg-blue-400"
|
icon="i-[custom--graph] bg-blue-400"
|
||||||
>
|
>
|
||||||
|
<span class="block h-[1px]"></span>
|
||||||
<NestedSettings
|
<NestedSettings
|
||||||
id="graph-settings"
|
id="graph-settings"
|
||||||
type={graphSettingTypes}
|
type={graphSettingTypes}
|
||||||
bind:value={graphSettings}
|
bind:value={graphSettings}
|
||||||
/>
|
/>
|
||||||
<ActiveNodeSettings {manager} bind:node={activeNode} />
|
<ActiveNodeSettings {manager} bind:node={activeNode} />
|
||||||
|
<GroupSettings {manager} bind:node={activeNode} />
|
||||||
</Panel>
|
</Panel>
|
||||||
<Panel
|
<Panel
|
||||||
id="changelog"
|
id="changelog"
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
<script module lang="ts">
|
<script module lang="ts">
|
||||||
import { SvelteMap } from 'svelte/reactivity';
|
// eslint-disable-next-line svelte/prefer-svelte-reactivity
|
||||||
const cache = new SvelteMap<string, Record<string, boolean>>();
|
const cache = new Map<string, Record<string, boolean>>();
|
||||||
|
|
||||||
function getStore(root: string): Record<string, boolean> {
|
function getStore(root: string): Record<string, boolean> {
|
||||||
if (!cache.has(root)) {
|
if (!cache.has(root)) {
|
||||||
|
|||||||
Reference in New Issue
Block a user