[API] Fix several floating links issues & add Reroute.totalLinks (#815)

Resolves several issues with floating links.  Highlights:

- Caches floating links on slots, removing some loop checks (inefficient
/ does not scale)
- Simpler APIs
- Adds `Reroute.totalLinks` (regular and floating
This commit is contained in:
filtered
2025-03-22 06:17:54 +11:00
committed by GitHub
parent 766e69bbf1
commit 87aeab16a0
7 changed files with 104 additions and 71 deletions

View File

@@ -269,6 +269,8 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
this.reroutes.clear()
this.#floatingLinks.clear()
this.#lastFloatingLinkId = 0
// other scene stuff
this._groups = []
@@ -1376,6 +1378,16 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
}
this.#floatingLinks.set(link.id, link)
const slot = link.target_id !== -1
? this.getNodeById(link.target_id)?.inputs?.[link.target_slot]
: this.getNodeById(link.origin_id)?.outputs?.[link.origin_slot]
if (slot) {
slot._floatingLinks ??= new Set()
slot._floatingLinks.add(link)
} else {
console.warn(`Adding invalid floating link: target/slot: [${link.target_id}/${link.target_slot}] origin/slot: [${link.origin_id}/${link.origin_slot}]`)
}
const reroutes = LLink.getReroutes(this, link)
for (const reroute of reroutes) {
reroute.floatingLinkIds.add(link.id)
@@ -1386,6 +1398,13 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
removeFloatingLink(link: LLink): void {
this.#floatingLinks.delete(link.id)
const slot = link.target_id !== -1
? this.getNodeById(link.target_id)?.inputs?.[link.target_slot]
: this.getNodeById(link.origin_id)?.outputs?.[link.origin_slot]
if (slot) {
slot._floatingLinks?.delete(link)
}
const reroutes = LLink.getReroutes(this, link)
for (const reroute of reroutes) {
reroute.floatingLinkIds.delete(link.id)
@@ -1393,8 +1412,7 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
delete reroute.floating
}
const totalLinks = reroute.floatingLinkIds.size + reroute.linkIds.size
if (totalLinks === 0) this.removeReroute(reroute.id)
if (reroute.totalLinks === 0) this.removeReroute(reroute.id)
}
}
@@ -1479,12 +1497,19 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
continue
}
// Remove floating links with no reroutes
// A floating link is a unique branch; if there is no parent reroute, or
// the parent reroute has any other links, remove this floating link.
const floatingReroutes = LLink.getReroutes(this, link)
if (!(floatingReroutes.length > 0)) {
this.#floatingLinks.delete(linkId)
const lastReroute = floatingReroutes.at(-1)
const secondLastReroute = floatingReroutes.at(-2)
if (reroute !== lastReroute) {
continue
} else if (secondLastReroute?.totalLinks !== 1) {
this.removeFloatingLink(link)
} else if (link.parentId === id) {
link.parentId = parentId
secondLastReroute.floating = reroute.floating
}
}
@@ -1646,16 +1671,6 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
reroutes = data.reroutes
}
// Floating links
if (Array.isArray(data.floatingLinks)) {
for (const linkData of data.floatingLinks) {
const floatingLink = LLink.create(linkData)
this.#floatingLinks.set(floatingLink.id, floatingLink)
if (floatingLink.id > this.#lastFloatingLinkId) this.#lastFloatingLinkId = floatingLink.id
}
}
// Reroutes
if (Array.isArray(reroutes)) {
for (const rerouteData of reroutes) {
@@ -1663,22 +1678,6 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
}
}
// Cache floating link IDs on reroutes
for (const floatingLink of this.floatingLinks.values()) {
const reroutes = LLink.getReroutes(this, floatingLink)
for (const reroute of reroutes) {
reroute.floatingLinkIds.add(floatingLink.id)
}
}
// Drop broken reroutes
for (const reroute of this.reroutes.values()) {
// Drop broken links, and ignore reroutes with no valid links
if (!reroute.validateLinks(this._links, this.floatingLinks)) {
this.reroutes.delete(reroute.id)
}
}
const nodesData = data.nodes
// copy all stored fields
@@ -1723,6 +1722,24 @@ export class LGraph implements LinkNetwork, Serialisable<SerialisableGraph> {
}
}
// Floating links
if (Array.isArray(data.floatingLinks)) {
for (const linkData of data.floatingLinks) {
const floatingLink = LLink.create(linkData)
this.addFloatingLink(floatingLink)
if (floatingLink.id > this.#lastFloatingLinkId) this.#lastFloatingLinkId = floatingLink.id
}
}
// Drop broken reroutes
for (const reroute of this.reroutes.values()) {
// Drop broken links, and ignore reroutes with no valid links
if (!reroute.validateLinks(this._links, this.floatingLinks)) {
this.reroutes.delete(reroute.id)
}
}
// groups
this._groups.length = 0
const groupData = data.groups

View File

@@ -2200,7 +2200,7 @@ export class LGraphCanvas implements ConnectionColorContext {
const link_pos = node.getOutputPos(i)
if (isInRectangle(x, y, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) {
// Drag multiple output links
if (e.shiftKey && output.links?.length) {
if (e.shiftKey && (output.links?.length || output._floatingLinks?.size)) {
linkConnector.moveOutputLink(graph, output)
pointer.onDragEnd = upEvent => linkConnector.dropLinks(graph, upEvent)
@@ -2242,24 +2242,17 @@ export class LGraphCanvas implements ConnectionColorContext {
pointer.onDoubleClick = () => node.onInputDblClick?.(i, e)
pointer.onClick = () => node.onInputClick?.(i, e)
if (input.link !== null) {
if (
LiteGraph.click_do_break_link_to ||
(LiteGraph.ctrl_alt_click_do_break_link &&
ctrlOrMeta &&
e.altKey &&
!e.shiftKey)
) {
const shouldBreakLink = LiteGraph.ctrl_alt_click_do_break_link &&
ctrlOrMeta &&
e.altKey &&
!e.shiftKey
if (input.link !== null || input._floatingLinks?.size) {
// Existing link
if (shouldBreakLink || LiteGraph.click_do_break_link_to) {
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

View File

@@ -2564,15 +2564,19 @@ 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) return false
if (output._floatingLinks) {
for (const link of output._floatingLinks) {
if (link.hasOrigin(this.id, slot)) {
this.graph?.removeFloatingLink(link)
}
}
}
// get output slot
const output = this.outputs[slot]
if (!output || !output.links || output.links.length == 0) return false
if (!output.links || output.links.length == 0) return false
const { links } = output
// one of the output links in this slot
@@ -2686,16 +2690,24 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
const input = this.inputs[slot]
if (!input) return false
const { graph } = this
if (!graph) throw new NullGraphError()
// Break floating links
if (input._floatingLinks?.size) {
for (const link of input._floatingLinks) {
graph.removeFloatingLink(link)
}
}
const link_id = this.inputs[slot].link
if (link_id != null) {
this.inputs[slot].link = null
if (!this.graph) throw new NullGraphError()
// remove other side
const link_info = this.graph._links.get(link_id)
const link_info = graph._links.get(link_id)
if (link_info) {
const target_node = this.graph.getNodeById(link_info.origin_id)
const target_node = graph.getNodeById(link_info.origin_id)
if (!target_node) return false
const output = target_node.outputs[link_info.origin_slot]
@@ -2710,8 +2722,8 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
}
}
link_info.disconnect(this.graph, keepReroutes ? "output" : undefined)
if (this.graph) this.graph._version++
link_info.disconnect(graph, keepReroutes ? "output" : undefined)
if (graph) graph._version++
this.onConnectionsChange?.(
NodeSlotType.INPUT,
@@ -2731,7 +2743,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
}
this.setDirtyCanvas(false, true)
this.graph?.connectionChange(this)
graph?.connectionChange(this)
return true
}

View File

@@ -221,7 +221,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
for (const reroute of reroutes) {
reroute.linkIds.delete(this.id)
if (!keepReroutes && !reroute.linkIds.size && !reroute.floatingLinkIds.size) {
if (!keepReroutes && !reroute.totalLinks) {
network.reroutes.delete(reroute.id)
}
}

View File

@@ -78,6 +78,11 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
return [x - radius, y - radius, 2 * radius, 2 * radius]
}
/** The total number of links & floating links using this reroute */
get totalLinks(): number {
return this.linkIds.size + this.floatingLinkIds.size
}
/** @inheritdoc */
selected?: boolean
@@ -287,7 +292,11 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
return results
function addAllResults(network: ReadonlyLinkNetwork, linkIds: Iterable<LinkId>, links: ReadonlyMap<LinkId, LLink>) {
function addAllResults(
network: ReadonlyLinkNetwork,
linkIds: Iterable<LinkId>,
links: ReadonlyMap<LinkId, LLink>,
) {
for (const linkId of linkIds) {
const link = links.get(linkId)
if (!link) continue
@@ -329,15 +338,13 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
if (!(lastRenderTime > this.#lastRenderTime)) return
this.#lastRenderTime = lastRenderTime
const { links, floatingLinks } = network
const { id, linkIds, floatingLinkIds, pos: thisPos } = this
const angles: number[] = []
let sum = 0
const { id, pos: thisPos } = this
// Add all link angles
calculateLinks(linkIds, links)
calculateLinks(floatingLinkIds, floatingLinks)
const angles: number[] = []
let sum = 0
calculateAngles(this.linkIds, network.links)
calculateAngles(this.floatingLinkIds, network.floatingLinks)
// Invalid - reset
if (!angles.length) {
@@ -373,7 +380,7 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
* @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>) {
function calculateAngles(linkIds: Iterable<LinkId>, links: ReadonlyMap<LinkId, LLink>) {
for (const linkId of linkIds) {
const link = links.get(linkId)
const pos = getNextPos(network, link, id)

View File

@@ -356,8 +356,7 @@ export class LinkConnector {
for (const reroute of reroutes.slice(0, -1).reverse()) {
if (reroute.id === fromReroute?.id) break
const totalLinks = reroute.linkIds.size + reroute.floatingLinkIds.size
if (totalLinks === 1) reroute.remove()
if (reroute.totalLinks === 1) reroute.remove()
}
}
// Set the parentId of the reroute we dropped on, to the reroute we dragged from

View File

@@ -286,6 +286,11 @@ export interface INodeSlot {
* Set by {@link LGraphNode.#layoutSlots}.
*/
_layoutElement?: LayoutElement<INodeSlot>
/**
* A list of floating link IDs that are connected to this slot.
* This is calculated at runtime; it is **not** serialized.
*/
_floatingLinks?: Set<LLink>
}
export interface INodeFlags {