Autogenerate layout from graph

This commit is contained in:
space-nuko
2023-04-29 11:42:16 -07:00
parent 52fb1d7001
commit f62fa69122
5 changed files with 290 additions and 112 deletions

View File

@@ -10,9 +10,6 @@
export let app: ComfyApp; export let app: ComfyApp;
let dragConfigured: boolean = false; let dragConfigured: boolean = false;
export let dragItems: DragItem[][] = []
export let totalId = 0;
// //
// function addUIForNewNode(node: LGraphNode, paneIndex?: number) { // function addUIForNewNode(node: LGraphNode, paneIndex?: number) {
// if (!paneIndex) // if (!paneIndex)
@@ -32,48 +29,32 @@
} }
export function restore(panels: SerializedPanes) { export function restore(panels: SerializedPanes) {
const a: ContainerLayout = { const id = 0;
type: "container", $layoutState.root = layoutState.addContainer(null, { direction: "horizontal", showTitle: false });
id: "0", const left = layoutState.addContainer($layoutState.root.id, { direction: "vertical", showTitle: false });
attrs: { const right = layoutState.addContainer($layoutState.root.id, { direction: "vertical", showTitle: false });
title: "root!",
direction: "horizontal" for (const node of app.lGraph.computeExecutionOrder(false, null)) {
} layoutState.nodeAdded(node)
} }
$layoutState.root = a; console.warn($layoutState)
$layoutState.children[0] = [
{
type: "widget",
id: "1",
nodeId: 7,
widgetName: "text",
attrs: {
}
},
{
type: "widget",
id: "2",
nodeId: 6,
widgetName: "text",
attrs: {
}
},
]
$layoutState.children[1] = []
$layoutState.children[2] = []
} }
</script> </script>
<div id="comfy-ui-panes" > <div id="comfy-ui-panes" >
<WidgetContainer bind:dragItem={$layoutState.root} /> <WidgetContainer bind:dragItem={$layoutState.root} classes={["root-container"]} />
</div> </div>
<style> <style lang="scss">
#comfy-ui-panes { #comfy-ui-panes {
width: 100%; width: 100%;
height: 100%; height: 100%;
overflow: scroll; overflow: scroll;
} }
#comfy-ui-panes > :global(.root-container > .block) {
padding: 0px;
}
</style> </style>

View File

@@ -1,8 +1,5 @@
<script lang="ts"> <script lang="ts">
import { onDestroy } from "svelte";
import { get } from "svelte/store"
import { Block, BlockTitle } from "@gradio/atoms"; import { Block, BlockTitle } from "@gradio/atoms";
import { Move } from 'radix-icons-svelte';
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import nodeState, { type WidgetUIState } from "$lib/stores/nodeState"; import nodeState, { type WidgetUIState } from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
@@ -13,14 +10,15 @@
// notice - fade in works fine but don't add svelte's fade-out (known issue) // notice - fade in works fine but don't add svelte's fade-out (known issue)
import {cubicIn} from 'svelte/easing'; import {cubicIn} from 'svelte/easing';
import { flip } from 'svelte/animate'; import { flip } from 'svelte/animate';
import ComfyApp from "./ComfyApp";
import type { LGraphNode } from "@litegraph-ts/core";
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { getComponentForWidgetState } from "$lib/utils" import { getComponentForWidgetState } from "$lib/utils"
export let dragItem: DragItem | null = null; export let dragItem: IDragItem | null = null;
export let zIndex: number = 100;
export let classes: string[] = [];
let container: ContainerLayout | null = null; let container: ContainerLayout | null = null;
let widget: WidgetUIState | null = null; let widget: WidgetLayout | null = null;
let widgetState: WidgetUIState | null = null;
let children: IDragItem[] | null = null; let children: IDragItem[] | null = null;
let dragDisabled = true; let dragDisabled = true;
const flipDurationMs = 200; const flipDurationMs = 200;
@@ -28,12 +26,12 @@
$: if (dragItem) { $: if (dragItem) {
if (dragItem.type === "container") { if (dragItem.type === "container") {
container = dragItem as ContainerLayout; container = dragItem as ContainerLayout;
children = $layoutState.children[dragItem.id]; children = $layoutState.allItems[dragItem.id].children;
widget = null; widget = null;
} }
else if (dragItem.type === "widget") { else if (dragItem.type === "widget") {
const widgetLayout = dragItem as WidgetLayout; widget = dragItem as WidgetLayout;
widget = nodeState.findWidgetByName(widgetLayout.nodeId, widgetLayout.widgetName) widgetState = nodeState.findWidgetByName(widget.nodeId, widget.widgetName)
children = null; children = null;
container = null; container = null;
} }
@@ -41,14 +39,13 @@
$: dragDisabled = !$uiState.unlocked; $: dragDisabled = !$uiState.unlocked;
const handleConsider = evt => { function handleConsider(evt: any) {
$layoutState.children[dragItem.id] = evt.detail.items; children = layoutState.updateChildren(dragItem, evt.detail.items)
children = $layoutState.children[dragItem.id];
// console.log(dragItems); // console.log(dragItems);
}; };
const handleFinalize = evt => {
$layoutState.children[dragItem.id] = evt.detail.items; function handleFinalize(evt: any) {
children = $layoutState.children[dragItem.id]; children = layoutState.updateChildren(dragItem, evt.detail.items)
// Ensure dragging is stopped on drag finish // Ensure dragging is stopped on drag finish
// dragDisabled = true; // dragDisabled = true;
}; };
@@ -64,66 +61,106 @@
// dragDisabled = true; // dragDisabled = true;
}; };
const unsubscribe = nodeState.subscribe(state => {
if (container) {
$layoutState.children[container.id] = $layoutState.children[container.id].filter(item => item.node.id in state);
children = $layoutState.children[container.id];
}
});
onDestroy(unsubscribe);
$: if ($queueState && widget) { $: if ($queueState && widget) {
widget.isNodeExecuting = $queueState.runningNodeId === widget.nodeId; widget.isNodeExecuting = $queueState.runningNodeId === widget.nodeId;
children = $layoutState.children[widget.nodeId]; children = $layoutState.allItems[widget.id].children;
}
function updateNodeName(node: LGraphNode, value: string) {
nodeState.nodeStateChanged(node);
} }
</script> </script>
{#if container} {#if container && children}
{@const node = container.node}
{@const id = container.id} {@const id = container.id}
<Block> <div class="container {container.attrs.direction} {container.attrs.classes} {classes.join(' ')}">
<label for={String(id)} class={$uiState.unlocked ? "edit-title-label" : ""}> <Block>
<BlockTitle> {#if container.attrs.showTitle}
{#if $uiState.unlocked} <label for={String(id)} class={$uiState.unlocked ? "edit-title-label" : ""}>
<input class="edit-title" bind:value={container.attrs.title} type="text" minlength="1" on:input="{(v) => { updateNodeName(node, v) }}"/> <BlockTitle>
{:else} {#if $uiState.unlocked}
{container.attrs.title} <input class="edit-title" bind:value={container.attrs.title} type="text" minlength="1" />
{/if} {:else}
</BlockTitle> {container.attrs.title}
</label> {/if}
<div class="v-pane" </BlockTitle>
use:dndzone="{{ items: children, dragDisabled, flipDurationMs }}" </label>
on:consider="{handleConsider}" {/if}
on:finalize="{handleFinalize}" <div class="v-pane"
> class:empty={children.length === 0}
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)} use:dndzone="{{ items: children, dragDisabled, flipDurationMs }}"
<div class="animation-wrapper" class:is-executing={item.isNodeExecuting} animate:flip={{duration:flipDurationMs}}> on:consider="{handleConsider}"
<svelte:self dragItem={item}/> on:finalize="{handleFinalize}"
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]} >
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/> {#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
{/if} <div class="animation-wrapper" class:is-executing={item.isNodeExecuting} animate:flip={{duration:flipDurationMs}}>
</div> <svelte:self dragItem={item} zIndex={zIndex+1} />
{/each} {#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
</div> <div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
</Block> {/if}
</div>
{/each}
</div>
{#if $uiState.unlocked}
<div class="handle handle-container" style="z-index: {zIndex}" on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if}
</Block>
</div>
{:else if widget} {:else if widget}
<svelte:component this={getComponentForWidgetState(widget)} item={widget} /> <svelte:component this={getComponentForWidgetState(widgetState)} item={widgetState} />
{#if $uiState.unlocked} {#if $uiState.unlocked}
<div class="handle" on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-widget" style="z-index: {zIndex}" on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
{/if} {/if}
{/if} {/if}
<style> <style lang="scss">
.v-pane { .v-pane {
height: 100%; height: 100%;
width: 100%; width: 100%;
overflow-y: auto ; overflow: visible;
display: flex;
&.empty {
border-width: 3px;
border-color: var(--color-grey-400);
border-radius: var(--block-radius);
background: var(--color-grey-300);
min-height: 50px;
border-style: dashed;
}
}
.container {
display: flex;
:global(.block) {
height: fit-content;
}
&.horizontal {
flex-wrap: wrap;
gap: var(--layout-gap);
width: var(--size-full);
.v-pane {
flex-direction: row;
}
> :global(*), > :global(.form > *) {
flex: 1 1 0%;
flex-wrap: wrap;
min-width: min(160px, 100%);
}
}
&.vertical {
position: relative;
.v-pane {
flex-direction: column;
}
> :global(*), > :global(.form > *), .v-pane {
width: var(--size-full);
}
}
} }
.is-executing :global(.block) { .is-executing :global(.block) {
@@ -132,8 +169,7 @@
.animation-wrapper { .animation-wrapper {
position: relative; position: relative;
width: 100%; flex-grow: 1;
height: 100%;
} }
.handle { .handle {
@@ -146,10 +182,14 @@
height: 100%; height: 100%;
} }
.handle:hover { .handle-widget:hover {
background-color: #add8e680; background-color: #add8e680;
} }
.handle-container:hover {
background-color: #d8ade680;
}
.drag-item-shadow { .drag-item-shadow {
position: absolute; position: absolute;
top: 0; left:0; right: 0; bottom: 0; top: 0; left:0; right: 0; bottom: 0;

View File

@@ -2,21 +2,34 @@ import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store'; import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp" import type ComfyApp from "$lib/components/ComfyApp"
import type { LGraphNode, IWidget } from "@litegraph-ts/core" import type { LGraphNode, IWidget } from "@litegraph-ts/core"
import nodeState from "$lib/state/nodeState";
import type { NodeStateStore } from './nodeState';
type DragItemEntry = {
dragItem: IDragItem,
children: IDragItem[] | null,
parent: IDragItem | null
}
export type LayoutState = { export type LayoutState = {
root: IDragItem | null, root: IDragItem | null,
children: Record<number, IDragItem[]>, allItems: Record<DragItemID, DragItemEntry>,
currentId: number,
} }
export type Properties = { export type Attributes = {
direction: string, direction: string,
title: string,
showTitle: boolean,
classes: string,
associatedNode: number | null
} }
export interface IDragItem { export interface IDragItem {
type: string, type: string,
id: number, id: DragItemID,
isNodeExecuting?: boolean, isNodeExecuting?: boolean,
properties: Properties attrs: Attributes
} }
export interface ContainerLayout extends IDragItem { export interface ContainerLayout extends IDragItem {
@@ -29,37 +42,181 @@ export interface WidgetLayout extends IDragItem {
widgetName: string widgetName: string
} }
type DragItemID = string;
type LayoutStateOps = { type LayoutStateOps = {
addContainer: (parentId: DragItemID, attrs: Partial<Attributes>) => ContainerLayout,
findDefaultContainerForInsertion: () => ContainerLayout | null, findDefaultContainerForInsertion: () => ContainerLayout | null,
reset: () => void, addWidget: (parentId: DragItemID, node: LGraphNode, widget: IWidget<any, any>, attrs: Partial<Attributes>) => WidgetLayout,
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode) => void,
nodeRemoved: (node: LGraphNode) => void,
clear: () => void,
resetLayout: () => void,
} }
export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps; export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps;
const store: Writable<LayoutState> = writable({ const store: Writable<LayoutState> = writable({
root: null, root: null,
children: [] allItems: [],
currentId: 0
}) })
function findDefaultContainerForInsertion(): ContainerLayout | null { function findDefaultContainerForInsertion(): ContainerLayout | null {
const state = get(store); const state = get(store);
if ("children" in state.root) {
if (state.root === null) {
// Should never happen
throw "Root container was null!";
}
if (state.root.type === "container") {
const container = state.root as ContainerLayout; const container = state.root as ContainerLayout;
const found = state.children[container.id].find((di) => {"children" in di}) const children: IDragItem[] = state.allItems[container.id]?.children || []
if (found && "children" in found) const found = children.find((di) => di.type === "container" )
if (found && found.type === "container")
return found as ContainerLayout; return found as ContainerLayout;
return container; return container;
} }
return null return null
} }
function reset() { function addContainer(parentId: DragItemID | null, attrs: Partial<Attributes> = {}): ContainerLayout {
const state = get(store);
const dragItem: ContainerLayout = {
type: "container",
id: `${state.currentId++}`,
attrs: {
title: "Container",
showTitle: true,
direction: "vertical",
classes: "",
associatedNode: null,
...attrs
}
}
const parent = parentId ? state.allItems[parentId] : null;
const entry: DragItemEntry = { dragItem, children: [], parent: parent?.dragItem };
state.allItems[dragItem.id] = entry;
if (parent) {
parent.children ||= []
parent.children.push(dragItem)
}
store.set(state)
return dragItem;
}
function addWidget(parentId: DragItemID, node: LGraphNode, widget: IWidget<any, any>, attrs: Partial<Attributes> = {}): WidgetLayout {
const state = get(store);
const dragItem: WidgetLayout = {
type: "widget",
id: `${state.currentId++}`,
nodeId: node.id,
widgetName: widget.name, // TODO name and displayName
attrs: {
title: widget.name,
showTitle: true,
direction: "horizontal",
classes: "",
associatedNode: null,
...attrs
}
}
const parent = state.allItems[parentId]
const entry: DragItemEntry = { dragItem, children: [], parent: parent.dragItem };
state.allItems[dragItem.id] = entry;
parent.children ||= []
parent.children.push(dragItem)
store.set(state)
return dragItem;
}
function updateChildren(parent: IDragItem, children: IDragItem[]): IDragItem[] {
const state = get(store);
state.allItems[parent.id].children = children;
for (const child of children) {
state.allItems[child.id].parent = parent;
}
store.set(state)
return children
}
function nodeAdded(node: LGraphNode) {
const parent = findDefaultContainerForInsertion();
// Add default node panel containing all widgets
if (node.widgets && node.widgets.length > 0) {
const container = addContainer(parent.id, { title: node.title, direction: "vertical", associatedNode: node.id });
for (const widget of node.widgets) {
addWidget(container.id, node, widget, { associatedNode: node.id });
}
}
}
function removeEntry(state: LayoutState, id: DragItemID) {
const entry = state.allItems[id]
if (entry.children && entry.children.length > 0) {
console.error(entry)
throw `Tried removing entry ${id} but it still had children!`
}
const parent = entry.parent;
if (parent) {
const parentEntry = state.allItems[parent.id];
parentEntry.children = parentEntry.children.filter(item => item.id !== id)
}
delete state.allItems[id]
}
function nodeRemoved(node: LGraphNode) {
const state = get(store)
// Remove widgets bound to the node
let del = Object.entries(state.allItems).filter(pair =>
pair[1].dragItem.type === "widget"
&& pair[1].dragItem.attrs.associatedNode === node.id)
for (const id in del) {
console.debug("[layoutState] Remove widget", id, state.allItems[id])
removeEntry(state, id)
}
const isAssociatedContainer = (dragItem: IDragItem) =>
dragItem.type === "container"
&& dragItem.attrs.associatedNode === node.id;
let delContainers = []
// Remove widget from all children lists
for (const entry of Object.values(state.allItems)) {
if (entry.children?.length === 0 && isAssociatedContainer(entry.dragItem))
delContainers.push(entry.dragItem.id)
}
// Remove empty containers bound to the node
for (const id in delContainers) {
console.debug("[layoutState] Remove container", id, state.allItems[id])
removeEntry(state, id)
}
store.set(state)
}
function clear() {
}
function resetLayout() {
// TODO // TODO
} }
const uiStateStore: WritableLayoutStateStore = const uiStateStore: WritableLayoutStateStore =
{ {
...store, ...store,
addContainer,
addWidget,
findDefaultContainerForInsertion, findDefaultContainerForInsertion,
reset updateChildren,
nodeAdded,
nodeRemoved,
clear,
resetLayout
} }
export default uiStateStore; export default uiStateStore;

View File

@@ -14,7 +14,7 @@ const store: WritableUIStateStore = writable(
{ {
graphLocked: true, graphLocked: true,
nodesLocked: false, nodesLocked: false,
unlocked: true, unlocked: false,
}) })
const uiStateStore: WritableUIStateStore = const uiStateStore: WritableUIStateStore =

View File

@@ -45,6 +45,6 @@
} }
:global(.svelte-select-list) { :global(.svelte-select-list) {
z-index: var(--layer-5) !important; z-index: var(--layer-top) !important;
} }
</style> </style>