596 lines
16 KiB
JavaScript
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();
|
|
}
|
|
}
|
|
}
|