Bind widget values to special widget nodes

This commit is contained in:
space-nuko
2023-05-03 00:53:29 -07:00
parent 890c839b4d
commit 573970eac6
21 changed files with 89 additions and 725 deletions

View File

@@ -4,7 +4,6 @@
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form" import { Checkbox } from "@gradio/form"
import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils" import { download } from "$lib/utils"

View File

@@ -1,18 +1,13 @@
import type { LGraph } from "@litegraph-ts/core"; import type { LGraph, LGraphNode } from "@litegraph-ts/core";
import nodeState, { type WidgetUIState, type WidgetUIStateStore, type NodeStateStore, type NodeUIState, type NodeUIStateStore } from "./stores/nodeState";
import type ComfyApp from "./components/ComfyApp"; import type ComfyApp from "./components/ComfyApp";
import type { Unsubscriber } from "svelte/store"; import type { Unsubscriber, Writable } from "svelte/store";
import type { ComfyWidgetNode } from "./nodes";
type WidgetSubStore = { type WidgetSubStore = {
store: WidgetUIStateStore, store: WidgetUIStateStore,
unsubscribe: Unsubscriber unsubscribe: Unsubscriber
} }
type NodeSubStore = {
store: NodeUIStateStore,
unsubscribe: Unsubscriber
}
/* /*
* Responsible for watching for and synchronizing state changes from the * Responsible for watching for and synchronizing state changes from the
* frontend to the litegraph instance. * frontend to the litegraph instance.
@@ -29,68 +24,55 @@ type NodeSubStore = {
*/ */
export default class GraphSync { export default class GraphSync {
graph: LGraph; graph: LGraph;
private _unsubscribe: Unsubscriber;
private _finalizer: FinalizationRegistry<number>;
// nodeId -> widgetSubStores[] // nodeId -> widgetSubStore
private stores: Record<string, WidgetSubStore[]> = {} private stores: Record<string, WidgetSubStore> = {}
constructor(app: ComfyApp) { constructor(app: ComfyApp) {
this.graph = app.lGraph; this.graph = app.lGraph;
this._unsubscribe = nodeState.subscribe(this.onAllNodeStateChanged.bind(this)); app.eventBus.on("nodeAdded", this.onNodeAdded.bind(this));
this._finalizer = new FinalizationRegistry((id: number) => { app.eventBus.on("nodeRemoved", this.onNodeRemoved.bind(this));
console.log(`${this} has been garbage collected`);
this._unsubscribe();
});
} }
private onAllNodeStateChanged(state: NodeStateStore) { private onNodeAdded(node: LGraphNode) {
// TODO assumes only a single graph's widget state. // TODO assumes only a single graph's widget state.
for (let nodeId in state) { if ("svelteComponentType" in node) {
state[nodeId].node.title = state[nodeId].name; this.addStore(node as ComfyWidgetNode);
if (!this.stores[nodeId]) {
this.addStores(state[nodeId].widgetStates, nodeId);
}
}
for (let nodeId in this.stores) {
if (!state[nodeId]) {
this.removeStores(nodeId);
}
} }
this.graph.setDirtyCanvas(true, true); this.graph.setDirtyCanvas(true, true);
} }
private addStores(widgetStates: WidgetUIState[], nodeId: string) { private onNodeRemoved(node: LGraphNode) {
if (this.stores[nodeId]) { if ("svelteComponentType" in node) {
console.warn("Stores already exist!", nodeId, this.stores[nodeId]) this.removeStore(node as ComfyWidgetNode);
} }
this.stores[nodeId] = [] this.graph.setDirtyCanvas(true, true);
for (const wuis of widgetStates) {
const unsub = wuis.value.subscribe((v) => this.onWidgetStateChanged(wuis, v))
this.stores[nodeId].push({ store: wuis.value, unsubscribe: unsub });
}
console.debug("NEWSTORES", this.stores[nodeId])
} }
private removeStores(nodeId: string) { private addStore(node: ComfyWidgetNode) {
console.debug("DELSTORES", this.stores[nodeId]) if (this.stores[node.id]) {
for (const ss of this.stores[nodeId]) { console.warn("[GraphSync] Stores already exist!", node.id, this.stores[node.id])
ss.unsubscribe();
} }
delete this.stores[nodeId]
const unsub = node.value.subscribe((v) => this.onWidgetStateChanged(node, v))
this.stores[node.id] = ({ store: node.value, unsubscribe: unsub });
console.debug("[GraphSync] NEWSTORE", this.stores[node.id])
}
private removeStore(node: ComfyWidgetNode) {
console.debug("[GraphSync] DELSTORE", this.stores[node.id])
this.stores[node.id].unsubscribe()
delete this.stores[node.id]
} }
/* /*
* Fired when a single widget's value changes. * Fired when a single widget's value changes.
*/ */
private onWidgetStateChanged(wuis: WidgetUIState, value: any) { private onWidgetStateChanged(node: ComfyWidgetNode, value: any) {
wuis.widget.value = value;
this.graph.setDirtyCanvas(true, true); this.graph.setDirtyCanvas(true, true);
} }
} }

View File

@@ -6,7 +6,6 @@
import ComfyUIPane from "./ComfyUIPane.svelte"; import ComfyUIPane from "./ComfyUIPane.svelte";
import ComfyApp, { type SerializedAppState } from "./ComfyApp"; import ComfyApp, { type SerializedAppState } from "./ComfyApp";
import { Checkbox } from "@gradio/form" import { Checkbox } from "@gradio/form"
import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState"; import layoutState from "$lib/stores/layoutState";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
@@ -105,11 +104,6 @@
onMount(async () => { onMount(async () => {
app = new ComfyApp(); app = new ComfyApp();
app.eventBus.on("nodeAdded", nodeState.nodeAdded);
app.eventBus.on("nodeRemoved", nodeState.nodeRemoved);
app.eventBus.on("configured", nodeState.configureFinished);
app.eventBus.on("cleared", nodeState.clear);
app.eventBus.on("nodeAdded", layoutState.nodeAdded); app.eventBus.on("nodeAdded", layoutState.nodeAdded);
app.eventBus.on("nodeRemoved", layoutState.nodeRemoved); app.eventBus.on("nodeRemoved", layoutState.nodeRemoved);
app.eventBus.on("configured", layoutState.configureFinished); app.eventBus.on("configured", layoutState.configureFinished);

View File

@@ -433,7 +433,7 @@ export default class ComfyApp {
this.clean(); this.clean();
if (!graphData) { if (!graphData) {
// graphData = structuredClone(defaultGraph.workflow) graphData = structuredClone(defaultGraph.workflow)
} }
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now // Patch T2IAdapterLoader to ControlNetLoader since they are the same node now

View File

@@ -6,7 +6,6 @@
import ComfyApp from "./ComfyApp"; import ComfyApp from "./ComfyApp";
import type { SerializedPanes } from "./ComfyApp" import type { SerializedPanes } from "./ComfyApp"
import WidgetContainer from "./WidgetContainer.svelte"; import WidgetContainer from "./WidgetContainer.svelte";
import nodeState from "$lib/stores/nodeState";
import layoutState, { type ContainerLayout, type DragItem } from "$lib/stores/layoutState"; import layoutState, { type ContainerLayout, type DragItem } from "$lib/stores/layoutState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";

View File

@@ -1,10 +1,9 @@
<script lang="ts"> <script lang="ts">
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import nodeState, { type WidgetUIState } from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
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 { startDrag, stopDrag, getComponentForWidgetState } from "$lib/utils" import { startDrag, stopDrag } from "$lib/utils"
import BlockContainer from "./BlockContainer.svelte" import BlockContainer from "./BlockContainer.svelte"
export let dragItem: IDragItem | null = null; export let dragItem: IDragItem | null = null;
@@ -12,14 +11,12 @@
export let classes: string[] = []; export let classes: string[] = [];
let container: ContainerLayout | null = null; let container: ContainerLayout | null = null;
let widget: WidgetLayout | null = null; let widget: WidgetLayout | null = null;
let widgetState: WidgetUIState | null = null;
let showHandles: boolean = false; let showHandles: boolean = false;
$: if (!dragItem || !$layoutState.allItems[dragItem.id]) { $: if (!dragItem || !$layoutState.allItems[dragItem.id]) {
dragItem = null; dragItem = null;
container = null; container = null;
widget = null; widget = null;
widgetState = null;
} }
else if (dragItem.type === "container") { else if (dragItem.type === "container") {
container = dragItem as ContainerLayout; container = dragItem as ContainerLayout;
@@ -48,7 +45,7 @@
class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)} class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id} class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
> >
<svelte:component this={widget.node.svelteComponentType} node={widget.node} /> <svelte:component this={widget.node.svelteComponentType} {widget} />
</div> </div>
{#if showHandles} {#if showHandles}
<div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-widget" style="z-index: {zIndex+100}" data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>

View File

@@ -4,398 +4,16 @@ const defaultGraph: SerializedAppState = {
createdBy: "ComfyBox", createdBy: "ComfyBox",
version: 1, version: 1,
workflow: { workflow: {
last_node_id: 9, last_node_id: 0,
last_link_id: 9, last_link_id: 0,
nodes: [ nodes: [],
{ links: [],
id: 7,
type: "CLIPTextEncode",
pos: [
413,
389
],
size: [
425.27801513671875,
180.6060791015625
],
flags: {},
order: 3,
mode: 0,
inputs: [
{
name: "clip",
type: "CLIP",
link: 5
}
],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [
6
],
slot_index: 0
}
],
title: "CLIPTextEncode",
properties: {},
widgets_values: [
"bad hands"
]
},
{
id: 6,
type: "CLIPTextEncode",
pos: [
415,
186
],
size: [
422.84503173828125,
164.31304931640625
],
flags: {},
order: 2,
mode: 0,
inputs: [
{
name: "clip",
type: "CLIP",
link: 3
}
],
outputs: [
{
name: "CONDITIONING",
type: "CONDITIONING",
links: [
4
],
slot_index: 0
}
],
title: "CLIPTextEncode",
properties: {},
widgets_values: [
"masterpiece best quality girl"
]
},
{
id: 5,
type: "EmptyLatentImage",
pos: [
473,
609
],
size: [
315,
106
],
flags: {},
order: 0,
mode: 0,
inputs: [],
outputs: [
{
name: "LATENT",
type: "LATENT",
links: [
2
],
slot_index: 0
}
],
title: "EmptyLatentImage",
properties: {},
widgets_values: [
512,
512,
1
]
},
{
id: 3,
type: "KSampler",
pos: [
863,
186
],
size: [
315,
262
],
flags: {},
order: 4,
mode: 0,
inputs: [
{
name: "model",
type: "MODEL",
link: 1
},
{
name: "positive",
type: "CONDITIONING",
link: 4
},
{
name: "negative",
type: "CONDITIONING",
link: 6
},
{
name: "latent_image",
type: "LATENT",
link: 2
}
],
outputs: [
{
name: "LATENT",
type: "LATENT",
links: [
7
],
slot_index: 0
}
],
title: "KSampler",
properties: {},
widgets_values: [
8566257,
"randomize",
8,
8,
"euler",
"normal",
1
]
},
{
id: 8,
type: "VAEDecode",
pos: [
1209,
188
],
size: [
210,
46
],
flags: {},
order: 5,
mode: 0,
inputs: [
{
name: "samples",
type: "LATENT",
link: 7
},
{
name: "vae",
type: "VAE",
link: 8
}
],
outputs: [
{
name: "IMAGE",
type: "IMAGE",
links: [
9
],
slot_index: 0
}
],
title: "VAEDecode",
properties: {}
},
{
id: 9,
type: "SaveImage",
pos: [
1451,
189
],
size: [
210,
82
],
flags: {},
order: 6,
mode: 0,
inputs: [
{
name: "images",
type: "IMAGE",
link: 9
}
],
outputs: [],
title: "SaveImage",
properties: {},
widgets_values: [
[],
"ComfyUI"
]
},
{
id: 4,
type: "CheckpointLoaderSimple",
pos: [
26,
474
],
size: [
315,
98
],
flags: {},
order: 1,
mode: 0,
inputs: [],
outputs: [
{
name: "MODEL",
type: "MODEL",
links: [
1
],
slot_index: 0
},
{
name: "CLIP",
type: "CLIP",
links: [
3,
5
],
slot_index: 1
},
{
name: "VAE",
type: "VAE",
links: [
8
],
slot_index: 2
}
],
title: "CheckpointLoaderSimple",
properties: {},
widgets_values: [
"v1-5-pruned-emaonly.ckpt"
]
}
],
links: [
[
1,
4,
0,
3,
0,
"MODEL"
],
[
2,
5,
0,
3,
3,
"LATENT"
],
[
3,
4,
1,
6,
0,
"CLIP"
],
[
4,
6,
0,
3,
1,
"CONDITIONING"
],
[
5,
4,
1,
7,
0,
"CLIP"
],
[
6,
7,
0,
3,
2,
"CONDITIONING"
],
[
7,
3,
0,
8,
0,
"LATENT"
],
[
8,
4,
2,
8,
1,
"VAE"
],
[
9,
8,
0,
9,
0,
"IMAGE"
]
],
groups: [], groups: [],
config: {}, config: {},
extra: {}, extra: {},
version: 10 version: 0
}, },
panes: { panes: {}
panels: [
[
{
nodeId: 7
},
{
nodeId: 3
}
],
[
{
nodeId: 6
},
{
nodeId: 9
},
{
nodeId: 4
}
],
[
{
nodeId: 5
}
]
]
}
} }
export default defaultGraph; export default defaultGraph;

View File

@@ -17,7 +17,7 @@ export interface ComfyWidgetProperties extends Record<string, any> {
* widget is changed, the value of the first output in the node is updated * widget is changed, the value of the first output in the node is updated
* in the litegraph instance. * in the litegraph instance.
*/ */
export abstract class ComfyWidgetNode<T> extends ComfyGraphNode { export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
abstract properties: ComfyWidgetProperties; abstract properties: ComfyWidgetProperties;
value: Writable<T> value: Writable<T>
@@ -134,7 +134,7 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
this.setProperty("min", Math.max(this.properties.min, input.config.min)) this.setProperty("min", Math.max(this.properties.min, input.config.min))
this.setProperty("max", Math.max(this.properties.max, input.config.max)) this.setProperty("max", Math.max(this.properties.max, input.config.max))
this.setProperty("step", Math.max(this.properties.step, input.config.step)) this.setProperty("step", Math.max(this.properties.step, input.config.step))
this.setProperty("value", Math.max(Math.min(this.properties.value, this.properties.max), this.properties.min)) this.setValue(Math.max(Math.min(this.properties.value, this.properties.max), this.properties.min))
} }
} }
@@ -151,7 +151,7 @@ export interface ComfyComboProperties extends ComfyWidgetProperties {
export class ComfyComboNode extends ComfyWidgetNode<string> { export class ComfyComboNode extends ComfyWidgetNode<string> {
override properties: ComfyComboProperties = { override properties: ComfyComboProperties = {
options: ["*"] options: ["A", "B", "C", "D"]
} }
static slotLayout: SlotLayout = { static slotLayout: SlotLayout = {
@@ -164,7 +164,7 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
override inputWidgetTypes = ["combo", "enum"] override inputWidgetTypes = ["combo", "enum"]
constructor(name?: string) { constructor(name?: string) {
super(name, "*") super(name, "A")
} }
onConnectOutput( onConnectOutput(
@@ -194,9 +194,9 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
override clampOneConfig(input: IComfyInputSlot) { override clampOneConfig(input: IComfyInputSlot) {
if (!(this.properties.value in input.config.values)) { if (!(this.properties.value in input.config.values)) {
if (input.config.values.length === 0) if (input.config.values.length === 0)
this.setProperty("value", "") this.setValue("")
else else
this.setProperty("value", input.config.values[0]) this.setValue(input.config.values[0])
} }
} }
} }

View File

@@ -2,8 +2,6 @@ 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, LGraph } from "@litegraph-ts/core" import type { LGraphNode, IWidget, LGraph } from "@litegraph-ts/core"
import nodeState from "$lib/state/nodeState";
import type { NodeStateStore } from './nodeState';
import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyWidgetNode } from '$lib/nodes'; import type { ComfyWidgetNode } from '$lib/nodes';
@@ -90,8 +88,8 @@ export interface WidgetLayout extends IDragItem {
type DragItemID = string; type DragItemID = string;
type LayoutStateOps = { type LayoutStateOps = {
addContainer: (parentId: DragItemID, attrs: Partial<Attributes>, index: number) => ContainerLayout, addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index: number) => ContainerLayout,
addWidget: (parentId: DragItemID, node: LGraphNode, widget: IWidget<any, any>, attrs: Partial<Attributes>, index: number) => WidgetLayout, addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index: number) => WidgetLayout,
findDefaultContainerForInsertion: () => ContainerLayout | null, findDefaultContainerForInsertion: () => ContainerLayout | null,
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[], updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode) => void, nodeAdded: (node: LGraphNode) => void,
@@ -100,7 +98,7 @@ type LayoutStateOps = {
groupItems: (dragItems: IDragItem[]) => ContainerLayout, groupItems: (dragItems: IDragItem[]) => ContainerLayout,
ungroup: (container: ContainerLayout) => void, ungroup: (container: ContainerLayout) => void,
getCurrentSelection: () => IDragItem[], getCurrentSelection: () => IDragItem[],
clear: () => void, clear: (state?: Partial<LayoutState>) => void,
resetLayout: () => void, resetLayout: () => void,
} }
@@ -135,7 +133,7 @@ function findDefaultContainerForInsertion(): ContainerLayout | null {
return null return null
} }
function addContainer(parentId: DragItemID | null, attrs: Partial<Attributes> = {}, index: number = -1): ContainerLayout { function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index: number = -1): ContainerLayout {
const state = get(store); const state = get(store);
const dragItem: ContainerLayout = { const dragItem: ContainerLayout = {
type: "container", type: "container",
@@ -148,15 +146,10 @@ function addContainer(parentId: DragItemID | null, attrs: Partial<Attributes> =
...attrs ...attrs
} }
} }
const parent = parentId ? state.allItems[parentId] : null; const entry: DragItemEntry = { dragItem, children: [], parent: null };
const entry: DragItemEntry = { dragItem, children: [], parent: parent?.dragItem };
state.allItems[dragItem.id] = entry; state.allItems[dragItem.id] = entry;
if (parent) { if (parent) {
parent.children ||= [] moveItem(dragItem, parent)
if (index)
parent.children.splice(index, 0, dragItem)
else
parent.children.push(dragItem)
} }
store.set(state) store.set(state)
return dragItem; return dragItem;
@@ -178,7 +171,7 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
} }
} }
const parentEntry = state.allItems[parent.id] const parentEntry = state.allItems[parent.id]
const entry: DragItemEntry = { dragItem, children: [], parent: parentEntry.dragItem }; const entry: DragItemEntry = { dragItem, children: [], parent: null };
state.allItems[dragItem.id] = entry; state.allItems[dragItem.id] = entry;
moveItem(dragItem, parent) moveItem(dragItem, parent)
return dragItem; return dragItem;
@@ -209,6 +202,7 @@ function nodeAdded(node: LGraphNode) {
// 2. User adds a node with inputs that can be filled by frontend widgets. // 2. User adds a node with inputs that can be filled by frontend widgets.
// Depending on config, this means we should instantiate default UI nodes connected to those inputs. // Depending on config, this means we should instantiate default UI nodes connected to those inputs.
console.debug(node)
if ("svelteComponentType" in node) { if ("svelteComponentType" in node) {
addWidget(parent, node as ComfyWidgetNode); addWidget(parent, node as ComfyWidgetNode);
} }
@@ -254,21 +248,25 @@ function nodeRemoved(node: LGraphNode) {
} }
function configureFinished(graph: LGraph) { function configureFinished(graph: LGraph) {
const state = get(store)
const id = 0; const id = 0;
state.isConfiguring = false; clear({isConfiguring: false})
state.root = addContainer(null, { direction: "horizontal", showTitle: false }); const root = addContainer(null, { direction: "horizontal", showTitle: false });
const left = addContainer(state.root.id, { direction: "vertical", showTitle: false }); const left = addContainer(root, { direction: "vertical", showTitle: false });
const right = addContainer(state.root.id, { direction: "vertical", showTitle: false }); const right = addContainer(root, { direction: "vertical", showTitle: false });
for (const node of graph._nodes_in_order) { const state = get(store)
state.root = root;
store.set(state)
console.debug("[layoutState] configure begin", state, graph)
for (const node of graph._nodes) {
nodeAdded(node) nodeAdded(node)
} }
console.debug("[layoutState] configureFinished", state) console.debug("[layoutState] configureFinished", state)
store.set(state)
} }
function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) { function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
@@ -324,7 +322,7 @@ function groupItems(dragItems: IDragItem[]): ContainerLayout {
index = indexFound index = indexFound
} }
const container = addContainer(parent.id, { title: "Group" }, index) const container = addContainer(parent as ContainerLayout, { title: "Group" }, index)
for (const item of dragItems) { for (const item of dragItems) {
moveItem(item, container) moveItem(item, container)
@@ -364,7 +362,7 @@ function ungroup(container: ContainerLayout) {
store.set(state) store.set(state)
} }
function clear() { function clear(state: Partial<LayoutState> = {}) {
store.set({ store.set({
root: null, root: null,
allItems: {}, allItems: {},
@@ -372,8 +370,8 @@ function clear() {
currentSelection: [], currentSelection: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true, isConfiguring: true,
...state
}) })
addContainer(null, { direction: "horizontal", showTitle: false });
} }
function resetLayout() { function resetLayout() {

View File

@@ -1,154 +0,0 @@
import { writable, get } from 'svelte/store';
import type { LGraph, LGraphNode, IWidget } from "@litegraph-ts/core";
import type { Readable, Writable } from 'svelte/store';
import type ComfyGraphNode from '$lib/nodes/ComfyGraphNode';
import type ComfyWidget from '$lib/widgets/ComfyWidget';
/** store for one node's state */
export type NodeUIStateStore = Writable<any>
export type NodeUIState = {
name: string,
node: LGraphNode,
widgetStates: WidgetUIState[]
}
export type WidgetDrawState = {
isNodeExecuting: boolean
}
/** store for one widget's state */
export type WidgetUIStateStore = Writable<any>
export type WidgetUIState = {
/** position in the node's list of widgets */
index: number,
/** parent node containing the widget */
node: LGraphNode,
/** actual widget instance */
widget: IWidget,
/** widget value as a store, to react to changes */
value: WidgetUIStateStore,
/**
* true if this widget was added purely from the frontend. what this means:
* - this widget's state will not be saved to the workflow
* - the widget was added on startup by some subclass of ComfyGraphNode
*/
isVirtual: boolean
}
export type NodeID = number;
type NodeStateOps = {
nodeAdded: (node: LGraphNode) => void,
nodeRemoved: (node: LGraphNode) => void,
configureFinished: (graph: LGraph) => void,
nodeStateChanged: (node: LGraphNode) => void,
widgetStateChanged: (nodeId: number, widget: IWidget<any, any>) => void,
findWidgetByName: (nodeId: number, name: string) => WidgetUIState | null,
clear: () => void,
}
export type NodeStateStore = Record<NodeID, NodeUIState>;
type WritableNodeStateStore = Writable<NodeStateStore> & NodeStateOps;
const store: Writable<NodeStateStore> = writable({})
function clear() {
store.set({})
}
function nodeAdded(node: LGraphNode) {
let state = get(store)
const widgets = [];
// if (node.widgets) {
// for (const [index, widget] of node.widgets.entries()) {
// let isVirtual = false;
// if ("isVirtual" in widget)
// isVirtual = (widget as ComfyWidget<any, any>).isVirtual;
// widgets.push({ index, widget, value: writable(widget.value), isVirtual: isVirtual })
// }
// }
state[node.id] = { node: node, name: node.title, widgetStates: widgets }
store.set(state);
console.debug("call nodeAdded", state[node.id])
}
function nodeRemoved(node: LGraphNode) {
const state = get(store)
delete state[node.id]
store.set(state)
}
function nodeStateChanged(node: LGraphNode) {
const state = get(store)
const nodeState = state[node.id]
nodeState.name = node.title
store.set(state);
}
function configureFinished(graph: LGraph) {
let state = get(store);
for (const node of graph.computeExecutionOrder(false, null)) {
state[node.id].name = node.title;
// const widgetStates = state[node.id].widgetStates;
// if (node.widgets_values) {
// for (const [i, value] of node.widgets_values.entries()) {
// if (i < widgetStates.length && !widgetStates[i].isVirtual) {
// widgetStates[i].value.set(value);
// }
// else {
// console.log("Skip virtual widget", node.id, node.type, widgetStates[i].widget)
// }
// }
// }
}
store.set(state)
}
function widgetStateChanged(nodeId: number, widget: IWidget<any, any>) {
const state = get(store)
const entries = state[nodeId].widgetStates
if (entries) {
let widgetState = entries.find(e => e.widget === widget);
if (widgetState) {
widgetState.value.set(widget.value);
store.set(state);
}
else {
console.error("Widget state changed and node was found, but widget was not found in state!", widget, state[nodeId].node, entries)
}
}
else {
console.error("Widget state changed but node was not found in state!", widget, state[nodeId].node)
}
}
function findWidgetByName(nodeId: NodeID, name: string): WidgetUIState | null {
let state = get(store);
if (!(nodeId in state))
return null;
return state[nodeId].widgetStates.find((v) => v.widget.name === name);
}
const nodeStateStore: WritableNodeStateStore =
{
...store,
nodeAdded,
nodeRemoved,
nodeStateChanged,
configureFinished,
widgetStateChanged,
findWidgetByName,
clear
}
export default nodeStateStore;

View File

@@ -14,7 +14,8 @@ export type UIState = {
export type WritableUIStateStore = Writable<UIState>; export type WritableUIStateStore = Writable<UIState>;
const store: WritableUIStateStore = writable( const store: WritableUIStateStore = writable(
{ {
graphLocked: true, app: null,
graphLocked: false,
nodesLocked: false, nodesLocked: false,
uiEditMode: "disabled", uiEditMode: "disabled",
}) })

View File

@@ -2,7 +2,6 @@ import ComfyApp from "./components/ComfyApp";
import ComboWidget from "$lib/widgets/ComboWidget.svelte"; import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte"; import RangeWidget from "$lib/widgets/RangeWidget.svelte";
import TextWidget from "$lib/widgets/TextWidget.svelte"; import TextWidget from "$lib/widgets/TextWidget.svelte";
import { type WidgetUIState } from "$lib/stores/nodeState";
import { get } from "svelte/store" import { get } from "svelte/store"
import layoutState from "$lib/stores/layoutState" import layoutState from "$lib/stores/layoutState"
import type { SvelteComponentDev } from "svelte/internal"; import type { SvelteComponentDev } from "svelte/internal";
@@ -21,26 +20,6 @@ export function download(filename: string, text: string, type: string = "text/pl
}, 0); }, 0);
} }
export function getComponentForWidgetState(item: WidgetUIState): typeof SvelteComponentDev {
// custom widgets with TypeScript sources
let override = ComfyApp.widget_type_overrides[item.widget.type]
if (override) {
return override
}
// litegraph.ts built-in widgets
switch (item.widget.type) {
case "combo":
return ComboWidget;
case "number":
return RangeWidget;
case "text":
return TextWidget;
}
return null;
}
export function startDrag(evt: MouseEvent) { export function startDrag(evt: MouseEvent) {
const dragItemId: string = evt.target.dataset["dragItemId"]; const dragItemId: string = evt.target.dataset["dragItemId"];
const ls = get(layoutState) const ls = get(layoutState)

View File

@@ -1,7 +1,5 @@
<script lang="ts"> <script lang="ts">
import type { WidgetDrawState, WidgetUIState, WidgetUIStateStore } from "$lib/stores/nodeState";
import { BlockTitle } from "@gradio/atoms"; import { BlockTitle } from "@gradio/atoms";
import { Dropdown } from "@gradio/form";
import Select from 'svelte-select'; import Select from 'svelte-select';
import type { ComfyComboNode } from "$lib/nodes/index"; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
@@ -9,32 +7,35 @@
export let widget: WidgetLayout | null = null; export let widget: WidgetLayout | null = null;
let node: ComfyComboNode | null = null; let node: ComfyComboNode | null = null;
let nodeValue: Writable<string> | null = null; let nodeValue: Writable<string> | null = null;
let itemValue: WidgetUIStateStore | null = null; let option: any
let option: any;
$: if(widget) { $: widget && setNodeValue(widget);
node = widget.node as ComfyComboNode
nodeValue = node.value;
updateOption(); // don't react on option
};
function updateOption() { function setNodeValue(widget: WidgetLayout) {
option = get(nodeValue); if(widget && !node) {
node = widget.node as ComfyComboNode
nodeValue = node.value;
setOption($nodeValue) // don't react on option
}
} }
$: if (option && itemValue) { function setOption(value: any) {
$itemValue = option.value option = value;
}
$: if (nodeValue && option) {
$nodeValue = option.value;
} }
</script> </script>
<div class="wrapper gr-combo"> <div class="wrapper gr-combo">
{#if node !== null && option !== undefined} {#if node !== null && nodeValue !== null}
<label> <label>
<BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle> <BlockTitle show_label={true}>{widget.attrs.title}</BlockTitle>
<Select <Select
bind:value={option} bind:value={option}
bind:items={node.properties.values} bind:items={node.properties.options}
disabled={node.properties.values.length === 0} disabled={node.properties.options.length === 0}
clearable={false} clearable={false}
on:change on:change
on:select on:select

View File

@@ -1,7 +1,6 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte"; import { onMount } from "svelte";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/nodeState";
import { Block } from "@gradio/atoms"; import { Block } from "@gradio/atoms";
import { Gallery } from "@gradio/gallery"; import { Gallery } from "@gradio/gallery";
import type { Styles } from "@gradio/utils"; import type { Styles } from "@gradio/utils";

View File

@@ -2,7 +2,6 @@ import type { IEnumWidget, IEnumWidgetOptions, INumberWidget, LGraphNode, Widget
import ComfyWidget from "./ComfyWidget"; import ComfyWidget from "./ComfyWidget";
import type { ComfyImageResult } from "$lib/nodes/ComfySaveImageNode"; import type { ComfyImageResult } from "$lib/nodes/ComfySaveImageNode";
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import nodeState from "$lib/stores/nodeState"
export interface ComfyValueControlWidgetOptions extends IEnumWidgetOptions { export interface ComfyValueControlWidgetOptions extends IEnumWidgetOptions {
} }
@@ -50,6 +49,6 @@ export default class ComfyValueControlWidget extends ComfyWidget<ComfyValueContr
if (this.targetWidget.value > max) if (this.targetWidget.value > max)
this.targetWidget.value = max; this.targetWidget.value = max;
nodeState.widgetStateChanged(this.node.id, this.targetWidget); // nodeState.widgetStateChanged(this.node.id, this.targetWidget);
} }
} }

View File

@@ -1,6 +1,5 @@
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode"; import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core"; import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core";
import nodeState from "$lib/stores/nodeState";
export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T, V> { export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T, V> {
name: string; name: string;
@@ -27,7 +26,6 @@ export default abstract class ComfyWidget<T = any, V = any> implements IWidget<T
setValue(value: V) { setValue(value: V) {
this.value = value; this.value = value;
nodeState.widgetStateChanged(this.node.id, this);
} }
draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number): void; draw?(ctx: CanvasRenderingContext2D, node: LGraphNode, width: number, posY: number, height: number): void;

View File

@@ -1,5 +1,4 @@
<script lang="ts"> <script lang="ts">
import type { WidgetUIStateStore } from "$lib/stores/nodeState";
import { TextBox } from "@gradio/form"; import { TextBox } from "@gradio/form";
import type { ComfyComboNode } from "$lib/nodes/index"; import type { ComfyComboNode } from "$lib/nodes/index";
import { type WidgetLayout } from "$lib/stores/layoutState"; import { type WidgetLayout } from "$lib/stores/layoutState";
@@ -18,7 +17,7 @@
<div class="wrapper gr-textbox"> <div class="wrapper gr-textbox">
{#if node !== null && nodeValue !== null} {#if node !== null && nodeValue !== null}
<TextBox <TextBox
bind:value={$itemValue} bind:value={$nodeValue}
label={widget.attrs.title} label={widget.attrs.title}
lines={node.properties.multiline ? 5 : 1} lines={node.properties.multiline ? 5 : 1}
max_lines={node.properties.multiline ? 5 : 1} max_lines={node.properties.multiline ? 5 : 1}

View File

@@ -1,20 +1,8 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import { get } from "svelte/store";
import { Pane, Splitpanes } from 'svelte-splitpanes';
import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form"
import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
import { LGraph, LGraphNode } from "@litegraph-ts/core"; import { Link, Toolbar } from "framework7-svelte"
import type { ComfyAPIStatus } from "$lib/api";
import queueState from "$lib/stores/queueState";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
import { getComponentForWidgetState } from "$lib/utils"
import { f7 } from "framework7-svelte" import { f7 } from "framework7-svelte"
export let subworkflowID: number = -1; export let subworkflowID: number = -1;

View File

@@ -5,7 +5,6 @@
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form" import { Checkbox } from "@gradio/form"
import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils" import { download } from "$lib/utils"
@@ -46,11 +45,6 @@
return return
app = $uiState.app = new ComfyApp(); app = $uiState.app = new ComfyApp();
app.eventBus.on("nodeAdded", nodeState.nodeAdded);
app.eventBus.on("nodeRemoved", nodeState.nodeRemoved);
app.eventBus.on("configured", nodeState.configureFinished);
app.eventBus.on("cleared", nodeState.clear);
app.eventBus.on("autosave", doAutosave); app.eventBus.on("autosave", doAutosave);
app.eventBus.on("restored", doRestore); app.eventBus.on("restored", doRestore);

View File

@@ -5,7 +5,6 @@
import { Button } from "@gradio/button"; import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form" import { Checkbox } from "@gradio/form"
import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer"; import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils" import { download } from "$lib/utils"

View File

@@ -1,21 +1,9 @@
<script lang="ts"> <script lang="ts">
import { onMount } from "svelte";
import { get } from "svelte/store";
import { Pane, Splitpanes } from 'svelte-splitpanes';
import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp"; import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form"
import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState"; import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
import { LGraph, LGraphNode } from "@litegraph-ts/core";
import type { ComfyAPIStatus } from "$lib/api";
import queueState from "$lib/stores/queueState"; import queueState from "$lib/stores/queueState";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte" import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
import { getComponentForWidgetState } from "$lib/utils"
import { f7 } from "framework7-svelte"
export let subworkflowID: number = -1; export let subworkflowID: number = -1;
let app: ComfyApp = undefined; let app: ComfyApp = undefined;
@@ -28,21 +16,7 @@
<Page name="subworkflow"> <Page name="subworkflow">
<Navbar title="Workflow {subworkflowID}" backLink="Back" /> <Navbar title="Workflow {subworkflowID}" backLink="Back" />
{#each Object.entries($nodeState) as [id, ws]} <div>Workflow!</div>
{@const node = app.lGraph.getNodeById(id)}
<div class:is-executing={$queueState.runningNodeId === node.id}>
<Block>
<label for={String(id)}>
<BlockTitle>
{node.title}
</BlockTitle>
</label>
{#each $nodeState[id].widgetStates as item}
<svelte:component this={getComponentForWidgetState(item)} {item} />
{/each}
</Block>
</div>
{/each}
</Page> </Page>
<style> <style>