mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-02 06:19:58 +00:00
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:
@@ -24,7 +24,6 @@ import { LGraphNode, type NodeId } from "./LGraphNode"
|
||||
import { LiteGraph } from "./litegraph"
|
||||
import { type LinkId, LLink } from "./LLink"
|
||||
import { MapProxyHandler } from "./MapProxyHandler"
|
||||
import { isSortaInsideOctagon } from "./measure"
|
||||
import { Reroute, RerouteId } from "./Reroute"
|
||||
import { stringOrEmpty } from "./strings"
|
||||
import { LGraphEventMode } from "./types/globalEnums"
|
||||
@@ -1001,12 +1000,9 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
|
||||
* @param y Y co-ordinate in graph space
|
||||
* @returns The first reroute under the given co-ordinates, or undefined
|
||||
*/
|
||||
getRerouteOnPos(x: number, y: number): Reroute | undefined {
|
||||
for (const reroute of this.reroutes.values()) {
|
||||
const { pos } = reroute
|
||||
|
||||
if (isSortaInsideOctagon(x - pos[0], y - pos[1], 2 * Reroute.radius))
|
||||
return reroute
|
||||
getRerouteOnPos(x: number, y: number, reroutes?: Iterable<Reroute>): Reroute | undefined {
|
||||
for (const reroute of reroutes ?? this.reroutes.values()) {
|
||||
if (reroute.containsPoint([x, y])) return reroute
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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",
|
||||
|
||||
203
src/Reroute.ts
203
src/Reroute.ts
@@ -14,7 +14,7 @@ import type { Serialisable, SerialisableReroute } from "./types/serialisation"
|
||||
|
||||
import { LGraphBadge } from "./LGraphBadge"
|
||||
import { type LinkId, LLink } from "./LLink"
|
||||
import { distance } from "./measure"
|
||||
import { distance, isPointInRect } from "./measure"
|
||||
|
||||
export type RerouteId = number
|
||||
|
||||
@@ -36,6 +36,12 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
/** Maximum distance from reroutes to their bezier curve control points. */
|
||||
static maxSplineOffset: number = 80
|
||||
static drawIdBadge: boolean = false
|
||||
static slotRadius: number = 5
|
||||
/** Distance from reroute centre to slot centre. */
|
||||
static get slotOffset(): number {
|
||||
const gap = Reroute.slotRadius * 0.33
|
||||
return Reroute.radius + gap + Reroute.slotRadius
|
||||
}
|
||||
|
||||
#malloc = new Float32Array(8)
|
||||
|
||||
@@ -81,6 +87,18 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
return [x - radius, y - radius, 2 * radius, 2 * radius]
|
||||
}
|
||||
|
||||
/**
|
||||
* Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection.
|
||||
* Eliminates most hover positions using an extremely cheap check.
|
||||
*/
|
||||
get #hoverArea(): ReadOnlyRect {
|
||||
const xOffset = 2 * Reroute.slotOffset
|
||||
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)
|
||||
|
||||
const [x, y] = this.#pos
|
||||
return [x - xOffset, y - yOffset, 2 * xOffset, 2 * yOffset]
|
||||
}
|
||||
|
||||
/** The total number of links & floating links using this reroute */
|
||||
get totalLinks(): number {
|
||||
return this.linkIds.size + this.floatingLinkIds.size
|
||||
@@ -115,12 +133,32 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
/** Colour of the first link that rendered this reroute */
|
||||
_colour?: CanvasColour
|
||||
|
||||
/** Colour of the first link that rendered this reroute */
|
||||
get colour(): CanvasColour {
|
||||
return this._colour ?? "#18184d"
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to ensure reroute angles are only executed once per frame.
|
||||
* @todo Calculate on change instead.
|
||||
*/
|
||||
#lastRenderTime: number = -Infinity
|
||||
|
||||
#inputSlot = new RerouteSlot(this, true)
|
||||
#outputSlot = new RerouteSlot(this, false)
|
||||
|
||||
get isSlotHovered(): boolean {
|
||||
return this.isInputHovered || this.isOutputHovered
|
||||
}
|
||||
|
||||
get isInputHovered(): boolean {
|
||||
return this.#inputSlot.hovering
|
||||
}
|
||||
|
||||
get isOutputHovered(): boolean {
|
||||
return this.#outputSlot.hovering
|
||||
}
|
||||
|
||||
get firstLink(): LLink | undefined {
|
||||
const linkId = this.linkIds.values().next().value
|
||||
return linkId === undefined
|
||||
@@ -499,7 +537,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
ctx.globalAlpha = globalAlpha * 0.33
|
||||
}
|
||||
|
||||
ctx.fillStyle = this._colour ?? "#18184d"
|
||||
ctx.fillStyle = this.colour
|
||||
ctx.lineWidth = Reroute.radius * 0.1
|
||||
ctx.strokeStyle = "rgb(0,0,0,0.5)"
|
||||
ctx.fill()
|
||||
@@ -529,13 +567,19 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
ctx.globalAlpha = globalAlpha
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the input and output slots on the canvas, if the slots are visible.
|
||||
* @param ctx The canvas context to draw on.
|
||||
*/
|
||||
drawSlots(ctx: CanvasRenderingContext2D): void {
|
||||
this.#inputSlot.draw(ctx)
|
||||
this.#outputSlot.draw(ctx)
|
||||
}
|
||||
|
||||
drawHighlight(ctx: CanvasRenderingContext2D, colour: CanvasColour): void {
|
||||
const { pos } = this
|
||||
|
||||
const { strokeStyle, lineWidth } = ctx
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineWidth = lineWidth
|
||||
|
||||
ctx.strokeStyle = colour
|
||||
ctx.lineWidth = 1
|
||||
|
||||
@@ -547,6 +591,56 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
ctx.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates visibility of the input and output slots, based on the position of the pointer.
|
||||
* @param pos The position of the pointer.
|
||||
* @returns `true` if any changes require a redraw.
|
||||
*/
|
||||
updateVisibility(pos: Point): boolean {
|
||||
const input = this.#inputSlot
|
||||
const output = this.#outputSlot
|
||||
input.dirty = false
|
||||
output.dirty = false
|
||||
|
||||
const { firstFloatingLink } = this
|
||||
const hasLink = !!this.firstLink
|
||||
|
||||
const showInput = hasLink || firstFloatingLink?.isFloatingOutput
|
||||
const showOutput = hasLink || firstFloatingLink?.isFloatingInput
|
||||
const showEither = showInput || showOutput
|
||||
|
||||
// Check if even in the vicinity
|
||||
if (showEither && isPointInRect(pos, this.#hoverArea)) {
|
||||
const outlineOnly = this.#contains(pos)
|
||||
|
||||
if (showInput) input.update(pos, outlineOnly)
|
||||
if (showOutput) output.update(pos, outlineOnly)
|
||||
} else {
|
||||
this.hideSlots()
|
||||
}
|
||||
|
||||
return input.dirty || output.dirty
|
||||
}
|
||||
|
||||
/** Prevents rendering of the input and output slots. */
|
||||
hideSlots() {
|
||||
this.#inputSlot.hide()
|
||||
this.#outputSlot.hide()
|
||||
}
|
||||
|
||||
/**
|
||||
* Precisely determines if {@link pos} is inside this reroute.
|
||||
* @param pos The position to check (canvas space)
|
||||
* @returns `true` if {@link pos} is within the reroute's radius.
|
||||
*/
|
||||
containsPoint(pos: Point): boolean {
|
||||
return isPointInRect(pos, this.#hoverArea) && this.#contains(pos)
|
||||
}
|
||||
|
||||
#contains(pos: Point): boolean {
|
||||
return distance(this.pos, pos) <= Reroute.radius
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
asSerialisable(): SerialisableReroute {
|
||||
const { id, parentId, pos, linkIds } = this
|
||||
@@ -560,6 +654,105 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Represents a slot on a reroute.
|
||||
* @private Designed for internal use within this module.
|
||||
*/
|
||||
class RerouteSlot {
|
||||
/** The reroute that the slot belongs to. */
|
||||
readonly #reroute: Reroute
|
||||
|
||||
readonly #offsetMultiplier: 1 | -1
|
||||
/** Centre point of this slot. */
|
||||
get pos(): Point {
|
||||
const [x, y] = this.#reroute.pos
|
||||
return [x + Reroute.slotOffset * this.#offsetMultiplier, y]
|
||||
}
|
||||
|
||||
/** Whether any changes require a redraw. */
|
||||
dirty: boolean = false
|
||||
|
||||
#hovering = false
|
||||
/** Whether the pointer is hovering over the slot itself. */
|
||||
get hovering() {
|
||||
return this.#hovering
|
||||
}
|
||||
|
||||
set hovering(value) {
|
||||
if (!Object.is(this.#hovering, value)) {
|
||||
this.#hovering = value
|
||||
this.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
#showOutline = false
|
||||
/** Whether the slot outline / faint background is visible. */
|
||||
get showOutline() {
|
||||
return this.#showOutline
|
||||
}
|
||||
|
||||
set showOutline(value) {
|
||||
if (!Object.is(this.#showOutline, value)) {
|
||||
this.#showOutline = value
|
||||
this.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
constructor(reroute: Reroute, isInput: boolean) {
|
||||
this.#reroute = reroute
|
||||
this.#offsetMultiplier = isInput ? -1 : 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the slot's visibility based on the position of the pointer.
|
||||
* @param pos The position of the pointer.
|
||||
* @param outlineOnly If `true`, slot will display with the faded outline only ({@link showOutline}).
|
||||
*/
|
||||
update(pos: Point, outlineOnly?: boolean) {
|
||||
if (outlineOnly) {
|
||||
this.hovering = false
|
||||
this.showOutline = true
|
||||
} else {
|
||||
const dist = distance(this.pos, pos)
|
||||
this.hovering = dist <= 2 * Reroute.slotRadius
|
||||
this.showOutline = dist <= 5 * Reroute.slotRadius
|
||||
}
|
||||
}
|
||||
|
||||
/** Hides the slot. */
|
||||
hide() {
|
||||
this.hovering = false
|
||||
this.showOutline = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the slot on the canvas.
|
||||
* @param ctx The canvas context to draw on.
|
||||
*/
|
||||
draw(ctx: CanvasRenderingContext2D): void {
|
||||
const { fillStyle, strokeStyle, lineWidth } = ctx
|
||||
const { showOutline, hovering, pos: [x, y] } = this
|
||||
if (!showOutline) return
|
||||
|
||||
try {
|
||||
ctx.fillStyle = hovering
|
||||
? this.#reroute.colour
|
||||
: "rgba(127,127,127,0.3)"
|
||||
ctx.strokeStyle = "rgb(0,0,0,0.5)"
|
||||
ctx.lineWidth = 1
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, Reroute.slotRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
} finally {
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineWidth = lineWidth
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the position of the next reroute in the chain, or the destination input slot on this link.
|
||||
* @param network The network of links
|
||||
|
||||
@@ -15,6 +15,7 @@ import { MovingInputLink } from "./MovingInputLink"
|
||||
import { MovingLinkBase } from "./MovingLinkBase"
|
||||
import { MovingOutputLink } from "./MovingOutputLink"
|
||||
import { ToInputRenderLink } from "./ToInputRenderLink"
|
||||
import { ToOutputFromRerouteLink } from "./ToOutputFromRerouteLink"
|
||||
import { ToOutputRenderLink } from "./ToOutputRenderLink"
|
||||
|
||||
/**
|
||||
@@ -268,13 +269,22 @@ export class LinkConnector {
|
||||
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||
|
||||
const link = reroute.firstLink ?? reroute.firstFloatingLink
|
||||
if (!link) return
|
||||
if (!link) {
|
||||
console.warn("No link found for reroute.")
|
||||
return
|
||||
}
|
||||
|
||||
const outputNode = network.getNodeById(link.origin_id)
|
||||
if (!outputNode) return
|
||||
if (!outputNode) {
|
||||
console.warn("No output node found for link.", link)
|
||||
return
|
||||
}
|
||||
|
||||
const outputSlot = outputNode.outputs.at(link.origin_slot)
|
||||
if (!outputSlot) return
|
||||
if (!outputSlot) {
|
||||
console.warn("No output slot found for link.", link)
|
||||
return
|
||||
}
|
||||
|
||||
const renderLink = new ToInputRenderLink(network, outputNode, outputSlot, reroute)
|
||||
renderLink.fromDirection = LinkDirection.NONE
|
||||
@@ -285,6 +295,41 @@ export class LinkConnector {
|
||||
this.#setLegacyLinks(false)
|
||||
}
|
||||
|
||||
/**
|
||||
* Drags a new link from a reroute to an output slot.
|
||||
* @param network The network that the link being connected belongs to
|
||||
* @param reroute The reroute that the link is being dragged from
|
||||
*/
|
||||
dragFromRerouteToOutput(network: LinkNetwork, reroute: Reroute): void {
|
||||
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||
|
||||
const link = reroute.firstLink ?? reroute.firstFloatingLink
|
||||
if (!link) {
|
||||
console.warn("No link found for reroute.")
|
||||
return
|
||||
}
|
||||
|
||||
const inputNode = network.getNodeById(link.target_id)
|
||||
if (!inputNode) {
|
||||
console.warn("No input node found for link.", link)
|
||||
return
|
||||
}
|
||||
|
||||
const inputSlot = inputNode.inputs.at(link.target_slot)
|
||||
if (!inputSlot) {
|
||||
console.warn("No input slot found for link.", link)
|
||||
return
|
||||
}
|
||||
|
||||
const renderLink = new ToOutputFromRerouteLink(network, inputNode, inputSlot, reroute, this)
|
||||
renderLink.fromDirection = LinkDirection.LEFT
|
||||
this.renderLinks.push(renderLink)
|
||||
|
||||
this.state.connectingTo = "output"
|
||||
|
||||
this.#setLegacyLinks(true)
|
||||
}
|
||||
|
||||
dragFromLinkSegment(network: LinkNetwork, linkSegment: LinkSegment): void {
|
||||
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||
|
||||
@@ -387,39 +432,7 @@ export class LinkConnector {
|
||||
if (this.renderLinks.length !== 1) throw new Error(`Attempted to connect ${this.renderLinks.length} input links to a reroute.`)
|
||||
|
||||
const renderLink = this.renderLinks[0]
|
||||
|
||||
const results = reroute.findTargetInputs()
|
||||
if (!results?.length) return
|
||||
|
||||
const maybeReroutes = reroute.getReroutes()
|
||||
if (maybeReroutes === null) throw new Error("Reroute loop detected.")
|
||||
|
||||
const originalReroutes = maybeReroutes.slice(0, -1).reverse()
|
||||
|
||||
// From reroute to reroute
|
||||
if (renderLink instanceof ToInputRenderLink) {
|
||||
const { node, fromSlot, fromSlotIndex, fromReroute } = renderLink
|
||||
|
||||
reroute.setFloatingLinkOrigin(node, fromSlot, fromSlotIndex)
|
||||
|
||||
// Clean floating link IDs from reroutes about to be removed from the chain
|
||||
if (fromReroute != null) {
|
||||
for (const originalReroute of originalReroutes) {
|
||||
if (originalReroute.id === fromReroute.id) break
|
||||
|
||||
for (const linkId of reroute.floatingLinkIds) {
|
||||
originalReroute.floatingLinkIds.delete(linkId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter before any connections are re-created
|
||||
const filtered = results.filter(result => renderLink.toType === "input" && canConnectInputLinkToReroute(renderLink, result.node, result.input, reroute))
|
||||
|
||||
for (const result of filtered) {
|
||||
renderLink.connectToRerouteInput(reroute, result, this.events, originalReroutes)
|
||||
}
|
||||
this._connectOutputToReroute(reroute, renderLink)
|
||||
|
||||
return
|
||||
}
|
||||
@@ -438,6 +451,44 @@ export class LinkConnector {
|
||||
}
|
||||
}
|
||||
|
||||
/** @internal Temporary workaround - requires refactor. */
|
||||
_connectOutputToReroute(reroute: Reroute, renderLink: RenderLinkUnion): void {
|
||||
const results = reroute.findTargetInputs()
|
||||
if (!results?.length) return
|
||||
|
||||
const maybeReroutes = reroute.getReroutes()
|
||||
if (maybeReroutes === null) throw new Error("Reroute loop detected.")
|
||||
|
||||
const originalReroutes = maybeReroutes.slice(0, -1).reverse()
|
||||
|
||||
// From reroute to reroute
|
||||
if (renderLink instanceof ToInputRenderLink) {
|
||||
const { node, fromSlot, fromSlotIndex, fromReroute } = renderLink
|
||||
|
||||
reroute.setFloatingLinkOrigin(node, fromSlot, fromSlotIndex)
|
||||
|
||||
// Clean floating link IDs from reroutes about to be removed from the chain
|
||||
if (fromReroute != null) {
|
||||
for (const originalReroute of originalReroutes) {
|
||||
if (originalReroute.id === fromReroute.id) break
|
||||
|
||||
for (const linkId of reroute.floatingLinkIds) {
|
||||
originalReroute.floatingLinkIds.delete(linkId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Filter before any connections are re-created
|
||||
const filtered = results.filter(result => renderLink.toType === "input" && canConnectInputLinkToReroute(renderLink, result.node, result.input, reroute))
|
||||
|
||||
for (const result of filtered) {
|
||||
renderLink.connectToRerouteInput(reroute, result, this.events, originalReroutes)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
dropOnNothing(event: CanvasPointerEvent): void {
|
||||
// For external event only.
|
||||
const mayContinue = this.events.dispatch("dropped-on-canvas", event)
|
||||
|
||||
31
src/canvas/ToOutputFromRerouteLink.ts
Normal file
31
src/canvas/ToOutputFromRerouteLink.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import type { LinkConnector } from "./LinkConnector"
|
||||
import type { LGraphNode } from "@/LGraphNode"
|
||||
import type { INodeInputSlot, INodeOutputSlot, LinkNetwork } from "@/litegraph"
|
||||
import type { Reroute } from "@/Reroute"
|
||||
|
||||
import { ToInputRenderLink } from "./ToInputRenderLink"
|
||||
import { ToOutputRenderLink } from "./ToOutputRenderLink"
|
||||
|
||||
/**
|
||||
* @internal A workaround class to support connecting to reroutes to node outputs.
|
||||
*/
|
||||
export class ToOutputFromRerouteLink extends ToOutputRenderLink {
|
||||
constructor(
|
||||
network: LinkNetwork,
|
||||
node: LGraphNode,
|
||||
fromSlot: INodeInputSlot,
|
||||
readonly fromReroute: Reroute,
|
||||
readonly linkConnector: LinkConnector,
|
||||
) {
|
||||
super(network, node, fromSlot, fromReroute)
|
||||
}
|
||||
|
||||
override canConnectToReroute(): false {
|
||||
return false
|
||||
}
|
||||
|
||||
override connectToOutput(node: LGraphNode, output: INodeOutputSlot) {
|
||||
const nuRenderLink = new ToInputRenderLink(this.network, node, output)
|
||||
this.linkConnector._connectOutputToReroute(this.fromReroute, nuRenderLink)
|
||||
}
|
||||
}
|
||||
@@ -36,6 +36,8 @@ export enum CanvasItem {
|
||||
Link = 1 << 3,
|
||||
/** A resize in the bottom-right corner */
|
||||
ResizeSe = 1 << 4,
|
||||
/** A reroute slot */
|
||||
RerouteSlot = 1 << 5,
|
||||
}
|
||||
|
||||
/** The direction that a link point will flow towards - e.g. horizontal outputs are right by default */
|
||||
|
||||
Reference in New Issue
Block a user