| | import type { ComfyApp, ComfyNodeConstructor, ComfyObjectInfo } from "typings/comfy.js";
|
| | import type {
|
| | Vector2,
|
| | LGraphCanvas,
|
| | ContextMenuItem,
|
| | LLink,
|
| | LGraph,
|
| | IContextMenuOptions,
|
| | ContextMenu,
|
| | LGraphNode,
|
| | INodeSlot,
|
| | INodeInputSlot,
|
| | INodeOutputSlot,
|
| | } from "typings/litegraph.js";
|
| | import type { Constructor } from "typings/index.js";
|
| | import { app } from "scripts/app.js";
|
| | import { api } from "scripts/api.js";
|
| | import { Resolver, getResolver, wait } from "rgthree/common/shared_utils.js";
|
| | import { RgthreeHelpDialog } from "rgthree/common/dialog.js";
|
| |
|
| | |
| | |
| | |
| | |
| |
|
| | const oldApiGetNodeDefs = api.getNodeDefs;
|
| | api.getNodeDefs = async function () {
|
| | const defs = await oldApiGetNodeDefs.call(api);
|
| | this.dispatchEvent(new CustomEvent("fresh-node-defs", { detail: defs }));
|
| | return defs;
|
| | };
|
| |
|
| | export enum IoDirection {
|
| | INPUT,
|
| | OUTPUT,
|
| | }
|
| |
|
| | const PADDING = 0;
|
| |
|
| | type LiteGraphDir =
|
| | | typeof LiteGraph.LEFT
|
| | | typeof LiteGraph.RIGHT
|
| | | typeof LiteGraph.UP
|
| | | typeof LiteGraph.DOWN;
|
| | export const LAYOUT_LABEL_TO_DATA: { [label: string]: [LiteGraphDir, Vector2, Vector2] } = {
|
| | Left: [LiteGraph.LEFT, [0, 0.5], [PADDING, 0]],
|
| | Right: [LiteGraph.RIGHT, [1, 0.5], [-PADDING, 0]],
|
| | Top: [LiteGraph.UP, [0.5, 0], [0, PADDING]],
|
| | Bottom: [LiteGraph.DOWN, [0.5, 1], [0, -PADDING]],
|
| | };
|
| | export const LAYOUT_LABEL_OPPOSITES: { [label: string]: string } = {
|
| | Left: "Right",
|
| | Right: "Left",
|
| | Top: "Bottom",
|
| | Bottom: "Top",
|
| | };
|
| | export const LAYOUT_CLOCKWISE = ["Top", "Right", "Bottom", "Left"];
|
| |
|
| | interface MenuConfig {
|
| | name: string | ((node: LGraphNode) => string);
|
| | property?: string;
|
| | prepareValue?: (value: string, node: LGraphNode) => any;
|
| | callback?: (node: LGraphNode, value?: string) => void;
|
| | subMenuOptions?: (string | null)[] | ((node: LGraphNode) => (string | null)[]);
|
| | }
|
| |
|
| | export function addMenuItem(
|
| | node: Constructor<LGraphNode>,
|
| | _app: ComfyApp,
|
| | config: MenuConfig,
|
| | after = "Shape",
|
| | ) {
|
| | const oldGetExtraMenuOptions = node.prototype.getExtraMenuOptions;
|
| | node.prototype.getExtraMenuOptions = function (
|
| | canvas: LGraphCanvas,
|
| | menuOptions: ContextMenuItem[],
|
| | ) {
|
| | oldGetExtraMenuOptions && oldGetExtraMenuOptions.apply(this, [canvas, menuOptions]);
|
| | addMenuItemOnExtraMenuOptions(this, config, menuOptions, after);
|
| | };
|
| | }
|
| |
|
| | |
| | |
| |
|
| | let canvasResolver: Resolver<LGraphCanvas> | null = null;
|
| | export function waitForCanvas() {
|
| | if (canvasResolver === null) {
|
| | canvasResolver = getResolver<LGraphCanvas>();
|
| | function _waitForCanvas() {
|
| | if (!canvasResolver!.completed) {
|
| | if (app?.canvas) {
|
| | canvasResolver!.resolve(app.canvas);
|
| | } else {
|
| | requestAnimationFrame(_waitForCanvas);
|
| | }
|
| | }
|
| | }
|
| | _waitForCanvas();
|
| | }
|
| | return canvasResolver.promise;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | let graphResolver: Resolver<LGraph> | null = null;
|
| | export function waitForGraph() {
|
| | if (graphResolver === null) {
|
| | graphResolver = getResolver<LGraph>();
|
| | function _wait() {
|
| | if (!graphResolver!.completed) {
|
| | if (app?.graph) {
|
| | graphResolver!.resolve(app.graph);
|
| | } else {
|
| | requestAnimationFrame(_wait);
|
| | }
|
| | }
|
| | }
|
| | _wait();
|
| | }
|
| | return graphResolver.promise;
|
| | }
|
| |
|
| | export function addMenuItemOnExtraMenuOptions(
|
| | node: LGraphNode,
|
| | config: MenuConfig,
|
| | menuOptions: ContextMenuItem[],
|
| | after = "Shape",
|
| | ) {
|
| | let idx = menuOptions
|
| | .slice()
|
| | .reverse()
|
| | .findIndex((option) => (option as any)?.isRgthree);
|
| | if (idx == -1) {
|
| | idx = menuOptions.findIndex((option) => option?.content?.includes(after)) + 1;
|
| | if (!idx) {
|
| | idx = menuOptions.length - 1;
|
| | }
|
| |
|
| | menuOptions.splice(idx, 0, null);
|
| | idx++;
|
| | } else {
|
| | idx = menuOptions.length - idx;
|
| | }
|
| |
|
| | const subMenuOptions =
|
| | typeof config.subMenuOptions === "function"
|
| | ? config.subMenuOptions(node)
|
| | : config.subMenuOptions;
|
| |
|
| | menuOptions.splice(idx, 0, {
|
| | content: typeof config.name == "function" ? config.name(node) : config.name,
|
| | has_submenu: !!subMenuOptions?.length,
|
| | isRgthree: true,
|
| | callback: (
|
| | value: ContextMenuItem,
|
| | _options: IContextMenuOptions,
|
| | event: MouseEvent,
|
| | parentMenu: ContextMenu | undefined,
|
| | _node: LGraphNode,
|
| | ) => {
|
| | if (!!subMenuOptions?.length) {
|
| | new LiteGraph.ContextMenu(
|
| | subMenuOptions.map((option) => (option ? { content: option } : null)),
|
| | {
|
| | event,
|
| | parentMenu,
|
| | callback: (
|
| | subValue: ContextMenuItem,
|
| | _options: IContextMenuOptions,
|
| | _event: MouseEvent,
|
| | _parentMenu: ContextMenu | undefined,
|
| | _node: LGraphNode,
|
| | ) => {
|
| | if (config.property) {
|
| | node.properties = node.properties || {};
|
| | node.properties[config.property] = config.prepareValue
|
| | ? config.prepareValue(subValue!.content || '', node)
|
| | : subValue!.content || '';
|
| | }
|
| | config.callback && config.callback(node, subValue?.content);
|
| | },
|
| | },
|
| | );
|
| | return;
|
| | }
|
| | if (config.property) {
|
| | node.properties = node.properties || {};
|
| | node.properties[config.property] = config.prepareValue
|
| | ? config.prepareValue(node.properties[config.property], node)
|
| | : !node.properties[config.property];
|
| | }
|
| | config.callback && config.callback(node, value?.content);
|
| | },
|
| | } as ContextMenuItem);
|
| | }
|
| |
|
| | export function addConnectionLayoutSupport(
|
| | node: Constructor<LGraphNode>,
|
| | app: ComfyApp,
|
| | options = [
|
| | ["Left", "Right"],
|
| | ["Right", "Left"],
|
| | ],
|
| | callback?: (node: LGraphNode) => void,
|
| | ) {
|
| | addMenuItem(node, app, {
|
| | name: "Connections Layout",
|
| | property: "connections_layout",
|
| | subMenuOptions: options.map((option) => option[0] + (option[1] ? " -> " + option[1] : "")),
|
| | prepareValue: (value, node) => {
|
| | const values = value.split(" -> ");
|
| | if (!values[1] && !node.outputs?.length) {
|
| | values[1] = LAYOUT_LABEL_OPPOSITES[values[0]!]!;
|
| | }
|
| | if (!LAYOUT_LABEL_TO_DATA[values[0]!] || !LAYOUT_LABEL_TO_DATA[values[1]!]) {
|
| | throw new Error(`New Layout invalid: [${values[0]}, ${values[1]}]`);
|
| | }
|
| | return values;
|
| | },
|
| | callback: (node) => {
|
| | callback && callback(node);
|
| | app.graph.setDirtyCanvas(true, true);
|
| | },
|
| | });
|
| |
|
| |
|
| | node.prototype.getConnectionPos = function (isInput: boolean, slotNumber: number, out: Vector2) {
|
| |
|
| |
|
| | return getConnectionPosForLayout(this, isInput, slotNumber, out);
|
| | };
|
| | }
|
| |
|
| | export function setConnectionsLayout(node: LGraphNode, newLayout: [string, string]) {
|
| | newLayout = newLayout || (node as any).defaultConnectionsLayout || ["Left", "Right"];
|
| |
|
| |
|
| | if (!newLayout[1] && !node.outputs?.length) {
|
| | newLayout[1] = LAYOUT_LABEL_OPPOSITES[newLayout[0]!]!;
|
| | }
|
| | if (!LAYOUT_LABEL_TO_DATA[newLayout[0]] || !LAYOUT_LABEL_TO_DATA[newLayout[1]]) {
|
| | throw new Error(`New Layout invalid: [${newLayout[0]}, ${newLayout[1]}]`);
|
| | }
|
| | node.properties = node.properties || {};
|
| | node.properties["connections_layout"] = newLayout;
|
| | }
|
| |
|
| |
|
| | export function setConnectionsCollapse(
|
| | node: LGraphNode,
|
| | collapseConnections: boolean | null = null,
|
| | ) {
|
| | node.properties = node.properties || {};
|
| | collapseConnections =
|
| | collapseConnections !== null ? collapseConnections : !node.properties["collapse_connections"];
|
| | node.properties["collapse_connections"] = collapseConnections;
|
| | }
|
| |
|
| | export function getConnectionPosForLayout(
|
| | node: LGraphNode,
|
| | isInput: boolean,
|
| | slotNumber: number,
|
| | out: Vector2,
|
| | ) {
|
| | out = out || new Float32Array(2);
|
| | node.properties = node.properties || {};
|
| | const layout = node.properties["connections_layout"] ||
|
| | (node as any).defaultConnectionsLayout || ["Left", "Right"];
|
| | const collapseConnections = node.properties["collapse_connections"] || false;
|
| | const offset = (node.constructor as any).layout_slot_offset ?? LiteGraph.NODE_SLOT_HEIGHT * 0.5;
|
| | let side = isInput ? layout[0] : layout[1];
|
| | const otherSide = isInput ? layout[1] : layout[0];
|
| | let data = LAYOUT_LABEL_TO_DATA[side]!;
|
| | const slotList = node[isInput ? "inputs" : "outputs"];
|
| | const cxn = slotList[slotNumber];
|
| | if (!cxn) {
|
| | console.log("No connection found.. weird", isInput, slotNumber);
|
| | return out;
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | if (cxn.disabled) {
|
| |
|
| | if (cxn.color_on !== "#666665") {
|
| | (cxn as any)._color_on_org = (cxn as any)._color_on_org || cxn.color_on;
|
| | (cxn as any)._color_off_org = (cxn as any)._color_off_org || cxn.color_off;
|
| | }
|
| | cxn.color_on = "#666665";
|
| | cxn.color_off = "#666665";
|
| | } else if (cxn.color_on === "#666665") {
|
| | cxn.color_on = (cxn as any)._color_on_org || undefined;
|
| | cxn.color_off = (cxn as any)._color_off_org || undefined;
|
| | }
|
| | const displaySlot = collapseConnections
|
| | ? 0
|
| | : slotNumber -
|
| | slotList.reduce<number>((count, ioput, index) => {
|
| | count += index < slotNumber && ioput.hidden ? 1 : 0;
|
| | return count;
|
| | }, 0);
|
| |
|
| | cxn.dir = data[0];
|
| |
|
| |
|
| | if ((node.size[0] == 10 || node.size[1] == 10) && node.properties["connections_dir"]) {
|
| | cxn.dir = node.properties["connections_dir"][isInput ? 0 : 1]!;
|
| | }
|
| |
|
| | if (side === "Left") {
|
| | if (node.flags.collapsed) {
|
| | var w = (node as any)._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
|
| | out[0] = node.pos[0];
|
| | out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
|
| | } else {
|
| |
|
| |
|
| | toggleConnectionLabel(cxn, !isInput || collapseConnections || !!(node as any).hideSlotLabels);
|
| | out[0] = node.pos[0] + offset;
|
| | if ((node.constructor as any)?.type.includes("Reroute")) {
|
| | out[1] = node.pos[1] + node.size[1] * 0.5;
|
| | } else {
|
| | out[1] =
|
| | node.pos[1] +
|
| | (displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
|
| | ((node.constructor as any).slot_start_y || 0);
|
| | }
|
| | }
|
| | } else if (side === "Right") {
|
| | if (node.flags.collapsed) {
|
| | var w = (node as any)._collapsed_width || LiteGraph.NODE_COLLAPSED_WIDTH;
|
| | out[0] = node.pos[0] + w;
|
| | out[1] = node.pos[1] - LiteGraph.NODE_TITLE_HEIGHT * 0.5;
|
| | } else {
|
| |
|
| |
|
| | toggleConnectionLabel(cxn, isInput || collapseConnections || !!(node as any).hideSlotLabels);
|
| | out[0] = node.pos[0] + node.size[0] + 1 - offset;
|
| | if ((node.constructor as any)?.type.includes("Reroute")) {
|
| | out[1] = node.pos[1] + node.size[1] * 0.5;
|
| | } else {
|
| | out[1] =
|
| | node.pos[1] +
|
| | (displaySlot + 0.7) * LiteGraph.NODE_SLOT_HEIGHT +
|
| | ((node.constructor as any).slot_start_y || 0);
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| | } else if (side === "Top") {
|
| | if (!(cxn as any).has_old_label) {
|
| | (cxn as any).has_old_label = true;
|
| | (cxn as any).old_label = cxn.label;
|
| | cxn.label = " ";
|
| | }
|
| | out[0] = node.pos[0] + node.size[0] * 0.5;
|
| | out[1] = node.pos[1] + offset;
|
| | } else if (side === "Bottom") {
|
| | if (!(cxn as any).has_old_label) {
|
| | (cxn as any).has_old_label = true;
|
| | (cxn as any).old_label = cxn.label;
|
| | cxn.label = " ";
|
| | }
|
| | out[0] = node.pos[0] + node.size[0] * 0.5;
|
| | out[1] = node.pos[1] + node.size[1] - offset;
|
| | }
|
| | return out;
|
| | }
|
| |
|
| | function toggleConnectionLabel(cxn: any, hide = true) {
|
| | if (hide) {
|
| | if (!(cxn as any).has_old_label) {
|
| | (cxn as any).has_old_label = true;
|
| | (cxn as any).old_label = cxn.label;
|
| | }
|
| | cxn.label = " ";
|
| | } else if (!hide && (cxn as any).has_old_label) {
|
| | (cxn as any).has_old_label = false;
|
| | cxn.label = (cxn as any).old_label;
|
| | (cxn as any).old_label = undefined;
|
| | }
|
| | return cxn;
|
| | }
|
| |
|
| | export function addHelpMenuItem(node: LGraphNode, content: string, menuOptions: ContextMenuItem[]) {
|
| | addMenuItemOnExtraMenuOptions(
|
| | node,
|
| | {
|
| | name: "🛟 Node Help",
|
| | callback: (node) => {
|
| | if ((node as any).showHelp) {
|
| | (node as any).showHelp();
|
| | } else {
|
| | new RgthreeHelpDialog(node, content).show();
|
| | }
|
| | },
|
| | },
|
| | menuOptions,
|
| | "Properties Panel",
|
| | );
|
| | }
|
| |
|
| | export enum PassThroughFollowing {
|
| | ALL,
|
| | NONE,
|
| | REROUTE_ONLY,
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | export function shouldPassThrough(
|
| | node?: LGraphNode | null,
|
| | passThroughFollowing = PassThroughFollowing.ALL,
|
| | ) {
|
| | const type = (node?.constructor as typeof LGraphNode)?.type;
|
| | if (!type || passThroughFollowing === PassThroughFollowing.NONE) {
|
| | return false;
|
| | }
|
| | if (passThroughFollowing === PassThroughFollowing.REROUTE_ONLY) {
|
| | return type.includes("Reroute");
|
| | }
|
| | return (
|
| | type.includes("Reroute") || type.includes("Node Combiner") || type.includes("Node Collector")
|
| | );
|
| | }
|
| |
|
| |
|
| | function filterOutPassthroughNodes(
|
| | infos: ConnectedNodeInfo[],
|
| | passThroughFollowing = PassThroughFollowing.ALL,
|
| | ) {
|
| | return infos.filter((i) => !shouldPassThrough(i.node, passThroughFollowing));
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | export function getConnectedInputNodes(
|
| | startNode: LGraphNode,
|
| | currentNode?: LGraphNode,
|
| | slot?: number,
|
| | passThroughFollowing = PassThroughFollowing.ALL,
|
| | ): LGraphNode[] {
|
| | return getConnectedNodesInfo(
|
| | startNode,
|
| | IoDirection.INPUT,
|
| | currentNode,
|
| | slot,
|
| | passThroughFollowing,
|
| | ).map((n) => n.node);
|
| | }
|
| | export function getConnectedInputInfosAndFilterPassThroughs(
|
| | startNode: LGraphNode,
|
| | currentNode?: LGraphNode,
|
| | slot?: number,
|
| | passThroughFollowing = PassThroughFollowing.ALL) {
|
| | return filterOutPassthroughNodes(
|
| | getConnectedNodesInfo(startNode, IoDirection.INPUT, currentNode, slot, passThroughFollowing),
|
| | passThroughFollowing);
|
| | }
|
| | export function getConnectedInputNodesAndFilterPassThroughs(
|
| | startNode: LGraphNode,
|
| | currentNode?: LGraphNode,
|
| | slot?: number,
|
| | passThroughFollowing = PassThroughFollowing.ALL,
|
| | ): LGraphNode[] {
|
| | return getConnectedInputInfosAndFilterPassThroughs(startNode, currentNode, slot, passThroughFollowing).map(n => n.node);
|
| | }
|
| |
|
| | export function getConnectedOutputNodes(
|
| | startNode: LGraphNode,
|
| | currentNode?: LGraphNode,
|
| | slot?: number,
|
| | passThroughFollowing = PassThroughFollowing.ALL,
|
| | ): LGraphNode[] {
|
| | return getConnectedNodesInfo(
|
| | startNode,
|
| | IoDirection.OUTPUT,
|
| | currentNode,
|
| | slot,
|
| | passThroughFollowing,
|
| | ).map((n) => n.node);
|
| | }
|
| |
|
| | export function getConnectedOutputNodesAndFilterPassThroughs(
|
| | startNode: LGraphNode,
|
| | currentNode?: LGraphNode,
|
| | slot?: number,
|
| | passThroughFollowing = PassThroughFollowing.ALL,
|
| | ): LGraphNode[] {
|
| | return filterOutPassthroughNodes(
|
| | getConnectedNodesInfo(startNode, IoDirection.OUTPUT, currentNode, slot, passThroughFollowing),
|
| | passThroughFollowing,
|
| | ).map(n => n.node);
|
| | }
|
| |
|
| | export type ConnectedNodeInfo = {
|
| | node: LGraphNode;
|
| | travelFromSlot: number;
|
| | travelToSlot: number;
|
| | originTravelFromSlot: number;
|
| | };
|
| |
|
| | export function getConnectedNodesInfo(
|
| | startNode: LGraphNode,
|
| | dir = IoDirection.INPUT,
|
| | currentNode?: LGraphNode,
|
| | slot?: number,
|
| | passThroughFollowing = PassThroughFollowing.ALL,
|
| | originTravelFromSlot?: number,
|
| | ): ConnectedNodeInfo[] {
|
| | currentNode = currentNode || startNode;
|
| | let rootNodes: ConnectedNodeInfo[] = [];
|
| | if (startNode === currentNode || shouldPassThrough(currentNode, passThroughFollowing)) {
|
| | let linkIds: Array<number | undefined | null>;
|
| |
|
| | slot = slot != null && slot > -1 ? slot : undefined;
|
| | if (dir == IoDirection.OUTPUT) {
|
| | if (slot != null) {
|
| | linkIds = [...(currentNode.outputs?.[slot]?.links || [])];
|
| | } else {
|
| | linkIds = currentNode.outputs?.flatMap((i) => i.links) || [];
|
| | }
|
| | } else {
|
| | if (slot != null) {
|
| | linkIds = [currentNode.inputs?.[slot]?.link];
|
| | } else {
|
| | linkIds = currentNode.inputs?.map((i) => i.link) || [];
|
| | }
|
| | }
|
| | let graph = app.graph as LGraph;
|
| | for (const linkId of linkIds) {
|
| | let link: LLink | null = null;
|
| | if (typeof linkId == "number") {
|
| | link = graph.links[linkId] as LLink;
|
| | }
|
| | if (!link) {
|
| | continue;
|
| | }
|
| | const travelFromSlot = dir == IoDirection.OUTPUT ? link.origin_slot : link.target_slot;
|
| | const connectedId = dir == IoDirection.OUTPUT ? link.target_id : link.origin_id;
|
| | const travelToSlot = dir == IoDirection.OUTPUT ? link.target_slot : link.origin_slot;
|
| | originTravelFromSlot = originTravelFromSlot != null ? originTravelFromSlot : travelFromSlot;
|
| | const originNode: LGraphNode = graph.getNodeById(connectedId)!;
|
| | if (!link) {
|
| | console.error("No connected node found... weird");
|
| | continue;
|
| | }
|
| | if (rootNodes.some((n) => n.node == originNode)) {
|
| | console.log(
|
| | `${startNode.title} (${startNode.id}) seems to have two links to ${originNode.title} (${
|
| | originNode.id
|
| | }). One may be stale: ${linkIds.join(", ")}`,
|
| | );
|
| | } else {
|
| |
|
| | rootNodes.push({ node: originNode, travelFromSlot, travelToSlot, originTravelFromSlot });
|
| | if (shouldPassThrough(originNode, passThroughFollowing)) {
|
| | for (const foundNode of getConnectedNodesInfo(
|
| | startNode,
|
| | dir,
|
| | originNode,
|
| | undefined,
|
| | undefined,
|
| | originTravelFromSlot,
|
| | )) {
|
| | if (!rootNodes.map((n) => n.node).includes(foundNode.node)) {
|
| | rootNodes.push(foundNode);
|
| | }
|
| | }
|
| | }
|
| | }
|
| | }
|
| | }
|
| | return rootNodes;
|
| | }
|
| |
|
| | export type ConnectionType = {
|
| | type: string | string[];
|
| | name: string | undefined;
|
| | label: string | undefined;
|
| | };
|
| |
|
| | |
| | |
| | |
| | |
| |
|
| | export function followConnectionUntilType(
|
| | node: LGraphNode,
|
| | dir: IoDirection,
|
| | slotNum?: number,
|
| | skipSelf = false,
|
| | ): ConnectionType | null {
|
| | const slots = dir === IoDirection.OUTPUT ? node.outputs : node.inputs;
|
| | if (!slots || !slots.length) {
|
| | return null;
|
| | }
|
| | let type: ConnectionType | null = null;
|
| | if (slotNum) {
|
| | if (!slots[slotNum]) {
|
| | return null;
|
| | }
|
| | type = getTypeFromSlot(slots[slotNum], dir, skipSelf);
|
| | } else {
|
| | for (const slot of slots) {
|
| | type = getTypeFromSlot(slot, dir, skipSelf);
|
| | if (type) {
|
| | break;
|
| | }
|
| | }
|
| | }
|
| | return type;
|
| | }
|
| |
|
| | |
| | |
| |
|
| | function getTypeFromSlot(
|
| | slot: INodeInputSlot | INodeOutputSlot | undefined,
|
| | dir: IoDirection,
|
| | skipSelf = false,
|
| | ): ConnectionType | null {
|
| | let graph = app.graph as LGraph;
|
| | let type = slot?.type;
|
| | if (!skipSelf && type != null && type != "*") {
|
| | return { type: type as string, label: slot?.label, name: slot?.name };
|
| | }
|
| | const links = getSlotLinks(slot);
|
| | for (const link of links) {
|
| | const connectedId = dir == IoDirection.OUTPUT ? link.link.target_id : link.link.origin_id;
|
| | const connectedSlotNum =
|
| | dir == IoDirection.OUTPUT ? link.link.target_slot : link.link.origin_slot;
|
| | const connectedNode: LGraphNode = graph.getNodeById(connectedId)!;
|
| |
|
| | const connectedSlots =
|
| | dir === IoDirection.OUTPUT ? connectedNode.inputs : connectedNode.outputs;
|
| | let connectedSlot = connectedSlots[connectedSlotNum];
|
| | if (connectedSlot?.type != null && connectedSlot?.type != "*") {
|
| | return {
|
| | type: connectedSlot.type as string,
|
| | label: connectedSlot?.label,
|
| | name: connectedSlot?.name,
|
| | };
|
| | } else if (connectedSlot?.type == "*") {
|
| | return followConnectionUntilType(connectedNode, dir);
|
| | }
|
| | }
|
| | return null;
|
| | }
|
| |
|
| | export async function replaceNode(
|
| | existingNode: LGraphNode,
|
| | typeOrNewNode: string | LGraphNode,
|
| | inputNameMap?: Map<string, string>,
|
| | ) {
|
| | const existingCtor = existingNode.constructor as typeof LGraphNode;
|
| |
|
| | const newNode =
|
| | typeof typeOrNewNode === "string" ? LiteGraph.createNode(typeOrNewNode) : typeOrNewNode;
|
| |
|
| | if (existingNode.title != existingCtor.title) {
|
| | newNode.title = existingNode.title;
|
| | }
|
| | newNode.pos = [...existingNode.pos];
|
| | newNode.properties = { ...existingNode.properties };
|
| | const oldComputeSize = [...existingNode.computeSize()];
|
| |
|
| |
|
| | const oldSize = [
|
| | existingNode.size[0] === oldComputeSize[0] ? null : existingNode.size[0],
|
| | existingNode.size[1] === oldComputeSize[1] ? null : existingNode.size[1],
|
| | ];
|
| |
|
| | let setSizeIters = 0;
|
| | const setSizeFn = () => {
|
| |
|
| |
|
| | const newComputesize = newNode.computeSize();
|
| | newNode.size[0] = Math.max(oldSize[0] || 0, newComputesize[0]);
|
| | newNode.size[1] = Math.max(oldSize[1] || 0, newComputesize[1]);
|
| | setSizeIters++;
|
| | if (setSizeIters > 10) {
|
| | requestAnimationFrame(setSizeFn);
|
| | }
|
| | };
|
| | setSizeFn();
|
| |
|
| |
|
| |
|
| | const links: {
|
| | node: LGraphNode;
|
| | slot: number | string;
|
| | targetNode: LGraphNode;
|
| | targetSlot: number | string;
|
| | }[] = [];
|
| | for (const [index, output] of existingNode.outputs.entries()) {
|
| | for (const linkId of output.links || []) {
|
| | const link: LLink = (app.graph as LGraph).links[linkId]!;
|
| | if (!link) continue;
|
| | const targetNode = app.graph.getNodeById(link.target_id)!;
|
| | links.push({ node: newNode, slot: output.name, targetNode, targetSlot: link.target_slot });
|
| | }
|
| | }
|
| | for (const [index, input] of existingNode.inputs.entries()) {
|
| | const linkId = input.link;
|
| | if (linkId) {
|
| | const link: LLink = (app.graph as LGraph).links[linkId]!;
|
| | const originNode = app.graph.getNodeById(link.origin_id)!;
|
| | links.push({
|
| | node: originNode,
|
| | slot: link.origin_slot,
|
| | targetNode: newNode,
|
| | targetSlot: inputNameMap?.has(input.name)
|
| | ? inputNameMap.get(input.name)!
|
| | : input.name || index,
|
| | });
|
| | }
|
| | }
|
| |
|
| | app.graph.add(newNode);
|
| | await wait();
|
| |
|
| | for (const link of links) {
|
| | link.node.connect(link.slot, link.targetNode, link.targetSlot);
|
| | }
|
| | await wait();
|
| | app.graph.remove(existingNode);
|
| | newNode.size = newNode.computeSize();
|
| | newNode.setDirtyCanvas(true, true);
|
| | return newNode;
|
| | }
|
| |
|
| | export function getOriginNodeByLink(linkId?: number | null) {
|
| | let node: LGraphNode | null = null;
|
| | if (linkId != null) {
|
| | const link: LLink = app.graph.links[linkId]!;
|
| | node = (link != null && app.graph.getNodeById(link.origin_id)) || null;
|
| | }
|
| | return node;
|
| | }
|
| |
|
| | export function applyMixins(original: Constructor<LGraphNode>, constructors: any[]) {
|
| | constructors.forEach((baseCtor) => {
|
| | Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
|
| | Object.defineProperty(
|
| | original.prototype,
|
| | name,
|
| | Object.getOwnPropertyDescriptor(baseCtor.prototype, name) || Object.create(null),
|
| | );
|
| | });
|
| | });
|
| | }
|
| |
|
| | |
| | |
| | |
| | |
| |
|
| | export function getSlotLinks(inputOrOutput?: INodeInputSlot | INodeOutputSlot | null) {
|
| | const links: { id: number; link: LLink }[] = [];
|
| | if (!inputOrOutput) {
|
| | return links;
|
| | }
|
| | if ((inputOrOutput as INodeOutputSlot).links?.length) {
|
| | const output = inputOrOutput as INodeOutputSlot;
|
| | for (const linkId of output.links || []) {
|
| | const link: LLink = (app.graph as LGraph).links[linkId]!;
|
| | if (link) {
|
| | links.push({ id: linkId, link: link });
|
| | }
|
| | }
|
| | }
|
| | if ((inputOrOutput as INodeInputSlot).link) {
|
| | const input = inputOrOutput as INodeInputSlot;
|
| | const link: LLink = (app.graph as LGraph).links[input.link!]!;
|
| | if (link) {
|
| | links.push({ id: input.link!, link: link });
|
| | }
|
| | }
|
| | return links;
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | export async function matchLocalSlotsToServer(
|
| | node: LGraphNode,
|
| | direction: IoDirection,
|
| | serverNodeData: ComfyObjectInfo,
|
| | ) {
|
| | const serverSlotNames =
|
| | direction == IoDirection.INPUT
|
| | ? Object.keys(serverNodeData.input?.optional || {})
|
| | : serverNodeData.output_name;
|
| | const serverSlotTypes =
|
| | direction == IoDirection.INPUT
|
| | ? (Object.values(serverNodeData.input?.optional || {}).map((i) => i[0]) as string[])
|
| | : serverNodeData.output;
|
| | const slots = direction == IoDirection.INPUT ? node.inputs : node.outputs;
|
| |
|
| |
|
| | let firstIndex = slots.findIndex((o, i) => i !== serverSlotNames.indexOf(o.name));
|
| | if (firstIndex > -1) {
|
| |
|
| | const links: { [key: string]: { id: number; link: LLink }[] } = {};
|
| | slots.map((slot) => {
|
| |
|
| |
|
| | links[slot.name] = links[slot.name] || [];
|
| | links[slot.name]?.push(...getSlotLinks(slot));
|
| | });
|
| |
|
| |
|
| | for (const [index, serverSlotName] of serverSlotNames.entries()) {
|
| | const currentNodeSlot = slots.map((s) => s.name).indexOf(serverSlotName);
|
| | if (currentNodeSlot > -1) {
|
| | if (currentNodeSlot != index) {
|
| | const splicedItem = slots.splice(currentNodeSlot, 1)[0]!;
|
| | slots.splice(index, 0, splicedItem as any);
|
| | }
|
| | } else if (currentNodeSlot === -1) {
|
| | const splicedItem = {
|
| | name: serverSlotName,
|
| | type: serverSlotTypes![index],
|
| | links: [],
|
| | };
|
| | slots.splice(index, 0, splicedItem as any);
|
| | }
|
| | }
|
| |
|
| | if (slots.length > serverSlotNames.length) {
|
| | for (let i = slots.length - 1; i > serverSlotNames.length - 1; i--) {
|
| | if (direction == IoDirection.INPUT) {
|
| | node.disconnectInput(i);
|
| | node.removeInput(i);
|
| | } else {
|
| | node.disconnectOutput(i);
|
| | node.removeOutput(i);
|
| | }
|
| | }
|
| | }
|
| |
|
| |
|
| | for (const [name, slotLinks] of Object.entries(links)) {
|
| | let currentNodeSlot = slots.map((s) => s.name).indexOf(name);
|
| | if (currentNodeSlot > -1) {
|
| | for (const linkData of slotLinks) {
|
| | if (direction == IoDirection.INPUT) {
|
| | linkData.link.target_slot = currentNodeSlot;
|
| | } else {
|
| | linkData.link.origin_slot = currentNodeSlot;
|
| |
|
| | const nextNode = app.graph.getNodeById(linkData.link.target_id);
|
| |
|
| |
|
| | if (
|
| | nextNode &&
|
| | (nextNode.constructor as ComfyNodeConstructor)?.type!.includes("Reroute")
|
| | ) {
|
| | (nextNode as any).stabilize && (nextNode as any).stabilize();
|
| | }
|
| | }
|
| | }
|
| | }
|
| | }
|
| | }
|
| | }
|
| |
|
| | export function isValidConnection(ioA?: INodeSlot | null, ioB?: INodeSlot | null) {
|
| | if (!ioA || !ioB) {
|
| | return false;
|
| | }
|
| | const typeA = String(ioA.type);
|
| | const typeB = String(ioB.type);
|
| |
|
| | let isValid = LiteGraph.isValidConnection(typeA, typeB);
|
| |
|
| |
|
| |
|
| | if (!isValid) {
|
| | let areCombos =
|
| | (typeA.includes(",") && typeB === "COMBO") || (typeA === "COMBO" && typeB.includes(","));
|
| |
|
| | if (areCombos) {
|
| |
|
| | const nameA = ioA.name.toUpperCase().replace("_NAME", "").replace("CKPT", "MODEL");
|
| | const nameB = ioB.name.toUpperCase().replace("_NAME", "").replace("CKPT", "MODEL");
|
| | isValid = nameA.includes(nameB) || nameB.includes(nameA);
|
| | }
|
| | }
|
| | return isValid;
|
| | }
|
| |
|
| | |
| | |
| | |
| |
|
| | const oldIsValidConnection = LiteGraph.isValidConnection;
|
| | LiteGraph.isValidConnection = function (typeA: string | string[], typeB: string | string[]) {
|
| | let isValid = oldIsValidConnection.call(LiteGraph, typeA, typeB);
|
| | if (!isValid) {
|
| | typeA = String(typeA);
|
| | typeB = String(typeB);
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | let areCombos =
|
| | (typeA.includes(",") && typeB === "COMBO") || (typeA === "COMBO" && typeB.includes(","));
|
| | isValid = areCombos;
|
| | }
|
| | return isValid;
|
| | };
|
| |
|
| | |
| | |
| |
|
| | export function getOutputNodes(nodes: LGraphNode[]) {
|
| | return (
|
| | nodes?.filter((n) => {
|
| | return (
|
| | n.mode != LiteGraph.NEVER &&
|
| | ((n.constructor as any).nodeData as ComfyObjectInfo)?.output_node
|
| | );
|
| | }) || []
|
| | );
|
| | } |