Sync frontend state to backend

This commit is contained in:
space-nuko
2023-04-08 13:28:25 -05:00
parent ff6b11102f
commit aafd349b30
4 changed files with 61 additions and 20 deletions

View File

@@ -1,5 +1,5 @@
import type { LGraph } from "@litegraph-ts/core";
import widgetState, { type WidgetStateStore, type WidgetUIStateStore } from "./stores/widgetState";
import widgetState, { type WidgetStateStore, type WidgetUIState, type WidgetUIStateStore } from "./stores/widgetState";
import type ComfyApp from "./components/ComfyApp";
import type { Unsubscriber } from "svelte/store";
@@ -9,12 +9,18 @@ type WidgetSubStore = {
}
/*
* 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.
* Responsible for watching for 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. If that were true the implementation would be way more
* complex since litegraph doesn't (currently) expose a global
* event-emitter-like thing for when nodes/widgets are changed.
*
* Assumptions:
* - Widgets can't be added to a node after they're created
* - Widgets can't be added to a node after they're created (messes up the indices in WidgetSubStore[])
* - Widgets can't be interacted with from the graph, only from the frontend
* - Only one workflow/graph can ever be loaded into the program
*/
export default class GraphSync {
graph: LGraph;
@@ -26,18 +32,19 @@ export default class GraphSync {
constructor(app: ComfyApp) {
this.graph = app.lGraph;
this._unsubscribe = widgetState.subscribe(this.onWidgetStateChanged.bind(this));
this._unsubscribe = widgetState.subscribe(this.onAllWidgetStateChanged.bind(this));
this._finalizer = new FinalizationRegistry((id: number) => {
console.log(`${this} has been garbage collected`);
this._unsubscribe();
});
}
private onWidgetStateChanged(state: WidgetStateStore) {
/*
* Fired when the entire widget graph changes.
*/
private onAllWidgetStateChanged(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);
@@ -59,10 +66,8 @@ export default class GraphSync {
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 });
const unsub = wuis.value.subscribe((v) => this.onWidgetStateChanged(wuis, v))
this.stores[nodeId].push({ store: wuis.vlue, unsubscribe: unsub });
}
console.log("NEWSTORES", this.stores[nodeId])
@@ -75,4 +80,12 @@ export default class GraphSync {
}
delete this.stores[nodeId]
}
/*
* Fired when a single widget's value changes.
*/
private onWidgetStateChanged(wuis: WidgetUIState, value: any) {
wuis.widget.value = value;
this.graph.setDirtyCanvas(true, true);
}
}

View File

@@ -10,9 +10,19 @@ import { subStore } from "immer-loves-svelte"
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
}

View File

@@ -9,18 +9,23 @@
let option: any;
$: if(item) {
option = get(item.value);
itemValue = item.value
if (!itemValue)
itemValue = item.value;
if (!option)
option = get(item.value);
};
$: if (option && itemValue) {
$itemValue = option.value
}
</script>
<div class="wrapper">
{#if item}
{#if item && $itemValue}
<label>
<BlockTitle show_label={true}>{item.widget.name}</BlockTitle>
<Select
bind:value={option}
bind:justValue={$itemValue}
bind:items={item.widget.options.values}
disabled={item.widget.options.values.length === 0}
clearable={false}

View File

@@ -1,22 +1,35 @@
<script lang="ts">
import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
import { Range } from "@gradio/form";
import { get } from "svelte/store";
export let item: WidgetUIState | null = null;
let itemValue: WidgetUIStateStore | null = null;
$: if (item) { itemValue = item.value; }
let option: number | null = null;
$: if (item && !option) {
if (!itemValue)
itemValue = item.value;
option = get(item.value)
}
function onRelease(e: Event) {
if (itemValue && option) {
$itemValue = option
}
}
</script>
<div class="wrapper">
{#if item && itemValue}
{#if item && option}
<Range
bind:value={$itemValue}
bind:value={option}
minimum={item.widget.options.min}
maximum={item.widget.options.max}
step={item.widget.options.step}
label={item.widget.name}
show_label={true}
on:release={onRelease}
on:change
on:release
/>
{/if}
</div>