[Refactor] Split moving links (#839)

Splits moving links out to separate input and output classes.
This commit is contained in:
filtered
2025-03-24 09:11:08 +11:00
committed by GitHub
parent 05587d8a19
commit 7b8f01f546
9 changed files with 267 additions and 191 deletions

View File

@@ -131,8 +131,6 @@ export class FloatingRenderLink implements RenderLink {
this.fromSlot._floatingLinks?.delete(floatingLink)
output._floatingLinks ??= new Set()
output._floatingLinks.add(floatingLink)
console.debug(`Set origin_id:origin_slot [${floatingLink.origin_id}:${floatingLink.origin_slot}].`)
}
connectToRerouteInput(
@@ -148,8 +146,6 @@ export class FloatingRenderLink implements RenderLink {
input._floatingLinks ??= new Set()
input._floatingLinks.add(floatingLink)
console.debug(`Set target_id:target_slot [${floatingLink.target_id}:${floatingLink.target_slot}].`)
events.dispatch("input-moved", this)
}
@@ -167,8 +163,6 @@ export class FloatingRenderLink implements RenderLink {
output._floatingLinks ??= new Set()
output._floatingLinks.add(floatingLink)
console.debug(`Set origin_id:origin_slot [${floatingLink.origin_id}:${floatingLink.origin_slot}].`)
events.dispatch("output-moved", this)
}
}

View File

@@ -11,7 +11,9 @@ import { LLink } from "@/LLink"
import { LinkDirection } from "@/types/globalEnums"
import { FloatingRenderLink } from "./FloatingRenderLink"
import { MovingRenderLink } from "./MovingRenderLink"
import { MovingInputLink } from "./MovingInputLink"
import { MovingLinkBase } from "./MovingLinkBase"
import { MovingOutputLink } from "./MovingOutputLink"
import { ToInputRenderLink } from "./ToInputRenderLink"
import { ToOutputRenderLink } from "./ToOutputRenderLink"
@@ -33,7 +35,7 @@ export interface LinkConnectorState {
}
/** Discriminated union to simplify type narrowing. */
type RenderLinkUnion = MovingRenderLink | FloatingRenderLink | ToInputRenderLink | ToOutputRenderLink
type RenderLinkUnion = MovingInputLink | MovingOutputLink | FloatingRenderLink | ToInputRenderLink | ToOutputRenderLink
export interface LinkConnectorExport {
renderLinks: RenderLink[]
@@ -130,7 +132,7 @@ export class LinkConnector {
try {
const reroute = network.getReroute(link.parentId)
const renderLink = new MovingRenderLink(network, link, "input", reroute)
const renderLink = new MovingInputLink(network, link, reroute)
const mayContinue = this.events.dispatch("before-move-input", renderLink)
if (mayContinue === false) return
@@ -196,7 +198,7 @@ export class LinkConnector {
this.outputLinks.push(link)
try {
const renderLink = new MovingRenderLink(network, link, "output", firstReroute, LinkDirection.RIGHT)
const renderLink = new MovingOutputLink(network, link, firstReroute, LinkDirection.RIGHT)
const mayContinue = this.events.dispatch("before-move-output", renderLink)
if (mayContinue === false) continue
@@ -449,7 +451,7 @@ export class LinkConnector {
// For external event only.
if (this.state.connectingTo === "input") {
for (const link of this.renderLinks) {
if (link instanceof MovingRenderLink) {
if (link instanceof MovingInputLink) {
link.inputNode.disconnectInput(link.inputIndex, true)
}
}
@@ -505,7 +507,7 @@ export class LinkConnector {
#dropOnOutput(node: LGraphNode, output: INodeOutputSlot): void {
for (const link of this.renderLinks) {
if (!link.canConnectToOutput(node, output)) {
if (link instanceof MovingRenderLink && link.link.parentId !== undefined) {
if (link instanceof MovingOutputLink && link.link.parentId !== undefined) {
// Reconnect link without reroutes
link.outputNode.connectSlots(link.outputSlot, link.inputNode, link.inputSlot, undefined!)
}
@@ -560,7 +562,7 @@ export class LinkConnector {
const input = fromSlotIsInput ? link.fromSlot as INodeInputSlot : null
const output = fromSlotIsInput ? null : link.fromSlot as INodeOutputSlot
const afterRerouteId = link instanceof MovingRenderLink ? link.link?.parentId : link.fromReroute?.id
const afterRerouteId = link instanceof MovingLinkBase ? link.link?.parentId : link.fromReroute?.id
return {
node: link.node,
@@ -634,7 +636,7 @@ export class LinkConnector {
/** Validates that a single {@link RenderLink} can be dropped on the specified reroute. */
function canConnectInputLinkToReroute(
link: ToInputRenderLink | MovingRenderLink | FloatingRenderLink,
link: ToInputRenderLink | MovingInputLink | FloatingRenderLink,
inputNode: LGraphNode,
input: INodeInputSlot,
reroute: Reroute,

View File

@@ -0,0 +1,78 @@
import type { LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/interfaces"
import type { LGraphNode } from "@/LGraphNode"
import type { LLink } from "@/LLink"
import type { Reroute } from "@/Reroute"
import { LinkDirection } from "@/types/globalEnums"
import { MovingLinkBase } from "./MovingLinkBase"
export class MovingInputLink extends MovingLinkBase {
readonly toType = "input"
readonly node: LGraphNode
readonly fromSlot: INodeOutputSlot
readonly fromPos: Point
readonly fromDirection: LinkDirection
readonly fromSlotIndex: number
constructor(network: LinkNetwork, link: LLink, fromReroute?: Reroute, dragDirection: LinkDirection = LinkDirection.CENTER) {
super(network, link, "input", fromReroute, dragDirection)
this.node = this.outputNode
this.fromSlot = this.outputSlot
this.fromPos = fromReroute?.pos ?? this.outputPos
this.fromDirection = LinkDirection.NONE
this.fromSlotIndex = this.outputIndex
}
canConnectToInput(inputNode: LGraphNode, input: INodeInputSlot): boolean {
return this.node.canConnectTo(inputNode, input, this.outputSlot)
}
canConnectToOutput(): false {
return false
}
canConnectToReroute(reroute: Reroute): boolean {
return reroute.origin_id !== this.inputNode.id
}
connectToInput(inputNode: LGraphNode, input: INodeInputSlot, events: LinkConnectorEventTarget): LLink | null | undefined {
if (input === this.inputSlot) return
const link = this.outputNode.connectSlots(this.outputSlot, inputNode, input, this.fromReroute?.id)
if (link) events.dispatch("input-moved", this)
return link
}
connectToOutput(): never {
throw new Error("MovingInputLink cannot connect to an output.")
}
connectToRerouteInput(
reroute: Reroute,
{ node: inputNode, input, link: existingLink }: { node: LGraphNode, input: INodeInputSlot, link: LLink },
events: LinkConnectorEventTarget,
originalReroutes: Reroute[],
): void {
const { outputNode, outputSlot, fromReroute } = this
// Clean up reroutes
for (const reroute of originalReroutes) {
if (reroute.id === this.link.parentId) break
if (reroute.totalLinks === 1) reroute.remove()
}
// Set the parentId of the reroute we dropped on, to the reroute we dragged from
reroute.parentId = fromReroute?.id
const newLink = outputNode.connectSlots(outputSlot, inputNode, input, existingLink.parentId)
if (newLink) events.dispatch("input-moved", this)
}
connectToRerouteOutput(): never {
throw new Error("MovingInputLink cannot connect to an output.")
}
}

View File

@@ -0,0 +1,86 @@
import type { RenderLink } from "./RenderLink"
import type { LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/interfaces"
import type { LGraphNode, NodeId } from "@/LGraphNode"
import type { LLink } from "@/LLink"
import type { Reroute } from "@/Reroute"
import { LinkDirection } from "@/types/globalEnums"
/**
* Represents an existing link that is currently being dragged by the user from one slot to another.
*
* This is a heavier, but short-lived convenience data structure.
* All refs to {@link MovingInputLink} and {@link MovingOutputLink} should be discarded on drop.
* @remarks
* At time of writing, Litegraph is using several different styles and methods to handle link dragging.
*
* Once the library has undergone more substantial changes to the way links are managed,
* many properties of this class will be superfluous and removable.
*/
export abstract class MovingLinkBase implements RenderLink {
abstract readonly node: LGraphNode
abstract readonly fromSlot: INodeOutputSlot | INodeInputSlot
abstract readonly fromPos: Point
abstract readonly fromDirection: LinkDirection
abstract readonly fromSlotIndex: number
readonly outputNodeId: NodeId
readonly outputNode: LGraphNode
readonly outputSlot: INodeOutputSlot
readonly outputIndex: number
readonly outputPos: Point
readonly inputNodeId: NodeId
readonly inputNode: LGraphNode
readonly inputSlot: INodeInputSlot
readonly inputIndex: number
readonly inputPos: Point
constructor(
readonly network: LinkNetwork,
readonly link: LLink,
readonly toType: "input" | "output",
readonly fromReroute?: Reroute,
readonly dragDirection: LinkDirection = LinkDirection.CENTER,
) {
const {
origin_id: outputNodeId,
target_id: inputNodeId,
origin_slot: outputIndex,
target_slot: inputIndex,
} = link
// Store output info
const outputNode = network.getNodeById(outputNodeId) ?? undefined
if (!outputNode) throw new Error(`Creating MovingRenderLink for link [${link.id}] failed: Output node [${outputNodeId}] not found.`)
const outputSlot = outputNode.outputs.at(outputIndex)
if (!outputSlot) throw new Error(`Creating MovingRenderLink for link [${link.id}] failed: Output slot [${outputIndex}] not found.`)
this.outputNodeId = outputNodeId
this.outputNode = outputNode
this.outputSlot = outputSlot
this.outputIndex = outputIndex
this.outputPos = outputNode.getOutputPos(outputIndex)
// Store input info
const inputNode = network.getNodeById(inputNodeId) ?? undefined
if (!inputNode) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Input node [${inputNodeId}] not found.`)
const inputSlot = inputNode.inputs.at(inputIndex)
if (!inputSlot) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Input slot [${inputIndex}] not found.`)
this.inputNodeId = inputNodeId
this.inputNode = inputNode
this.inputSlot = inputSlot
this.inputIndex = inputIndex
this.inputPos = inputNode.getInputPos(inputIndex)
}
abstract connectToInput(node: LGraphNode, input: INodeInputSlot, events?: LinkConnectorEventTarget): void
abstract connectToOutput(node: LGraphNode, output: INodeOutputSlot, events?: LinkConnectorEventTarget): void
abstract connectToRerouteInput(reroute: Reroute, { node, input, link }: { node: LGraphNode, input: INodeInputSlot, link: LLink }, events: LinkConnectorEventTarget, originalReroutes: Reroute[]): void
abstract connectToRerouteOutput(reroute: Reroute, outputNode: LGraphNode, output: INodeOutputSlot, events: LinkConnectorEventTarget): void
}

View File

@@ -0,0 +1,85 @@
import type { LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
import type { INodeInputSlot, INodeOutputSlot, LinkNetwork, Point } from "@/interfaces"
import type { LGraphNode } from "@/LGraphNode"
import type { LLink } from "@/LLink"
import type { Reroute } from "@/Reroute"
import { LinkDirection } from "@/types/globalEnums"
import { MovingLinkBase } from "./MovingLinkBase"
export class MovingOutputLink extends MovingLinkBase {
readonly toType = "output"
readonly node: LGraphNode
readonly fromSlot: INodeInputSlot
readonly fromPos: Point
readonly fromDirection: LinkDirection
readonly fromSlotIndex: number
constructor(network: LinkNetwork, link: LLink, fromReroute?: Reroute, dragDirection: LinkDirection = LinkDirection.CENTER) {
super(network, link, "output", fromReroute, dragDirection)
this.node = this.inputNode
this.fromSlot = this.inputSlot
this.fromPos = fromReroute?.pos ?? this.inputPos
this.fromDirection = LinkDirection.LEFT
this.fromSlotIndex = this.inputIndex
}
canConnectToInput(): false {
return false
}
canConnectToOutput(outputNode: LGraphNode, output: INodeOutputSlot): boolean {
return outputNode.canConnectTo(this.node, this.inputSlot, output)
}
canConnectToReroute(reroute: Reroute): boolean {
return reroute.origin_id !== this.outputNode.id
}
connectToInput(): never {
throw new Error("MovingOutputLink cannot connect to an input.")
}
connectToOutput(outputNode: LGraphNode, output: INodeOutputSlot, events: LinkConnectorEventTarget): LLink | null | undefined {
if (output === this.outputSlot) return
const link = outputNode.connectSlots(output, this.inputNode, this.inputSlot, this.link.parentId)
if (link) events.dispatch("output-moved", this)
return link
}
connectToRerouteInput(): never {
throw new Error("MovingOutputLink cannot connect to an input.")
}
connectToRerouteOutput(
reroute: Reroute,
outputNode: LGraphNode,
output: INodeOutputSlot,
events: LinkConnectorEventTarget,
): void {
// Moving output side of links
const { inputNode, inputSlot, fromReroute } = this
// Creating a new link removes floating prop - check before connecting
const floatingTerminus = reroute?.floating?.slotType === "output"
// Connect the first reroute of the link being dragged to the reroute being dropped on
if (fromReroute) {
fromReroute.parentId = reroute.id
} else {
// If there are no reroutes, directly connect the link
this.link.parentId = reroute.id
}
// Use the last reroute id on the link to retain all reroutes
outputNode.connectSlots(output, inputNode, inputSlot, this.link.parentId)
// Connecting from the final reroute of a floating reroute chain
if (floatingTerminus) reroute.removeAllFloatingLinks()
events.dispatch("output-moved", this)
}
}

View File

@@ -1,170 +0,0 @@
import type { RenderLink } from "./RenderLink"
import type { LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
import type { INodeOutputSlot, LinkNetwork } from "@/interfaces"
import type { INodeInputSlot } from "@/interfaces"
import type { Point } from "@/interfaces"
import type { LGraphNode, NodeId } from "@/LGraphNode"
import type { LLink } from "@/LLink"
import type { Reroute } from "@/Reroute"
import { LinkDirection } from "@/types/globalEnums"
/**
* Represents an existing link that is currently being dragged by the user from one slot to another.
*
* This is a heavier, but short-lived convenience data structure. All refs to MovingRenderLinks should be discarded on drop.
* @remarks
* At time of writing, Litegraph is using several different styles and methods to handle link dragging.
*
* Once the library has undergone more substantial changes to the way links are managed,
* many properties of this class will be superfluous and removable.
*/
export class MovingRenderLink implements RenderLink {
readonly node: LGraphNode
readonly fromSlot: INodeOutputSlot | INodeInputSlot
readonly fromPos: Point
readonly fromDirection: LinkDirection
readonly fromSlotIndex: number
readonly outputNodeId: NodeId
readonly outputNode: LGraphNode
readonly outputSlot: INodeOutputSlot
readonly outputIndex: number
readonly outputPos: Point
readonly inputNodeId: NodeId
readonly inputNode: LGraphNode
readonly inputSlot: INodeInputSlot
readonly inputIndex: number
readonly inputPos: Point
constructor(
readonly network: LinkNetwork,
readonly link: LLink,
readonly toType: "input" | "output",
readonly fromReroute?: Reroute,
readonly dragDirection: LinkDirection = LinkDirection.CENTER,
) {
const {
origin_id: outputNodeId,
target_id: inputNodeId,
origin_slot: outputIndex,
target_slot: inputIndex,
} = link
// Store output info
const outputNode = network.getNodeById(outputNodeId) ?? undefined
if (!outputNode) throw new Error(`Creating MovingRenderLink for link [${link.id}] failed: Output node [${outputNodeId}] not found.`)
const outputSlot = outputNode.outputs.at(outputIndex)
if (!outputSlot) throw new Error(`Creating MovingRenderLink for link [${link.id}] failed: Output slot [${outputIndex}] not found.`)
this.outputNodeId = outputNodeId
this.outputNode = outputNode
this.outputSlot = outputSlot
this.outputIndex = outputIndex
this.outputPos = outputNode.getOutputPos(outputIndex)
// Store input info
const inputNode = network.getNodeById(inputNodeId) ?? undefined
if (!inputNode) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Input node [${inputNodeId}] not found.`)
const inputSlot = inputNode.inputs.at(inputIndex)
if (!inputSlot) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Input slot [${inputIndex}] not found.`)
this.inputNodeId = inputNodeId
this.inputNode = inputNode
this.inputSlot = inputSlot
this.inputIndex = inputIndex
this.inputPos = inputNode.getInputPos(inputIndex)
// RenderLink props
this.node = this.toType === "input" ? outputNode : inputNode
this.fromSlot = this.toType === "input" ? outputSlot : inputSlot
this.fromPos = fromReroute?.pos ?? (this.toType === "input" ? this.outputPos : this.inputPos)
this.fromDirection = this.toType === "input" ? LinkDirection.NONE : LinkDirection.LEFT
this.fromSlotIndex = this.toType === "input" ? outputIndex : inputIndex
}
canConnectToInput(inputNode: LGraphNode, input: INodeInputSlot): this is this {
return this.node.canConnectTo(inputNode, input, this.outputSlot)
}
canConnectToOutput(outputNode: LGraphNode, output: INodeOutputSlot): this is this {
return outputNode.canConnectTo(this.node, this.inputSlot, output)
}
canConnectToReroute(reroute: Reroute): boolean {
if (this.toType === "input") {
if (reroute.origin_id === this.inputNode.id) return false
} else {
if (reroute.origin_id === this.outputNode.id) return false
}
return true
}
connectToInput(inputNode: LGraphNode, input: INodeInputSlot, events: LinkConnectorEventTarget): LLink | null | undefined {
if (input === this.inputSlot) return
const link = this.outputNode.connectSlots(this.outputSlot, inputNode, input, this.fromReroute?.id)
if (link) events.dispatch("input-moved", this)
return link
}
connectToOutput(outputNode: LGraphNode, output: INodeOutputSlot, events: LinkConnectorEventTarget): LLink | null | undefined {
if (output === this.outputSlot) return
const link = outputNode.connectSlots(output, this.inputNode, this.inputSlot, this.link.parentId)
if (link) events.dispatch("output-moved", this)
return link
}
connectToRerouteInput(
reroute: Reroute,
{ node: inputNode, input, link: existingLink }: { node: LGraphNode, input: INodeInputSlot, link: LLink },
events: LinkConnectorEventTarget,
originalReroutes: Reroute[],
): void {
const { outputNode, outputSlot, fromReroute } = this
// Clean up reroutes
for (const reroute of originalReroutes) {
if (reroute.id === this.link.parentId) break
if (reroute.totalLinks === 1) reroute.remove()
}
// Set the parentId of the reroute we dropped on, to the reroute we dragged from
reroute.parentId = fromReroute?.id
const newLink = outputNode.connectSlots(outputSlot, inputNode, input, existingLink.parentId)
if (newLink) events.dispatch("input-moved", this)
}
connectToRerouteOutput(
reroute: Reroute,
outputNode: LGraphNode,
output: INodeOutputSlot,
events: LinkConnectorEventTarget,
): void {
// Moving output side of links
const { inputNode, inputSlot, fromReroute } = this
// Creating a new link removes floating prop - check before connecting
const floatingTerminus = reroute?.floating?.slotType === "output"
// Connect the first reroute of the link being dragged to the reroute being dropped on
if (fromReroute) {
fromReroute.parentId = reroute.id
} else {
// If there are no reroutes, directly connect the link
this.link.parentId = reroute.id
}
// Use the last reroute id on the link to retain all reroutes
outputNode.connectSlots(output, inputNode, inputSlot, this.link.parentId)
// Connecting from the final reroute of a floating reroute chain
if (floatingTerminus) reroute.removeAllFloatingLinks()
events.dispatch("output-moved", this)
}
}

View File

@@ -31,7 +31,7 @@ export class ToInputRenderLink implements RenderLink {
: this.node.getOutputPos(outputIndex)
}
canConnectToInput(inputNode: LGraphNode, input: INodeInputSlot): this is this {
canConnectToInput(inputNode: LGraphNode, input: INodeInputSlot): boolean {
return this.node.canConnectTo(inputNode, input, this.fromSlot)
}

View File

@@ -34,7 +34,7 @@ export class ToOutputRenderLink implements RenderLink {
return false
}
canConnectToOutput(outputNode: LGraphNode, output: INodeOutputSlot): this is this {
canConnectToOutput(outputNode: LGraphNode, output: INodeOutputSlot): boolean {
return this.node.canConnectTo(outputNode, this.fromSlot, output)
}

View File

@@ -1,5 +1,6 @@
import type { FloatingRenderLink } from "@/canvas/FloatingRenderLink"
import type { MovingRenderLink } from "@/canvas/MovingRenderLink"
import type { MovingInputLink } from "@/canvas/MovingInputLink"
import type { MovingOutputLink } from "@/canvas/MovingOutputLink"
import type { RenderLink } from "@/canvas/RenderLink"
import type { ToInputRenderLink } from "@/canvas/ToInputRenderLink"
import type { LGraphNode } from "@/LGraphNode"
@@ -20,11 +21,11 @@ export interface LinkConnectorEventMap {
event: CanvasPointerEvent
}
"before-move-input": MovingRenderLink | FloatingRenderLink
"before-move-output": MovingRenderLink | FloatingRenderLink
"before-move-input": MovingInputLink | FloatingRenderLink
"before-move-output": MovingOutputLink | FloatingRenderLink
"input-moved": MovingRenderLink | FloatingRenderLink
"output-moved": MovingRenderLink | FloatingRenderLink
"input-moved": MovingInputLink | FloatingRenderLink
"output-moved": MovingOutputLink | FloatingRenderLink
"link-created": LLink | null | undefined