Add virtual slots to Reroutes (#970)

### Virtual helper "slots"

Adds a virtual input and output slot to native reroutes, allowing links
to be dragged from them to other reroutes or nodes.


https://github.com/user-attachments/assets/67d308c4-4732-4b04-a2b9-0a2b0c79b413

### Notes

- Reroute slots automatically show an outline as the pointer gets close
- When the slot is clickable, it will highlight in the same colour as
the reroute
- Enables opposite direction connecting: from reroute to node outputs
- Floating reroutes only show one slot - to whichever side is not
connected
This commit is contained in:
filtered
2025-04-27 03:00:01 +10:00
committed by GitHub
parent de0f0ebac1
commit 5c41e4e09c
6 changed files with 399 additions and 73 deletions

View File

@@ -272,6 +272,10 @@ export class LGraphCanvas implements ConnectionColorContext {
cursor = "se-resize"
} else if (this.state.hoveringOver & CanvasItem.Node) {
cursor = "crosshair"
} else if (this.state.hoveringOver & CanvasItem.Reroute) {
cursor = "grab"
} else if (this.state.hoveringOver & CanvasItem.RerouteSlot) {
cursor = "crosshair"
}
this.canvas.style.cursor = cursor
@@ -488,6 +492,8 @@ export class LGraphCanvas implements ConnectionColorContext {
node_capturing_input?: LGraphNode | null
highlighted_links: Dictionary<boolean> = {}
#visibleReroutes: Set<Reroute> = new Set()
dirty_canvas: boolean = true
dirty_bgcanvas: boolean = true
/** A map of nodes that require selective-redraw */
@@ -1907,7 +1913,7 @@ export class LGraphCanvas implements ConnectionColorContext {
this.processSelect(node, e, true)
} else if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) {
// Reroutes
const reroute = graph.getRerouteOnPos(e.canvasX, e.canvasY)
const reroute = graph.getRerouteOnPos(e.canvasX, e.canvasY, this.#visibleReroutes)
if (reroute) {
if (e.altKey) {
pointer.onClick = (upEvent) => {
@@ -1970,7 +1976,7 @@ export class LGraphCanvas implements ConnectionColorContext {
pointer.onClick = (eUp) => {
// Click, not drag
const clickedItem = node ??
graph.getRerouteOnPos(eUp.canvasX, eUp.canvasY) ??
graph.getRerouteOnPos(eUp.canvasX, eUp.canvasY, this.#visibleReroutes) ??
graph.getGroupTitlebarOnPos(eUp.canvasX, eUp.canvasY)
this.processSelect(clickedItem, eUp)
}
@@ -2020,20 +2026,30 @@ export class LGraphCanvas implements ConnectionColorContext {
} else {
// Reroutes
if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) {
const reroute = graph.getRerouteOnPos(x, y)
if (reroute) {
if (e.shiftKey) {
for (const reroute of this.#visibleReroutes) {
const overReroute = reroute.containsPoint([x, y])
if (!reroute.isSlotHovered && !overReroute) continue
if (overReroute) {
pointer.onClick = () => this.processSelect(reroute, e)
if (!e.shiftKey) {
pointer.onDragStart = pointer => this.#startDraggingItems(reroute, pointer, true)
pointer.onDragEnd = e => this.#processDraggedItems(e)
}
}
if (reroute.isOutputHovered || (overReroute && e.shiftKey)) {
linkConnector.dragFromReroute(graph, reroute)
this.#linkConnectorDrop()
this.dirty_bgcanvas = true
}
pointer.onClick = () => this.processSelect(reroute, e)
if (!pointer.onDragEnd) {
pointer.onDragStart = pointer => this.#startDraggingItems(reroute, pointer, true)
pointer.onDragEnd = e => this.#processDraggedItems(e)
if (reroute.isInputHovered) {
linkConnector.dragFromRerouteToOutput(graph, reroute)
this.#linkConnectorDrop()
}
reroute.hideSlots()
this.dirty_bgcanvas = true
return
}
}
@@ -2612,6 +2628,10 @@ export class LGraphCanvas implements ConnectionColorContext {
this.node_over = node
this.dirty_canvas = true
for (const reroute of this.#visibleReroutes) {
reroute.hideSlots()
this.dirty_bgcanvas = true
}
node.onMouseEnter?.(e)
}
@@ -2690,19 +2710,8 @@ export class LGraphCanvas implements ConnectionColorContext {
underPointer |= CanvasItem.ResizeSe
}
} else {
// Reroute
const reroute = graph.getRerouteOnPos(e.canvasX, e.canvasY)
if (reroute) {
underPointer |= CanvasItem.Reroute
linkConnector.overReroute = reroute
if (linkConnector.isConnecting && linkConnector.isRerouteValidDrop(reroute)) {
this._highlight_pos = reroute.pos
}
} else {
this._highlight_pos &&= undefined
linkConnector.overReroute &&= undefined
}
// Reroutes
underPointer = this.#updateReroutes(underPointer)
// Not over a node
const segment = this.#getLinkCentreOnPos(e)
@@ -2760,6 +2769,42 @@ export class LGraphCanvas implements ConnectionColorContext {
return
}
/**
* Updates the hover / snap state of all visible reroutes.
* @returns The original value of {@link underPointer}, with any found reroute items added.
*/
#updateReroutes(underPointer: CanvasItem): CanvasItem {
const { graph, pointer, linkConnector } = this
if (!graph) throw new NullGraphError()
// Update reroute hover state
if (!pointer.isDown) {
let anyChanges = false
for (const reroute of this.#visibleReroutes) {
anyChanges ||= reroute.updateVisibility(this.graph_mouse)
if (reroute.isSlotHovered) underPointer |= CanvasItem.RerouteSlot
}
if (anyChanges) this.dirty_bgcanvas = true
} else if (linkConnector.isConnecting) {
// Highlight the reroute that the mouse is over
for (const reroute of this.#visibleReroutes) {
if (reroute.containsPoint(this.graph_mouse)) {
if (linkConnector.isRerouteValidDrop(reroute)) {
linkConnector.overReroute = reroute
this._highlight_pos = reroute.pos
}
return underPointer |= CanvasItem.RerouteSlot
}
}
}
this._highlight_pos &&= undefined
linkConnector.overReroute &&= undefined
return underPointer
}
/**
* Start dragging an item, optionally including all other selected items.
*
@@ -4645,9 +4690,14 @@ export class LGraphCanvas implements ConnectionColorContext {
this.#renderFloatingLinks(ctx, graph, visibleReroutes, now)
}
const rerouteSet = this.#visibleReroutes
rerouteSet.clear()
// Render reroutes, ordered by number of non-floating links
visibleReroutes.sort((a, b) => a.linkIds.size - b.linkIds.size)
for (const reroute of visibleReroutes) {
rerouteSet.add(reroute)
if (
this.#snapToGrid &&
this.isDragging &&
@@ -4656,6 +4706,9 @@ export class LGraphCanvas implements ConnectionColorContext {
this.drawSnapGuide(ctx, reroute, RenderShape.CIRCLE)
}
reroute.draw(ctx, this._pattern)
// Never draw slots when the pointer is down
if (!this.pointer.isDown) reroute.drawSlots(ctx)
}
ctx.globalAlpha = 1
}
@@ -7143,7 +7196,7 @@ export class LGraphCanvas implements ConnectionColorContext {
// Check for reroutes
if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) {
const reroute = this.graph.getRerouteOnPos(event.canvasX, event.canvasY)
const reroute = this.graph.getRerouteOnPos(event.canvasX, event.canvasY, this.#visibleReroutes)
if (reroute) {
menu_info.unshift({
content: "Delete Reroute",