2023-10-21 02:49:04 +00:00
|
|
|
// @ts-check
|
|
|
|
/// <reference path="../../web/types/litegraph.d.ts" />
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @typedef { import("../../web/scripts/app")["app"] } app
|
|
|
|
* @typedef { import("../../web/types/litegraph") } LG
|
|
|
|
* @typedef { import("../../web/types/litegraph").IWidget } IWidget
|
|
|
|
* @typedef { import("../../web/types/litegraph").ContextMenuItem } ContextMenuItem
|
|
|
|
* @typedef { import("../../web/types/litegraph").INodeInputSlot } INodeInputSlot
|
|
|
|
* @typedef { import("../../web/types/litegraph").INodeOutputSlot } INodeOutputSlot
|
|
|
|
* @typedef { InstanceType<LG["LGraphNode"]> & { widgets?: Array<IWidget> } } LGNode
|
|
|
|
* @typedef { (...args: EzOutput[] | [...EzOutput[], Record<string, unknown>]) => EzNode } EzNodeFactory
|
|
|
|
*/
|
|
|
|
|
|
|
|
export class EzConnection {
|
|
|
|
/** @type { app } */
|
|
|
|
app;
|
|
|
|
/** @type { InstanceType<LG["LLink"]> } */
|
|
|
|
link;
|
|
|
|
|
|
|
|
get originNode() {
|
|
|
|
return new EzNode(this.app, this.app.graph.getNodeById(this.link.origin_id));
|
|
|
|
}
|
|
|
|
|
|
|
|
get originOutput() {
|
|
|
|
return this.originNode.outputs[this.link.origin_slot];
|
|
|
|
}
|
|
|
|
|
|
|
|
get targetNode() {
|
|
|
|
return new EzNode(this.app, this.app.graph.getNodeById(this.link.target_id));
|
|
|
|
}
|
|
|
|
|
|
|
|
get targetInput() {
|
|
|
|
return this.targetNode.inputs[this.link.target_slot];
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { app } app
|
|
|
|
* @param { InstanceType<LG["LLink"]> } link
|
|
|
|
*/
|
|
|
|
constructor(app, link) {
|
|
|
|
this.app = app;
|
|
|
|
this.link = link;
|
|
|
|
}
|
|
|
|
|
|
|
|
disconnect() {
|
|
|
|
this.targetInput.disconnect();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EzSlot {
|
|
|
|
/** @type { EzNode } */
|
|
|
|
node;
|
|
|
|
/** @type { number } */
|
|
|
|
index;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { EzNode } node
|
|
|
|
* @param { number } index
|
|
|
|
*/
|
|
|
|
constructor(node, index) {
|
|
|
|
this.node = node;
|
|
|
|
this.index = index;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EzInput extends EzSlot {
|
|
|
|
/** @type { INodeInputSlot } */
|
|
|
|
input;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { EzNode } node
|
|
|
|
* @param { number } index
|
|
|
|
* @param { INodeInputSlot } input
|
|
|
|
*/
|
|
|
|
constructor(node, index, input) {
|
|
|
|
super(node, index);
|
|
|
|
this.input = input;
|
|
|
|
}
|
|
|
|
|
2023-12-13 05:56:39 +00:00
|
|
|
get connection() {
|
|
|
|
const link = this.node.node.inputs?.[this.index]?.link;
|
|
|
|
if (link == null) {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
return new EzConnection(this.node.app, this.node.app.graph.links[link]);
|
|
|
|
}
|
|
|
|
|
2023-10-21 02:49:04 +00:00
|
|
|
disconnect() {
|
|
|
|
this.node.node.disconnectInput(this.index);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EzOutput extends EzSlot {
|
|
|
|
/** @type { INodeOutputSlot } */
|
|
|
|
output;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { EzNode } node
|
|
|
|
* @param { number } index
|
|
|
|
* @param { INodeOutputSlot } output
|
|
|
|
*/
|
|
|
|
constructor(node, index, output) {
|
|
|
|
super(node, index);
|
|
|
|
this.output = output;
|
|
|
|
}
|
|
|
|
|
|
|
|
get connections() {
|
|
|
|
return (this.node.node.outputs?.[this.index]?.links ?? []).map(
|
|
|
|
(l) => new EzConnection(this.node.app, this.node.app.graph.links[l])
|
|
|
|
);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { EzInput } input
|
|
|
|
*/
|
|
|
|
connectTo(input) {
|
|
|
|
if (!input) throw new Error("Invalid input");
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @type { LG["LLink"] | null }
|
|
|
|
*/
|
|
|
|
const link = this.node.node.connect(this.index, input.node.node, input.index);
|
|
|
|
if (!link) {
|
|
|
|
const inp = input.input;
|
|
|
|
const inName = inp.name || inp.label || inp.type;
|
|
|
|
throw new Error(
|
2023-12-05 20:28:05 +00:00
|
|
|
`Connecting from ${input.node.node.type}#${input.node.id}[${inName}#${input.index}] -> ${this.node.node.type}#${this.node.id}[${
|
2023-10-21 02:49:04 +00:00
|
|
|
this.output.name ?? this.output.type
|
|
|
|
}#${this.index}] failed.`
|
|
|
|
);
|
|
|
|
}
|
|
|
|
return link;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EzNodeMenuItem {
|
|
|
|
/** @type { EzNode } */
|
|
|
|
node;
|
|
|
|
/** @type { number } */
|
|
|
|
index;
|
|
|
|
/** @type { ContextMenuItem } */
|
|
|
|
item;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { EzNode } node
|
|
|
|
* @param { number } index
|
|
|
|
* @param { ContextMenuItem } item
|
|
|
|
*/
|
|
|
|
constructor(node, index, item) {
|
|
|
|
this.node = node;
|
|
|
|
this.index = index;
|
|
|
|
this.item = item;
|
|
|
|
}
|
|
|
|
|
|
|
|
call(selectNode = true) {
|
|
|
|
if (!this.item?.callback) throw new Error(`Menu Item ${this.item?.content ?? "[null]"} has no callback.`);
|
|
|
|
if (selectNode) {
|
|
|
|
this.node.select();
|
|
|
|
}
|
2023-11-30 19:13:27 +00:00
|
|
|
return this.item.callback.call(this.node.node, undefined, undefined, undefined, undefined, this.node.node);
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EzWidget {
|
|
|
|
/** @type { EzNode } */
|
|
|
|
node;
|
|
|
|
/** @type { number } */
|
|
|
|
index;
|
|
|
|
/** @type { IWidget } */
|
|
|
|
widget;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { EzNode } node
|
|
|
|
* @param { number } index
|
|
|
|
* @param { IWidget } widget
|
|
|
|
*/
|
|
|
|
constructor(node, index, widget) {
|
|
|
|
this.node = node;
|
|
|
|
this.index = index;
|
|
|
|
this.widget = widget;
|
|
|
|
}
|
|
|
|
|
|
|
|
get value() {
|
|
|
|
return this.widget.value;
|
|
|
|
}
|
|
|
|
|
|
|
|
set value(v) {
|
|
|
|
this.widget.value = v;
|
2023-12-05 20:28:05 +00:00
|
|
|
this.widget.callback?.call?.(this.widget, v)
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
get isConvertedToInput() {
|
|
|
|
// @ts-ignore : this type is valid for converted widgets
|
|
|
|
return this.widget.type === "converted-widget";
|
|
|
|
}
|
|
|
|
|
|
|
|
getConvertedInput() {
|
|
|
|
if (!this.isConvertedToInput) throw new Error(`Widget ${this.widget.name} is not converted to input.`);
|
|
|
|
|
|
|
|
return this.node.inputs.find((inp) => inp.input["widget"]?.name === this.widget.name);
|
|
|
|
}
|
|
|
|
|
|
|
|
convertToWidget() {
|
|
|
|
if (!this.isConvertedToInput)
|
|
|
|
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already a widget.`);
|
2024-04-18 20:41:23 +00:00
|
|
|
var menu = this.node.menu["Convert Input to Widget"].item.submenu.options;
|
2024-04-13 00:01:34 +00:00
|
|
|
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to widget`);
|
|
|
|
menu[index].callback.call();
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
convertToInput() {
|
|
|
|
if (this.isConvertedToInput)
|
|
|
|
throw new Error(`Widget ${this.widget.name} cannot be converted as it is already an input.`);
|
2024-04-18 20:41:23 +00:00
|
|
|
var menu = this.node.menu["Convert Widget to Input"].item.submenu.options;
|
2024-04-13 00:01:34 +00:00
|
|
|
var index = menu.findIndex(a => a.content == `Convert ${this.widget.name} to input`);
|
|
|
|
menu[index].callback.call();
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EzNode {
|
|
|
|
/** @type { app } */
|
|
|
|
app;
|
|
|
|
/** @type { LGNode } */
|
|
|
|
node;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { app } app
|
|
|
|
* @param { LGNode } node
|
|
|
|
*/
|
|
|
|
constructor(app, node) {
|
|
|
|
this.app = app;
|
|
|
|
this.node = node;
|
|
|
|
}
|
|
|
|
|
|
|
|
get id() {
|
|
|
|
return this.node.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
get inputs() {
|
|
|
|
return this.#makeLookupArray("inputs", "name", EzInput);
|
|
|
|
}
|
|
|
|
|
|
|
|
get outputs() {
|
|
|
|
return this.#makeLookupArray("outputs", "name", EzOutput);
|
|
|
|
}
|
|
|
|
|
|
|
|
get widgets() {
|
|
|
|
return this.#makeLookupArray("widgets", "name", EzWidget);
|
|
|
|
}
|
|
|
|
|
|
|
|
get menu() {
|
|
|
|
return this.#makeLookupArray(() => this.app.canvas.getNodeMenuOptions(this.node), "content", EzNodeMenuItem);
|
|
|
|
}
|
|
|
|
|
2023-11-30 19:13:27 +00:00
|
|
|
get isRemoved() {
|
|
|
|
return !this.app.graph.getNodeById(this.id);
|
|
|
|
}
|
|
|
|
|
|
|
|
select(addToSelection = false) {
|
|
|
|
this.app.canvas.selectNode(this.node, addToSelection);
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
// /**
|
|
|
|
// * @template { "inputs" | "outputs" } T
|
|
|
|
// * @param { T } type
|
|
|
|
// * @returns { Record<string, type extends "inputs" ? EzInput : EzOutput> & (type extends "inputs" ? EzInput [] : EzOutput[]) }
|
|
|
|
// */
|
|
|
|
// #getSlotItems(type) {
|
|
|
|
// // @ts-ignore : these items are correct
|
|
|
|
// return (this.node[type] ?? []).reduce((p, s, i) => {
|
|
|
|
// if (s.name in p) {
|
|
|
|
// throw new Error(`Unable to store input ${s.name} on array as name conflicts.`);
|
|
|
|
// }
|
|
|
|
// // @ts-ignore
|
|
|
|
// p.push((p[s.name] = new (type === "inputs" ? EzInput : EzOutput)(this, i, s)));
|
|
|
|
// return p;
|
|
|
|
// }, Object.assign([], { $: this }));
|
|
|
|
// }
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @template { { new(node: EzNode, index: number, obj: any): any } } T
|
|
|
|
* @param { "inputs" | "outputs" | "widgets" | (() => Array<unknown>) } nodeProperty
|
|
|
|
* @param { string } nameProperty
|
|
|
|
* @param { T } ctor
|
|
|
|
* @returns { Record<string, InstanceType<T>> & Array<InstanceType<T>> }
|
|
|
|
*/
|
|
|
|
#makeLookupArray(nodeProperty, nameProperty, ctor) {
|
|
|
|
const items = typeof nodeProperty === "function" ? nodeProperty() : this.node[nodeProperty];
|
|
|
|
// @ts-ignore
|
|
|
|
return (items ?? []).reduce((p, s, i) => {
|
|
|
|
if (!s) return p;
|
|
|
|
|
|
|
|
const name = s[nameProperty];
|
2023-11-30 19:13:27 +00:00
|
|
|
const item = new ctor(this, i, s);
|
2023-10-21 02:49:04 +00:00
|
|
|
// @ts-ignore
|
2023-11-30 19:13:27 +00:00
|
|
|
p.push(item);
|
|
|
|
if (name) {
|
|
|
|
// @ts-ignore
|
|
|
|
if (name in p) {
|
|
|
|
throw new Error(`Unable to store ${nodeProperty} ${name} on array as name conflicts.`);
|
|
|
|
}
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
// @ts-ignore
|
2023-11-30 19:13:27 +00:00
|
|
|
p[name] = item;
|
2023-10-21 02:49:04 +00:00
|
|
|
return p;
|
|
|
|
}, Object.assign([], { $: this }));
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
export class EzGraph {
|
|
|
|
/** @type { app } */
|
|
|
|
app;
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { app } app
|
|
|
|
*/
|
|
|
|
constructor(app) {
|
|
|
|
this.app = app;
|
|
|
|
}
|
|
|
|
|
|
|
|
get nodes() {
|
|
|
|
return this.app.graph._nodes.map((n) => new EzNode(this.app, n));
|
|
|
|
}
|
|
|
|
|
|
|
|
clear() {
|
|
|
|
this.app.graph.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
arrange() {
|
|
|
|
this.app.graph.arrange();
|
|
|
|
}
|
|
|
|
|
|
|
|
stringify() {
|
2023-12-05 20:28:05 +00:00
|
|
|
return JSON.stringify(this.app.graph.serialize(), undefined);
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param { number | LGNode | EzNode } obj
|
|
|
|
* @returns { EzNode }
|
|
|
|
*/
|
|
|
|
find(obj) {
|
|
|
|
let match;
|
|
|
|
let id;
|
|
|
|
if (typeof obj === "number") {
|
|
|
|
id = obj;
|
|
|
|
} else {
|
|
|
|
id = obj.id;
|
|
|
|
}
|
|
|
|
|
|
|
|
match = this.app.graph.getNodeById(id);
|
|
|
|
|
|
|
|
if (!match) {
|
|
|
|
throw new Error(`Unable to find node with ID ${id}.`);
|
|
|
|
}
|
|
|
|
|
|
|
|
return new EzNode(this.app, match);
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @returns { Promise<void> }
|
|
|
|
*/
|
|
|
|
reload() {
|
|
|
|
const graph = JSON.parse(JSON.stringify(this.app.graph.serialize()));
|
|
|
|
return new Promise((r) => {
|
|
|
|
this.app.graph.clear();
|
|
|
|
setTimeout(async () => {
|
|
|
|
await this.app.loadGraphData(graph);
|
|
|
|
r();
|
|
|
|
}, 10);
|
|
|
|
});
|
|
|
|
}
|
2023-11-30 19:13:27 +00:00
|
|
|
|
|
|
|
/**
|
|
|
|
* @returns { Promise<{
|
|
|
|
* workflow: {},
|
|
|
|
* output: Record<string, {
|
|
|
|
* class_name: string,
|
|
|
|
* inputs: Record<string, [string, number] | unknown>
|
|
|
|
* }>}> }
|
|
|
|
*/
|
|
|
|
toPrompt() {
|
|
|
|
// @ts-ignore
|
|
|
|
return this.app.graphToPrompt();
|
|
|
|
}
|
2023-10-21 02:49:04 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
export const Ez = {
|
|
|
|
/**
|
|
|
|
* Quickly build and interact with a ComfyUI graph
|
|
|
|
* @example
|
|
|
|
* const { ez, graph } = Ez.graph(app);
|
|
|
|
* graph.clear();
|
2023-11-30 19:13:27 +00:00
|
|
|
* const [model, clip, vae] = ez.CheckpointLoaderSimple().outputs;
|
|
|
|
* const [pos] = ez.CLIPTextEncode(clip, { text: "positive" }).outputs;
|
|
|
|
* const [neg] = ez.CLIPTextEncode(clip, { text: "negative" }).outputs;
|
|
|
|
* const [latent] = ez.KSampler(model, pos, neg, ...ez.EmptyLatentImage().outputs).outputs;
|
|
|
|
* const [image] = ez.VAEDecode(latent, vae).outputs;
|
|
|
|
* const saveNode = ez.SaveImage(image);
|
2023-10-21 02:49:04 +00:00
|
|
|
* console.log(saveNode);
|
|
|
|
* graph.arrange();
|
|
|
|
* @param { app } app
|
|
|
|
* @param { LG["LiteGraph"] } LiteGraph
|
|
|
|
* @param { LG["LGraphCanvas"] } LGraphCanvas
|
|
|
|
* @param { boolean } clearGraph
|
|
|
|
* @returns { { graph: EzGraph, ez: Record<string, EzNodeFactory> } }
|
|
|
|
*/
|
|
|
|
graph(app, LiteGraph = window["LiteGraph"], LGraphCanvas = window["LGraphCanvas"], clearGraph = true) {
|
|
|
|
// Always set the active canvas so things work
|
|
|
|
LGraphCanvas.active_canvas = app.canvas;
|
|
|
|
|
|
|
|
if (clearGraph) {
|
|
|
|
app.graph.clear();
|
|
|
|
}
|
|
|
|
|
|
|
|
// @ts-ignore : this proxy handles utility methods & node creation
|
|
|
|
const factory = new Proxy(
|
|
|
|
{},
|
|
|
|
{
|
|
|
|
get(_, p) {
|
|
|
|
if (typeof p !== "string") throw new Error("Invalid node");
|
|
|
|
const node = LiteGraph.createNode(p);
|
|
|
|
if (!node) throw new Error(`Unknown node "${p}"`);
|
|
|
|
app.graph.add(node);
|
|
|
|
|
|
|
|
/**
|
|
|
|
* @param {Parameters<EzNodeFactory>} args
|
|
|
|
*/
|
|
|
|
return function (...args) {
|
|
|
|
const ezNode = new EzNode(app, node);
|
|
|
|
const inputs = ezNode.inputs;
|
|
|
|
|
|
|
|
let slot = 0;
|
|
|
|
for (const arg of args) {
|
|
|
|
if (arg instanceof EzOutput) {
|
|
|
|
arg.connectTo(inputs[slot++]);
|
|
|
|
} else {
|
|
|
|
for (const k in arg) {
|
|
|
|
ezNode.widgets[k].value = arg[k];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return ezNode;
|
|
|
|
};
|
|
|
|
},
|
|
|
|
}
|
|
|
|
);
|
|
|
|
|
|
|
|
return { graph: new EzGraph(app), ez: factory };
|
|
|
|
},
|
|
|
|
};
|