Start serializing UI state

This commit is contained in:
space-nuko
2023-05-04 10:29:44 -05:00
parent fd417c6759
commit 93cf2ed98a
8 changed files with 294 additions and 149 deletions

View File

@@ -9,8 +9,8 @@
import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
import type { ComfyAPIStatus } from "$lib/api";
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
import { LGraph } from "@litegraph-ts/core";
import LightboxModal from "./LightboxModal.svelte";
@@ -19,11 +19,20 @@
let app: ComfyApp = undefined;
let imageViewer: ImageViewer;
let uiPane: ComfyUIPane = undefined;
let queue: ComfyQueue = undefined;
let mainElem: HTMLDivElement;
let uiPane: ComfyUIPane = undefined;
let containerElem: HTMLDivElement;
let resizeTimeout: typeof Timer = -1;
let resizeTimeout: NodeJS.Timeout | null;
let debugLayout: boolean = true;
const toastOptions = {
intro: { duration: 200 },
theme: {
'--toastBarHeight': 0
}
}
function refreshView(event?: Event) {
clearTimeout(resizeTimeout);
@@ -65,35 +74,41 @@
}
}
function doAutosave(graph: LGraph): void {
const savedWorkflow = app.serialize();
localStorage.setItem("workflow", JSON.stringify(savedWorkflow))
}
function doRestore(data: SerializedAppState) {
layoutState.deserialize(data.layout, app.lGraph);
}
function doSave(): void {
if (!app?.lGraph)
return;
const date = new Date();
const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", "");
app.saveStateToLocalStorage();
toast.push("Saved to local storage.")
//
// const date = new Date();
// const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", "");
//
// download(`workflow-${formattedDate}.json`, JSON.stringify(app.serialize()), "application/json")
}
download(`workflow-${formattedDate}.json`, JSON.stringify(app.serialize()), "application/json")
function doReset(): void {
var confirmed = confirm("Are you sure you want to clear the current workflow?");
if (confirmed) {
app.reset();
}
}
function doRecenter(): void {
app.lCanvas.recenter();
}
onMount(async () => {
app = new ComfyApp();
if (debugLayout) {
layoutState.subscribe(s => {
console.warn("UPDATESTATE", s)
})
}
app.eventBus.on("nodeAdded", layoutState.nodeAdded);
app.eventBus.on("nodeRemoved", layoutState.nodeRemoved);
app.eventBus.on("configured", layoutState.configureFinished);
app.eventBus.on("cleared", layoutState.clear);
app.eventBus.on("autosave", doAutosave);
app.eventBus.on("restored", doRestore);
app.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
@@ -117,7 +132,7 @@
})
</script>
<div id="main" bind:this={mainElem}>
<div id="main">
<div id="dropzone" class="dropzone"></div>
<div id="container" bind:this={containerElem}>
<Splitpanes theme="comfy" on:resize={refreshView}>
@@ -153,6 +168,12 @@
<Button variant="secondary" on:click={doSave}>
Save
</Button>
<Button variant="secondary" on:click={doReset}>
Reset
</Button>
<Button variant="secondary" on:click={doRecenter}>
Recenter
</Button>
<Checkbox label="Lock Nodes" bind:value={$uiState.nodesLocked}/>
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/>
<label for="enable-ui-editing">Enable UI Editing</label>
@@ -164,6 +185,8 @@
<LightboxModal />
</div>
<SvelteToast options={toastOptions} />
<style lang="scss">
#container {
height: calc(100vh - 60px);

View File

@@ -1,4 +1,4 @@
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode } from "@litegraph-ts/core";
import { LiteGraph, LGraph, LGraphCanvas, LGraphNode, type LGraphNodeConstructor, type LGraphNodeExecutable, type SerializedLGraph, type SerializedLGraphGroup, type SerializedLGraphNode, type SerializedLLink, NodeMode, type Vector2 } from "@litegraph-ts/core";
import type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI from "$lib/api"
import { ComfyWidgets } from "$lib/widgets"
@@ -10,7 +10,7 @@ import type TypedEmitter from "typed-emitter";
// Import nodes
import "@litegraph-ts/nodes-basic"
import * as nodes from "$lib/nodes/index"
import ComfyGraphCanvas from "$lib/ComfyGraphCanvas";
import ComfyGraphCanvas, { type SerializedGraphCanvasState } from "$lib/ComfyGraphCanvas";
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import * as widgets from "$lib/widgets/index"
import type ComfyWidget from "$lib/widgets/ComfyWidget";
@@ -21,6 +21,8 @@ import type IComfyInputSlot from "$lib/IComfyInputSlot";
import type { SerializedLayoutState } from "$lib/stores/layoutState";
import layoutState from "$lib/stores/layoutState";
export const COMFYBOX_SERIAL_VERSION = 1;
LiteGraph.catch_exceptions = false;
if (typeof window !== "undefined") {
@@ -34,7 +36,8 @@ export type SerializedAppState = {
createdBy: "ComfyBox",
version: number,
workflow: SerializedLGraph,
layout: SerializedLayoutState
layout: SerializedLayoutState,
canvas: SerializedGraphCanvasState
}
type ComfyAppEvents = {
@@ -45,7 +48,6 @@ type ComfyAppEvents = {
cleared: () => void
beforeChange: (graph: LGraph, param: any) => void
afterChange: (graph: LGraph, param: any) => void
autosave: (graph: LGraph) => void
restored: (workflow: SerializedAppState) => void
}
@@ -60,7 +62,7 @@ export default class ComfyApp {
canvasEl: HTMLCanvasElement | null = null;
canvasCtx: CanvasRenderingContext2D | null = null;
lGraph: LGraph | null = null;
lCanvas: LGraphCanvas | null = null;
lCanvas: ComfyGraphCanvas | null = null;
dropZone: HTMLElement | null = null;
nodeOutputs: Record<string, any> = {};
eventBus: TypedEmitter<ComfyAppEvents> = new EventEmitter() as TypedEmitter<ComfyAppEvents>;
@@ -102,9 +104,8 @@ export default class ComfyApp {
try {
const json = localStorage.getItem("workflow");
if (json) {
const workflow = JSON.parse(json) as SerializedAppState;
this.loadGraphData(workflow["workflow"]);
this.eventBus.emit("restored", workflow);
const state = JSON.parse(json) as SerializedAppState;
this.deserialize(state)
restored = true;
}
} catch (err) {
@@ -113,11 +114,11 @@ export default class ComfyApp {
// We failed to restore a workflow so load the default
if (!restored) {
this.loadGraphData();
this.initDefaultGraph();
}
// Save current workflow automatically
setInterval(this.requestAutosave.bind(this), 15000);
setInterval(this.saveStateToLocalStorage.bind(this), 1000);
this.addApiUpdateHandlers();
this.addDropHandler();
@@ -181,8 +182,10 @@ export default class ComfyApp {
this.eventBus.emit("cleared");
}
private requestAutosave() {
this.eventBus.emit("autosave", this.lGraph);
saveStateToLocalStorage() {
const savedWorkflow = this.serialize();
const json = JSON.stringify(savedWorkflow);
localStorage.setItem("workflow", json)
}
private addGraphLifecycleHooks() {
@@ -305,20 +308,6 @@ export default class ComfyApp {
// await this.#invokeExtensionsAsync("registerCustomNodes");
}
serialize(): SerializedAppState {
const graph = this.lGraph;
const serializedGraph = graph.serialize()
const serializedLayout = layoutState.serialize()
return {
createdBy: "ComfyBox",
version: 1,
workflow: serializedGraph,
layout: serializedLayout
}
}
private showDropZone() {
if (this.dropZone)
this.dropZone.style.display = "block";
@@ -365,24 +354,24 @@ export default class ComfyApp {
* Adds a handler on paste that extracts and loads workflows from pasted JSON data
*/
private addPasteHandler() {
document.addEventListener("paste", (e) => {
let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain");
let workflow;
try {
data = data.slice(data.indexOf("{"));
workflow = JSON.parse(data);
} catch (err) {
try {
data = data.slice(data.indexOf("workflow\n"));
data = data.slice(data.indexOf("{"));
workflow = JSON.parse(data);
} catch (error) { }
}
// document.addEventListener("paste", (e) => {
// let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain");
// let workflow;
// try {
// data = data.slice(data.indexOf("{"));
// workflow = JSON.parse(data);
// } catch (err) {
// try {
// data = data.slice(data.indexOf("workflow\n"));
// data = data.slice(data.indexOf("{"));
// workflow = JSON.parse(data);
// } catch (error) { }
// }
if (workflow && workflow.version && workflow.nodes && workflow.extra) {
this.loadGraphData(workflow);
}
});
// if (workflow && workflow.version && workflow.nodes && workflow.extra) {
// this.loadGraphData(workflow);
// }
// });
}
/**
@@ -436,17 +425,55 @@ export default class ComfyApp {
});
}
serialize(): SerializedAppState {
const graph = this.lGraph;
const serializedGraph = graph.serialize()
const serializedLayout = layoutState.serialize()
const serializedCanvas = this.lCanvas.serialize();
return {
createdBy: "ComfyBox",
version: COMFYBOX_SERIAL_VERSION,
workflow: serializedGraph,
layout: serializedLayout,
canvas: serializedCanvas
}
}
deserialize(data: SerializedAppState) {
if (data.version !== COMFYBOX_SERIAL_VERSION) {
throw `Invalid ComfyBox saved data format: ${data.version}`
}
// Ensure loadGraphData does not trigger any state changes in layoutState
// (isConfiguring is set to true here)
// lGraph.configure will add new nodes, triggering onNodeAdded, but we
// want to restore the layoutState ourselves
layoutState.onStartConfigure();
this.loadGraphData(data.workflow)
// Now restore the layout
// Subsequent added nodes will add the UI data to layoutState
layoutState.deserialize(data.layout, this.lGraph)
// Restore canvas offset/zoom
this.lCanvas.deserialize(data.canvas)
}
initDefaultGraph() {
const state = structuredClone(defaultGraph)
this.deserialize(state)
}
/**
* Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object
*/
loadGraphData(graphData?: SerializedLGraph) {
loadGraphData(graphData: SerializedLGraph) {
this.clean();
if (!graphData) {
graphData = structuredClone(defaultGraph.workflow)
}
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
for (let n of graphData.nodes) {
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
@@ -463,6 +490,25 @@ export default class ComfyApp {
}
}
reset() {
this.clean();
const blankGraph: SerializedLGraph = {
last_node_id: 0,
last_link_id: 0,
nodes: [],
links: [],
groups: [],
config: {},
extra: {},
version: 0
}
layoutState.onStartConfigure();
this.lGraph.configure(blankGraph)
layoutState.initDefaultLayout();
}
/**
* Converts the current graph workflow for sending to the API
* @returns The workflow and node links

View File

@@ -6,7 +6,7 @@
import ComfyApp from "./ComfyApp";
import type { SerializedPanes } from "./ComfyApp"
import WidgetContainer from "./WidgetContainer.svelte";
import layoutState, { type ContainerLayout, type DragItem } from "$lib/stores/layoutState";
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
import uiState from "$lib/stores/uiState";
import Menu from './menu/Menu.svelte';
@@ -15,18 +15,8 @@
import Icon from './menu/Icon.svelte'
export let app: ComfyApp;
let root: IDragItem | null;
let dragConfigured: boolean = false;
//
// function addUIForNewNode(node: LGraphNode, paneIndex?: number) {
// if (!paneIndex)
// paneIndex = findLeastPopulatedPaneIndex();
// dragItems[paneIndex].push({ id: totalId++, node: node });
// }
//
// $: if(app && !dragConfigured) {
// dragConfigured = true;
// app.eventBus.on("nodeAdded", addUIForNewNode);
// }
/*
* Serialize UI panel order so it can be restored when workflow is loaded
@@ -72,6 +62,12 @@
$: $layoutState.isMenuOpen = showMenu;
$: if ($layoutState.root) {
root = $layoutState.root
} else {
root = null;
}
async function onRightClick(e) {
if ($uiState.uiEditMode === "disabled")
return;
@@ -92,7 +88,7 @@
</script>
<div id="comfy-ui-panes" on:contextmenu={onRightClick}>
<WidgetContainer bind:dragItem={$layoutState.root} classes={["root-container"]} />
<WidgetContainer bind:dragItem={root} classes={["root-container"]} />
</div>
{#if showMenu}