From cef6ab6ced7be7f6cdd6b467c5317fc0394fbd3f Mon Sep 17 00:00:00 2001 From: filtered <176114999+webfiltered@users.noreply.github.com> Date: Tue, 4 Mar 2025 01:26:56 +1100 Subject: [PATCH] Improve link drag & drop (#380) - Resolves https://github.com/Comfy-Org/litegraph.js/issues/309#issuecomment-2508726168 - Output issue still pending - Splits connecting links `pointerup` handler to separate function, which can now be called from `CanvasPointer` callbacks - Minor refactor; no functional changes ### Behaviour change When moving existing links from an input slot, the link will not be disconnected until the drop event occurs. ### Current Shift + drag https://github.com/user-attachments/assets/0b98f9bf-3d5f-467e-9a9b-e5695e5a0d0b ### Proposed Shift + drag https://github.com/user-attachments/assets/0bc36215-0247-41da-8050-e8df09addf23 --- src/LGraphCanvas.ts | 201 ++++++++++++++++++++++++-------------------- 1 file changed, 109 insertions(+), 92 deletions(-) diff --git a/src/LGraphCanvas.ts b/src/LGraphCanvas.ts index b22121586..0471449e8 100644 --- a/src/LGraphCanvas.ts +++ b/src/LGraphCanvas.ts @@ -2242,16 +2242,16 @@ export class LGraphCanvas implements ConnectionColorContext { if (!link) continue const slot = link.target_slot - const linked_node = graph._nodes_by_id[link.target_id] - const input = linked_node.inputs[slot] - const pos = linked_node.getConnectionPos(true, slot) + const otherNode = graph._nodes_by_id[link.target_id] + const input = otherNode.inputs[slot] + const pos = otherNode.getConnectionPos(true, slot) this.connecting_links.push({ - node: linked_node, - slot: slot, - input: input, + node: otherNode, + slot, + input, output: null, - pos: pos, + pos, direction: LinkDirection.RIGHT, }) } @@ -2320,14 +2320,21 @@ export class LGraphCanvas implements ConnectionColorContext { slot, output: linked_node.outputs[slot], pos: linked_node.getConnectionPos(false, slot), + afterRerouteId: link_info.parentId, } this.connecting_links = [connecting] pointer.onDragStart = () => { - if (this.allow_reconnect_links && !LiteGraph.click_do_break_link_to) - node.disconnectInput(i) connecting.output = linked_node.outputs[slot] } + pointer.onDragEnd = (upEvent) => { + if (this.allow_reconnect_links && !LiteGraph.click_do_break_link_to) { + node.disconnectInput(i) + } + this.#processConnectingLinks(upEvent) + connecting.output = linked_node.outputs[slot] + this.connecting_links = null + } this.dirty_bgcanvas = true } @@ -2902,92 +2909,10 @@ export class LGraphCanvas implements ConnectionColorContext { const x = e.canvasX const y = e.canvasY - const node = graph.getNodeOnPos(x, y, this.visible_nodes) if (this.connecting_links?.length) { // node below mouse - const firstLink = this.connecting_links[0] - if (node) { - for (const link of this.connecting_links) { - // dragging a connection - this.#dirty() - - // slot below mouse? connect - if (link.output) { - const slot = this.isOverNodeInput(node, x, y) - if (slot != -1) { - link.node.connect(link.slot, node, slot, link.afterRerouteId) - } else if (this.link_over_widget) { - this.emitEvent({ - subType: "connectingWidgetLink", - link, - node, - widget: this.link_over_widget, - }) - this.link_over_widget = null - } else { - // not on top of an input - // look for a good slot - link.node.connectByType(link.slot, node, link.output.type, { - afterRerouteId: link.afterRerouteId, - }) - } - } else if (link.input) { - const slot = this.isOverNodeOutput(node, x, y) - - if (slot != -1) { - // this is inverted has output-input nature like - node.connect(slot, link.node, link.slot, link.afterRerouteId) - } else { - // not on top of an input - // look for a good slot - link.node.connectByTypeOutput( - link.slot, - node, - link.input.type, - { afterRerouteId: link.afterRerouteId }, - ) - } - } - } - } else if (firstLink.input || firstLink.output) { - // For external event only. - const linkReleaseContextExtended: LinkReleaseContextExtended = { - links: this.connecting_links, - } - this.emitEvent({ - subType: "empty-release", - originalEvent: e, - linkReleaseContext: linkReleaseContextExtended, - }) - // No longer in use - // add menu when releasing link in empty space - if (LiteGraph.release_link_on_empty_shows_menu) { - const linkReleaseContext = firstLink.output - ? { - node_from: firstLink.node, - slot_from: firstLink.output, - type_filter_in: firstLink.output.type, - } - : { - node_to: firstLink.node, - slot_from: firstLink.input, - type_filter_out: firstLink.input?.type, - } - - if (e.shiftKey) { - if (this.allow_searchbox) { - this.showSearchBox(e, linkReleaseContext) - } - } else { - if (firstLink.output) { - this.showConnectionMenu({ nodeFrom: firstLink.node, slotFrom: firstLink.output, e: e }) - } else if (firstLink.input) { - this.showConnectionMenu({ nodeTo: firstLink.node, slotTo: firstLink.input, e: e }) - } - } - } - } + this.#processConnectingLinks(e) } else { this.dirty_canvas = true @@ -3019,6 +2944,98 @@ export class LGraphCanvas implements ConnectionColorContext { return } + #processConnectingLinks(e: CanvasPointerEvent) { + const { graph, connecting_links } = this + if (!graph) throw new NullGraphError() + if (!connecting_links) return + + const { canvasX: x, canvasY: y } = e + const node = graph.getNodeOnPos(x, y, this.visible_nodes) + const firstLink = connecting_links[0] + + if (node) { + for (const link of connecting_links) { + // dragging a connection + this.#dirty() + + // slot below mouse? connect + if (link.output) { + const slot = this.isOverNodeInput(node, x, y) + if (slot != -1) { + link.node.connect(link.slot, node, slot, link.afterRerouteId) + } else if (this.link_over_widget) { + this.emitEvent({ + subType: "connectingWidgetLink", + link, + node, + widget: this.link_over_widget, + }) + this.link_over_widget = null + } else { + // not on top of an input + // look for a good slot + link.node.connectByType(link.slot, node, link.output.type, { + afterRerouteId: link.afterRerouteId, + }) + } + } else if (link.input) { + const slot = this.isOverNodeOutput(node, x, y) + + if (slot != -1) { + // this is inverted has output-input nature like + node.connect(slot, link.node, link.slot, link.afterRerouteId) + } else { + // not on top of an input + // look for a good slot + link.node.connectByTypeOutput( + link.slot, + node, + link.input.type, + { afterRerouteId: link.afterRerouteId }, + ) + } + } + } + } else if (firstLink.input || firstLink.output) { + // For external event only. + const linkReleaseContextExtended: LinkReleaseContextExtended = { + links: connecting_links, + } + this.emitEvent({ + subType: "empty-release", + originalEvent: e, + linkReleaseContext: linkReleaseContextExtended, + }) + // No longer in use + // add menu when releasing link in empty space + if (LiteGraph.release_link_on_empty_shows_menu) { + const linkReleaseContext = firstLink.output + ? { + node_from: firstLink.node, + slot_from: firstLink.output, + type_filter_in: firstLink.output.type, + } + : { + node_to: firstLink.node, + slot_from: firstLink.input, + type_filter_out: firstLink.input?.type, + } + + if (e.shiftKey) { + if (this.allow_searchbox) { + this.showSearchBox(e, linkReleaseContext) + } + } else { + if (firstLink.output) { + this.showConnectionMenu({ nodeFrom: firstLink.node, slotFrom: firstLink.output, e: e }) + } else if (firstLink.input) { + this.showConnectionMenu({ nodeTo: firstLink.node, slotTo: firstLink.input, e: e }) + } + } + } + } + } + /** * Called when the mouse moves off the canvas. Clears all node hover states. * @param e