Make widget state changes reactive
Substate stores
This commit is contained in:
@@ -33,9 +33,8 @@
|
||||
}
|
||||
|
||||
function queuePrompt() {
|
||||
const state = get(widgetState);
|
||||
console.log("Queuing!", state);
|
||||
app.queuePrompt(0, 1, state);
|
||||
console.log("Queuing!");
|
||||
app.queuePrompt(0, 1);
|
||||
}
|
||||
|
||||
$: if (app) app.lCanvas.allow_dragnodes = !nodesLocked;
|
||||
@@ -69,28 +68,10 @@
|
||||
|
||||
function serializeAppState(): SerializedAppState {
|
||||
const graph = app.lGraph;
|
||||
const frontendState = get(widgetState);
|
||||
|
||||
const serializedGraph = graph.serialize()
|
||||
const serializedPaneOrder = uiPane.serialize()
|
||||
|
||||
// Override the saved graph widget state with the properties set in the
|
||||
// frontend panels.
|
||||
for (let i = 0; i < serializedGraph.nodes.length; i++) {
|
||||
let serializedNode = serializedGraph.nodes[i];
|
||||
let frontendWidgetStates = frontendState[serializedNode.id];
|
||||
if (frontendWidgetStates && serializedNode.widgets_values) {
|
||||
for (let j = 0; j < serializedNode.widgets_values.length; j++) {
|
||||
let frontendWidgetState = frontendWidgetStates[j];
|
||||
|
||||
// Virtual widgets always come after real widgets in the current design
|
||||
if (frontendWidgetState && !frontendWidgetState.isVirtual) {
|
||||
serializedNode.widgets_values[j] = frontendWidgetState.value;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
createdBy: "ComfyBox",
|
||||
version: 1,
|
||||
|
||||
@@ -16,6 +16,7 @@ import type { WidgetStateStore, WidgetUIState } from "$lib/stores/widgetState";
|
||||
import * as widgets from "$lib/widgets/index"
|
||||
import type ComfyWidget from "$lib/widgets/ComfyWidget";
|
||||
import queueState from "$lib/stores/queueState";
|
||||
import GraphSync from "$lib/GraphSync";
|
||||
|
||||
LiteGraph.catch_exceptions = false;
|
||||
|
||||
@@ -70,6 +71,7 @@ export default class ComfyApp {
|
||||
dropZone: HTMLElement | null = null;
|
||||
nodeOutputs: Record<string, any> = {};
|
||||
eventBus: TypedEmitter<ComfyAppEvents> = new EventEmitter() as TypedEmitter<ComfyAppEvents>;
|
||||
graphSync: GraphSync;
|
||||
|
||||
dragOverNode: LGraphNode | null = null;
|
||||
shiftDown: boolean = false;
|
||||
@@ -88,6 +90,7 @@ export default class ComfyApp {
|
||||
this.lGraph = new LGraph();
|
||||
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl, this.lGraph);
|
||||
this.canvasCtx = this.canvasEl.getContext("2d");
|
||||
this.graphSync = new GraphSync(this);
|
||||
|
||||
this.addGraphLifecycleHooks();
|
||||
|
||||
@@ -458,14 +461,12 @@ export default class ComfyApp {
|
||||
* Converts the current graph workflow for sending to the API
|
||||
* @returns The workflow and node links
|
||||
*/
|
||||
async graphToPrompt(frontendState: WidgetStateStore = {}) {
|
||||
async graphToPrompt() {
|
||||
const workflow = this.lGraph.serialize();
|
||||
|
||||
const output = {};
|
||||
// Process nodes in order of execution
|
||||
for (const node of this.lGraph.computeExecutionOrder<ComfyGraphNodeExecutable>(false, null)) {
|
||||
const fromFrontend: WidgetUIState[] | null = frontendState[node.id];
|
||||
|
||||
const n = workflow.nodes.find((n) => n.id === node.id);
|
||||
|
||||
if (node.isVirtualNode || !node.comfyClass) {
|
||||
@@ -491,9 +492,6 @@ export default class ComfyApp {
|
||||
const widget = widgets[i];
|
||||
if (!widget.options || widget.options.serialize !== false) {
|
||||
let value = widget.serializeValue ? await widget.serializeValue(n, i) : widget.value;
|
||||
if (fromFrontend && !fromFrontend[i].isVirtual) {
|
||||
value = fromFrontend[i].value;
|
||||
}
|
||||
inputs[widget.name] = value
|
||||
}
|
||||
}
|
||||
@@ -540,7 +538,7 @@ export default class ComfyApp {
|
||||
return { workflow, output };
|
||||
}
|
||||
|
||||
async queuePrompt(num: number, batchCount: number = 1, frontendState: WidgetStateStore = {}) {
|
||||
async queuePrompt(num: number, batchCount: number = 1) {
|
||||
this.queueItems.push({ num, batchCount });
|
||||
|
||||
// Only have one action process the items so each one gets a unique seed correctly
|
||||
@@ -555,7 +553,7 @@ export default class ComfyApp {
|
||||
console.log(`Queue get! ${num} ${batchCount}`);
|
||||
|
||||
for (let i = 0; i < batchCount; i++) {
|
||||
const p = await this.graphToPrompt(frontendState);
|
||||
const p = await this.graphToPrompt();
|
||||
|
||||
try {
|
||||
await this.api.queuePrompt(num, p);
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
import ComfyApp from "./ComfyApp";
|
||||
import type { SerializedPanes } from "./ComfyApp"
|
||||
import ComfyPane from "./ComfyPane.svelte";
|
||||
import widgetState, { type WidgetUIState } from "$lib/stores/widgetState";
|
||||
import widgetState from "$lib/stores/widgetState";
|
||||
import type { DragItem } from "./ComfyUIPane";
|
||||
|
||||
export let app: ComfyApp;
|
||||
|
||||
@@ -53,6 +53,7 @@
|
||||
|
||||
#modalImage {
|
||||
overflow: hidden;
|
||||
user-drag: none;
|
||||
}
|
||||
|
||||
.modalControls {
|
||||
|
||||
78
src/lib/graphSync.ts
Normal file
78
src/lib/graphSync.ts
Normal file
@@ -0,0 +1,78 @@
|
||||
import type { LGraph } from "@litegraph-ts/core";
|
||||
import widgetState, { type WidgetStateStore, type WidgetUIStateStore } from "./stores/widgetState";
|
||||
import type ComfyApp from "./components/ComfyApp";
|
||||
import type { Unsubscriber } from "svelte/store";
|
||||
|
||||
type WidgetSubStore = {
|
||||
store: WidgetUIStateStore,
|
||||
unsubscribe: Unsubscriber
|
||||
}
|
||||
|
||||
/*
|
||||
* Responsible for watching and synchronizing state changes from the frontend to the litegraph instance.
|
||||
* The other way around is unnecessary since the nodes in ComfyBox can't be interacted with.
|
||||
*
|
||||
* Assumptions:
|
||||
* - Widgets can't be added to a node after they're created
|
||||
* - Widgets can't be interacted with from the graph, only from the frontend
|
||||
*/
|
||||
export default class GraphSync {
|
||||
graph: LGraph;
|
||||
private _unsubscribe: Unsubscriber;
|
||||
private _finalizer: FinalizationRegistry<number>;
|
||||
|
||||
// nodeId -> widgetSubStores[]
|
||||
private stores: Record<string, WidgetSubStore[]> = {}
|
||||
|
||||
constructor(app: ComfyApp) {
|
||||
this.graph = app.lGraph;
|
||||
this._unsubscribe = widgetState.subscribe(this.onWidgetStateChanged.bind(this));
|
||||
this._finalizer = new FinalizationRegistry((id: number) => {
|
||||
console.log(`${this} has been garbage collected`);
|
||||
this._unsubscribe();
|
||||
});
|
||||
}
|
||||
|
||||
private onWidgetStateChanged(state: WidgetStateStore) {
|
||||
// TODO assumes only a single graph's widget state.
|
||||
|
||||
console.warn("ONWIDGETSTATECHANGE")
|
||||
|
||||
for (let nodeId in state) {
|
||||
if (!this.stores[nodeId]) {
|
||||
this.addStores(state, nodeId);
|
||||
}
|
||||
}
|
||||
|
||||
for (let nodeId in this.stores) {
|
||||
if (!state[nodeId]) {
|
||||
this.removeStores(nodeId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private addStores(state: WidgetStateStore, nodeId: string) {
|
||||
if (this.stores[nodeId]) {
|
||||
console.warn("Stores already exist!", nodeId, this.stores[nodeId])
|
||||
}
|
||||
|
||||
this.stores[nodeId] = []
|
||||
|
||||
for (const wuis of state[nodeId]) {
|
||||
const unsub = wuis.value.subscribe((v) => {
|
||||
console.log("CHANGE", v)
|
||||
})
|
||||
this.stores[nodeId].push({ store: wuis.value, unsubscribe: unsub });
|
||||
}
|
||||
|
||||
console.log("NEWSTORES", this.stores[nodeId])
|
||||
}
|
||||
|
||||
private removeStores(nodeId: string) {
|
||||
console.log("DELSTORES", this.stores[nodeId])
|
||||
for (const ss of this.stores[nodeId]) {
|
||||
ss.unsubscribe();
|
||||
}
|
||||
delete this.stores[nodeId]
|
||||
}
|
||||
}
|
||||
@@ -4,10 +4,15 @@ import type { Readable, Writable } from 'svelte/store';
|
||||
import type ComfyGraphNode from '$lib/nodes/ComfyGraphNode';
|
||||
import type ComfyWidget from '$lib/widgets/ComfyWidget';
|
||||
|
||||
import { subStore } from "immer-loves-svelte"
|
||||
|
||||
/** store for one widget's state */
|
||||
export type WidgetUIStateStore = Writable<any>
|
||||
|
||||
export type WidgetUIState = {
|
||||
node: LGraphNode,
|
||||
widget: IWidget,
|
||||
value: any,
|
||||
value: WidgetUIStateStore,
|
||||
isVirtual: boolean
|
||||
}
|
||||
|
||||
@@ -41,7 +46,7 @@ function nodeAdded(node: LGraphNode) {
|
||||
for (const widget of node.widgets) {
|
||||
if (!state[node.id])
|
||||
state[node.id] = []
|
||||
state[node.id].push({ node, widget, value: widget.value, isVirtual: false })
|
||||
state[node.id].push({ node, widget, value: writable(widget.value), isVirtual: false })
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,10 +55,12 @@ function nodeAdded(node: LGraphNode) {
|
||||
for (const widget of comfyNode.virtualWidgets) {
|
||||
if (!state[comfyNode.id])
|
||||
state[comfyNode.id] = []
|
||||
state[comfyNode.id].push({ node, widget, value: widget.value, isVirtual: true })
|
||||
state[comfyNode.id].push({ node, widget, value: writable(widget.value), isVirtual: true })
|
||||
}
|
||||
}
|
||||
|
||||
console.log("NODEADDED", state)
|
||||
|
||||
store.set(state);
|
||||
}
|
||||
|
||||
@@ -69,7 +76,7 @@ function widgetStateChanged(widget: ComfyWidget<any, any>) {
|
||||
if (entries) {
|
||||
let widgetState = entries.find(e => e.widget === widget);
|
||||
if (widgetState) {
|
||||
widgetState.value = widget.value;
|
||||
widgetState.value.set(widget.value);
|
||||
store.set(state);
|
||||
}
|
||||
else {
|
||||
@@ -88,7 +95,7 @@ function configureFinished(graph: LGraph) {
|
||||
if (node.widgets_values) {
|
||||
for (const [i, value] of node.widgets_values.entries()) {
|
||||
if (i < state[node.id].length && !state[node.id][i].isVirtual) { // Virtual widgets always come after real widgets
|
||||
state[node.id][i].value = value;
|
||||
state[node.id][i].value.set(value);
|
||||
}
|
||||
else {
|
||||
console.error("Mismatch in widgets_values!", state[node.id].map(i => i.value), node.widgets_values)
|
||||
|
||||
@@ -1,13 +1,17 @@
|
||||
<script lang="ts">
|
||||
import type { WidgetDrawState, WidgetUIState } from "$lib/stores/widgetState";
|
||||
import type { WidgetDrawState, WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import { BlockTitle } from "@gradio/atoms";
|
||||
import { Dropdown } from "@gradio/form";
|
||||
import { get } from "svelte/store";
|
||||
import Select from 'svelte-select';
|
||||
export let item: WidgetUIState | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
let option: any;
|
||||
|
||||
let option: any = null;
|
||||
|
||||
$: if(item && !option) option = item.value;
|
||||
$: if(item) {
|
||||
option = get(item.value);
|
||||
itemValue = item.value
|
||||
};
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
@@ -16,7 +20,7 @@
|
||||
<BlockTitle show_label={true}>{item.widget.name}</BlockTitle>
|
||||
<Select
|
||||
bind:value={option}
|
||||
bind:justValue={item.value}
|
||||
bind:justValue={$itemValue}
|
||||
bind:items={item.widget.options.values}
|
||||
disabled={item.widget.options.values.length === 0}
|
||||
clearable={false}
|
||||
|
||||
@@ -1,11 +1,17 @@
|
||||
<script lang="ts">
|
||||
import { onMount } from "svelte";
|
||||
import { ImageViewer } from "$lib/ImageViewer";
|
||||
import type { WidgetUIState } from "$lib/stores/widgetState";
|
||||
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import { Block } from "@gradio/atoms";
|
||||
import { Gallery } from "@gradio/gallery";
|
||||
import type { Styles } from "@gradio/utils";
|
||||
|
||||
export let item: WidgetUIState | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null; // stores must be declared at top level
|
||||
|
||||
$: if(item) {
|
||||
itemValue = item.value;
|
||||
}
|
||||
|
||||
let style: Styles = {
|
||||
// grid_cols: [2],
|
||||
@@ -28,10 +34,10 @@
|
||||
|
||||
</script>
|
||||
<div class="wrapper comfy-gallery-widget" bind:this={element}>
|
||||
{#if item}
|
||||
{#if item && itemValue}
|
||||
<Block variant="solid" padding={false}>
|
||||
<Gallery
|
||||
bind:value={item.value}
|
||||
bind:value={$itemValue}
|
||||
label={item.widget.name}
|
||||
show_label={true}
|
||||
{style}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { WidgetUIState } from "$lib/stores/widgetState";
|
||||
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import { Range } from "@gradio/form";
|
||||
export let item: WidgetUIState | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
$: if (item) { itemValue = item.value; }
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
{#if item}
|
||||
{#if item && itemValue}
|
||||
<Range
|
||||
bind:value={item.value}
|
||||
bind:value={$itemValue}
|
||||
minimum={item.widget.options.min}
|
||||
maximum={item.widget.options.max}
|
||||
step={item.widget.options.step}
|
||||
|
||||
@@ -1,13 +1,15 @@
|
||||
<script lang="ts">
|
||||
import type { WidgetUIState } from "$lib/stores/widgetState";
|
||||
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
|
||||
import { TextBox } from "@gradio/form";
|
||||
export let item: WidgetUIState | null = null;
|
||||
let itemValue: WidgetUIStateStore | null = null;
|
||||
$: if (item) { itemValue = item.value; }
|
||||
</script>
|
||||
|
||||
<div class="wrapper">
|
||||
{#if item}
|
||||
{#if item && itemValue}
|
||||
<TextBox
|
||||
bind:value={item.value}
|
||||
bind:value={$itemValue}
|
||||
label={item.widget.name}
|
||||
lines={item.widget.options.multiline ? 5 : 1}
|
||||
max_lines={item.widget.options.multiline ? 5 : 1}
|
||||
|
||||
Reference in New Issue
Block a user