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

@@ -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} />