/* 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(); } } }