mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 14:30:41 +00:00
[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:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user