Huge refactoring for multiple workflows
This commit is contained in:
@@ -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,6 +166,13 @@ export default class ComfyGraph extends LGraph {
|
||||
|
||||
override onNodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||
selectionState.clear(); // safest option
|
||||
|
||||
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
|
||||
@@ -160,8 +181,8 @@ export default class ComfyGraph extends LGraph {
|
||||
this.onNodeRemoved(child, options)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// console.debug("Removed", node);
|
||||
this.eventBus.emit("nodeRemoved", node);
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
|
||||
@@ -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,27 +408,29 @@ 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;
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
this.api.addEventListener("execution_start", (promptID: PromptID) => {
|
||||
@@ -635,70 +497,33 @@ 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 (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")
|
||||
if (workflow != null) {
|
||||
workflow.start("app", this.lCanvas);
|
||||
this.lCanvas.deserialize(workflow.canvases["app"].state)
|
||||
}
|
||||
|
||||
selectionState.clear();
|
||||
}
|
||||
|
||||
async initDefaultGraph() {
|
||||
let state = null;
|
||||
try {
|
||||
@@ -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
|
||||
|
||||
@@ -2,29 +2,30 @@
|
||||
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 { 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) {
|
||||
$: 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
|
||||
attrsChanged = target.attrsChanged;
|
||||
if (target.type === "widget") {
|
||||
node = (target as WidgetLayout).node
|
||||
}
|
||||
@@ -33,12 +34,15 @@
|
||||
else if ($selectionState.currentSelectionNodes.length > 0) {
|
||||
target = null;
|
||||
node = $selectionState.currentSelectionNodes[0]
|
||||
attrsChanged = null;
|
||||
}
|
||||
else {
|
||||
target = null
|
||||
node = null;
|
||||
attrsChanged = null;
|
||||
}
|
||||
}
|
||||
else {
|
||||
target = null;
|
||||
node = 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,7 +286,8 @@
|
||||
</div>
|
||||
</div>
|
||||
<div class="props-entries">
|
||||
{#key $refreshPropsPanel}
|
||||
{#if workflow != null && layoutState != null}
|
||||
{#key $layoutStates.refreshPropsPanel}
|
||||
{#each ALL_ATTRIBUTES as category(category.categoryName)}
|
||||
<div class="category-name">
|
||||
<span>
|
||||
@@ -448,6 +453,7 @@
|
||||
{/if}
|
||||
{/each}
|
||||
{/each}
|
||||
{/key}
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -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;
|
||||
@@ -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>
|
||||
|
||||
@@ -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}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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} />
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -6,6 +6,8 @@ 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';
|
||||
|
||||
function isComfyWidgetNode(node: LGraphNode): node is ComfyWidgetNode {
|
||||
return "svelteComponentType" in node
|
||||
@@ -85,11 +87,6 @@ export type LayoutState = {
|
||||
* Global workflow attributes
|
||||
*/
|
||||
attrs: LayoutAttributes
|
||||
|
||||
/*
|
||||
* Increment to force Svelte to re-render the props panel
|
||||
*/
|
||||
refreshPropsPanel: Writable<number>
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -678,6 +675,8 @@ export interface WidgetLayout extends IDragItem {
|
||||
export type DragItemID = UUID;
|
||||
|
||||
type LayoutStateOps = {
|
||||
workflow: ComfyWorkflow,
|
||||
|
||||
addContainer: (parent: ContainerLayout | null, attrs: Partial<Attributes>, index?: number) => ContainerLayout,
|
||||
addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes>, index?: number) => WidgetLayout,
|
||||
findDefaultContainerForInsertion: () => ContainerLayout | null,
|
||||
@@ -695,34 +694,57 @@ type LayoutStateOps = {
|
||||
onStartConfigure: () => void
|
||||
}
|
||||
|
||||
export type SerializedLayoutState = {
|
||||
root: DragItemID | null,
|
||||
allItems: Record<DragItemID, SerializedDragEntry>,
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
export type SerializedDragEntry = {
|
||||
dragItem: SerializedDragItem,
|
||||
children: DragItemID[],
|
||||
parent: DragItemID | null
|
||||
}
|
||||
|
||||
export type SerializedDragItem = {
|
||||
type: string,
|
||||
id: DragItemID,
|
||||
nodeId: UUID | null,
|
||||
attrs: Attributes
|
||||
}
|
||||
|
||||
export type WritableLayoutStateStore = Writable<LayoutState> & LayoutStateOps;
|
||||
const store: Writable<LayoutState> = writable({
|
||||
|
||||
function create(workflow: ComfyWorkflow): WritableLayoutStateStore {
|
||||
if (get(layoutStates).all[workflow.id] != null) {
|
||||
throw new Error(`Layout state already created! ${id}`)
|
||||
}
|
||||
|
||||
const store: Writable<LayoutState> = writable({
|
||||
root: null,
|
||||
allItems: {},
|
||||
allItemsByNode: {},
|
||||
isMenuOpen: false,
|
||||
isConfiguring: true,
|
||||
refreshPropsPanel: writable(0),
|
||||
attrs: {
|
||||
...defaultWorkflowAttributes
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
function clear() {
|
||||
function clear() {
|
||||
store.set({
|
||||
root: null,
|
||||
allItems: {},
|
||||
allItemsByNode: {},
|
||||
isMenuOpen: false,
|
||||
isConfiguring: true,
|
||||
refreshPropsPanel: writable(0),
|
||||
attrs: {
|
||||
...defaultWorkflowAttributes
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
function findDefaultContainerForInsertion(): ContainerLayout | null {
|
||||
function findDefaultContainerForInsertion(): ContainerLayout | null {
|
||||
const state = get(store);
|
||||
|
||||
if (state.root === null) {
|
||||
@@ -740,9 +762,9 @@ function findDefaultContainerForInsertion(): ContainerLayout | null {
|
||||
}
|
||||
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
function runOnChangedForWidgetDefaults(dragItem: IDragItem) {
|
||||
function runOnChangedForWidgetDefaults(dragItem: IDragItem) {
|
||||
for (const cat of Object.values(ALL_ATTRIBUTES)) {
|
||||
for (const spec of Object.values(cat.specs)) {
|
||||
if (defaultWidgetAttributes[spec.name] !== undefined && spec.onChanged != null) {
|
||||
@@ -750,9 +772,9 @@ function runOnChangedForWidgetDefaults(dragItem: IDragItem) {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index?: number): ContainerLayout {
|
||||
function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes> = {}, index?: number): ContainerLayout {
|
||||
const state = get(store);
|
||||
const dragItem: ContainerLayout = {
|
||||
type: "container",
|
||||
@@ -779,9 +801,9 @@ function addContainer(parent: ContainerLayout | null, attrs: Partial<Attributes>
|
||||
store.set(state)
|
||||
// runOnChangedForWidgetDefaults(dragItem)
|
||||
return dragItem;
|
||||
}
|
||||
}
|
||||
|
||||
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index?: number): WidgetLayout {
|
||||
function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial<Attributes> = {}, index?: number): WidgetLayout {
|
||||
const state = get(store);
|
||||
const widgetName = node.title || "Widget"
|
||||
const dragItem: WidgetLayout = {
|
||||
@@ -810,9 +832,9 @@ function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partia
|
||||
moveItem(dragItem, parent, index)
|
||||
// runOnChangedForWidgetDefaults(dragItem)
|
||||
return dragItem;
|
||||
}
|
||||
}
|
||||
|
||||
function updateChildren(parent: IDragItem, newChildren?: IDragItem[]): IDragItem[] {
|
||||
function updateChildren(parent: IDragItem, newChildren?: IDragItem[]): IDragItem[] {
|
||||
const state = get(store);
|
||||
if (newChildren)
|
||||
state.allItems[parent.id].children = newChildren;
|
||||
@@ -823,9 +845,9 @@ function updateChildren(parent: IDragItem, newChildren?: IDragItem[]): IDragItem
|
||||
}
|
||||
store.set(state)
|
||||
return state.allItems[parent.id].children
|
||||
}
|
||||
}
|
||||
|
||||
function removeEntry(state: LayoutState, id: DragItemID) {
|
||||
function removeEntry(state: LayoutState, id: DragItemID) {
|
||||
const entry = state.allItems[id]
|
||||
if (entry.children && entry.children.length > 0) {
|
||||
console.error(entry)
|
||||
@@ -841,9 +863,9 @@ function removeEntry(state: LayoutState, id: DragItemID) {
|
||||
delete state.allItemsByNode[widget.node.id]
|
||||
}
|
||||
delete state.allItems[id]
|
||||
}
|
||||
}
|
||||
|
||||
function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||
function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||
// Only concern ourselves with widget nodes
|
||||
if (!isComfyWidgetNode(node))
|
||||
return;
|
||||
@@ -901,9 +923,9 @@ function nodeAdded(node: LGraphNode, options: LGraphAddNodeOptions) {
|
||||
|
||||
console.debug("[layoutState] nodeAdded", node.id)
|
||||
addWidget(parent, node, attrs);
|
||||
}
|
||||
}
|
||||
|
||||
function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||
function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||
if (options.removedBy === "moveIntoSubgraph" || options.removedBy === "moveOutOfSubgraph") {
|
||||
// This node is being moved into a subgraph, so it will be readded under
|
||||
// a new node ID shortly.
|
||||
@@ -924,9 +946,9 @@ function nodeRemoved(node: LGraphNode, options: LGraphRemoveNodeOptions) {
|
||||
}
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
}
|
||||
|
||||
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||
function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||
const state = get(store)
|
||||
const entry = state.allItems[target.id]
|
||||
if (!entry || (entry.parent && entry.parent.id === to.id && entry.children.indexOf(target) === index))
|
||||
@@ -955,9 +977,9 @@ function moveItem(target: IDragItem, to: ContainerLayout, index?: number) {
|
||||
console.debug("[layoutState] Move child", target, toEntry, index)
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
}
|
||||
|
||||
function groupItems(dragItemIDs: DragItemID[], attrs: Partial<Attributes> = {}): ContainerLayout {
|
||||
function groupItems(dragItemIDs: DragItemID[], attrs: Partial<Attributes> = {}): ContainerLayout {
|
||||
if (dragItemIDs.length === 0)
|
||||
return;
|
||||
|
||||
@@ -985,9 +1007,9 @@ function groupItems(dragItemIDs: DragItemID[], attrs: Partial<Attributes> = {}):
|
||||
|
||||
store.set(state)
|
||||
return container
|
||||
}
|
||||
}
|
||||
|
||||
function ungroup(container: ContainerLayout) {
|
||||
function ungroup(container: ContainerLayout) {
|
||||
const state = get(store)
|
||||
|
||||
const parent = state.allItems[container.id].parent;
|
||||
@@ -1015,9 +1037,9 @@ function ungroup(container: ContainerLayout) {
|
||||
console.debug("[layoutState] Ungrouped", containerEntry, parent, parentChildren, index)
|
||||
|
||||
store.set(state)
|
||||
}
|
||||
}
|
||||
|
||||
function findLayoutEntryForNode(nodeId: ComfyNodeID): DragItemEntry | null {
|
||||
function findLayoutEntryForNode(nodeId: ComfyNodeID): DragItemEntry | null {
|
||||
const state = get(store)
|
||||
const found = Object.entries(state.allItems).find(pair =>
|
||||
pair[1].dragItem.type === "widget"
|
||||
@@ -1025,23 +1047,22 @@ function findLayoutEntryForNode(nodeId: ComfyNodeID): DragItemEntry | null {
|
||||
if (found)
|
||||
return found[1]
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function findLayoutForNode(nodeId: ComfyNodeID): WidgetLayout | null {
|
||||
function findLayoutForNode(nodeId: ComfyNodeID): WidgetLayout | null {
|
||||
const found = findLayoutEntryForNode(nodeId);
|
||||
if (!found)
|
||||
return null;
|
||||
return found.dragItem as WidgetLayout
|
||||
}
|
||||
}
|
||||
|
||||
function initDefaultLayout() {
|
||||
function initDefaultLayout() {
|
||||
store.set({
|
||||
root: null,
|
||||
allItems: {},
|
||||
allItemsByNode: {},
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false,
|
||||
refreshPropsPanel: writable(0),
|
||||
attrs: {
|
||||
...defaultWorkflowAttributes
|
||||
}
|
||||
@@ -1056,28 +1077,9 @@ function initDefaultLayout() {
|
||||
store.set(state)
|
||||
|
||||
console.debug("[layoutState] initDefault", state)
|
||||
}
|
||||
}
|
||||
|
||||
export type SerializedLayoutState = {
|
||||
root: DragItemID | null,
|
||||
allItems: Record<DragItemID, SerializedDragEntry>,
|
||||
attrs: LayoutAttributes
|
||||
}
|
||||
|
||||
export type SerializedDragEntry = {
|
||||
dragItem: SerializedDragItem,
|
||||
children: DragItemID[],
|
||||
parent: DragItemID | null
|
||||
}
|
||||
|
||||
export type SerializedDragItem = {
|
||||
type: string,
|
||||
id: DragItemID,
|
||||
nodeId: UUID | null,
|
||||
attrs: Attributes
|
||||
}
|
||||
|
||||
function serialize(): SerializedLayoutState {
|
||||
function serialize(): SerializedLayoutState {
|
||||
const state = get(store)
|
||||
|
||||
const allItems: Record<DragItemID, SerializedDragEntry> = {}
|
||||
@@ -1100,9 +1102,9 @@ function serialize(): SerializedLayoutState {
|
||||
allItems,
|
||||
attrs: state.attrs
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
const allItems: Record<DragItemID, DragItemEntry> = {}
|
||||
const allItemsByNode: Record<number, DragItemEntry> = {}
|
||||
for (const pair of Object.entries(data.allItems)) {
|
||||
@@ -1154,7 +1156,6 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
allItemsByNode,
|
||||
isMenuOpen: false,
|
||||
isConfiguring: false,
|
||||
refreshPropsPanel: writable(0),
|
||||
attrs: { ...defaultWorkflowAttributes, ...data.attrs }
|
||||
}
|
||||
|
||||
@@ -1163,19 +1164,23 @@ function deserialize(data: SerializedLayoutState, graph: LGraph) {
|
||||
store.set(state)
|
||||
|
||||
// Ensure properties panel is updated with new state
|
||||
state.refreshPropsPanel.set(get(state.refreshPropsPanel) + 1)
|
||||
}
|
||||
layoutStates.update(s => {
|
||||
s.refreshPropsPanel += 1;
|
||||
return s;
|
||||
})
|
||||
}
|
||||
|
||||
function onStartConfigure() {
|
||||
function onStartConfigure() {
|
||||
store.update(s => {
|
||||
s.isConfiguring = true;
|
||||
return s
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
const layoutStateStore: WritableLayoutStateStore =
|
||||
{
|
||||
const layoutStateStore: WritableLayoutStateStore =
|
||||
{
|
||||
...store,
|
||||
workflow,
|
||||
addContainer,
|
||||
addWidget,
|
||||
findDefaultContainerForInsertion,
|
||||
@@ -1191,5 +1196,73 @@ const layoutStateStore: WritableLayoutStateStore =
|
||||
onStartConfigure,
|
||||
serialize,
|
||||
deserialize
|
||||
}
|
||||
|
||||
layoutStates.update(s => {
|
||||
s.all[workflow.id] = layoutStateStore;
|
||||
return s;
|
||||
})
|
||||
|
||||
return layoutStateStore
|
||||
}
|
||||
export default layoutStateStore;
|
||||
|
||||
function remove(workflowID: WorkflowInstID) {
|
||||
const state = get(layoutStates)
|
||||
if (layoutStates[workflowID] == null)
|
||||
throw new Error(`No workflow with ID registered! ${workflowID}`)
|
||||
delete state.all[workflowID];
|
||||
}
|
||||
|
||||
function getLayout(workflowID: WorkflowInstID): WritableLayoutStateStore | null {
|
||||
return get(layoutStates).all[workflowID]
|
||||
}
|
||||
|
||||
function getLayoutByGraph(graph: LGraph): WritableLayoutStateStore | null {
|
||||
if ("workflowID" in graph && graph.workflowID != null) {
|
||||
return get(layoutStates).all[(graph as ComfyGraph).workflowID]
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function getLayoutByNode(node: LGraphNode): WritableLayoutStateStore | null {
|
||||
const rootGraph = node.getRootGraph();
|
||||
if (rootGraph == null)
|
||||
return null;
|
||||
|
||||
return getLayoutByGraph(rootGraph);
|
||||
}
|
||||
|
||||
export type LayoutStateStores = {
|
||||
all: Record<WorkflowInstID, WritableLayoutStateStore>,
|
||||
refreshPropsPanel: number
|
||||
}
|
||||
|
||||
export type LayoutStateStoresOps = {
|
||||
create: (workflow: ComfyWorkflow) => WritableLayoutStateStore,
|
||||
remove: (workflowID: WorkflowInstID) => void,
|
||||
getLayout: (workflowID: WorkflowInstID) => WritableLayoutStateStore | null,
|
||||
getLayoutByGraph: (graph: LGraph) => WritableLayoutStateStore | null,
|
||||
getLayoutByNode: (node: LGraphNode) => WritableLayoutStateStore | null,
|
||||
}
|
||||
|
||||
export type WritableLayoutStateStores = Writable<LayoutStateStores> & LayoutStateStoresOps;
|
||||
|
||||
const store = writable({
|
||||
all: {},
|
||||
|
||||
/*
|
||||
* Increment to force Svelte to re-render the props panel
|
||||
*/
|
||||
refreshPropsPanel: 0
|
||||
})
|
||||
|
||||
const layoutStates: WritableLayoutStateStores = {
|
||||
...store,
|
||||
create,
|
||||
remove,
|
||||
getLayout,
|
||||
getLayoutByGraph,
|
||||
getLayoutByNode
|
||||
}
|
||||
|
||||
export default layoutStates;
|
||||
@@ -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 = {
|
||||
|
||||
261
src/lib/stores/workflowState.ts
Normal file
261
src/lib/stores/workflowState.ts
Normal 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;
|
||||
@@ -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;
|
||||
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
@@ -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";
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
@@ -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" />
|
||||
|
||||
{#if layoutState}
|
||||
<div class="container">
|
||||
<WidgetContainer bind:dragItem={$layoutState.root} isMobile={true} classes={["root-container", "mobile"]} />
|
||||
<WidgetContainer bind:dragItem={$layoutState.root} {layoutState} isMobile={true} classes={["root-container", "mobile"]} />
|
||||
</div>
|
||||
{/if}
|
||||
</Page>
|
||||
|
||||
<style lang="scss">
|
||||
|
||||
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user