Workflow creation/modified state
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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,12 +287,15 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
selectionState.update(ss => {
|
||||
ss.currentSelectionNodes = Object.values(nodes)
|
||||
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) {
|
||||
const widget = ls.allItemsByNode[node.id]
|
||||
if (widget)
|
||||
ss.currentSelection.push(widget.dragItem.id)
|
||||
}
|
||||
}
|
||||
return ss
|
||||
})
|
||||
}
|
||||
@@ -303,12 +307,15 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
|
||||
ss.currentHoveredNodes.add(node.id)
|
||||
}
|
||||
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) {
|
||||
const widget = ls.allItemsByNode[nodeID]
|
||||
if (widget)
|
||||
ss.currentHovered.add(widget.dragItem.id)
|
||||
}
|
||||
}
|
||||
return ss
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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<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 widget_type_overrides: Record<string, typeof SvelteComponentDev> = {}
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
</script>
|
||||
@@ -189,13 +199,22 @@
|
||||
<button class="workflow-tab"
|
||||
class:selected={index === $workflowState.activeWorkflowIdx}
|
||||
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"
|
||||
on:click={(e) => closeWorkflow(e, index)}>
|
||||
on:click={(e) => closeWorkflow(e, index, workflow)}>
|
||||
✕
|
||||
</button>
|
||||
</button>
|
||||
{/each}
|
||||
<button class="workflow-add-new-button"
|
||||
on:click={createNewWorkflow}>
|
||||
➕
|
||||
</button>
|
||||
</div>
|
||||
<div id="bottombar">
|
||||
<div class="bottombar-content">
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
</script>
|
||||
|
||||
|
||||
@@ -85,7 +93,13 @@
|
||||
<div class="handle handle-hidden" class:hidden={!edit} />
|
||||
{/if}
|
||||
{#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}
|
||||
{/key}
|
||||
{/key}
|
||||
|
||||
@@ -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 => {
|
||||
|
||||
@@ -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}`)
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user