Files
2026-04-04 18:57:57 +11:00

596 lines
16 KiB
JavaScript

/* eslint no-tabs: 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.maxSuggestions = 15;
this.doNotAcceptType = /^\*$/;
this.boxAlpha = 0.7;
this.boxBackground = '#000';
this.graph_mouse = [0, 0];
}
mouseUp(origThis, origFunc, args) {
const t = this;
if (!t.enabled || !t.canvas) {
return origFunc.apply(origThis, args);
}
// 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 = origFunc.apply(origThis, args);
} 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;
}
init() {
const t = this;
const origProcessMouseDown = LGraphCanvas.prototype.processMouseDown;
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 newProcessMouseUp() {
return t.mouseUp(this, origProcessMouseUp, arguments);
};
// ComfyUI has it's own version of litegraph.js
// https://github.com/Comfy-Org/litegraph.js
}
setGraphMouseFromClientXY(clientX, clientY) {
if (!this.canvas) {
return;
}
const { offset, scale } = this.canvas.ds;
const boundingRect = this.canvas.canvas.getBoundingClientRect();
this.graph_mouse = [
(clientX - boundingRect.x) / scale - offset[0],
(clientY - boundingRect.y) / scale - offset[1],
];
}
initListeners(canvas) {
const t = this;
this.enabled = true;
this.graph = canvas.graph;
this.canvas = canvas;
if (!this.canvas.canvas) {
console.error('no canvas', this.canvas);
} 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) => {
try {
this.onDrawOverlay(ctx);
} catch (e) {
console.error('onDrawOverlayCrash', e, ctx);
}
});
// src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts:onPointerDown
// uses useEventListener(..., { capture: true } )
// Which stops the mousemove / pointermove event from working.
//
let origEventsReplacementId = 1;
const origEventsReplacement = {};
if (LiteGraph.vueNodesMode) {
const oldAddEventListener = window.addEventListener;
const uncaptureEvents = {
pointermove: true,
pointerup: true,
};
window.addEventListener = function newAddEventListener(name, func, opts) {
if (uncaptureEvents[name] && opts?.capture) {
const newArgs = Array.from(arguments);
try {
const newOpts = { ...opts };
newArgs[2] = newOpts;
const origEventFunc = newArgs[1];
let newFunc = null;
if (name === 'pointerup') {
newFunc = function newPointerUp() {
return t.mouseUp(this, origEventFunc, arguments);
};
} else if (name === 'pointermove') {
newFunc = function newPointerMove(e) {
t.setGraphMouseFromClientXY(e.clientX, e.clientY);
return origEventFunc.apply(this, arguments);
};
}
if (newFunc) {
newArgs[1] = newFunc;
origEventFunc.__id = ++origEventsReplacementId;
origEventsReplacement[origEventFunc.__id] = newFunc;
}
} catch (e) {
console.error('QuickConnections.newAddEventListener crash');
console.error(e);
}
return oldAddEventListener.apply(window, newArgs);
}
return oldAddEventListener.apply(window, arguments);
};
const oldRemoveEventListener = window.removeEventListener;
window.removeEventListener = function newRemoveEventListener() {
const newArgs = [...arguments];
try {
const funcId = arguments[1].__id;
if (funcId) {
const newEventFunc = origEventsReplacement[funcId];
if (!newEventFunc) {
console.warn('Could not find replaced event to remove, id:', funcId, arguments);
} else {
newArgs[1] = newEventFunc;
}
}
} catch (e) {
console.error('QuickConnections.newAddEventListener crash');
console.error(e);
}
return oldRemoveEventListener.apply(window, newArgs);
};
}
this.canvas.canvas.addEventListener('pointermove', (e) => {
this.graph_mouse[0] = e.canvasX;
this.graph_mouse[1] = e.canvasY;
});
}
// Get the current connection that we are dragging
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,
pos: connectingLink.pos,
};
}
} 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) {
if (connectionInfo.node.connect) {
this.insideConnection.node.connect(
this.insideConnection.connection_slot_index,
connectionInfo.node,
connectionInfo.slot,
);
} else {
// subgraph
connectionInfo.input.connect(
this.insideConnection.node.outputs[this.insideConnection.connection_slot_index],
this.insideConnection.node,
);
}
} else {
// output
if (connectionInfo.node.connect) {
connectionInfo.node.connect(
connectionInfo.slot,
this.insideConnection.node,
this.insideConnection.connection_slot_index,
);
} else {
// subgraph
connectionInfo.output.connect(
this.insideConnection.node.inputs[this.insideConnection.connection_slot_index],
this.insideConnection.node,
);
}
}
return true;
}
return false;
}
// Find the list of nodes we can drag to
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) => {
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.canvas.subgraph?._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]);
if (this.maxSuggestions) {
return accepting.slice(0, this.maxSuggestions);
}
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) {
console.error('no canvas or mouse yet', this.canvas);
return;
}
this.insideConnection = null;
const connectionInfo = this.getCurrentConnection();
if (connectionInfo) {
const {
node, input, output, slot,
} = connectionInfo;
if (!input && !output) {
return;
}
// const slotPos = new Float32Array(2);
const isInput = input ? true : false;
const connecting = isInput ? input : output;
const connectionSlot = slot;
let pos;
if (!node.getOutputPos || !node.getInputPos) {
pos = connectionInfo.pos;
} else if (LiteGraph.vueNodesMode && node.getSlotPosition) {
pos = node.getSlotPosition(
connectionSlot,
isInput,
);
} else {
pos = isInput ?
node.getInputPos(connectionSlot)
: node.getOutputPos(connectionSlot);
}
if (!this.acceptingNodes) {
this.acceptingNodes = this.findAcceptingNodes(
connecting,
// this.canvas.connecting_node,
node,
!isInput,
);
}
const mouseX = this.graph_mouse[0];
const mouseY = this.graph_mouse[1];
// const hasNodeTooltip = document.querySelector('.node-tooltip');
const buttonShift = [
isInput ? -32 : +32,
// 2025-08-29: tooltip seems to be far away now, don't change position
0,
/*
(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 = [];
ctx.save();
this.canvas.ds.toCanvasContext(ctx);
// const oldFillStyle = ctx.fillStyle;
if (isInsideClosePosition) {
// We are inside the spot next to the node
// where we can release the mouse and connect to something
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]) {
boxRect[0] = rRect[0];
}
if (boxRect[2] < rRect[2]) {
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);
if (!aNode?.getOutputPos || !aNode.getInputPos) {
console.warn('Node has no getInputPos/getOutputPos', aNode);
} else {
const destPos = isInput ?
aNode.getOutputPos(acceptingNode.connection_slot_index)
: aNode.getInputPos(acceptingNode.connection_slot_index);
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();
}
}
}