mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-09 17:40:09 +00:00
Add floating reroutes (#773)
### Floating reroutes Native reroutes can now be kept in a disconnected state. Link chains may be kept provided they are connected to _either_ an input or an output. By design, reroutes will be automatically removed if both sides are disconnected.
This commit is contained in:
@@ -883,7 +883,7 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
|
||||
// disconnect inputs
|
||||
if (inputs) {
|
||||
for (const [i, slot] of inputs.entries()) {
|
||||
if (slot.link != null) node.disconnectInput(i)
|
||||
if (slot.link != null) node.disconnectInput(i, true)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1373,6 +1373,31 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
|
||||
this.canvasAction(c => c.setDirty(fg, bg))
|
||||
}
|
||||
|
||||
addFloatingLink(link: LLink): LLink {
|
||||
if (link.id === -1) {
|
||||
link.id = ++this.#lastFloatingLinkId
|
||||
}
|
||||
this.#floatingLinks.set(link.id, link)
|
||||
|
||||
const reroutes = LLink.getReroutes(this, link)
|
||||
for (const reroute of reroutes) {
|
||||
reroute.floatingLinkIds.add(link.id)
|
||||
}
|
||||
return link
|
||||
}
|
||||
|
||||
removeFloatingLink(link: LLink): void {
|
||||
this.#floatingLinks.delete(link.id)
|
||||
|
||||
const reroutes = LLink.getReroutes(this, link)
|
||||
for (const reroute of reroutes) {
|
||||
reroute.floatingLinkIds.delete(link.id)
|
||||
if (reroute.floatingLinkIds.size === 0) {
|
||||
delete reroute.floating
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Configures a reroute on the graph where ID is already known (probably deserialisation).
|
||||
* Creates the object if it does not exist.
|
||||
@@ -1444,8 +1469,12 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
|
||||
}
|
||||
|
||||
// Remove floating links with no reroutes
|
||||
if (parentId === undefined) this.#floatingLinks.delete(linkId)
|
||||
else if (link.parentId === id) link.parentId = parentId
|
||||
const floatingReroutes = LLink.getReroutes(this, link)
|
||||
if (!(floatingReroutes.length > 0)) {
|
||||
this.#floatingLinks.delete(linkId)
|
||||
} else if (link.parentId === id) {
|
||||
link.parentId = parentId
|
||||
}
|
||||
}
|
||||
|
||||
reroutes.delete(id)
|
||||
@@ -1460,7 +1489,7 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
|
||||
if (!link) return
|
||||
|
||||
const node = this.getNodeById(link.target_id)
|
||||
node?.disconnectInput(link.target_slot)
|
||||
node?.disconnectInput(link.target_slot, false)
|
||||
|
||||
link.disconnect(this)
|
||||
}
|
||||
|
||||
@@ -2250,10 +2250,16 @@ export class LGraphCanvas implements ConnectionColorContext {
|
||||
e.altKey &&
|
||||
!e.shiftKey)
|
||||
) {
|
||||
node.disconnectInput(i)
|
||||
node.disconnectInput(i, true)
|
||||
} else if (e.shiftKey || this.allow_reconnect_links) {
|
||||
linkConnector.moveInputLink(graph, input)
|
||||
}
|
||||
} else {
|
||||
for (const link of graph.floatingLinks.values()) {
|
||||
if (link.target_id === node.id && link.target_slot === i) {
|
||||
graph.removeFloatingLink(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Dragging a new link from input to output
|
||||
@@ -4596,6 +4602,10 @@ export class LGraphCanvas implements ConnectionColorContext {
|
||||
}
|
||||
}
|
||||
|
||||
if (graph.floatingLinks.size > 0) {
|
||||
this.#renderFloatingLinks(ctx, graph, visibleReroutes, now)
|
||||
}
|
||||
|
||||
// Render the reroute circles
|
||||
for (const reroute of visibleReroutes) {
|
||||
if (
|
||||
@@ -4610,6 +4620,39 @@ export class LGraphCanvas implements ConnectionColorContext {
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
|
||||
#renderFloatingLinks(ctx: CanvasRenderingContext2D, graph: LGraph, visibleReroutes: Reroute[], now: number) {
|
||||
// Floating reroutes
|
||||
for (const link of graph.floatingLinks.values()) {
|
||||
const reroutes = LLink.getReroutes(graph, link)
|
||||
const firstReroute = reroutes[0]
|
||||
const reroute = reroutes.at(-1)
|
||||
if (!firstReroute || !reroute?.floating) continue
|
||||
|
||||
// Input not connected
|
||||
if (reroute.floating.slotType === "input") {
|
||||
const node = graph.getNodeById(link.target_id)
|
||||
if (!node) continue
|
||||
|
||||
const startPos = firstReroute.pos
|
||||
const endPos = node.getInputPos(link.target_slot)
|
||||
const endDirection = node.inputs[link.target_slot]?.dir
|
||||
|
||||
firstReroute._dragging = true
|
||||
this.#renderAllLinkSegments(ctx, link, startPos, endPos, visibleReroutes, now, LinkDirection.CENTER, endDirection)
|
||||
} else {
|
||||
const node = graph.getNodeById(link.origin_id)
|
||||
if (!node) continue
|
||||
|
||||
const startPos = node.getOutputPos(link.origin_slot)
|
||||
const endPos = reroute.pos
|
||||
const startDirection = node.outputs[link.origin_slot]?.dir
|
||||
|
||||
link._dragging = true
|
||||
this.#renderAllLinkSegments(ctx, link, startPos, endPos, visibleReroutes, now, startDirection, LinkDirection.CENTER)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#renderAllLinkSegments(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
link: LLink,
|
||||
@@ -4687,10 +4730,15 @@ export class LGraphCanvas implements ConnectionColorContext {
|
||||
}
|
||||
}
|
||||
|
||||
// Calculate start control for the next iter control point
|
||||
const nextPos = reroutes[j + 1]?.pos ?? endPos
|
||||
const dist = Math.min(80, distance(reroute.pos, nextPos) * 0.25)
|
||||
startControl = [dist * reroute.cos, dist * reroute.sin]
|
||||
if (!startControl && reroutes.at(-1)?.floating?.slotType === "input") {
|
||||
// Floating link connected to an input
|
||||
startControl = [0, 0] satisfies Point
|
||||
} else {
|
||||
// Calculate start control for the next iter control point
|
||||
const nextPos = reroutes[j + 1]?.pos ?? endPos
|
||||
const dist = Math.min(80, distance(reroute.pos, nextPos) * 0.25)
|
||||
startControl = [dist * reroute.cos, dist * reroute.sin]
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the last segment if it is being dragged
|
||||
@@ -7091,7 +7139,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
||||
if (info.output) {
|
||||
node.disconnectOutput(info.slot)
|
||||
} else if (info.input) {
|
||||
node.disconnectInput(info.slot)
|
||||
node.disconnectInput(info.slot, true)
|
||||
}
|
||||
node.graph.afterChange()
|
||||
return
|
||||
|
||||
@@ -1471,7 +1471,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
||||
* remove an existing input slot
|
||||
*/
|
||||
removeInput(slot: number): void {
|
||||
this.disconnectInput(slot)
|
||||
this.disconnectInput(slot, true)
|
||||
const { inputs } = this
|
||||
const slot_info = inputs.splice(slot, 1)
|
||||
|
||||
@@ -2503,8 +2503,22 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
||||
inputNode.inputs[inputIndex].link = link.id
|
||||
|
||||
// Reroutes
|
||||
for (const reroute of LLink.getReroutes(graph, link)) {
|
||||
reroute?.linkIds.add(link.id)
|
||||
const reroutes = LLink.getReroutes(graph, link)
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds.add(link.id)
|
||||
if (reroute.floating) delete reroute.floating
|
||||
reroute._dragging = undefined
|
||||
}
|
||||
|
||||
// If this is the terminus of a floating link, remove it
|
||||
const lastReroute = reroutes.at(-1)
|
||||
if (lastReroute) {
|
||||
for (const linkId of lastReroute.floatingLinkIds) {
|
||||
const link = graph.floatingLinks.get(linkId)
|
||||
if (link?.parentId === lastReroute.id) {
|
||||
graph.removeFloatingLink(link)
|
||||
}
|
||||
}
|
||||
}
|
||||
graph._version++
|
||||
|
||||
@@ -2551,6 +2565,12 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
||||
return false
|
||||
}
|
||||
|
||||
for (const link of this.graph?.floatingLinks.values() ?? []) {
|
||||
if (link.origin_id === this.id && link.origin_slot === slot) {
|
||||
this.graph?.removeFloatingLink(link)
|
||||
}
|
||||
}
|
||||
|
||||
// get output slot
|
||||
const output = this.outputs[slot]
|
||||
if (!output || !output.links || output.links.length == 0) return false
|
||||
@@ -2578,7 +2598,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
||||
input.link = null
|
||||
|
||||
// remove the link from the links pool
|
||||
link_info.disconnect(graph)
|
||||
link_info.disconnect(graph, "input")
|
||||
graph._version++
|
||||
|
||||
// link_info hasn't been modified so its ok
|
||||
@@ -2625,7 +2645,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
||||
)
|
||||
}
|
||||
// remove the link from the links pool
|
||||
link_info.disconnect(graph)
|
||||
link_info.disconnect(graph, "input")
|
||||
|
||||
this.onConnectionsChange?.(
|
||||
NodeSlotType.OUTPUT,
|
||||
@@ -2691,7 +2711,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
||||
}
|
||||
}
|
||||
|
||||
link_info.disconnect(this.graph, keepReroutes)
|
||||
link_info.disconnect(this.graph, keepReroutes ? "output" : undefined)
|
||||
if (this.graph) this.graph._version++
|
||||
|
||||
this.onConnectionsChange?.(
|
||||
|
||||
41
src/LLink.ts
41
src/LLink.ts
@@ -104,12 +104,12 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all reroutes from the output slot to this segment. If this segment is a reroute, it will be the last element.
|
||||
* Gets all reroutes from the output slot to this segment. If this segment is a reroute, it will not be included.
|
||||
* @returns An ordered array of all reroutes from the node output to
|
||||
* this reroute or the reroute before it. Otherwise, an empty array.
|
||||
*/
|
||||
static getReroutes(
|
||||
network: ReadonlyLinkNetwork,
|
||||
network: Pick<ReadonlyLinkNetwork, "reroutes">,
|
||||
linkSegment: LinkSegment,
|
||||
): Reroute[] {
|
||||
if (!linkSegment.parentId) return []
|
||||
@@ -119,7 +119,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
}
|
||||
|
||||
static getFirstReroute(
|
||||
network: LinkNetwork,
|
||||
network: Pick<ReadonlyLinkNetwork, "reroutes">,
|
||||
linkSegment: LinkSegment,
|
||||
): Reroute | undefined {
|
||||
return LLink.getReroutes(network, linkSegment).at(0)
|
||||
@@ -166,15 +166,44 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
/**
|
||||
* Disconnects a link and removes it from the graph, cleaning up any reroutes that are no longer used
|
||||
* @param network The container (LGraph) where reroutes should be updated
|
||||
* @param keepReroutes If `true`, reroutes will not be garbage collected.
|
||||
* @param keepReroutes If `undefined`, reroutes will be automatically removed if no links remain.
|
||||
* If `input` or `output`, reroutes will not be automatically removed, and retain a connection to the input or output, respectively.
|
||||
*/
|
||||
disconnect(network: LinkNetwork, keepReroutes?: boolean): void {
|
||||
disconnect(network: LinkNetwork, keepReroutes?: "input" | "output"): void {
|
||||
const reroutes = LLink.getReroutes(network, this)
|
||||
|
||||
const lastReroute = reroutes.at(-1)
|
||||
|
||||
// When floating from output, 1-to-1 ratio of floating link to final reroute (tree-like)
|
||||
const outputFloating = keepReroutes === "output" &&
|
||||
lastReroute?.linkIds.size === 1 &&
|
||||
lastReroute.floatingLinkIds.size === 0
|
||||
|
||||
// When floating from inputs, the final (input side) reroute may have many floating links
|
||||
if (outputFloating || (keepReroutes === "input" && lastReroute)) {
|
||||
const newLink = LLink.create(this)
|
||||
newLink.id = -1
|
||||
|
||||
if (keepReroutes === "input") {
|
||||
newLink.origin_id = -1
|
||||
newLink.origin_slot = -1
|
||||
|
||||
lastReroute.floating = { slotType: "input" }
|
||||
} else {
|
||||
newLink.target_id = -1
|
||||
newLink.target_slot = -1
|
||||
|
||||
lastReroute.floating = { slotType: "output" }
|
||||
}
|
||||
|
||||
network.addFloatingLink(newLink)
|
||||
}
|
||||
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds.delete(this.id)
|
||||
if (!keepReroutes && !reroute.linkIds.size)
|
||||
if (!keepReroutes && !reroute.linkIds.size && !reroute.floatingLinkIds.size) {
|
||||
network.reroutes.delete(reroute.id)
|
||||
}
|
||||
}
|
||||
network.links.delete(this.id)
|
||||
}
|
||||
|
||||
172
src/Reroute.ts
172
src/Reroute.ts
@@ -1,5 +1,6 @@
|
||||
import type {
|
||||
CanvasColour,
|
||||
INodeInputSlot,
|
||||
LinkNetwork,
|
||||
LinkSegment,
|
||||
Point,
|
||||
@@ -7,7 +8,7 @@ import type {
|
||||
ReadonlyLinkNetwork,
|
||||
ReadOnlyRect,
|
||||
} from "./interfaces"
|
||||
import type { NodeId } from "./LGraphNode"
|
||||
import type { LGraphNode, NodeId } from "./LGraphNode"
|
||||
import type { Serialisable, SerialisableReroute } from "./types/serialisation"
|
||||
|
||||
import { type LinkId, LLink } from "./LLink"
|
||||
@@ -17,10 +18,6 @@ export type RerouteId = number
|
||||
|
||||
/** The input or output slot that an incomplete reroute link is connected to. */
|
||||
export interface FloatingRerouteSlot {
|
||||
/** The ID of the node that the slot belongs to */
|
||||
nodeId: NodeId
|
||||
/** The index of the slot on the node */
|
||||
slot: number
|
||||
/** Floating connection to an input or output */
|
||||
slotType: "input" | "output"
|
||||
}
|
||||
@@ -52,7 +49,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
this.#parentId = value
|
||||
}
|
||||
|
||||
/** Set when the reroute has no complete links but is still on the canvas. */
|
||||
/** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */
|
||||
floating?: FloatingRerouteSlot
|
||||
|
||||
#pos = this.#malloc.subarray(0, 2)
|
||||
@@ -110,29 +107,34 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
*/
|
||||
#lastRenderTime: number = -Infinity
|
||||
|
||||
/** @inheritdoc */
|
||||
get origin_id(): NodeId | undefined {
|
||||
// if (!this.linkIds.size) return this.#network.deref()?.reroutes.get(this.parentId)
|
||||
const nextId = this.linkIds.values().next().value
|
||||
return nextId === undefined
|
||||
get firstLink(): LLink | undefined {
|
||||
const linkId = this.linkIds.values().next().value
|
||||
return linkId === undefined
|
||||
? undefined
|
||||
: this.#network
|
||||
.deref()
|
||||
?.links
|
||||
.get(nextId)
|
||||
?.origin_id
|
||||
.get(linkId)
|
||||
}
|
||||
|
||||
get firstFloatingLink(): LLink | undefined {
|
||||
const linkId = this.floatingLinkIds.values().next().value
|
||||
return linkId === undefined
|
||||
? undefined
|
||||
: this.#network
|
||||
.deref()
|
||||
?.floatingLinks
|
||||
.get(linkId)
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get origin_id(): NodeId | undefined {
|
||||
return this.firstLink?.origin_id
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
get origin_slot(): number | undefined {
|
||||
const nextId = this.linkIds.values().next().value
|
||||
return nextId === undefined
|
||||
? undefined
|
||||
: this.#network
|
||||
.deref()
|
||||
?.links
|
||||
.get(nextId)
|
||||
?.origin_slot
|
||||
return this.firstLink?.origin_slot
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -151,8 +153,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
floatingLinkIds?: Iterable<LinkId>,
|
||||
) {
|
||||
this.#network = new WeakRef(network)
|
||||
this.update(parentId, pos, linkIds)
|
||||
this.linkIds ??= new Set()
|
||||
this.parentId = parentId
|
||||
if (pos) this.pos = pos
|
||||
this.linkIds = new Set(linkIds)
|
||||
this.floatingLinkIds = new Set(floatingLinkIds)
|
||||
}
|
||||
|
||||
@@ -243,10 +246,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
|
||||
findSourceOutput() {
|
||||
const network = this.#network.deref()
|
||||
const originId = this.linkIds.values().next().value
|
||||
if (!network || !originId) return
|
||||
if (!network) return
|
||||
|
||||
const link = network.links.get(originId)
|
||||
const link = this.firstLink ?? this.firstFloatingLink
|
||||
if (!link) return
|
||||
|
||||
const node = network.getNodeById(link.origin_id)
|
||||
@@ -260,6 +262,39 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the first input slot for links or floating links passing through this reroute.
|
||||
*/
|
||||
findTargetInputs(): { node: LGraphNode, input: INodeInputSlot, inputIndex: number, link: LLink }[] | undefined {
|
||||
const network = this.#network.deref()
|
||||
if (!network) return
|
||||
|
||||
const results: {
|
||||
node: LGraphNode
|
||||
input: INodeInputSlot
|
||||
inputIndex: number
|
||||
link: LLink
|
||||
}[] = []
|
||||
|
||||
addAllResults(network, this.linkIds, network.links)
|
||||
addAllResults(network, this.floatingLinkIds, network.floatingLinks)
|
||||
|
||||
return results
|
||||
|
||||
function addAllResults(network: ReadonlyLinkNetwork, linkIds: Iterable<LinkId>, links: ReadonlyMap<LinkId, LLink>) {
|
||||
for (const linkId of linkIds) {
|
||||
const link = links.get(linkId)
|
||||
if (!link) continue
|
||||
|
||||
const node = network.getNodeById(link.target_id)
|
||||
const input = node?.inputs[link.target_slot]
|
||||
if (!input) continue
|
||||
|
||||
results.push({ node, input, inputIndex: link.target_slot, link })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
move(deltaX: number, deltaY: number) {
|
||||
this.#pos[0] += deltaX
|
||||
@@ -276,31 +311,36 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
return true
|
||||
}
|
||||
|
||||
remove() {
|
||||
const network = this.#network.deref()
|
||||
if (!network) return
|
||||
|
||||
network.removeReroute(this.id)
|
||||
}
|
||||
|
||||
calculateAngle(lastRenderTime: number, network: ReadonlyLinkNetwork, linkStart: Point): void {
|
||||
// Ensure we run once per render
|
||||
if (!(lastRenderTime > this.#lastRenderTime)) return
|
||||
this.#lastRenderTime = lastRenderTime
|
||||
|
||||
const { links } = network
|
||||
const { linkIds, id } = this
|
||||
const { links, floatingLinks } = network
|
||||
const { id, linkIds, floatingLinkIds, pos: thisPos } = this
|
||||
|
||||
const angles: number[] = []
|
||||
let sum = 0
|
||||
for (const linkId of linkIds) {
|
||||
const link = links.get(linkId)
|
||||
// Remove the linkId or just ignore?
|
||||
if (!link) continue
|
||||
|
||||
const pos = LLink.findNextReroute(network, link, id)?.pos ??
|
||||
network.getNodeById(link.target_id)
|
||||
?.getInputPos(link.target_slot)
|
||||
if (!pos) continue
|
||||
// Add all link angles
|
||||
calculateLinks(linkIds, links)
|
||||
calculateLinks(floatingLinkIds, floatingLinks)
|
||||
|
||||
// TODO: Store points/angles, check if changed, skip calcs.
|
||||
const angle = Math.atan2(pos[1] - this.#pos[1], pos[0] - this.#pos[0])
|
||||
angles.push(angle)
|
||||
sum += angle
|
||||
// Invalid - reset
|
||||
if (!angles.length) {
|
||||
this.cos = 0
|
||||
this.sin = 0
|
||||
this.controlPoint[0] = 0
|
||||
this.controlPoint[1] = 0
|
||||
return
|
||||
}
|
||||
if (!angles.length) return
|
||||
|
||||
sum /= angles.length
|
||||
|
||||
@@ -321,6 +361,23 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
this.sin = sin
|
||||
this.controlPoint[0] = dist * -cos
|
||||
this.controlPoint[1] = dist * -sin
|
||||
|
||||
/**
|
||||
* Calculates the direction of each link and adds it to the array.
|
||||
* @param linkIds The IDs of the links to calculate
|
||||
* @param links The link container from the link network.
|
||||
*/
|
||||
function calculateLinks(linkIds: Iterable<LinkId>, links: ReadonlyMap<LinkId, LLink>) {
|
||||
for (const linkId of linkIds) {
|
||||
const link = links.get(linkId)
|
||||
const pos = getNextPos(network, link, id)
|
||||
if (!pos) continue
|
||||
|
||||
const angle = getDirection(thisPos, pos)
|
||||
angles.push(angle)
|
||||
sum += angle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -357,23 +414,36 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
/** @inheritdoc */
|
||||
asSerialisable(): SerialisableReroute {
|
||||
const { id, parentId, pos, linkIds } = this
|
||||
const floating = floatingToSerialisable(this.floating)
|
||||
return {
|
||||
id,
|
||||
parentId,
|
||||
pos: [pos[0], pos[1]],
|
||||
linkIds: [...linkIds],
|
||||
floating,
|
||||
floating: this.floating ? { slotType: this.floating.slotType } : undefined,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function floatingToSerialisable(floating: FloatingRerouteSlot | undefined): FloatingRerouteSlot | undefined {
|
||||
return floating
|
||||
? {
|
||||
nodeId: floating.nodeId,
|
||||
slot: floating.slot,
|
||||
slotType: floating.slotType,
|
||||
}
|
||||
: undefined
|
||||
/**
|
||||
* Retrieves the position of the next reroute in the chain, or the destination input slot on this link.
|
||||
* @param network The network of links
|
||||
* @param link The link representing the current reroute chain
|
||||
* @param id The ID of "this" reroute
|
||||
* @returns The position of the next reroute or the input slot target, otherwise `undefined`.
|
||||
*/
|
||||
function getNextPos(network: ReadonlyLinkNetwork, link: LLink | undefined, id: RerouteId) {
|
||||
if (!link) return
|
||||
|
||||
const linkPos = LLink.findNextReroute(network, link, id)?.pos
|
||||
if (linkPos) return linkPos
|
||||
|
||||
// Floating link with no input to find
|
||||
if (link.target_id === -1 || link.target_slot === -1) return
|
||||
|
||||
return network.getNodeById(link.target_id)?.getInputPos(link.target_slot)
|
||||
}
|
||||
|
||||
/** Returns the direction from one point to another in radians. */
|
||||
function getDirection(fromPos: Point, toPos: Point) {
|
||||
return Math.atan2(toPos[1] - fromPos[1], toPos[0] - fromPos[0])
|
||||
}
|
||||
|
||||
@@ -113,7 +113,7 @@ export class LinkConnector {
|
||||
renderLinks.push(renderLink)
|
||||
|
||||
this.listenUntilReset("input-moved", (e) => {
|
||||
e.detail.link.disconnect(network, true)
|
||||
e.detail.link.disconnect(network, "output")
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Could not create render link for link id: [${link.id}].`, link, error)
|
||||
@@ -215,13 +215,7 @@ export class LinkConnector {
|
||||
dragFromReroute(network: LinkNetwork, reroute: Reroute): void {
|
||||
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||
|
||||
const { state } = this
|
||||
|
||||
// Connect new link from reroute
|
||||
const linkId = reroute.linkIds.values().next().value
|
||||
if (linkId == null) return
|
||||
|
||||
const link = network.links.get(linkId)
|
||||
const link = reroute.firstLink ?? reroute.firstFloatingLink
|
||||
if (!link) return
|
||||
|
||||
const outputNode = network.getNodeById(link.origin_id)
|
||||
@@ -234,7 +228,7 @@ export class LinkConnector {
|
||||
renderLink.fromDirection = LinkDirection.NONE
|
||||
this.renderLinks.push(renderLink)
|
||||
|
||||
state.connectingTo = "input"
|
||||
this.state.connectingTo = "input"
|
||||
}
|
||||
|
||||
dragFromLinkSegment(network: LinkNetwork, linkSegment: LinkSegment): void {
|
||||
@@ -278,7 +272,7 @@ export class LinkConnector {
|
||||
// Get reroute if no node is found
|
||||
const reroute = locator.getRerouteOnPos(canvasX, canvasY)
|
||||
// Drop output->input link on reroute is not impl.
|
||||
if (reroute && this.state.connectingTo === "output") {
|
||||
if (reroute) {
|
||||
this.dropOnReroute(reroute, event)
|
||||
} else {
|
||||
this.dropOnNothing(event)
|
||||
@@ -329,6 +323,43 @@ export class LinkConnector {
|
||||
const mayContinue = this.events.dispatch("dropped-on-reroute", { reroute, event })
|
||||
if (mayContinue === false) return
|
||||
|
||||
if (this.state.connectingTo === "input") {
|
||||
const results = reroute.findTargetInputs()
|
||||
if (!results?.length) return
|
||||
|
||||
for (const { node: inputNode, input, link: resultLink } of results) {
|
||||
for (const renderLink of this.renderLinks) {
|
||||
if (renderLink.toType !== "input") continue
|
||||
|
||||
if (renderLink instanceof MovingRenderLink) {
|
||||
const { outputNode, inputSlot, outputSlot, fromReroute } = renderLink
|
||||
// Link is already connected here
|
||||
if (inputSlot === input) continue
|
||||
|
||||
const newLink = outputNode.connectSlots(outputSlot, inputNode, input, fromReroute?.id)
|
||||
if (newLink) this.events.dispatch("input-moved", renderLink)
|
||||
} else {
|
||||
const reroutes = reroute.getReroutes()
|
||||
if (reroutes === null) throw new Error("Reroute loop detected.")
|
||||
|
||||
if (reroutes) {
|
||||
for (const reroute of reroutes.slice(0, -1)) {
|
||||
reroute.remove()
|
||||
}
|
||||
}
|
||||
const { node: outputNode, fromSlot, fromReroute } = renderLink
|
||||
// Set the parentId of the reroute we dropped on, to the reroute we dragged from
|
||||
reroute.parentId = fromReroute?.id
|
||||
|
||||
const newLink = outputNode.connectSlots(fromSlot, inputNode, input, resultLink.parentId)
|
||||
this.events.dispatch("link-created", newLink)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
for (const link of this.renderLinks) {
|
||||
if (link.toType !== "output") continue
|
||||
|
||||
|
||||
@@ -124,7 +124,8 @@ export interface ReadonlyLinkNetwork {
|
||||
export interface LinkNetwork extends ReadonlyLinkNetwork {
|
||||
readonly links: Map<LinkId, LLink>
|
||||
readonly reroutes: Map<RerouteId, Reroute>
|
||||
getNodeById(id: NodeId): LGraphNode | null
|
||||
addFloatingLink(link: LLink): LLink
|
||||
removeReroute(id: number): unknown
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
Reference in New Issue
Block a user