mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-20 06:20:11 +00:00
Amp-Thread-ID: https://ampcode.com/threads/T-019c9886-c7b7-73f0-b85a-8a046e556ca8 Co-authored-by: Amp <amp@ampcode.com>
492 lines
15 KiB
TypeScript
492 lines
15 KiB
TypeScript
import {
|
|
SUBGRAPH_INPUT_ID,
|
|
SUBGRAPH_OUTPUT_ID
|
|
} from '@/lib/litegraph/src/constants'
|
|
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
|
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
|
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
|
import { LayoutSource } from '@/renderer/core/layout/types'
|
|
|
|
import type { LGraphNode, NodeId } from './LGraphNode'
|
|
import type { Reroute, RerouteId } from './Reroute'
|
|
import type {
|
|
CanvasColour,
|
|
INodeInputSlot,
|
|
INodeOutputSlot,
|
|
ISlotType,
|
|
LinkNetwork,
|
|
LinkSegment,
|
|
Point,
|
|
ReadonlyLinkNetwork
|
|
} from './interfaces'
|
|
import type { Serialisable, SerialisableLLink } from './types/serialisation'
|
|
|
|
const layoutMutations = useLayoutMutations()
|
|
|
|
export type LinkId = number
|
|
|
|
export type SerialisedLLinkArray = [
|
|
id: LinkId,
|
|
origin_id: NodeId,
|
|
origin_slot: number,
|
|
target_id: NodeId,
|
|
target_slot: number,
|
|
type: ISlotType
|
|
]
|
|
|
|
// Resolved connection union; eliminates subgraph in/out as a possibility
|
|
export type ResolvedConnection = BaseResolvedConnection &
|
|
(
|
|
| (ResolvedSubgraphInput & ResolvedNormalOutput)
|
|
| (ResolvedNormalInput & ResolvedSubgraphOutput)
|
|
| (ResolvedNormalInput & ResolvedNormalOutput)
|
|
)
|
|
|
|
interface BaseResolvedConnection {
|
|
link: LLink
|
|
/** The node on the input side of the link (owns {@link input}) */
|
|
inputNode?: LGraphNode
|
|
/** The input the link is connected to (mutually exclusive with {@link subgraphOutput}) */
|
|
input?: INodeInputSlot
|
|
/** The node on the output side of the link (owns {@link output}) */
|
|
outputNode?: LGraphNode
|
|
/** The output the link is connected to (mutually exclusive with {@link subgraphInput}) */
|
|
output?: INodeOutputSlot
|
|
/** The subgraph output the link is connected to (mutually exclusive with {@link input}) */
|
|
subgraphOutput?: SubgraphOutput
|
|
/** The subgraph input the link is connected to (mutually exclusive with {@link output}) */
|
|
subgraphInput?: SubgraphInput
|
|
}
|
|
|
|
interface ResolvedNormalInput {
|
|
inputNode: LGraphNode | undefined
|
|
input: INodeInputSlot | undefined
|
|
subgraphOutput?: undefined
|
|
}
|
|
|
|
interface ResolvedNormalOutput {
|
|
outputNode: LGraphNode | undefined
|
|
output: INodeOutputSlot | undefined
|
|
subgraphInput?: undefined
|
|
}
|
|
|
|
interface ResolvedSubgraphInput {
|
|
inputNode?: undefined
|
|
/** The actual input slot the link is connected to (mutually exclusive with {@link subgraphOutput}) */
|
|
input?: undefined
|
|
subgraphOutput: SubgraphOutput
|
|
}
|
|
|
|
interface ResolvedSubgraphOutput {
|
|
outputNode?: undefined
|
|
output?: undefined
|
|
subgraphInput: SubgraphInput
|
|
}
|
|
|
|
type BasicReadonlyNetwork = Pick<
|
|
ReadonlyLinkNetwork,
|
|
'getNodeById' | 'links' | 'getLink' | 'inputNode' | 'outputNode'
|
|
>
|
|
|
|
// this is the class in charge of storing link information
|
|
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
|
static _drawDebug = false
|
|
|
|
/** Link ID */
|
|
id: LinkId
|
|
parentId?: RerouteId
|
|
type: ISlotType
|
|
/** Output node ID */
|
|
origin_id: NodeId
|
|
/** Output slot index */
|
|
origin_slot: number
|
|
/** Input node ID */
|
|
target_id: NodeId
|
|
/** Input slot index */
|
|
target_slot: number
|
|
|
|
data?: number | string | boolean | { toToolTip?(): string }
|
|
_data?: unknown
|
|
/** Centre point of the link, calculated during render only - can be inaccurate */
|
|
_pos: Point
|
|
/** @todo Clean up - never implemented in comfy. */
|
|
_last_time?: number
|
|
/** The last canvas 2D path that was used to render this link */
|
|
path?: Path2D
|
|
/** @inheritdoc */
|
|
_centreAngle?: number
|
|
|
|
/** @inheritdoc */
|
|
_dragging?: boolean
|
|
|
|
private _color?: CanvasColour | null
|
|
/** Custom colour for this link only */
|
|
public get color(): CanvasColour | null | undefined {
|
|
return this._color
|
|
}
|
|
|
|
public set color(value: CanvasColour) {
|
|
this._color = value === '' ? null : value
|
|
}
|
|
|
|
public get isFloatingOutput(): boolean {
|
|
return this.origin_id === -1 && this.origin_slot === -1
|
|
}
|
|
|
|
public get isFloatingInput(): boolean {
|
|
return this.target_id === -1 && this.target_slot === -1
|
|
}
|
|
|
|
public get isFloating(): boolean {
|
|
return this.isFloatingOutput || this.isFloatingInput
|
|
}
|
|
|
|
/** `true` if this link is connected to a subgraph input node (the actual origin is in a different graph). */
|
|
get originIsIoNode(): boolean {
|
|
return this.origin_id === SUBGRAPH_INPUT_ID
|
|
}
|
|
|
|
/** `true` if this link is connected to a subgraph output node (the actual target is in a different graph). */
|
|
get targetIsIoNode(): boolean {
|
|
return this.target_id === SUBGRAPH_OUTPUT_ID
|
|
}
|
|
|
|
constructor(
|
|
id: LinkId,
|
|
type: ISlotType,
|
|
origin_id: NodeId,
|
|
origin_slot: number,
|
|
target_id: NodeId,
|
|
target_slot: number,
|
|
parentId?: RerouteId
|
|
) {
|
|
this.id = id
|
|
this.type = type
|
|
this.origin_id = origin_id
|
|
this.origin_slot = origin_slot
|
|
this.target_id = target_id
|
|
this.target_slot = target_slot
|
|
this.parentId = parentId
|
|
|
|
this._data = null
|
|
// center
|
|
this._pos = [0, 0]
|
|
}
|
|
|
|
/** @deprecated Use {@link LLink.create} */
|
|
static createFromArray(data: SerialisedLLinkArray): LLink {
|
|
return new LLink(data[0], data[5], data[1], data[2], data[3], data[4])
|
|
}
|
|
|
|
/**
|
|
* LLink static factory: creates a new LLink from the provided data.
|
|
* @param data Serialised LLink data to create the link from
|
|
* @returns A new LLink
|
|
*/
|
|
static create(data: SerialisableLLink): LLink {
|
|
return new LLink(
|
|
data.id,
|
|
data.type,
|
|
data.origin_id,
|
|
data.origin_slot,
|
|
data.target_id,
|
|
data.target_slot,
|
|
data.parentId
|
|
)
|
|
}
|
|
|
|
/**
|
|
* 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: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
|
linkSegment: LinkSegment
|
|
): Reroute[] {
|
|
if (linkSegment.parentId === undefined) return []
|
|
return network.reroutes.get(linkSegment.parentId)?.getReroutes() ?? []
|
|
}
|
|
|
|
static getFirstReroute(
|
|
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
|
linkSegment: LinkSegment
|
|
): Reroute | undefined {
|
|
return LLink.getReroutes(network, linkSegment).at(0)
|
|
}
|
|
|
|
/**
|
|
* Finds the reroute in the chain after the provided reroute ID.
|
|
* @param network The network this link belongs to
|
|
* @param linkSegment The starting point of the search (input side).
|
|
* Typically the LLink object itself, but can be any link segment.
|
|
* @param rerouteId The matching reroute will have this set as its {@link parentId}.
|
|
* @returns The reroute that was found, `undefined` if no reroute was found, or `null` if an infinite loop was detected.
|
|
*/
|
|
static findNextReroute(
|
|
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
|
linkSegment: LinkSegment,
|
|
rerouteId: RerouteId
|
|
): Reroute | null | undefined {
|
|
if (linkSegment.parentId === undefined) return
|
|
return network.reroutes
|
|
.get(linkSegment.parentId)
|
|
?.findNextReroute(rerouteId)
|
|
}
|
|
|
|
/**
|
|
* Gets the origin node of a link.
|
|
* @param network The network to search
|
|
* @param linkId The ID of the link to get the origin node of
|
|
* @returns The origin node of the link, or `undefined` if the link is not found or the origin node is not found
|
|
*/
|
|
static getOriginNode(
|
|
network: BasicReadonlyNetwork,
|
|
linkId: LinkId
|
|
): LGraphNode | undefined {
|
|
const id = network.getLink(linkId)?.origin_id
|
|
return network.getNodeById(id) ?? undefined
|
|
}
|
|
|
|
/**
|
|
* Gets the target node of a link.
|
|
* @param network The network to search
|
|
* @param linkId The ID of the link to get the target node of
|
|
* @returns The target node of the link, or `undefined` if the link is not found or the target node is not found
|
|
*/
|
|
static getTargetNode(
|
|
network: BasicReadonlyNetwork,
|
|
linkId: LinkId
|
|
): LGraphNode | undefined {
|
|
const id = network.getLink(linkId)?.target_id
|
|
return network.getNodeById(id) ?? undefined
|
|
}
|
|
|
|
/**
|
|
* Resolves a link ID to the link, node, and slot objects.
|
|
* @param linkId The {@link id} of the link to resolve
|
|
* @param network The link network to search
|
|
* @returns An object containing the input and output nodes, as well as the input and output slots.
|
|
* @remarks This method is heavier than others; it will always resolve all objects.
|
|
* Whilst the performance difference should in most cases be negligible,
|
|
* it is recommended to use simpler methods where appropriate.
|
|
*/
|
|
static resolve(
|
|
linkId: LinkId | null | undefined,
|
|
network: BasicReadonlyNetwork
|
|
): ResolvedConnection | undefined {
|
|
return network.getLink(linkId)?.resolve(network)
|
|
}
|
|
|
|
/**
|
|
* Resolves a list of link IDs to the link, node, and slot objects.
|
|
* Discards invalid link IDs.
|
|
* @param linkIds An iterable of link {@link id}s to resolve
|
|
* @param network The link network to search
|
|
* @returns An array of resolved connections. If a link is not found, it is not included in the array.
|
|
* @see {@link LLink.resolve}
|
|
*/
|
|
static resolveMany(
|
|
linkIds: Iterable<LinkId>,
|
|
network: BasicReadonlyNetwork
|
|
): ResolvedConnection[] {
|
|
const resolved: ResolvedConnection[] = []
|
|
for (const id of linkIds) {
|
|
const r = network.getLink(id)?.resolve(network)
|
|
if (r) resolved.push(r)
|
|
}
|
|
return resolved
|
|
}
|
|
|
|
/**
|
|
* Resolves the primitive ID values stored in the link to the referenced objects.
|
|
* @param network The link network to search
|
|
* @returns An object containing the input and output nodes, as well as the input and output slots.
|
|
* @remarks This method is heavier than others; it will always resolve all objects.
|
|
* Whilst the performance difference should in most cases be negligible,
|
|
* it is recommended to use simpler methods where appropriate.
|
|
*/
|
|
resolve(network: BasicReadonlyNetwork): ResolvedConnection {
|
|
const inputNode =
|
|
this.target_id === -1
|
|
? undefined
|
|
: (network.getNodeById(this.target_id) ?? undefined)
|
|
const input = inputNode?.inputs[this.target_slot]
|
|
const subgraphInput = this.originIsIoNode
|
|
? network.inputNode?.slots[this.origin_slot]
|
|
: undefined
|
|
if (subgraphInput) {
|
|
return { inputNode, input, subgraphInput, link: this }
|
|
}
|
|
|
|
const outputNode =
|
|
this.origin_id === -1
|
|
? undefined
|
|
: (network.getNodeById(this.origin_id) ?? undefined)
|
|
const output = outputNode?.outputs[this.origin_slot]
|
|
const subgraphOutput = this.targetIsIoNode
|
|
? network.outputNode?.slots[this.target_slot]
|
|
: undefined
|
|
if (subgraphOutput) {
|
|
return {
|
|
outputNode,
|
|
output,
|
|
subgraphInput: undefined,
|
|
subgraphOutput,
|
|
link: this
|
|
}
|
|
}
|
|
|
|
return {
|
|
inputNode,
|
|
outputNode,
|
|
input,
|
|
output,
|
|
subgraphInput,
|
|
subgraphOutput,
|
|
link: this
|
|
}
|
|
}
|
|
|
|
configure(o: LLink | SerialisedLLinkArray) {
|
|
if (Array.isArray(o)) {
|
|
this.id = o[0]
|
|
this.origin_id = o[1]
|
|
this.origin_slot = o[2]
|
|
this.target_id = o[3]
|
|
this.target_slot = o[4]
|
|
this.type = o[5]
|
|
} else {
|
|
this.id = o.id
|
|
this.type = o.type
|
|
this.origin_id = o.origin_id
|
|
this.origin_slot = o.origin_slot
|
|
this.target_id = o.target_id
|
|
this.target_slot = o.target_slot
|
|
this.parentId = o.parentId
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Checks if the specified node id and output index are this link's origin (output side).
|
|
* @param nodeId ID of the node to check
|
|
* @param outputIndex The array index of the node output
|
|
* @returns `true` if the origin matches, otherwise `false`.
|
|
*/
|
|
hasOrigin(nodeId: NodeId, outputIndex: number): boolean {
|
|
return this.origin_id === nodeId && this.origin_slot === outputIndex
|
|
}
|
|
|
|
/**
|
|
* Checks if the specified node id and input index are this link's target (input side).
|
|
* @param nodeId ID of the node to check
|
|
* @param inputIndex The array index of the node input
|
|
* @returns `true` if the target matches, otherwise `false`.
|
|
*/
|
|
hasTarget(nodeId: NodeId, inputIndex: number): boolean {
|
|
return this.target_id === nodeId && this.target_slot === inputIndex
|
|
}
|
|
|
|
/**
|
|
* Creates a floating link from this link.
|
|
* @param slotType The side of the link that is still connected
|
|
* @param parentId The parent reroute ID of the link
|
|
* @returns A new LLink that is floating
|
|
*/
|
|
toFloating(slotType: 'input' | 'output', parentId: RerouteId): LLink {
|
|
const exported = this.asSerialisable()
|
|
exported.id = -1
|
|
exported.parentId = parentId
|
|
|
|
if (slotType === 'input') {
|
|
exported.origin_id = -1
|
|
exported.origin_slot = -1
|
|
} else {
|
|
exported.target_id = -1
|
|
exported.target_slot = -1
|
|
}
|
|
|
|
return LLink.create(exported)
|
|
}
|
|
|
|
/**
|
|
* 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 `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?: '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.totalLinks) {
|
|
network.reroutes.delete(reroute.id)
|
|
// Delete reroute from Layout Store
|
|
layoutMutations.setSource(LayoutSource.Canvas)
|
|
layoutMutations.deleteReroute(reroute.id)
|
|
}
|
|
}
|
|
network.links.delete(this.id)
|
|
// Delete link from Layout Store
|
|
layoutMutations.setSource(LayoutSource.Canvas)
|
|
layoutMutations.deleteLink(this.id)
|
|
}
|
|
|
|
/**
|
|
* @deprecated Prefer {@link LLink.asSerialisable} (returns an object, not an array)
|
|
* @returns An array representing this LLink
|
|
*/
|
|
serialize(): SerialisedLLinkArray {
|
|
return [
|
|
this.id,
|
|
this.origin_id,
|
|
this.origin_slot,
|
|
this.target_id,
|
|
this.target_slot,
|
|
this.type
|
|
]
|
|
}
|
|
|
|
asSerialisable(): SerialisableLLink {
|
|
const copy: SerialisableLLink = {
|
|
id: this.id,
|
|
origin_id: this.origin_id,
|
|
origin_slot: this.origin_slot,
|
|
target_id: this.target_id,
|
|
target_slot: this.target_slot,
|
|
type: this.type
|
|
}
|
|
if (this.parentId !== undefined) copy.parentId = this.parentId
|
|
return copy
|
|
}
|
|
}
|