Files
2025-07-05 19:14:15 +10:00

464 lines
12 KiB
JavaScript

/* eslint no-tabs: 0 */
/* eslint import/prefer-default-export:0 */
/* eslint no-underscore-dangle:0 */
/* eslint no-plusplus:0 */
/* eslint prefer-rest-params:0 */
/* eslint operator-linebreak:0 */
/* eslint no-unneeded-ternary:0 */
/* global LGraphCanvas */
/* global LiteGraph */
export class QuickConnection {
constructor() {
this.insideConnection = null;
this.enabled = false;
// use inputs that already have a link to them.
this.useInputsWithLinks = false;
this.release_link_on_empty_shows_menu = true;
this.connectDotOnly = true;
this.doNotAcceptType = /^\*$/;
this.boxAlpha = 0.7;
this.boxBackground = '#000';
}
init() {
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
const t = this;
this.acceptingNodes = null;
LGraphCanvas.prototype.processMouseDown = function mouseDown() {
t.pointerDown();
const ret = origProcessMouseDown.apply(this, arguments);
return ret;
};
const origProcessMouseUp = LGraphCanvas.prototype.processMouseUp;
LGraphCanvas.prototype.processMouseUp = function mouseUp() {
if (!t.enabled) {
return origProcessMouseUp.apply(this, arguments);
}
// Let's not popup the release on empty spot menu if we've released the mouse on a dot
const origReleaseLink = LiteGraph.release_link_on_empty_shows_menu;
const origShowConnectionMenu = t.canvas.showConnectionMenu;
let ret = null;
try {
if (t.pointerUp()) {
if (!t.isComfyUI) {
LiteGraph.release_link_on_empty_shows_menu = false;
} else {
t.canvas.showConnectionMenu = () => {};
}
t.release_link_on_empty_shows_menu = false;
}
ret = origProcessMouseUp.apply(this, arguments);
} finally {
if (!t.release_link_on_empty_shows_menu) {
if (!t.isComfyUI) {
LiteGraph.release_link_on_empty_shows_menu = origReleaseLink;
} else {
t.canvas.showConnectionMenu = origShowConnectionMenu;
t.canvas.linkConnector.reset();
}
t.release_link_on_empty_shows_menu = true;
}
}
return ret;
};
// ComfyUI has it's own version of litegraph.js
// https://github.com/Comfy-Org/litegraph.js
}
initListeners(canvas) {
this.enabled = true;
this.graph = canvas.graph;
this.canvas = canvas;
if (!this.canvas.canvas) {
console.error('no canvas', this.canvas); // eslint-disable-line no-console
} else {
this.canvas.canvas.addEventListener('litegraph:canvas', (e) => {
const { detail } = e;
if (!this.release_link_on_empty_shows_menu
&& detail && detail.subType === 'empty-release'
) {
e.stopPropagation();
}
});
}
this.isComfyUI = this.canvas.connecting_links !== undefined ? true : false;
this.addOnCanvas('onDrawOverlay', (ctx) => this.onDrawOverlay(ctx));
}
getCurrentConnection() {
if (this.isComfyUI) {
const connectingLink =
(this.canvas.connecting_links
&& this.canvas.connecting_links.length > 0
) ?
this.canvas.connecting_links[0] : null;
if (connectingLink) {
return {
node: connectingLink.node,
slot: connectingLink.slot,
input: connectingLink.input,
output: connectingLink.output,
};
}
} else if (this.canvas.connecting_node) {
return {
node: this.canvas.connecting_node,
input: this.canvas.connecting_input,
slot: this.canvas.connecting_slot,
output: this.canvas.connecting_output,
};
}
return null;
}
pointerDown() {
this.acceptingNodes = null;
return false;
}
pointerUp() {
this.acceptingNodes = null;
const connectionInfo = this.getCurrentConnection();
if (this.insideConnection && connectionInfo) {
if (connectionInfo.input) {
this.insideConnection.node.connect(
this.insideConnection.connection_slot_index,
connectionInfo.node,
connectionInfo.slot,
);
} else {
connectionInfo.node.connect(
connectionInfo.slot,
this.insideConnection.node,
this.insideConnection.connection_slot_index,
);
}
return true;
}
return false;
}
findAcceptingNodes(fromConnection, fromNode, findInput) {
const accepting = [];
if (this.doNotAcceptType.exec(fromConnection.type)) {
// Too many connections are available if we area a * connection
return accepting;
}
const addToAccepting = (arr, node) => {
// eslint-disable-next-line eqeqeq
if (node.mode == 4) {
// bypassed
return;
}
if (node.id === fromNode.id) {
// Don't connect to myself
return;
}
for (let c = 0; c < arr.length; ++c) {
const input = arr[c];
if (!input.link || this.useInputsWithLinks) {
const accept = LiteGraph.isValidConnection(
input.type,
fromConnection.type,
);
if (accept && !this.doNotAcceptType.exec(input.type)) {
accepting.push({
node,
connection: input,
connection_slot_index: c,
});
}
}
}
};
const nodes = this.graph._nodes;
for (let i = 0; i < nodes.length; ++i) {
const node = nodes[i];
if (node.inputs && findInput) {
addToAccepting(node.inputs, node);
}
if (node.outputs && !findInput) {
addToAccepting(node.outputs, node);
}
}
accepting.sort((a, b) => a.node.pos[1] - b.node.pos[1]);
return accepting;
}
addOnCanvas(name, func) {
const obj = this.canvas;
const oldFunc = obj[name];
obj[name] = function callFunc() {
if (oldFunc) {
oldFunc.apply(obj, arguments);
}
return func.apply(obj, arguments);
};
}
onDrawOverlay(ctx) {
if (!this.enabled) {
return;
}
if (!this.canvas || !this.canvas.graph_mouse) {
console.error('no canvas or mouse yet', this.canvas); // eslint-disable-line no-console
return;
}
this.insideConnection = null;
const connectionInfo = this.getCurrentConnection();
if (connectionInfo) {
const {
node, input, output, slot,
} = connectionInfo;
if (!input && !output) {
return;
}
ctx.save();
this.canvas.ds.toCanvasContext(ctx);
const slotPos = new Float32Array(2);
const isInput = input ? true : false;
const connecting = isInput ? input : output;
const connectionSlot = slot;
const pos = node.getConnectionPos(isInput, connectionSlot, slotPos);
if (!this.acceptingNodes) {
this.acceptingNodes = this.findAcceptingNodes(
connecting,
// this.canvas.connecting_node,
node,
!isInput,
);
}
const mouseX = this.canvas.graph_mouse[0];
const mouseY = this.canvas.graph_mouse[1];
// const hasNodeTooltip = document.querySelector('.node-tooltip');
const buttonShift = [
isInput ? -32 : +32,
// force for now so the dots don't move around when the tooltip pops up.
// No need to avoid tool tip if we're using the input,
// tool tip is visible to the right of dot
isInput ? 0 : LiteGraph.NODE_SLOT_HEIGHT,
/*
(true || this.acceptingNodes.length === 1 || hasNodeTooltip)
? 0
: (
((-this.acceptingNodes.length * LiteGraph.NODE_SLOT_HEIGHT) / 2)
+ (LiteGraph.NODE_SLOT_HEIGHT / 2)
),
*/
];
const linkPos = [
pos[0] + buttonShift[0],
pos[1] + buttonShift[1],
];
let scale = 1 / this.canvas.ds.scale;
if (scale < 1.0) {
scale = 1.0;
}
const linkCloseArea = [
linkPos[0] - (LiteGraph.NODE_SLOT_HEIGHT * 6 * scale),
linkPos[1] - LiteGraph.NODE_SLOT_HEIGHT,
LiteGraph.NODE_SLOT_HEIGHT * 8 * scale,
LiteGraph.NODE_SLOT_HEIGHT * (this.acceptingNodes.length + 1) * scale,
];
if (!isInput) {
linkCloseArea[0] = linkPos[0] - ((LiteGraph.NODE_SLOT_HEIGHT * 2) * scale);
}
const isInsideClosePosition = LiteGraph.isInsideRectangle(
mouseX,
mouseY,
linkCloseArea[0],
linkCloseArea[1],
linkCloseArea[2],
linkCloseArea[3],
);
let boxRect = null;
const textsToDraw = [];
// const oldFillStyle = ctx.fillStyle;
if (isInsideClosePosition) {
const oldFont = ctx.font;
let font = oldFont;
const fontM = /([0-9]+)px/.exec(font);
if (!fontM) {
fontM[1] = 'px';
font += ' 12px';
}
if (fontM) {
const fontSize = parseInt(fontM[1], 10) * scale;
ctx.font = font.replace(/[0-9]+px/, `${fontSize}px`);
}
this.acceptingNodes.filter((acceptingNode) => {
const textxy = [
linkPos[0] + (isInput ? -LiteGraph.NODE_SLOT_HEIGHT : LiteGraph.NODE_SLOT_HEIGHT),
linkPos[1],
];
const acceptingText = `${acceptingNode.connection.name} @${acceptingNode.node.title}`;
const textBox = ctx.measureText(acceptingText);
const box = [
textxy[0],
textxy[1] - textBox.fontBoundingBoxAscent,
textBox.width,
LiteGraph.NODE_SLOT_HEIGHT,
// (textBox.fontBoundingBoxAscent + textBox.fontBoundingBoxDescent),
];
let textAlign;
if (!isInput) {
textAlign = 'left';
} else {
box[0] -= textBox.width;
textAlign = 'right';
}
const rRect = [
box[0] - 8 * scale,
box[1] - 4 * scale,
box[2] + 16 * scale,
box[3], // + 5 * scale,
];
if (!boxRect) {
boxRect = rRect.slice(0);
} else {
if (boxRect[0] > rRect[0]) {
// eslint-disable-next-line prefer-destructuring
boxRect[0] = rRect[0];
}
if (boxRect[2] < rRect[2]) {
// eslint-disable-next-line prefer-destructuring
boxRect[2] = rRect[2];
}
boxRect[3] += rRect[3];
}
textsToDraw.push({
x: textxy[0],
y: textxy[1],
acceptingText,
textAlign,
});
let isInsideRect;
if (this.connectDotOnly) {
isInsideRect = LiteGraph.isInsideRectangle(
mouseX,
mouseY,
linkPos[0] - ((LiteGraph.NODE_SLOT_HEIGHT / 2) * scale),
linkPos[1] - ((LiteGraph.NODE_SLOT_HEIGHT / 2) * scale),
LiteGraph.NODE_SLOT_HEIGHT * scale,
LiteGraph.NODE_SLOT_HEIGHT * scale,
);
} else {
isInsideRect = LiteGraph.isInsideRectangle(
mouseX,
mouseY,
isInput ? box[0] : (linkPos[0] - (LiteGraph.NODE_SLOT_HEIGHT / 2)),
linkPos[1] - 10,
isInput ?
((linkPos[0] - box[0]) + LiteGraph.NODE_SLOT_HEIGHT / 2)
: (rRect[2] + LiteGraph.NODE_SLOT_HEIGHT / 2),
rRect[3],
);
}
if (isInsideRect && !this.insideConnection) {
this.insideConnection = acceptingNode;
ctx.fillStyle = LiteGraph.EVENT_LINK_COLOR; // "#ffcc00";
// highlight destination if mouseover
ctx.beginPath();
ctx.arc(
linkPos[0],
linkPos[1],
6 * scale,
0,
Math.PI * 2,
);
ctx.fill();
ctx.closePath();
ctx.beginPath();
ctx.strokeStyle = '#6a6';
ctx.setLineDash([5, 10]);
ctx.lineWidth = 3;
const aNode = acceptingNode.node;
const destPos = new Float32Array(2);
aNode.getConnectionPos(!isInput, acceptingNode.connection_slot_index, destPos);
ctx.moveTo(pos[0], pos[1]);
ctx.lineTo(destPos[0], destPos[1]);
ctx.stroke();
ctx.closePath();
} else {
const slotColor =
this.canvas.default_connection_color_byType[acceptingNode.connection.type]
|| this.canvas.default_connection_color.input_on;
ctx.fillStyle = slotColor || this.canvas.default_connection_color.input_on;
ctx.beginPath();
ctx.arc(linkPos[0], linkPos[1], 4 * scale, 0, Math.PI * 2);
ctx.fill();
ctx.closePath();
}
linkPos[1] += LiteGraph.NODE_SLOT_HEIGHT * scale;
return false;
});
if (boxRect) {
ctx.beginPath();
ctx.fillStyle = this.boxBackground;
const oldAlpha = ctx.globalAlpha;
ctx.globalAlpha = this.boxAlpha;
ctx.roundRect(
boxRect[0],
boxRect[1],
boxRect[2],
boxRect[3],
5,
);
ctx.fill();
ctx.closePath();
ctx.globalAlpha = oldAlpha;
}
ctx.fillStyle = LiteGraph.WIDGET_TEXT_COLOR;
textsToDraw.filter((textToDraw) => {
ctx.textAlign = textToDraw.textAlign;
ctx.fillText(textToDraw.acceptingText, textToDraw.x, textToDraw.y);
return true;
});
ctx.font = oldFont;
}
ctx.restore();
}
}
}