mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
[API] Add LinkConnector - replaces connecting_links (#726)
### LinkConnector Replaces the minimal-change effort of `connecting_links` with a more reliable implementation. - Subscribable, event-based API - Uses browser-standard `e.preventDefault()` to cancel `before-*` events - Uses standard `state` POJO - can be proxied without issue - Structures code and separates concerns out of `LGraphCanvas` - Link creation calls can now be made from anywhere, without the need for a rewrite - New canvas sub-components now live in `src/canvas/` ### Rendering - Skips link segments by setting a `_dragging` bool flag on the LLink or Reroute - Moves some previously nested code to event listeners, configured in the `LGraphCanvas` constructor ### Deprecation `LGraphCanvas.connecting_links` is now deprecated and will later be removed. Until it is removed, to prevent breaking extensions it will continue to be set and cleared by a legacy callback. The contents of this property are ignored; code search revealed no exentsions actually modifying the array.
This commit is contained in:
@@ -36,6 +36,8 @@ import type {
|
|||||||
import type { ClipboardItems } from "./types/serialisation"
|
import type { ClipboardItems } from "./types/serialisation"
|
||||||
import type { IWidget } from "./types/widgets"
|
import type { IWidget } from "./types/widgets"
|
||||||
|
|
||||||
|
import { LinkConnector } from "@/canvas/LinkConnector"
|
||||||
|
|
||||||
import { isOverNodeInput, isOverNodeOutput } from "./canvas/measureSlots"
|
import { isOverNodeInput, isOverNodeOutput } from "./canvas/measureSlots"
|
||||||
import { CanvasPointer } from "./CanvasPointer"
|
import { CanvasPointer } from "./CanvasPointer"
|
||||||
import { DragAndScale } from "./DragAndScale"
|
import { DragAndScale } from "./DragAndScale"
|
||||||
@@ -43,7 +45,7 @@ import { strokeShape } from "./draw"
|
|||||||
import { NullGraphError } from "./infrastructure/NullGraphError"
|
import { NullGraphError } from "./infrastructure/NullGraphError"
|
||||||
import { LGraphGroup } from "./LGraphGroup"
|
import { LGraphGroup } from "./LGraphGroup"
|
||||||
import { LGraphNode, type NodeId, type NodeProperty } from "./LGraphNode"
|
import { LGraphNode, type NodeId, type NodeProperty } from "./LGraphNode"
|
||||||
import { LinkReleaseContextExtended, LiteGraph } from "./litegraph"
|
import { LiteGraph } from "./litegraph"
|
||||||
import { type LinkId, LLink } from "./LLink"
|
import { type LinkId, LLink } from "./LLink"
|
||||||
import {
|
import {
|
||||||
containsRect,
|
containsRect,
|
||||||
@@ -70,7 +72,7 @@ import {
|
|||||||
TitleMode,
|
TitleMode,
|
||||||
} from "./types/globalEnums"
|
} from "./types/globalEnums"
|
||||||
import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange"
|
import { alignNodes, distributeNodes, getBoundaryNodes } from "./utils/arrange"
|
||||||
import { findFirstNode, getAllNestedItems, isDraggingLink } from "./utils/collections"
|
import { findFirstNode, getAllNestedItems } from "./utils/collections"
|
||||||
import { toClass } from "./utils/type"
|
import { toClass } from "./utils/type"
|
||||||
import { WIDGET_TYPE_MAP } from "./widgets/widgetMap"
|
import { WIDGET_TYPE_MAP } from "./widgets/widgetMap"
|
||||||
|
|
||||||
@@ -448,7 +450,9 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
/** Contains all links and reroutes that were rendered. Repopulated every render cycle. */
|
/** Contains all links and reroutes that were rendered. Repopulated every render cycle. */
|
||||||
renderedPaths: Set<LinkSegment> = new Set()
|
renderedPaths: Set<LinkSegment> = new Set()
|
||||||
visible_links: LLink[]
|
visible_links: LLink[]
|
||||||
|
/** @deprecated This array is populated and cleared to support legacy extensions. The contents are ignored by Litegraph. */
|
||||||
connecting_links: ConnectingLink[] | null
|
connecting_links: ConnectingLink[] | null
|
||||||
|
linkConnector = new LinkConnector(links => this.connecting_links = links)
|
||||||
/** The viewport of this canvas. Tightly coupled with {@link ds}. */
|
/** The viewport of this canvas. Tightly coupled with {@link ds}. */
|
||||||
readonly viewport?: Rect
|
readonly viewport?: Rect
|
||||||
autoresize: boolean
|
autoresize: boolean
|
||||||
@@ -475,8 +479,6 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
node_over?: LGraphNode
|
node_over?: LGraphNode
|
||||||
node_capturing_input?: LGraphNode | null
|
node_capturing_input?: LGraphNode | null
|
||||||
highlighted_links: Dictionary<boolean> = {}
|
highlighted_links: Dictionary<boolean> = {}
|
||||||
link_over_widget?: IWidget
|
|
||||||
link_over_widget_type?: string
|
|
||||||
|
|
||||||
dirty_canvas: boolean = true
|
dirty_canvas: boolean = true
|
||||||
dirty_bgcanvas: boolean = true
|
dirty_bgcanvas: boolean = true
|
||||||
@@ -601,6 +603,54 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
|
|
||||||
this.ds = new DragAndScale()
|
this.ds = new DragAndScale()
|
||||||
this.pointer = new CanvasPointer(canvas)
|
this.pointer = new CanvasPointer(canvas)
|
||||||
|
|
||||||
|
// @deprecated Workaround: Keep until connecting_links is removed.
|
||||||
|
this.linkConnector.events.addEventListener("reset", () => {
|
||||||
|
this.connecting_links = null
|
||||||
|
})
|
||||||
|
|
||||||
|
// Dropped a link on the canvas
|
||||||
|
this.linkConnector.events.addEventListener("dropped-on-canvas", (customEvent) => {
|
||||||
|
if (!this.connecting_links) return
|
||||||
|
|
||||||
|
const e = customEvent.detail
|
||||||
|
this.emitEvent({
|
||||||
|
subType: "empty-release",
|
||||||
|
originalEvent: e,
|
||||||
|
linkReleaseContext: { links: this.connecting_links },
|
||||||
|
})
|
||||||
|
|
||||||
|
const firstLink = this.linkConnector.renderLinks[0]
|
||||||
|
|
||||||
|
// No longer in use
|
||||||
|
// add menu when releasing link in empty space
|
||||||
|
if (LiteGraph.release_link_on_empty_shows_menu) {
|
||||||
|
const linkReleaseContext = this.linkConnector.state.connectingTo === "input"
|
||||||
|
? {
|
||||||
|
node_from: firstLink.node,
|
||||||
|
slot_from: firstLink.fromSlot,
|
||||||
|
type_filter_in: firstLink.fromSlot.type,
|
||||||
|
}
|
||||||
|
: {
|
||||||
|
node_to: firstLink.node,
|
||||||
|
slot_from: firstLink.fromSlot,
|
||||||
|
type_filter_out: firstLink.fromSlot.type,
|
||||||
|
}
|
||||||
|
|
||||||
|
if ("shiftKey" in e && e.shiftKey) {
|
||||||
|
if (this.allow_searchbox) {
|
||||||
|
this.showSearchBox(e as unknown as MouseEvent, linkReleaseContext)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
if (this.linkConnector.state.connectingTo === "input") {
|
||||||
|
this.showConnectionMenu({ nodeFrom: firstLink.node, slotFrom: firstLink.fromSlot, e: e as unknown as CanvasMouseEvent })
|
||||||
|
} else {
|
||||||
|
this.showConnectionMenu({ nodeTo: firstLink.node, slotTo: firstLink.fromSlot, e: e as unknown as CanvasMouseEvent })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
// otherwise it generates ugly patterns when scaling down too much
|
// otherwise it generates ugly patterns when scaling down too much
|
||||||
this.zoom_modify_alpha = true
|
this.zoom_modify_alpha = true
|
||||||
// in range (1.01, 2.5). Less than 1 will invert the zoom direction
|
// in range (1.01, 2.5). Less than 1 will invert the zoom direction
|
||||||
@@ -1816,7 +1866,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
otherNode.mouseOver = null
|
otherNode.mouseOver = null
|
||||||
this._highlight_input = undefined
|
this._highlight_input = undefined
|
||||||
this._highlight_pos = undefined
|
this._highlight_pos = undefined
|
||||||
this.link_over_widget = undefined
|
this.linkConnector.overWidget = undefined
|
||||||
|
|
||||||
// Hover transitions
|
// Hover transitions
|
||||||
// TODO: Implement single lerp ease factor for current progress on hover in/out.
|
// TODO: Implement single lerp ease factor for current progress on hover in/out.
|
||||||
@@ -1910,7 +1960,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#processPrimaryButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
|
#processPrimaryButton(e: CanvasPointerEvent, node: LGraphNode | undefined) {
|
||||||
const { pointer, graph } = this
|
const { pointer, graph, linkConnector } = this
|
||||||
if (!graph) throw new NullGraphError()
|
if (!graph) throw new NullGraphError()
|
||||||
|
|
||||||
const x = e.canvasX
|
const x = e.canvasX
|
||||||
@@ -1983,33 +2033,16 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
const reroute = graph.getRerouteOnPos(x, y)
|
const reroute = graph.getRerouteOnPos(x, y)
|
||||||
if (reroute) {
|
if (reroute) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
// Connect new link from reroute
|
linkConnector.dragFromReroute(graph, reroute)
|
||||||
const linkId = reroute.linkIds.values().next().value
|
|
||||||
if (linkId == null) return
|
|
||||||
|
|
||||||
const link = graph._links.get(linkId)
|
pointer.onDragEnd = upEvent => linkConnector.dropLinks(graph, upEvent)
|
||||||
if (!link) return
|
pointer.finally = () => linkConnector.reset(true)
|
||||||
|
|
||||||
const outputNode = graph.getNodeById(link.origin_id)
|
|
||||||
if (!outputNode) return
|
|
||||||
|
|
||||||
const slot = link.origin_slot
|
|
||||||
const connecting: ConnectingLink = {
|
|
||||||
node: outputNode,
|
|
||||||
slot,
|
|
||||||
input: null,
|
|
||||||
pos: outputNode.getConnectionPos(false, slot),
|
|
||||||
afterRerouteId: reroute.id,
|
|
||||||
}
|
|
||||||
this.connecting_links = [connecting]
|
|
||||||
pointer.onDragStart = () => connecting.output = outputNode.outputs[slot]
|
|
||||||
// pointer.finally = () => this.connecting_links = null
|
|
||||||
|
|
||||||
this.dirty_bgcanvas = true
|
this.dirty_bgcanvas = true
|
||||||
}
|
}
|
||||||
|
|
||||||
pointer.onClick = () => this.processSelect(reroute, e)
|
pointer.onClick = () => this.processSelect(reroute, e)
|
||||||
if (!pointer.onDragStart) {
|
if (!pointer.onDragEnd) {
|
||||||
pointer.onDragStart = pointer => this.#startDraggingItems(reroute, pointer, true)
|
pointer.onDragStart = pointer => this.#startDraggingItems(reroute, pointer, true)
|
||||||
pointer.onDragEnd = e => this.#processDraggedItems(e)
|
pointer.onDragEnd = e => this.#processDraggedItems(e)
|
||||||
}
|
}
|
||||||
@@ -2036,22 +2069,10 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
this.ctx.lineWidth = lineWidth
|
this.ctx.lineWidth = lineWidth
|
||||||
|
|
||||||
if (e.shiftKey && !e.altKey) {
|
if (e.shiftKey && !e.altKey) {
|
||||||
const slot = linkSegment.origin_slot
|
linkConnector.dragFromLinkSegment(graph, linkSegment)
|
||||||
if (slot == null) return console.warn("Connecting link from corrupt link segment: `slot` null", linkSegment)
|
|
||||||
if (linkSegment.origin_id == null) return console.warn("Connecting link from corrupt link segment: `origin_id` null", linkSegment)
|
|
||||||
|
|
||||||
const originNode = graph._nodes_by_id[linkSegment.origin_id]
|
pointer.onDragEnd = upEvent => linkConnector.dropLinks(graph, upEvent)
|
||||||
|
pointer.finally = () => linkConnector.reset(true)
|
||||||
const connecting: ConnectingLink = {
|
|
||||||
node: originNode,
|
|
||||||
slot,
|
|
||||||
pos: originNode.getConnectionPos(false, slot),
|
|
||||||
}
|
|
||||||
this.connecting_links = [connecting]
|
|
||||||
if (linkSegment.parentId) connecting.afterRerouteId = linkSegment.parentId
|
|
||||||
|
|
||||||
pointer.onDragStart = () => connecting.output = originNode.outputs[slot]
|
|
||||||
// pointer.finally = () => this.connecting_links = null
|
|
||||||
|
|
||||||
return
|
return
|
||||||
} else if (this.reroutesEnabled && e.altKey && !e.shiftKey) {
|
} else if (this.reroutesEnabled && e.altKey && !e.shiftKey) {
|
||||||
@@ -2170,7 +2191,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
ctrlOrMeta: boolean,
|
ctrlOrMeta: boolean,
|
||||||
node: LGraphNode,
|
node: LGraphNode,
|
||||||
): void {
|
): void {
|
||||||
const { pointer, graph } = this
|
const { pointer, graph, linkConnector } = this
|
||||||
if (!graph) throw new NullGraphError()
|
if (!graph) throw new NullGraphError()
|
||||||
|
|
||||||
const x = e.canvasX
|
const x = e.canvasX
|
||||||
@@ -2239,46 +2260,19 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
if (isInRectangle(x, y, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) {
|
if (isInRectangle(x, y, link_pos[0] - 15, link_pos[1] - 10, 30, 20)) {
|
||||||
// Drag multiple output links
|
// Drag multiple output links
|
||||||
if (e.shiftKey && output.links?.length) {
|
if (e.shiftKey && output.links?.length) {
|
||||||
const connectingLinks: ConnectingLink[] = []
|
linkConnector.moveOutputLink(graph, output)
|
||||||
|
|
||||||
for (const linkId of output.links) {
|
pointer.onDragEnd = upEvent => linkConnector.dropLinks(graph, upEvent)
|
||||||
const link = graph._links.get(linkId)
|
pointer.finally = () => linkConnector.reset(true)
|
||||||
if (!link) continue
|
|
||||||
|
|
||||||
const slot = link.target_slot
|
|
||||||
const otherNode = graph._nodes_by_id[link.target_id]
|
|
||||||
const input = otherNode.inputs[slot]
|
|
||||||
const pos = otherNode.getConnectionPos(true, slot)
|
|
||||||
const afterRerouteId = LLink.getReroutes(graph, link).at(-1)?.id
|
|
||||||
const firstRerouteId = LLink.getReroutes(graph, link).at(0)?.id
|
|
||||||
|
|
||||||
connectingLinks.push({
|
|
||||||
node: otherNode,
|
|
||||||
slot,
|
|
||||||
input,
|
|
||||||
output: null,
|
|
||||||
pos,
|
|
||||||
direction: LinkDirection.RIGHT,
|
|
||||||
afterRerouteId,
|
|
||||||
firstRerouteId,
|
|
||||||
link,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
this.connecting_links = connectingLinks
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
output.slot_index = i
|
// New output link
|
||||||
this.connecting_links = [
|
linkConnector.dragNewFromOutput(graph, node, output)
|
||||||
{
|
|
||||||
node: node,
|
pointer.onDragEnd = upEvent => linkConnector.dropLinks(graph, upEvent)
|
||||||
slot: i,
|
pointer.finally = () => linkConnector.reset(true)
|
||||||
input: null,
|
|
||||||
output: output,
|
|
||||||
pos: link_pos,
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
if (LiteGraph.shift_click_do_break_link_from) {
|
if (LiteGraph.shift_click_do_break_link_from) {
|
||||||
if (e.shiftKey) {
|
if (e.shiftKey) {
|
||||||
@@ -2308,12 +2302,6 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
pointer.onClick = () => node.onInputClick?.(i, e)
|
pointer.onClick = () => node.onInputClick?.(i, e)
|
||||||
|
|
||||||
if (input.link !== null) {
|
if (input.link !== null) {
|
||||||
// before disconnecting
|
|
||||||
const link_info = graph._links.get(input.link)
|
|
||||||
if (!link_info) throw new TypeError("Input link ID was invalid.")
|
|
||||||
|
|
||||||
const slot = link_info.origin_slot
|
|
||||||
const linked_node = graph._nodes_by_id[link_info.origin_id]
|
|
||||||
if (
|
if (
|
||||||
LiteGraph.click_do_break_link_to ||
|
LiteGraph.click_do_break_link_to ||
|
||||||
(LiteGraph.ctrl_alt_click_do_break_link &&
|
(LiteGraph.ctrl_alt_click_do_break_link &&
|
||||||
@@ -2323,50 +2311,18 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
) {
|
) {
|
||||||
node.disconnectInput(i)
|
node.disconnectInput(i)
|
||||||
} else if (e.shiftKey || this.allow_reconnect_links) {
|
} else if (e.shiftKey || this.allow_reconnect_links) {
|
||||||
if (!linked_node) throw new TypeError("linked_node was null")
|
linkConnector.moveInputLink(graph, input)
|
||||||
|
|
||||||
const connecting: ConnectingLink = {
|
|
||||||
node: linked_node,
|
|
||||||
slot,
|
|
||||||
output: linked_node.outputs[slot],
|
|
||||||
pos: linked_node.getConnectionPos(false, slot),
|
|
||||||
afterRerouteId: link_info.parentId,
|
|
||||||
link: link_info,
|
|
||||||
}
|
|
||||||
const connectingLinks = [connecting]
|
|
||||||
this.connecting_links = connectingLinks
|
|
||||||
|
|
||||||
pointer.onDragStart = () => {
|
|
||||||
connecting.output = linked_node.outputs[slot]
|
|
||||||
}
|
|
||||||
pointer.onDragEnd = (upEvent) => {
|
|
||||||
const shouldDisconnect = this.#processConnectingLinks(upEvent, connectingLinks)
|
|
||||||
|
|
||||||
if (shouldDisconnect && this.allow_reconnect_links && !LiteGraph.click_do_break_link_to) {
|
|
||||||
node.disconnectInput(i)
|
|
||||||
}
|
|
||||||
connecting.output = linked_node.outputs[slot]
|
|
||||||
this.connecting_links = null
|
|
||||||
}
|
|
||||||
|
|
||||||
this.dirty_bgcanvas = true
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!pointer.onDragStart) {
|
|
||||||
// Connect from input to output
|
|
||||||
const connecting: ConnectingLink = {
|
|
||||||
node,
|
|
||||||
slot: i,
|
|
||||||
output: null,
|
|
||||||
pos: link_pos,
|
|
||||||
}
|
|
||||||
this.connecting_links = [connecting]
|
|
||||||
pointer.onDragStart = () => connecting.input = input
|
|
||||||
|
|
||||||
this.dirty_bgcanvas = true
|
// Dragging a new link from input to output
|
||||||
|
if (!linkConnector.isConnecting) {
|
||||||
|
linkConnector.dragNewFromInput(graph, node, input)
|
||||||
}
|
}
|
||||||
|
pointer.onDragEnd = upEvent => linkConnector.dropLinks(graph, upEvent)
|
||||||
|
pointer.finally = () => linkConnector.reset(true)
|
||||||
|
this.dirty_bgcanvas = true
|
||||||
|
|
||||||
// pointer.finally = () => this.connecting_links = null
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -2631,7 +2587,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
e.canvasY,
|
e.canvasY,
|
||||||
this.visible_nodes,
|
this.visible_nodes,
|
||||||
)
|
)
|
||||||
const { resizingGroup } = this
|
const { resizingGroup, linkConnector } = this
|
||||||
|
|
||||||
const dragRect = this.dragging_rectangle
|
const dragRect = this.dragging_rectangle
|
||||||
if (dragRect) {
|
if (dragRect) {
|
||||||
@@ -2649,7 +2605,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
(this.allow_interaction || (node && node.flags.allow_interaction)) &&
|
(this.allow_interaction || (node && node.flags.allow_interaction)) &&
|
||||||
!this.read_only
|
!this.read_only
|
||||||
) {
|
) {
|
||||||
if (this.connecting_links) this.dirty_canvas = true
|
if (linkConnector.isConnecting) this.dirty_canvas = true
|
||||||
|
|
||||||
// remove mouseover flag
|
// remove mouseover flag
|
||||||
this.updateMouseOverNodes(node, e)
|
this.updateMouseOverNodes(node, e)
|
||||||
@@ -2694,59 +2650,53 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
node.mouseOver.overWidget = overWidget
|
node.mouseOver.overWidget = overWidget
|
||||||
|
|
||||||
// Check if link is over anything it could connect to - record position of valid target for snap / highlight
|
// Check if link is over anything it could connect to - record position of valid target for snap / highlight
|
||||||
if (this.connecting_links?.length) {
|
if (linkConnector.isConnecting) {
|
||||||
const firstLink = this.connecting_links[0]
|
const firstLink = linkConnector.renderLinks.at(0)
|
||||||
|
|
||||||
// Default: nothing highlighted
|
// Default: nothing highlighted
|
||||||
let highlightPos: Point | undefined
|
let highlightPos: Point | undefined
|
||||||
let highlightInput: INodeInputSlot | undefined
|
let highlightInput: INodeInputSlot | undefined
|
||||||
let linkOverWidget: IWidget | undefined
|
|
||||||
|
|
||||||
if (firstLink.node === node) {
|
if (!firstLink || firstLink.node === node) {
|
||||||
// Cannot connect link from a node to itself
|
// No link / node loopback
|
||||||
} else if (firstLink.output) {
|
} else if (linkConnector.state.connectingTo === "input") {
|
||||||
// Connecting from an output to an input
|
|
||||||
if (inputId === -1 && outputId === -1) {
|
if (inputId === -1 && outputId === -1) {
|
||||||
// Allow support for linking to widgets, handled externally to LiteGraph
|
// Allow support for linking to widgets, handled externally to LiteGraph
|
||||||
if (this.getWidgetLinkType && overWidget) {
|
if (this.getWidgetLinkType && overWidget) {
|
||||||
const widgetLinkType = this.getWidgetLinkType(overWidget, node)
|
const widgetLinkType = this.getWidgetLinkType(overWidget, node)
|
||||||
if (
|
if (
|
||||||
widgetLinkType &&
|
widgetLinkType &&
|
||||||
LiteGraph.isValidConnection(firstLink.output.type, widgetLinkType)
|
LiteGraph.isValidConnection(linkConnector.renderLinks[0]?.fromSlot.type, widgetLinkType) &&
|
||||||
|
firstLink.node.isValidWidgetLink?.(firstLink.fromSlotIndex, node, overWidget) !== false
|
||||||
) {
|
) {
|
||||||
if (firstLink.output.slot_index == null) throw new TypeError("Connecting link output.slot_index was null.")
|
linkConnector.overWidget = overWidget
|
||||||
|
linkConnector.overWidgetType = widgetLinkType
|
||||||
if (firstLink.node.isValidWidgetLink?.(firstLink.output.slot_index, node, overWidget) !== false) {
|
|
||||||
linkOverWidget = overWidget
|
|
||||||
this.link_over_widget_type = widgetLinkType
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Node background / title under the pointer
|
// Node background / title under the pointer
|
||||||
if (!linkOverWidget) {
|
if (!linkConnector.overWidget) {
|
||||||
const targetSlotId = firstLink.node.findConnectByTypeSlot(true, node, firstLink.output.type)
|
const result = node.findInputByType(firstLink.fromSlot.type)
|
||||||
if (targetSlotId !== undefined && targetSlotId >= 0) {
|
if (result) {
|
||||||
node.getConnectionPos(true, targetSlotId, pos)
|
highlightInput = result.slot
|
||||||
|
node.getConnectionPos(true, result.index, pos)
|
||||||
highlightPos = pos
|
highlightPos = pos
|
||||||
highlightInput = node.inputs[targetSlotId]
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
} else if (
|
} else if (
|
||||||
inputId != -1 &&
|
inputId != -1 &&
|
||||||
node.inputs[inputId] &&
|
node.inputs[inputId] &&
|
||||||
LiteGraph.isValidConnection(firstLink.output.type, node.inputs[inputId].type)
|
LiteGraph.isValidConnection(firstLink.fromSlot.type, node.inputs[inputId].type)
|
||||||
) {
|
) {
|
||||||
highlightPos = pos
|
highlightPos = pos
|
||||||
// XXX CHECK THIS
|
// XXX CHECK THIS
|
||||||
highlightInput = node.inputs[inputId]
|
highlightInput = node.inputs[inputId]
|
||||||
}
|
}
|
||||||
} else if (firstLink.input) {
|
} else if (linkConnector.state.connectingTo === "output") {
|
||||||
// Connecting from an input to an output
|
// Connecting from an input to an output
|
||||||
if (inputId === -1 && outputId === -1) {
|
if (inputId === -1 && outputId === -1) {
|
||||||
const targetSlotId = firstLink.node.findConnectByTypeSlot(false, node, firstLink.input.type)
|
const result = node.findOutputByType(firstLink.fromSlot.type)
|
||||||
|
if (result) {
|
||||||
if (targetSlotId !== undefined && targetSlotId >= 0) {
|
node.getConnectionPos(false, result.index, pos)
|
||||||
node.getConnectionPos(false, targetSlotId, pos)
|
|
||||||
highlightPos = pos
|
highlightPos = pos
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
@@ -2754,7 +2704,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
if (
|
if (
|
||||||
outputId != -1 &&
|
outputId != -1 &&
|
||||||
node.outputs[outputId] &&
|
node.outputs[outputId] &&
|
||||||
LiteGraph.isValidConnection(firstLink.input.type, node.outputs[outputId].type)
|
LiteGraph.isValidConnection(firstLink.fromSlot.type, node.outputs[outputId].type)
|
||||||
) {
|
) {
|
||||||
highlightPos = pos
|
highlightPos = pos
|
||||||
}
|
}
|
||||||
@@ -2762,7 +2712,6 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
}
|
}
|
||||||
this._highlight_pos = highlightPos
|
this._highlight_pos = highlightPos
|
||||||
this._highlight_input = highlightInput
|
this._highlight_input = highlightInput
|
||||||
this.link_over_widget = linkOverWidget
|
|
||||||
}
|
}
|
||||||
|
|
||||||
this.dirty_canvas = true
|
this.dirty_canvas = true
|
||||||
@@ -2916,10 +2865,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
const x = e.canvasX
|
const x = e.canvasX
|
||||||
const y = e.canvasY
|
const y = e.canvasY
|
||||||
|
|
||||||
if (this.connecting_links?.length) {
|
if (!this.linkConnector.isConnecting) {
|
||||||
// node below mouse
|
|
||||||
this.#processConnectingLinks(e, this.connecting_links)
|
|
||||||
} else {
|
|
||||||
this.dirty_canvas = true
|
this.dirty_canvas = true
|
||||||
|
|
||||||
// @ts-expect-error Unused param
|
// @ts-expect-error Unused param
|
||||||
@@ -2929,8 +2875,6 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
y - this.node_capturing_input.pos[1],
|
y - this.node_capturing_input.pos[1],
|
||||||
])
|
])
|
||||||
}
|
}
|
||||||
|
|
||||||
this.connecting_links = null
|
|
||||||
} else if (e.button === 1) {
|
} else if (e.button === 1) {
|
||||||
// middle button
|
// middle button
|
||||||
this.dirty_canvas = true
|
this.dirty_canvas = true
|
||||||
@@ -2950,112 +2894,6 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
#processConnectingLinks(e: CanvasPointerEvent, connecting_links: ConnectingLink[]): boolean | undefined {
|
|
||||||
const { graph } = this
|
|
||||||
if (!graph) throw new NullGraphError()
|
|
||||||
|
|
||||||
const { canvasX: x, canvasY: y } = e
|
|
||||||
const node = graph.getNodeOnPos(x, y, this.visible_nodes)
|
|
||||||
const firstLink = connecting_links[0]
|
|
||||||
|
|
||||||
if (node) {
|
|
||||||
let madeNewLink: boolean | undefined
|
|
||||||
|
|
||||||
for (const link of connecting_links) {
|
|
||||||
// dragging a connection
|
|
||||||
this.#dirty()
|
|
||||||
|
|
||||||
// One should avoid linking things to oneself
|
|
||||||
if (node === link.node) continue
|
|
||||||
|
|
||||||
// slot below mouse? connect
|
|
||||||
if (link.output) {
|
|
||||||
const slot = isOverNodeInput(node, x, y)
|
|
||||||
if (slot != -1) {
|
|
||||||
// Trying to move link onto itself
|
|
||||||
if (link.link?.target_id === node.id && link.link?.target_slot === slot) return
|
|
||||||
|
|
||||||
const newLink = link.node.connect(link.slot, node, slot, link.afterRerouteId)
|
|
||||||
madeNewLink ||= newLink !== null
|
|
||||||
} else if (this.link_over_widget) {
|
|
||||||
this.emitEvent({
|
|
||||||
subType: "connectingWidgetLink",
|
|
||||||
link,
|
|
||||||
node,
|
|
||||||
widget: this.link_over_widget,
|
|
||||||
})
|
|
||||||
this.link_over_widget = undefined
|
|
||||||
} else {
|
|
||||||
// not on top of an input
|
|
||||||
// look for a good slot
|
|
||||||
const slotIndex = link.node.findConnectByTypeSlot(true, node, link.output.type)
|
|
||||||
if (slotIndex !== undefined) {
|
|
||||||
// Trying to move link onto itself
|
|
||||||
if (link.link?.target_id === node.id && link.link?.target_slot === slotIndex) return
|
|
||||||
|
|
||||||
const newLink = link.node.connect(link.slot, node, slotIndex, link.afterRerouteId)
|
|
||||||
madeNewLink ||= newLink !== null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (link.input) {
|
|
||||||
const slot = isOverNodeOutput(node, x, y)
|
|
||||||
|
|
||||||
const newLink = slot != -1
|
|
||||||
// this is inverted has output-input nature like
|
|
||||||
? node.connect(slot, link.node, link.slot, link.afterRerouteId)
|
|
||||||
// not on top of an input
|
|
||||||
// look for a good slot
|
|
||||||
: link.node.connectByTypeOutput(
|
|
||||||
link.slot,
|
|
||||||
node,
|
|
||||||
link.input.type,
|
|
||||||
{ afterRerouteId: link.afterRerouteId },
|
|
||||||
)
|
|
||||||
madeNewLink ||= newLink !== null
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return madeNewLink
|
|
||||||
} else if (firstLink.input || firstLink.output) {
|
|
||||||
// For external event only.
|
|
||||||
const linkReleaseContextExtended: LinkReleaseContextExtended = {
|
|
||||||
links: connecting_links,
|
|
||||||
}
|
|
||||||
this.emitEvent({
|
|
||||||
subType: "empty-release",
|
|
||||||
originalEvent: e,
|
|
||||||
linkReleaseContext: linkReleaseContextExtended,
|
|
||||||
})
|
|
||||||
// No longer in use
|
|
||||||
// add menu when releasing link in empty space
|
|
||||||
if (LiteGraph.release_link_on_empty_shows_menu) {
|
|
||||||
const linkReleaseContext = firstLink.output
|
|
||||||
? {
|
|
||||||
node_from: firstLink.node,
|
|
||||||
slot_from: firstLink.output,
|
|
||||||
type_filter_in: firstLink.output.type,
|
|
||||||
}
|
|
||||||
: {
|
|
||||||
node_to: firstLink.node,
|
|
||||||
slot_from: firstLink.input,
|
|
||||||
type_filter_out: firstLink.input?.type,
|
|
||||||
}
|
|
||||||
|
|
||||||
if (e.shiftKey) {
|
|
||||||
if (this.allow_searchbox) {
|
|
||||||
this.showSearchBox(e, linkReleaseContext)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (firstLink.output) {
|
|
||||||
this.showConnectionMenu({ nodeFrom: firstLink.node, slotFrom: firstLink.output, e: e })
|
|
||||||
} else if (firstLink.input) {
|
|
||||||
this.showConnectionMenu({ nodeTo: firstLink.node, slotTo: firstLink.input, e: e })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Called when the mouse moves off the canvas. Clears all node hover states.
|
* Called when the mouse moves off the canvas. Clears all node hover states.
|
||||||
* @param e
|
* @param e
|
||||||
@@ -3993,7 +3831,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
drawFrontCanvas(): void {
|
drawFrontCanvas(): void {
|
||||||
this.dirty_canvas = false
|
this.dirty_canvas = false
|
||||||
|
|
||||||
const { ctx, canvas } = this
|
const { ctx, canvas, linkConnector } = this
|
||||||
|
|
||||||
// @ts-expect-error
|
// @ts-expect-error
|
||||||
if (ctx.start2D && !this.viewport) {
|
if (ctx.start2D && !this.viewport) {
|
||||||
@@ -4081,33 +3919,21 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
this.drawConnections(ctx)
|
this.drawConnections(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
if (this.connecting_links?.length) {
|
if (linkConnector.isConnecting) {
|
||||||
// current connection (the one being dragged by the mouse)
|
// current connection (the one being dragged by the mouse)
|
||||||
|
const { renderLinks } = linkConnector
|
||||||
const highlightPos = this.#getHighlightPosition()
|
const highlightPos = this.#getHighlightPosition()
|
||||||
ctx.lineWidth = this.connections_width
|
ctx.lineWidth = this.connections_width
|
||||||
|
|
||||||
for (const link of this.connecting_links) {
|
for (const renderLink of renderLinks) {
|
||||||
const connInOrOut = link.output || link.input
|
const { fromSlot, fromPos: pos, fromDirection, dragDirection } = renderLink
|
||||||
|
const connShape = fromSlot.shape
|
||||||
|
const connType = fromSlot.type
|
||||||
|
|
||||||
const connType = connInOrOut?.type
|
const colour = connType === LiteGraph.EVENT
|
||||||
let connDir = connInOrOut?.dir
|
|
||||||
if (connDir == null) {
|
|
||||||
if (link.output)
|
|
||||||
connDir = LinkDirection.RIGHT
|
|
||||||
else
|
|
||||||
connDir = LinkDirection.LEFT
|
|
||||||
}
|
|
||||||
const connShape = connInOrOut?.shape
|
|
||||||
|
|
||||||
const link_color = connType === LiteGraph.EVENT
|
|
||||||
? LiteGraph.EVENT_LINK_COLOR
|
? LiteGraph.EVENT_LINK_COLOR
|
||||||
: LiteGraph.CONNECTING_LINK_COLOR
|
: LiteGraph.CONNECTING_LINK_COLOR
|
||||||
|
|
||||||
// If not using reroutes, link.afterRerouteId should be undefined.
|
|
||||||
const rerouteIdToConnectTo = link.firstRerouteId ?? link.afterRerouteId
|
|
||||||
const pos = rerouteIdToConnectTo == null
|
|
||||||
? link.pos
|
|
||||||
: (this.graph.reroutes.get(rerouteIdToConnectTo)?.pos ?? link.pos)
|
|
||||||
// the connection being dragged by the mouse
|
// the connection being dragged by the mouse
|
||||||
this.renderLink(
|
this.renderLink(
|
||||||
ctx,
|
ctx,
|
||||||
@@ -4116,9 +3942,9 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
null,
|
null,
|
||||||
false,
|
false,
|
||||||
null,
|
null,
|
||||||
link_color,
|
colour,
|
||||||
connDir,
|
fromDirection,
|
||||||
link.direction ?? LinkDirection.CENTER,
|
dragDirection,
|
||||||
)
|
)
|
||||||
|
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
@@ -4239,7 +4065,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
|
|
||||||
// Ensure we're mousing over a node and connecting a link
|
// Ensure we're mousing over a node and connecting a link
|
||||||
const node = this.node_over
|
const node = this.node_over
|
||||||
if (!(node && this.connecting_links?.[0])) return
|
if (!(node && this.linkConnector.isConnecting)) return
|
||||||
|
|
||||||
const { strokeStyle, lineWidth } = ctx
|
const { strokeStyle, lineWidth } = ctx
|
||||||
|
|
||||||
@@ -4256,7 +4082,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
ctx.roundRect(x, y, width, height, radius)
|
ctx.roundRect(x, y, width, height, radius)
|
||||||
|
|
||||||
// TODO: Currently works on LTR slots only. Add support for other directions.
|
// TODO: Currently works on LTR slots only. Add support for other directions.
|
||||||
const start = this.connecting_links[0].output === null ? 0 : 1
|
const start = this.linkConnector.state.connectingTo === "output" ? 0 : 1
|
||||||
const inverter = start ? -1 : 1
|
const inverter = start ? -1 : 1
|
||||||
|
|
||||||
// Radial highlight centred on highlight pos
|
// Radial highlight centred on highlight pos
|
||||||
@@ -4547,7 +4373,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
node.layoutWidgetInputSlots()
|
node.layoutWidgetInputSlots()
|
||||||
|
|
||||||
node.drawSlots(ctx, {
|
node.drawSlots(ctx, {
|
||||||
connectingLink: this.connecting_links?.[0],
|
fromSlot: this.linkConnector.renderLinks[0]?.fromSlot,
|
||||||
colorContext: this,
|
colorContext: this,
|
||||||
editorAlpha: this.editor_alpha,
|
editorAlpha: this.editor_alpha,
|
||||||
lowQuality: this.low_quality,
|
lowQuality: this.low_quality,
|
||||||
@@ -4858,8 +4684,6 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
const link = this.graph._links.get(link_id)
|
const link = this.graph._links.get(link_id)
|
||||||
if (!link) continue
|
if (!link) continue
|
||||||
|
|
||||||
const draggingLink = isDraggingLink(link.id, this.connecting_links)
|
|
||||||
|
|
||||||
// find link info
|
// find link info
|
||||||
const start_node = this.graph.getNodeById(link.origin_id)
|
const start_node = this.graph.getNodeById(link.origin_id)
|
||||||
if (start_node == null) continue
|
if (start_node == null) continue
|
||||||
@@ -4920,23 +4744,24 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
reroute.calculateAngle(this.last_draw_time, this.graph, startPos)
|
reroute.calculateAngle(this.last_draw_time, this.graph, startPos)
|
||||||
|
|
||||||
// Skip the first segment if it is being dragged
|
// Skip the first segment if it is being dragged
|
||||||
if (j === 0 && draggingLink?.input) continue
|
if (!reroute._dragging) {
|
||||||
this.renderLink(
|
this.renderLink(
|
||||||
ctx,
|
ctx,
|
||||||
startPos,
|
startPos,
|
||||||
reroute.pos,
|
reroute.pos,
|
||||||
link,
|
link,
|
||||||
false,
|
false,
|
||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
start_dir,
|
start_dir,
|
||||||
end_dir,
|
end_dir,
|
||||||
{
|
{
|
||||||
startControl,
|
startControl,
|
||||||
endControl: reroute.controlPoint,
|
endControl: reroute.controlPoint,
|
||||||
reroute,
|
reroute,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Calculate start control for the next iter control point
|
// Calculate start control for the next iter control point
|
||||||
@@ -4946,7 +4771,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Skip the last segment if it is being dragged
|
// Skip the last segment if it is being dragged
|
||||||
if (draggingLink?.output) continue
|
if (link._dragging) continue
|
||||||
|
|
||||||
// Use runtime fallback; TypeScript cannot evaluate this correctly.
|
// Use runtime fallback; TypeScript cannot evaluate this correctly.
|
||||||
const segmentStartPos = points.at(-2) ?? start_node_slotpos
|
const segmentStartPos = points.at(-2) ?? start_node_slotpos
|
||||||
@@ -4965,7 +4790,7 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
{ startControl },
|
{ startControl },
|
||||||
)
|
)
|
||||||
// Skip normal render when link is being dragged
|
// Skip normal render when link is being dragged
|
||||||
} else if (!draggingLink) {
|
} else if (!link._dragging) {
|
||||||
this.renderLink(
|
this.renderLink(
|
||||||
ctx,
|
ctx,
|
||||||
start_node_slotpos,
|
start_node_slotpos,
|
||||||
@@ -5413,11 +5238,12 @@ export class LGraphCanvas implements ConnectionColorContext {
|
|||||||
posY: number,
|
posY: number,
|
||||||
ctx: CanvasRenderingContext2D,
|
ctx: CanvasRenderingContext2D,
|
||||||
): void {
|
): void {
|
||||||
|
const { linkConnector } = this
|
||||||
|
|
||||||
node.drawWidgets(ctx, {
|
node.drawWidgets(ctx, {
|
||||||
colorContext: this,
|
colorContext: this,
|
||||||
linkOverWidget: this.link_over_widget,
|
linkOverWidget: linkConnector.overWidget,
|
||||||
// @ts-expect-error https://github.com/Comfy-Org/litegraph.js/issues/616
|
linkOverWidgetType: linkConnector.overWidgetType,
|
||||||
linkOverWidgetType: this.link_over_widget_type,
|
|
||||||
lowQuality: this.low_quality,
|
lowQuality: this.low_quality,
|
||||||
editorAlpha: this.editor_alpha,
|
editorAlpha: this.editor_alpha,
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ import type { DragAndScale } from "./DragAndScale"
|
|||||||
import type {
|
import type {
|
||||||
CanvasColour,
|
CanvasColour,
|
||||||
ColorOption,
|
ColorOption,
|
||||||
ConnectingLink,
|
|
||||||
Dictionary,
|
Dictionary,
|
||||||
IColorable,
|
IColorable,
|
||||||
IContextMenuValue,
|
IContextMenuValue,
|
||||||
@@ -3256,7 +3255,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
|||||||
drawWidgets(ctx: CanvasRenderingContext2D, options: {
|
drawWidgets(ctx: CanvasRenderingContext2D, options: {
|
||||||
colorContext: ConnectionColorContext
|
colorContext: ConnectionColorContext
|
||||||
linkOverWidget: IWidget | null | undefined
|
linkOverWidget: IWidget | null | undefined
|
||||||
linkOverWidgetType: ISlotType
|
linkOverWidgetType?: ISlotType
|
||||||
lowQuality?: boolean
|
lowQuality?: boolean
|
||||||
editorAlpha?: number
|
editorAlpha?: number
|
||||||
}): void {
|
}): void {
|
||||||
@@ -3281,6 +3280,7 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
|||||||
// Manually draw a slot next to the widget simulating an input
|
// Manually draw a slot next to the widget simulating an input
|
||||||
new NodeInputSlot({
|
new NodeInputSlot({
|
||||||
name: "",
|
name: "",
|
||||||
|
// @ts-expect-error https://github.com/Comfy-Org/litegraph.js/issues/616
|
||||||
type: linkOverWidgetType,
|
type: linkOverWidgetType,
|
||||||
link: 0,
|
link: 0,
|
||||||
// @ts-expect-error https://github.com/Comfy-Org/litegraph.js/issues/616
|
// @ts-expect-error https://github.com/Comfy-Org/litegraph.js/issues/616
|
||||||
@@ -3419,18 +3419,18 @@ export class LGraphNode implements Positionable, IPinnable, IColorable {
|
|||||||
* Draws the node's input and output slots.
|
* Draws the node's input and output slots.
|
||||||
*/
|
*/
|
||||||
drawSlots(ctx: CanvasRenderingContext2D, options: {
|
drawSlots(ctx: CanvasRenderingContext2D, options: {
|
||||||
connectingLink: ConnectingLink | undefined
|
fromSlot?: INodeInputSlot | INodeOutputSlot
|
||||||
colorContext: ConnectionColorContext
|
colorContext: ConnectionColorContext
|
||||||
editorAlpha: number
|
editorAlpha: number
|
||||||
lowQuality: boolean
|
lowQuality: boolean
|
||||||
}) {
|
}) {
|
||||||
const { connectingLink, colorContext, editorAlpha, lowQuality } = options
|
const { fromSlot, colorContext, editorAlpha, lowQuality } = options
|
||||||
|
|
||||||
for (const slot of this.slots) {
|
for (const slot of this.slots) {
|
||||||
// change opacity of incompatible slots when dragging a connection
|
// change opacity of incompatible slots when dragging a connection
|
||||||
const layoutElement = slot._layoutElement
|
const layoutElement = slot._layoutElement
|
||||||
const slotInstance = toNodeSlotClass(slot)
|
const slotInstance = toNodeSlotClass(slot)
|
||||||
const isValid = slotInstance.isValidTarget(connectingLink)
|
const isValid = !fromSlot || slotInstance.isValidTarget(fromSlot)
|
||||||
const highlight = isValid && this.#isMouseOverSlot(slot)
|
const highlight = isValid && this.#isMouseOverSlot(slot)
|
||||||
const labelColor = highlight
|
const labelColor = highlight
|
||||||
? this.highlightColor
|
? this.highlightColor
|
||||||
|
|||||||
10
src/LLink.ts
10
src/LLink.ts
@@ -45,6 +45,9 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
|||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
_centreAngle?: number
|
_centreAngle?: number
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_dragging?: boolean
|
||||||
|
|
||||||
#color?: CanvasColour | null
|
#color?: CanvasColour | null
|
||||||
/** Custom colour for this link only */
|
/** Custom colour for this link only */
|
||||||
public get color(): CanvasColour | null | undefined {
|
public get color(): CanvasColour | null | undefined {
|
||||||
@@ -114,6 +117,13 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
|||||||
?.getReroutes() ?? []
|
?.getReroutes() ?? []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static getFirstReroute(
|
||||||
|
network: LinkNetwork,
|
||||||
|
linkSegment: LinkSegment,
|
||||||
|
): Reroute | undefined {
|
||||||
|
return LLink.getReroutes(network, linkSegment).at(0)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Finds the reroute in the chain after the provided reroute ID.
|
* Finds the reroute in the chain after the provided reroute ID.
|
||||||
* @param network The network this link belongs to
|
* @param network The network this link belongs to
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
import type { CanvasColour, ConnectingLink, Dictionary, INodeInputSlot, INodeOutputSlot, INodeSlot, ISlotType, IWidgetInputSlot, Point } from "./interfaces"
|
import type { CanvasColour, Dictionary, INodeInputSlot, INodeOutputSlot, INodeSlot, ISlotType, IWidgetInputSlot, Point } from "./interfaces"
|
||||||
import type { LinkId } from "./LLink"
|
import type { LinkId } from "./LLink"
|
||||||
import type { IWidget } from "./types/widgets"
|
import type { IWidget } from "./types/widgets"
|
||||||
|
|
||||||
@@ -83,9 +83,9 @@ export abstract class NodeSlot implements INodeSlot {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether this slot is a valid target for a dragging link.
|
* Whether this slot is a valid target for a dragging link.
|
||||||
* @param link The link to check against.
|
* @param fromSlot The slot that the link is being connected from.
|
||||||
*/
|
*/
|
||||||
abstract isValidTarget(link: ConnectingLink | undefined): boolean
|
abstract isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot): boolean
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The label to display in the UI.
|
* The label to display in the UI.
|
||||||
@@ -270,10 +270,8 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
|
|||||||
return this.link != null
|
return this.link != null
|
||||||
}
|
}
|
||||||
|
|
||||||
override isValidTarget(link: ConnectingLink | undefined): boolean {
|
override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot): boolean {
|
||||||
if (!link) return true
|
return "links" in fromSlot && LiteGraph.isValidConnection(this.type, fromSlot.type)
|
||||||
|
|
||||||
return !!link.output && LiteGraph.isValidConnection(this.type, link.output.type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override draw(ctx: CanvasRenderingContext2D, options: Omit<IDrawOptions, "doStroke" | "labelPosition">) {
|
override draw(ctx: CanvasRenderingContext2D, options: Omit<IDrawOptions, "doStroke" | "labelPosition">) {
|
||||||
@@ -306,10 +304,8 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
|
|||||||
this.slot_index = slot.slot_index
|
this.slot_index = slot.slot_index
|
||||||
}
|
}
|
||||||
|
|
||||||
override isValidTarget(link: ConnectingLink | undefined): boolean {
|
override isValidTarget(fromSlot: INodeInputSlot | INodeOutputSlot): boolean {
|
||||||
if (!link) return true
|
return "link" in fromSlot && LiteGraph.isValidConnection(this.type, fromSlot.type)
|
||||||
|
|
||||||
return !!link.input && LiteGraph.isValidConnection(this.type, link.input.type)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
override isConnected(): boolean {
|
override isConnected(): boolean {
|
||||||
|
|||||||
@@ -84,6 +84,9 @@ export class Reroute implements Positionable, LinkSegment, Serialisable<Serialis
|
|||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
_pos: Float32Array = this.#malloc.subarray(6, 8)
|
_pos: Float32Array = this.#malloc.subarray(6, 8)
|
||||||
|
|
||||||
|
/** @inheritdoc */
|
||||||
|
_dragging?: boolean
|
||||||
|
|
||||||
/** Colour of the first link that rendered this reroute */
|
/** Colour of the first link that rendered this reroute */
|
||||||
_colour?: CanvasColour
|
_colour?: CanvasColour
|
||||||
|
|
||||||
|
|||||||
499
src/canvas/LinkConnector.ts
Normal file
499
src/canvas/LinkConnector.ts
Normal file
@@ -0,0 +1,499 @@
|
|||||||
|
import type { RenderLink } from "./RenderLink"
|
||||||
|
import type { ConnectingLink, ItemLocator, LinkNetwork, LinkSegment } from "@/interfaces"
|
||||||
|
import type { INodeInputSlot, INodeOutputSlot } from "@/interfaces"
|
||||||
|
import type { LGraphNode } from "@/LGraphNode"
|
||||||
|
import type { Reroute } from "@/Reroute"
|
||||||
|
import type { CanvasPointerEvent } from "@/types/events"
|
||||||
|
import type { IWidget } from "@/types/widgets"
|
||||||
|
|
||||||
|
import { LinkConnectorEventMap, LinkConnectorEventTarget } from "@/infrastructure/LinkConnectorEventTarget"
|
||||||
|
import { LLink } from "@/LLink"
|
||||||
|
import { LinkDirection } from "@/types/globalEnums"
|
||||||
|
|
||||||
|
import { getNodeOutputOnPos } from "./measureSlots"
|
||||||
|
import { MovingRenderLink } from "./MovingRenderLink"
|
||||||
|
import { ToInputRenderLink } from "./ToInputRenderLink"
|
||||||
|
import { ToOutputRenderLink } from "./ToOutputRenderLink"
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A Litegraph state object for the {@link LinkConnector}.
|
||||||
|
* References are only held atomically within a function, never passed.
|
||||||
|
* The concrete implementation may be replaced or proxied without side-effects.
|
||||||
|
*/
|
||||||
|
export interface LinkConnectorState {
|
||||||
|
/**
|
||||||
|
* The type of slot that links are being connected **to**.
|
||||||
|
* - When `undefined`, no operation is being performed.
|
||||||
|
* - A change in this property indicates the start or end of dragging links.
|
||||||
|
*/
|
||||||
|
connectingTo: "input" | "output" | undefined
|
||||||
|
multi: boolean
|
||||||
|
/** When `true`, existing links are being repositioned. Otherwise, new links are being created. */
|
||||||
|
draggingExistingLinks: boolean
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Discriminated union to simplify type narrowing. */
|
||||||
|
type RenderLinkUnion = MovingRenderLink | ToInputRenderLink | ToOutputRenderLink
|
||||||
|
|
||||||
|
export interface LinkConnectorExport {
|
||||||
|
renderLinks: RenderLink[]
|
||||||
|
inputLinks: LLink[]
|
||||||
|
outputLinks: LLink[]
|
||||||
|
state: LinkConnectorState
|
||||||
|
network: LinkNetwork
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Component of {@link LGraphCanvas} that handles connecting and moving links.
|
||||||
|
* @see {@link LLink}
|
||||||
|
*/
|
||||||
|
export class LinkConnector {
|
||||||
|
/**
|
||||||
|
* Link connection state POJO. Source of truth for state of link drag operations.
|
||||||
|
*
|
||||||
|
* Can be replaced or proxied to allow notifications.
|
||||||
|
* Is always dereferenced at the start of an operation.
|
||||||
|
*/
|
||||||
|
state: LinkConnectorState = {
|
||||||
|
connectingTo: undefined,
|
||||||
|
multi: false,
|
||||||
|
draggingExistingLinks: false,
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly events = new LinkConnectorEventTarget()
|
||||||
|
|
||||||
|
/** Contains information for rendering purposes only. */
|
||||||
|
readonly renderLinks: RenderLinkUnion[] = []
|
||||||
|
|
||||||
|
/** Existing links that are being moved **to** a new input slot. */
|
||||||
|
readonly inputLinks: LLink[] = []
|
||||||
|
/** Existing links that are being moved **to** a new output slot. */
|
||||||
|
readonly outputLinks: LLink[] = []
|
||||||
|
|
||||||
|
readonly hiddenReroutes: Set<Reroute> = new Set()
|
||||||
|
|
||||||
|
/** The widget beneath the pointer, if it is a valid connection target. */
|
||||||
|
overWidget?: IWidget
|
||||||
|
/** The type (returned by downstream callback) for {@link overWidget} */
|
||||||
|
overWidgetType?: string
|
||||||
|
|
||||||
|
readonly #setConnectingLinks: (value: ConnectingLink[]) => void
|
||||||
|
|
||||||
|
constructor(setConnectingLinks: (value: ConnectingLink[]) => void) {
|
||||||
|
this.#setConnectingLinks = setConnectingLinks
|
||||||
|
}
|
||||||
|
|
||||||
|
get isConnecting() {
|
||||||
|
return this.state.connectingTo !== undefined
|
||||||
|
}
|
||||||
|
|
||||||
|
get draggingExistingLinks() {
|
||||||
|
return this.state.draggingExistingLinks
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drag an existing link to a different input. */
|
||||||
|
moveInputLink(network: LinkNetwork, input: INodeInputSlot, fromReroute?: Reroute): void {
|
||||||
|
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||||
|
|
||||||
|
const { state, inputLinks, renderLinks } = this
|
||||||
|
|
||||||
|
const linkId = input.link
|
||||||
|
if (linkId == null) return
|
||||||
|
|
||||||
|
const link = network.links.get(linkId)
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
try {
|
||||||
|
const renderLink = new MovingRenderLink(network, link, "input", fromReroute)
|
||||||
|
|
||||||
|
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: [${link.id}].`, link, error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
link._dragging = true
|
||||||
|
inputLinks.push(link)
|
||||||
|
|
||||||
|
state.connectingTo = "input"
|
||||||
|
state.draggingExistingLinks = true
|
||||||
|
|
||||||
|
this.#setLegacyLinks(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Drag all links from an output to a new output. */
|
||||||
|
moveOutputLink(network: LinkNetwork, output: INodeOutputSlot): void {
|
||||||
|
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
|
||||||
|
|
||||||
|
const firstReroute = LLink.getFirstReroute(network, link)
|
||||||
|
if (firstReroute) {
|
||||||
|
firstReroute._dragging = true
|
||||||
|
this.hiddenReroutes.add(firstReroute)
|
||||||
|
} else {
|
||||||
|
link._dragging = true
|
||||||
|
}
|
||||||
|
this.outputLinks.push(link)
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (renderLinks.length === 0) return this.reset()
|
||||||
|
|
||||||
|
state.draggingExistingLinks = true
|
||||||
|
state.multi = true
|
||||||
|
state.connectingTo = "output"
|
||||||
|
|
||||||
|
this.#setLegacyLinks(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drags a new link from an output slot to an input slot.
|
||||||
|
* @param network The network that the link being connected belongs to
|
||||||
|
* @param node The node the link is being dragged from
|
||||||
|
* @param output The output slot that the link is being dragged from
|
||||||
|
*/
|
||||||
|
dragNewFromOutput(network: LinkNetwork, node: LGraphNode, output: INodeOutputSlot, fromReroute?: Reroute): void {
|
||||||
|
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||||
|
|
||||||
|
const { state } = this
|
||||||
|
const renderLink = new ToInputRenderLink(network, node, output, fromReroute)
|
||||||
|
this.renderLinks.push(renderLink)
|
||||||
|
|
||||||
|
state.connectingTo = "input"
|
||||||
|
|
||||||
|
this.#setLegacyLinks(false)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drags a new link from an input slot to an output slot.
|
||||||
|
* @param network The network that the link being connected belongs to
|
||||||
|
* @param node The node the link is being dragged from
|
||||||
|
* @param input The input slot that the link is being dragged from
|
||||||
|
*/
|
||||||
|
dragNewFromInput(network: LinkNetwork, node: LGraphNode, input: INodeInputSlot, fromReroute?: Reroute): void {
|
||||||
|
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||||
|
|
||||||
|
const { state } = this
|
||||||
|
const renderLink = new ToOutputRenderLink(network, node, input, fromReroute)
|
||||||
|
this.renderLinks.push(renderLink)
|
||||||
|
|
||||||
|
state.connectingTo = "output"
|
||||||
|
|
||||||
|
this.#setLegacyLinks(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Drags a new link from a reroute to an input slot.
|
||||||
|
* @param network The network that the link being connected belongs to
|
||||||
|
* @param reroute The reroute that the link is being dragged from
|
||||||
|
*/
|
||||||
|
dragFromReroute(network: LinkNetwork, reroute: Reroute): void {
|
||||||
|
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||||
|
|
||||||
|
const { state } = this
|
||||||
|
|
||||||
|
// Connect new link from reroute
|
||||||
|
const linkId = reroute.linkIds.values().next().value
|
||||||
|
if (linkId == null) return
|
||||||
|
|
||||||
|
const link = network.links.get(linkId)
|
||||||
|
if (!link) return
|
||||||
|
|
||||||
|
const outputNode = network.getNodeById(link.origin_id)
|
||||||
|
if (!outputNode) return
|
||||||
|
|
||||||
|
const outputSlot = outputNode.outputs.at(link.origin_slot)
|
||||||
|
if (!outputSlot) return
|
||||||
|
|
||||||
|
const renderLink = new ToInputRenderLink(network, outputNode, outputSlot, reroute)
|
||||||
|
renderLink.fromDirection = LinkDirection.NONE
|
||||||
|
this.renderLinks.push(renderLink)
|
||||||
|
|
||||||
|
state.connectingTo = "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
dragFromLinkSegment(network: LinkNetwork, linkSegment: LinkSegment): void {
|
||||||
|
if (this.isConnecting) throw new Error("Already dragging links.")
|
||||||
|
|
||||||
|
const { state } = this
|
||||||
|
if (linkSegment.origin_id == null || linkSegment.origin_slot == null) return
|
||||||
|
|
||||||
|
const node = network.getNodeById(linkSegment.origin_id)
|
||||||
|
if (!node) return
|
||||||
|
|
||||||
|
const slot = getNodeOutputOnPos(node, linkSegment._pos[0], linkSegment._pos[1])?.output
|
||||||
|
if (!slot) return
|
||||||
|
|
||||||
|
const reroute = linkSegment.parentId ? network.reroutes.get(linkSegment.parentId) : undefined
|
||||||
|
if (!reroute) return
|
||||||
|
|
||||||
|
const renderLink = new ToInputRenderLink(network, node, slot, reroute)
|
||||||
|
renderLink.fromDirection = LinkDirection.NONE
|
||||||
|
this.renderLinks.push(renderLink)
|
||||||
|
|
||||||
|
state.connectingTo = "input"
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects the links being droppe
|
||||||
|
* @param event Contains the drop location, in canvas space
|
||||||
|
*/
|
||||||
|
dropLinks(locator: ItemLocator, event: CanvasPointerEvent): void {
|
||||||
|
if (!this.isConnecting) return this.reset()
|
||||||
|
|
||||||
|
const { renderLinks, state } = this
|
||||||
|
const { connectingTo } = state
|
||||||
|
|
||||||
|
const mayContinue = this.events.dispatch("before-drop-links", { renderLinks, event })
|
||||||
|
if (mayContinue === false) return this.reset()
|
||||||
|
|
||||||
|
const { canvasX, canvasY } = event
|
||||||
|
const node = locator.getNodeOnPos(canvasX, canvasY) ?? undefined
|
||||||
|
if (!node) return this.dropOnNothing(event)
|
||||||
|
|
||||||
|
// To output
|
||||||
|
if (connectingTo === "output") {
|
||||||
|
const output = node.getOutputOnPos([canvasX, canvasY])
|
||||||
|
|
||||||
|
if (output) {
|
||||||
|
this.#dropOnOutput(node, output)
|
||||||
|
} else {
|
||||||
|
this.dropOnNode(node, event)
|
||||||
|
}
|
||||||
|
// To input
|
||||||
|
} else if (connectingTo === "input") {
|
||||||
|
const input = node.getInputOnPos([canvasX, canvasY])
|
||||||
|
|
||||||
|
// Input slot
|
||||||
|
if (input) {
|
||||||
|
this.#dropOnInput(node, input)
|
||||||
|
} else if (this.overWidget && this.renderLinks[0] instanceof ToInputRenderLink) {
|
||||||
|
// Widget
|
||||||
|
this.events.dispatch("dropped-on-widget", {
|
||||||
|
link: this.renderLinks[0],
|
||||||
|
node,
|
||||||
|
widget: this.overWidget,
|
||||||
|
})
|
||||||
|
this.overWidget = undefined
|
||||||
|
} else {
|
||||||
|
// Node background / title
|
||||||
|
this.dropOnNode(node, event)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.events.dispatch("after-drop-links", { renderLinks, event })
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Connects the links being dropped onto a node.
|
||||||
|
* @param node The node that the links are being dropped on
|
||||||
|
* @param event Contains the drop location, in canvas space
|
||||||
|
*/
|
||||||
|
dropOnNode(node: LGraphNode, event: CanvasPointerEvent): void {
|
||||||
|
const { state: { connectingTo } } = this
|
||||||
|
|
||||||
|
const mayContinue = this.events.dispatch("dropped-on-node", { node, event })
|
||||||
|
if (mayContinue === false) return
|
||||||
|
|
||||||
|
// Assume all links are the same type, disallow loopback
|
||||||
|
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 this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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 this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Dropping new output link
|
||||||
|
if (connectingTo === "output") {
|
||||||
|
const output = node.findOutputByType(firstLink.fromSlot.type)?.slot
|
||||||
|
if (!output) {
|
||||||
|
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const link of this.renderLinks) {
|
||||||
|
if ("link" in link.fromSlot) {
|
||||||
|
node.connectSlots(output, link.node, link.fromSlot, link.fromReroute?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Dropping new input link
|
||||||
|
} else if (connectingTo === "input") {
|
||||||
|
const input = node.findInputByType(firstLink.fromSlot.type)?.slot
|
||||||
|
if (!input) {
|
||||||
|
console.warn(`Could not find slot for link type: [${firstLink.fromSlot.type}].`)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const link of this.renderLinks) {
|
||||||
|
if ("links" in link.fromSlot) {
|
||||||
|
link.node.connectSlots(link.fromSlot, node, input, link.fromReroute?.id)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
dropOnNothing(event: CanvasPointerEvent): void {
|
||||||
|
// For external event only.
|
||||||
|
if (this.state.connectingTo === "input") {
|
||||||
|
for (const link of this.renderLinks) {
|
||||||
|
if (link instanceof MovingRenderLink) {
|
||||||
|
link.inputNode.disconnectInput(link.inputIndex, true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
this.events.dispatch("dropped-on-canvas", event)
|
||||||
|
this.reset()
|
||||||
|
}
|
||||||
|
|
||||||
|
#dropOnInput(node: LGraphNode, input: INodeInputSlot): void {
|
||||||
|
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
|
||||||
|
|
||||||
|
outputNode.connectSlots(outputSlot, node, input, fromReroute?.id)
|
||||||
|
this.events.dispatch("input-moved", link)
|
||||||
|
} else {
|
||||||
|
const { node: outputNode, fromSlot, fromReroute } = link
|
||||||
|
const newLink = outputNode.connectSlots(fromSlot, node, input, fromReroute?.id)
|
||||||
|
this.events.dispatch("link-created", newLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#dropOnOutput(node: LGraphNode, output: INodeOutputSlot): void {
|
||||||
|
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
|
||||||
|
const newLink = node.connectSlots(output, inputNode, fromSlot, fromReroute?.id)
|
||||||
|
this.events.dispatch("link-created", newLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Sets connecting_links, used by some extensions still. */
|
||||||
|
#setLegacyLinks(fromSlotIsInput: boolean): void {
|
||||||
|
const links = this.renderLinks.map((link) => {
|
||||||
|
const input = fromSlotIsInput ? link.fromSlot as INodeInputSlot : null
|
||||||
|
const output = fromSlotIsInput ? null : link.fromSlot as INodeOutputSlot
|
||||||
|
|
||||||
|
return {
|
||||||
|
node: link.node,
|
||||||
|
slot: link.fromSlotIndex,
|
||||||
|
input,
|
||||||
|
output,
|
||||||
|
pos: link.fromPos,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.#setConnectingLinks(links)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exports the current state of the link connector.
|
||||||
|
* @param network The network that the links being connected belong to.
|
||||||
|
* @returns A POJO with the state of the link connector, links being connected, and their network.
|
||||||
|
* @remarks Other than {@link network}, all properties are shallow cloned.
|
||||||
|
*/
|
||||||
|
export(network: LinkNetwork): LinkConnectorExport {
|
||||||
|
return {
|
||||||
|
renderLinks: [...this.renderLinks],
|
||||||
|
inputLinks: [...this.inputLinks],
|
||||||
|
outputLinks: [...this.outputLinks],
|
||||||
|
state: { ...this.state },
|
||||||
|
network,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds an event listener that will be automatically removed when the reset event is fired.
|
||||||
|
* @param eventName The event to listen for.
|
||||||
|
* @param listener The listener to call when the event is fired.
|
||||||
|
*/
|
||||||
|
listenUntilReset<K extends keyof LinkConnectorEventMap>(
|
||||||
|
eventName: K,
|
||||||
|
listener: Parameters<typeof this.events.addEventListener<K>>[1],
|
||||||
|
options?: Parameters<typeof this.events.addEventListener<K>>[2],
|
||||||
|
) {
|
||||||
|
this.events.addEventListener(eventName, listener, options)
|
||||||
|
this.events.addEventListener("reset", () => this.events.removeEventListener(eventName, listener), { once: true })
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resets everything to its initial state.
|
||||||
|
*
|
||||||
|
* Effectively cancels moving or connecting links.
|
||||||
|
*/
|
||||||
|
reset(force = false): void {
|
||||||
|
this.events.dispatch("reset", force)
|
||||||
|
|
||||||
|
const { state, outputLinks, inputLinks, hiddenReroutes, renderLinks } = this
|
||||||
|
|
||||||
|
if (!force && state.connectingTo === undefined) return
|
||||||
|
state.connectingTo = undefined
|
||||||
|
|
||||||
|
for (const link of outputLinks) delete link._dragging
|
||||||
|
for (const link of inputLinks) delete link._dragging
|
||||||
|
for (const reroute of hiddenReroutes) delete reroute._dragging
|
||||||
|
|
||||||
|
renderLinks.length = 0
|
||||||
|
inputLinks.length = 0
|
||||||
|
outputLinks.length = 0
|
||||||
|
hiddenReroutes.clear()
|
||||||
|
state.multi = false
|
||||||
|
state.draggingExistingLinks = false
|
||||||
|
}
|
||||||
|
}
|
||||||
87
src/canvas/MovingRenderLink.ts
Normal file
87
src/canvas/MovingRenderLink.ts
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
import type { RenderLink } from "./RenderLink"
|
||||||
|
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 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.getConnectionPos(false, 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.getConnectionPos(true, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
26
src/canvas/RenderLink.ts
Normal file
26
src/canvas/RenderLink.ts
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import type { LinkNetwork, Point } from "@/interfaces"
|
||||||
|
import type { LGraphNode } from "@/LGraphNode"
|
||||||
|
import type { INodeInputSlot, INodeOutputSlot, Reroute } from "@/litegraph"
|
||||||
|
import type { LinkDirection } from "@/types/globalEnums"
|
||||||
|
|
||||||
|
export interface RenderLink {
|
||||||
|
/** The type of link being connected. */
|
||||||
|
readonly toType: "input" | "output"
|
||||||
|
/** The source {@link Point} of the link being connected. */
|
||||||
|
readonly fromPos: Point
|
||||||
|
/** The direction the link starts off as. If {@link toType} is `output`, this will be the direction the link input faces. */
|
||||||
|
readonly fromDirection: LinkDirection
|
||||||
|
/** If set, this will force a dragged link "point" from the cursor in the specified direction. */
|
||||||
|
dragDirection: LinkDirection
|
||||||
|
|
||||||
|
/** The network that the link belongs to. */
|
||||||
|
readonly network: LinkNetwork
|
||||||
|
/** The node that the link is being connected from. */
|
||||||
|
readonly node: LGraphNode
|
||||||
|
/** The slot that the link is being connected from. */
|
||||||
|
readonly fromSlot: INodeOutputSlot | INodeInputSlot
|
||||||
|
/** The index of the slot that the link is being connected from. */
|
||||||
|
readonly fromSlotIndex: number
|
||||||
|
/** The reroute that the link is being connected from. */
|
||||||
|
readonly fromReroute?: Reroute
|
||||||
|
}
|
||||||
32
src/canvas/ToInputRenderLink.ts
Normal file
32
src/canvas/ToInputRenderLink.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { RenderLink } from "./RenderLink"
|
||||||
|
import type { LinkNetwork, Point } from "@/interfaces"
|
||||||
|
import type { LGraphNode } from "@/LGraphNode"
|
||||||
|
import type { INodeOutputSlot } from "@/litegraph"
|
||||||
|
import type { Reroute } from "@/Reroute"
|
||||||
|
|
||||||
|
import { LinkDirection } from "@/types/globalEnums"
|
||||||
|
|
||||||
|
/** Connecting TO an input slot. */
|
||||||
|
|
||||||
|
export class ToInputRenderLink implements RenderLink {
|
||||||
|
readonly toType = "input"
|
||||||
|
readonly fromPos: Point
|
||||||
|
readonly fromSlotIndex: number
|
||||||
|
fromDirection: LinkDirection = LinkDirection.RIGHT
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly network: LinkNetwork,
|
||||||
|
readonly node: LGraphNode,
|
||||||
|
readonly fromSlot: INodeOutputSlot,
|
||||||
|
readonly fromReroute?: Reroute,
|
||||||
|
public dragDirection: LinkDirection = LinkDirection.CENTER,
|
||||||
|
) {
|
||||||
|
const outputIndex = node.outputs.indexOf(fromSlot)
|
||||||
|
if (outputIndex === -1) throw new Error(`Creating render link for node [${this.node.id}] failed: Slot index not found.`)
|
||||||
|
|
||||||
|
this.fromSlotIndex = outputIndex
|
||||||
|
this.fromPos = fromReroute
|
||||||
|
? fromReroute.pos
|
||||||
|
: this.node.getConnectionPos(false, outputIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
32
src/canvas/ToOutputRenderLink.ts
Normal file
32
src/canvas/ToOutputRenderLink.ts
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
import type { RenderLink } from "./RenderLink"
|
||||||
|
import type { LinkNetwork, Point } from "@/interfaces"
|
||||||
|
import type { LGraphNode } from "@/LGraphNode"
|
||||||
|
import type { INodeInputSlot } from "@/litegraph"
|
||||||
|
import type { Reroute } from "@/Reroute"
|
||||||
|
|
||||||
|
import { LinkDirection } from "@/types/globalEnums"
|
||||||
|
|
||||||
|
/** Connecting TO an output slot. */
|
||||||
|
|
||||||
|
export class ToOutputRenderLink implements RenderLink {
|
||||||
|
readonly toType = "output"
|
||||||
|
readonly fromPos: Point
|
||||||
|
readonly fromSlotIndex: number
|
||||||
|
fromDirection: LinkDirection = LinkDirection.LEFT
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
readonly network: LinkNetwork,
|
||||||
|
readonly node: LGraphNode,
|
||||||
|
readonly fromSlot: INodeInputSlot,
|
||||||
|
readonly fromReroute?: Reroute,
|
||||||
|
public dragDirection: LinkDirection = LinkDirection.CENTER,
|
||||||
|
) {
|
||||||
|
const inputIndex = node.inputs.indexOf(fromSlot)
|
||||||
|
if (inputIndex === -1) throw new Error(`Creating render link for node [${this.node.id}] failed: Slot index not found.`)
|
||||||
|
|
||||||
|
this.fromSlotIndex = inputIndex
|
||||||
|
this.fromPos = fromReroute
|
||||||
|
? fromReroute.pos
|
||||||
|
: this.node.getConnectionPos(true, inputIndex)
|
||||||
|
}
|
||||||
|
}
|
||||||
99
src/infrastructure/LinkConnectorEventTarget.ts
Normal file
99
src/infrastructure/LinkConnectorEventTarget.ts
Normal file
@@ -0,0 +1,99 @@
|
|||||||
|
import type { MovingRenderLink } from "@/canvas/MovingRenderLink"
|
||||||
|
import type { RenderLink } from "@/canvas/RenderLink"
|
||||||
|
import type { ToInputRenderLink } from "@/canvas/ToInputRenderLink"
|
||||||
|
import type { LGraphNode } from "@/LGraphNode"
|
||||||
|
import type { LLink } from "@/LLink"
|
||||||
|
import type { CanvasPointerEvent } from "@/types/events"
|
||||||
|
import type { IWidget } from "@/types/widgets"
|
||||||
|
|
||||||
|
export interface LinkConnectorEventMap {
|
||||||
|
"reset": boolean
|
||||||
|
|
||||||
|
"before-drop-links": {
|
||||||
|
renderLinks: RenderLink[]
|
||||||
|
event: CanvasPointerEvent
|
||||||
|
}
|
||||||
|
"after-drop-links": {
|
||||||
|
renderLinks: RenderLink[]
|
||||||
|
event: CanvasPointerEvent
|
||||||
|
}
|
||||||
|
|
||||||
|
"before-move-input": MovingRenderLink
|
||||||
|
"before-move-output": MovingRenderLink
|
||||||
|
|
||||||
|
"input-moved": MovingRenderLink
|
||||||
|
"output-moved": MovingRenderLink
|
||||||
|
|
||||||
|
"link-created": LLink | null | undefined
|
||||||
|
|
||||||
|
"dropped-on-node": {
|
||||||
|
node: LGraphNode
|
||||||
|
event: CanvasPointerEvent
|
||||||
|
}
|
||||||
|
"dropped-on-canvas": CanvasPointerEvent
|
||||||
|
|
||||||
|
"dropped-on-widget": {
|
||||||
|
link: ToInputRenderLink
|
||||||
|
node: LGraphNode
|
||||||
|
widget: IWidget
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link Omit} all properties that evaluate to `never`. */
|
||||||
|
type NeverNever<T> = {
|
||||||
|
[K in keyof T as T[K] extends never ? never : K]: T[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
/** {@link Pick} only properties that evaluate to `never`. */
|
||||||
|
type PickNevers<T> = {
|
||||||
|
[K in keyof T as T[K] extends never ? K : never]: T[K]
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkConnectorEventListeners = {
|
||||||
|
readonly [K in keyof LinkConnectorEventMap]: ((this: EventTarget, ev: CustomEvent<LinkConnectorEventMap[K]>) => any) | EventListenerObject | null
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Events that _do not_ pass a {@link CustomEvent} `detail` object. */
|
||||||
|
type SimpleEvents = keyof PickNevers<LinkConnectorEventMap>
|
||||||
|
|
||||||
|
/** Events that pass a {@link CustomEvent} `detail` object. */
|
||||||
|
type ComplexEvents = keyof NeverNever<LinkConnectorEventMap>
|
||||||
|
|
||||||
|
export class LinkConnectorEventTarget extends EventTarget {
|
||||||
|
/**
|
||||||
|
* Type-safe event dispatching.
|
||||||
|
* @see {@link EventTarget.dispatchEvent}
|
||||||
|
* @param type Name of the event to dispatch
|
||||||
|
* @param detail A custom object to send with the event
|
||||||
|
* @returns `true` if the event was dispatched successfully, otherwise `false`.
|
||||||
|
*/
|
||||||
|
dispatch<T extends ComplexEvents>(type: T, detail: LinkConnectorEventMap[T]): boolean
|
||||||
|
dispatch<T extends SimpleEvents>(type: T): boolean
|
||||||
|
dispatch<T extends keyof LinkConnectorEventMap>(type: T, detail?: LinkConnectorEventMap[T]) {
|
||||||
|
const event = new CustomEvent(type, { detail })
|
||||||
|
return super.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
|
||||||
|
override addEventListener<K extends keyof LinkConnectorEventMap>(
|
||||||
|
type: K,
|
||||||
|
listener: LinkConnectorEventListeners[K],
|
||||||
|
options?: boolean | AddEventListenerOptions,
|
||||||
|
): void {
|
||||||
|
// Assertion: Contravariance on CustomEvent => Event
|
||||||
|
super.addEventListener(type, listener as EventListener, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
override removeEventListener<K extends keyof LinkConnectorEventMap>(
|
||||||
|
type: K,
|
||||||
|
listener: LinkConnectorEventListeners[K],
|
||||||
|
options?: boolean | EventListenerOptions,
|
||||||
|
): void {
|
||||||
|
// Assertion: Contravariance on CustomEvent => Event
|
||||||
|
super.removeEventListener(type, listener as EventListener, options)
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @deprecated Use {@link dispatch}. */
|
||||||
|
override dispatchEvent(event: never): boolean {
|
||||||
|
return super.dispatchEvent(event)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -113,6 +113,13 @@ export interface LinkNetwork {
|
|||||||
getNodeById(id: NodeId): LGraphNode | null
|
getNodeById(id: NodeId): LGraphNode | null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Locates graph items.
|
||||||
|
*/
|
||||||
|
export interface ItemLocator {
|
||||||
|
getNodeOnPos(x: number, y: number, nodeList?: LGraphNode[]): LGraphNode | null
|
||||||
|
}
|
||||||
|
|
||||||
/** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */
|
/** Contains a cached 2D canvas path and a centre point, with an optional forward angle. */
|
||||||
export interface LinkSegment {
|
export interface LinkSegment {
|
||||||
/** Link / reroute ID */
|
/** Link / reroute ID */
|
||||||
@@ -131,6 +138,9 @@ export interface LinkSegment {
|
|||||||
*/
|
*/
|
||||||
_centreAngle?: number
|
_centreAngle?: number
|
||||||
|
|
||||||
|
/** Whether the link is currently being moved. @internal */
|
||||||
|
_dragging?: boolean
|
||||||
|
|
||||||
/** Output node ID */
|
/** Output node ID */
|
||||||
readonly origin_id: NodeId | undefined
|
readonly origin_id: NodeId | undefined
|
||||||
/** Output slot index */
|
/** Output slot index */
|
||||||
|
|||||||
Reference in New Issue
Block a user