Huge refactoring for multiple workflows

This commit is contained in:
space-nuko
2023-05-20 19:18:01 -05:00
parent a631d97efb
commit 61d9803e17
35 changed files with 1228 additions and 974 deletions

View File

@@ -2,7 +2,6 @@ import { LConnectionKind, LGraph, LGraphNode, type INodeSlot, type SlotIndex, Li
import GraphSync from "./GraphSync";
import EventEmitter from "events";
import type TypedEmitter from "typed-emitter";
import layoutState from "./stores/layoutState";
import uiState from "./stores/uiState";
import { get } from "svelte/store";
import type ComfyGraphNode from "./nodes/ComfyGraphNode";
@@ -10,6 +9,9 @@ import type IComfyInputSlot from "./IComfyInputSlot";
import type { ComfyBackendNode } from "./nodes/ComfyBackendNode";
import type { ComfyComboNode, ComfyWidgetNode } from "./nodes/widgets";
import selectionState from "./stores/selectionState";
import type { WritableLayoutStateStore } from "./stores/layoutStates";
import type { WorkflowInstID } from "./components/ComfyApp";
import layoutStates from "./stores/layoutStates";
type ComfyGraphEvents = {
configured: (graph: LGraph) => void
@@ -25,6 +27,13 @@ type ComfyGraphEvents = {
export default class ComfyGraph extends LGraph {
eventBus: TypedEmitter<ComfyGraphEvents> = new EventEmitter() as TypedEmitter<ComfyGraphEvents>;
workflowID: WorkflowInstID | null = null;
constructor(workflowID?: WorkflowInstID) {
super();
this.workflowID = workflowID;
}
override onConfigure() {
console.debug("Configured");
}
@@ -50,19 +59,24 @@ export default class ComfyGraph extends LGraph {
override onNodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
// Don't add nodes in subgraphs until this callback reaches the root
// graph
if (node.getRootGraph() == null || this._is_subgraph)
return;
// Only root graphs will have a workflow ID, so we don't mind subgraphs
// missing it
if (node.getRootGraph() != null && !this._is_subgraph && this.workflowID != null) {
const layoutState = get(layoutStates).all[this.workflowID]
if (layoutState === null) {
throw new Error(`LGraph with workflow missing layout! ${this.workflowID}`)
}
this.doAddNode(node, options);
this.doAddNode(node, layoutState, options);
}
// console.debug("Added", node);
this.eventBus.emit("nodeAdded", node);
}
/*
* Add widget UI/groups for newly added nodes.
*/
private doAddNode(node: LGraphNode, options: LGraphAddNodeOptions) {
private doAddNode(node: LGraphNode, layoutState: WritableLayoutStateStore, options: LGraphAddNodeOptions) {
layoutState.nodeAdded(node, options)
// All nodes whether they come from base litegraph or ComfyBox should
@@ -144,7 +158,7 @@ export default class ComfyGraph extends LGraph {
// ************** RECURSION ALERT ! **************
if (node.is(Subgraph)) {
for (const child of node.subgraph.iterateNodesInOrder()) {
this.doAddNode(child, options)
this.doAddNode(child, layoutState, options)
}
}
// ************** RECURSION ALERT ! **************
@@ -152,16 +166,23 @@ export default class ComfyGraph extends LGraph {
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
selectionState.clear(); // safest option
layoutState.nodeRemoved(node, options);
// Handle subgraphs being removed
if (node.is(Subgraph)) {
for (const child of node.subgraph.iterateNodesInOrder()) {
this.onNodeRemoved(child, options)
if (node.getRootGraph() != null && !this._is_subgraph && this.workflowID != null) {
const layoutState = get(layoutStates).all[this.workflowID]
if (layoutState === null) {
throw new Error(`LGraph with workflow missing layout! ${this.workflowID}`)
}
layoutState.nodeRemoved(node, options);
// Handle subgraphs being removed
if (node.is(Subgraph)) {
for (const child of node.subgraph.iterateNodesInOrder()) {
this.onNodeRemoved(child, options)
}
}
}
// console.debug("Removed", node);
this.eventBus.emit("nodeRemoved", node);
}

View File

@@ -3,11 +3,11 @@ import type ComfyApp from "./components/ComfyApp";
import queueState from "./stores/queueState";
import { get, type Unsubscriber } from "svelte/store";
import uiState from "./stores/uiState";
import layoutState from "./stores/layoutState";
import { Watch } from "@litegraph-ts/nodes-basic";
import { ComfyReroute } from "./nodes";
import type { Progress } from "./components/ComfyApp";
import selectionState from "./stores/selectionState";
import type ComfyGraph from "./ComfyGraph";
export type SerializedGraphCanvasState = {
offset: Vector2,
@@ -18,10 +18,14 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
app: ComfyApp | null;
private _unsubscribe: Unsubscriber;
get comfyGraph(): ComfyGraph | null {
return this.graph as ComfyGraph;
}
constructor(
app: ComfyApp,
graph: LGraph,
canvas: HTMLCanvasElement | string,
graph?: ComfyGraph,
options: {
skip_render?: boolean;
skip_events?: boolean;
@@ -282,7 +286,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
selectionState.update(ss => {
ss.currentSelectionNodes = Object.values(nodes)
ss.currentSelection = []
const ls = get(layoutState)
const ls = get(this.comfyGraph.layoutState)
for (const node of ss.currentSelectionNodes) {
const widget = ls.allItemsByNode[node.id]
if (widget)
@@ -299,7 +303,7 @@ export default class ComfyGraphCanvas extends LGraphCanvas {
ss.currentHoveredNodes.add(node.id)
}
ss.currentHovered.clear()
const ls = get(layoutState)
const ls = get(this.comfyGraph.layoutState)
for (const nodeID of ss.currentHoveredNodes) {
const widget = ls.allItemsByNode[nodeID]
if (widget)

View File

@@ -3,7 +3,7 @@ import type TypedEmitter from "typed-emitter";
import EventEmitter from "events";
import type { ComfyImageLocation } from "$lib/utils";
import type { SerializedLGraph, UUID } from "@litegraph-ts/core";
import type { SerializedLayoutState } from "./stores/layoutState";
import type { SerializedLayoutState } from "./stores/layoutStates";
import type { ComfyNodeDef } from "./ComfyNodeDef";
export type ComfyPromptRequest = {

View File

@@ -11,11 +11,12 @@
// notice - fade in works fine but don't add svelte's fade-out (known issue)
import {cubicIn} from 'svelte/easing';
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutStates";
import { startDrag, stopDrag } from "$lib/utils"
import { writable, type Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let layoutState: WritableLayoutStateStore;
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
@@ -59,6 +60,14 @@
navigator.vibrate(20)
$isOpen = e.detail
}
function _startDrag(e: MouseEvent | TouchEvent) {
startDrag(e, layoutState)
}
function _stopDrag(e: MouseEvent | TouchEvent) {
stopDrag(e, layoutState)
}
</script>
{#if container}
@@ -93,7 +102,7 @@
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.style || ""}
>
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if}
@@ -101,10 +110,18 @@
{/each}
</div>
{#if isHidden(container) && edit}
<div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
<div class="handle handle-hidden"
style:z-index={zIndex+100}
class:hidden={!edit} />
{/if}
{#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
<div class="handle handle-container"
style:z-index={zIndex+100}
data-drag-item-id={container.id}
on:mousedown={_startDrag}
on:touchstart={_startDrag}
on:mouseup={_stopDrag}
on:touchend={_stopDrag}/>
{/if}
</Accordion>
</Block>
@@ -112,7 +129,7 @@
<Block elem_classes={["gradio-accordion"]}>
<Accordion label={container.attrs.title} open={$isOpen} on:click={handleClick}>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item(item.id)}
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
{/each}
</Accordion>
</Block>

View File

@@ -10,11 +10,12 @@
// notice - fade in works fine but don't add svelte's fade-out (known issue)
import {cubicIn} from 'svelte/easing';
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let layoutState: WritableLayoutStateStore;
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
@@ -53,6 +54,14 @@
children = layoutState.updateChildren(container, evt.detail.items)
// Ensure dragging is stopped on drag finish
};
function _startDrag(e: MouseEvent | TouchEvent) {
startDrag(e, layoutState)
}
function _stopDrag(e: MouseEvent | TouchEvent) {
stopDrag(e, layoutState)
}
</script>
{#if container}
@@ -92,7 +101,7 @@
animate:flip={{duration:flipDurationMs}}
style={item?.attrs?.style || ""}
>
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if}
@@ -100,10 +109,18 @@
{/each}
</div>
{#if isHidden(container) && edit}
<div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
<div class="handle handle-hidden"
style:z-index={zIndex+100}
class:hidden={!edit} />
{/if}
{#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
<div class="handle handle-container"
style:z-index={zIndex+100}
data-drag-item-id={container.id}
on:mousedown={_startDrag}
on:touchstart={_startDrag}
on:mouseup={_stopDrag}
on:touchend={_stopDrag}/>
{/if}
</Block>
</div>

View File

@@ -2,7 +2,6 @@
import { ListIcon as List, ImageIcon as Image, SettingsIcon as Settings } from "svelte-feather-icons";
import ComfyApp, { type A1111PromptAndInfo, type SerializedAppState } from "./ComfyApp";
import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState";
import { SvelteToast, toast } from '@zerodevx/svelte-toast'
import LightboxModal from "./LightboxModal.svelte";
@@ -18,8 +17,6 @@
let hasShownUIHelpToast: boolean = false;
let uiTheme: string = "gradio-dark";
let debugLayout: boolean = false;
const toastOptions = {
intro: { duration: 200 },
theme: {
@@ -32,12 +29,6 @@
notify("Right-click to open context menu.")
}
if (debugLayout) {
layoutState.subscribe(s => {
console.warn("UPDATESTATE", s)
})
}
$: if (uiTheme === "gradio-dark") {
document.getElementById("app-root").classList.add("dark")
}

View File

@@ -21,8 +21,7 @@ import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import queueState from "$lib/stores/queueState";
import { type SvelteComponentDev } from "svelte/internal";
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutState";
import layoutState from "$lib/stores/layoutState";
import type { LayoutState, SerializedLayoutState, WritableLayoutStateStore } from "$lib/stores/layoutStates";
import { toast } from '@zerodevx/svelte-toast'
import ComfyGraph from "$lib/ComfyGraph";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
@@ -41,7 +40,10 @@ import parseA1111, { type A1111ParsedInfotext } from "$lib/parseA1111";
import convertA1111ToStdPrompt from "$lib/convertA1111ToStdPrompt";
import type { ComfyBoxStdPrompt } from "$lib/ComfyBoxStdPrompt";
import ComfyBoxStdPromptSerializer from "$lib/ComfyBoxStdPromptSerializer";
import { v4 as uuidv4 } from "uuid";
import selectionState from "$lib/stores/selectionState";
import layoutStates from "$lib/stores/layoutStates";
import { ComfyWorkflow } from "$lib/stores/workflowState";
import workflowState from "$lib/stores/workflowState";
export const COMFYBOX_SERIAL_VERSION = 1;
@@ -134,133 +136,6 @@ type CanvasState = {
canvas: ComfyGraphCanvas,
}
type ActiveCanvas = {
canvas: LGraphCanvas | null;
canvasHandler: () => void | null;
state: SerializedGraphCanvasState;
}
export type SerializedWorkflowState = {
graph: SerializedLGraph,
layout: SerializedLayoutState
}
/*
* ID for an opened workflow.
*
* Unlike NodeID and PromptID, these are *not* saved to the workflow itself.
* They are only used for identifying an open workflow in the program. If the
* workflow is closed and reopened, a different workflow ID will be assigned to
* it.
*/
export type WorkflowInstID = UUID;
export class ComfyWorkflow {
/*
* Used for uniquely identifying the instance of the opened workflow in the frontend.
*/
id: WorkflowInstID;
title: string;
graph: ComfyGraph;
layout: WritableLayoutStateStore;
canvases: Record<string, ActiveCanvas> = {};
constructor(title: string, graph: ComfyGraph, layout: WritableLayoutStateStore) {
this.id = uuidv4();
this.title = title;
this.layout = layout;
this.graph = graph;
}
start(key: string, canvas: ComfyGraphCanvas) {
if (this.canvases[key] != null)
throw new Error(`This workflow is already being displayed on canvas ${key}`)
const canvasHandler = () => canvas.draw(true);
this.canvases[key] = {
canvas,
canvasHandler,
state: {
// TODO
offset: [0, 0],
scale: 1
}
}
this.graph.attachCanvas(canvas);
this.graph.eventBus.on("afterExecute", canvasHandler)
if (Object.keys(this.canvases).length === 1)
this.graph.start();
}
stop(key: string) {
const canvas = this.canvases[key]
if (canvas == null)
throw new Error(`This workflow is not being displayed on canvas ${key}`)
this.graph.detachCanvas(canvas.canvas);
this.graph.eventBus.removeListener("afterExecute", canvas.canvasHandler)
delete this.canvases[key]
if (Object.keys(this.canvases).length === 0)
this.graph.stop();
}
stopAll() {
for (const key of Object.keys(this.canvases))
this.stop(key)
this.graph.stop()
}
serialize(): SerializedWorkflowState {
const graph = this.graph;
const serializedGraph = graph.serialize()
const serializedLayout = this.layout.serialize()
return {
graph: serializedGraph,
layout: serializedLayout
}
}
static deserialize(data: SerializedWorkflowState): ComfyWorkflow {
const layout = layoutState; // TODO
// 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
layout.onStartConfigure();
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
for (let n of data.graph.nodes) {
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
}
const graph = new ComfyGraph();
graph.configure(data.graph);
for (const node of graph._nodes) {
const size = node.computeSize();
size[0] = Math.max(node.size[0], size[0]);
size[1] = Math.max(node.size[1], size[1]);
node.size = size;
// this.#invokeExtensions("loadedGraphNode", node);
}
// Now restore the layout
// Subsequent added nodes will add the UI data to layoutState
// TODO
layout.deserialize(data.layout, graph)
return new ComfyWorkflow("Workflow X", graph, layout);
}
}
export default class ComfyApp {
api: ComfyAPI;
@@ -269,22 +144,6 @@ export default class ComfyApp {
canvasCtx: CanvasRenderingContext2D | null = null;
lCanvas: ComfyGraphCanvas | null = null;
openedWorkflows: ComfyWorkflow[] = [];
openedWorkflowsByID: Record<WorkflowInstID, ComfyWorkflow> = {};
activeWorkflowIdx: number = -1;
get activeWorkflow(): ComfyWorkflow | null {
return this.openedWorkflows[this.activeWorkflowIdx]
}
get activeGraph(): ComfyGraph | null {
return this.activeWorkflow?.graph;
}
getWorkflow(id: WorkflowInstID): ComfyWorkflow | null {
return this.openedWorkflowsByID[id];
}
shiftDown: boolean = false;
ctrlDown: boolean = false;
selectedGroupMoving: boolean = false;
@@ -312,7 +171,7 @@ export default class ComfyApp {
this.rootEl = document.getElementById("app-root") as HTMLDivElement;
this.canvasEl = document.getElementById("graph-canvas") as HTMLCanvasElement;
this.lCanvas = new ComfyGraphCanvas(this, null, this.canvasEl);
this.lCanvas = new ComfyGraphCanvas(this, this.canvasEl);
this.canvasCtx = this.canvasEl.getContext("2d");
const uiUnlocked = get(uiState).uiUnlocked;
@@ -371,12 +230,12 @@ export default class ComfyApp {
this.lCanvas.draw(true, true);
}
serialize(): SerializedAppState {
const workflow = this.activeWorkflow
if (workflow == null)
throw new Error("No workflow active!")
serialize(workflow: ComfyWorkflow): SerializedAppState {
const layoutState = layoutStates.getLayout(workflow.id);
if (layoutState == null)
throw new Error("Workflow has no layout!")
const { graph, layout } = workflow.serialize();
const { graph, layout } = workflow.serialize(layoutState);
const canvas = this.lCanvas.serialize();
return {
@@ -390,14 +249,15 @@ export default class ComfyApp {
}
saveStateToLocalStorage() {
if (this.activeWorkflow == null) {
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();
const savedWorkflow = this.serialize(workflow);
const json = JSON.stringify(savedWorkflow);
localStorage.setItem("workflow", json)
notify("Saved to local storage.")
@@ -548,25 +408,27 @@ export default class ComfyApp {
this.api.addEventListener("progress", (progress: Progress) => {
queueState.progressUpdated(progress);
this.activeGraph?.setDirtyCanvas(true, false); // TODO PromptID
workflowState.getActiveWorkflow()?.graph?.setDirtyCanvas(true, false); // TODO PromptID
});
this.api.addEventListener("executing", (promptID: PromptID | null, nodeID: ComfyNodeID | null) => {
const queueEntry = queueState.executingUpdated(promptID, nodeID);
if (queueEntry != null) {
const workflow = this.getWorkflow(queueEntry.workflowID)
workflow?.graph.setDirtyCanvas(true, false);
const workflow = workflowState.getWorkflow(queueEntry.workflowID);
workflow?.graph?.setDirtyCanvas(true, false);
}
});
this.api.addEventListener("executed", (promptID: PromptID, nodeID: ComfyNodeID, output: SerializedPromptOutput) => {
const queueEntry = queueState.onExecuted(promptID, nodeID, output)
if (queueEntry != null) {
const workflow = this.getWorkflow(queueEntry.workflowID)
workflow?.graph.setDirtyCanvas(true, false);
const node = workflow?.graph.getNodeByIdRecursive(nodeID) as ComfyGraphNode;
if (node?.onExecuted) {
node.onExecuted(output);
const workflow = workflowState.getWorkflow(queueEntry.workflowID);
if (workflow != null) {
workflow.graph.setDirtyCanvas(true, false);
const node = workflow.graph.getNodeByIdRecursive(nodeID) as ComfyGraphNode;
if (node?.onExecuted) {
node.onExecuted(output);
}
}
}
});
@@ -635,68 +497,31 @@ export default class ComfyApp {
setColor(BuiltInSlotType.ACTION, "lightseagreen")
}
createNewWorkflow(): ComfyWorkflow {
// TODO remove
const workflow = ComfyWorkflow.deserialize({ graph: blankGraph.workflow, layout: blankGraph.layout })
this.openedWorkflows.push(workflow);
this.setActiveWorkflow(this.openedWorkflows.length - 1)
return workflow;
}
async openWorkflow(data: SerializedAppState): Promise<ComfyWorkflow> {
if (data.version !== COMFYBOX_SERIAL_VERSION) {
throw `Invalid ComfyBox saved data format: ${data.version}`
}
this.clean();
const workflow = ComfyWorkflow.deserialize({ graph: data.workflow, layout: data.layout })
const workflow = workflowState.openWorkflow(data);
// Restore canvas offset/zoom
this.lCanvas.deserialize(data.canvas)
await this.refreshComboInNodes(workflow);
this.openedWorkflows.push(workflow);
this.setActiveWorkflow(this.openedWorkflows.length - 1)
return workflow;
}
closeWorkflow(index: number) {
if (index < 0 || index >= this.openedWorkflows.length)
return;
const workflow = this.openedWorkflows[index];
workflow.stopAll();
this.openedWorkflows.splice(index, 1)
this.setActiveWorkflow(0);
}
closeAllWorkflows() {
while (this.openedWorkflows.length > 0)
this.closeWorkflow(0)
}
setActiveWorkflow(index: number) {
if (this.openedWorkflows.length === 0) {
this.activeWorkflowIdx = -1;
return;
const workflow = workflowState.setActiveWorkflow(index);
if (workflow != null) {
workflow.start("app", this.lCanvas);
this.lCanvas.deserialize(workflow.canvases["app"].state)
}
if (index < 0 || index >= this.openedWorkflows.length || this.activeWorkflowIdx === index)
return;
if (this.activeWorkflow != null)
this.activeWorkflow.stop("app")
const workflow = this.openedWorkflows[index]
this.activeWorkflowIdx = index;
console.warn("START")
workflow.start("app", this.lCanvas);
this.lCanvas.deserialize(workflow.canvases["app"].state)
selectionState.clear();
}
async initDefaultGraph() {
@@ -717,7 +542,7 @@ export default class ComfyApp {
this.clean();
this.lCanvas.closeAllSubgraphs();
this.closeAllWorkflows();
workflowState.closeAllWorkflows();
uiState.update(s => {
s.uiUnlocked = true;
s.uiEditMode = "widgets";
@@ -726,16 +551,17 @@ export default class ComfyApp {
}
runDefaultQueueAction() {
if (this.activeWorkflow == null)
const workflow = workflowState.getActiveWorkflow();
if (workflow == null)
return;
for (const node of this.activeGraph.iterateNodesInOrderRecursive()) {
for (const node of workflow.graph.iterateNodesInOrderRecursive()) {
if ("onDefaultQueueAction" in node) {
(node as ComfyGraphNode).onDefaultQueueAction()
}
}
if (get(layoutState).attrs.queuePromptButtonRunWorkflow) {
if (get(workflow.layout).attrs.queuePromptButtonRunWorkflow) {
// Hold control to queue at the front
const num = this.ctrlDown ? -1 : 0;
this.queuePrompt(num, 1);
@@ -743,7 +569,8 @@ export default class ComfyApp {
}
querySave() {
if (this.activeWorkflow == null) {
const workflow = workflowState.getActiveWorkflow();
if (workflow == null) {
notify("No active workflow!", { type: "error" })
return;
}
@@ -765,7 +592,7 @@ export default class ComfyApp {
}
const indent = 2
const json = JSON.stringify(this.serialize(), null, indent)
const json = JSON.stringify(this.serialize(workflow), null, indent)
download(filename, json, "application/json")
@@ -781,12 +608,13 @@ export default class ComfyApp {
}
async queuePrompt(num: number, batchCount: number = 1, tag: string | null = null) {
if (this.activeWorkflow === null) {
const activeWorkflow = workflowState.getActiveWorkflow();
if (activeWorkflow == null) {
notify("No workflow is opened!", { type: "error" })
return;
}
this.queueItems.push({ num, batchCount, workflow: this.activeWorkflow });
this.queueItems.push({ num, batchCount, workflow: activeWorkflow });
// Only have one action process the items so each one gets a unique seed correctly
if (this.processingQueue) {
@@ -830,7 +658,7 @@ export default class ComfyApp {
}
const p = this.graphToPrompt(workflow, tag);
const l = layoutState.serialize();
const l = workflow.layout.serialize();
console.debug(graphToGraphVis(workflow.graph))
console.debug(promptToGraphVis(p))
@@ -942,7 +770,7 @@ export default class ComfyApp {
* Refresh combo list on whole nodes
*/
async refreshComboInNodes(workflow?: ComfyWorkflow, flashUI: boolean = false) {
workflow ||= this.activeWorkflow;
workflow ||= workflowState.getActiveWorkflow();
if (workflow == null) {
notify("No active workflow!", { type: "error" })
return

View File

@@ -2,43 +2,47 @@
import { Block, BlockTitle } from "@gradio/atoms";
import { TextBox, Checkbox } from "@gradio/form";
import { LGraphNode } from "@litegraph-ts/core"
import layoutState, { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec } from "$lib/stores/layoutState"
import { type IDragItem, type WidgetLayout, ALL_ATTRIBUTES, type AttributesSpec, type WritableLayoutStateStore } from "$lib/stores/layoutStates"
import uiState from "$lib/stores/uiState"
import layoutStates from "$lib/stores/layoutStates"
import selectionState from "$lib/stores/selectionState"
import { get, type Writable, writable } from "svelte/store"
import ComfyNumberProperty from "./ComfyNumberProperty.svelte";
import ComfyComboProperty from "./ComfyComboProperty.svelte";
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
import type { ComfyWorkflow } from "$lib/stores/workflowState";
export let workflow: ComfyWorkflow | null;
let layoutState: WritableLayoutStateStore | null = null
let target: IDragItem | null = null;
let node: LGraphNode | null = null;
let attrsChanged: Writable<number> | null = null;
let refreshPropsPanel: Writable<number> | null
$: refreshPropsPanel = $layoutState.refreshPropsPanel;
$: if ($selectionState.currentSelection.length > 0) {
node = null;
const targetId = $selectionState.currentSelection.slice(-1)[0]
const entry = $layoutState.allItems[targetId]
if (entry != null) {
target = entry.dragItem
attrsChanged = target.attrsChanged;
if (target.type === "widget") {
node = (target as WidgetLayout).node
$: if (layoutState) {
if ($selectionState.currentSelection.length > 0) {
node = null;
const targetId = $selectionState.currentSelection.slice(-1)[0]
const entry = $layoutState.allItems[targetId]
if (entry != null) {
target = entry.dragItem
if (target.type === "widget") {
node = (target as WidgetLayout).node
}
}
}
}
else if ($selectionState.currentSelectionNodes.length > 0) {
target = null;
node = $selectionState.currentSelectionNodes[0]
attrsChanged = null;
else if ($selectionState.currentSelectionNodes.length > 0) {
target = null;
node = $selectionState.currentSelectionNodes[0]
}
else {
target = null
node = null;
}
}
else {
target = null
target = null;
node = null;
attrsChanged = null;
}
$: if (target) {
@@ -55,7 +59,7 @@
let value = spec.defaultValue;
target.attrs[spec.name] = value;
if (spec.refreshPanelOnChange)
$refreshPropsPanel += 1;
doRefreshPanel();
}
}
}
@@ -265,7 +269,7 @@
function doRefreshPanel() {
console.warn("[ComfyProperties] doRefreshPanel")
$refreshPropsPanel += 1;
$layoutStates.refreshPropsPanel += 1;
}
</script>
@@ -282,172 +286,174 @@
</div>
</div>
<div class="props-entries">
{#key $refreshPropsPanel}
{#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name">
<span>
<span class="title">{category.categoryName}</span>
</span>
</div>
{#each category.specs as spec(spec.id)}
{#if validWidgetAttribute(spec, target)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getAttribute(target, spec)}
on:change={(e) => updateAttribute(spec, target, e.detail)}
on:input={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
max_lines={spec.multiline ? 5 : 1}
/>
{:else if spec.type === "boolean"}
<Checkbox
{#if workflow != null && layoutState != null}
{#key $layoutStates.refreshPropsPanel}
{#each ALL_ATTRIBUTES as category(category.categoryName)}
<div class="category-name">
<span>
<span class="title">{category.categoryName}</span>
</span>
</div>
{#each category.specs as spec(spec.id)}
{#if validWidgetAttribute(spec, target)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getAttribute(target, spec)}
on:change={(e) => updateAttribute(spec, target, e.detail)}
on:input={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
max_lines={spec.multiline ? 5 : 1}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getAttribute(target, spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
{:else if spec.type === "boolean"}
<Checkbox
value={getAttribute(target, spec)}
on:change={(e) => updateAttribute(spec, target, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getAttribute(target, spec)}
values={spec.values}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{/if}
</div>
{:else if node}
{#if validNodeProperty(spec, node)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getProperty(node, spec)}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={spec.multiline ? 5 : 1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getProperty(node, spec)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getProperty(node, spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getProperty(node, spec)}
value={getAttribute(target, spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
on:change={(e) => updateAttribute(spec, target, e.detail)}
/>
{/if}
</div>
{:else if validNodeVar(spec, node)}
{:else if node}
{#if validNodeProperty(spec, node)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getProperty(node, spec)}
on:change={(e) => updateProperty(spec, e.detail)}
on:input={(e) => updateProperty(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={spec.multiline ? 5 : 1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getProperty(node, spec)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getProperty(node, spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getProperty(node, spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateProperty(spec, e.detail)}
/>
{/if}
</div>
{:else if validNodeVar(spec, node)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
on:input={(e) => updateVar(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={spec.multiline ? 5 : 1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getVar(node, spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getVar(node, spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)}
/>
{/if}
</div>
{/if}
{:else if !node && !target && validWorkflowAttribute(spec)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
on:input={(e) => updateVar(spec, e.detail)}
value={getWorkflowAttribute(spec)}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={spec.multiline ? 5 : 1}
max_lines={spec.multiline ? 5 : 1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getVar(node, spec)}
on:change={(e) => updateVar(spec, e.detail)}
value={getWorkflowAttribute(spec)}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getVar(node, spec)}
value={getWorkflowAttribute(spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getVar(node, spec)}
value={getWorkflowAttribute(spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateVar(spec, e.detail)}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{/if}
</div>
{:else if !node && !target && validWorkflowAttribute(spec)}
<div class="props-entry">
{#if spec.type === "string"}
<TextBox
value={getWorkflowAttribute(spec)}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
on:input={(e) => updateWorkflowAttribute(spec, e.detail)}
label={spec.name}
disabled={!$uiState.uiUnlocked || !spec.editable}
max_lines={spec.multiline ? 5 : 1}
/>
{:else if spec.type === "boolean"}
<Checkbox
value={getWorkflowAttribute(spec)}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
disabled={!$uiState.uiUnlocked || !spec.editable}
label={spec.name}
/>
{:else if spec.type === "number"}
<ComfyNumberProperty
name={spec.name}
value={getWorkflowAttribute(spec)}
step={spec.step || 1}
min={spec.min || -1024}
max={spec.max || 1024}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{:else if spec.type === "enum"}
<ComfyComboProperty
name={spec.name}
value={getWorkflowAttribute(spec)}
values={spec.values}
disabled={!$uiState.uiUnlocked || !spec.editable}
on:change={(e) => updateWorkflowAttribute(spec, e.detail)}
/>
{/if}
</div>
{/if}
{/each}
{/each}
{/each}
{/each}
{/key}
{/if}
</div>
</div>

View File

@@ -1,34 +1,23 @@
<script lang="ts">
import { tick } from 'svelte'
import { get } from "svelte/store";
import { LGraphNode, LGraph } from "@litegraph-ts/core";
import type { IWidget } from "@litegraph-ts/core";
import ComfyApp from "./ComfyApp";
import type { SerializedPanes } from "./ComfyApp"
import WidgetContainer from "./WidgetContainer.svelte";
import layoutState, { type ContainerLayout, type DragItem, type IDragItem } from "$lib/stores/layoutState";
import { type ContainerLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import uiState from "$lib/stores/uiState";
import selectionState from "$lib/stores/selectionState";
import Menu from './menu/Menu.svelte';
import MenuOption from './menu/MenuOption.svelte';
import MenuDivider from './menu/MenuDivider.svelte';
import Icon from './menu/Icon.svelte'
import type { ComfyWorkflow } from "$lib/stores/workflowState";
export let app: ComfyApp;
export let workflow: ComfyWorkflow;
let layoutState: WritableLayoutStateStore | null;
$: layoutState = workflow?.layout;
let root: IDragItem | null;
let dragConfigured: boolean = false;
/*
* Serialize UI panel order so it can be restored when workflow is loaded
*/
export function serialize(): any {
// TODO
}
export function restore(panels: SerializedPanes) {
// TODO
}
function moveTo(delta: number | ((cur: number, total: number) => number)) {
const dragItemID = $selectionState.currentSelection[0];
@@ -149,9 +138,11 @@
}
</script>
<div id="comfy-ui-panes" on:contextmenu={onRightClick}>
<WidgetContainer bind:dragItem={root} classes={["root-container"]} />
</div>
{#if layoutState != null}
<div id="comfy-workflow-view" on:contextmenu={onRightClick}>
<WidgetContainer bind:dragItem={root} classes={["root-container"]} {layoutState} />
</div>
{/if}
{#if showMenu}
<Menu {...menuPos} on:click={closeMenu} on:clickoutside={closeMenu}>
@@ -188,7 +179,7 @@
{/if}
<style lang="scss">
#comfy-ui-panes {
#comfy-workflow-view {
width: 100%;
height: 100%;
overflow: auto;

View File

@@ -2,7 +2,7 @@
import { Pane, Splitpanes } from 'svelte-splitpanes';
import { Button } from "@gradio/button";
import { BlockTitle } from "@gradio/atoms";
import ComfyUIPane from "./ComfyUIPane.svelte";
import ComfyWorkflowView from "./ComfyWorkflowView.svelte";
import { Checkbox, TextBox } from "@gradio/form"
import ComfyQueue from "./ComfyQueue.svelte";
import ComfyUnlockUIButton from "./ComfyUnlockUIButton.svelte";
@@ -10,15 +10,17 @@
import { get, writable, type Writable } from "svelte/store";
import ComfyProperties from "./ComfyProperties.svelte";
import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState";
import workflowState from "$lib/stores/workflowState";
import selectionState from "$lib/stores/selectionState";
import type ComfyApp from './ComfyApp';
import { onMount } from "svelte";
import Spinner from './Spinner.svelte';
import type { WritableLayoutStateStore } from '$lib/stores/layoutStates';
export let app: ComfyApp;
export let uiTheme: string = "gradio-dark" // TODO config
let layoutState: WritableLayoutStateStore | null = null;
let containerElem: HTMLDivElement;
let resizeTimeout: NodeJS.Timeout | null;
let alreadySetup: Writable<boolean> = writable(false);
@@ -27,6 +29,8 @@
let appSetupPromise: Promise<void> = null;
$: layoutState = $workflowState.activeWorkflow?.layout;
onMount(async () => {
appSetupPromise = app.setup().then(() => {
loading = false;
@@ -158,13 +162,17 @@
<Splitpanes theme="comfy" on:resize={refreshView}>
<Pane bind:size={propsSidebarSize}>
<div class="sidebar-wrapper pane-wrapper">
<ComfyProperties />
<ComfyProperties layoutState={$workflowState.activeWorkflow} />
</div>
</Pane>
<Pane>
<Splitpanes theme="comfy" on:resize={refreshView} horizontal="{true}">
<Pane>
<ComfyUIPane {app} />
{#if $workflowState.activeWorkflow != null}
<ComfyWorkflowView {app} workflow={$workflowState.activeWorkflow} />
{:else}
<span>No workflow loaded</span>
{/if}
</Pane>
<Pane bind:size={graphSize}>
<ComfyGraphView {app} transitioning={graphTransitioning} />
@@ -178,8 +186,8 @@
</Pane>
</Splitpanes>
<div id="workflow-tabs">
{#each app.openedWorkflows as workflow, index}
<button class="workflow-tab" class:selected={index === app.activeWorkflowIdx}>
{#each $workflowState.openedWorkflows as workflow, index}
<button class="workflow-tab" class:selected={index === $workflowState.activeWorkflowIdx}>
{workflow.title}
</button>
{/each}
@@ -187,7 +195,7 @@
<div id="bottombar">
<div class="bottombar-content">
<div class="left">
{#if $layoutState.attrs.queuePromptButtonName != ""}
{#if layoutState != null && $layoutState.attrs.queuePromptButtonName != ""}
<Button variant="primary" disabled={!$alreadySetup} on:click={queuePrompt}>
{$layoutState.attrs.queuePromptButtonName}
</Button>

View File

@@ -6,10 +6,11 @@
import TabsContainer from "./TabsContainer.svelte"
// notice - fade in works fine but don't add svelte's fade-out (known issue)
import { type ContainerLayout } from "$lib/stores/layoutState";
import { type ContainerLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let layoutState: WritableLayoutStateStore;
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
@@ -31,11 +32,11 @@
{#key $attrsChanged}
{#if edit || !isHidden(container)}
{#if container.attrs.variant === "tabs"}
<TabsContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
<TabsContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{:else if container.attrs.variant === "accordion"}
<AccordionContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
<AccordionContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{:else}
<BlockContainer {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
<BlockContainer {layoutState} {container} {zIndex} {classes} {showHandles} {edit} {dragDisabled} {isMobile} />
{/if}
{/if}
{/key}

View File

@@ -11,11 +11,12 @@
// notice - fade in works fine but don't add svelte's fade-out (known issue)
import {cubicIn} from 'svelte/easing';
import { flip } from 'svelte/animate';
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import { startDrag, stopDrag } from "$lib/utils"
import type { Writable } from "svelte/store";
import { isHidden } from "$lib/widgets/utils";
export let layoutState: WritableLayoutStateStore;
export let container: ContainerLayout | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
@@ -24,19 +25,17 @@
export let dragDisabled: boolean = false;
export let isMobile: boolean = false;
let attrsChanged: Writable<boolean> | null = null;
// let attrsChanged: Writable<number> = writable(0);
let children: IDragItem[] = [];
const flipDurationMs = 100;
let selectedIndex: number = 0;
$: if (container) {
children = $layoutState.allItems[container.id].children;
attrsChanged = container.attrsChanged
// attrsChanged = container.attrsChanged
}
else {
children = [];
attrsChanged = null
// attrsChanged = writable(0)
}
function handleConsider(evt: any) {
@@ -66,6 +65,14 @@
function handleSelect() {
navigator.vibrate(20)
}
function _startDrag(e: MouseEvent | TouchEvent) {
startDrag(e, layoutState)
}
function _stopDrag(e: MouseEvent | TouchEvent) {
stopDrag(e, layoutState)
}
</script>
{#if container}
@@ -103,7 +110,7 @@
<label for={String(item.id)}>
<BlockTitle><strong>Tab {i+1}:</strong> {tabName}</BlockTitle>
</label>
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
{#if item[SHADOW_ITEM_MARKER_PROPERTY_NAME]}
<div in:fade={{duration:200, easing: cubicIn}} class='drag-item-shadow'/>
{/if}
@@ -112,18 +119,26 @@
{/each}
</div>
{#if isHidden(container) && edit}
<div class="handle handle-hidden" style="z-index: {zIndex+100}" class:hidden={!edit} />
<div class="handle handle-hidden"
style:z-index={zIndex+100}
class:hidden={!edit} />
{/if}
{#if showHandles}
<div class="handle handle-container" style="z-index: {zIndex+100}" data-drag-item-id={container.id} on:mousedown={startDrag} on:touchstart={startDrag} on:mouseup={stopDrag} on:touchend={stopDrag}/>
<div class="handle handle-container"
style:z-index={zIndex+100}
data-drag-item-id={container.id}
on:mousedown={_startDrag}
on:touchstart={_startDrag}
on:mouseup={_stopDrag}
on:touchend={_stopDrag}/>
{/if}
</Block>
{:else}
<Tabs elem_classes={["gradio-tabs"]} on:select={handleSelect}>
{#each children.filter(item => item.id !== SHADOW_PLACEHOLDER_ITEM_ID) as item, i(item.id)}
{@const tabName = getTabName(container, i)}
<TabItem name={tabName}>
<WidgetContainer dragItem={item} zIndex={zIndex+1} {isMobile} />
<TabItem id={tabName} name={tabName}>
<WidgetContainer {layoutState} dragItem={item} zIndex={zIndex+1} {isMobile} />
</TabItem>
{/each}
</Tabs>

View File

@@ -2,21 +2,22 @@
import queueState from "$lib/stores/queueState";
import uiState from "$lib/stores/uiState";
import layoutState, { type ContainerLayout, type WidgetLayout, type IDragItem } from "$lib/stores/layoutState";
import { type ContainerLayout, type WidgetLayout, type IDragItem, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import selectionState from "$lib/stores/selectionState";
import { startDrag, stopDrag } from "$lib/utils"
import Container from "./Container.svelte"
import { type Writable } from "svelte/store"
import { writable, type Writable } from "svelte/store"
import type { ComfyWidgetNode } from "$lib/nodes/widgets";
import { isHidden } from "$lib/widgets/utils";
export let layoutState: WritableLayoutStateStore;
export let dragItem: IDragItem | null = null;
export let zIndex: number = 0;
export let classes: string[] = [];
export let isMobile: boolean = false;
let container: ContainerLayout | null = null;
let attrsChanged: Writable<boolean> | null = null;
let propsChanged: Writable<number> | null = null;
let attrsChanged: Writable<number> = writable(0);
let propsChanged: Writable<number> = writable(0);
let widget: WidgetLayout | null = null;
let showHandles: boolean = false;
@@ -24,8 +25,8 @@
dragItem = null;
container = null;
widget = null;
attrsChanged = null;
propsChanged = null;
attrsChanged = writable(0);
propsChanged = writable(0);
}
else if (dragItem.type === "container") {
container = dragItem as ContainerLayout;
@@ -40,7 +41,7 @@
if (widget.node && "propsChanged" in widget.node)
propsChanged = (widget.node as ComfyWidgetNode).propsChanged
else
propsChanged = null;
propsChanged = writable(0);
}
$: showHandles = $uiState.uiUnlocked
@@ -62,7 +63,7 @@
{#if container}
{#key $attrsChanged}
<Container {container} {classes} {zIndex} {showHandles} {isMobile} />
<Container {layoutState} {container} {classes} {zIndex} {showHandles} {isMobile} />
{/key}
{:else if widget && widget.node}
{@const edit = $uiState.uiUnlocked && $uiState.uiEditMode === "widgets"}
@@ -78,7 +79,7 @@
class:is-executing={$queueState.runningNodeID && $queueState.runningNodeID == widget.node.id}
class:hidden={hidden}
>
<svelte:component this={widget.node.svelteComponentType} {widget} {isMobile} />
<svelte:component this={widget.node.svelteComponentType} {layoutState} {widget} {isMobile} />
</div>
{#if hidden && edit}
<div class="handle handle-hidden" class:hidden={!edit} />

View File

@@ -1,6 +1,6 @@
import ComfyGraph from '$lib/ComfyGraph';
import { LGraphCanvas, LiteGraph, Subgraph } from '@litegraph-ts/core';
import layoutState from './stores/layoutState';
import layoutStates from './stores/layoutStates';
import { get } from 'svelte/store';
export function configureLitegraph(isMobile: boolean = false) {
@@ -28,5 +28,5 @@ export function configureLitegraph(isMobile: boolean = false) {
(window as any).LiteGraph = LiteGraph;
(window as any).LGraphCanvas = LGraphCanvas;
(window as any).layoutState = get(layoutState)
(window as any).layoutStates = get(layoutStates)
}

View File

@@ -1,6 +1,6 @@
import type { SerializedPrompt } from "$lib/components/ComfyApp";
import notify from "$lib/notify";
import layoutState, { type DragItemID } from "$lib/stores/layoutState";
import { type DragItemID } from "$lib/stores/layoutStates";
import queueState from "$lib/stores/queueState";
import { BuiltInSlotType, LiteGraph, NodeMode, type ITextWidget, type IToggleWidget, type SerializedLGraphNode, type SlotLayout, type PropertyLayout } from "@litegraph-ts/core";
import { get } from "svelte/store";
@@ -389,7 +389,7 @@ export class ComfySetNodeModeAction extends ComfyGraphNode {
}
}
for (const entry of Object.values(get(layoutState).allItems)) {
for (const entry of Object.values(get(this.layoutState).allItems)) {
if (entry.dragItem.type === "container") {
const container = entry.dragItem;
const hasTag = tags.some(t => container.attrs.tags.indexOf(t) != -1);

View File

@@ -1,13 +1,9 @@
import layoutState from "$lib/stores/layoutState"
import layoutStates from "$lib/stores/layoutStates"
import { BuiltInSlotType, LGraphNode, LiteGraph, type ITextWidget, type OptionalSlots, type PropertyLayout, type SlotLayout, type Vector2 } from "@litegraph-ts/core"
import { get } from "svelte/store"
import ComfyGraphNode from "./ComfyGraphNode"
export interface ComfyConfigureQueuePromptButtonProperties extends Record<string, any> {
}
export default class ComfyConfigureQueuePromptButton extends LGraphNode {
override properties: ComfyConfigureQueuePromptButtonProperties = {
}
export default class ComfyConfigureQueuePromptButton extends ComfyGraphNode {
static slotLayout: SlotLayout = {
inputs: [
{ name: "config", type: BuiltInSlotType.ACTION },
@@ -28,7 +24,12 @@ export default class ComfyConfigureQueuePromptButton extends LGraphNode {
override onAction(action: any, param: any, options: { action_call?: string }) {
if (action === "config" && param != null) {
layoutState.update(state => {
if (this.layoutState == null) {
console.error(this, this.getRootGraph(), Object.keys(get(layoutStates).all))
throw new Error(`Could not find layout attached to this node! ${this.id}`)
}
this.layoutState.update(state => {
if (typeof param === "string")
state.attrs.queuePromptButtonName = param || ""
else if (typeof param === "object" && "buttonName" in param)

View File

@@ -8,6 +8,8 @@ import type IComfyInputSlot from "$lib/IComfyInputSlot";
import uiState from "$lib/stores/uiState";
import { get } from "svelte/store";
import configState from "$lib/stores/configState";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
import layoutStates from "$lib/stores/layoutStates";
export type DefaultWidgetSpec = {
defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
@@ -100,6 +102,10 @@ export default class ComfyGraphNode extends LGraphNode {
return null;
}
get layoutState(): WritableLayoutStateStore | null {
return layoutStates.getLayoutByNode(this);
}
constructor(title?: string) {
super(title)
this.addProperty("tags", [], "array")

View File

@@ -1,5 +1,4 @@
import type IComfyInputSlot from "$lib/IComfyInputSlot";
import layoutState from "$lib/stores/layoutState";
import { range } from "$lib/utils";
import { LConnectionKind, LGraphCanvas, LLink, LiteGraph, NodeMode, type INodeInputSlot, type INodeOutputSlot, type ITextWidget, type LGraphNode, type SerializedLGraphNode, type Vector2 } from "@litegraph-ts/core";
import { Watch } from "@litegraph-ts/nodes-basic";
@@ -269,7 +268,7 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
if (options.setWidgetTitle) {
const widget = layoutState.findLayoutForNode(this.id as ComfyNodeID)
const widget = this.layoutState.findLayoutForNode(this.id as ComfyNodeID)
if (widget && input.name !== "") {
widget.attrs.title = input.name;
}
@@ -288,7 +287,7 @@ export default abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
}
notifyPropsChanged() {
const layoutEntry = layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
const layoutEntry = this.layoutState.findLayoutEntryForNode(this.id as ComfyNodeID)
if (layoutEntry && layoutEntry.parent) {
layoutEntry.parent.attrsChanged.set(get(layoutEntry.parent.attrsChanged) + 1)
}

View File

@@ -1,6 +1,6 @@
import { writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type { DragItemID, IDragItem } from './layoutState';
import type { DragItemID, IDragItem } from './layoutStates';
import type { LGraphNode, NodeID } from '@litegraph-ts/core';
export type SelectionState = {

View File

@@ -0,0 +1,261 @@
import type { SerializedGraphCanvasState } from '$lib/ComfyGraphCanvas';
import type { LGraphCanvas, SerializedLGraph, UUID } from '@litegraph-ts/core';
import { get, writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type { SerializedLayoutState, WritableLayoutStateStore } from './layoutStates';
import ComfyGraph from '$lib/ComfyGraph';
import layoutStates from './layoutStates';
import { v4 as uuidv4 } from "uuid";
import type ComfyGraphCanvas from '$lib/ComfyGraphCanvas';
import { blankGraph } from '$lib/defaultGraph';
import type { SerializedAppState } from '$lib/components/ComfyApp';
type ActiveCanvas = {
canvas: LGraphCanvas | null;
canvasHandler: () => void | null;
state: SerializedGraphCanvasState;
}
export type SerializedWorkflowState = {
graph: SerializedLGraph,
layout: SerializedLayoutState
}
/*
* ID for an opened workflow.
*
* Unlike NodeID and PromptID, these are *not* saved to the workflow itself.
* They are only used for identifying an open workflow in the program. If the
* workflow is closed and reopened, a different workflow ID will be assigned to
* it.
*/
export type WorkflowInstID = UUID;
export class ComfyWorkflow {
/*
* Used for uniquely identifying the instance of the opened workflow in the frontend.
*/
id: WorkflowInstID;
title: string;
graph: ComfyGraph;
get layout(): WritableLayoutStateStore | null {
return layoutStates.getLayout(this.id)
}
canvases: Record<string, ActiveCanvas> = {};
constructor(title: string) {
this.id = uuidv4();
this.title = title;
this.graph = new ComfyGraph(this.id);
}
start(key: string, canvas: ComfyGraphCanvas) {
if (this.canvases[key] != null)
throw new Error(`This workflow is already being displayed on canvas ${key}`)
const canvasHandler = () => canvas.draw(true);
this.canvases[key] = {
canvas,
canvasHandler,
state: {
// TODO
offset: [0, 0],
scale: 1
}
}
this.graph.attachCanvas(canvas);
this.graph.eventBus.on("afterExecute", canvasHandler)
if (Object.keys(this.canvases).length === 1)
this.graph.start();
}
stop(key: string) {
const canvas = this.canvases[key]
if (canvas == null)
throw new Error(`This workflow is not being displayed on canvas ${key}`)
this.graph.detachCanvas(canvas.canvas);
this.graph.eventBus.removeListener("afterExecute", canvas.canvasHandler)
delete this.canvases[key]
if (Object.keys(this.canvases).length === 0)
this.graph.stop();
}
stopAll() {
for (const key of Object.keys(this.canvases))
this.stop(key)
this.graph.stop()
}
serialize(layoutState: WritableLayoutStateStore): SerializedWorkflowState {
const graph = this.graph;
const serializedGraph = graph.serialize()
const serializedLayout = layoutState.serialize()
return {
graph: serializedGraph,
layout: serializedLayout
}
}
static create(title: string = "New Workflow"): [ComfyWorkflow, WritableLayoutStateStore] {
const workflow = new ComfyWorkflow(title);
const layoutState = layoutStates.create(workflow);
return [workflow, layoutState]
}
deserialize(layoutState: WritableLayoutStateStore, data: SerializedWorkflowState) {
// 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();
// Patch T2IAdapterLoader to ControlNetLoader since they are the same node now
for (let n of data.graph.nodes) {
if (n.type == "T2IAdapterLoader") n.type = "ControlNetLoader";
}
this.graph.configure(data.graph);
for (const node of this.graph._nodes) {
const size = node.computeSize();
size[0] = Math.max(node.size[0], size[0]);
size[1] = Math.max(node.size[1], size[1]);
node.size = size;
// this.#invokeExtensions("loadedGraphNode", node);
}
// Now restore the layout
// Subsequent added nodes will add the UI data to layoutState
// TODO
layoutState.deserialize(data.layout, this.graph)
}
}
export type WorkflowState = {
openedWorkflows: ComfyWorkflow[],
openedWorkflowsByID: Record<WorkflowInstID, ComfyWorkflow>,
activeWorkflowIdx: number,
activeWorkflow: ComfyWorkflow | null,
}
type WorkflowStateOps = {
getWorkflow: (id: WorkflowInstID) => ComfyWorkflow | null
getActiveWorkflow: () => ComfyWorkflow | null
createNewWorkflow: () => ComfyWorkflow,
openWorkflow: (data: SerializedAppState) => ComfyWorkflow,
closeWorkflow: (index: number) => void,
closeAllWorkflows: () => void,
setActiveWorkflow: (index: number) => ComfyWorkflow | null
}
export type WritableWorkflowStateStore = Writable<WorkflowState> & WorkflowStateOps;
const store: Writable<WorkflowState> = writable(
{
openedWorkflows: [],
openedWorkflowsByID: {},
activeWorkflowIdx: -1
})
function getWorkflow(id: WorkflowInstID): ComfyWorkflow | null {
return get(store).openedWorkflowsByID[id];
}
function getActiveWorkflow(): ComfyWorkflow | null {
const state = get(store);
if (state.activeWorkflowIdx === -1)
return null;
return state.openedWorkflows[state.activeWorkflowIdx];
}
function createNewWorkflow(): ComfyWorkflow {
const workflow = new ComfyWorkflow("Workflow X");
const layoutState = layoutStates.create(workflow);
workflow.deserialize(layoutState, { graph: blankGraph.workflow, layout: blankGraph.layout })
const state = get(store);
this.openedWorkflows.push(workflow);
setActiveWorkflow(state.openedWorkflows.length - 1)
store.set(state)
return workflow;
}
function openWorkflow(data: SerializedAppState): ComfyWorkflow {
const [workflow, layoutState] = ComfyWorkflow.create("Workflow X")
workflow.deserialize(layoutState, { graph: data.workflow, layout: data.layout })
const state = get(store);
state.openedWorkflows.push(workflow);
setActiveWorkflow(state.openedWorkflows.length - 1)
store.set(state)
return workflow;
}
function closeWorkflow(index: number) {
const state = get(store);
if (index < 0 || index >= state.openedWorkflows.length)
return;
const workflow = state.openedWorkflows[index];
workflow.stopAll();
state.openedWorkflows.splice(index, 1)
setActiveWorkflow(0);
store.set(state);
}
function closeAllWorkflows() {
const state = get(store)
while (state.openedWorkflows.length > 0)
closeWorkflow(0)
}
function setActiveWorkflow(index: number): ComfyWorkflow | null {
const state = get(store);
if (state.openedWorkflows.length === 0) {
state.activeWorkflowIdx = -1;
state.activeWorkflow = null
return null;
}
if (index < 0 || index >= state.openedWorkflows.length || state.activeWorkflowIdx === index)
return state.activeWorkflow;
if (state.activeWorkflow != null)
state.activeWorkflow.stop("app")
const workflow = state.openedWorkflows[index]
state.activeWorkflowIdx = index;
state.activeWorkflow = workflow;
return workflow;
}
const workflowStateStore: WritableWorkflowStateStore =
{
...store,
getWorkflow,
getActiveWorkflow,
createNewWorkflow,
openWorkflow,
closeWorkflow,
closeAllWorkflows,
setActiveWorkflow,
}
export default workflowStateStore;

View File

@@ -1,10 +1,11 @@
import layoutState, { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout, type WritableLayoutStateStore } from "$lib/stores/layoutStates";
import selectionState from "$lib/stores/selectionState";
import type { FileData as GradioFileData } from "@gradio/upload";
import { Subgraph, type LGraph, type LGraphNode, type LLink, type SerializedLGraph, type UUID } from "@litegraph-ts/core";
import { get } from "svelte/store";
import type { ComfyNodeID } from "./api";
import { type SerializedPrompt } from "./components/ComfyApp";
import workflowState from "./stores/workflowState";
export function clamp(n: number, min: number, max: number): number {
return Math.min(Math.max(n, min), max)
@@ -39,7 +40,7 @@ export function download(filename: string, text: string, type: string = "text/pl
}, 0);
}
export function startDrag(evt: MouseEvent) {
export function startDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) {
const dragItemId: string = evt.target.dataset["dragItemId"];
const ss = get(selectionState)
const ls = get(layoutState)
@@ -80,7 +81,7 @@ export function startDrag(evt: MouseEvent) {
selectionState.set(ss)
};
export function stopDrag(evt: MouseEvent) {
export function stopDrag(evt: MouseEvent, layoutState: WritableLayoutStateStore) {
};
export function graphToGraphVis(graph: LGraph): string {
@@ -208,7 +209,8 @@ export function getNodeInfo(nodeId: ComfyNodeID): string {
const displayNodeID = nodeId ? (nodeId.split("-")[0]) : String(nodeId);
const title = app.activeGraph.getNodeByIdRecursive(nodeId)?.title || String(nodeId);
const workflow = workflowState.getActiveWorkflow();
const title = workflow?.graph?.getNodeByIdRecursive(nodeId)?.title || String(nodeId);
return title + " (" + displayNodeID + ")"
}
@@ -238,7 +240,7 @@ export function convertComfyOutputEntryToGradio(r: ComfyImageLocation): GradioFi
return fileData
}
export function convertComfyOutputToComfyURL(output: FileNameOrGalleryData): string {
export function convertComfyOutputToComfyURL(output: string | ComfyImageLocation): string {
if (typeof output === "string")
return output;

View File

@@ -1,21 +1,23 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { Button } from "@gradio/button";
import { get, type Writable, writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { ComfyButtonNode } from "$lib/nodes/widgets";
export let widget: WidgetLayout | null = null;
export let isMobile: boolean = false;
let node: ComfyButtonNode | null = null;
let nodeValue: Writable<boolean> | null = null;
let attrsChanged: Writable<boolean> | null = null;
// let nodeValue: Writable<boolean> = writable(false);
let attrsChanged: Writable<number> = writable(0);
$: widget && setNodeValue(widget);
function setNodeValue(widget: WidgetLayout) {
if (widget) {
node = widget.node as ComfyButtonNode
nodeValue = node.value;
// nodeValue = node.value;
attrsChanged = widget.attrsChanged;
}
};
@@ -26,7 +28,7 @@
}
const style = {
full_width: "100%",
full_width: true
}
</script>

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { Block } from "@gradio/atoms";
import { Checkbox } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store";

View File

@@ -5,7 +5,7 @@
// import VirtualList from '$lib/components/VirtualList.svelte';
import VirtualList from 'svelte-tiny-virtual-list';
import type { ComfyComboNode } from "$lib/nodes/widgets";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { get, writable, type Writable } from "svelte/store";
import { isDisabled } from "./utils"
export let widget: WidgetLayout | null = null;

View File

@@ -5,7 +5,7 @@
import { Image } from "@gradio/icons";
import { StaticImage } from "$lib/components/gradio/image";
import type { Styles } from "@gradio/utils";
import type { WidgetLayout } from "$lib/stores/layoutState";
import type { WidgetLayout } from "$lib/stores/layoutStates";
import { writable, type Writable } from "svelte/store";
import type { FileData as GradioFileData } from "@gradio/upload";
import type { SelectData as GradioSelectData } from "@gradio/utils";

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { Block } from "@gradio/atoms";
import { TextBox } from "@gradio/form";
import Row from "$lib/components/gradio/app/Row.svelte";

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import type { ComfyNumberNode } from "$lib/nodes/widgets";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { Range } from "$lib/components/gradio/form";
import { get, type Writable } from "svelte/store";
import { debounce } from "$lib/utils";

View File

@@ -1,5 +1,5 @@
<script lang="ts">
import { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { Block } from "@gradio/atoms";
import { Radio } from "@gradio/form";
import { get, type Writable, writable } from "svelte/store";

View File

@@ -1,6 +1,6 @@
<script lang="ts">
import { TextBox } from "@gradio/form";
import { type WidgetLayout } from "$lib/stores/layoutState";
import { type WidgetLayout } from "$lib/stores/layoutStates";
import { type Writable } from "svelte/store";
import { isDisabled } from "./utils"
import type { ComfyTextNode } from "$lib/nodes/widgets";

View File

@@ -1,5 +1,4 @@
import type { IDragItem } from "$lib/stores/layoutState";
import layoutState from "$lib/stores/layoutState";
import type { IDragItem } from "$lib/stores/layoutStates";
import { LGraphNode, NodeMode } from "@litegraph-ts/core";
import { get } from "svelte/store";

View File

@@ -1,21 +1,22 @@
<script lang="ts">
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import notify from "$lib/notify";
import uiState from "$lib/stores/uiState";
import layoutState from "$lib/stores/layoutState";
import queueState from "$lib/stores/queueState";
import workflowState from "$lib/stores/workflowState";
import { getNodeInfo } from "$lib/utils"
import { Link, Toolbar } from "framework7-svelte"
import ProgressBar from "$lib/components/ProgressBar.svelte";
import Indicator from "./Indicator.svelte";
import interfaceState from "$lib/stores/interfaceState";
import LightboxModal from "$lib/components/LightboxModal.svelte";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
export let subworkflowID: number = -1;
export let app: ComfyApp = undefined;
let layoutState: WritableLayoutStateStore = null;
let fileInput: HTMLInputElement = undefined;
$: layoutState = $workflowState.activeWorkflow?.layout;
function queuePrompt() {
navigator.vibrate(20)
app.runDefaultQueueAction()

View File

@@ -18,21 +18,22 @@
lCanvas.draw(true, true);
}
$: if (app?.activeGraph != null && canvasEl != null) {
if (!lCanvas) {
lCanvas = new ComfyGraphCanvas(app, app.activeGraph, canvasEl);
lCanvas.allow_interaction = false;
app.activeGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
}
resizeCanvas();
}
// TODO
// $: if (app?.activeGraph != null && canvasEl != null) {
// if (!lCanvas) {
// lCanvas = new ComfyGraphCanvas(app, app.activeGraph, canvasEl);
// lCanvas.allow_interaction = false;
// app.activeGraph.eventBus.on("afterExecute", () => lCanvas.draw(true))
// }
// resizeCanvas();
// }
</script>
<Page>
<Navbar title="Node Graph" backLink="Back" />
<div class="canvas-wrapper pane-wrapper">
<canvas bind:this={canvasEl} id="extra-canvas" />
<!-- <canvas bind:this={canvasEl} id="extra-canvas" /> -->
</div>
</Page>

View File

@@ -1,21 +1,29 @@
<script lang="ts">
import layoutState, { type IDragItem } from "$lib/stores/layoutState";
import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
import WidgetContainer from "$lib/components/WidgetContainer.svelte";
import type ComfyApp from "$lib/components/ComfyApp";
import type { ComfyWorkflow } from "$lib/components/ComfyApp";
import { writable, type Writable } from "svelte/store";
import type { WritableLayoutStateStore } from "$lib/stores/layoutStates";
export let subworkflowID: number = -1;
export let app: ComfyApp
// TODO move
let workflow: ComfyWorkflow | null = null
let layoutState: WritableLayoutStateStore | null = null;
$: layoutState = workflow ? workflow.layout : null;
</script>
<Page name="subworkflow">
<Navbar title="Workflow {subworkflowID}" backLink="Back" />
<div class="container">
<WidgetContainer bind:dragItem={$layoutState.root} isMobile={true} classes={["root-container", "mobile"]} />
</div>
{#if layoutState}
<div class="container">
<WidgetContainer bind:dragItem={$layoutState.root} {layoutState} isMobile={true} classes={["root-container", "mobile"]} />
</div>
{/if}
</Page>
<style lang="scss">

View File

@@ -7,13 +7,14 @@ import ComfyPromptSerializer from "$lib/components/ComfyPromptSerializer";
import { ComfyBackendNode } from "$lib/nodes/ComfyBackendNode";
import ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import { graphToGraphVis } from "$lib/utils";
import layoutState from "$lib/stores/layoutState";
import { ComfyNumberNode } from "$lib/nodes/widgets";
import { get } from "svelte/store";
import layoutStates from "$lib/stores/layoutStates";
import { ComfyWorkflow } from "$lib/components/ComfyApp";
export default class ComfyGraphTests extends UnitTest {
test__onNodeAdded__updatesLayoutState() {
const graph = new ComfyGraph();
const [{ graph }, layoutState] = ComfyWorkflow.create()
layoutState.initDefaultLayout() // adds 3 containers
const state = get(layoutState)
@@ -38,7 +39,7 @@ export default class ComfyGraphTests extends UnitTest {
}
test__onNodeAdded__handlesNodesAddedInSubgraphs() {
const graph = new ComfyGraph();
const [{ graph }, layoutState] = ComfyWorkflow.create()
layoutState.initDefaultLayout()
const subgraph = LiteGraph.createNode(Subgraph);
@@ -57,7 +58,7 @@ export default class ComfyGraphTests extends UnitTest {
}
test__onNodeAdded__handlesSubgraphsWithNodes() {
const graph = new ComfyGraph();
const [{ graph }, layoutState] = ComfyWorkflow.create()
layoutState.initDefaultLayout()
const state = get(layoutState)
@@ -75,7 +76,7 @@ export default class ComfyGraphTests extends UnitTest {
}
test__onNodeRemoved__updatesLayoutState() {
const graph = new ComfyGraph();
const [{ graph }, layoutState] = ComfyWorkflow.create()
layoutState.initDefaultLayout()
const widget = LiteGraph.createNode(ComfyNumberNode);