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

@@ -15,6 +15,7 @@
"build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src" "build:css": "pollen -c gradio/js/theme/src/pollen.config.cjs && mv src/pollen.css node_modules/@gradio/theme/src"
}, },
"devDependencies": { "devDependencies": {
"@zerodevx/svelte-toast": "^0.9.3",
"eslint": "^8.37.0", "eslint": "^8.37.0",
"eslint-config-prettier": "^8.8.0", "eslint-config-prettier": "^8.8.0",
"eslint-plugin-svelte3": "^4.0.0", "eslint-plugin-svelte3": "^4.0.0",

102
pnpm-lock.yaml generated
View File

@@ -63,7 +63,7 @@ importers:
version: 1.2.1 version: 1.2.1
svelte-preprocess: svelte-preprocess:
specifier: ^5.0.3 specifier: ^5.0.3
version: 5.0.3(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) version: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
svelte-select: svelte-select:
specifier: ^5.5.3 specifier: ^5.5.3
version: 5.5.3 version: 5.5.3
@@ -72,7 +72,7 @@ importers:
version: 0.7.13(svelte@3.58.0) version: 0.7.13(svelte@3.58.0)
tailwindcss: tailwindcss:
specifier: ^3.3.1 specifier: ^3.3.1
version: 3.3.1(postcss@8.4.21) version: 3.3.1
typed-emitter: typed-emitter:
specifier: github:andywer/typed-emitter specifier: github:andywer/typed-emitter
version: github.com/andywer/typed-emitter/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4 version: github.com/andywer/typed-emitter/9a139b6fa0ec6b0db6141b5b756b784e4f7ef4e4
@@ -80,6 +80,9 @@ importers:
specifier: ^1.0.5 specifier: ^1.0.5
version: 1.0.5(vite@4.3.1) version: 1.0.5(vite@4.3.1)
devDependencies: devDependencies:
'@zerodevx/svelte-toast':
specifier: ^0.9.3
version: 0.9.3(svelte@3.58.0)
eslint: eslint:
specifier: ^8.37.0 specifier: ^8.37.0
version: 8.37.0 version: 8.37.0
@@ -103,7 +106,7 @@ importers:
version: 3.58.0 version: 3.58.0
svelte-check: svelte-check:
specifier: ^3.2.0 specifier: ^3.2.0
version: 3.2.0(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0) version: 3.2.0(sass@1.61.0)(svelte@3.58.0)
svelte-dnd-action: svelte-dnd-action:
specifier: ^0.9.22 specifier: ^0.9.22
version: 0.9.22(svelte@3.58.0) version: 0.9.22(svelte@3.58.0)
@@ -112,7 +115,7 @@ importers:
version: 5.0.3 version: 5.0.3
vite: vite:
specifier: ^4.3.1 specifier: ^4.3.1
version: 4.3.1(@types/node@18.16.0)(sass@1.61.0) version: 4.3.1(sass@1.61.0)
vite-tsconfig-paths: vite-tsconfig-paths:
specifier: ^4.0.8 specifier: ^4.0.8
version: 4.0.8(typescript@5.0.3)(vite@4.3.1) version: 4.0.8(typescript@5.0.3)(vite@4.3.1)
@@ -2035,7 +2038,7 @@ packages:
magic-string: 0.30.0 magic-string: 0.30.0
svelte: 3.58.0 svelte: 3.58.0
svelte-hmr: 0.15.1(svelte@3.58.0) svelte-hmr: 0.15.1(svelte@3.58.0)
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0) vite: 4.3.1(sass@1.61.0)
vitefu: 0.2.4(vite@4.3.1) vitefu: 0.2.4(vite@4.3.1)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
@@ -2178,6 +2181,7 @@ packages:
/@types/node@18.16.0: /@types/node@18.16.0:
resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==} resolution: {integrity: sha512-BsAaKhB+7X+H4GnSjGhJG9Qi8Tw+inU9nJDwmD5CgOmBLEI6ArdhikpLX7DjbjDRDTbqZzU2LSQNZg8WGPiSZQ==}
dev: true
/@types/node@8.10.66: /@types/node@8.10.66:
resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==} resolution: {integrity: sha512-tktOkFUA4kXx2hhhrB8bIFb5TbwzS4uOhKEmwiD+NoiL0qtP2OQ9mFldbgD4dV1djrlBYP6eBuQZiWjuHUpqFw==}
@@ -2281,6 +2285,14 @@ packages:
eslint-visitor-keys: 3.4.0 eslint-visitor-keys: 3.4.0
dev: true dev: true
/@zerodevx/svelte-toast@0.9.3(svelte@3.58.0):
resolution: {integrity: sha512-VPKWR4A9y01fyXRscu9HiTj7tV2hFrpRKZvGwMmaPXfHIXR1D9+NNsz0HXcQ7qZ0C5UaHS3n9uNtPtIcAXT7RQ==}
peerDependencies:
svelte: ^3.57.0
dependencies:
svelte: 3.58.0
dev: true
/acorn-jsx@5.3.2(acorn@8.8.2): /acorn-jsx@5.3.2(acorn@8.8.2):
resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==}
peerDependencies: peerDependencies:
@@ -5911,7 +5923,7 @@ packages:
- sugarss - sugarss
dev: true dev: true
/svelte-check@3.2.0(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0): /svelte-check@3.2.0(sass@1.61.0)(svelte@3.58.0):
resolution: {integrity: sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==} resolution: {integrity: sha512-6ZnscN8dHEN5Eq5LgIzjj07W9nc9myyBH+diXsUAuiY/3rt0l65/LCIQYlIuoFEjp2F1NhXqZiJwV9omPj9tMw==}
hasBin: true hasBin: true
peerDependencies: peerDependencies:
@@ -5924,7 +5936,7 @@ packages:
picocolors: 1.0.0 picocolors: 1.0.0
sade: 1.8.1 sade: 1.8.1
svelte: 3.58.0 svelte: 3.58.0
svelte-preprocess: 5.0.3(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3) svelte-preprocess: 5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3)
typescript: 5.0.3 typescript: 5.0.3
transitivePeerDependencies: transitivePeerDependencies:
- '@babel/core' - '@babel/core'
@@ -6084,7 +6096,7 @@ packages:
typescript: 5.0.3 typescript: 5.0.3
dev: true dev: true
/svelte-preprocess@5.0.3(postcss@8.4.21)(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3): /svelte-preprocess@5.0.3(sass@1.61.0)(svelte@3.58.0)(typescript@5.0.3):
resolution: {integrity: sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==} resolution: {integrity: sha512-GrHF1rusdJVbOZOwgPWtpqmaexkydznKzy5qIC2FabgpFyKN57bjMUUUqPRfbBXK5igiEWn1uO/DXsa2vJ5VHA==}
engines: {node: '>= 14.10.0'} engines: {node: '>= 14.10.0'}
requiresBuild: true requiresBuild: true
@@ -6125,7 +6137,6 @@ packages:
'@types/pug': 2.0.6 '@types/pug': 2.0.6
detect-indent: 6.1.0 detect-indent: 6.1.0
magic-string: 0.27.0 magic-string: 0.27.0
postcss: 8.4.21
sass: 1.61.0 sass: 1.61.0
sorcery: 0.11.0 sorcery: 0.11.0
strip-indent: 3.0.0 strip-indent: 3.0.0
@@ -6190,6 +6201,39 @@ packages:
get-port: 3.2.0 get-port: 3.2.0
dev: false dev: false
/tailwindcss@3.3.1:
resolution: {integrity: sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==}
engines: {node: '>=12.13.0'}
hasBin: true
dependencies:
arg: 5.0.2
chokidar: 3.5.3
color-name: 1.1.4
didyoumean: 1.2.2
dlv: 1.1.3
fast-glob: 3.2.12
glob-parent: 6.0.2
is-glob: 4.0.3
jiti: 1.18.2
lilconfig: 2.1.0
micromatch: 4.0.5
normalize-path: 3.0.0
object-hash: 3.0.0
picocolors: 1.0.0
postcss: 8.4.21
postcss-import: 14.1.0(postcss@8.4.21)
postcss-js: 4.0.1(postcss@8.4.21)
postcss-load-config: 3.1.4(postcss@8.4.21)
postcss-nested: 6.0.0(postcss@8.4.21)
postcss-selector-parser: 6.0.11
postcss-value-parser: 4.2.0
quick-lru: 5.1.1
resolve: 1.22.2
sucrase: 3.32.0
transitivePeerDependencies:
- ts-node
dev: false
/tailwindcss@3.3.1(postcss@8.4.21): /tailwindcss@3.3.1(postcss@8.4.21):
resolution: {integrity: sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==} resolution: {integrity: sha512-Vkiouc41d4CEq0ujXl6oiGFQ7bA3WEhUZdTgXAhtKxSy49OmKs8rEfQmupsfF0IGW8fv2iQkp1EVUuapCFrZ9g==}
engines: {node: '>=12.13.0'} engines: {node: '>=12.13.0'}
@@ -6223,6 +6267,7 @@ packages:
sucrase: 3.32.0 sucrase: 3.32.0
transitivePeerDependencies: transitivePeerDependencies:
- ts-node - ts-node
dev: true
/test-exclude@6.0.0: /test-exclude@6.0.0:
resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==} resolution: {integrity: sha512-cAGWPIyOHU6zlmg88jwm7VRyXnMN7iV68OGAbYDk/Mh/xC/pzVPlQtY6ngoIH/5/tciuhGfvESU8GrHrcxD56w==}
@@ -6961,7 +7006,7 @@ packages:
dependencies: dependencies:
picocolors: 1.0.0 picocolors: 1.0.0
picomatch: 2.3.1 picomatch: 2.3.1
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0) vite: 4.3.1(sass@1.61.0)
dev: false dev: false
/vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.1): /vite-tsconfig-paths@4.0.8(typescript@5.0.3)(vite@4.3.1):
@@ -6975,7 +7020,7 @@ packages:
debug: 4.3.4 debug: 4.3.4
globrex: 0.1.2 globrex: 0.1.2
tsconfck: 2.1.1(typescript@5.0.3) tsconfck: 2.1.1(typescript@5.0.3)
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0) vite: 4.3.1(sass@1.61.0)
transitivePeerDependencies: transitivePeerDependencies:
- supports-color - supports-color
- typescript - typescript
@@ -7071,6 +7116,39 @@ packages:
sass: 1.61.0 sass: 1.61.0
optionalDependencies: optionalDependencies:
fsevents: 2.3.2 fsevents: 2.3.2
dev: true
/vite@4.3.1(sass@1.61.0):
resolution: {integrity: sha512-EPmfPLAI79Z/RofuMvkIS0Yr091T2ReUoXQqc5ppBX/sjFRhHKiPPF/R46cTdoci/XgeQpB23diiJxq5w30vdg==}
engines: {node: ^14.18.0 || >=16.0.0}
hasBin: true
peerDependencies:
'@types/node': '>= 14'
less: '*'
sass: '*'
stylus: '*'
sugarss: '*'
terser: ^5.4.0
peerDependenciesMeta:
'@types/node':
optional: true
less:
optional: true
sass:
optional: true
stylus:
optional: true
sugarss:
optional: true
terser:
optional: true
dependencies:
esbuild: 0.17.18
postcss: 8.4.21
rollup: 3.21.0
sass: 1.61.0
optionalDependencies:
fsevents: 2.3.2
/vitefu@0.2.4(vite@4.3.1): /vitefu@0.2.4(vite@4.3.1):
resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==} resolution: {integrity: sha512-fanAXjSaf9xXtOOeno8wZXIhgia+CZury481LsDaV++lSvcU2R9Ch2bPh3PYFyoHW+w9LqAeYRISVQjUIew14g==}
@@ -7080,7 +7158,7 @@ packages:
vite: vite:
optional: true optional: true
dependencies: dependencies:
vite: 4.3.1(@types/node@18.16.0)(sass@1.61.0) vite: 4.3.1(sass@1.61.0)
/vitest@0.25.8(sass@1.61.0): /vitest@0.25.8(sass@1.61.0):
resolution: {integrity: sha512-X75TApG2wZTJn299E/TIYevr4E9/nBo1sUtZzn0Ci5oK8qnpZAZyhwg0qCeMSakGIWtc6oRwcQFyFfW14aOFWg==} resolution: {integrity: sha512-X75TApG2wZTJn299E/TIYevr4E9/nBo1sUtZzn0Ci5oK8qnpZAZyhwg0qCeMSakGIWtc6oRwcQFyFfW14aOFWg==}

View File

@@ -3,6 +3,11 @@ import type ComfyApp from "./components/ComfyApp";
import queueState from "./stores/queueState"; import queueState from "./stores/queueState";
import { get } from "svelte/store"; import { get } from "svelte/store";
export type SerializedGraphCanvasState = {
offset: Vector2,
scale: number
}
export default class ComfyGraphCanvas extends LGraphCanvas { export default class ComfyGraphCanvas extends LGraphCanvas {
app: ComfyApp app: ComfyApp
@@ -20,6 +25,23 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
this.app = app; this.app = app;
} }
serialize(): SerializedGraphCanvasState {
return {
offset: this.ds.offset,
scale: this.ds.scale
}
}
deserialize(data: SerializedGraphCanvasState) {
this.ds.offset = data.offset;
this.ds.scale = data.scale;
}
recenter() {
this.ds.reset();
this.setDirty(true, true)
}
override drawNodeShape( override drawNodeShape(
node: LGraphNode, node: LGraphNode,
ctx: CanvasRenderingContext2D, ctx: CanvasRenderingContext2D,

View File

@@ -9,8 +9,8 @@
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";
import { download } from "$lib/utils"
import type { ComfyAPIStatus } from "$lib/api"; import type { ComfyAPIStatus } from "$lib/api";
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
import { LGraph } from "@litegraph-ts/core"; import { LGraph } from "@litegraph-ts/core";
import LightboxModal from "./LightboxModal.svelte"; import LightboxModal from "./LightboxModal.svelte";
@@ -19,11 +19,20 @@
let app: ComfyApp = undefined; let app: ComfyApp = undefined;
let imageViewer: ImageViewer; let imageViewer: ImageViewer;
let uiPane: ComfyUIPane = undefined;
let queue: ComfyQueue = undefined; let queue: ComfyQueue = undefined;
let mainElem: HTMLDivElement; let mainElem: HTMLDivElement;
let uiPane: ComfyUIPane = undefined;
let containerElem: HTMLDivElement; 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) { function refreshView(event?: Event) {
clearTimeout(resizeTimeout); 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 { function doSave(): void {
if (!app?.lGraph) if (!app?.lGraph)
return; return;
const date = new Date(); app.saveStateToLocalStorage();
const formattedDate = date.toISOString().replace(/:/g, '-').replace(/\.\d{3}/g, '').replace('T', '_').replace("Z", ""); 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 () => { onMount(async () => {
app = new ComfyApp(); app = new ComfyApp();
if (debugLayout) {
layoutState.subscribe(s => {
console.warn("UPDATESTATE", s)
})
}
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("cleared", layoutState.clear);
app.eventBus.on("autosave", doAutosave);
app.eventBus.on("restored", doRestore);
app.api.addEventListener("status", (ev: CustomEvent) => { app.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIStatus); queueState.statusUpdated(ev.detail as ComfyAPIStatus);
@@ -117,7 +132,7 @@
}) })
</script> </script>
<div id="main" bind:this={mainElem}> <div id="main">
<div id="dropzone" class="dropzone"></div> <div id="dropzone" class="dropzone"></div>
<div id="container" bind:this={containerElem}> <div id="container" bind:this={containerElem}>
<Splitpanes theme="comfy" on:resize={refreshView}> <Splitpanes theme="comfy" on:resize={refreshView}>
@@ -153,6 +168,12 @@
<Button variant="secondary" on:click={doSave}> <Button variant="secondary" on:click={doSave}>
Save Save
</Button> </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="Lock Nodes" bind:value={$uiState.nodesLocked}/>
<Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/> <Checkbox label="Disable Interaction" bind:value={$uiState.graphLocked}/>
<label for="enable-ui-editing">Enable UI Editing</label> <label for="enable-ui-editing">Enable UI Editing</label>
@@ -164,6 +185,8 @@
<LightboxModal /> <LightboxModal />
</div> </div>
<SvelteToast options={toastOptions} />
<style lang="scss"> <style lang="scss">
#container { #container {
height: calc(100vh - 60px); 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 type { LConnectionKind, INodeSlot } from "@litegraph-ts/core";
import ComfyAPI from "$lib/api" import ComfyAPI from "$lib/api"
import { ComfyWidgets } from "$lib/widgets" import { ComfyWidgets } from "$lib/widgets"
@@ -10,7 +10,7 @@ import type TypedEmitter from "typed-emitter";
// Import nodes // Import nodes
import "@litegraph-ts/nodes-basic" import "@litegraph-ts/nodes-basic"
import * as nodes from "$lib/nodes/index" 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 type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import * as widgets from "$lib/widgets/index" import * as widgets from "$lib/widgets/index"
import type ComfyWidget from "$lib/widgets/ComfyWidget"; 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 type { SerializedLayoutState } from "$lib/stores/layoutState";
import layoutState from "$lib/stores/layoutState"; import layoutState from "$lib/stores/layoutState";
export const COMFYBOX_SERIAL_VERSION = 1;
LiteGraph.catch_exceptions = false; LiteGraph.catch_exceptions = false;
if (typeof window !== "undefined") { if (typeof window !== "undefined") {
@@ -34,7 +36,8 @@ export type SerializedAppState = {
createdBy: "ComfyBox", createdBy: "ComfyBox",
version: number, version: number,
workflow: SerializedLGraph, workflow: SerializedLGraph,
layout: SerializedLayoutState layout: SerializedLayoutState,
canvas: SerializedGraphCanvasState
} }
type ComfyAppEvents = { type ComfyAppEvents = {
@@ -45,7 +48,6 @@ type ComfyAppEvents = {
cleared: () => void cleared: () => void
beforeChange: (graph: LGraph, param: any) => void beforeChange: (graph: LGraph, param: any) => void
afterChange: (graph: LGraph, param: any) => void afterChange: (graph: LGraph, param: any) => void
autosave: (graph: LGraph) => void
restored: (workflow: SerializedAppState) => void restored: (workflow: SerializedAppState) => void
} }
@@ -60,7 +62,7 @@ export default class ComfyApp {
canvasEl: HTMLCanvasElement | null = null; canvasEl: HTMLCanvasElement | null = null;
canvasCtx: CanvasRenderingContext2D | null = null; canvasCtx: CanvasRenderingContext2D | null = null;
lGraph: LGraph | null = null; lGraph: LGraph | null = null;
lCanvas: LGraphCanvas | null = null; lCanvas: ComfyGraphCanvas | null = null;
dropZone: HTMLElement | null = null; dropZone: HTMLElement | null = null;
nodeOutputs: Record<string, any> = {}; nodeOutputs: Record<string, any> = {};
eventBus: TypedEmitter<ComfyAppEvents> = new EventEmitter() as TypedEmitter<ComfyAppEvents>; eventBus: TypedEmitter<ComfyAppEvents> = new EventEmitter() as TypedEmitter<ComfyAppEvents>;
@@ -102,9 +104,8 @@ export default class ComfyApp {
try { try {
const json = localStorage.getItem("workflow"); const json = localStorage.getItem("workflow");
if (json) { if (json) {
const workflow = JSON.parse(json) as SerializedAppState; const state = JSON.parse(json) as SerializedAppState;
this.loadGraphData(workflow["workflow"]); this.deserialize(state)
this.eventBus.emit("restored", workflow);
restored = true; restored = true;
} }
} catch (err) { } catch (err) {
@@ -113,11 +114,11 @@ export default class ComfyApp {
// We failed to restore a workflow so load the default // We failed to restore a workflow so load the default
if (!restored) { if (!restored) {
this.loadGraphData(); this.initDefaultGraph();
} }
// Save current workflow automatically // Save current workflow automatically
setInterval(this.requestAutosave.bind(this), 15000); setInterval(this.saveStateToLocalStorage.bind(this), 1000);
this.addApiUpdateHandlers(); this.addApiUpdateHandlers();
this.addDropHandler(); this.addDropHandler();
@@ -181,8 +182,10 @@ export default class ComfyApp {
this.eventBus.emit("cleared"); this.eventBus.emit("cleared");
} }
private requestAutosave() { saveStateToLocalStorage() {
this.eventBus.emit("autosave", this.lGraph); const savedWorkflow = this.serialize();
const json = JSON.stringify(savedWorkflow);
localStorage.setItem("workflow", json)
} }
private addGraphLifecycleHooks() { private addGraphLifecycleHooks() {
@@ -305,20 +308,6 @@ export default class ComfyApp {
// await this.#invokeExtensionsAsync("registerCustomNodes"); // 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() { private showDropZone() {
if (this.dropZone) if (this.dropZone)
this.dropZone.style.display = "block"; 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 * Adds a handler on paste that extracts and loads workflows from pasted JSON data
*/ */
private addPasteHandler() { private addPasteHandler() {
document.addEventListener("paste", (e) => { // document.addEventListener("paste", (e) => {
let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain"); // let data = (e.clipboardData || (window as any).clipboardData).getData("text/plain");
let workflow; // let workflow;
try { // try {
data = data.slice(data.indexOf("{")); // data = data.slice(data.indexOf("{"));
workflow = JSON.parse(data); // workflow = JSON.parse(data);
} catch (err) { // } catch (err) {
try { // try {
data = data.slice(data.indexOf("workflow\n")); // data = data.slice(data.indexOf("workflow\n"));
data = data.slice(data.indexOf("{")); // data = data.slice(data.indexOf("{"));
workflow = JSON.parse(data); // workflow = JSON.parse(data);
} catch (error) { } // } catch (error) { }
} // }
if (workflow && workflow.version && workflow.nodes && workflow.extra) { // if (workflow && workflow.version && workflow.nodes && workflow.extra) {
this.loadGraphData(workflow); // 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 * Populates the graph with the specified workflow data
* @param {*} graphData A serialized graph object * @param {*} graphData A serialized graph object
*/ */
loadGraphData(graphData?: SerializedLGraph) { loadGraphData(graphData: SerializedLGraph) {
this.clean(); this.clean();
if (!graphData) {
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
for (let n of graphData.nodes) { for (let n of graphData.nodes) {
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader"; 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 * Converts the current graph workflow for sending to the API
* @returns The workflow and node links * @returns The workflow and node links

View File

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

View File

@@ -94,15 +94,14 @@ type LayoutStateOps = {
updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[], updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
nodeAdded: (node: LGraphNode) => void, nodeAdded: (node: LGraphNode) => void,
nodeRemoved: (node: LGraphNode) => void, nodeRemoved: (node: LGraphNode) => void,
configureFinished: (graph: LGraph) => void,
groupItems: (dragItems: IDragItem[]) => ContainerLayout, groupItems: (dragItems: IDragItem[]) => ContainerLayout,
ungroup: (container: ContainerLayout) => void, ungroup: (container: ContainerLayout) => void,
getCurrentSelection: () => IDragItem[], getCurrentSelection: () => IDragItem[],
findLayoutForNode: (nodeId: number) => IDragItem | null; findLayoutForNode: (nodeId: number) => IDragItem | null;
clear: (state?: Partial<LayoutState>) => void,
serialize: () => SerializedLayoutState, serialize: () => SerializedLayoutState,
deserialize: (data: SerializedLayoutState, graph: LGraph) => void, deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
resetLayout: () => void, initDefaultLayout: () => void,
onStartConfigure: () => void
} }
export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps; export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps;
@@ -114,7 +113,6 @@ const store: Writable<LayoutState> = writable({
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true isConfiguring: true
}) })
addContainer(null, { direction: "horizontal", showTitle: false });
function findDefaultContainerForInsertion(): ContainerLayout | null { function findDefaultContainerForInsertion(): ContainerLayout | null {
const state = get(store); const state = get(store);
@@ -154,6 +152,7 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
if (parent) { if (parent) {
moveItem(dragItem, parent) moveItem(dragItem, parent)
} }
console.debug("[layoutState] addContainer", state)
store.set(state) store.set(state)
return dragItem; return dragItem;
} }
@@ -176,6 +175,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: null }; const entry: DragItemEntry = { dragItem, children: [], parent: null };
state.allItems[dragItem.id] = entry; state.allItems[dragItem.id] = entry;
console.debug("[layoutState] addWidget", state)
moveItem(dragItem, parent) moveItem(dragItem, parent)
return dragItem; return dragItem;
} }
@@ -250,28 +250,6 @@ function nodeRemoved(node: LGraphNode) {
store.set(state) store.set(state)
} }
function configureFinished(graph: LGraph) {
const id = 0;
clear({ isConfiguring: false })
const root = addContainer(null, { direction: "horizontal", showTitle: false });
const left = addContainer(root, { direction: "vertical", showTitle: false });
const right = addContainer(root, { direction: "vertical", showTitle: false });
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)
}
console.debug("[layoutState] configureFinished", state)
}
function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) { function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
const state = get(store) const state = get(store)
const entry = state.allItems[target.id] const entry = state.allItems[target.id]
@@ -375,20 +353,25 @@ function findLayoutForNode(nodeId: number): WidgetLayout | null {
return null; return null;
} }
function clear(state: Partial<LayoutState> = {}) { function initDefaultLayout() {
store.set({ store.set({
root: null, root: null,
allItems: {}, allItems: {},
currentId: 0, currentId: 0,
currentSelection: [], currentSelection: [],
isMenuOpen: false, isMenuOpen: false,
isConfiguring: true, isConfiguring: false
...state
}) })
}
function resetLayout() { const root = addContainer(null, { direction: "horizontal", showTitle: false });
// TODO const left = addContainer(root, { direction: "vertical", showTitle: false });
const right = addContainer(root, { direction: "vertical", showTitle: false });
const state = get(store)
state.root = root;
store.set(state)
console.debug("[layoutState] initDefault", state)
} }
export type SerializedLayoutState = { export type SerializedLayoutState = {
@@ -436,8 +419,6 @@ function serialize(): SerializedLayoutState {
} }
function deserialize(data: SerializedLayoutState, graph: LGraph) { function deserialize(data: SerializedLayoutState, graph: LGraph) {
clear();
const allItems: Record<DragItemID, DragItemEntry> = {} const allItems: Record<DragItemID, DragItemEntry> = {}
for (const pair of Object.entries(data.allItems)) { for (const pair of Object.entries(data.allItems)) {
const [id, entry] = pair; const [id, entry] = pair;
@@ -469,16 +450,28 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
} }
} }
let root = null; let root: IDragItem = null;
if (data.root) if (data.root)
root = allItems[data.root] root = allItems[data.root].dragItem
const state = get(store) const state: LayoutState = {
store.set({
...state,
root, root,
allItems, allItems,
currentId: data.currentId, currentId: data.currentId,
currentSelection: [],
isMenuOpen: false,
isConfiguring: false
}
console.debug("[layoutState] deserialize", data, state)
store.set(state)
}
function onStartConfigure() {
store.update(s => {
s.isConfiguring = true;
return s
}) })
} }
@@ -491,13 +484,12 @@ const layoutStateStore: WritableLayoutStateStore =
updateChildren, updateChildren,
nodeAdded, nodeAdded,
nodeRemoved, nodeRemoved,
configureFinished,
getCurrentSelection, getCurrentSelection,
groupItems, groupItems,
findLayoutForNode, findLayoutForNode,
ungroup, ungroup,
clear, initDefaultLayout,
resetLayout, onStartConfigure,
serialize, serialize,
deserialize deserialize
} }

View File

@@ -16,24 +16,11 @@
let app: ComfyApp | null = null; let app: ComfyApp | null = null;
let serializedPaneOrder = {};
function doAutosave(graph: LGraph): void {
const savedWorkflow = app.serialize();
localStorage.setItem("workflow", JSON.stringify(savedWorkflow))
}
function doRestore(workflow: SerializedAppState) {
serializedPaneOrder = workflow.panes;
}
onMount(async () => { onMount(async () => {
if (app) if (app)
return return
app = $uiState.app = new ComfyApp(); app = $uiState.app = new ComfyApp();
app.eventBus.on("autosave", doAutosave);
app.eventBus.on("restored", doRestore);
app.api.addEventListener("status", (ev: CustomEvent) => { app.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIStatus); queueState.statusUpdated(ev.detail as ComfyAPIStatus);