Copy action and button
This commit is contained in:
53
src/lib/nodes/ComfyActionNodes.ts
Normal file
53
src/lib/nodes/ComfyActionNodes.ts
Normal file
@@ -0,0 +1,53 @@
|
||||
import { LiteGraph, type ContextMenuItem, type LGraphNode, type Vector2, LConnectionKind, LLink, LGraphCanvas, type SlotType, TitleMode, type SlotLayout, BuiltInSlotType, type ITextWidget } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
import { Watch } from "@litegraph-ts/nodes-basic";
|
||||
|
||||
export interface ComfyCopyActionProperties extends Record<any, any> {
|
||||
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<ITextWidget>(
|
||||
"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"
|
||||
})
|
||||
92
src/lib/nodes/ComfyBackendNode.ts
Normal file
92
src/lib/nodes/ComfyBackendNode.ts
Normal file
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,7 +2,6 @@ import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
||||
import { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
|
||||
export default class ComfyGraphNode extends LGraphNode {
|
||||
comfyClass: string | null
|
||||
isBackendNode?: boolean;
|
||||
|
||||
afterQueued?(): void;
|
||||
|
||||
@@ -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<ComfyImageResult> = [];
|
||||
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 {
|
||||
}
|
||||
@@ -1,15 +1,18 @@
|
||||
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 } from "@litegraph-ts/core";
|
||||
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 { ComfyWidgets } from "$lib/widgets";
|
||||
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<string, any> {
|
||||
defaultValue: any
|
||||
@@ -36,6 +39,10 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
override isBackendNode = false;
|
||||
override serialize_widgets = true;
|
||||
|
||||
outputIndex: number = 0;
|
||||
inputIndex: number = 0;
|
||||
changedIndex: number = 1;
|
||||
|
||||
displayWidget: ITextWidget;
|
||||
|
||||
override size: Vector2 = [60, 40];
|
||||
@@ -55,24 +62,47 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
this.unsubscribe = this.value.subscribe(this.onValueUpdated.bind(this))
|
||||
}
|
||||
|
||||
formatValue(value: any): string {
|
||||
return Watch.toString(value)
|
||||
}
|
||||
|
||||
private onValueUpdated(value: any) {
|
||||
console.debug("[Widget] valueUpdated", this, value)
|
||||
this.displayWidget.value = Watch.toString(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)
|
||||
}
|
||||
|
||||
abstract validateValue(value: any): boolean;
|
||||
|
||||
/*
|
||||
* Logic to run if this widget can be treated as output (slider, combo, text)
|
||||
*/
|
||||
override onExecute() {
|
||||
// Assumption: we will have one output in the inherited class with the
|
||||
// correct type
|
||||
this.setOutputData(0, get(this.value))
|
||||
if (this.inputs.length >= this.inputIndex) {
|
||||
const data = this.getInputData(this.inputIndex)
|
||||
if (data && this.validateValue(data)) { // TODO can "null" be a legitimate value here?
|
||||
this.setValue(data)
|
||||
}
|
||||
}
|
||||
if (this.outputs.length >= this.outputIndex) {
|
||||
this.setOutputData(this.outputIndex, get(this.value))
|
||||
}
|
||||
}
|
||||
|
||||
const outputLinks = this.getOutputLinks(0)
|
||||
console.debug("[Widget] onExecute", this, outputLinks)
|
||||
|
||||
// TODO send event to linked nodes
|
||||
/** Called when a backend node sends a ComfyUI output over a link */
|
||||
receiveOutput() {
|
||||
}
|
||||
|
||||
onConnectOutput(
|
||||
@@ -93,7 +123,7 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
this.setValue(this.properties.defaultValue)
|
||||
|
||||
const widget = layoutState.findLayoutForNode(this.id)
|
||||
if (widget) {
|
||||
if (widget && input.name !== "") {
|
||||
widget.attrs.title = input.name;
|
||||
}
|
||||
|
||||
@@ -164,8 +194,12 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
|
||||
override svelteComponentType = RangeWidget
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
inputs: [
|
||||
{ name: "value", type: "number" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "number" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -173,6 +207,12 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
|
||||
super(name, 0)
|
||||
}
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
return typeof value === "number"
|
||||
&& value >= this.properties.min
|
||||
&& value <= 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))
|
||||
@@ -199,8 +239,12 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
inputs: [
|
||||
{ name: "value", type: "string" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "string" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -236,6 +280,12 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||
return true;
|
||||
}
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
if (typeof value !== "string")
|
||||
return false;
|
||||
return this.properties.values.indexOf(value) !== -1;
|
||||
}
|
||||
|
||||
override clampOneConfig(input: IComfyInputSlot) {
|
||||
if (input.config.values.indexOf(this.properties.value) === -1) {
|
||||
if (input.config.values.length === 0)
|
||||
@@ -264,8 +314,12 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
inputs: [
|
||||
{ name: "value", type: "string" }
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "string" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||
]
|
||||
}
|
||||
|
||||
@@ -274,6 +328,10 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||
constructor(name?: string) {
|
||||
super(name, "")
|
||||
}
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
return typeof value === "string"
|
||||
}
|
||||
}
|
||||
|
||||
LiteGraph.registerNodeType({
|
||||
@@ -282,3 +340,121 @@ LiteGraph.registerNodeType({
|
||||
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<GradioFileData[]> {
|
||||
override properties: ComfyGalleryProperties = {
|
||||
defaultValue: []
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
inputs: [
|
||||
{ name: "images", type: "OUTPUT" }
|
||||
]
|
||||
}
|
||||
|
||||
override svelteComponentType = GalleryWidget
|
||||
|
||||
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[]): string {
|
||||
return `Images: ${value.length}`
|
||||
}
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
return Array.isArray(value) && value.every(e => "images" in e)
|
||||
}
|
||||
|
||||
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[] = data.images.map(r => {
|
||||
// TODO configure backend URL
|
||||
const url = "http://localhost:8188/view?"
|
||||
const params = new URLSearchParams(r)
|
||||
return {
|
||||
name: null,
|
||||
data: url + params
|
||||
}
|
||||
});
|
||||
|
||||
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<boolean> {
|
||||
override properties: ComfyButtonProperties = {
|
||||
defaultValue: false,
|
||||
message: "bang"
|
||||
}
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
{ name: "event", type: BuiltInSlotType.EVENT },
|
||||
{ name: "isClicked", type: "boolean" },
|
||||
]
|
||||
}
|
||||
|
||||
override outputIndex = 1;
|
||||
override svelteComponentType = ButtonWidget;
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
return typeof value === "boolean"
|
||||
}
|
||||
|
||||
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"
|
||||
})
|
||||
|
||||
@@ -1,3 +1,3 @@
|
||||
export { default as ComfyReroute } from "./ComfyReroute"
|
||||
export { ComfySaveImageNode, ComfyPreviewImageNode } from "./ComfyImageNodes"
|
||||
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
|
||||
export { ComfyCopyAction } from "./ComfyActionNodes"
|
||||
|
||||
Reference in New Issue
Block a user