Workflow creation/modified state

This commit is contained in:
space-nuko
2023-05-20 22:20:36 -05:00
parent d12b4ac03e
commit ee97bd43bc
9 changed files with 179 additions and 48 deletions

View File

@@ -12,6 +12,8 @@ import selectionState from "./stores/selectionState";
import type { WritableLayoutStateStore } from "./stores/layoutStates"; import type { WritableLayoutStateStore } from "./stores/layoutStates";
import type { WorkflowInstID } from "./components/ComfyApp"; import type { WorkflowInstID } from "./components/ComfyApp";
import layoutStates from "./stores/layoutStates"; import layoutStates from "./stores/layoutStates";
import type { ComfyWorkflow } from "./stores/workflowState";
import workflowState from "./stores/workflowState";
type ComfyGraphEvents = { type ComfyGraphEvents = {
configured: (graph: LGraph) => void configured: (graph: LGraph) => void
@@ -29,6 +31,12 @@ export default class ComfyGraph extends LGraph {
workflowID: WorkflowInstID | null = null; workflowID: WorkflowInstID | null = null;
get workflow(): ComfyWorkflow | null {
if (this.workflowID == null)
return null;
return workflowState.getWorkflow(this.workflowID)
}
constructor(workflowID?: WorkflowInstID) { constructor(workflowID?: WorkflowInstID) {
super(); super();
this.workflowID = workflowID; this.workflowID = workflowID;
@@ -39,6 +47,9 @@ export default class ComfyGraph extends LGraph {
} }
override onBeforeChange(graph: LGraph, info: any) { override onBeforeChange(graph: LGraph, info: any) {
if (this.workflow != null)
this.workflow.notifyModified()
console.debug("BeforeChange", info); console.debug("BeforeChange", info);
} }
@@ -70,6 +81,9 @@ export default class ComfyGraph extends LGraph {
this.doAddNode(node, layoutState, options); this.doAddNode(node, layoutState, options);
} }
if (this.workflow != null)
this.workflow.notifyModified()
this.eventBus.emit("nodeAdded", node); this.eventBus.emit("nodeAdded", node);
} }
@@ -80,9 +94,9 @@ export default class ComfyGraph extends LGraph {
layoutState.nodeAdded(node, options) layoutState.nodeAdded(node, options)
// All nodes whether they come from base litegraph or ComfyBox should // All nodes whether they come from base litegraph or ComfyBox should
// have tags added to them. Can't override serialization for existing // have tags added to them. Can't override serialization for litegraph's
// node types to add `tags` as a new field so putting it in properties // base node types to add `tags` as a new field so putting it in
// is better. // properties is better.
if (node.properties.tags == null) if (node.properties.tags == null)
node.properties.tags = [] node.properties.tags = []
@@ -161,6 +175,9 @@ export default class ComfyGraph extends LGraph {
} }
} }
// ************** RECURSION ALERT ! ************** // ************** RECURSION ALERT ! **************
if (this.workflow != null)
this.workflow.notifyModified()
} }
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) { 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); 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) { override onNodeConnectionChange(kind: LConnectionKind, node: LGraphNode, slot: SlotIndex, targetNode: LGraphNode, targetSlot: SlotIndex) {
if (this.workflow != null)
this.workflow.notifyModified()
// console.debug("ConnectionChange", node); // console.debug("ConnectionChange", node);
this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot); this.eventBus.emit("nodeConnectionChanged", kind, node, slot, targetNode, targetSlot);
} }

View File

@@ -8,6 +8,7 @@ import { ComfyReroute } from "./nodes";
import type { Progress } from "./components/ComfyApp"; import type { Progress } from "./components/ComfyApp";
import selectionState from "./stores/selectionState"; import selectionState from "./stores/selectionState";
import type ComfyGraph from "./ComfyGraph"; import type ComfyGraph from "./ComfyGraph";
import layoutStates from "./stores/layoutStates";
export type SerializedGraphCanvasState = { export type SerializedGraphCanvasState = {
offset: Vector2, offset: Vector2,
@@ -286,12 +287,15 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
selectionState.update(ss => { selectionState.update(ss => {
ss.currentSelectionNodes = Object.values(nodes) ss.currentSelectionNodes = Object.values(nodes)
ss.currentSelection = [] ss.currentSelection = []
const ls = get(this.comfyGraph.layoutState) const layoutState = layoutStates.getLayoutByGraph(this.graph);
if (layoutState) {
const ls = get(layoutState)
for (const node of ss.currentSelectionNodes) { for (const node of ss.currentSelectionNodes) {
const widget = ls.allItemsByNode[node.id] const widget = ls.allItemsByNode[node.id]
if (widget) if (widget)
ss.currentSelection.push(widget.dragItem.id) ss.currentSelection.push(widget.dragItem.id)
} }
}
return ss return ss
}) })
} }
@@ -303,12 +307,15 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
ss.currentHoveredNodes.add(node.id) ss.currentHoveredNodes.add(node.id)
} }
ss.currentHovered.clear() ss.currentHovered.clear()
const ls = get(this.comfyGraph.layoutState) const layoutState = layoutStates.getLayoutByGraph(this.graph);
if (layoutState) {
const ls = get(layoutState)
for (const nodeID of ss.currentHoveredNodes) { for (const nodeID of ss.currentHoveredNodes) {
const widget = ls.allItemsByNode[nodeID] const widget = ls.allItemsByNode[nodeID]
if (widget) if (widget)
ss.currentHovered.add(widget.dragItem.id) ss.currentHovered.add(widget.dragItem.id)
} }
}
return ss return ss
}) })
} }

View File

@@ -186,12 +186,7 @@ export default class ComfyApp {
// Load previous workflow // Load previous workflow
let restored = false; let restored = false;
try { try {
const json = localStorage.getItem("workflow"); restored = await this.loadStateFromLocalStorage();
if (json) {
const state = JSON.parse(json) as SerializedAppState;
await this.openWorkflow(state)
restored = true;
}
} catch (err) { } catch (err) {
console.error("Error loading previous workflow", err); console.error("Error loading previous workflow", err);
notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null }) notify(`Error loading previous workflow:\n${err}`, { type: "error", timeout: null })
@@ -202,10 +197,6 @@ export default class ComfyApp {
await this.initDefaultWorkflow(); await this.initDefaultWorkflow();
} }
workflowState.createNewWorkflow(this.lCanvas);
workflowState.createNewWorkflow(this.lCanvas);
workflowState.createNewWorkflow(this.lCanvas);
// Save current workflow automatically // Save current workflow automatically
// setInterval(this.saveStateToLocalStorage.bind(this), 1000); // setInterval(this.saveStateToLocalStorage.bind(this), 1000);
@@ -256,17 +247,15 @@ export default class ComfyApp {
} }
saveStateToLocalStorage() { saveStateToLocalStorage() {
const workflow = workflowState.getActiveWorkflow();
if (workflow == null) {
notify("No active workflow!", { type: "error" })
return;
}
try { try {
uiState.update(s => { s.isSavingToLocalStorage = true; return s; }) uiState.update(s => { s.isSavingToLocalStorage = true; return s; })
const savedWorkflow = this.serialize(workflow); const workflows = get(workflowState).openedWorkflows
const json = JSON.stringify(savedWorkflow); const savedWorkflows = workflows.map(w => this.serialize(w));
localStorage.setItem("workflow", json) 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.") notify("Saved to local storage.")
} }
catch (err) { catch (err) {
@@ -277,6 +266,17 @@ export default class ComfyApp {
} }
} }
async loadStateFromLocalStorage(): Promise<boolean> {
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<string, typeof ComfyBackendNode> = {} static node_type_overrides: Record<string, typeof ComfyBackendNode> = {}
static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {} static widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
@@ -525,6 +525,11 @@ export default class ComfyApp {
selectionState.clear(); selectionState.clear();
} }
createNewWorkflow(index: number) {
workflowState.createNewWorkflow(this.lCanvas, undefined, true);
selectionState.clear();
}
closeWorkflow(index: number) { closeWorkflow(index: number) {
workflowState.closeWorkflow(this.lCanvas, index); workflowState.closeWorkflow(this.lCanvas, index);
selectionState.clear(); selectionState.clear();
@@ -602,6 +607,9 @@ export default class ComfyApp {
download(filename, json, "application/json") download(filename, json, "application/json")
workflow.isModified = false;
workflowState.set(get(workflowState));
console.debug(jsonToJsObject(json)) console.debug(jsonToJsObject(json))
} }

View File

@@ -170,6 +170,8 @@
if (spec.refreshPanelOnChange) { if (spec.refreshPanelOnChange) {
doRefreshPanel() doRefreshPanel()
} }
workflow.notifyModified()
} }
function getProperty(node: LGraphNode, spec: AttributesSpec) { function getProperty(node: LGraphNode, spec: AttributesSpec) {
@@ -205,6 +207,8 @@
if (spec.refreshPanelOnChange) if (spec.refreshPanelOnChange)
doRefreshPanel() doRefreshPanel()
workflow.notifyModified()
} }
function getVar(node: LGraphNode, spec: AttributesSpec) { function getVar(node: LGraphNode, spec: AttributesSpec) {
@@ -241,6 +245,8 @@
if (spec.refreshPanelOnChange) { if (spec.refreshPanelOnChange) {
doRefreshPanel() doRefreshPanel()
} }
workflow.notifyModified()
} }
function getWorkflowAttribute(spec: AttributesSpec): any { function getWorkflowAttribute(spec: AttributesSpec): any {
@@ -275,6 +281,8 @@
// if (spec.refreshPanelOnChange) // if (spec.refreshPanelOnChange)
doRefreshPanel() doRefreshPanel()
workflow.notifyModified()
} }
function doRefreshPanel() { function doRefreshPanel() {

View File

@@ -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.preventDefault();
event.stopImmediatePropagation() event.stopImmediatePropagation()
if (workflow.isModified) {
if (!confirm("This workflow has unsaved changes. Are you sure you want to close it?"))
return;
}
app.closeWorkflow(index); app.closeWorkflow(index);
} }
</script> </script>
@@ -189,13 +199,22 @@
<button class="workflow-tab" <button class="workflow-tab"
class:selected={index === $workflowState.activeWorkflowIdx} class:selected={index === $workflowState.activeWorkflowIdx}
on:click={() => app.setActiveWorkflow(index)}> on:click={() => app.setActiveWorkflow(index)}>
<span class="workflow-tab-title">{workflow.attrs.title}</span> <span class="workflow-tab-title">
{workflow.attrs.title}
{#if workflow.isModified}
*
{/if}
</span>
<button class="workflow-close-button" <button class="workflow-close-button"
on:click={(e) => closeWorkflow(e, index)}> on:click={(e) => closeWorkflow(e, index, workflow)}>
</button> </button>
</button> </button>
{/each} {/each}
<button class="workflow-add-new-button"
on:click={createNewWorkflow}>
</button>
</div> </div>
<div id="bottombar"> <div id="bottombar">
<div class="bottombar-content"> <div class="bottombar-content">
@@ -373,7 +392,7 @@
display: flex; display: flex;
flex-direction: row; flex-direction: row;
justify-content: center; justify-content: center;
gap: var(--size-2); gap: var(--size-4);
&:last-child { &:last-child {
border-right: 1px solid var(--neutral-600); border-right: 1px solid var(--neutral-600);
@@ -392,8 +411,10 @@
> .workflow-close-button { > .workflow-close-button {
display:block; display:block;
width: 1.5rem; width: 1.2rem;
height: 1.5rem; height: 1.2rem;
font-size: 13px;
margin: auto;
border-radius: 50%; border-radius: 50%;
opacity: 50%; opacity: 50%;
background: var(--neutral-500); 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 { #bottombar {

View File

@@ -58,6 +58,14 @@
const title = widget.node.type.replace("/", "-").replace(".", "-") const title = widget.node.type.replace("/", "-").replace(".", "-")
return `widget--${title}` return `widget--${title}`
} }
function _startDrag(e: MouseEvent | TouchEvent) {
startDrag(e, layoutState)
}
function _stopDrag(e: MouseEvent | TouchEvent) {
stopDrag(e, layoutState)
}
</script> </script>
@@ -85,7 +93,13 @@
<div class="handle handle-hidden" class:hidden={!edit} /> <div class="handle handle-hidden" class:hidden={!edit} />
{/if} {/if}
{#if showHandles || hovered} {#if showHandles || hovered}
<div class="handle handle-widget" class:hovered data-drag-item-id={widget.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/> <div class="handle handle-widget"
class:hovered
data-drag-item-id={widget.id}
on:mousedown={_startDrag}
on:touchstart={_startDrag}
on:mouseup={_stopDrag}
on:touchend={_stopDrag}/>
{/if} {/if}
{/key} {/key}
{/key} {/key}

View File

@@ -6,9 +6,9 @@ import { SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
import type { ComfyNodeID } from '$lib/api'; import type { ComfyNodeID } from '$lib/api';
import { v4 as uuidv4 } from "uuid"; import { v4 as uuidv4 } from "uuid";
import type { ComfyWidgetNode } from '$lib/nodes/widgets'; import type { ComfyWidgetNode } from '$lib/nodes/widgets';
import type { ComfyWorkflow, WorkflowInstID } from '$lib/components/ComfyApp';
import type ComfyGraph from '$lib/ComfyGraph'; 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 { function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode {
return "svelteComponentType" in node return "svelteComponentType" in node
@@ -670,6 +670,7 @@ type LayoutStateOps = {
deserialize: (data: SerializedLayoutState, graph: LGraph) => void, deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
initDefaultLayout: () => void, initDefaultLayout: () => void,
onStartConfigure: () => void onStartConfigure: () => void
notifyWorkflowModified: () => void
} }
export type SerializedLayoutState = { export type SerializedLayoutState = {
@@ -770,6 +771,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
console.debug("[layoutState] addContainer", state) console.debug("[layoutState] addContainer", state)
store.set(state) store.set(state)
notifyWorkflowModified();
// runOnChangedForWidgetDefaults(dragItem) // runOnChangedForWidgetDefaults(dragItem)
return dragItem; return dragItem;
} }
@@ -801,6 +803,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
console.debug("[layoutState] addWidget", state) console.debug("[layoutState] addWidget", state)
moveItem(dragItem, parent, index) moveItem(dragItem, parent, index)
notifyWorkflowModified();
// runOnChangedForWidgetDefaults(dragItem) // runOnChangedForWidgetDefaults(dragItem)
return dragItem; return dragItem;
} }
@@ -834,6 +837,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
delete state.allItemsByNode[widget.node.id] delete state.allItemsByNode[widget.node.id]
} }
delete state.allItems[id] delete state.allItems[id]
notifyWorkflowModified();
} }
function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) { function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
@@ -946,6 +950,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
state.allItems[target.id].parent = toEntry.dragItem; state.allItems[target.id].parent = toEntry.dragItem;
console.debug("[layoutState] Move child", target, toEntry, index) console.debug("[layoutState] Move child", target, toEntry, index)
notifyWorkflowModified();
store.set(state) store.set(state)
} }
@@ -976,6 +981,7 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
console.debug("[layoutState] Grouped", container, parent, state.allItems[container.id].children, index) console.debug("[layoutState] Grouped", container, parent, state.allItems[container.id].children, index)
notifyWorkflowModified();
store.set(state) store.set(state)
return container return container
} }
@@ -1033,18 +1039,20 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
allItems: {}, allItems: {},
allItemsByNode: {}, allItemsByNode: {},
isMenuOpen: false, isMenuOpen: false,
isConfiguring: false, isConfiguring: true,
}) })
const root = addContainer(null, { direction: "horizontal", title: "" }); const root = addContainer(null, { direction: "horizontal", title: "" });
const left = addContainer(root, { direction: "vertical", title: "" }); const left = addContainer(root, { direction: "vertical", title: "" });
const right = addContainer(root, { direction: "vertical", title: "" }); const right = addContainer(root, { direction: "vertical", title: "" });
const state = get(store) store.update(s => {
state.root = root; s.root = root;
store.set(state) s.isConfiguring = false;
return s;
})
console.debug("[layoutState] initDefault", state) console.debug("[layoutState] initDefault")
} }
function serialize(): SerializedLayoutState { function serialize(): SerializedLayoutState {
@@ -1143,6 +1151,11 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
}) })
} }
function notifyWorkflowModified() {
if (!get(store).isConfiguring)
workflow.notifyModified();
}
const layoutStateStore: WritableLayoutStateStore = const layoutStateStore: WritableLayoutStateStore =
{ {
...store, ...store,
@@ -1161,7 +1174,8 @@ function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
initDefaultLayout, initDefaultLayout,
onStartConfigure, onStartConfigure,
serialize, serialize,
deserialize deserialize,
notifyWorkflowModified
} }
layoutStates.update(s => { layoutStates.update(s => {

View File

@@ -69,7 +69,12 @@ export class ComfyWorkflow {
/* /*
* Global workflow attributes * 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 { get layout(): WritableLayoutStateStore | null {
return layoutStates.getLayout(this.id) return layoutStates.getLayout(this.id)
@@ -89,6 +94,11 @@ export class ComfyWorkflow {
this.graph = new ComfyGraph(this.id); this.graph = new ComfyGraph(this.id);
} }
notifyModified() {
this.isModified = true;
store.set(get(store));
}
start(key: string, canvas: ComfyGraphCanvas) { start(key: string, canvas: ComfyGraphCanvas) {
if (this.canvases[key] != null) if (this.canvases[key] != null)
throw new Error(`This workflow is already being displayed on canvas ${key}`) throw new Error(`This workflow is already being displayed on canvas ${key}`)

View File

@@ -79,9 +79,11 @@ export function startDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore
layoutState.set(ls) layoutState.set(ls)
selectionState.set(ss) selectionState.set(ss)
layoutState.notifyWorkflowModified();
}; };
export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) { export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) {
layoutState.notifyWorkflowModified();
}; };
export function graphToGraphVis(graph: LGraph): string { export function graphToGraphVis(graph: LGraph): string {