From ee97bd43bc7c9d3a8d588a4a3c4e51eeb0244ade Mon Sep 17 00:00:00 2001 From: space-nuko <24979496+space-nuko@users.noreply.github.com> Date: Sat, 20 May 2023 22:20:36 -0500 Subject: [PATCH] Workflow creation/modified state --- src/lib/ComfyGraph.ts | 34 +++++++++++-- src/lib/ComfyGraphCanvas.ts | 27 ++++++---- src/lib/components/ComfyApp.ts | 46 ++++++++++------- src/lib/components/ComfyProperties.svelte | 8 +++ src/lib/components/ComfyWorkflowsView.svelte | 52 +++++++++++++++++--- src/lib/components/WidgetContainer.svelte | 16 +++++- src/lib/stores/layoutStates.ts | 30 ++++++++--- src/lib/stores/workflowState.ts | 12 ++++- src/lib/utils.ts | 2 + 9 files changed, 179 insertions(+), 48 deletions(-) diff --git a/src/lib/ComfyGraph.ts b/src/lib/ComfyGraph.ts index 10ea7dc..776051e 100644 --- a/src/lib/ComfyGraph.ts +++ b/src/lib/ComfyGraph.ts @@ -12,6 +12,8 @@ import selectionState from "./stores/selectionState"; import type { WritableLayoutStateStore } from "./stores/layoutStates"; import type { WorkflowInstID } from "./components/ComfyApp"; import layoutStates from "./stores/layoutStates"; +import type { ComfyWorkflow } from "./stores/workflowState"; +import workflowState from "./stores/workflowState"; type ComfyGraphEvents = { configured: (graph: LGraph) => void @@ -29,6 +31,12 @@ export default class ComfyGraph extends LGraph { workflowID: WorkflowInstID | null = null; + get workflow(): ComfyWorkflow | null { + if (this.workflowID == null) + return null; + return workflowState.getWorkflow(this.workflowID) + } + constructor(workflowID?: WorkflowInstID) { super(); this.workflowID = workflowID; @@ -39,6 +47,9 @@ export default class ComfyGraph extends LGraph { } override onBeforeChange(graph: LGraph, info: any) { + if (this.workflow != null) + this.workflow.notifyModified() + console.debug("BeforeChange", info); } @@ -70,6 +81,9 @@ export default class ComfyGraph extends LGraph { this.doAddNode(node, layoutState, options); } + if (this.workflow != null) + this.workflow.notifyModified() + this.eventBus.emit("nodeAdded", node); } @@ -80,9 +94,9 @@ export default class ComfyGraph extends LGraph { layoutState.nodeAdded(node, options) // All nodes whether they come from base litegraph or ComfyBox should - // have tags added to them. Can't override serialization for existing - // node types to add `tags` as a new field so putting it in properties - // is better. + // have tags added to them. Can't override serialization for litegraph's + // base node types to add `tags` as a new field so putting it in + // properties is better. if (node.properties.tags == null) node.properties.tags = [] @@ -161,6 +175,9 @@ export default class ComfyGraph extends LGraph { } } // ************** RECURSION ALERT ! ************** + + if (this.workflow != null) + this.workflow.notifyModified() } override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) { @@ -182,10 +199,21 @@ export default class ComfyGraph extends LGraph { } } + if (this.workflow != null) + this.workflow.notifyModified() + this.eventBus.emit("nodeRemoved", node); } + override onInputsOutputsChange() { + if (this.workflow != null) + this.workflow.notifyModified() + } + override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) { + if (this.workflow != null) + this.workflow.notifyModified() + // console.debug("ConnectionChange", node); this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot); } diff --git a/src/lib/ComfyGraphCanvas.ts b/src/lib/ComfyGraphCanvas.ts index 00cc87b..4070323 100644 --- a/src/lib/ComfyGraphCanvas.ts +++ b/src/lib/ComfyGraphCanvas.ts @@ -8,6 +8,7 @@ import { ComfyReroute } from "./nodes"; import type { Progress } from "./components/ComfyApp"; import selectionState from "./stores/selectionState"; import type ComfyGraph from "./ComfyGraph"; +import layoutStates from "./stores/layoutStates"; export type SerializedGraphCanvasState = { offset: Vector2, @@ -286,11 +287,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas { selectionState.update(ss => { ss.currentSelectionNodes = Object.values(nodes) ss.currentSelection = [] - const ls = get(this.comfyGraph.layoutState) - for (const node of ss.currentSelectionNodes) { - const widget = ls.allItemsByNode[node.id] - if (widget) - ss.currentSelection.push(widget.dragItem.id) + const layoutState = layoutStates.getLayoutByGraph(this.graph); + if (layoutState) { + const ls = get(layoutState) + for (const node of ss.currentSelectionNodes) { + const widget = ls.allItemsByNode[node.id] + if (widget) + ss.currentSelection.push(widget.dragItem.id) + } } return ss }) @@ -303,11 +307,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas { ss.currentHoveredNodes.add(node.id) } ss.currentHovered.clear() - const ls = get(this.comfyGraph.layoutState) - for (const nodeID of ss.currentHoveredNodes) { - const widget = ls.allItemsByNode[nodeID] - if (widget) - ss.currentHovered.add(widget.dragItem.id) + const layoutState = layoutStates.getLayoutByGraph(this.graph); + if (layoutState) { + const ls = get(layoutState) + for (const nodeID of ss.currentHoveredNodes) { + const widget = ls.allItemsByNode[nodeID] + if (widget) + ss.currentHovered.add(widget.dragItem.id) + } } return ss }) diff --git a/src/lib/components/ComfyApp.ts b/src/lib/components/ComfyApp.ts index 31fa2f1..4c06ad9 100644 --- a/src/lib/components/ComfyApp.ts +++ b/src/lib/components/ComfyApp.ts @@ -186,12 +186,7 @@ export default class ComfyApp { // Load previous workflow let restored = false; try { - const json = localStorage.getItem("workflow"); - if (json) { - const state = JSON.parse(json) as SerializedAppState; - await this.openWorkflow(state) - restored = true; - } + restored = await this.loadStateFromLocalStorage(); } catch (err) { console.error("Error loading previous workflow", err); notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null }) @@ -202,10 +197,6 @@ export default class ComfyApp { await this.initDefaultWorkflow(); } - workflowState.createNewWorkflow(this.lCanvas); - workflowState.createNewWorkflow(this.lCanvas); - workflowState.createNewWorkflow(this.lCanvas); - // Save current workflow automatically // setInterval(this.saveStateToLocalStorage.bind(this), 1000); @@ -256,17 +247,15 @@ export default class ComfyApp { } saveStateToLocalStorage() { - const workflow = workflowState.getActiveWorkflow(); - if (workflow == null) { - notify("No active workflow!", { type: "error" }) - return; - } - try { uiState.update(s => { s.isSavingToLocalStorage = true; return s; }) - const savedWorkflow = this.serialize(workflow); - const json = JSON.stringify(savedWorkflow); - localStorage.setItem("workflow", json) + const workflows = get(workflowState).openedWorkflows + const savedWorkflows = workflows.map(w => this.serialize(w)); + const json = JSON.stringify(savedWorkflows); + localStorage.setItem("workflows", json) + for (const workflow of workflows) + workflow.isModified = false; + workflowState.set(get(workflowState)); notify("Saved to local storage.") } catch (err) { @@ -277,6 +266,17 @@ export default class ComfyApp { } } + async loadStateFromLocalStorage(): Promise { + const json = localStorage.getItem("workflows"); + if (!json) { + return false + } + const workflows = JSON.parse(json) as SerializedAppState[]; + for (const workflow of workflows) + await this.openWorkflow(workflow) + return true; + } + static node_type_overrides: Record = {} static widget_type_overrides: Record = {} @@ -525,6 +525,11 @@ export default class ComfyApp { selectionState.clear(); } + createNewWorkflow(index: number) { + workflowState.createNewWorkflow(this.lCanvas, undefined, true); + selectionState.clear(); + } + closeWorkflow(index: number) { workflowState.closeWorkflow(this.lCanvas, index); selectionState.clear(); @@ -602,6 +607,9 @@ export default class ComfyApp { download(filename, json, "application/json") + workflow.isModified = false; + workflowState.set(get(workflowState)); + console.debug(jsonToJsObject(json)) } diff --git a/src/lib/components/ComfyProperties.svelte b/src/lib/components/ComfyProperties.svelte index feb628f..568a20a 100644 --- a/src/lib/components/ComfyProperties.svelte +++ b/src/lib/components/ComfyProperties.svelte @@ -170,6 +170,8 @@ if (spec.refreshPanelOnChange) { doRefreshPanel() } + + workflow.notifyModified() } function getProperty(node: LGraphNode, spec: AttributesSpec) { @@ -205,6 +207,8 @@ if (spec.refreshPanelOnChange) doRefreshPanel() + + workflow.notifyModified() } function getVar(node: LGraphNode, spec: AttributesSpec) { @@ -241,6 +245,8 @@ if (spec.refreshPanelOnChange) { doRefreshPanel() } + + workflow.notifyModified() } function getWorkflowAttribute(spec: AttributesSpec): any { @@ -275,6 +281,8 @@ // if (spec.refreshPanelOnChange) doRefreshPanel() + + workflow.notifyModified() } function doRefreshPanel() { diff --git a/src/lib/components/ComfyWorkflowsView.svelte b/src/lib/components/ComfyWorkflowsView.svelte index 2d9eaeb..cfbab07 100644 --- a/src/lib/components/ComfyWorkflowsView.svelte +++ b/src/lib/components/ComfyWorkflowsView.svelte @@ -150,9 +150,19 @@ } } - function closeWorkflow(event: Event, index: number) { + function createNewWorkflow() { + app.createNewWorkflow(); + } + + function closeWorkflow(event: Event, index: number, workflow: ComfyWorkflow) { event.preventDefault(); event.stopImmediatePropagation() + + if (workflow.isModified) { + if (!confirm("This workflow has unsaved changes. Are you sure you want to close it?")) + return; + } + app.closeWorkflow(index); } @@ -189,13 +199,22 @@ {/each} +
@@ -373,7 +392,7 @@ display: flex; flex-direction: row; justify-content: center; - gap: var(--size-2); + gap: var(--size-4); &:last-child { border-right: 1px solid var(--neutral-600); @@ -392,8 +411,10 @@ > .workflow-close-button { display:block; - width: 1.5rem; - height: 1.5rem; + width: 1.2rem; + height: 1.2rem; + font-size: 13px; + margin: auto; border-radius: 50%; opacity: 50%; background: var(--neutral-500); @@ -405,6 +426,25 @@ } } } + + .workflow-add-new-button { + background: var(--neutral-700); + filter: brightness(80%); + color: var(--neutral-500); + padding: 0.5rem 1rem; + border-top: 3px solid var(--neutral-600); + border-left: 1px solid var(--neutral-600); + + display: flex; + flex-direction: row; + justify-content: center; + gap: var(--size-4); + + &:hover { + filter: brightness(100%); + border-top-color: var(--neutral-600); + } + } } #bottombar { diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte index 4456b21..1389692 100644 --- a/src/lib/components/WidgetContainer.svelte +++ b/src/lib/components/WidgetContainer.svelte @@ -58,6 +58,14 @@ const title = widget.node.type.replace("/", "-").replace(".", "-") return `widget--${title}` } + + function _startDrag(e: MouseEvent | TouchEvent) { + startDrag(e, layoutState) + } + + function _stopDrag(e: MouseEvent | TouchEvent) { + stopDrag(e, layoutState) + } @@ -85,7 +93,13 @@
{/if} {#if showHandles || hovered} -
+
{/if} {/key} {/key} diff --git a/src/lib/stores/layoutStates.ts b/src/lib/stores/layoutStates.ts index 8762b55..78dd4a2 100644 --- a/src/lib/stores/layoutStates.ts +++ b/src/lib/stores/layoutStates.ts @@ -6,9 +6,9 @@ import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action'; import type { ComfyNodeID } from '$lib/api'; import { v4 as uuidv4 } from "uuid"; import type { ComfyWidgetNode } from '$lib/nodes/widgets'; -import type { ComfyWorkflow, WorkflowInstID } from '$lib/components/ComfyApp'; import type ComfyGraph from '$lib/ComfyGraph'; -import type { WorkflowAttributes } from './workflowState'; +import type { ComfyWorkflow, WorkflowAttributes, WorkflowInstID } from './workflowState'; +import workflowState from './workflowState'; function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode { return "svelteComponentType" in node @@ -670,6 +670,7 @@ type LayoutStateOps = { deserialize: (data: SerializedLayoutState, graph: LGraph) => void, initDefaultLayout: () => void, onStartConfigure: () => void + notifyWorkflowModified: () => void } export type SerializedLayoutState = { @@ -770,6 +771,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { console.debug("[layoutState] addContainer", state) store.set(state) + notifyWorkflowModified(); // runOnChangedForWidgetDefaults(dragItem) return dragItem; } @@ -801,6 +803,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { console.debug("[layoutState] addWidget", state) moveItem(dragItem, parent, index) + notifyWorkflowModified(); // runOnChangedForWidgetDefaults(dragItem) return dragItem; } @@ -834,6 +837,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { delete state.allItemsByNode[widget.node.id] } delete state.allItems[id] + notifyWorkflowModified(); } function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { @@ -946,6 +950,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { state.allItems[target.id].parent = toEntry.dragItem; console.debug("[layoutState] Move child", target, toEntry, index) + notifyWorkflowModified(); store.set(state) } @@ -976,6 +981,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { console.debug("[layoutState] Grouped", container, parent, state.allItems[container.id].children, index) + notifyWorkflowModified(); store.set(state) return container } @@ -1033,18 +1039,20 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { allItems: {}, allItemsByNode: {}, isMenuOpen: false, - isConfiguring: false, + isConfiguring: true, }) const root = addContainer(null, { direction: "horizontal", title: "" }); const left = addContainer(root, { direction: "vertical", title: "" }); const right = addContainer(root, { direction: "vertical", title: "" }); - const state = get(store) - state.root = root; - store.set(state) + store.update(s => { + s.root = root; + s.isConfiguring = false; + return s; + }) - console.debug("[layoutState] initDefault", state) + console.debug("[layoutState] initDefault") } function serialize(): SerializedLayoutState { @@ -1143,6 +1151,11 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { }) } + function notifyWorkflowModified() { + if (!get(store).isConfiguring) + workflow.notifyModified(); + } + const layoutStateStore: WritableLayoutStateStore = { ...store, @@ -1161,7 +1174,8 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore { initDefaultLayout, onStartConfigure, serialize, - deserialize + deserialize, + notifyWorkflowModified } layoutStates.update(s => { diff --git a/src/lib/stores/workflowState.ts b/src/lib/stores/workflowState.ts index 46d1bd6..3b6b2e3 100644 --- a/src/lib/stores/workflowState.ts +++ b/src/lib/stores/workflowState.ts @@ -69,7 +69,12 @@ export class ComfyWorkflow { /* * Global workflow attributes */ - attrs: WorkflowAttributes + attrs: WorkflowAttributes; + + /* + * True if an unsaved modification has been detected on this workflow + */ + isModified: boolean = false; get layout(): WritableLayoutStateStore | null { return layoutStates.getLayout(this.id) @@ -89,6 +94,11 @@ export class ComfyWorkflow { this.graph = new ComfyGraph(this.id); } + notifyModified() { + this.isModified = true; + store.set(get(store)); + } + start(key: string, canvas: ComfyGraphCanvas) { if (this.canvases[key] != null) throw new Error(`This workflow is already being displayed on canvas ${key}`) diff --git a/src/lib/utils.ts b/src/lib/utils.ts index 5cccbde..35c2162 100644 --- a/src/lib/utils.ts +++ b/src/lib/utils.ts @@ -79,9 +79,11 @@ export function startDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore layoutState.set(ls) selectionState.set(ss) + layoutState.notifyWorkflowModified(); }; export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) { + layoutState.notifyWorkflowModified(); }; export function graphToGraphVis(graph: LGraph): string {