Improve connecting link logic (#697)

### Current

- Connections are disconnected the moment a link starts being dragged
- Reseating a connection where it came from creates a new connection
- If the process is somehow interrupted, the links are already gone

### Proposed

- Connection is disconnected after a new connection is made
- Rendering is bypassed for the link segment being moved
- Does nothing if dropping a link exactly where it came from
- Adds early return when trying to connect a node to itself
This commit is contained in:
filtered
2025-03-04 04:03:31 +11:00
committed by GitHub
parent cef6ab6ced
commit b227eefbdd
4 changed files with 64 additions and 26 deletions

View File

@@ -69,7 +69,7 @@ import {
TitleMode, TitleMode,
} from "./types/globalEnums" } from "./types/globalEnums"
import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange" import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange"
import { findFirstNode, getAllNestedItems } from "./utils/collections" import { findFirstNode, getAllNestedItems, isDraggingLink } from "./utils/collections"
import { toClass } from "./utils/type" import { toClass } from "./utils/type"
import { WIDGET_TYPE_MAP } from "./widgets/widgetMap" import { WIDGET_TYPE_MAP } from "./widgets/widgetMap"
@@ -2253,6 +2253,7 @@ export class LGraphCanvas implements ConnectionColorContext {
output: null, output: null,
pos, pos,
direction: LinkDirection.RIGHT, direction: LinkDirection.RIGHT,
link,
}) })
} }
@@ -2321,17 +2322,20 @@ export class LGraphCanvas implements ConnectionColorContext {
output: linked_node.outputs[slot], output: linked_node.outputs[slot],
pos: linked_node.getConnectionPos(false, slot), pos: linked_node.getConnectionPos(false, slot),
afterRerouteId: link_info.parentId, afterRerouteId: link_info.parentId,
link: link_info,
} }
this.connecting_links = [connecting] const connectingLinks = [connecting]
this.connecting_links = connectingLinks
pointer.onDragStart = () => { pointer.onDragStart = () => {
connecting.output = linked_node.outputs[slot] connecting.output = linked_node.outputs[slot]
} }
pointer.onDragEnd = (upEvent) => { pointer.onDragEnd = (upEvent) => {
if (this.allow_reconnect_links && !LiteGraph.click_do_break_link_to) { const shouldDisconnect = this.#processConnectingLinks(upEvent, connectingLinks)
if (shouldDisconnect && this.allow_reconnect_links && !LiteGraph.click_do_break_link_to) {
node.disconnectInput(i) node.disconnectInput(i)
} }
this.#processConnectingLinks(upEvent)
connecting.output = linked_node.outputs[slot] connecting.output = linked_node.outputs[slot]
this.connecting_links = null this.connecting_links = null
} }
@@ -2712,7 +2716,7 @@ export class LGraphCanvas implements ConnectionColorContext {
// Node background / title under the pointer // Node background / title under the pointer
if (!linkOverWidget) { if (!linkOverWidget) {
const targetSlotId = firstLink.node.findConnectByTypeSlot(true, node, firstLink.output.type) const targetSlotId = firstLink.node.findConnectByTypeSlot(true, node, firstLink.output.type)
if (targetSlotId !== null && targetSlotId >= 0) { if (targetSlotId !== undefined && targetSlotId >= 0) {
node.getConnectionPos(true, targetSlotId, pos) node.getConnectionPos(true, targetSlotId, pos)
highlightPos = pos highlightPos = pos
highlightInput = node.inputs[targetSlotId] highlightInput = node.inputs[targetSlotId]
@@ -2739,7 +2743,7 @@ export class LGraphCanvas implements ConnectionColorContext {
if (inputId === -1 && outputId === -1) { if (inputId === -1 && outputId === -1) {
const targetSlotId = firstLink.node.findConnectByTypeSlot(false, node, firstLink.input.type) const targetSlotId = firstLink.node.findConnectByTypeSlot(false, node, firstLink.input.type)
if (targetSlotId !== null && targetSlotId >= 0) { if (targetSlotId !== undefined && targetSlotId >= 0) {
node.getConnectionPos(false, targetSlotId, pos) node.getConnectionPos(false, targetSlotId, pos)
highlightPos = pos highlightPos = pos
} }
@@ -2912,7 +2916,7 @@ export class LGraphCanvas implements ConnectionColorContext {
if (this.connecting_links?.length) { if (this.connecting_links?.length) {
// node below mouse // node below mouse
this.#processConnectingLinks(e) this.#processConnectingLinks(e, this.connecting_links)
} else { } else {
this.dirty_canvas = true this.dirty_canvas = true
@@ -2944,25 +2948,33 @@ export class LGraphCanvas implements ConnectionColorContext {
return return
} }
#processConnectingLinks(e: CanvasPointerEvent) { #processConnectingLinks(e: CanvasPointerEvent, connecting_links: ConnectingLink[]): boolean | undefined {
const { graph, connecting_links } = this const { graph } = this
if (!graph) throw new NullGraphError() if (!graph) throw new NullGraphError()
if (!connecting_links) return
const { canvasX: x, canvasY: y } = e const { canvasX: x, canvasY: y } = e
const node = graph.getNodeOnPos(x, y, this.visible_nodes) const node = graph.getNodeOnPos(x, y, this.visible_nodes)
const firstLink = connecting_links[0] const firstLink = connecting_links[0]
if (node) { if (node) {
let madeNewLink: boolean | undefined
for (const link of connecting_links) { for (const link of connecting_links) {
// dragging a connection // dragging a connection
this.#dirty() this.#dirty()
// One should avoid linking things to oneself
if (node === link.node) continue
// slot below mouse? connect // slot below mouse? connect
if (link.output) { if (link.output) {
const slot = this.isOverNodeInput(node, x, y) const slot = this.isOverNodeInput(node, x, y)
if (slot != -1) { if (slot != -1) {
link.node.connect(link.slot, node, slot, link.afterRerouteId) // Trying to move link onto itself
if (link.link?.target_id === node.id && link.link?.target_slot === slot) return
const newLink = link.node.connect(link.slot, node, slot, link.afterRerouteId)
madeNewLink ||= newLink !== null
} else if (this.link_over_widget) { } else if (this.link_over_widget) {
this.emitEvent({ this.emitEvent({
subType: "connectingWidgetLink", subType: "connectingWidgetLink",
@@ -2974,28 +2986,33 @@ export class LGraphCanvas implements ConnectionColorContext {
} else { } else {
// not on top of an input // not on top of an input
// look for a good slot // look for a good slot
link.node.connectByType(link.slot, node, link.output.type, { const slotIndex = link.node.findConnectByTypeSlot(true, node, link.output.type)
afterRerouteId: link.afterRerouteId, if (slotIndex !== undefined) {
}) // Trying to move link onto itself
if (link.link?.target_id === node.id && link.link?.target_slot === slotIndex) return
const newLink = link.node.connect(link.slot, node, slotIndex, link.afterRerouteId)
madeNewLink ||= newLink !== null
}
} }
} else if (link.input) { } else if (link.input) {
const slot = this.isOverNodeOutput(node, x, y) const slot = this.isOverNodeOutput(node, x, y)
if (slot != -1) { const newLink = slot != -1
// this is inverted has output-input nature like // this is inverted has output-input nature like
node.connect(slot, link.node, link.slot, link.afterRerouteId) ? node.connect(slot, link.node, link.slot, link.afterRerouteId)
} else {
// not on top of an input // not on top of an input
// look for a good slot // look for a good slot
link.node.connectByTypeOutput( : link.node.connectByTypeOutput(
link.slot, link.slot,
node, node,
link.input.type, link.input.type,
{ afterRerouteId: link.afterRerouteId }, { afterRerouteId: link.afterRerouteId },
) )
} madeNewLink ||= newLink !== null
} }
} }
return madeNewLink
} else if (firstLink.input || firstLink.output) { } else if (firstLink.input || firstLink.output) {
// For external event only. // For external event only.
const linkReleaseContextExtended: LinkReleaseContextExtended = { const linkReleaseContextExtended: LinkReleaseContextExtended = {
@@ -3033,6 +3050,7 @@ export class LGraphCanvas implements ConnectionColorContext {
} }
} }
} }
return true
} }
} }
@@ -4897,6 +4915,8 @@ export class LGraphCanvas implements ConnectionColorContext {
const link = this.graph._links.get(link_id) const link = this.graph._links.get(link_id)
if (!link) continue if (!link) continue
const draggingLink = isDraggingLink(link.id, this.connecting_links)
// find link info // find link info
const start_node = this.graph.getNodeById(link.origin_id) const start_node = this.graph.getNodeById(link.origin_id)
if (start_node == null) continue if (start_node == null) continue
@@ -4960,6 +4980,8 @@ export class LGraphCanvas implements ConnectionColorContext {
const startPos = prevReroute?.pos ?? start_node_slotpos const startPos = prevReroute?.pos ?? start_node_slotpos
reroute.calculateAngle(this.last_draw_time, this.graph, startPos) reroute.calculateAngle(this.last_draw_time, this.graph, startPos)
// Skip the first segment if it is being dragged
if (j === 0 && draggingLink?.input) continue
this.renderLink( this.renderLink(
ctx, ctx,
startPos, startPos,
@@ -4984,6 +5006,9 @@ export class LGraphCanvas implements ConnectionColorContext {
startControl = [dist * reroute.cos, dist * reroute.sin] startControl = [dist * reroute.cos, dist * reroute.sin]
} }
// Skip the last segment if it is being dragged
if (draggingLink?.output) continue
// Use runtime fallback; TypeScript cannot evaluate this correctly. // Use runtime fallback; TypeScript cannot evaluate this correctly.
const segmentStartPos = points.at(-2) ?? start_node_slotpos const segmentStartPos = points.at(-2) ?? start_node_slotpos
@@ -5000,7 +5025,8 @@ export class LGraphCanvas implements ConnectionColorContext {
end_dir, end_dir,
{ startControl }, { startControl },
) )
} else { // Skip normal render when link is being dragged
} else if (!draggingLink) {
this.renderLink( this.renderLink(
ctx, ctx,
start_node_slotpos, start_node_slotpos,

View File

@@ -2170,7 +2170,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
node: LGraphNode, node: LGraphNode,
slotType: ISlotType, slotType: ISlotType,
options?: ConnectByTypeOptions, options?: ConnectByTypeOptions,
): number | null { ): number | undefined {
// LEGACY: Old options names // LEGACY: Old options names
if (options && typeof options === "object") { if (options && typeof options === "object") {
if ("firstFreeIfInputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfInputGeneralInCase if ("firstFreeIfInputGeneralInCase" in options) options.wildcardToTyped = !!options.firstFreeIfInputGeneralInCase
@@ -2188,7 +2188,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
if (node && typeof node === "number") { if (node && typeof node === "number") {
const nodeById = this.graph.getNodeById(node) const nodeById = this.graph.getNodeById(node)
if (!nodeById) return null if (!nodeById) return
node = nodeById node = nodeById
} }
@@ -2217,7 +2217,6 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
: node.findOutputSlotFree(opt) : node.findOutputSlotFree(opt)
if (nonEventSlot >= 0) return nonEventSlot if (nonEventSlot >= 0) return nonEventSlot
} }
return null
} }
/** /**
@@ -2239,7 +2238,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
target_slotType, target_slotType,
optsIn, optsIn,
) )
if (slotIndex !== null) if (slotIndex !== undefined)
return this.connect(slot, target_node, slotIndex, optsIn?.afterRerouteId) return this.connect(slot, target_node, slotIndex, optsIn?.afterRerouteId)
console.debug("[connectByType]: no way to connect type:", target_slotType, "to node:", target_node) console.debug("[connectByType]: no way to connect type:", target_slotType, "to node:", target_node)
@@ -2270,7 +2269,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
source_slotType, source_slotType,
optsIn, optsIn,
) )
if (slotIndex !== null) if (slotIndex !== undefined)
return source_node.connect(slotIndex, this, slot, optsIn?.afterRerouteId) return source_node.connect(slotIndex, this, slot, optsIn?.afterRerouteId)
console.debug("[connectByType]: no way to connect type:", source_slotType, "to node:", source_node) console.debug("[connectByType]: no way to connect type:", source_slotType, "to node:", source_node)

View File

@@ -290,6 +290,8 @@ export interface ConnectingLink extends IInputOrOutput {
pos: Point pos: Point
direction?: LinkDirection direction?: LinkDirection
afterRerouteId?: RerouteId afterRerouteId?: RerouteId
/** The link being moved, or `undefined` if creating a new link. */
link?: LLink
} }
interface IContextMenuBase { interface IContextMenuBase {

View File

@@ -1,4 +1,5 @@
import type { Positionable } from "../interfaces" import type { ConnectingLink, Positionable } from "../interfaces"
import type { LinkId } from "@/LLink"
import { LGraphNode } from "@/LGraphNode" import { LGraphNode } from "@/LGraphNode"
@@ -35,3 +36,13 @@ export function findFirstNode(items: Iterable<Positionable>): LGraphNode | undef
if (item instanceof LGraphNode) return item if (item instanceof LGraphNode) return item
} }
} }
/** @returns `true` if the provided link ID is currently being dragged. */
export function isDraggingLink(linkId: LinkId, connectingLinks: ConnectingLink[] | null | undefined): ConnectingLink | undefined {
if (connectingLinks == null) return
for (const connectingLink of connectingLinks) {
if (connectingLink.link == null) continue
if (linkId === connectingLink.link.id) return connectingLink
}
}