Add some branching nodes
This commit is contained in:
@@ -1,10 +1,24 @@
|
||||
import type { ComfyInputConfig } from "$lib/IComfyInputSlot";
|
||||
import type { SerializedPrompt } from "$lib/components/ComfyApp";
|
||||
import type ComfyWidget from "$lib/components/widgets/ComfyWidget";
|
||||
import { LGraph, LGraphNode } from "@litegraph-ts/core";
|
||||
import type { SvelteComponentDev } from "svelte/internal";
|
||||
import type { ComfyWidgetNode } from "./ComfyWidgetNodes";
|
||||
|
||||
export type DefaultWidgetSpec = {
|
||||
defaultWidgetNode: new (name?: string) => ComfyWidgetNode,
|
||||
config?: ComfyInputConfig
|
||||
}
|
||||
|
||||
export type DefaultWidgetLayout = {
|
||||
inputs?: Record<number, DefaultWidgetSpec>,
|
||||
}
|
||||
|
||||
export default class ComfyGraphNode extends LGraphNode {
|
||||
isBackendNode?: boolean;
|
||||
|
||||
afterQueued?(prompt: SerializedPrompt): void;
|
||||
onExecuted?(output: any): void;
|
||||
|
||||
defaultWidgets?: DefaultWidgetLayout
|
||||
}
|
||||
|
||||
120
src/lib/nodes/ComfySelector.ts
Normal file
120
src/lib/nodes/ComfySelector.ts
Normal file
@@ -0,0 +1,120 @@
|
||||
import { BuiltInSlotType, LiteGraph, type SlotLayout } from "@litegraph-ts/core";
|
||||
import ComfyGraphNode from "./ComfyGraphNode";
|
||||
|
||||
export interface ComfySelectorProperties extends Record<any, any> {
|
||||
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<any, any> {
|
||||
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"
|
||||
})
|
||||
107
src/lib/nodes/ComfyValueControl.ts
Normal file
107
src/lib/nodes/ComfyValueControl.ts
Normal file
@@ -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<any, any> {
|
||||
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"
|
||||
})
|
||||
@@ -36,6 +36,15 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
/** 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<string, { type: string, index: number }> = {}
|
||||
outputProperties: { name: string, type: string }[] = []
|
||||
|
||||
override isBackendNode = false;
|
||||
override serialize_widgets = true;
|
||||
|
||||
@@ -62,6 +71,18 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
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)
|
||||
}
|
||||
@@ -84,21 +105,33 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
this.value.set(value)
|
||||
}
|
||||
|
||||
abstract validateValue(value: any): boolean;
|
||||
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.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.copyFromInputLink) {
|
||||
if (this.inputs.length >= this.inputIndex) {
|
||||
const data = this.getInputData(this.inputIndex)
|
||||
if (data) { // TODO can "null" be a legitimate value here?
|
||||
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 */
|
||||
@@ -118,18 +151,19 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
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))
|
||||
}
|
||||
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))
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -167,11 +201,13 @@ export abstract class ComfyWidgetNode<T = any> extends ComfyGraphNode {
|
||||
clampOneConfig(input: IComfyInputSlot) { }
|
||||
|
||||
override onSerialize(o: SerializedLGraphNode) {
|
||||
(o as any).comfyValue = get(this.value)
|
||||
(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.value.set((o as any).comfyValue);
|
||||
this.shownOutputProperties = (o as any).shownOutputProperties;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -199,25 +235,32 @@ export class ComfySliderNode extends ComfyWidgetNode<number> {
|
||||
],
|
||||
outputs: [
|
||||
{ name: "value", type: "number" },
|
||||
{ name: "changed", type: BuiltInSlotType.EVENT }
|
||||
{ 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 validateValue(value: any): boolean {
|
||||
return typeof value === "number"
|
||||
&& value >= this.properties.min
|
||||
&& value <= this.properties.max
|
||||
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(clamp(this.properties.defaultValue, this.properties.min, this.properties.max))
|
||||
this.setValue(this.properties.defaultValue)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -280,10 +323,10 @@ 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 setValue(value: any) {
|
||||
if (typeof value !== "string" || this.properties.values.indexOf(value) === -1)
|
||||
return;
|
||||
super.setValue(value)
|
||||
}
|
||||
|
||||
override clampOneConfig(input: IComfyInputSlot) {
|
||||
@@ -291,7 +334,7 @@ export class ComfyComboNode extends ComfyWidgetNode<string> {
|
||||
if (input.config.values.length === 0)
|
||||
this.setValue("")
|
||||
else
|
||||
this.setValue(input.config.values[0])
|
||||
this.setValue(input.config.defaultValue || input.config.values[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -329,8 +372,8 @@ export class ComfyTextNode extends ComfyWidgetNode<string> {
|
||||
super(name, "")
|
||||
}
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
return typeof value === "string"
|
||||
override setValue(value: any) {
|
||||
super.setValue(`${value}`)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -368,6 +411,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
}
|
||||
|
||||
override svelteComponentType = GalleryWidget
|
||||
override copyFromInputLink = false;
|
||||
|
||||
constructor(name?: string) {
|
||||
super(name, [])
|
||||
@@ -380,12 +424,29 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
}
|
||||
}
|
||||
|
||||
override formatValue(value: GradioFileData[]): string {
|
||||
return `Images: ${value.length}`
|
||||
override formatValue(value: GradioFileData[] | null): string {
|
||||
return `Images: ${value?.length || 0}`
|
||||
}
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
return Array.isArray(value) && value.every(e => "images" in e)
|
||||
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() {
|
||||
@@ -394,15 +455,7 @@ export class ComfyGalleryNode extends ComfyWidgetNode<GradioFileData[]> {
|
||||
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 galleryItems: GradioFileData[] = this.convertItems(link.data)
|
||||
|
||||
const currentValue = get(this.value)
|
||||
this.setValue(currentValue.concat(galleryItems))
|
||||
@@ -429,7 +482,7 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||
|
||||
static slotLayout: SlotLayout = {
|
||||
outputs: [
|
||||
{ name: "event", type: BuiltInSlotType.EVENT },
|
||||
{ name: "clicked", type: BuiltInSlotType.EVENT },
|
||||
{ name: "isClicked", type: "boolean" },
|
||||
]
|
||||
}
|
||||
@@ -437,8 +490,8 @@ export class ComfyButtonNode extends ComfyWidgetNode<boolean> {
|
||||
override outputIndex = 1;
|
||||
override svelteComponentType = ButtonWidget;
|
||||
|
||||
override validateValue(value: any): boolean {
|
||||
return typeof value === "boolean"
|
||||
override setValue(value: any) {
|
||||
super.setValue(Boolean(value))
|
||||
}
|
||||
|
||||
onClick() {
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
export { default as ComfyReroute } from "./ComfyReroute"
|
||||
export { ComfyWidgetNode, ComfySliderNode, ComfyComboNode, ComfyTextNode } from "./ComfyWidgetNodes"
|
||||
export { ComfyCopyAction } from "./ComfyActionNodes"
|
||||
export { default as ComfyValueControl } from "./ComfyValueControl"
|
||||
export { default as ComfySelector } from "./ComfySelector"
|
||||
|
||||
Reference in New Issue
Block a user