mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-06 08:00:05 +00:00
[API] Finalise LinkConnector design, adding reroute logic (#817)
- Splits link connect logic out of `LinkConnector` to individual `RenderLink` classes - Add support for connecting / reconnecting reroutes in various configurations - Adds support for moving existing floating links from outputs / inputs - Fixes numerous corruption issues when reconnecting reroutes / moving links - Tests in separate PR #816
This commit is contained in:
@@ -4716,7 +4716,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
||||
LGraphCanvas.link_type_colors[link.type] ||
|
||||
this.default_link_color
|
||||
|
||||
const prevReroute = reroute.parentId == null ? undefined : graph.reroutes.get(reroute.parentId)
|
||||
const prevReroute = graph.getReroute(reroute.parentId)
|
||||
const rerouteStartPos = prevReroute?.pos ?? startPos
|
||||
reroute.calculateAngle(this.last_draw_time, graph, rerouteStartPos)
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type {
|
||||
CanvasColour,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
LinkNetwork,
|
||||
LinkSegment,
|
||||
Point,
|
||||
@@ -255,14 +256,11 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
?.findNextReroute(withParentId, visited)
|
||||
}
|
||||
|
||||
findSourceOutput() {
|
||||
const network = this.#network.deref()
|
||||
if (!network) return
|
||||
|
||||
findSourceOutput(): { node: LGraphNode, output: INodeOutputSlot, outputIndex: number, link: LLink } | undefined {
|
||||
const link = this.firstLink ?? this.firstFloatingLink
|
||||
if (!link) return
|
||||
|
||||
const node = network.getNodeById(link.origin_id)
|
||||
const node = this.#network.deref()?.getNodeById(link.origin_id)
|
||||
if (!node) return
|
||||
|
||||
return {
|
||||
@@ -310,6 +308,25 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves all floating links passing through this reroute.
|
||||
* @param from Filters the links by the currently connected link side.
|
||||
* @returns An array of floating links
|
||||
*/
|
||||
getFloatingLinks(from: "input" | "output"): LLink[] | undefined {
|
||||
const floatingLinks = this.#network.deref()?.floatingLinks
|
||||
if (!floatingLinks) return
|
||||
|
||||
const idProp = from === "input" ? "origin_id" : "target_id"
|
||||
const out: LLink[] = []
|
||||
|
||||
for (const linkId of this.floatingLinkIds) {
|
||||
const link = floatingLinks.get(linkId)
|
||||
if (link?.[idProp] === -1) out.push(link)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
/** @inheritdoc */
|
||||
move(deltaX: number, deltaY: number) {
|
||||
this.#pos[0] += deltaX
|
||||
@@ -326,6 +343,43 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
||||
return true
|
||||
}
|
||||
|
||||
removeAllFloatingLinks() {
|
||||
for (const linkId of this.floatingLinkIds) {
|
||||
this.removeFloatingLink(linkId)
|
||||
}
|
||||
}
|
||||
|
||||
removeFloatingLink(linkId: LinkId) {
|
||||
const network = this.#network.deref()
|
||||
if (!network) return
|
||||
|
||||
const floatingLink = network.floatingLinks.get(linkId)
|
||||
if (!floatingLink) {
|
||||
console.warn(`[Reroute.removeFloatingLink] Floating link not found: ${linkId}, ignoring and discarding ID.`)
|
||||
this.floatingLinkIds.delete(linkId)
|
||||
return
|
||||
}
|
||||
|
||||
network.removeFloatingLink(floatingLink)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a link or floating link from this reroute, by matching link object instance equality.
|
||||
* @param link The link to remove.
|
||||
* @remarks Does not remove the link from the network.
|
||||
*/
|
||||
removeLink(link: LLink) {
|
||||
const network = this.#network.deref()
|
||||
if (!network) return
|
||||
|
||||
const floatingLink = network.floatingLinks.get(link.id)
|
||||
if (link === floatingLink) {
|
||||
this.floatingLinkIds.delete(link.id)
|
||||
} else {
|
||||
this.linkIds.delete(link.id)
|
||||
}
|
||||
}
|
||||
|
||||
remove() {
|
||||
const network = this.#network.deref()
|
||||
if (!network) return
|
||||
|
||||
157
src/canvas/FloatingRenderLink.ts
Normal file
157
src/canvas/FloatingRenderLink.ts
Normal file
@@ -0,0 +1,157 @@
|
||||
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 a floating link that is currently being dragged from one slot to another.
|
||||
*
|
||||
* This is a heavier, but short-lived convenience data structure. All refs to FloatingRenderLinks 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 FloatingRenderLink implements RenderLink {
|
||||
readonly node: LGraphNode
|
||||
readonly fromSlot: INodeOutputSlot | INodeInputSlot
|
||||
readonly fromPos: Point
|
||||
readonly fromDirection: LinkDirection
|
||||
readonly fromSlotIndex: number
|
||||
|
||||
readonly outputNodeId: NodeId = -1
|
||||
readonly outputNode?: LGraphNode
|
||||
readonly outputSlot?: INodeOutputSlot
|
||||
readonly outputIndex: number = -1
|
||||
readonly outputPos?: Point
|
||||
|
||||
readonly inputNodeId: NodeId = -1
|
||||
readonly inputNode?: LGraphNode
|
||||
readonly inputSlot?: INodeInputSlot
|
||||
readonly inputIndex: number = -1
|
||||
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
|
||||
|
||||
if (outputNodeId !== -1) {
|
||||
// Output connected
|
||||
const outputNode = network.getNodeById(outputNodeId) ?? undefined
|
||||
if (!outputNode) throw new Error(`Creating DraggingRenderLink for link [${link.id}] failed: Output node [${outputNodeId}] not found.`)
|
||||
|
||||
const outputSlot = outputNode?.outputs.at(outputIndex)
|
||||
if (!outputSlot) throw new Error(`Creating DraggingRenderLink 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)
|
||||
|
||||
// RenderLink props
|
||||
this.node = outputNode
|
||||
this.fromSlot = outputSlot
|
||||
this.fromPos = fromReroute?.pos ?? this.outputPos
|
||||
this.fromDirection = LinkDirection.LEFT
|
||||
this.dragDirection = LinkDirection.RIGHT
|
||||
this.fromSlotIndex = outputIndex
|
||||
} else {
|
||||
// Input connected
|
||||
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 = inputNode
|
||||
this.fromSlot = inputSlot
|
||||
this.fromDirection = LinkDirection.RIGHT
|
||||
this.fromSlotIndex = inputIndex
|
||||
}
|
||||
this.fromPos = fromReroute.pos
|
||||
}
|
||||
|
||||
connectToInput(node: LGraphNode, input: INodeInputSlot, _events?: LinkConnectorEventTarget): void {
|
||||
const floatingLink = this.link
|
||||
floatingLink.target_id = node.id
|
||||
floatingLink.target_slot = node.inputs.indexOf(input)
|
||||
|
||||
this.fromSlot._floatingLinks?.delete(floatingLink)
|
||||
input._floatingLinks ??= new Set()
|
||||
input._floatingLinks.add(floatingLink)
|
||||
}
|
||||
|
||||
connectToOutput(node: LGraphNode, output: INodeOutputSlot, _events?: LinkConnectorEventTarget): void {
|
||||
const floatingLink = this.link
|
||||
floatingLink.origin_id = node.id
|
||||
floatingLink.origin_slot = node.outputs.indexOf(output)
|
||||
|
||||
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(
|
||||
reroute: Reroute,
|
||||
{ node: inputNode, input }: { node: LGraphNode, input: INodeInputSlot },
|
||||
events: LinkConnectorEventTarget,
|
||||
) {
|
||||
const floatingLink = this.link
|
||||
floatingLink.target_id = inputNode.id
|
||||
floatingLink.target_slot = inputNode.inputs.indexOf(input)
|
||||
|
||||
this.fromSlot._floatingLinks?.delete(floatingLink)
|
||||
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)
|
||||
}
|
||||
|
||||
connectToRerouteOutput(
|
||||
reroute: Reroute,
|
||||
outputNode: LGraphNode,
|
||||
output: INodeOutputSlot,
|
||||
events: LinkConnectorEventTarget,
|
||||
) {
|
||||
const floatingLink = this.link
|
||||
floatingLink.origin_id = outputNode.id
|
||||
floatingLink.origin_slot = outputNode.outputs.indexOf(output)
|
||||
|
||||
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}].`)
|
||||
|
||||
events.dispatch("output-moved", this)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ import { LiteGraph } from "@/litegraph"
|
||||
import { LLink } from "@/LLink"
|
||||
import { LinkDirection } from "@/types/globalEnums"
|
||||
|
||||
import { FloatingRenderLink } from "./FloatingRenderLink"
|
||||
import { MovingRenderLink } from "./MovingRenderLink"
|
||||
import { ToInputRenderLink } from "./ToInputRenderLink"
|
||||
import { ToOutputRenderLink } from "./ToOutputRenderLink"
|
||||
@@ -33,12 +34,13 @@ export interface LinkConnectorState {
|
||||
}
|
||||
|
||||
/** Discriminated union to simplify type narrowing. */
|
||||
type RenderLinkUnion = MovingRenderLink | ToInputRenderLink | ToOutputRenderLink
|
||||
type RenderLinkUnion = MovingRenderLink | FloatingRenderLink | ToInputRenderLink | ToOutputRenderLink
|
||||
|
||||
export interface LinkConnectorExport {
|
||||
renderLinks: RenderLink[]
|
||||
inputLinks: LLink[]
|
||||
outputLinks: LLink[]
|
||||
floatingLinks: LLink[]
|
||||
state: LinkConnectorState
|
||||
network: LinkNetwork
|
||||
}
|
||||
@@ -69,6 +71,8 @@ export class LinkConnector {
|
||||
readonly inputLinks: LLink[] = []
|
||||
/** Existing links that are being moved **to** a new output slot. */
|
||||
readonly outputLinks: LLink[] = []
|
||||
/** Existing floating links that are being moved to a new slot. */
|
||||
readonly floatingLinks: LLink[] = []
|
||||
|
||||
readonly hiddenReroutes: Set<Reroute> = new Set()
|
||||
|
||||
@@ -101,31 +105,51 @@ export class LinkConnector {
|
||||
const { state, inputLinks, renderLinks } = this
|
||||
|
||||
const linkId = input.link
|
||||
if (linkId == null) return
|
||||
if (linkId == null) {
|
||||
// No link connected, check for a floating link
|
||||
const floatingLink = input._floatingLinks?.values().next().value
|
||||
if (floatingLink?.parentId == null) return
|
||||
|
||||
const link = network.links.get(linkId)
|
||||
if (!link) return
|
||||
try {
|
||||
const reroute = network.reroutes.get(floatingLink.parentId)
|
||||
if (!reroute) throw new Error(`Invalid reroute id: [${floatingLink.parentId}] for floating link id: [${floatingLink.id}].`)
|
||||
|
||||
try {
|
||||
const reroute = link.parentId != null ? network.reroutes.get(link.parentId) : undefined
|
||||
const renderLink = new MovingRenderLink(network, link, "input", reroute)
|
||||
const renderLink = new FloatingRenderLink(network, floatingLink, "input", reroute)
|
||||
const mayContinue = this.events.dispatch("before-move-input", renderLink)
|
||||
if (mayContinue === false) return
|
||||
|
||||
const mayContinue = this.events.dispatch("before-move-input", renderLink)
|
||||
if (mayContinue === false) return
|
||||
renderLinks.push(renderLink)
|
||||
} catch (error) {
|
||||
console.warn(`Could not create render link for link id: [${floatingLink.id}].`, floatingLink, error)
|
||||
}
|
||||
|
||||
renderLinks.push(renderLink)
|
||||
floatingLink._dragging = true
|
||||
this.floatingLinks.push(floatingLink)
|
||||
} else {
|
||||
const link = network.links.get(linkId)
|
||||
if (!link) return
|
||||
|
||||
this.listenUntilReset("input-moved", (e) => {
|
||||
e.detail.link.disconnect(network, "output")
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Could not create render link for link id: [${link.id}].`, link, error)
|
||||
return
|
||||
try {
|
||||
const reroute = network.getReroute(link.parentId)
|
||||
const renderLink = new MovingRenderLink(network, link, "input", reroute)
|
||||
|
||||
const mayContinue = this.events.dispatch("before-move-input", renderLink)
|
||||
if (mayContinue === false) return
|
||||
|
||||
renderLinks.push(renderLink)
|
||||
|
||||
this.listenUntilReset("input-moved", (e) => {
|
||||
e.detail.link.disconnect(network, "output")
|
||||
})
|
||||
} catch (error) {
|
||||
console.warn(`Could not create render link for link id: [${link.id}].`, link, error)
|
||||
return
|
||||
}
|
||||
|
||||
link._dragging = true
|
||||
inputLinks.push(link)
|
||||
}
|
||||
|
||||
link._dragging = true
|
||||
inputLinks.push(link)
|
||||
|
||||
state.connectingTo = "input"
|
||||
state.draggingExistingLinks = true
|
||||
|
||||
@@ -137,31 +161,52 @@ export class LinkConnector {
|
||||
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||
|
||||
const { state, renderLinks } = this
|
||||
if (!output.links?.length) return
|
||||
|
||||
for (const linkId of output.links) {
|
||||
const link = network.links.get(linkId)
|
||||
if (!link) continue
|
||||
// Floating links
|
||||
if (output._floatingLinks?.size) {
|
||||
for (const floatingLink of output._floatingLinks.values()) {
|
||||
try {
|
||||
const reroute = LLink.getFirstReroute(network, floatingLink)
|
||||
if (!reroute) throw new Error(`Invalid reroute id: [${floatingLink.parentId}] for floating link id: [${floatingLink.id}].`)
|
||||
|
||||
const firstReroute = LLink.getFirstReroute(network, link)
|
||||
if (firstReroute) {
|
||||
firstReroute._dragging = true
|
||||
this.hiddenReroutes.add(firstReroute)
|
||||
} else {
|
||||
link._dragging = true
|
||||
const renderLink = new FloatingRenderLink(network, floatingLink, "output", reroute)
|
||||
const mayContinue = this.events.dispatch("before-move-output", renderLink)
|
||||
if (mayContinue === false) continue
|
||||
|
||||
renderLinks.push(renderLink)
|
||||
this.floatingLinks.push(floatingLink)
|
||||
} catch (error) {
|
||||
console.warn(`Could not create render link for link id: [${floatingLink.id}].`, floatingLink, error)
|
||||
}
|
||||
}
|
||||
this.outputLinks.push(link)
|
||||
}
|
||||
|
||||
try {
|
||||
const renderLink = new MovingRenderLink(network, link, "output", firstReroute, LinkDirection.RIGHT)
|
||||
// Normal links
|
||||
if (output.links?.length) {
|
||||
for (const linkId of output.links) {
|
||||
const link = network.links.get(linkId)
|
||||
if (!link) continue
|
||||
|
||||
const mayContinue = this.events.dispatch("before-move-output", renderLink)
|
||||
if (mayContinue === false) continue
|
||||
const firstReroute = LLink.getFirstReroute(network, link)
|
||||
if (firstReroute) {
|
||||
firstReroute._dragging = true
|
||||
this.hiddenReroutes.add(firstReroute)
|
||||
} else {
|
||||
link._dragging = true
|
||||
}
|
||||
this.outputLinks.push(link)
|
||||
|
||||
renderLinks.push(renderLink)
|
||||
} catch (error) {
|
||||
console.warn(`Could not create render link for link id: [${link.id}].`, link, error)
|
||||
continue
|
||||
try {
|
||||
const renderLink = new MovingRenderLink(network, link, "output", firstReroute, LinkDirection.RIGHT)
|
||||
|
||||
const mayContinue = this.events.dispatch("before-move-output", renderLink)
|
||||
if (mayContinue === false) continue
|
||||
|
||||
renderLinks.push(renderLink)
|
||||
} catch (error) {
|
||||
console.warn(`Could not create render link for link id: [${link.id}].`, link, error)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -332,42 +377,57 @@ export class LinkConnector {
|
||||
|
||||
// Connecting to input
|
||||
if (this.state.connectingTo === "input") {
|
||||
if (this.renderLinks.length !== 1) throw new Error(`Attempted to connect ${this.renderLinks.length} input links to a reroute.`)
|
||||
|
||||
const renderLink = this.renderLinks[0]
|
||||
|
||||
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 (!canConnectInputLinkToReroute(renderLink, input, reroute)) continue
|
||||
const maybeReroutes = reroute.getReroutes()
|
||||
if (maybeReroutes === null) throw new Error("Reroute loop detected.")
|
||||
|
||||
if (renderLink instanceof MovingRenderLink) {
|
||||
const { outputNode, outputSlot, fromReroute } = renderLink
|
||||
const originalReroutes = maybeReroutes.slice(0, -1).reverse()
|
||||
|
||||
const newLink = outputNode.connectSlots(outputSlot, inputNode, input, fromReroute?.id)
|
||||
if (newLink) this.events.dispatch("input-moved", renderLink)
|
||||
} else {
|
||||
const { node: outputNode, fromSlot, fromReroute } = renderLink
|
||||
// From reroute to reroute
|
||||
if (this.renderLinks.length === 1 && renderLink instanceof ToInputRenderLink) {
|
||||
const { node, fromSlotIndex, fromReroute } = renderLink
|
||||
const floatingOutLinks = reroute.getFloatingLinks("output")
|
||||
const floatingInLinks = reroute.getFloatingLinks("input")
|
||||
|
||||
const reroutes = reroute.getReroutes()
|
||||
if (reroutes === null) throw new Error("Reroute loop detected.")
|
||||
// Clean floating link IDs from reroutes about to be removed from the chain
|
||||
if (floatingOutLinks && floatingInLinks) {
|
||||
for (const link of floatingOutLinks) {
|
||||
link.origin_id = node.id
|
||||
link.origin_slot = fromSlotIndex
|
||||
|
||||
// Clean up reroutes
|
||||
if (reroutes) {
|
||||
for (const reroute of reroutes.slice(0, -1).reverse()) {
|
||||
if (reroute.id === fromReroute?.id) break
|
||||
for (const originalReroute of originalReroutes) {
|
||||
if (fromReroute != null && originalReroute.id === fromReroute.id) break
|
||||
|
||||
if (reroute.totalLinks === 1) reroute.remove()
|
||||
}
|
||||
originalReroute.floatingLinkIds.delete(link.id)
|
||||
}
|
||||
// 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)
|
||||
for (const link of floatingInLinks) {
|
||||
for (const originalReroute of originalReroutes) {
|
||||
if (fromReroute != null && originalReroute.id === fromReroute.id) break
|
||||
originalReroute.floatingLinkIds.delete(link.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Flat map and filter before any connections are re-created
|
||||
const better = this.renderLinks
|
||||
.flatMap(renderLink => results.map(result => ({ renderLink, result })))
|
||||
.filter(({ renderLink, result }) => renderLink.toType === "input" && canConnectInputLinkToReroute(renderLink, result.node, result.input, reroute))
|
||||
|
||||
for (const { renderLink, result } of better) {
|
||||
if (renderLink.toType !== "input") continue
|
||||
|
||||
renderLink.connectToRerouteInput(reroute, result, this.events, originalReroutes)
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
@@ -376,29 +436,12 @@ export class LinkConnector {
|
||||
if (link.toType !== "output") continue
|
||||
|
||||
const result = reroute.findSourceOutput()
|
||||
if (!result) return
|
||||
if (!result) continue
|
||||
|
||||
const { node, output } = result
|
||||
if (!isValidConnectionToOutput(link, output)) continue
|
||||
if (!isValidConnectionToOutput(link, node, output)) continue
|
||||
|
||||
if (link instanceof MovingRenderLink) {
|
||||
const { inputNode, inputSlot, fromReroute } = link
|
||||
|
||||
// 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
|
||||
link.link.parentId = reroute.id
|
||||
}
|
||||
// Use the last reroute id on the link to retain all reroutes
|
||||
node.connectSlots(output, inputNode, inputSlot, link.link.parentId)
|
||||
this.events.dispatch("output-moved", link)
|
||||
} else {
|
||||
const { node: inputNode, fromSlot } = link
|
||||
const newLink = node.connectSlots(output, inputNode, fromSlot, reroute?.id)
|
||||
this.events.dispatch("link-created", newLink)
|
||||
}
|
||||
link.connectToRerouteOutput(reroute, node, output, this.events)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -429,30 +472,9 @@ export class LinkConnector {
|
||||
const firstLink = this.renderLinks[0]
|
||||
if (!firstLink || firstLink.node === node) return
|
||||
|
||||
// Dragging output links
|
||||
if (connectingTo === "output" && this.draggingExistingLinks) {
|
||||
const output = node.findOutputByType(firstLink.fromSlot.type)?.slot
|
||||
if (!output) {
|
||||
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
|
||||
return
|
||||
}
|
||||
this.#dropOnOutput(node, output)
|
||||
return
|
||||
}
|
||||
|
||||
// Dragging input links
|
||||
if (connectingTo === "input" && this.draggingExistingLinks) {
|
||||
const input = node.findInputByType(firstLink.fromSlot.type)?.slot
|
||||
if (!input) {
|
||||
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
|
||||
return
|
||||
}
|
||||
this.#dropOnInput(node, input)
|
||||
return
|
||||
}
|
||||
|
||||
// Dropping new output link
|
||||
// Use a single type check before looping; ensures all dropped links go to the same slot
|
||||
if (connectingTo === "output") {
|
||||
// Dropping new output link
|
||||
const output = node.findOutputByType(firstLink.fromSlot.type)?.slot
|
||||
if (!output) {
|
||||
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
|
||||
@@ -460,12 +482,10 @@ export class LinkConnector {
|
||||
}
|
||||
|
||||
for (const link of this.renderLinks) {
|
||||
if ("link" in link.fromSlot) {
|
||||
node.connectSlots(output, link.node, link.fromSlot, link.fromReroute?.id)
|
||||
}
|
||||
link.connectToOutput(node, output, this.events)
|
||||
}
|
||||
// Dropping new input link
|
||||
} else if (connectingTo === "input") {
|
||||
// Dropping new input link
|
||||
const input = node.findInputByType(firstLink.fromSlot.type)?.slot
|
||||
if (!input) {
|
||||
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
|
||||
@@ -473,9 +493,7 @@ export class LinkConnector {
|
||||
}
|
||||
|
||||
for (const link of this.renderLinks) {
|
||||
if ("links" in link.fromSlot) {
|
||||
link.node.connectSlots(link.fromSlot, node, input, link.fromReroute?.id)
|
||||
}
|
||||
link.connectToInput(node, input, this.events)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -484,20 +502,7 @@ export class LinkConnector {
|
||||
for (const link of this.renderLinks) {
|
||||
if (link.toType !== "input") continue
|
||||
|
||||
if (link instanceof MovingRenderLink) {
|
||||
const { outputNode, inputSlot, outputSlot, fromReroute } = link
|
||||
// Link is already connected here
|
||||
if (inputSlot === input) continue
|
||||
|
||||
const newLink = outputNode.connectSlots(outputSlot, node, input, fromReroute?.id)
|
||||
if (newLink) this.events.dispatch("input-moved", link)
|
||||
} else {
|
||||
const { node: outputNode, fromSlot, fromReroute } = link
|
||||
if (node === outputNode) continue
|
||||
|
||||
const newLink = outputNode.connectSlots(fromSlot, node, input, fromReroute?.id)
|
||||
this.events.dispatch("link-created", newLink)
|
||||
}
|
||||
link.connectToInput(node, input, this.events)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -505,21 +510,7 @@ export class LinkConnector {
|
||||
for (const link of this.renderLinks) {
|
||||
if (link.toType !== "output") continue
|
||||
|
||||
if (link instanceof MovingRenderLink) {
|
||||
const { inputNode, inputSlot, outputSlot } = link
|
||||
// Link is already connected here
|
||||
if (outputSlot === output) continue
|
||||
|
||||
// Use the last reroute id on the link to retain all reroutes
|
||||
node.connectSlots(output, inputNode, inputSlot, link.link.parentId)
|
||||
this.events.dispatch("output-moved", link)
|
||||
} else {
|
||||
const { node: inputNode, fromSlot, fromReroute } = link
|
||||
if (inputNode) continue
|
||||
|
||||
const newLink = node.connectSlots(output, inputNode, fromSlot, fromReroute?.id)
|
||||
this.events.dispatch("link-created", newLink)
|
||||
}
|
||||
link.connectToOutput(node, output, this.events)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -533,19 +524,21 @@ export class LinkConnector {
|
||||
const results = reroute.findTargetInputs()
|
||||
if (!results?.length) return false
|
||||
|
||||
for (const { input } of results) {
|
||||
for (const { node, input } of results) {
|
||||
for (const renderLink of this.renderLinks) {
|
||||
if (renderLink.toType !== "input") continue
|
||||
if (canConnectInputLinkToReroute(renderLink, input, reroute)) return true
|
||||
if (canConnectInputLinkToReroute(renderLink, node, input, reroute)) return true
|
||||
}
|
||||
}
|
||||
} else {
|
||||
const output = reroute.findSourceOutput()?.output
|
||||
if (!output) return false
|
||||
const result = reroute.findSourceOutput()
|
||||
if (!result) return false
|
||||
|
||||
const { node, output } = result
|
||||
|
||||
for (const renderLink of this.renderLinks) {
|
||||
if (renderLink.toType !== "output") continue
|
||||
if (isValidConnectionToOutput(renderLink, output)) return true
|
||||
if (isValidConnectionToOutput(renderLink, node, output)) return true
|
||||
}
|
||||
}
|
||||
|
||||
@@ -583,6 +576,7 @@ export class LinkConnector {
|
||||
renderLinks: [...this.renderLinks],
|
||||
inputLinks: [...this.inputLinks],
|
||||
outputLinks: [...this.outputLinks],
|
||||
floatingLinks: [...this.floatingLinks],
|
||||
state: { ...this.state },
|
||||
network,
|
||||
}
|
||||
@@ -610,7 +604,7 @@ export class LinkConnector {
|
||||
reset(force = false): void {
|
||||
this.events.dispatch("reset", force)
|
||||
|
||||
const { state, outputLinks, inputLinks, hiddenReroutes, renderLinks } = this
|
||||
const { state, outputLinks, inputLinks, hiddenReroutes, renderLinks, floatingLinks } = this
|
||||
|
||||
if (!force && state.connectingTo === undefined) return
|
||||
state.connectingTo = undefined
|
||||
@@ -622,18 +616,24 @@ export class LinkConnector {
|
||||
renderLinks.length = 0
|
||||
inputLinks.length = 0
|
||||
outputLinks.length = 0
|
||||
floatingLinks.length = 0
|
||||
hiddenReroutes.clear()
|
||||
state.multi = false
|
||||
state.draggingExistingLinks = false
|
||||
}
|
||||
}
|
||||
|
||||
function isValidConnectionToOutput(link: ToOutputRenderLink | MovingRenderLink, output: INodeOutputSlot): boolean {
|
||||
function isValidConnectionToOutput(link: ToOutputRenderLink | MovingRenderLink | FloatingRenderLink, outputNode: LGraphNode, output: INodeOutputSlot): boolean {
|
||||
const { node: fromNode } = link
|
||||
|
||||
// Node cannot connect to itself
|
||||
if (fromNode === outputNode) return false
|
||||
|
||||
if (link instanceof MovingRenderLink) {
|
||||
const { inputSlot: { type }, outputSlot } = link
|
||||
const { inputSlot: { type } } = link
|
||||
|
||||
// Link is already connected here / type mismatch
|
||||
if (outputSlot === output || !LiteGraph.isValidConnection(type, output.type)) {
|
||||
if (!LiteGraph.isValidConnection(type, output.type)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
@@ -644,37 +644,33 @@ function isValidConnectionToOutput(link: ToOutputRenderLink | MovingRenderLink,
|
||||
}
|
||||
|
||||
/** Validates that a single {@link RenderLink} can be dropped on the specified reroute. */
|
||||
function canConnectInputLinkToReroute(link: ToInputRenderLink | MovingRenderLink, input: INodeInputSlot, reroute: Reroute): boolean {
|
||||
if (link instanceof MovingRenderLink) {
|
||||
const { inputSlot, outputSlot, fromReroute } = link
|
||||
function canConnectInputLinkToReroute(link: ToInputRenderLink | MovingRenderLink | FloatingRenderLink, inputNode: LGraphNode, input: INodeInputSlot, reroute: Reroute): boolean {
|
||||
const { node: fromNode, fromSlot, fromReroute } = link
|
||||
|
||||
// Link is already connected here
|
||||
if (inputSlot === input || validate(outputSlot.type, reroute, fromReroute)) {
|
||||
return false
|
||||
}
|
||||
} else {
|
||||
const { fromSlot, fromReroute } = link
|
||||
if (
|
||||
// Node cannot connect to itself
|
||||
fromNode === inputNode ||
|
||||
// Would result in no change
|
||||
fromReroute?.id === reroute.id ||
|
||||
isInvalid(fromSlot.type, reroute, fromReroute)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Connect to yourself
|
||||
if (fromReroute?.id === reroute.id || validate(fromSlot.type, reroute, fromReroute)) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Link would make no change - output to reroute
|
||||
if (
|
||||
reroute.parentId == null &&
|
||||
reroute.firstLink?.hasOrigin(link.node.id, link.fromSlotIndex)
|
||||
) {
|
||||
// Would result in no change
|
||||
if (link instanceof ToInputRenderLink) {
|
||||
if (reroute.parentId == null) {
|
||||
// Link would make no change - output to reroute
|
||||
if (reroute.firstLink?.hasOrigin(link.node.id, link.fromSlotIndex)) return false
|
||||
} else if (link.fromReroute?.id === reroute.parentId) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
|
||||
/** Checks connection type & rejects infinite loops. */
|
||||
function validate(type: ISlotType, reroute: Reroute, fromReroute?: Reroute): boolean {
|
||||
function isInvalid(type: ISlotType, reroute: Reroute, fromReroute?: Reroute): boolean {
|
||||
return Boolean(
|
||||
// Link would make no changes
|
||||
(fromReroute?.id != null && fromReroute.id === reroute.parentId) ||
|
||||
// Type mismatch
|
||||
!LiteGraph.isValidConnection(type, input.type) ||
|
||||
// Cannot connect from child to parent reroute
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
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"
|
||||
@@ -84,4 +85,69 @@ export class MovingRenderLink implements RenderLink {
|
||||
this.fromDirection = this.toType === "input" ? LinkDirection.NONE : LinkDirection.LEFT
|
||||
this.fromSlotIndex = this.toType === "input" ? outputIndex : inputIndex
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
|
||||
import type { LinkNetwork, Point } from "@/interfaces"
|
||||
import type { LGraphNode } from "@/LGraphNode"
|
||||
import type { INodeInputSlot, INodeOutputSlot, Reroute } from "@/litegraph"
|
||||
import type { INodeInputSlot, INodeOutputSlot, LLink, Reroute } from "@/litegraph"
|
||||
import type { LinkDirection } from "@/types/globalEnums"
|
||||
|
||||
export interface RenderLink {
|
||||
@@ -23,4 +24,21 @@ export interface RenderLink {
|
||||
readonly fromSlotIndex: number
|
||||
/** The reroute that the link is being connected from. */
|
||||
readonly fromReroute?: Reroute
|
||||
|
||||
connectToInput(node: LGraphNode, input: INodeInputSlot, events?: LinkConnectorEventTarget): void
|
||||
connectToOutput(node: LGraphNode, output: INodeOutputSlot, events?: LinkConnectorEventTarget): void
|
||||
|
||||
connectToRerouteInput(
|
||||
reroute: Reroute,
|
||||
{ node, input, link }: { node: LGraphNode, input: INodeInputSlot, link: LLink },
|
||||
events: LinkConnectorEventTarget,
|
||||
originalReroutes: Reroute[],
|
||||
): void
|
||||
|
||||
connectToRerouteOutput(
|
||||
reroute: Reroute,
|
||||
outputNode: LGraphNode,
|
||||
output: INodeOutputSlot,
|
||||
events: LinkConnectorEventTarget,
|
||||
): void
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { RenderLink } from "./RenderLink"
|
||||
import type { LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
|
||||
import type { LinkNetwork, Point } from "@/interfaces"
|
||||
import type { LGraphNode } from "@/LGraphNode"
|
||||
import type { INodeOutputSlot } from "@/litegraph"
|
||||
import type { INodeInputSlot, INodeOutputSlot, LLink } from "@/litegraph"
|
||||
import type { Reroute } from "@/Reroute"
|
||||
|
||||
import { LinkDirection } from "@/types/globalEnums"
|
||||
@@ -29,4 +30,53 @@ export class ToInputRenderLink implements RenderLink {
|
||||
? fromReroute.pos
|
||||
: this.node.getOutputPos(outputIndex)
|
||||
}
|
||||
|
||||
connectToInput(node: LGraphNode, input: INodeInputSlot, events: LinkConnectorEventTarget) {
|
||||
const { node: outputNode, fromSlot, fromReroute } = this
|
||||
if (node === outputNode) return
|
||||
|
||||
const newLink = outputNode.connectSlots(fromSlot, node, input, fromReroute?.id)
|
||||
events.dispatch("link-created", newLink)
|
||||
}
|
||||
|
||||
connectToRerouteInput(
|
||||
reroute: Reroute,
|
||||
{
|
||||
node: inputNode,
|
||||
input,
|
||||
link: existingLink,
|
||||
}: { node: LGraphNode, input: INodeInputSlot, link: LLink },
|
||||
events: LinkConnectorEventTarget,
|
||||
originalReroutes: Reroute[],
|
||||
) {
|
||||
const { node: outputNode, fromSlot, fromReroute } = this
|
||||
|
||||
// Check before creating new link overwrites the value
|
||||
const floatingTerminus = fromReroute?.floating?.slotType === "output"
|
||||
|
||||
// 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, existingLink.parentId)
|
||||
|
||||
// Connecting from the final reroute of a floating reroute chain
|
||||
if (floatingTerminus) fromReroute.removeAllFloatingLinks()
|
||||
|
||||
// Clean up reroutes
|
||||
for (const reroute of originalReroutes) {
|
||||
if (reroute.id === fromReroute?.id) break
|
||||
|
||||
reroute.removeLink(existingLink)
|
||||
if (reroute.totalLinks === 0) reroute.remove()
|
||||
}
|
||||
events.dispatch("link-created", newLink)
|
||||
}
|
||||
|
||||
connectToOutput() {
|
||||
throw new Error("ToInputRenderLink cannot connect to an output.")
|
||||
}
|
||||
|
||||
connectToRerouteOutput() {
|
||||
throw new Error("ToInputRenderLink cannot connect to an output.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
import type { RenderLink } from "./RenderLink"
|
||||
import type { LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
|
||||
import type { LinkNetwork, Point } from "@/interfaces"
|
||||
import type { LGraphNode } from "@/LGraphNode"
|
||||
import type { INodeInputSlot } from "@/litegraph"
|
||||
import type { INodeInputSlot, INodeOutputSlot } from "@/litegraph"
|
||||
import type { Reroute } from "@/Reroute"
|
||||
|
||||
import { LinkDirection } from "@/types/globalEnums"
|
||||
@@ -29,4 +30,31 @@ export class ToOutputRenderLink implements RenderLink {
|
||||
? fromReroute.pos
|
||||
: this.node.getInputPos(inputIndex)
|
||||
}
|
||||
|
||||
connectToOutput(node: LGraphNode, output: INodeOutputSlot, events: LinkConnectorEventTarget) {
|
||||
const { node: inputNode, fromSlot, fromReroute } = this
|
||||
if (inputNode) return
|
||||
|
||||
const newLink = node.connectSlots(output, inputNode, fromSlot, fromReroute?.id)
|
||||
events.dispatch("link-created", newLink)
|
||||
}
|
||||
|
||||
connectToRerouteOutput(
|
||||
reroute: Reroute,
|
||||
outputNode: LGraphNode,
|
||||
output: INodeOutputSlot,
|
||||
events: LinkConnectorEventTarget,
|
||||
): void {
|
||||
const { node: inputNode, fromSlot } = this
|
||||
const newLink = outputNode.connectSlots(output, inputNode, fromSlot, reroute?.id)
|
||||
events.dispatch("link-created", newLink)
|
||||
}
|
||||
|
||||
connectToInput() {
|
||||
throw new Error("ToOutputRenderLink cannot connect to an input.")
|
||||
}
|
||||
|
||||
connectToRerouteInput() {
|
||||
throw new Error("ToOutputRenderLink cannot connect to an input.")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import type { FloatingRenderLink } from "@/canvas/FloatingRenderLink"
|
||||
import type { MovingRenderLink } from "@/canvas/MovingRenderLink"
|
||||
import type { RenderLink } from "@/canvas/RenderLink"
|
||||
import type { ToInputRenderLink } from "@/canvas/ToInputRenderLink"
|
||||
@@ -19,11 +20,11 @@ export interface LinkConnectorEventMap {
|
||||
event: CanvasPointerEvent
|
||||
}
|
||||
|
||||
"before-move-input": MovingRenderLink
|
||||
"before-move-output": MovingRenderLink
|
||||
"before-move-input": MovingRenderLink | FloatingRenderLink
|
||||
"before-move-output": MovingRenderLink | FloatingRenderLink
|
||||
|
||||
"input-moved": MovingRenderLink
|
||||
"output-moved": MovingRenderLink
|
||||
"input-moved": MovingRenderLink | FloatingRenderLink
|
||||
"output-moved": MovingRenderLink | FloatingRenderLink
|
||||
|
||||
"link-created": LLink | null | undefined
|
||||
|
||||
|
||||
@@ -5,54 +5,43 @@ import type { ISlotType } from "@/interfaces"
|
||||
import { describe, expect, test as baseTest, vi } from "vitest"
|
||||
|
||||
import { LinkConnector } from "@/canvas/LinkConnector"
|
||||
import { ToInputRenderLink } from "@/canvas/ToInputRenderLink"
|
||||
import { LGraph } from "@/LGraph"
|
||||
import { LGraphNode } from "@/LGraphNode"
|
||||
import { LLink } from "@/LLink"
|
||||
import { Reroute } from "@/Reroute"
|
||||
import { Reroute, type RerouteId } from "@/Reroute"
|
||||
import { LinkDirection } from "@/types/globalEnums"
|
||||
|
||||
type TestNetwork = LinkNetwork & { add(node: LGraphNode): void }
|
||||
|
||||
interface TestContext {
|
||||
network: TestNetwork
|
||||
network: LinkNetwork & { add(node: LGraphNode): void }
|
||||
connector: LinkConnector
|
||||
setConnectingLinks: ReturnType<typeof vi.fn>
|
||||
createTestNode: (id: number, slotType?: ISlotType) => LGraphNode
|
||||
createTestLink: (id: number, sourceId: number, targetId: number, slotType?: ISlotType) => LLink
|
||||
}
|
||||
|
||||
function createNetwork(): TestNetwork {
|
||||
const graph = new LGraph()
|
||||
const floatingLinks = new Map<number, LLink>()
|
||||
return {
|
||||
links: new Map<number, LLink>(),
|
||||
reroutes: new Map<number, Reroute>(),
|
||||
floatingLinks,
|
||||
getNodeById: (id: number) => graph.getNodeById(id),
|
||||
addFloatingLink: (link: LLink) => {
|
||||
floatingLinks.set(link.id, link)
|
||||
return link
|
||||
},
|
||||
removeReroute: () => true,
|
||||
add: (node: LGraphNode) => graph.add(node),
|
||||
}
|
||||
}
|
||||
|
||||
function createTestNode(id: number): LGraphNode {
|
||||
const node = new LGraphNode("test")
|
||||
node.id = id
|
||||
return node
|
||||
}
|
||||
|
||||
function createTestLink(id: number, sourceId: number, targetId: number, slotType: ISlotType = "number"): LLink {
|
||||
return new LLink(id, slotType, sourceId, 0, targetId, 0)
|
||||
}
|
||||
|
||||
const test = baseTest.extend<TestContext>({
|
||||
network: async ({}, use) => {
|
||||
const network = createNetwork()
|
||||
await use(network)
|
||||
const graph = new LGraph()
|
||||
const floatingLinks = new Map<number, LLink>()
|
||||
const reroutes = new Map<number, Reroute>()
|
||||
|
||||
await use({
|
||||
links: new Map<number, LLink>(),
|
||||
reroutes,
|
||||
floatingLinks,
|
||||
getNodeById: (id: number) => graph.getNodeById(id),
|
||||
addFloatingLink: (link: LLink) => {
|
||||
floatingLinks.set(link.id, link)
|
||||
return link
|
||||
},
|
||||
removeFloatingLink: (link: LLink) => floatingLinks.delete(link.id),
|
||||
getReroute: ((id: RerouteId | null | undefined) => id == null ? undefined : reroutes.get(id)) as LinkNetwork["getReroute"],
|
||||
removeReroute: (id: number) => reroutes.delete(id),
|
||||
add: (node: LGraphNode) => graph.add(node),
|
||||
})
|
||||
},
|
||||
|
||||
setConnectingLinks: async ({}, use: (mock: ReturnType<typeof vi.fn>) => Promise<void>) => {
|
||||
const mock = vi.fn()
|
||||
await use(mock)
|
||||
@@ -61,11 +50,26 @@ const test = baseTest.extend<TestContext>({
|
||||
const connector = new LinkConnector(setConnectingLinks)
|
||||
await use(connector)
|
||||
},
|
||||
createTestNode: async ({}, use) => {
|
||||
await use(createTestNode)
|
||||
|
||||
createTestNode: async ({ network }, use) => {
|
||||
await use((id: number): LGraphNode => {
|
||||
const node = new LGraphNode("test")
|
||||
node.id = id
|
||||
network.add(node)
|
||||
return node
|
||||
})
|
||||
},
|
||||
createTestLink: async ({}, use) => {
|
||||
await use(createTestLink)
|
||||
createTestLink: async ({ network }, use) => {
|
||||
await use((
|
||||
id: number,
|
||||
sourceId: number,
|
||||
targetId: number,
|
||||
slotType: ISlotType = "number",
|
||||
): LLink => {
|
||||
const link = new LLink(id, slotType, sourceId, 0, targetId, 0)
|
||||
network.links.set(link.id, link)
|
||||
return link
|
||||
})
|
||||
},
|
||||
})
|
||||
|
||||
@@ -90,8 +94,6 @@ describe("LinkConnector", () => {
|
||||
const slotType: ISlotType = "number"
|
||||
sourceNode.addOutput("out", slotType)
|
||||
targetNode.addInput("in", slotType)
|
||||
network.add(sourceNode)
|
||||
network.add(targetNode)
|
||||
|
||||
const link = new LLink(1, slotType, 1, 0, 2, 0)
|
||||
network.links.set(link.id, link)
|
||||
@@ -122,8 +124,6 @@ describe("LinkConnector", () => {
|
||||
const slotType: ISlotType = "number"
|
||||
sourceNode.addOutput("out", slotType)
|
||||
targetNode.addInput("in", slotType)
|
||||
network.add(sourceNode)
|
||||
network.add(targetNode)
|
||||
|
||||
const link = new LLink(1, slotType, 1, 0, 2, 0)
|
||||
network.links.set(link.id, link)
|
||||
@@ -173,6 +173,36 @@ describe("LinkConnector", () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe("Dragging from reroutes", () => {
|
||||
test("should handle dragging from reroutes", ({ network, connector, createTestNode, createTestLink }) => {
|
||||
const originNode = createTestNode(1)
|
||||
const targetNode = createTestNode(2)
|
||||
|
||||
const output = originNode.addOutput("out", "number")
|
||||
targetNode.addInput("in", "number")
|
||||
|
||||
const link = createTestLink(1, 1, 2)
|
||||
const reroute = new Reroute(1, network, [0, 0], undefined, [link.id])
|
||||
network.reroutes.set(reroute.id, reroute)
|
||||
link.parentId = reroute.id
|
||||
|
||||
connector.dragFromReroute(network, reroute)
|
||||
|
||||
expect(connector.state.connectingTo).toBe("input")
|
||||
expect(connector.state.draggingExistingLinks).toBe(false)
|
||||
expect(connector.renderLinks.length).toBe(1)
|
||||
|
||||
const renderLink = connector.renderLinks[0]
|
||||
expect(renderLink instanceof ToInputRenderLink).toBe(true)
|
||||
expect(renderLink.toType).toEqual("input")
|
||||
expect(renderLink.node).toEqual(originNode)
|
||||
expect(renderLink.fromSlot).toEqual(output)
|
||||
expect(renderLink.fromReroute).toEqual(reroute)
|
||||
expect(renderLink.fromDirection).toEqual(LinkDirection.NONE)
|
||||
expect(renderLink.network).toEqual(network)
|
||||
})
|
||||
})
|
||||
|
||||
describe("Reset", () => {
|
||||
test("should reset state and clear links", ({ network, connector }) => {
|
||||
connector.state.connectingTo = "input"
|
||||
|
||||
Reference in New Issue
Block a user