-
-
-
+
+{:else if widget && widget.node}
+
+
-
diff --git a/src/lib/components/ComfyUIPane.ts b/src/lib/components/ComfyUIPane.ts
deleted file mode 100644
index c555e03..0000000
--- a/src/lib/components/ComfyUIPane.ts
+++ /dev/null
@@ -1,7 +0,0 @@
-import type { LGraphNode } from "@litegraph-ts/core"
-
-export type DragItem = {
- id: number,
- node: LGraphNode,
- isNodeExecuting?: boolean
-}
diff --git a/src/lib/components/WidgetContainer.svelte b/src/lib/components/WidgetContainer.svelte
new file mode 100644
index 0000000..1fd3b41
--- /dev/null
+++ b/src/lib/components/WidgetContainer.svelte
@@ -0,0 +1,93 @@
+
+
+
+{#if container}
+ 1}
+ class:selected={$uiState.uiEditMode !== "disabled" && $layoutState.currentSelection.includes(widget.id)}
+ class:is-executing={$queueState.runningNodeId && $queueState.runningNodeId == widget.node.id}
+ >
+
+
+ {#if showHandles}
+
+ {/if}
+{/if}
+
+
diff --git a/src/lib/components/menu/ContextMenu.svelte b/src/lib/components/menu/ContextMenu.svelte
new file mode 100644
index 0000000..a4c963a
--- /dev/null
+++ b/src/lib/components/menu/ContextMenu.svelte
@@ -0,0 +1,47 @@
+
+
+{#if showMenu}
+
+{/if}
+
+
diff --git a/src/lib/components/menu/Icon.svelte b/src/lib/components/menu/Icon.svelte
new file mode 100644
index 0000000..0af8dc8
--- /dev/null
+++ b/src/lib/components/menu/Icon.svelte
@@ -0,0 +1,3 @@
+
diff --git a/src/lib/components/menu/Menu.svelte b/src/lib/components/menu/Menu.svelte
new file mode 100644
index 0000000..6322a94
--- /dev/null
+++ b/src/lib/components/menu/Menu.svelte
@@ -0,0 +1,45 @@
+
+
+
+
+ + diff --git a/src/lib/components/menu/MenuOption.svelte b/src/lib/components/menu/MenuOption.svelte new file mode 100644 index 0000000..bb7cca9 --- /dev/null +++ b/src/lib/components/menu/MenuOption.svelte @@ -0,0 +1,50 @@ + + + {
+ prompt: SerializedPrompt
+}
+
+export class ComfyAfterQueuedAction extends ComfyGraphNode {
+ override properties: ComfyCopyActionProperties = {
+ prompt: null
+ }
+
+ static slotLayout: SlotLayout = {
+ outputs: [
+ { name: "afterQueued", type: BuiltInSlotType.EVENT },
+ { name: "prompt", type: "*" }
+ ],
+ }
+
+ override onPropertyChanged(property: string, value: any, prevValue?: any) {
+ if (property === "value") {
+ this.setOutputData(0, this.properties.prompt)
+ }
+ }
+
+ override onExecute() {
+ this.setOutputData(0, this.properties.prompt)
+ }
+
+ override afterQueued(p: SerializedPrompt) {
+ this.setProperty("value", p)
+ this.triggerSlot(0, "bang")
+ }
+
+ override onSerialize(o: SerializedLGraphNode) {
+ super.onSerialize(o)
+ o.properties = { prompt: null }
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfyAfterQueuedAction,
+ title: "Comfy.AfterQueuedAction",
+ desc: "Triggers a 'bang' event when a prompt is queued.",
+ type: "actions/after_queued"
+})
+
+export interface ComfyCopyActionProperties extends Record {
+ value: any
+}
+
+export class ComfyCopyAction extends ComfyGraphNode {
+ override properties: ComfyCopyActionProperties = {
+ value: null
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "in", type: "*" },
+ { name: "copy", type: BuiltInSlotType.ACTION }
+ ],
+ outputs: [
+ { name: "out", type: "*" }
+ ],
+ }
+
+ displayWidget: ITextWidget;
+
+ constructor(title?: string) {
+ super(title);
+ this.displayWidget = this.addWidget(
+ "text",
+ "Value",
+ "",
+ "value"
+ );
+ this.displayWidget.disabled = true;
+ }
+
+ override onExecute() {
+ this.setProperty("value", this.getInputData(0))
+ }
+
+ override onAction(action: any, param: any) {
+ this.setProperty("value", this.getInputData(0))
+ this.setOutputData(0, this.properties.value)
+ console.log("setData", this.properties.value)
+ };
+}
+
+LiteGraph.registerNodeType({
+ class: ComfyCopyAction,
+ title: "Comfy.CopyAction",
+ desc: "Copies its input to its output when an event is received",
+ type: "actions/copy"
+})
+
+export interface ComfySwapActionProperties extends Record {
+}
+
+export class ComfySwapAction extends ComfyGraphNode {
+ override properties: ComfySwapActionProperties = {
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "A", type: "*" },
+ { name: "B", type: "*" },
+ { name: "swap", type: BuiltInSlotType.ACTION }
+ ],
+ outputs: [
+ { name: "B", type: "*" },
+ { name: "A", type: "*" }
+ ],
+ }
+
+ override onAction(action: any, param: any) {
+ const a = this.getInputData(0)
+ const b = this.getInputData(1)
+ this.setOutputData(0, a)
+ this.setOutputData(1, b)
+ };
+}
+
+LiteGraph.registerNodeType({
+ class: ComfySwapAction,
+ title: "Comfy.SwapAction",
+ desc: "Swaps two inputs when triggered",
+ type: "actions/swap"
+})
diff --git a/src/lib/nodes/ComfyBackendNode.ts b/src/lib/nodes/ComfyBackendNode.ts
new file mode 100644
index 0000000..3cde566
--- /dev/null
+++ b/src/lib/nodes/ComfyBackendNode.ts
@@ -0,0 +1,92 @@
+import LGraphCanvas from "@litegraph-ts/core/src/LGraphCanvas";
+import ComfyGraphNode from "./ComfyGraphNode";
+import ComfyWidgets from "$lib/widgets"
+import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
+
+/*
+ * Base class for any node with configuration sent by the backend.
+ */
+export class ComfyBackendNode extends ComfyGraphNode {
+ comfyClass: string;
+
+ constructor(title: string, comfyClass: string, nodeData: any) {
+ super(title)
+ this.type = comfyClass; // XXX: workaround dependency in LGraphNode.addInput()
+ this.comfyClass = comfyClass;
+ this.isBackendNode = true;
+
+ const color = LGraphCanvas.node_colors["yellow"];
+ this.color = color.color
+ this.bgColor = color.bgColor
+
+ this.setup(nodeData)
+
+ // ComfyUI has no obvious way to identify if a node will return outputs back to the frontend based on its properties.
+ // It just returns a hash like { "ui": { "images": results } } internally.
+ // So this will need to be hardcoded for now.
+ if (["PreviewImage", "SaveImage"].indexOf(comfyClass) !== -1) {
+ this.addOutput("output", "OUTPUT");
+ }
+ }
+
+ private setup(nodeData: any) {
+ var inputs = nodeData["input"]["required"];
+ if (nodeData["input"]["optional"] != undefined) {
+ inputs = Object.assign({}, nodeData["input"]["required"], nodeData["input"]["optional"])
+ }
+
+ const config = { minWidth: 1, minHeight: 1 };
+ for (const inputName in inputs) {
+ const inputData = inputs[inputName];
+ const type = inputData[0];
+
+ if (inputData[1]?.forceInput) {
+ this.addInput(inputName, type);
+ } else {
+ if (Array.isArray(type)) {
+ // Enums
+ Object.assign(config, ComfyWidgets.COMBO(this, inputName, inputData) || {});
+ } else if (`${type}:${inputName}` in ComfyWidgets) {
+ // Support custom ComfyWidgets by Type:Name
+ Object.assign(config, ComfyWidgets[`${type}:${inputName}`](this, inputName, inputData) || {});
+ } else if (type in ComfyWidgets) {
+ // Standard type ComfyWidgets
+ Object.assign(config, ComfyWidgets[type](this, inputName, inputData) || {});
+ } else {
+ // Node connection inputs (backend)
+ this.addInput(inputName, type);
+ }
+ }
+ }
+
+ for (const o in nodeData["output"]) {
+ const output = nodeData["output"][o];
+ const outputName = nodeData["output_name"][o] || output;
+ this.addOutput(outputName, output);
+ }
+
+ const s = this.computeSize();
+ s[0] = Math.max(config.minWidth, s[0] * 1.5);
+ s[1] = Math.max(config.minHeight, s[1]);
+ this.size = s;
+ this.serialize_widgets = false;
+
+ // app.#invokeExtensionsAsync("nodeCreated", this);
+ }
+
+ override onExecuted(outputData: any) {
+ console.warn("onExecuted outputs", outputData)
+ for (let index = 0; index < this.outputs.length; index++) {
+ const output = this.outputs[index]
+ if (output.type === "OUTPUT") {
+ this.setOutputData(index, outputData)
+ for (const node of this.getOutputNodes(index)) {
+ if ("receiveOutput" in node) {
+ const widgetNode = node as ComfyWidgetNode;
+ widgetNode.receiveOutput();
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/lib/nodes/ComfyGraphNode.ts b/src/lib/nodes/ComfyGraphNode.ts
index f3e6f86..acc8c24 100644
--- a/src/lib/nodes/ComfyGraphNode.ts
+++ b/src/lib/nodes/ComfyGraphNode.ts
@@ -1,9 +1,56 @@
+import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
+import type { SerializedPrompt } from "$lib/components/ComfyApp";
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
-import { LGraphNode } from "@litegraph-ts/core";
+import { LGraph, LGraphNode, LiteGraph, type SerializedLGraphNode } from "@litegraph-ts/core";
+import type { SvelteComponentDev } from "svelte/internal";
+import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
+import type IComfyInputSlot from "$lib/IComfyInputSlot";
+
+export type DefaultWidgetSpec = {
+ defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
+ config?: ComfyInputConfig
+}
+
+export type DefaultWidgetLayout = {
+ inputs?: Record,
+}
export default class ComfyGraphNode extends LGraphNode {
- isVirtualNode: boolean = false;
+ isBackendNode?: boolean;
- afterQueued?(): void;
+ afterQueued?(prompt: SerializedPrompt): void;
onExecuted?(output: any): void;
+
+ defaultWidgets?: DefaultWidgetLayout
+
+ override onSerialize(o: SerializedLGraphNode) {
+ for (let index = 0; index < this.inputs.length; index++) {
+ const input = this.inputs[index]
+ const serInput = o.inputs[index]
+ if ("defaultWidgetNode" in input) {
+ const comfyInput = input as IComfyInputSlot
+ const widgetNode = comfyInput.defaultWidgetNode
+ const ty = Object.values(LiteGraph.registered_node_types)
+ .find(v => v.class === widgetNode)
+ if (ty)
+ (serInput as any).widgetNodeType = ty.type;
+ (serInput as any).defaultWidgetNode = null
+ }
+ }
+ }
+
+ override onConfigure(o: SerializedLGraphNode) {
+ for (let index = 0; index < this.inputs.length; index++) {
+ const input = this.inputs[index]
+ const serInput = o.inputs[index]
+ if ("widgetNodeType" in serInput) {
+ const comfyInput = input as IComfyInputSlot
+ const ty: string = serInput.widgetNodeType as any
+ const widgetNode = Object.values(LiteGraph.registered_node_types)
+ .find(v => v.type === ty)
+ if (widgetNode)
+ comfyInput.defaultWidgetNode = widgetNode.class as any
+ }
+ }
+ }
}
diff --git a/src/lib/nodes/ComfyImageNodes.ts b/src/lib/nodes/ComfyImageNodes.ts
deleted file mode 100644
index 996e1e8..0000000
--- a/src/lib/nodes/ComfyImageNodes.ts
+++ /dev/null
@@ -1,43 +0,0 @@
-import ComfyGalleryWidget, { type ComfyGalleryEntry } from "$lib/widgets/ComfyGalleryWidget";
-import ComfyGraphNode from "./ComfyGraphNode";
-
-export type ComfyImageResult = {
- filename: string,
- subfolder: string,
- type: "output" | "temp"
-}
-export type ComfyImageExecOutput = {
- images: ComfyImageResult[]
-}
-
-/*
- * Node with a single extra image output widget
- */
-class ComfyImageNode extends ComfyGraphNode {
- private _imageResults: Array = [];
- private _galleryWidget: ComfyGalleryWidget;
-
- constructor(title?: any) {
- super(title)
- this._galleryWidget = new ComfyGalleryWidget("Images", [], this);
- this.addCustomWidget(this._galleryWidget);
- }
-
- override onExecuted(output: ComfyImageExecOutput) {
- this._imageResults = Array.from(output.images); // TODO append?
- const galleryItems = this._imageResults.map(r => {
- // TODO
- const url = "http://localhost:8188/view?"
- const params = new URLSearchParams(r)
- let entry: ComfyGalleryEntry = [url + params, null]
- return entry
- });
- this._galleryWidget.addImages(galleryItems);
- }
-}
-
-export class ComfySaveImageNode extends ComfyImageNode {
-}
-
-export class ComfyPreviewImageNode extends ComfyImageNode {
-}
diff --git a/src/lib/nodes/ComfyReroute.ts b/src/lib/nodes/ComfyReroute.ts
index 4127446..57d4e46 100644
--- a/src/lib/nodes/ComfyReroute.ts
+++ b/src/lib/nodes/ComfyReroute.ts
@@ -18,9 +18,6 @@ export default class ComfyReroute extends ComfyGraphNode {
}
}
- // This node is purely frontend and does not impact the resulting prompt so should not be serialized
- override isVirtualNode: boolean = true;
-
override titleMode: TitleMode = TitleMode.NO_TITLE;
override collapsable: boolean = false;
diff --git a/src/lib/nodes/ComfySelector.ts b/src/lib/nodes/ComfySelector.ts
new file mode 100644
index 0000000..d152d9c
--- /dev/null
+++ b/src/lib/nodes/ComfySelector.ts
@@ -0,0 +1,120 @@
+import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
+import ComfyGraphNode from "./ComfyGraphNode";
+
+export interface ComfySelectorProperties extends Record {
+ value: any
+}
+
+export default class ComfySelector extends ComfyGraphNode {
+ override properties: ComfySelectorProperties = {
+ value: null
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "select", type: "number" },
+ { name: "A", type: "*" },
+ { name: "B", type: "*" },
+ { name: "C", type: "*" },
+ { name: "D", type: "*" },
+ ],
+ outputs: [
+ { name: "out", type: "*" }
+ ],
+ }
+
+ private selected: number = 0;
+
+ constructor(title?: string) {
+ super(title);
+ }
+
+ override onDrawBackground(ctx: CanvasRenderingContext2D) {
+ if (this.flags.collapsed) {
+ return;
+ }
+ ctx.fillStyle = "#AFB";
+ var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6;
+ ctx.beginPath();
+ ctx.moveTo(50, y);
+ ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
+ ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
+ ctx.fill();
+ };
+
+ override onExecute() {
+ var sel = this.getInputData(0);
+ if (sel == null || sel.constructor !== Number)
+ sel = 0;
+ this.selected = sel = Math.round(sel) % (this.inputs.length - 1);
+ var v = this.getInputData(sel + 1);
+ if (v !== undefined) {
+ this.setOutputData(0, v);
+ }
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfySelector,
+ title: "Comfy.Selector",
+ desc: "Selects an output from two or more inputs",
+ type: "utils/selector"
+})
+
+export interface ComfySelectorTwoProperties extends Record {
+ value: any
+}
+
+export class ComfySelectorTwo extends ComfyGraphNode {
+ override properties: ComfySelectorTwoProperties = {
+ value: null
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "select", type: "boolean" },
+ { name: "true", type: "*" },
+ { name: "false", type: "*" },
+ ],
+ outputs: [
+ { name: "out", type: "*" }
+ ],
+ }
+
+ private selected: number = 0;
+
+ constructor(title?: string) {
+ super(title);
+ }
+
+ override onDrawBackground(ctx: CanvasRenderingContext2D) {
+ if (this.flags.collapsed) {
+ return;
+ }
+ ctx.fillStyle = "#AFB";
+ var y = (this.selected + 1) * LiteGraph.NODE_SLOT_HEIGHT + 6;
+ ctx.beginPath();
+ ctx.moveTo(50, y);
+ ctx.lineTo(50, y + LiteGraph.NODE_SLOT_HEIGHT);
+ ctx.lineTo(34, y + LiteGraph.NODE_SLOT_HEIGHT * 0.5);
+ ctx.fill();
+ };
+
+ override onExecute() {
+ var sel = this.getInputData(0);
+ if (sel == null || sel.constructor !== Boolean)
+ sel = 0;
+ this.selected = sel ? 0 : 1;
+ var v = this.getInputData(this.selected + 1);
+ if (v !== undefined) {
+ this.setOutputData(0, v);
+ }
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfySelectorTwo,
+ title: "Comfy.Selector2",
+ desc: "Selects an output from two inputs with a boolean",
+ type: "utils/selector2"
+})
diff --git a/src/lib/nodes/ComfyValueControl.ts b/src/lib/nodes/ComfyValueControl.ts
new file mode 100644
index 0000000..fed07ec
--- /dev/null
+++ b/src/lib/nodes/ComfyValueControl.ts
@@ -0,0 +1,107 @@
+import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
+import ComfyGraphNode, { type DefaultWidgetLayout } from "./ComfyGraphNode";
+import { clamp } from "$lib/utils";
+import ComboWidget from "$lib/widgets/ComboWidget.svelte";
+import { ComfyComboNode } from "./ComfyWidgetNodes";
+
+export interface ComfyValueControlProperties extends Record {
+ value: any,
+ action: "fixed" | "increment" | "decrement" | "randomize",
+ min: number,
+ max: number,
+ step: number
+}
+
+const INT_MAX = 1125899906842624;
+
+export default class ComfyValueControl extends ComfyGraphNode {
+ override properties: ComfyValueControlProperties = {
+ value: null,
+ action: "fixed",
+ min: -INT_MAX,
+ max: INT_MAX,
+ step: 1
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "value", type: "number" },
+ { name: "trigger", type: BuiltInSlotType.ACTION },
+ { name: "action", type: "string" },
+ { name: "min", type: "number" },
+ { name: "max", type: "number" },
+ { name: "step", type: "number" }
+ ],
+ outputs: [
+ { name: "value", type: "*" }
+ ],
+ }
+
+ override defaultWidgets: DefaultWidgetLayout = {
+ inputs: {
+ 2: {
+ defaultWidgetNode: ComfyComboNode,
+ config: {
+ defaultValue: "randomize",
+ values: ["fixed", "increment", "decrement", "randomize"]
+ }
+ }
+ }
+ }
+
+ constructor(title?: string) {
+ super(title);
+ }
+
+ override onExecute() {
+ this.setProperty("action", this.getInputData(2) || "fixed")
+ this.setProperty("min", this.getInputData(3))
+ this.setProperty("max", this.getInputData(4))
+ this.setProperty("step", this.getInputData(5) || 1)
+ }
+
+ override onAction(action: any, param: any) {
+ var v = this.getInputData(0)
+ if (typeof v !== "number")
+ return
+
+ let min = this.properties.min
+ let max = this.properties.max
+ if (min == null) min = -INT_MAX
+ if (max == null) max = INT_MAX
+
+ // limit to something that javascript can handle
+ min = Math.max(-INT_MAX, this.properties.min);
+ max = Math.min(INT_MAX, this.properties.max);
+ let range = (max - min) / (this.properties.step);
+
+ //adjust values based on valueControl Behaviour
+ switch (this.properties.action) {
+ case "fixed":
+ break;
+ case "increment":
+ v += this.properties.step;
+ break;
+ case "decrement":
+ v -= this.properties.step;
+ break;
+ case "randomize":
+ v = Math.floor(Math.random() * range) * (this.properties.step) + min;
+ default:
+ break;
+ }
+
+ v = clamp(v, min, max)
+ this.setProperty("value", v)
+ this.setOutputData(0, v)
+
+ console.debug("ValueControl", v, this.properties)
+ };
+}
+
+LiteGraph.registerNodeType({
+ class: ComfyValueControl,
+ title: "Comfy.ValueControl",
+ desc: "Adjusts an incoming value based on behavior",
+ type: "utils/value_control"
+})
diff --git a/src/lib/nodes/ComfyWidgetNodes.ts b/src/lib/nodes/ComfyWidgetNodes.ts
new file mode 100644
index 0000000..7c127a7
--- /dev/null
+++ b/src/lib/nodes/ComfyWidgetNodes.ts
@@ -0,0 +1,518 @@
+import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, LGraph, type INodeInputSlot, type ITextWidget, type INodeOutputSlot, type SerializedLGraphNode, BuiltInSlotType } from "@litegraph-ts/core";
+import ComfyGraphNode from "./ComfyGraphNode";
+import ComboWidget from "$lib/widgets/ComboWidget.svelte";
+import RangeWidget from "$lib/widgets/RangeWidget.svelte";
+import TextWidget from "$lib/widgets/TextWidget.svelte";
+import GalleryWidget from "$lib/widgets/GalleryWidget.svelte";
+import ButtonWidget from "$lib/widgets/ButtonWidget.svelte";
+import type { SvelteComponentDev } from "svelte/internal";
+import { Watch } from "@litegraph-ts/nodes-basic";
+import type IComfyInputSlot from "$lib/IComfyInputSlot";
+import { writable, type Unsubscriber, type Writable, get } from "svelte/store";
+import { clamp } from "$lib/utils"
+import layoutState from "$lib/stores/layoutState";
+import type { FileData as GradioFileData } from "@gradio/upload";
+import queueState from "$lib/stores/queueState";
+
+export interface ComfyWidgetProperties extends Record {
+ defaultValue: any
+}
+
+/*
+ * A node that is tied to a UI widget in the frontend. When the frontend's
+ * widget is changed, the value of the first output in the node is updated
+ * in the litegraph instance.
+ */
+export abstract class ComfyWidgetNode extends ComfyGraphNode {
+ abstract properties: ComfyWidgetProperties;
+
+ value: Writable
+ propsChanged: Writable = writable(0) // dummy to indicate if props changed
+ unsubscribe: Unsubscriber;
+
+ /** Svelte class for the frontend logic */
+ abstract svelteComponentType: typeof SvelteComponentDev
+
+ /** If false, user manually set min/max/step, and should not be autoinherited from connected input */
+ autoConfig: boolean = true;
+
+ copyFromInputLink: boolean = true;
+
+ /** Names of properties to add as inputs */
+ // shownInputProperties: string[] = []
+
+ /** Names of properties to add as outputs */
+ private shownOutputProperties: Record = {}
+ outputProperties: { name: string, type: string }[] = []
+
+ override isBackendNode = false;
+ override serialize_widgets = true;
+
+ outputIndex: number = 0;
+ inputIndex: number = 0;
+ changedIndex: number = 1;
+
+ displayWidget: ITextWidget;
+
+ override size: Vector2 = [60, 40];
+
+ constructor(name: string, value: T) {
+ const color = LGraphCanvas.node_colors["blue"]
+ super(name)
+ this.value = writable(value)
+ this.color ||= color.color
+ this.bgColor ||= color.bgColor
+ this.displayWidget = this.addWidget(
+ "text",
+ "Value",
+ ""
+ );
+ this.displayWidget.disabled = true; // prevent editing
+ this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
+ }
+
+ addPropertyAsOutput(propertyName: string, type: string) {
+ if (this.shownOutputProperties[propertyName])
+ return;
+
+ if (!(propertyName in this.properties)) {
+ throw `No property named ${propertyName} found!`
+ }
+
+ this.shownOutputProperties[propertyName] = { type, index: this.outputs.length }
+ this.addOutput(propertyName, type)
+ }
+
+ formatValue(value: any): string {
+ return Watch.toString(value)
+ }
+
+ private onValueUpdated(value: any) {
+ console.debug("[Widget] valueUpdated", this, value)
+ this.displayWidget.value = this.formatValue(value)
+
+ if (this.outputs.length >= this.outputIndex) {
+ this.setOutputData(this.outputIndex, get(this.value))
+ }
+ if (this.outputs.length >= this.changedIndex) {
+ const changedOutput = this.outputs[this.changedIndex]
+ if (changedOutput.type === BuiltInSlotType.EVENT)
+ this.triggerSlot(this.changedIndex, "changed")
+ }
+ }
+
+ setValue(value: any) {
+ this.value.set(value)
+ }
+
+ override onPropertyChanged(property: string, value: any, prevValue?: any) {
+ const data = this.shownOutputProperties[property]
+ if (data)
+ this.setOutputData(data.index, value)
+ }
+
+ /*
+ * Logic to run if this widget can be treated as output (slider, combo, text)
+ */
+ override onExecute() {
+ if (this.copyFromInputLink) {
+ if (this.inputs.length >= this.inputIndex) {
+ const data = this.getInputData(this.inputIndex)
+ if (data) { // TODO can "null" be a legitimate value here?
+ console.log(data)
+ this.setValue(data)
+ const input = this.getInputLink(this.inputIndex)
+ input.data = null;
+ }
+ }
+ }
+ if (this.outputs.length >= this.outputIndex) {
+ this.setOutputData(this.outputIndex, get(this.value))
+ }
+ for (const propName in this.shownOutputProperties) {
+ const data = this.shownOutputProperties[propName]
+ this.setOutputData(data.index, this.properties[propName])
+ }
+ }
+
+ /** Called when a backend node sends a ComfyUI output over a link */
+ receiveOutput() {
+ }
+
+ onConnectOutput(
+ outputIndex: number,
+ inputType: INodeInputSlot["type"],
+ input: INodeInputSlot,
+ inputNode: LGraphNode,
+ inputIndex: number
+ ): boolean {
+ if (this.autoConfig && "config" in input) {
+ this.doAutoConfig(input as IComfyInputSlot)
+ }
+
+ return true;
+ }
+
+ doAutoConfig(input: IComfyInputSlot) {
+ // Copy properties from default config in input slot
+ const comfyInput = input as IComfyInputSlot;
+ for (const key in comfyInput.config)
+ this.setProperty(key, comfyInput.config[key])
+
+ if ("defaultValue" in this.properties)
+ this.setValue(this.properties.defaultValue)
+
+ const widget = layoutState.findLayoutForNode(this.id)
+ if (widget && input.name !== "") {
+ widget.attrs.title = input.name;
+ }
+
+ console.debug("Property copy", input, this.properties)
+
+ this.setValue(get(this.value))
+ this.propsChanged.set(get(this.propsChanged) + 1)
+ }
+
+ onConnectionsChange(
+ type: LConnectionKind,
+ slotIndex: number,
+ isConnected: boolean,
+ link: LLink,
+ ioSlot: (INodeOutputSlot | INodeInputSlot)
+ ): void {
+ this.clampConfig();
+ }
+
+ clampConfig() {
+ let changed = false;
+ for (const link of this.getOutputLinks(0)) {
+ if (link) { // can be undefined if the link is removed
+ const node = this.graph._nodes_by_id[link.target_id]
+ if (node) {
+ const input = node.inputs[link.target_slot]
+ if (input && "config" in input) {
+ this.clampOneConfig(input as IComfyInputSlot)
+ changed = true;
+ }
+ }
+ }
+ }
+
+ // Force reactivity change so the frontend can be updated with the new props
+ this.propsChanged.set(get(this.propsChanged) + 1)
+ }
+
+ clampOneConfig(input: IComfyInputSlot) { }
+
+ override onSerialize(o: SerializedLGraphNode) {
+ super.onSerialize(o);
+ (o as any).comfyValue = get(this.value);
+ (o as any).shownOutputProperties = this.shownOutputProperties
+ }
+
+ override onConfigure(o: SerializedLGraphNode) {
+ this.value.set((o as any).comfyValue);
+ this.shownOutputProperties = (o as any).shownOutputProperties;
+ }
+}
+
+export interface ComfySliderProperties extends ComfyWidgetProperties {
+ min: number,
+ max: number,
+ step: number,
+ precision: number
+}
+
+export class ComfySliderNode extends ComfyWidgetNode {
+ override properties: ComfySliderProperties = {
+ defaultValue: 0,
+ min: 0,
+ max: 10,
+ step: 1,
+ precision: 1
+ }
+
+ override svelteComponentType = RangeWidget
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "value", type: "number" }
+ ],
+ outputs: [
+ { name: "value", type: "number" },
+ { name: "changed", type: BuiltInSlotType.EVENT },
+ ]
+ }
+
+ override outputProperties = [
+ { name: "min", type: "number" },
+ { name: "max", type: "number" },
+ { name: "step", type: "number" },
+ { name: "precision", type: "number" },
+ ]
+
+ constructor(name?: string) {
+ super(name, 0)
+ }
+
+ override setValue(value: any) {
+ if (typeof value !== "number")
+ return;
+ super.setValue(clamp(value, this.properties.min, this.properties.max))
+ }
+
+ override clampOneConfig(input: IComfyInputSlot) {
+ // this.setProperty("min", clamp(this.properties.min, input.config.min, input.config.max))
+ // this.setProperty("max", clamp(this.properties.max, input.config.max, input.config.min))
+ // this.setProperty("step", Math.min(this.properties.step, input.config.step))
+ this.setValue(this.properties.defaultValue)
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfySliderNode,
+ title: "UI.Slider",
+ desc: "Slider outputting a number value",
+ type: "ui/slider"
+})
+
+export interface ComfyComboProperties extends ComfyWidgetProperties {
+ values: string[]
+}
+
+export class ComfyComboNode extends ComfyWidgetNode {
+ override properties: ComfyComboProperties = {
+ defaultValue: "A",
+ values: ["A", "B", "C", "D"]
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "value", type: "string" }
+ ],
+ outputs: [
+ { name: "value", type: "string" },
+ { name: "changed", type: BuiltInSlotType.EVENT }
+ ]
+ }
+
+ override svelteComponentType = ComboWidget
+
+ constructor(name?: string) {
+ super(name, "A")
+ }
+
+ onConnectOutput(
+ outputIndex: number,
+ inputType: INodeInputSlot["type"],
+ input: INodeInputSlot,
+ inputNode: LGraphNode,
+ inputIndex: number
+ ): boolean {
+ if (!super.onConnectOutput(outputIndex, inputType, input, inputNode, inputIndex))
+ return false;
+
+ const thisProps = this.properties;
+ if (!("config" in input))
+ return true;
+
+ const comfyInput = input as IComfyInputSlot;
+ const otherProps = comfyInput.config;
+
+ // Ensure combo options match
+ if (!(otherProps.values instanceof Array))
+ return false;
+ if (thisProps.values.find((v, i) => otherProps.values.indexOf(v) === -1))
+ return false;
+
+ return true;
+ }
+
+ override setValue(value: any) {
+ if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
+ return;
+ super.setValue(value)
+ }
+
+ override clampOneConfig(input: IComfyInputSlot) {
+ if (input.config.values.indexOf(this.properties.value) === -1) {
+ if (input.config.values.length === 0)
+ this.setValue("")
+ else
+ this.setValue(input.config.defaultValue || input.config.values[0])
+ }
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfyComboNode,
+ title: "UI.Combo",
+ desc: "Combo box outputting a string value",
+ type: "ui/combo"
+})
+
+export interface ComfyTextProperties extends ComfyWidgetProperties {
+ multiline: boolean;
+}
+
+export class ComfyTextNode extends ComfyWidgetNode {
+ override properties: ComfyTextProperties = {
+ defaultValue: "",
+ multiline: false
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "value", type: "string" }
+ ],
+ outputs: [
+ { name: "value", type: "string" },
+ { name: "changed", type: BuiltInSlotType.EVENT }
+ ]
+ }
+
+ override svelteComponentType = TextWidget
+
+ constructor(name?: string) {
+ super(name, "")
+ }
+
+ override setValue(value: any) {
+ super.setValue(`${value}`)
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfyTextNode,
+ title: "UI.Text",
+ desc: "Textbox outputting a string value",
+ type: "ui/text"
+})
+
+/** Raw output as received from ComfyUI's backend */
+export type GalleryOutput = {
+ images: GalleryOutputEntry[]
+}
+
+/** Raw output entry as received from ComfyUI's backend */
+export type GalleryOutputEntry = {
+ filename: string,
+ subfolder: string,
+ type: string
+}
+
+export interface ComfyGalleryProperties extends ComfyWidgetProperties {
+}
+
+export class ComfyGalleryNode extends ComfyWidgetNode {
+ override properties: ComfyGalleryProperties = {
+ defaultValue: []
+ }
+
+ static slotLayout: SlotLayout = {
+ inputs: [
+ { name: "images", type: "OUTPUT" }
+ ]
+ }
+
+ override svelteComponentType = GalleryWidget
+ override copyFromInputLink = false;
+
+ constructor(name?: string) {
+ super(name, [])
+ }
+
+ override afterQueued() {
+ let queue = get(queueState)
+ if (!(typeof queue.queueRemaining === "number" && queue.queueRemaining > 1)) {
+ this.setValue([])
+ }
+ }
+
+ override formatValue(value: GradioFileData[] | null): string {
+ return `Images: ${value?.length || 0}`
+ }
+
+ private convertItems(output: GalleryOutput): GradioFileData[] {
+ return output.images.map(r => {
+ // TODO configure backend URL
+ const url = "http://localhost:8188/view?"
+ const params = new URLSearchParams(r)
+ return {
+ name: null,
+ data: url + params
+ }
+ });
+ }
+
+ override setValue(value: any) {
+ if (Array.isArray(value)) {
+ super.setValue(value)
+ }
+ else {
+ super.setValue([])
+ }
+ }
+
+ receiveOutput() {
+ const link = this.getInputLink(0)
+ if (link.data && "images" in link.data) {
+ const data = link.data as GalleryOutput
+ console.debug("[ComfyGalleryNode] Received output!", data)
+
+ const galleryItems: GradioFileData[] = this.convertItems(link.data)
+
+ const currentValue = get(this.value)
+ this.setValue(currentValue.concat(galleryItems))
+ }
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfyGalleryNode,
+ title: "UI.Gallery",
+ desc: "Gallery that shows most recent outputs",
+ type: "ui/gallery"
+})
+
+export interface ComfyButtonProperties extends ComfyWidgetProperties {
+ message: string
+}
+
+export class ComfyButtonNode extends ComfyWidgetNode {
+ override properties: ComfyButtonProperties = {
+ defaultValue: false,
+ message: "bang"
+ }
+
+ static slotLayout: SlotLayout = {
+ outputs: [
+ { name: "clicked", type: BuiltInSlotType.EVENT },
+ { name: "isClicked", type: "boolean" },
+ ]
+ }
+
+ override outputIndex = 1;
+ override svelteComponentType = ButtonWidget;
+
+ override setValue(value: any) {
+ super.setValue(Boolean(value))
+ }
+
+ onClick() {
+ this.setValue(true)
+ this.triggerSlot(0, this.properties.message);
+ this.setValue(false)
+ }
+
+ constructor(name?: string) {
+ super(name, false)
+ }
+}
+
+LiteGraph.registerNodeType({
+ class: ComfyButtonNode,
+ title: "UI.Button",
+ desc: "Button that triggers an event when clicked",
+ type: "ui/button"
+})
diff --git a/src/lib/nodes/index.ts b/src/lib/nodes/index.ts
index e355079..c2777d1 100644
--- a/src/lib/nodes/index.ts
+++ b/src/lib/nodes/index.ts
@@ -1,2 +1,5 @@
export { default as ComfyReroute } from "./ComfyReroute"
-export { ComfySaveImageNode, ComfyPreviewImageNode } from "./ComfyImageNodes"
+export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
+export { ComfyCopyAction, ComfySwapAction } from "./ComfyActionNodes"
+export { default as ComfyValueControl } from "./ComfyValueControl"
+export { default as ComfySelector } from "./ComfySelector"
diff --git a/src/lib/stores/layoutState.ts b/src/lib/stores/layoutState.ts
new file mode 100644
index 0000000..a299319
--- /dev/null
+++ b/src/lib/stores/layoutState.ts
@@ -0,0 +1,513 @@
+import { get, writable } from 'svelte/store';
+import type { Readable, Writable } from 'svelte/store';
+import type ComfyApp from "$lib/components/ComfyApp"
+import type { LGraphNode, IWidget, LGraph } from "@litegraph-ts/core"
+import { dndzone, SHADOW_PLACEHOLDER_ITEM_ID } from 'svelte-dnd-action';
+import type { ComfyWidgetNode } from '$lib/nodes';
+
+type DragItemEntry = {
+ dragItem: IDragItem,
+ children: IDragItem[] | null,
+ parent: IDragItem | null
+}
+
+export type LayoutState = {
+ root: IDragItem | null,
+ allItems: Record,
+ allItemsByNode: Record,
+ currentId: number,
+ currentSelection: DragItemID[],
+ isConfiguring: boolean,
+ isMenuOpen: boolean
+}
+
+export type AttributesSpec = {
+ name: string,
+ type: string,
+ editable: boolean
+}
+
+export type AttributesCategorySpec = {
+ categoryName: string,
+ specs: AttributesSpec[]
+}
+
+export type AttributesSpecList = AttributesCategorySpec[]
+
+const ALL_ATTRIBUTES: AttributesSpecList = [
+ {
+ categoryName: "appearance",
+ specs: [
+ {
+ name: "title",
+ type: "string",
+ editable: true,
+ },
+ {
+ name: "showTitle",
+ type: "boolean",
+ editable: true,
+ },
+ {
+ name: "direction",
+ type: "string",
+ editable: true,
+ },
+ {
+ name: "classes",
+ type: "string",
+ editable: true,
+ },
+ ]
+ }
+];
+export { ALL_ATTRIBUTES };
+
+export type Attributes = {
+ direction: "horizontal" | "vertical",
+ title: string,
+ showTitle: boolean,
+ classes: string
+}
+
+export interface IDragItem {
+ type: string,
+ id: DragItemID,
+ isNodeExecuting?: boolean,
+ attrs: Attributes
+}
+
+export interface ContainerLayout extends IDragItem {
+ type: "container",
+}
+
+export interface WidgetLayout extends IDragItem {
+ type: "widget",
+ node: ComfyWidgetNode
+}
+
+type DragItemID = string;
+
+type LayoutStateOps = {
+ addContainer: (parent: ContainerLayout | null, attrs: Partial, index: number) => ContainerLayout,
+ addWidget: (parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial, index: number) => WidgetLayout,
+ findDefaultContainerForInsertion: () => ContainerLayout | null,
+ updateChildren: (parent: IDragItem, children: IDragItem[]) => IDragItem[],
+ nodeAdded: (node: LGraphNode) => void,
+ nodeRemoved: (node: LGraphNode) => void,
+ groupItems: (dragItems: IDragItem[], attrs?: Partial) => ContainerLayout,
+ ungroup: (container: ContainerLayout) => void,
+ getCurrentSelection: () => IDragItem[],
+ findLayoutForNode: (nodeId: number) => IDragItem | null;
+ serialize: () => SerializedLayoutState,
+ deserialize: (data: SerializedLayoutState, graph: LGraph) => void,
+ initDefaultLayout: () => void,
+ onStartConfigure: () => void
+}
+
+export type WritableLayoutStateStore = Writable & LayoutStateOps;
+const store: Writable = writable({
+ root: null,
+ allItems: {},
+ allItemsByNode: {},
+ currentId: 0,
+ currentSelection: [],
+ isMenuOpen: false,
+ isConfiguring: true
+})
+
+function findDefaultContainerForInsertion(): ContainerLayout | null {
+ const state = get(store);
+
+ if (state.root === null) {
+ // Should never happen
+ throw "Root container was null!";
+ }
+
+ if (state.root.type === "container") {
+ const container = state.root as ContainerLayout;
+ const children: IDragItem[] = state.allItems[container.id]?.children || []
+ const found = children.find((di) => di.type === "container")
+ if (found && found.type === "container")
+ return found as ContainerLayout;
+ return container;
+ }
+
+ return null
+}
+
+function addContainer(parent: ContainerLayout | null, attrs: Partial = {}, index: number = -1): ContainerLayout {
+ const state = get(store);
+ const dragItem: ContainerLayout = {
+ type: "container",
+ id: `${state.currentId++}`,
+ attrs: {
+ title: "Container",
+ showTitle: true,
+ direction: "vertical",
+ classes: "",
+ ...attrs
+ }
+ }
+ const entry: DragItemEntry = { dragItem, children: [], parent: null };
+ state.allItems[dragItem.id] = entry;
+ if (parent) {
+ moveItem(dragItem, parent)
+ }
+ console.debug("[layoutState] addContainer", state)
+ store.set(state)
+ return dragItem;
+}
+
+function addWidget(parent: ContainerLayout, node: ComfyWidgetNode, attrs: Partial = {}, index: number = -1): WidgetLayout {
+ const state = get(store);
+ const widgetName = "Widget"
+ const dragItem: WidgetLayout = {
+ type: "widget",
+ id: `${state.currentId++}`,
+ node: node,
+ attrs: {
+ title: widgetName,
+ showTitle: true,
+ direction: "horizontal",
+ classes: "",
+ ...attrs
+ }
+ }
+ const parentEntry = state.allItems[parent.id]
+ const entry: DragItemEntry = { dragItem, children: [], parent: null };
+ state.allItems[dragItem.id] = entry;
+ state.allItemsByNode[node.id] = entry;
+ console.debug("[layoutState] addWidget", state)
+ moveItem(dragItem, parent)
+ return dragItem;
+}
+
+function updateChildren(parent: IDragItem, newChildren?: IDragItem[]): IDragItem[] {
+ const state = get(store);
+ if (newChildren)
+ state.allItems[parent.id].children = newChildren;
+ for (const child of state.allItems[parent.id].children) {
+ if (child.id === SHADOW_PLACEHOLDER_ITEM_ID)
+ continue;
+ state.allItems[child.id].parent = parent;
+ }
+ store.set(state)
+ return state.allItems[parent.id].children
+}
+
+function nodeAdded(node: LGraphNode) {
+ const state = get(store)
+ if (state.isConfiguring)
+ return;
+
+ const parent = findDefaultContainerForInsertion();
+
+ // Two cases where we want to add nodes:
+ // 1. User adds a new UI node, so we should instantiate its widget in the frontend.
+ // 2. User adds a node with inputs that can be filled by frontend widgets.
+ // Depending on config, this means we should instantiate default UI nodes connected to those inputs.
+
+ console.debug(node)
+ if ("svelteComponentType" in node) {
+ addWidget(parent, node as ComfyWidgetNode);
+ }
+
+ // Add default node panel with all widgets autoinstantiated
+ // if (node.widgets && node.widgets.length > 0) {
+ // const container = addContainer(parent.id, { title: node.title, direction: "vertical", associatedNode: node.id });
+ // for (const widget of node.widgets) {
+ // addWidget(container.id, node, widget, { associatedNode: node.id });
+ // }
+ // }
+}
+
+function removeEntry(state: LayoutState, id: DragItemID) {
+ const entry = state.allItems[id]
+ if (entry.children && entry.children.length > 0) {
+ console.error(entry)
+ throw `Tried removing entry ${id} but it still had children!`
+ }
+ const parent = entry.parent;
+ if (parent) {
+ const parentEntry = state.allItems[parent.id];
+ parentEntry.children = parentEntry.children.filter(item => item.id !== id)
+ }
+ if (entry.dragItem.type === "widget") {
+ const widget = entry.dragItem as WidgetLayout;
+ delete state.allItemsByNode[widget.node.id]
+ }
+ delete state.allItems[id]
+}
+
+function nodeRemoved(node: LGraphNode) {
+ const state = get(store)
+
+ console.debug("[layoutState] nodeRemoved", node)
+
+ let del = Object.entries(state.allItems).filter(pair =>
+ pair[1].dragItem.type === "widget"
+ && (pair[1].dragItem as WidgetLayout).node.id === node.id)
+
+ for (const pair of del) {
+ const [id, dragItem] = pair;
+ removeEntry(state, id)
+ }
+
+ store.set(state)
+}
+
+function moveItem(target: IDragItem, to: ContainerLayout, index: number = -1) {
+ const state = get(store)
+ const entry = state.allItems[target.id]
+ if (entry.parent && entry.parent.id === to.id)
+ return;
+
+ if (entry.parent) {
+ const parentEntry = state.allItems[entry.parent.id];
+ const index = parentEntry.children.findIndex(c => c.id === target.id)
+ if (index !== -1) {
+ parentEntry.children.splice(index, 1)
+ }
+ else {
+ console.error(parentEntry)
+ console.error(target)
+ throw "Child not found in parent!"
+ }
+ }
+
+ const toEntry = state.allItems[to.id];
+ if (index !== -1)
+ toEntry.children.splice(index, 0, target)
+ else
+ toEntry.children.push(target)
+ state.allItems[target.id].parent = toEntry.dragItem;
+
+ console.debug("[layoutState] Move child", target, toEntry, index)
+
+ store.set(state)
+}
+
+function getCurrentSelection(): IDragItem[] {
+ const state = get(store)
+ return state.currentSelection.map(id => state.allItems[id].dragItem)
+}
+
+function groupItems(dragItems: IDragItem[], attrs: Partial = {}): ContainerLayout {
+ if (dragItems.length === 0)
+ return;
+
+ const state = get(store)
+ const parent = state.allItems[dragItems[0].id].parent || findDefaultContainerForInsertion();
+
+ if (parent === null || parent.type !== "container")
+ return;
+
+ let index = undefined;
+ if (parent) {
+ const indexFound = state.allItems[parent.id].children.findIndex(c => c.id === dragItems[0].id)
+ if (indexFound !== -1)
+ index = indexFound
+ }
+
+ const container = addContainer(parent as ContainerLayout, attrs, index)
+
+ for (const item of dragItems) {
+ moveItem(item, container)
+ }
+
+ console.debug("[layoutState] Grouped", container, parent, state.allItems[container.id].children, index)
+
+ store.set(state)
+ return container
+}
+
+function ungroup(container: ContainerLayout) {
+ const state = get(store)
+
+ const parent = state.allItems[container.id].parent;
+ if (!parent || parent.type !== "container") {
+ console.warn("No parent to ungroup into!", container)
+ return;
+ }
+
+ let index = undefined;
+ const parentChildren = state.allItems[parent.id].children;
+ const indexFound = parentChildren.findIndex(c => c.id === container.id)
+ if (indexFound !== -1)
+ index = indexFound
+
+ const containerEntry = state.allItems[container.id]
+ console.debug("[layoutState] About to ungroup", containerEntry, parent, parentChildren, index)
+
+ const children = [...containerEntry.children]
+ for (const item of children) {
+ moveItem(item, parent as ContainerLayout, index)
+ }
+
+ removeEntry(state, container.id)
+
+ console.debug("[layoutState] Ungrouped", containerEntry, parent, parentChildren, index)
+
+ store.set(state)
+}
+
+function findLayoutForNode(nodeId: number): WidgetLayout | null {
+ const state = get(store)
+ const found = Object.entries(state.allItems).find(pair =>
+ pair[1].dragItem.type === "widget"
+ && (pair[1].dragItem as WidgetLayout).node.id === nodeId)
+ if (found)
+ return found[1].dragItem as WidgetLayout
+ return null;
+}
+
+function initDefaultLayout() {
+ store.set({
+ root: null,
+ allItems: {},
+ currentId: 0,
+ currentSelection: [],
+ isMenuOpen: false,
+ isConfiguring: false
+ })
+
+ const root = addContainer(null, { direction: "horizontal", showTitle: false });
+ const left = addContainer(root, { direction: "vertical", showTitle: false });
+ const right = addContainer(root, { direction: "vertical", showTitle: false });
+
+ const state = get(store)
+ state.root = root;
+ store.set(state)
+
+ console.debug("[layoutState] initDefault", state)
+}
+
+export type SerializedLayoutState = {
+ root: DragItemID | null,
+ allItems: Record,
+ currentId: number,
+}
+
+export type SerializedDragEntry = {
+ dragItem: SerializedDragItem,
+ children: DragItemID[],
+ parent: DragItemID | null
+}
+
+export type SerializedDragItem = {
+ type: string,
+ id: DragItemID,
+ nodeId: number | null,
+ attrs: Attributes
+}
+
+function serialize(): SerializedLayoutState {
+ const state = get(store)
+
+ const allItems: Record = {}
+ for (const pair of Object.entries(state.allItems)) {
+ const [id, entry] = pair;
+ allItems[id] = {
+ dragItem: {
+ type: entry.dragItem.type,
+ id: entry.dragItem.id,
+ nodeId: (entry.dragItem as any).node?.id,
+ attrs: entry.dragItem.attrs
+ },
+ children: entry.children.map((di) => di.id),
+ parent: entry.parent?.id
+ }
+ }
+
+ return {
+ root: state.root?.id,
+ allItems,
+ currentId: state.currentId,
+ }
+}
+
+function deserialize(data: SerializedLayoutState, graph: LGraph) {
+ const allItems: Record = {}
+ const allItemsByNode: Record = {}
+ for (const pair of Object.entries(data.allItems)) {
+ const [id, entry] = pair;
+
+ const dragItem: IDragItem = {
+ type: entry.dragItem.type,
+ id: entry.dragItem.id,
+ attrs: entry.dragItem.attrs
+ };
+
+ const dragEntry: DragItemEntry = {
+ dragItem,
+ children: [],
+ parent: null
+ }
+
+ allItems[id] = dragEntry
+
+ if (dragItem.type === "widget") {
+ const widget = dragItem as WidgetLayout;
+ widget.node = graph.getNodeById(entry.dragItem.nodeId) as ComfyWidgetNode
+ allItemsByNode[entry.dragItem.nodeId] = dragEntry
+ }
+ }
+
+ // reconnect parent/child tree
+ for (const pair of Object.entries(data.allItems)) {
+ const [id, entry] = pair;
+
+ for (const childId of entry.children) {
+ allItems[id].children.push(allItems[childId].dragItem)
+ }
+ if (entry.parent) {
+ allItems[id].parent = allItems[entry.parent].dragItem;
+ }
+ }
+
+ let root: IDragItem = null;
+ if (data.root)
+ root = allItems[data.root].dragItem
+
+ const state: LayoutState = {
+ root,
+ allItems,
+ allItemsByNode,
+ currentId: data.currentId,
+ currentSelection: [],
+ isMenuOpen: false,
+ isConfiguring: false
+ }
+
+ console.debug("[layoutState] deserialize", data, state)
+
+ store.set(state)
+}
+
+function onStartConfigure() {
+ store.update(s => {
+ s.isConfiguring = true;
+ return s
+ })
+}
+
+const layoutStateStore: WritableLayoutStateStore =
+{
+ ...store,
+ addContainer,
+ addWidget,
+ findDefaultContainerForInsertion,
+ updateChildren,
+ nodeAdded,
+ nodeRemoved,
+ getCurrentSelection,
+ groupItems,
+ findLayoutForNode,
+ ungroup,
+ initDefaultLayout,
+ onStartConfigure,
+ serialize,
+ deserialize
+}
+export default layoutStateStore;
diff --git a/src/lib/stores/nodeState.ts b/src/lib/stores/nodeState.ts
deleted file mode 100644
index ccd6eae..0000000
--- a/src/lib/stores/nodeState.ts
+++ /dev/null
@@ -1,71 +0,0 @@
-import { writable, get } from 'svelte/store';
-import type { LGraph, LGraphNode } from "@litegraph-ts/core";
-import type { Readable, Writable } from 'svelte/store';
-import type ComfyGraphNode from '$lib/nodes/ComfyGraphNode';
-
-/** store for one node's state */
-export type NodeUIStateStore = Writable
-
-export type NodeUIState = {
- name: string,
- node: LGraphNode
-}
-
-type NodeID = number;
-
-type NodeStateOps = {
- nodeAdded: (node: LGraphNode) => void,
- nodeRemoved: (node: LGraphNode) => void,
- configureFinished: (graph: LGraph) => void,
- nodeStateChanged: (node: LGraphNode) => void,
- clear: () => void,
-}
-
-export type NodeStateStore = Record;
-type WritableNodeStateStore = Writable & NodeStateOps;
-
-const store: Writable = writable({})
-
-function clear() {
- store.set({})
-}
-
-function nodeAdded(node: LGraphNode) {
- let state = get(store)
- state[node.id] = { node: node, name: node.name }
- store.set(state);
-}
-
-function nodeRemoved(node: LGraphNode) {
- const state = get(store)
- delete state[node.id]
- store.set(state)
-}
-
-function nodeStateChanged(node: LGraphNode) {
- const state = get(store)
- const nodeState = state[node.id]
- nodeState.name = node.name
- store.set(state);
-}
-
-function configureFinished(graph: LGraph) {
- let state = get(store);
-
- for (const node of graph.computeExecutionOrder(false, null)) {
- state[node.id].name = name;
- }
-
- store.set(state)
-}
-
-const nodeStateStore: WritableNodeStateStore =
-{
- ...store,
- nodeAdded,
- nodeRemoved,
- nodeStateChanged,
- configureFinished,
- clear
-}
-export default nodeStateStore;
diff --git a/src/lib/stores/uiState.ts b/src/lib/stores/uiState.ts
index 0fbb6b8..e9422be 100644
--- a/src/lib/stores/uiState.ts
+++ b/src/lib/stores/uiState.ts
@@ -2,15 +2,25 @@ import { writable } from 'svelte/store';
import type { Readable, Writable } from 'svelte/store';
import type ComfyApp from "$lib/components/ComfyApp"
+export type UIEditMode = "disabled" | "widgets" | "containers" | "layout";
+
export type UIState = {
app: ComfyApp,
nodesLocked: boolean,
graphLocked: boolean,
- unlocked: boolean,
+ autoAddUI: boolean,
+ uiEditMode: UIEditMode
}
export type WritableUIStateStore = Writable;
-const store: WritableUIStateStore = writable({ unlocked: false, graphLocked: true, nodesLocked:false })
+const store: WritableUIStateStore = writable(
+ {
+ app: null,
+ graphLocked: false,
+ nodesLocked: false,
+ autoAddUI: true,
+ uiEditMode: "disabled",
+ })
const uiStateStore: WritableUIStateStore =
{
diff --git a/src/lib/stores/widgetState.ts b/src/lib/stores/widgetState.ts
deleted file mode 100644
index 06084b9..0000000
--- a/src/lib/stores/widgetState.ts
+++ /dev/null
@@ -1,132 +0,0 @@
-import { writable, get } from 'svelte/store';
-import type { LGraph, LGraphNode, IWidget } from "@litegraph-ts/core";
-import type { Readable, Writable } from 'svelte/store';
-import type ComfyGraphNode from '$lib/nodes/ComfyGraphNode';
-import type ComfyWidget from '$lib/widgets/ComfyWidget';
-
-/** store for one widget's state */
-export type WidgetUIStateStore = Writable
-
-export type WidgetUIState = {
- /** position in the node's list of widgets */
- index: number,
- /** parent node containing the widget */
- node: LGraphNode,
- /** actual widget instance */
- widget: IWidget,
- /** widget value as a store, to react to changes */
- value: WidgetUIStateStore,
- /**
- * true if this widget was added purely from the frontend. what this means:
- * - this widget's state will not be saved to the workflow
- * - the widget was added on startup by some subclass of ComfyGraphNode
- */
- isVirtual: boolean
-}
-
-export type WidgetDrawState = {
- isNodeExecuting: boolean
-}
-
-type NodeID = number;
-
-type WidgetStateOps = {
- nodeAdded: (node: LGraphNode) => void,
- nodeRemoved: (node: LGraphNode) => void,
- configureFinished: (graph: LGraph) => void,
- widgetStateChanged: (nodeId: number, widget: IWidget) => void,
- findWidgetByName: (nodeId: number, name: string) => WidgetUIState | null,
- clear: () => void,
-}
-
-export type WidgetStateStore = Record;
-type WritableWidgetStateStore = Writable & WidgetStateOps;
-
-const store: Writable = writable({})
-
-function clear() {
- store.set({})
-}
-
-function nodeAdded(node: LGraphNode) {
- let state = get(store)
-
- if (node.widgets) {
- for (const [index, widget] of node.widgets.entries()) {
- if (!state[node.id])
- state[node.id] = []
- let isVirtual = false;
- if ("isVirtual" in widget)
- isVirtual = (widget as ComfyWidget).isVirtual;
- state[node.id].push({ index, node, widget, value: writable(widget.value), isVirtual: isVirtual })
- }
- }
-
- console.debug("NODEADDED", state)
-
- store.set(state);
-}
-
-function nodeRemoved(node: LGraphNode) {
- const state = get(store)
- delete state[node.id]
- store.set(state)
-}
-
-function widgetStateChanged(nodeId: number, widget: IWidget) {
- const state = get(store)
- const entries = state[nodeId]
- if (entries) {
- let widgetState = entries.find(e => e.widget === widget);
- if (widgetState) {
- widgetState.value.set(widget.value);
- store.set(state);
- }
- else {
- console.error("Widget state changed and node was found, but widget was not found in state!", widget, widget.node, entries)
- }
- }
- else {
- console.error("Widget state changed but node was not found in state!", widget, widget.node)
- }
-}
-
-function configureFinished(graph: LGraph) {
- let state = get(store);
-
- for (const node of graph.computeExecutionOrder(false, null)) {
- if (node.widgets_values) {
- for (const [i, value] of node.widgets_values.entries()) {
- if (i < state[node.id].length && !state[node.id][i].isVirtual) {
- state[node.id][i].value.set(value);
- }
- else {
- console.log("Skip virtual widget", node.id, node.type, state[node.id][i].widget)
- }
- }
- }
- }
-
- store.set(state)
-}
-
-function findWidgetByName(nodeId: number, name: string): WidgetUIState | null {
- let state = get(store);
-
- if (!(nodeId in state))
- return null;
-
- return state[nodeId].find((v) => v.widget.name === name);
-}
-
-const widgetStateStore: WritableWidgetStateStore =
-{
- ...store,
- nodeAdded,
- nodeRemoved,
- widgetStateChanged,
- configureFinished,
- findWidgetByName,
- clear
-}
-export default widgetStateStore;
diff --git a/src/lib/utils.ts b/src/lib/utils.ts
index 6f24cad..f5c47a7 100644
--- a/src/lib/utils.ts
+++ b/src/lib/utils.ts
@@ -2,7 +2,13 @@ import ComfyApp from "./components/ComfyApp";
import ComboWidget from "$lib/widgets/ComboWidget.svelte";
import RangeWidget from "$lib/widgets/RangeWidget.svelte";
import TextWidget from "$lib/widgets/TextWidget.svelte";
- import widgetState, { type WidgetDrawState, type WidgetUIState } from "$lib/stores/widgetState";
+import { get } from "svelte/store"
+import layoutState from "$lib/stores/layoutState"
+import type { SvelteComponentDev } from "svelte/internal";
+
+export function clamp(n: number, min: number, max: number): number {
+ return Math.min(Math.max(n, min), max)
+}
export function download(filename: string, text: string, type: string = "text/plain") {
const blob = new Blob([text], { type: type });
@@ -18,24 +24,31 @@ export function download(filename: string, text: string, type: string = "text/pl
}, 0);
}
-export function getComponentForWidgetState(item: WidgetUIState): any {
- let ctor: any = null;
+export function startDrag(evt: MouseEvent) {
+ const dragItemId: string = evt.target.dataset["dragItemId"];
+ const ls = get(layoutState)
- // custom widgets with TypeScript sources
- let override = ComfyApp.widget_type_overrides[item.widget.type]
- if (override) {
- return override;
+ if (evt.button !== 0) {
+ if (ls.currentSelection.length <= 1 && !ls.isMenuOpen)
+ ls.currentSelection = [dragItemId]
+ return;
}
- // litegraph.ts built-in widgets
- switch (item.widget.type) {
- case "combo":
- return ComboWidget;
- case "number":
- return RangeWidget;
- case "text":
- return TextWidget;
- }
+ const item = ls.allItems[dragItemId].dragItem
- return null;
-}
+ if (evt.ctrlKey) {
+ const index = ls.currentSelection.indexOf(item.id)
+ if (index === -1)
+ ls.currentSelection.push(item.id);
+ else
+ ls.currentSelection.splice(index, 1);
+ ls.currentSelection = ls.currentSelection;
+ }
+ else {
+ ls.currentSelection = [item.id]
+ }
+ layoutState.set(ls)
+};
+
+export function stopDrag(evt: MouseEvent) {
+};
diff --git a/src/lib/widgets.ts b/src/lib/widgets.ts
index 2bf5bab..e3083aa 100644
--- a/src/lib/widgets.ts
+++ b/src/lib/widgets.ts
@@ -1,147 +1,80 @@
import type { IWidget, LGraphNode } from "@litegraph-js/core";
-import type ComfyApp from "$lib/components/ComfyApp";
import ComfyValueControlWidget from "./widgets/ComfyValueControlWidget";
+import type { ComfyInputConfig } from "./IComfyInputSlot";
+import type IComfyInputSlot from "./IComfyInputSlot";
+import { BuiltInSlotShape, LiteGraph } from "@litegraph-ts/core";
+import { ComfyComboNode, ComfySliderNode, ComfyTextNode } from "./nodes";
-export interface WidgetData {
- widget: IWidget,
- minWidth?: number,
- minHeight?: number
-}
+type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any) => IComfyInputSlot;
-type WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app: ComfyApp) => WidgetData;
-
-
-type NumberConfig = { min: number, max: number, step: number, precision: number }
-type NumberDefaults = { val: number, config: NumberConfig }
-
-function getNumberDefaults(inputData: any, defaultStep: number): NumberDefaults {
- let defaultVal = inputData[1]["default"];
+function getNumberDefaults(inputData: any, defaultStep: number): ComfyInputConfig {
+ let defaultValue = inputData[1]["default"];
let { min, max, step } = inputData[1];
- if (defaultVal == undefined) defaultVal = 0;
+ if (defaultValue == undefined) defaultValue = 0;
if (min == undefined) min = 0;
if (max == undefined) max = 2048;
if (step == undefined) step = defaultStep;
- return { val: defaultVal, config: { min, max, step: step, precision: 0 } };
+ return { min, max, step: step, precision: 0, defaultValue };
}
+function addComfyInput(node: LGraphNode, inputName: string, extraInfo: Partial = {}): IComfyInputSlot {
+ const input = node.addInput(inputName) as IComfyInputSlot
+ for (const [k, v] of Object.entries(extraInfo))
+ input[k] = v
-const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
- const { val, config } = getNumberDefaults(inputData, 0.5);
- return { widget: node.addWidget("number", inputName, val, () => { }, config) };
+ if (input.defaultWidgetNode) {
+ const ty = Object.values(LiteGraph.registered_node_types)
+ .find(v => v.class === input.defaultWidgetNode)
+ if (ty)
+ input.widgetNodeType = ty.type
+ }
+
+ input.serialize = true;
+ return input;
}
+const FLOAT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
+ const config = getNumberDefaults(inputData, 0.5);
+ return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
+}
-const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
- const { val, config } = getNumberDefaults(inputData, 1);
- return {
- widget: node.addWidget(
- "number",
- inputName,
- val,
- function(v) {
- const s = this.options.step;
- this.value = Math.round(v / s) * s;
- },
- config
- ),
- };
+const INT: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
+ const config = getNumberDefaults(inputData, 1);
+ return addComfyInput(node, inputName, { type: "number", config, defaultWidgetNode: ComfySliderNode })
};
-function seedWidget(node, inputName, inputData, app) {
- const seed = INT(node, inputName, inputData, app);
- const seedControl = new ComfyValueControlWidget("control_after_generate", "randomize", node, seed.widget);
- node.addCustomWidget(seedControl);
-
- // seed.widget.linkedWidgets = [seedControl];
- return seed;
-}
-
-const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app: ComfyApp): WidgetData => {
- const defaultVal = inputData[1].default || "";
+const STRING: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
+ const defaultValue = inputData[1].default || "";
const multiline = !!inputData[1].multiline;
- // if (multiline) {
- // return addMultilineWidget(node, inputName, { defaultVal, ...inputData[1] }, app);
- // } else {
- return { widget: node.addWidget("text", inputName, defaultVal, () => { }, { multiline }) };
- // }
+ return addComfyInput(node, inputName, { type: "string", config: { defaultValue, multiline }, defaultWidgetNode: ComfyTextNode })
};
-const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): WidgetData => {
+const COMBO: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
const type = inputData[0];
let defaultValue = type[0];
if (inputData[1] && inputData[1].default) {
defaultValue = inputData[1].default;
}
- return { widget: node.addWidget("combo", inputName, defaultValue, () => { }, { values: type }) };
+ return addComfyInput(node, inputName, { type: "string", config: { values: type, defaultValue }, defaultWidgetNode: ComfyComboNode })
}
-const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any, app): WidgetData => {
- const imageWidget = node.widgets.find((w) => w.name === "image");
- let uploadWidget: IWidget;
-
- // async function uploadFile(file: File, updateNode: boolean) {
- // try {
- // // Wrap file in formdata so it includes filename
- // const body = new FormData();
- // body.append("image", file);
- // const resp = await fetch("/upload/image", {
- // method: "POST",
- // body,
- // });
-
- // if (resp.status === 200) {
- // const data = await resp.json();
- // // Add the file as an option and update the widget value
- // if (!imageWidget.options.values.includes(data.name)) {
- // imageWidget.options.values.push(data.name);
- // }
-
- // if (updateNode) {
- // // showImage(data.name);
- // imageWidget.value = data.name;
- // }
- // } else {
- // alert(resp.status + " - " + resp.statusText);
- // }
- // } catch (error) {
- // alert(error);
- // }
- // }
-
- // const fileInput = document.createElement("input");
- // Object.assign(fileInput, {
- // type: "file",
- // accept: "image/jpeg,image/png",
- // style: "display: none",
- // onchange: async () => {
- // if (fileInput.files.length) {
- // await uploadFile(fileInput.files[0], true);
- // }
- // },
- // });
- // document.body.append(fileInput);
-
- // Create the button widget for selecting the files
- uploadWidget = node.addWidget("button", "choose file to upload", "image", () => {
- // fileInput.click();
- });
- uploadWidget.options = { serialize: false };
-
- return { widget: uploadWidget };
+const IMAGEUPLOAD: WidgetFactory = (node: LGraphNode, inputName: string, inputData: any): IComfyInputSlot => {
+ return addComfyInput(node, inputName, { type: "number", config: {} })
}
-
export type WidgetRepository = Record
-export const ComfyWidgets: WidgetRepository = {
- "INT:seed": seedWidget,
- "INT:noise_seed": seedWidget,
+const ComfyWidgets: WidgetRepository = {
+ "INT:seed": INT,
+ "INT:noise_seed": INT,
FLOAT,
INT,
STRING,
COMBO,
IMAGEUPLOAD,
}
+
+export default ComfyWidgets
diff --git a/src/lib/widgets/ButtonWidget.svelte b/src/lib/widgets/ButtonWidget.svelte
new file mode 100644
index 0000000..b2b8b6e
--- /dev/null
+++ b/src/lib/widgets/ButtonWidget.svelte
@@ -0,0 +1,44 @@
+
+
+
+
+
+
+
diff --git a/src/lib/components/menu/MenuDivider.svelte b/src/lib/components/menu/MenuDivider.svelte
new file mode 100644
index 0000000..13041d5
--- /dev/null
+++ b/src/lib/components/menu/MenuDivider.svelte
@@ -0,0 +1,9 @@
++ + diff --git a/src/lib/components/menu/MenuOption.svelte b/src/lib/components/menu/MenuOption.svelte new file mode 100644 index 0000000..bb7cca9 --- /dev/null +++ b/src/lib/components/menu/MenuOption.svelte @@ -0,0 +1,50 @@ + + +
+ {#if text}
+ {text}
+ {:else}
+
+ {/if}
+
+
+
diff --git a/src/lib/components/menu/menu.ts b/src/lib/components/menu/menu.ts
new file mode 100644
index 0000000..108b2fd
--- /dev/null
+++ b/src/lib/components/menu/menu.ts
@@ -0,0 +1,3 @@
+const key = {};
+
+export { key };
diff --git a/src/lib/defaultGraph.ts b/src/lib/defaultGraph.ts
index eecb0e3..0ef7690 100644
--- a/src/lib/defaultGraph.ts
+++ b/src/lib/defaultGraph.ts
@@ -1,401 +1,3314 @@
import type SerializedAppState from "./ComfyApp"
const defaultGraph: SerializedAppState = {
- createdBy: "ComfyBox",
- version: 1,
- workflow: {
- last_node_id: 9,
- last_link_id: 9,
- nodes: [
- {
- id: 7,
- type: "CLIPTextEncode",
- pos: [
- 413,
- 389
+ createdBy: "ComfyBox",
+ version: 1,
+ workflow: {
+ last_node_id: 75,
+ last_link_id: 101,
+ nodes: [
+ {
+ id: 19,
+ type: "ui/slider",
+ pos: [
+ 77.09750000000287,
+ 235.9643000000003
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 0,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 18
+ ],
+ _data: 8
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 0
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 100
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 0.5
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 0
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 8,
+ min: 0,
+ max: 100,
+ step: 0.5,
+ precision: 0
+ },
+ widgets_values: [
+ "8.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 8,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 20,
+ type: "ui/combo",
+ pos: [
+ 77.09750000000287,
+ 255.9643000000003
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 1,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "string",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "string",
+ links: [
+ 19
+ ],
+ _data: "euler"
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ }
+ ],
+ title: "UI.Combo",
+ properties: {
+ defaultValue: "euler",
+ values: [
+ "euler",
+ "euler_ancestral",
+ "heun",
+ "dpm_2",
+ "dpm_2_ancestral",
+ "lms",
+ "dpm_fast",
+ "dpm_adaptive",
+ "dpmpp_2s_ancestral",
+ "dpmpp_sde",
+ "dpmpp_2m",
+ "ddim",
+ "uni_pc",
+ "uni_pc_bh2"
+ ]
+ },
+ widgets_values: [
+ "euler"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: "euler",
+ shownOutputProperties: {}
+ },
+ {
+ id: 21,
+ type: "ui/combo",
+ pos: [
+ 77.09750000000287,
+ 275.9643000000003
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 2,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "string",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "string",
+ links: [
+ 20
+ ],
+ _data: "karras"
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ }
+ ],
+ title: "UI.Combo",
+ properties: {
+ defaultValue: "karras",
+ values: [
+ "karras",
+ "normal",
+ "simple",
+ "ddim_uniform"
+ ]
+ },
+ widgets_values: [
+ "karras"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: "karras",
+ shownOutputProperties: {}
+ },
+ {
+ id: 22,
+ type: "ui/slider",
+ pos: [
+ 77.09750000000287,
+ 355.9643000000003
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 3,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 21
+ ],
+ _data: 1
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 0
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 1
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 0.01
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 0
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 1,
+ min: 0,
+ max: 1,
+ step: 0.01,
+ precision: 0
+ },
+ widgets_values: [
+ "1.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 1,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 33,
+ type: "ui/text",
+ pos: [
+ -347.6124999999959,
+ 309.06430000000114
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 4,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "string",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "string",
+ links: [
+ 30
+ ],
+ _data: "masterpiece, best quality, 1girl, dress, space opera, portrait, planet landscape, starry sky"
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ }
+ ],
+ title: "UI.Text",
+ properties: {
+ defaultValue: "",
+ multiline: true
+ },
+ widgets_values: [
+ "masterpiece, best quality, 1girl, dress, space opera, portrait, planet landscape, starry sky"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: "masterpiece, best quality, 1girl, dress, space opera, portrait, planet landscape, starry sky",
+ shownOutputProperties: {}
+ },
+ {
+ id: 35,
+ type: "ui/text",
+ pos: [
+ -346.40249999999617,
+ 399.8143000000014
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 5,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "string",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "string",
+ links: [
+ 31
+ ],
+ _data: "worst quality, bad quality, nsfw"
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ }
+ ],
+ title: "UI.Text",
+ properties: {
+ defaultValue: "",
+ multiline: true
+ },
+ widgets_values: [
+ "worst quality, bad quality, nsfw"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: "worst quality, bad quality, nsfw",
+ shownOutputProperties: {}
+ },
+ {
+ id: 40,
+ type: "VAEDecode",
+ pos: [
+ 532.5875000000013,
+ 160.9183000000009
+ ],
+ size: [
+ 210,
+ 46
+ ],
+ flags: {},
+ order: 31,
+ mode: 0,
+ inputs: [
+ {
+ name: "samples",
+ type: "LATENT",
+ link: 40,
+ color_off: "orange",
+ color_on: "orange"
+ },
+ {
+ name: "vae",
+ type: "VAE",
+ link: 101,
+ color_off: "orange",
+ color_on: "orange"
+ }
+ ],
+ outputs: [
+ {
+ name: "IMAGE",
+ type: "IMAGE",
+ links: [
+ 44
+ ],
+ color_off: "orange",
+ color_on: "orange",
+ slot_index: 0
+ }
+ ],
+ title: "VAEDecode",
+ properties: {},
+ color: "#432",
+ bgColor: "#653"
+ },
+ {
+ id: 45,
+ type: "ui/text",
+ pos: [
+ 779.2900000000001,
+ 208.44999999999965
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 6,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "string",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "string",
+ links: [
+ 43
+ ],
+ _data: "ComfyUI"
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ }
+ ],
+ title: "UI.Text",
+ properties: {
+ defaultValue: "ComfyUI",
+ multiline: false
+ },
+ widgets_values: [
+ "ComfyUI"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: "ComfyUI",
+ shownOutputProperties: {}
+ },
+ {
+ id: 32,
+ type: "CLIPTextEncode",
+ pos: [
+ -218,
+ 285
+ ],
+ size: [
+ 216.60000000000002,
+ 46
+ ],
+ flags: {},
+ order: 24,
+ mode: 0,
+ inputs: [
+ {
+ name: "text",
+ type: "string",
+ link: 30,
+ config: {
+ defaultValue: "",
+ multiline: true
+ },
+ serialize: true
+ },
+ {
+ name: "clip",
+ type: "CLIP",
+ link: 99,
+ color_off: "orange",
+ color_on: "orange"
+ }
+ ],
+ outputs: [
+ {
+ name: "CONDITIONING",
+ type: "CONDITIONING",
+ links: [
+ 33
+ ],
+ color_off: "orange",
+ color_on: "orange",
+ slot_index: 0
+ }
+ ],
+ title: "CLIPTextEncode",
+ properties: {},
+ color: "#432",
+ bgColor: "#653"
+ },
+ {
+ id: 34,
+ type: "CLIPTextEncode",
+ pos: [
+ -216,
+ 376
+ ],
+ size: [
+ 216.60000000000002,
+ 46
+ ],
+ flags: {},
+ order: 25,
+ mode: 0,
+ inputs: [
+ {
+ name: "text",
+ type: "string",
+ link: 31,
+ config: {
+ defaultValue: "",
+ multiline: true
+ },
+ serialize: true
+ },
+ {
+ name: "clip",
+ type: "CLIP",
+ link: 100,
+ color_off: "orange",
+ color_on: "orange"
+ }
+ ],
+ outputs: [
+ {
+ name: "CONDITIONING",
+ type: "CONDITIONING",
+ links: [
+ 32
+ ],
+ color_off: "orange",
+ color_on: "orange",
+ slot_index: 0
+ }
+ ],
+ title: "CLIPTextEncode",
+ properties: {},
+ color: "#432",
+ bgColor: "#653"
+ },
+ {
+ id: 47,
+ type: "ui/gallery",
+ pos: [
+ 1284,
+ 165
+ ],
+ size: [
+ 210,
+ 58
+ ],
+ flags: {},
+ order: 33,
+ mode: 0,
+ inputs: [
+ {
+ name: "images",
+ type: "OUTPUT",
+ link: 46
+ }
+ ],
+ outputs: [],
+ title: "UI.Gallery",
+ properties: {
+ defaultValue: []
+ },
+ widgets_values: [
+ "Images: 4"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: [
+ {
+ name: null,
+ data: "http://localhost:8188/view?filename=ComfyUI_00352_.png&subfolder=&type=output"
+ },
+ {
+ name: null,
+ data: "http://localhost:8188/view?filename=ComfyUI_00353_.png&subfolder=&type=output"
+ },
+ {
+ name: null,
+ data: "http://localhost:8188/view?filename=ComfyUI_00354_.png&subfolder=&type=output"
+ },
+ {
+ name: null,
+ data: "http://localhost:8188/view?filename=ComfyUI_00355_.png&subfolder=&type=output"
+ }
+ ],
+ shownOutputProperties: {}
+ },
+ {
+ id: 49,
+ type: "events/log",
+ pos: [
+ 771,
+ 22
+ ],
+ size: [
+ 140,
+ 26
+ ],
+ flags: {},
+ order: 17,
+ mode: 0,
+ inputs: [
+ {
+ name: "event",
+ type: -1,
+ link: 47,
+ shape: 1
+ }
+ ],
+ outputs: [],
+ title: "Log Event",
+ properties: {}
+ },
+ {
+ id: 52,
+ type: "basic/watch",
+ pos: [
+ 1063.1426107321954,
+ -118.99396962098862
+ ],
+ size: [
+ 140,
+ 26
+ ],
+ flags: {},
+ order: 22,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: 0,
+ link: 50,
+ label: "null",
+ slot_index: 0
+ }
+ ],
+ outputs: [],
+ title: "Watch",
+ properties: {
+ value: 1
+ }
+ },
+ {
+ id: 54,
+ type: "ui/slider",
+ pos: [
+ 1038,
+ -226
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {},
+ order: 23,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: 52
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: null,
+ _data: 6
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 0
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 10
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 1
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 1
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 0,
+ min: 0,
+ max: 10,
+ step: 1,
+ precision: 1
+ },
+ widgets_values: [
+ "6.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 6,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 48,
+ type: "ui/button",
+ pos: [
+ 372,
+ 12
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {},
+ order: 7,
+ mode: 0,
+ inputs: [],
+ outputs: [
+ {
+ name: "event",
+ type: -2,
+ links: [
+ 47,
+ 62
+ ],
+ shape: 1,
+ slot_index: 0
+ },
+ {
+ name: "isClicked",
+ type: "boolean",
+ links: null,
+ slot_index: 1,
+ _data: false
+ }
+ ],
+ title: "UI.Button",
+ properties: {
+ defaultValue: false,
+ message: "bang"
+ },
+ widgets_values: [
+ "false"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: false,
+ shownOutputProperties: {}
+ },
+ {
+ id: 50,
+ type: "actions/copy",
+ pos: [
+ 729,
+ -125
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {},
+ order: 18,
+ mode: 0,
+ inputs: [
+ {
+ name: "",
+ type: "*",
+ link: 66
+ },
+ {
+ name: "copy",
+ type: -1,
+ link: 62,
+ shape: 1
+ }
+ ],
+ outputs: [
+ {
+ name: "",
+ type: "*",
+ links: [
+ 50,
+ 52
+ ],
+ slot_index: 0
+ }
+ ],
+ title: "Comfy.CopyAction",
+ properties: {
+ value: 6
+ }
+ },
+ {
+ id: 18,
+ type: "ui/slider",
+ pos: [
+ 79,
+ 219
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 8,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 17
+ ],
+ _data: 20
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 1
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 10000
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 1
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 0
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 20,
+ min: 1,
+ max: 10000,
+ step: 1,
+ precision: 0
+ },
+ widgets_values: [
+ "20.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 20,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 53,
+ type: "ui/slider",
+ pos: [
+ 369,
+ -174
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {},
+ order: 9,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 66
+ ],
+ slot_index: 0,
+ _data: 6
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: [],
+ shape: 1,
+ slot_index: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 0
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 10
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 1
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 1
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 0,
+ min: 0,
+ max: 10,
+ step: 1,
+ precision: 1
+ },
+ widgets_values: [
+ "6.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 6,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 64,
+ type: "basic/CompareValues",
+ pos: [
+ -131,
+ -587
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {},
+ order: 19,
+ mode: 0,
+ inputs: [
+ {
+ name: "A",
+ type: 0,
+ link: 78
+ },
+ {
+ name: "B",
+ type: 0,
+ link: 79
+ }
+ ],
+ outputs: [
+ {
+ name: "true",
+ type: "boolean",
+ links: [
+ 74
+ ],
+ slot_index: 0,
+ _data: true
+ },
+ {
+ name: "false",
+ type: "boolean",
+ links: null,
+ _data: false
+ }
+ ],
+ title: "GenericCompare",
+ properties: {
+ A: "randomize",
+ B: "randomize",
+ OP: "==",
+ enabled: true
+ }
+ },
+ {
+ id: 39,
+ type: "ui/slider",
+ pos: [
+ -350,
+ 554
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 10,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 38,
+ 83
+ ],
+ _data: 4
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 1
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 64
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 1
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 0
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 1,
+ min: 1,
+ max: 64,
+ step: 1,
+ precision: 0
+ },
+ widgets_values: [
+ "4.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 4,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 66,
+ type: "basic/string",
+ pos: [
+ -363,
+ -578
+ ],
+ size: [
+ 210,
+ 38
+ ],
+ flags: {},
+ order: 11,
+ mode: 0,
+ inputs: [],
+ outputs: [
+ {
+ name: "string",
+ type: "string",
+ links: [
+ 78
+ ],
+ slot_index: 0,
+ _data: "randomize"
+ }
+ ],
+ title: "Const String",
+ properties: {
+ value: "randomize"
+ }
+ },
+ {
+ id: 57,
+ type: "ui/combo",
+ pos: [
+ -378,
+ -304
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 12,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "string",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "string",
+ links: [
+ 55,
+ 79
+ ],
+ slot_index: 0,
+ _data: "randomize"
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ }
+ ],
+ title: "UI.Combo",
+ properties: {
+ defaultValue: "randomize",
+ values: [
+ "fixed",
+ "increment",
+ "decrement",
+ "randomize"
+ ]
+ },
+ widgets_values: [
+ "randomize"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: "randomize",
+ shownOutputProperties: {}
+ },
+ {
+ id: 17,
+ type: "ui/slider",
+ pos: [
+ -48,
+ -291
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {
+ collapsed: false
+ },
+ order: 27,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: 67,
+ slot_index: 0
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 68,
+ 80
+ ],
+ slot_index: 0,
+ _data: 572904899609889
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: [
+ 69
+ ],
+ slot_index: 2,
+ _data: 0
+ },
+ {
+ name: "max",
+ type: "number",
+ links: [
+ 70
+ ],
+ slot_index: 3,
+ _data: 18446744073709552000
+ },
+ {
+ name: "step",
+ type: "number",
+ links: [
+ 75
+ ],
+ slot_index: 4,
+ _data: 1
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 0
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 0,
+ min: 0,
+ max: 18446744073709552000,
+ step: 1,
+ precision: 0
+ },
+ widgets_values: [
+ "572904899609889.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 572904899609889,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 16,
+ type: "KSampler",
+ pos: [
+ 242,
+ 156
+ ],
+ size: [
+ 241.79999999999998,
+ 206
+ ],
+ flags: {},
+ order: 26,
+ mode: 0,
+ inputs: [
+ {
+ name: "model",
+ type: "MODEL",
+ link: 98,
+ color_off: "orange",
+ color_on: "orange"
+ },
+ {
+ name: "seed",
+ type: "number",
+ link: 80,
+ config: {
+ min: 0,
+ max: 18446744073709552000,
+ step: 1,
+ precision: 0,
+ defaultValue: 0
+ },
+ serialize: true
+ },
+ {
+ name: "steps",
+ type: "number",
+ link: 17,
+ config: {
+ min: 1,
+ max: 10000,
+ step: 1,
+ precision: 0,
+ defaultValue: 20
+ },
+ serialize: true
+ },
+ {
+ name: "cfg",
+ type: "number",
+ link: 18,
+ config: {
+ min: 0,
+ max: 100,
+ step: 0.5,
+ precision: 0,
+ defaultValue: 8
+ },
+ serialize: true
+ },
+ {
+ name: "sampler_name",
+ type: "string",
+ link: 19,
+ config: {
+ values: [
+ "euler",
+ "euler_ancestral",
+ "heun",
+ "dpm_2",
+ "dpm_2_ancestral",
+ "lms",
+ "dpm_fast",
+ "dpm_adaptive",
+ "dpmpp_2s_ancestral",
+ "dpmpp_sde",
+ "dpmpp_2m",
+ "ddim",
+ "uni_pc",
+ "uni_pc_bh2"
+ ],
+ defaultValue: "euler"
+ },
+ serialize: true
+ },
+ {
+ name: "scheduler",
+ type: "string",
+ link: 20,
+ config: {
+ values: [
+ "karras",
+ "normal",
+ "simple",
+ "ddim_uniform"
+ ],
+ defaultValue: "karras"
+ },
+ serialize: true
+ },
+ {
+ name: "positive",
+ type: "CONDITIONING",
+ link: 33,
+ color_off: "orange",
+ color_on: "orange"
+ },
+ {
+ name: "negative",
+ type: "CONDITIONING",
+ link: 32,
+ color_off: "orange",
+ color_on: "orange"
+ },
+ {
+ name: "latent_image",
+ type: "LATENT",
+ link: 39,
+ color_off: "orange",
+ color_on: "orange"
+ },
+ {
+ name: "denoise",
+ type: "number",
+ link: 21,
+ config: {
+ min: 0,
+ max: 1,
+ step: 0.01,
+ precision: 0,
+ defaultValue: 1
+ },
+ serialize: true
+ }
+ ],
+ outputs: [
+ {
+ name: "LATENT",
+ type: "LATENT",
+ links: [
+ 40
+ ],
+ color_off: "orange",
+ color_on: "orange",
+ slot_index: 0
+ }
+ ],
+ title: "KSampler",
+ properties: {},
+ color: "#432",
+ bgColor: "#653"
+ },
+ {
+ id: 69,
+ type: "basic/integer",
+ pos: [
+ 261,
+ -405
+ ],
+ size: [
+ 210,
+ 38
+ ],
+ flags: {},
+ order: 13,
+ mode: 0,
+ inputs: [],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 85
+ ],
+ label: "1",
+ slot_index: 0,
+ _data: 1
+ }
+ ],
+ title: "Const Integer",
+ properties: {
+ value: 1
+ }
+ },
+ {
+ id: 56,
+ type: "utils/value_control",
+ pos: [
+ -252,
+ -372
+ ],
+ size: [
+ 151.2,
+ 126
+ ],
+ flags: {},
+ order: 34,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: 68
+ },
+ {
+ name: "trigger",
+ type: -1,
+ link: 73,
+ shape: 1
+ },
+ {
+ name: "action",
+ type: "string",
+ link: 55,
+ config: {
+ defaultValue: "randomize",
+ values: [
+ "fixed",
+ "increment",
+ "decrement",
+ "randomize"
+ ]
+ }
+ },
+ {
+ name: "min",
+ type: "number",
+ link: 69
+ },
+ {
+ name: "max",
+ type: "number",
+ link: 70
+ },
+ {
+ name: "step",
+ type: "number",
+ link: 88
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "*",
+ links: [
+ 67
+ ],
+ slot_index: 0
+ }
+ ],
+ title: "Comfy.ValueControl",
+ properties: {
+ value: 572904899609889,
+ action: "randomize",
+ min: 0,
+ max: 18446744073709552000,
+ step: 1
+ }
+ },
+ {
+ id: 63,
+ type: "utils/selector2",
+ pos: [
+ 174,
+ -565
+ ],
+ size: [
+ 140,
+ 66
+ ],
+ flags: {},
+ order: 35,
+ mode: 0,
+ inputs: [
+ {
+ name: "select",
+ type: "boolean",
+ link: 74
+ },
+ {
+ name: "true",
+ type: "*",
+ link: 75
+ },
+ {
+ name: "false",
+ type: "*",
+ link: 86,
+ slot_index: 2
+ }
+ ],
+ outputs: [
+ {
+ name: "out",
+ type: "*",
+ links: [
+ 87,
+ 88
+ ],
+ slot_index: 0,
+ _data: 1
+ }
+ ],
+ title: "Comfy.Selector2",
+ properties: {
+ value: null
+ }
+ },
+ {
+ id: 36,
+ type: "EmptyLatentImage",
+ pos: [
+ -220,
+ 490
+ ],
+ size: [
+ 216.60000000000002,
+ 66
+ ],
+ flags: {},
+ order: 28,
+ mode: 0,
+ inputs: [
+ {
+ name: "width",
+ type: "number",
+ link: 93,
+ config: {
+ min: 64,
+ max: 8192,
+ step: 8,
+ precision: 0,
+ defaultValue: 512
+ },
+ serialize: true
+ },
+ {
+ name: "height",
+ type: "number",
+ link: 94,
+ config: {
+ min: 64,
+ max: 8192,
+ step: 8,
+ precision: 0,
+ defaultValue: 512
+ },
+ serialize: true
+ },
+ {
+ name: "batch_size",
+ type: "number",
+ link: 38,
+ config: {
+ min: 1,
+ max: 64,
+ step: 1,
+ precision: 0,
+ defaultValue: 1
+ },
+ serialize: true
+ }
+ ],
+ outputs: [
+ {
+ name: "LATENT",
+ type: "LATENT",
+ links: [
+ 39
+ ],
+ color_off: "orange",
+ color_on: "orange",
+ slot_index: 0
+ }
+ ],
+ title: "EmptyLatentImage",
+ properties: {},
+ color: "#432",
+ bgColor: "#653"
+ },
+ {
+ id: 38,
+ type: "ui/slider",
+ pos: [
+ -351,
+ 532
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 30,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: 91
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 90,
+ 94
+ ],
+ slot_index: 0,
+ _data: 512
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 64
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 8192
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 8
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 0
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 512,
+ min: 64,
+ max: 8192,
+ step: 8,
+ precision: 0
+ },
+ widgets_values: [
+ "512.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 512,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 37,
+ type: "ui/slider",
+ pos: [
+ -352,
+ 509
+ ],
+ size: [
+ 210,
+ 158
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 29,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "number",
+ link: 92
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "number",
+ links: [
+ 89,
+ 93
+ ],
+ slot_index: 0,
+ _data: 848
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ },
+ {
+ name: "min",
+ type: "number",
+ links: null,
+ _data: 64
+ },
+ {
+ name: "max",
+ type: "number",
+ links: null,
+ _data: 8192
+ },
+ {
+ name: "step",
+ type: "number",
+ links: null,
+ _data: 8
+ },
+ {
+ name: "precision",
+ type: "number",
+ links: null,
+ _data: 0
+ }
+ ],
+ title: "UI.Slider",
+ properties: {
+ defaultValue: 512,
+ min: 64,
+ max: 8192,
+ step: 8,
+ precision: 0
+ },
+ widgets_values: [
+ "848.000"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: 848,
+ shownOutputProperties: {
+ min: {
+ type: "number",
+ index: 2
+ },
+ max: {
+ type: "number",
+ index: 3
+ },
+ step: {
+ type: "number",
+ index: 4
+ },
+ precision: {
+ type: "number",
+ index: 5
+ }
+ }
+ },
+ {
+ id: 71,
+ type: "actions/swap",
+ pos: [
+ -527,
+ 486
+ ],
+ size: [
+ 140,
+ 66
+ ],
+ flags: {
+ collapsed: false
+ },
+ order: 37,
+ mode: 0,
+ inputs: [
+ {
+ name: "A",
+ type: "*",
+ link: 89
+ },
+ {
+ name: "B",
+ type: "*",
+ link: 90
+ },
+ {
+ name: "swap",
+ type: -1,
+ link: 95,
+ shape: 1
+ }
+ ],
+ outputs: [
+ {
+ name: "B",
+ type: "*",
+ links: [
+ 91
+ ],
+ slot_index: 0
+ },
+ {
+ name: "A",
+ type: "*",
+ links: [
+ 92
+ ],
+ slot_index: 1
+ }
+ ],
+ title: "Comfy.SwapAction",
+ properties: {}
+ },
+ {
+ id: 62,
+ type: "ui/button",
+ pos: [
+ -648,
+ 550
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 14,
+ mode: 0,
+ inputs: [],
+ outputs: [
+ {
+ name: "clicked",
+ type: -2,
+ links: [
+ 95
+ ],
+ shape: 1,
+ slot_index: 0
+ },
+ {
+ name: "isClicked",
+ type: "boolean",
+ links: null,
+ _data: false
+ }
+ ],
+ title: "UI.Button",
+ properties: {
+ defaultValue: false,
+ message: "bang"
+ },
+ widgets_values: [
+ "false"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: false,
+ shownOutputProperties: {}
+ },
+ {
+ id: 44,
+ type: "SaveImage",
+ pos: [
+ 888,
+ 164
+ ],
+ size: [
+ 315,
+ 78
+ ],
+ flags: {},
+ order: 32,
+ mode: 0,
+ inputs: [
+ {
+ name: "images",
+ type: "IMAGE",
+ link: 44,
+ color_off: "orange",
+ color_on: "orange",
+ slot_index: 0
+ },
+ {
+ name: "filename_prefix",
+ type: "string",
+ link: 43,
+ config: {
+ defaultValue: "ComfyUI",
+ multiline: false
+ },
+ serialize: true
+ }
+ ],
+ outputs: [
+ {
+ name: "output",
+ type: "OUTPUT",
+ links: [
+ 46
+ ],
+ slot_index: 0
+ }
+ ],
+ title: "SaveImage",
+ properties: {},
+ color: "#432",
+ bgColor: "#653"
+ },
+ {
+ id: 55,
+ type: "actions/after_queued",
+ pos: [
+ -591,
+ -349
+ ],
+ size: [
+ 193.2,
+ 46
+ ],
+ flags: {},
+ order: 15,
+ mode: 0,
+ inputs: [],
+ outputs: [
+ {
+ name: "afterQueued",
+ type: -2,
+ links: [
+ 73
+ ],
+ shape: 1,
+ slot_index: 0,
+ _data: null
+ },
+ {
+ name: "prompt",
+ type: "*",
+ links: null
+ }
+ ],
+ title: "Comfy.AfterQueuedAction",
+ properties: {
+ prompt: null
+ }
+ },
+ {
+ id: 75,
+ type: "ui/combo",
+ pos: [
+ -420.44163113999997,
+ 169.75962460075795
+ ],
+ size: [
+ 210,
+ 78
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 16,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: "string",
+ link: null
+ }
+ ],
+ outputs: [
+ {
+ name: "value",
+ type: "string",
+ links: [
+ 97
+ ],
+ _data: "refslaveV2_v2.safetensors"
+ },
+ {
+ name: "changed",
+ type: -2,
+ links: null,
+ shape: 1
+ }
+ ],
+ title: "UI.Combo",
+ properties: {
+ defaultValue: "AbyssOrangeMix2_nsfw.safetensors",
+ values: [
+ "AbyssOrangeMix2_nsfw.safetensors",
+ "refslaveV2_v2.safetensors"
+ ]
+ },
+ widgets_values: [
+ "refslaveV2_v2.safetensors"
+ ],
+ color: "#223",
+ bgColor: "#335",
+ comfyValue: "refslaveV2_v2.safetensors",
+ shownOutputProperties: {}
+ },
+ {
+ id: 74,
+ type: "CheckpointLoaderSimple",
+ pos: [
+ -290,
+ 146
+ ],
+ size: [
+ 277.2,
+ 66
+ ],
+ flags: {},
+ order: 21,
+ mode: 0,
+ inputs: [
+ {
+ name: "ckpt_name",
+ type: "string",
+ link: 97,
+ config: {
+ values: [
+ "AbyssOrangeMix2_nsfw.safetensors",
+ "refslaveV2_v2.safetensors"
+ ],
+ defaultValue: "AbyssOrangeMix2_nsfw.safetensors"
+ },
+ widgetNodeType: "ui/combo",
+ serialize: true,
+ defaultWidgetNode: null
+ }
+ ],
+ outputs: [
+ {
+ name: "MODEL",
+ type: "MODEL",
+ links: [
+ 98
+ ],
+ slot_index: 0
+ },
+ {
+ name: "CLIP",
+ type: "CLIP",
+ links: [
+ 99,
+ 100
+ ],
+ slot_index: 1
+ },
+ {
+ name: "VAE",
+ type: "VAE",
+ links: [
+ 101
+ ],
+ slot_index: 2
+ }
+ ],
+ title: "CheckpointLoaderSimple",
+ properties: {},
+ color: "#432",
+ bgColor: "#653"
+ },
+ {
+ id: 70,
+ type: "basic/watch",
+ pos: [
+ 341,
+ -543
+ ],
+ size: [
+ 140,
+ 26
+ ],
+ flags: {
+ collapsed: true
+ },
+ order: 36,
+ mode: 0,
+ inputs: [
+ {
+ name: "value",
+ type: 0,
+ link: 87,
+ label: "1.000"
+ }
+ ],
+ outputs: [],
+ title: "Watch",
+ properties: {
+ value: 1
+ }
+ },
+ {
+ id: 67,
+ type: "math/operation",
+ pos: [
+ 519,
+ -409
+ ],
+ size: [
+ 140,
+ 46
+ ],
+ flags: {},
+ order: 20,
+ mode: 0,
+ inputs: [
+ {
+ name: "A",
+ type: "number,array,object",
+ link: 85
+ },
+ {
+ name: "B",
+ type: "number",
+ link: 83
+ }
+ ],
+ outputs: [
+ {
+ name: "=",
+ type: "number",
+ links: [
+ 86
+ ],
+ slot_index: 0,
+ _data: 5
+ }
+ ],
+ title: "Operation",
+ properties: {
+ A: 1,
+ B: 4,
+ OP: "+"
+ }
+ }
],
- size: [
- 425.27801513671875,
- 180.6060791015625
- ],
- flags: {},
- order: 3,
- mode: 0,
- inputs: [
- {
- name: "clip",
- type: "CLIP",
- link: 5
- }
- ],
- outputs: [
- {
- name: "CONDITIONING",
- type: "CONDITIONING",
- links: [
- 6
+ links: [
+ [
+ 17,
+ 18,
+ 0,
+ 16,
+ 2,
+ "number"
],
- slot_index: 0
- }
- ],
- title: "CLIPTextEncode",
- properties: {},
- widgets_values: [
- "bad hands"
- ]
- },
- {
- id: 6,
- type: "CLIPTextEncode",
- pos: [
- 415,
- 186
- ],
- size: [
- 422.84503173828125,
- 164.31304931640625
- ],
- flags: {},
- order: 2,
- mode: 0,
- inputs: [
- {
- name: "clip",
- type: "CLIP",
- link: 3
- }
- ],
- outputs: [
- {
- name: "CONDITIONING",
- type: "CONDITIONING",
- links: [
- 4
+ [
+ 18,
+ 19,
+ 0,
+ 16,
+ 3,
+ "number"
],
- slot_index: 0
- }
- ],
- title: "CLIPTextEncode",
- properties: {},
- widgets_values: [
- "masterpiece best quality girl"
- ]
- },
- {
- id: 5,
- type: "EmptyLatentImage",
- pos: [
- 473,
- 609
- ],
- size: [
- 315,
- 106
- ],
- flags: {},
- order: 0,
- mode: 0,
- inputs: [],
- outputs: [
- {
- name: "LATENT",
- type: "LATENT",
- links: [
- 2
+ [
+ 19,
+ 20,
+ 0,
+ 16,
+ 4,
+ "string"
],
- slot_index: 0
- }
- ],
- title: "EmptyLatentImage",
- properties: {},
- widgets_values: [
- 512,
- 512,
- 1
- ]
- },
- {
- id: 3,
- type: "KSampler",
- pos: [
- 863,
- 186
- ],
- size: [
- 315,
- 262
- ],
- flags: {},
- order: 4,
- mode: 0,
- inputs: [
- {
- name: "model",
- type: "MODEL",
- link: 1
- },
- {
- name: "positive",
- type: "CONDITIONING",
- link: 4
- },
- {
- name: "negative",
- type: "CONDITIONING",
- link: 6
- },
- {
- name: "latent_image",
- type: "LATENT",
- link: 2
- }
- ],
- outputs: [
- {
- name: "LATENT",
- type: "LATENT",
- links: [
- 7
+ [
+ 20,
+ 21,
+ 0,
+ 16,
+ 5,
+ "string"
],
- slot_index: 0
- }
- ],
- title: "KSampler",
- properties: {},
- widgets_values: [
- 8566257,
- "randomize",
- 8,
- 8,
- "euler",
- "normal",
- 1
- ]
- },
- {
- id: 8,
- type: "VAEDecode",
- pos: [
- 1209,
- 188
- ],
- size: [
- 210,
- 46
- ],
- flags: {},
- order: 5,
- mode: 0,
- inputs: [
- {
- name: "samples",
- type: "LATENT",
- link: 7
- },
- {
- name: "vae",
- type: "VAE",
- link: 8
- }
- ],
- outputs: [
- {
- name: "IMAGE",
- type: "IMAGE",
- links: [
- 9
+ [
+ 21,
+ 22,
+ 0,
+ 16,
+ 9,
+ "number"
],
- slot_index: 0
- }
- ],
- title: "VAEDecode",
- properties: {}
- },
- {
- id: 9,
- type: "SaveImage",
- pos: [
- 1451,
- 189
- ],
- size: [
- 210,
- 82
- ],
- flags: {},
- order: 6,
- mode: 0,
- inputs: [
- {
- name: "images",
- type: "IMAGE",
- link: 9
- }
- ],
- outputs: [],
- title: "SaveImage",
- properties: {},
- widgets_values: [
- [],
- "ComfyUI"
- ]
- },
- {
- id: 4,
- type: "CheckpointLoaderSimple",
- pos: [
- 26,
- 474
- ],
- size: [
- 315,
- 98
- ],
- flags: {},
- order: 1,
- mode: 0,
- inputs: [],
- outputs: [
- {
- name: "MODEL",
- type: "MODEL",
- links: [
- 1
+ [
+ 30,
+ 33,
+ 0,
+ 32,
+ 0,
+ "string"
],
- slot_index: 0
- },
- {
- name: "CLIP",
- type: "CLIP",
- links: [
- 3,
- 5
+ [
+ 31,
+ 35,
+ 0,
+ 34,
+ 0,
+ "string"
],
- slot_index: 1
- },
- {
- name: "VAE",
- type: "VAE",
- links: [
- 8
+ [
+ 32,
+ 34,
+ 0,
+ 16,
+ 7,
+ "CONDITIONING"
],
- slot_index: 2
- }
+ [
+ 33,
+ 32,
+ 0,
+ 16,
+ 6,
+ "CONDITIONING"
+ ],
+ [
+ 38,
+ 39,
+ 0,
+ 36,
+ 2,
+ "number"
+ ],
+ [
+ 39,
+ 36,
+ 0,
+ 16,
+ 8,
+ "LATENT"
+ ],
+ [
+ 40,
+ 16,
+ 0,
+ 40,
+ 0,
+ "LATENT"
+ ],
+ [
+ 43,
+ 45,
+ 0,
+ 44,
+ 1,
+ "string"
+ ],
+ [
+ 44,
+ 40,
+ 0,
+ 44,
+ 0,
+ "IMAGE"
+ ],
+ [
+ 46,
+ 44,
+ 0,
+ 47,
+ 0,
+ "OUTPUT"
+ ],
+ [
+ 47,
+ 48,
+ 0,
+ 49,
+ 0,
+ -1
+ ],
+ [
+ 50,
+ 50,
+ 0,
+ 52,
+ 0,
+ "*"
+ ],
+ [
+ 52,
+ 50,
+ 0,
+ 54,
+ 0,
+ "number"
+ ],
+ [
+ 55,
+ 57,
+ 0,
+ 56,
+ 2,
+ "string"
+ ],
+ [
+ 62,
+ 48,
+ 0,
+ 50,
+ 1,
+ -1
+ ],
+ [
+ 66,
+ 53,
+ 0,
+ 50,
+ 0,
+ "*"
+ ],
+ [
+ 67,
+ 56,
+ 0,
+ 17,
+ 0,
+ "number"
+ ],
+ [
+ 68,
+ 17,
+ 0,
+ 56,
+ 0,
+ "number"
+ ],
+ [
+ 69,
+ 17,
+ 2,
+ 56,
+ 3,
+ "number"
+ ],
+ [
+ 70,
+ 17,
+ 3,
+ 56,
+ 4,
+ "number"
+ ],
+ [
+ 73,
+ 55,
+ 0,
+ 56,
+ 1,
+ -1
+ ],
+ [
+ 74,
+ 64,
+ 0,
+ 63,
+ 0,
+ "boolean"
+ ],
+ [
+ 75,
+ 17,
+ 4,
+ 63,
+ 1,
+ "*"
+ ],
+ [
+ 78,
+ 66,
+ 0,
+ 64,
+ 0,
+ "string"
+ ],
+ [
+ 79,
+ 57,
+ 0,
+ 64,
+ 1,
+ "string"
+ ],
+ [
+ 80,
+ 17,
+ 0,
+ 16,
+ 1,
+ "number"
+ ],
+ [
+ 83,
+ 39,
+ 0,
+ 67,
+ 1,
+ "number"
+ ],
+ [
+ 85,
+ 69,
+ 0,
+ 67,
+ 0,
+ "number,array,object"
+ ],
+ [
+ 86,
+ 67,
+ 0,
+ 63,
+ 2,
+ "*"
+ ],
+ [
+ 87,
+ 63,
+ 0,
+ 70,
+ 0,
+ "*"
+ ],
+ [
+ 88,
+ 63,
+ 0,
+ 56,
+ 5,
+ "number"
+ ],
+ [
+ 89,
+ 37,
+ 0,
+ 71,
+ 0,
+ "*"
+ ],
+ [
+ 90,
+ 38,
+ 0,
+ 71,
+ 1,
+ "*"
+ ],
+ [
+ 91,
+ 71,
+ 0,
+ 38,
+ 0,
+ "number"
+ ],
+ [
+ 92,
+ 71,
+ 1,
+ 37,
+ 0,
+ "number"
+ ],
+ [
+ 93,
+ 37,
+ 0,
+ 36,
+ 0,
+ "number"
+ ],
+ [
+ 94,
+ 38,
+ 0,
+ 36,
+ 1,
+ "number"
+ ],
+ [
+ 95,
+ 62,
+ 0,
+ 71,
+ 2,
+ -1
+ ],
+ [
+ 97,
+ 75,
+ 0,
+ 74,
+ 0,
+ "string"
+ ],
+ [
+ 98,
+ 74,
+ 0,
+ 16,
+ 0,
+ "MODEL"
+ ],
+ [
+ 99,
+ 74,
+ 1,
+ 32,
+ 1,
+ "CLIP"
+ ],
+ [
+ 100,
+ 74,
+ 1,
+ 34,
+ 1,
+ "CLIP"
+ ],
+ [
+ 101,
+ 74,
+ 2,
+ 40,
+ 1,
+ "VAE"
+ ]
],
- title: "CheckpointLoaderSimple",
- properties: {},
- widgets_values: [
- "v1-5-pruned-emaonly.ckpt"
- ]
- }
- ],
- links: [
- [
- 1,
- 4,
- 0,
- 3,
- 0,
- "MODEL"
- ],
- [
- 2,
- 5,
- 0,
- 3,
- 3,
- "LATENT"
- ],
- [
- 3,
- 4,
- 1,
- 6,
- 0,
- "CLIP"
- ],
- [
- 4,
- 6,
- 0,
- 3,
- 1,
- "CONDITIONING"
- ],
- [
- 5,
- 4,
- 1,
- 7,
- 0,
- "CLIP"
- ],
- [
- 6,
- 7,
- 0,
- 3,
- 2,
- "CONDITIONING"
- ],
- [
- 7,
- 3,
- 0,
- 8,
- 0,
- "LATENT"
- ],
- [
- 8,
- 4,
- 2,
- 8,
- 1,
- "VAE"
- ],
- [
- 9,
- 8,
- 0,
- 9,
- 0,
- "IMAGE"
- ]
- ],
- groups: [],
- config: {},
- extra: {},
- version: 10
- },
- panes: {
- panels: [
- [
- {
- nodeId: 7
+ groups: [],
+ config: {},
+ extra: {},
+ version: 10
+ },
+ layout: {
+ root: "0",
+ allItems: {
+ 0: {
+ dragItem: {
+ type: "container",
+ id: "0",
+ attrs: {
+ title: "Container",
+ showTitle: false,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [
+ "1",
+ "2"
+ ]
+ },
+ 1: {
+ dragItem: {
+ type: "container",
+ id: "1",
+ attrs: {
+ title: "Container",
+ showTitle: false,
+ direction: "vertical",
+ classes: ""
+ }
+ },
+ children: [
+ "16",
+ "33",
+ "41",
+ "52"
+ ],
+ parent: "0"
+ },
+ 2: {
+ dragItem: {
+ type: "container",
+ id: "2",
+ attrs: {
+ title: "Container",
+ showTitle: false,
+ direction: "vertical",
+ classes: ""
+ }
+ },
+ children: [
+ "37",
+ "35",
+ "27"
+ ],
+ parent: "0"
+ },
+ 10: {
+ dragItem: {
+ type: "widget",
+ id: "10",
+ nodeId: 17,
+ attrs: {
+ title: "seed",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "48"
+ },
+ 11: {
+ dragItem: {
+ type: "widget",
+ id: "11",
+ nodeId: 18,
+ attrs: {
+ title: "steps",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "16"
+ },
+ 12: {
+ dragItem: {
+ type: "widget",
+ id: "12",
+ nodeId: 19,
+ attrs: {
+ title: "cfg",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "16"
+ },
+ 13: {
+ dragItem: {
+ type: "widget",
+ id: "13",
+ nodeId: 20,
+ attrs: {
+ title: "sampler_name",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "16"
+ },
+ 14: {
+ dragItem: {
+ type: "widget",
+ id: "14",
+ nodeId: 21,
+ attrs: {
+ title: "scheduler",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "16"
+ },
+ 15: {
+ dragItem: {
+ type: "widget",
+ id: "15",
+ nodeId: 22,
+ attrs: {
+ title: "denoise",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "16"
+ },
+ 16: {
+ dragItem: {
+ type: "container",
+ id: "16",
+ attrs: {
+ title: "KSampler",
+ showTitle: true,
+ direction: "vertical",
+ classes: ""
+ }
+ },
+ children: [
+ "48",
+ "11",
+ "12",
+ "13",
+ "14",
+ "15"
+ ],
+ parent: "1"
+ },
+ 26: {
+ dragItem: {
+ type: "widget",
+ id: "26",
+ nodeId: 33,
+ attrs: {
+ title: "text",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "27"
+ },
+ 27: {
+ dragItem: {
+ type: "container",
+ id: "27",
+ attrs: {
+ title: "Conditioning",
+ showTitle: true,
+ direction: "vertical",
+ classes: ""
+ }
+ },
+ children: [
+ "26",
+ "28"
+ ],
+ parent: "2"
+ },
+ 28: {
+ dragItem: {
+ type: "widget",
+ id: "28",
+ nodeId: 35,
+ attrs: {
+ title: "text",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "27"
+ },
+ 30: {
+ dragItem: {
+ type: "widget",
+ id: "30",
+ nodeId: 37,
+ attrs: {
+ title: "width",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "47"
+ },
+ 31: {
+ dragItem: {
+ type: "widget",
+ id: "31",
+ nodeId: 38,
+ attrs: {
+ title: "height",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "47"
+ },
+ 32: {
+ dragItem: {
+ type: "widget",
+ id: "32",
+ nodeId: 39,
+ attrs: {
+ title: "batch_size",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "33"
+ },
+ 33: {
+ dragItem: {
+ type: "container",
+ id: "33",
+ attrs: {
+ title: "EmptyLatentImage",
+ showTitle: true,
+ direction: "vertical",
+ classes: ""
+ }
+ },
+ children: [
+ "32",
+ "47"
+ ],
+ parent: "1"
+ },
+ 34: {
+ dragItem: {
+ type: "widget",
+ id: "34",
+ nodeId: 45,
+ attrs: {
+ title: "filename_prefix",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "35"
+ },
+ 35: {
+ dragItem: {
+ type: "container",
+ id: "35",
+ attrs: {
+ title: "SaveImage",
+ showTitle: true,
+ direction: "vertical",
+ classes: ""
+ }
+ },
+ children: [
+ "34"
+ ],
+ parent: "2"
+ },
+ 37: {
+ dragItem: {
+ type: "widget",
+ id: "37",
+ nodeId: 47,
+ attrs: {
+ title: "Widget",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "2"
+ },
+ 38: {
+ dragItem: {
+ type: "widget",
+ id: "38",
+ nodeId: 48,
+ attrs: {
+ title: "copy",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "41"
+ },
+ 39: {
+ dragItem: {
+ type: "widget",
+ id: "39",
+ nodeId: 53,
+ attrs: {
+ title: "value",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "41"
+ },
+ 40: {
+ dragItem: {
+ type: "widget",
+ id: "40",
+ nodeId: 54,
+ attrs: {
+ title: "Widget",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "41"
+ },
+ 41: {
+ dragItem: {
+ type: "container",
+ id: "41",
+ attrs: {
+ title: "Copy Test",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [
+ "39",
+ "38",
+ "40"
+ ],
+ parent: "1"
+ },
+ 42: {
+ dragItem: {
+ type: "widget",
+ id: "42",
+ nodeId: 57,
+ attrs: {
+ title: "action",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "48"
+ },
+ 46: {
+ dragItem: {
+ type: "widget",
+ id: "46",
+ nodeId: 62,
+ attrs: {
+ title: "trigger",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "47"
+ },
+ 47: {
+ dragItem: {
+ type: "container",
+ id: "47",
+ attrs: {
+ title: "Size",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [
+ "30",
+ "46",
+ "31"
+ ],
+ parent: "33"
+ },
+ 48: {
+ dragItem: {
+ type: "container",
+ id: "48",
+ attrs: {
+ title: "Seed",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [
+ "10",
+ "42"
+ ],
+ parent: "16"
+ },
+ 51: {
+ dragItem: {
+ type: "widget",
+ id: "51",
+ nodeId: 75,
+ attrs: {
+ title: "ckpt_name",
+ showTitle: true,
+ direction: "horizontal",
+ classes: ""
+ }
+ },
+ children: [],
+ parent: "52"
+ },
+ 52: {
+ dragItem: {
+ type: "container",
+ id: "52",
+ attrs: {
+ title: "CheckpointLoaderSimple",
+ showTitle: true,
+ direction: "vertical",
+ classes: ""
+ }
+ },
+ children: [
+ "51"
+ ],
+ parent: "1"
+ }
},
- {
- nodeId: 3
- }
- ],
- [
- {
- nodeId: 6
- },
- {
- nodeId: 9
- },
- {
- nodeId: 4
- }
- ],
- [
- {
- nodeId: 5
- }
- ]
- ]
- }
+ currentId: 53
+ },
+ canvas: {
+ offset: [
+ 0,
+ 0
+ ],
+ scale: 1
+ }
+}
+
+const blankGraph: SerializedAppState = {
+ createdBy: "ComfyBox",
+ version: 1,
+ workflow: {
+ last_node_id: 0,
+ last_link_id: 0,
+ nodes: [],
+ links: [],
+ groups: [],
+ config: {},
+ extra: {},
+ version: 0
+ },
+ panes: {}
}
export default defaultGraph;
+export { blankGraph }
diff --git a/src/lib/nodes/ComfyActionNodes.ts b/src/lib/nodes/ComfyActionNodes.ts
new file mode 100644
index 0000000..971c029
--- /dev/null
+++ b/src/lib/nodes/ComfyActionNodes.ts
@@ -0,0 +1,132 @@
+import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget, type SerializedLGraphNode } from "@litegraph-ts/core";
+import ComfyGraphNode from "./ComfyGraphNode";
+import { Watch } from "@litegraph-ts/nodes-basic";
+import type { SerializedPrompt } from "$lib/components/ComfyApp";
+
+export interface ComfyAfterQueuedAction extends Record
+ {#if node !== null}
+
+ {/if}
+
+
+
diff --git a/src/lib/widgets/ComboWidget.svelte b/src/lib/widgets/ComboWidget.svelte
index 36a0103..6d1cedf 100644
--- a/src/lib/widgets/ComboWidget.svelte
+++ b/src/lib/widgets/ComboWidget.svelte
@@ -1,50 +1,117 @@
-
- {#if item !== null && option !== undefined}
-
- {/if}
+
src and alt/title, gradio format
-
-export interface ComfyGalleryWidgetOptions extends WidgetPanelOptions {
-}
-
-export default class ComfyGalleryWidget extends ComfyWidget {
- override type = "comfy/gallery";
- override isVirtual = true;
-
- addImages(images: ComfyImageResult[]) {
- this.setValue(this.value.concat(images));
- }
-
- override afterQueued() {
- let queue = get(queueState)
- if (!(typeof queue.queueRemaining === "number" && queue.queueRemaining > 1)) {
- this.setValue([])
- }
- }
-}
diff --git a/src/lib/widgets/ComfyValueControlWidget.ts b/src/lib/widgets/ComfyValueControlWidget.ts
deleted file mode 100644
index 065d6f9..0000000
--- a/src/lib/widgets/ComfyValueControlWidget.ts
+++ /dev/null
@@ -1,55 +0,0 @@
-import type { IEnumWidget, IEnumWidgetOptions, INumberWidget, LGraphNode, WidgetPanelOptions } from "@litegraph-ts/core";
-import ComfyWidget from "./ComfyWidget";
-import type { ComfyImageResult } from "$lib/nodes/ComfySaveImageNode";
-import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
-import widgetState from "$lib/stores/widgetState"
-
-export interface ComfyValueControlWidgetOptions extends IEnumWidgetOptions {
-}
-
-export default class ComfyValueControlWidget extends ComfyWidget {
- override type = "combo";
- targetWidget: INumberWidget;
-
- constructor(name: string, value: string, node: ComfyGraphNode, targetWidget: INumberWidget) {
- super(name, value, node)
- this.targetWidget = targetWidget;
- this.options = { values: ["fixed", "increment", "decrement", "randomize"], serialize: false };
- }
-
- override afterQueued() {
- var v = this.value;
-
- let min = this.targetWidget.options.min;
- let max = this.targetWidget.options.max;
- // limit to something that javascript can handle
- max = Math.min(1125899906842624, max);
- min = Math.max(-1125899906842624, min);
- let range = (max - min) / (this.targetWidget.options.step);
-
- //adjust values based on valueControl Behaviour
- switch (v) {
- case "fixed":
- break;
- case "increment":
- this.targetWidget.value += this.targetWidget.options.step;
- break;
- case "decrement":
- this.targetWidget.value -= this.targetWidget.options.step;
- break;
- case "randomize":
- this.targetWidget.value = Math.floor(Math.random() * range) * (this.targetWidget.options.step) + min;
- default:
- break;
- }
- /*check if values are over or under their respective
- * ranges and set them to min or max.*/
- if (this.targetWidget.value < min)
- this.targetWidget.value = min;
-
- if (this.targetWidget.value > max)
- this.targetWidget.value = max;
-
- widgetState.widgetStateChanged(this.node.id, this.targetWidget);
- }
-}
diff --git a/src/lib/widgets/ComfyWidget.ts b/src/lib/widgets/ComfyWidget.ts
index eec08ab..f90e86b 100644
--- a/src/lib/widgets/ComfyWidget.ts
+++ b/src/lib/widgets/ComfyWidget.ts
@@ -1,6 +1,5 @@
import type ComfyGraphNode from "$lib/nodes/ComfyGraphNode";
import type { IWidget, LGraphNode, SerializedLGraphNode, Vector2, WidgetCallback, WidgetTypes } from "@litegraph-ts/core";
-import widgetState from "$lib/stores/widgetState";
export default abstract class ComfyWidget implements IWidget {
name: string;
@@ -27,7 +26,6 @@ export default abstract class ComfyWidget implements IWidget
+ import { ImageViewer } from "$lib/ImageViewer";
+ import { Block } from "@gradio/atoms";
+ import { Gallery } from "@gradio/gallery";
+ import type { Styles } from "@gradio/utils";
+ import type { WidgetLayout } from "$lib/stores/layoutState";
+ import type { Writable } from "svelte/store";
+ import type { ComfyGalleryNode } from "$lib/nodes/ComfyWidgetNodes";
+ import type { FileData as GradioFileData } from "@gradio/upload";
+
+ export let widget: WidgetLayout | null = null;
+ let node: ComfyGalleryNode | null = null;
+ let nodeValue: Writable | null = null;
+ let propsChanged: Writable | null = null;
+ let option: number | null = null;
+
+ $: widget && setNodeValue(widget);
+
+ function setNodeValue(widget: WidgetLayout) {
+ if (widget) {
+ node = widget.node as ComfyGalleryNode
+ nodeValue = node.value;
+ propsChanged = node.propsChanged;
+ }
+ };
+
+ let style: Styles = {
+ // grid_cols: [2],
+ grid: [3],
+ // object_fit: "cover",
+ }
+ let element: HTMLDivElement;
+
+ function updateForLightbox() {
+ // Wait for gradio gallery to show the large preview image, if no timeout then
+ // the event might fire too early
+ setTimeout(() => {
+ const images = element.querySelectorAll('div.block div > img')
+ if (images != null) {
+ images.forEach(ImageViewer.instance.setupImageForLightbox.bind(ImageViewer.instance));
+ }
+ ImageViewer.instance.updateOnBackgroundChange();
+ }, 200)
+ }
+
+
+
+ {#key $propsChanged}
+ {#if node !== null && nodeValue !== null}
+
+ {/if}
+ {/key}
-
diff --git a/src/lib/widgets/ComfyGalleryWidget.svelte b/src/lib/widgets/ComfyGalleryWidget.svelte
index ef8228a..e69de29 100644
--- a/src/lib/widgets/ComfyGalleryWidget.svelte
+++ b/src/lib/widgets/ComfyGalleryWidget.svelte
@@ -1,57 +0,0 @@
-
-
- {#if item && itemValue}
-
-
-
- {/if}
-
-
-
diff --git a/src/lib/widgets/ComfyGalleryWidget.ts b/src/lib/widgets/ComfyGalleryWidget.ts
deleted file mode 100644
index fa0b747..0000000
--- a/src/lib/widgets/ComfyGalleryWidget.ts
+++ /dev/null
@@ -1,26 +0,0 @@
-import { get } from "svelte/store";
-import type { WidgetPanelOptions } from "@litegraph-ts/core";
-import ComfyWidget from "./ComfyWidget";
-import type { ComfyImageResult } from "$lib/nodes/ComfySaveImageNode";
-import queueState from "$lib/stores/queueState";
-
-export type ComfyGalleryEntry = [string, string | null]; //
+ {#if widget && node && nodeValue}
+
+
+ {/if}
+
+
+
diff --git a/src/lib/widgets/RangeWidget.svelte b/src/lib/widgets/RangeWidget.svelte
index 20f1680..6e50522 100644
--- a/src/lib/widgets/RangeWidget.svelte
+++ b/src/lib/widgets/RangeWidget.svelte
@@ -1,35 +1,52 @@
+
+
+
- {#if item !== null && option !== null}
+ {#if node !== null && option !== null}
- import type { WidgetUIState, WidgetUIStateStore } from "$lib/stores/widgetState";
import { TextBox } from "@gradio/form";
- export let item: WidgetUIState | null = null;
+ import type { ComfyComboNode } from "$lib/nodes/index";
+ import { type WidgetLayout } from "$lib/stores/layoutState";
+ import { get, type Writable } from "svelte/store";
+ export let widget: WidgetLayout | null = null;
+ let node: ComfyComboNode | null = null;
+ let nodeValue: Writable | null = null;
+ let propsChanged: Writable | null = null;
let itemValue: WidgetUIStateStore | null = null;
- $: if (item) { itemValue = item.value; }
+
+ $: widget && setNodeValue(widget);
+
+ function setNodeValue(widget: WidgetLayout) {
+ if (widget) {
+ node = widget.node as ComfySliderNode
+ nodeValue = node.value;
+ propsChanged = node.propsChanged;
+ }
+ };
+
+ // I don't know why but this is necessary to watch for changes to node
+ // properties from ComfyWidgetNode.
+ $: if (nodeValue !== null && (!$propsChanged || $propsChanged)) {
+ setNodeValue(widget)
+ node.properties = node.properties
+ }
- {#if item !== null && itemValue !== null}
+ {#if node !== null && nodeValue !== null}
- import { onMount } from "svelte";
- import { get } from "svelte/store";
- import { Pane, Splitpanes } from 'svelte-splitpanes';
- import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
- import { Checkbox } from "@gradio/form"
- import widgetState from "$lib/stores/widgetState";
- import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState";
- import { ImageViewer } from "$lib/ImageViewer";
- import { download } from "$lib/utils"
- import { LGraph, LGraphNode } from "@litegraph-ts/core";
- import type { ComfyAPIStatus } from "$lib/api";
- import queueState from "$lib/stores/queueState";
- import { Page, Navbar, Link, BlockTitle, Block, List, ListItem, Toolbar } from "framework7-svelte"
- import { getComponentForWidgetState } from "$lib/utils"
+ import { Link, Toolbar } from "framework7-svelte"
import { f7 } from "framework7-svelte"
export let subworkflowID: number = -1;
diff --git a/src/mobile/routes/home.svelte b/src/mobile/routes/home.svelte
index 5d61f2d..c26bb47 100644
--- a/src/mobile/routes/home.svelte
+++ b/src/mobile/routes/home.svelte
@@ -5,8 +5,6 @@
import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form"
- import widgetState from "$lib/stores/widgetState";
- import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
@@ -18,47 +16,11 @@
let app: ComfyApp | null = null;
- let serializedPaneOrder = {};
-
- function serializeAppState(): SerializedAppState {
- const graph = app.lGraph;
-
- const serializedGraph = graph.serialize()
-
- return {
- createdBy: "ComfyBox",
- version: 1,
- workflow: serializedGraph,
- panes: serializedPaneOrder
- }
- }
-
- function doAutosave(graph: LGraph): void {
- const savedWorkflow = serializeAppState();
- localStorage.setItem("workflow", JSON.stringify(savedWorkflow))
- }
-
- function doRestore(workflow: SerializedAppState) {
- serializedPaneOrder = workflow.panes;
- }
-
onMount(async () => {
if (app)
return
app = $uiState.app = new ComfyApp();
- // TODO dedup
- app.eventBus.on("nodeAdded", nodeState.nodeAdded);
- app.eventBus.on("nodeRemoved", nodeState.nodeRemoved);
- app.eventBus.on("configured", nodeState.configureFinished);
- app.eventBus.on("cleared", nodeState.clear);
-
- app.eventBus.on("nodeAdded", widgetState.nodeAdded);
- app.eventBus.on("nodeRemoved", widgetState.nodeRemoved);
- app.eventBus.on("configured", widgetState.configureFinished);
- app.eventBus.on("cleared", widgetState.clear);
- app.eventBus.on("autosave", doAutosave);
- app.eventBus.on("restored", doRestore);
app.api.addEventListener("status", (ev: CustomEvent) => {
queueState.statusUpdated(ev.detail as ComfyAPIStatus);
diff --git a/src/mobile/routes/list-subworkflows.svelte b/src/mobile/routes/list-subworkflows.svelte
index 98f6706..c9a4fea 100644
--- a/src/mobile/routes/list-subworkflows.svelte
+++ b/src/mobile/routes/list-subworkflows.svelte
@@ -5,8 +5,6 @@
import { Button } from "@gradio/button";
import ComfyApp, { type SerializedAppState } from "$lib/components/ComfyApp";
import { Checkbox } from "@gradio/form"
- import widgetState from "$lib/stores/widgetState";
- import nodeState from "$lib/stores/nodeState";
import uiState from "$lib/stores/uiState";
import { ImageViewer } from "$lib/ImageViewer";
import { download } from "$lib/utils"
diff --git a/src/mobile/routes/subworkflow.svelte b/src/mobile/routes/subworkflow.svelte
index 0e1b905..88840b8 100644
--- a/src/mobile/routes/subworkflow.svelte
+++ b/src/mobile/routes/subworkflow.svelte
@@ -1,22 +1,9 @@