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
This commit is contained in:
filtered
2025-03-04 01:26:56 +11:00
committed by GitHub
parent 37b9798602
commit cef6ab6ced

View File

@@ -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