mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-19 14:30:07 +00:00
Squash: decouple link/reroute rendering changes
Range: c3b7053f..709570b8 Commits: - a2648349 fix(Reroute): remove unsafe this alias in computeRenderParams; capture id and use it inside helper - 89d6ae1a Lazy compute - e78326ac refactor(litegraph): decouple link dragging render state from model - 6d3a035a refactor(litegraph): remove reroute._dragging; track reroute hiding ephemerally - d0870533 refactor(litegraph): decouple reroute render geometry and respect hidden reroutes - 709570b8 refactor(litegraph): decouple reroute rendering state from models and canvas (cherry picked from commit 23f0a7403117a71e99095446443a8f5acec8f582)
This commit is contained in:
@@ -10,6 +10,13 @@ import { LGraphGroup } from './LGraphGroup'
|
||||
import { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode'
|
||||
import { LLink, type LinkId } from './LLink'
|
||||
import { Reroute, type RerouteId } from './Reroute'
|
||||
import { RenderedLinkSegment } from './canvas/RenderedLinkSegment'
|
||||
import { computeRerouteHoverState } from './canvas/RerouteHover'
|
||||
import {
|
||||
drawReroute,
|
||||
drawRerouteHighlight,
|
||||
drawRerouteSlots
|
||||
} from './canvas/RerouteRenderer'
|
||||
import { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
|
||||
import { strokeShape } from './draw'
|
||||
import type {
|
||||
@@ -287,6 +294,12 @@ export class LGraphCanvas
|
||||
selectionChanged: false
|
||||
}
|
||||
|
||||
/** Ephemeral per-frame colours for reroutes */
|
||||
// Render state kept per-frame; no persistent caches here.
|
||||
|
||||
/** Ephemeral hover/outline UI state for reroute slots. */
|
||||
// Hover state computed on-demand; no persistent caches here.
|
||||
|
||||
#subgraph?: Subgraph
|
||||
get subgraph(): Subgraph | undefined {
|
||||
return this.#subgraph
|
||||
@@ -2356,7 +2369,9 @@ export class LGraphCanvas
|
||||
if (this.links_render_mode !== LinkRenderType.HIDDEN_LINK) {
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
const overReroute = reroute.containsPoint([x, y])
|
||||
if (!reroute.isSlotHovered && !overReroute) continue
|
||||
const hover = computeRerouteHoverState(reroute, [x, y])
|
||||
const anySlotHovered = hover.inputHover || hover.outputHover
|
||||
if (!anySlotHovered && !overReroute) continue
|
||||
|
||||
if (overReroute) {
|
||||
pointer.onClick = () => this.processSelect(reroute, e)
|
||||
@@ -2367,17 +2382,16 @@ export class LGraphCanvas
|
||||
}
|
||||
}
|
||||
|
||||
if (reroute.isOutputHovered || (overReroute && e.shiftKey)) {
|
||||
if (hover.outputHover || (overReroute && e.shiftKey)) {
|
||||
linkConnector.dragFromReroute(graph, reroute)
|
||||
this.#linkConnectorDrop()
|
||||
}
|
||||
|
||||
if (reroute.isInputHovered) {
|
||||
if (hover.inputHover) {
|
||||
linkConnector.dragFromRerouteToOutput(graph, reroute)
|
||||
this.#linkConnectorDrop()
|
||||
}
|
||||
|
||||
reroute.hideSlots()
|
||||
this.dirty_bgcanvas = true
|
||||
return
|
||||
}
|
||||
@@ -3105,10 +3119,8 @@ export class LGraphCanvas
|
||||
this.node_over = node
|
||||
this.dirty_canvas = true
|
||||
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
reroute.hideSlots()
|
||||
this.dirty_bgcanvas = true
|
||||
}
|
||||
// invalidate background to ensure slot outlines update
|
||||
this.dirty_bgcanvas = true
|
||||
node.onMouseEnter?.(e)
|
||||
}
|
||||
|
||||
@@ -3283,13 +3295,19 @@ export class LGraphCanvas
|
||||
const { graph, pointer, linkConnector } = this
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
// Update reroute hover state
|
||||
// Update reroute hover state without caching
|
||||
if (!pointer.isDown) {
|
||||
let anyChanges = false
|
||||
for (const reroute of this.#visibleReroutes) {
|
||||
anyChanges ||= reroute.updateVisibility(this.graph_mouse)
|
||||
|
||||
if (reroute.isSlotHovered) underPointer |= CanvasItem.RerouteSlot
|
||||
const next = computeRerouteHoverState(reroute, this.graph_mouse)
|
||||
if (next.inputHover || next.outputHover)
|
||||
underPointer |= CanvasItem.RerouteSlot
|
||||
// pointer movement can change outlines/hover; mark dirty if any visible
|
||||
anyChanges ||=
|
||||
next.inputOutline ||
|
||||
next.outputOutline ||
|
||||
next.inputHover ||
|
||||
next.outputHover
|
||||
}
|
||||
if (anyChanges) this.dirty_bgcanvas = true
|
||||
} else if (linkConnector.isConnecting) {
|
||||
@@ -4682,7 +4700,7 @@ export class LGraphCanvas
|
||||
return
|
||||
|
||||
// Reroute highlight
|
||||
overReroute?.drawHighlight(ctx, '#ffcc00aa')
|
||||
if (overReroute) drawRerouteHighlight(ctx, overReroute, '#ffcc00aa')
|
||||
|
||||
// Ensure we're mousing over a node and connecting a link
|
||||
const node = this.node_over
|
||||
@@ -5312,6 +5330,10 @@ export class LGraphCanvas
|
||||
if (!graph) throw new NullGraphError()
|
||||
|
||||
const visibleReroutes: Reroute[] = []
|
||||
const visibleRerouteIds = new Set<RerouteId>()
|
||||
// Per-frame reroute colours computed while building segments
|
||||
const rerouteColours = new Map<RerouteId, CanvasColour>()
|
||||
// Colours are computed per render pass (stored in rerouteColours)
|
||||
|
||||
const now = LiteGraph.getTime()
|
||||
const { visible_area } = this
|
||||
@@ -5363,7 +5385,9 @@ export class LGraphCanvas
|
||||
visibleReroutes,
|
||||
now,
|
||||
output.dir,
|
||||
input.dir
|
||||
input.dir,
|
||||
visibleRerouteIds,
|
||||
rerouteColours
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5390,7 +5414,9 @@ export class LGraphCanvas
|
||||
visibleReroutes,
|
||||
now,
|
||||
input.dir,
|
||||
input.dir
|
||||
input.dir,
|
||||
visibleRerouteIds,
|
||||
rerouteColours
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -5415,13 +5441,22 @@ export class LGraphCanvas
|
||||
visibleReroutes,
|
||||
now,
|
||||
output.dir,
|
||||
input.dir
|
||||
input.dir,
|
||||
visibleRerouteIds,
|
||||
rerouteColours
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
if (graph.floatingLinks.size > 0) {
|
||||
this.#renderFloatingLinks(ctx, graph, visibleReroutes, now)
|
||||
this.#renderFloatingLinks(
|
||||
ctx,
|
||||
graph,
|
||||
visibleReroutes,
|
||||
now,
|
||||
visibleRerouteIds,
|
||||
rerouteColours
|
||||
)
|
||||
}
|
||||
|
||||
const rerouteSet = this.#visibleReroutes
|
||||
@@ -5430,6 +5465,8 @@ export class LGraphCanvas
|
||||
// Render reroutes, ordered by number of non-floating links
|
||||
visibleReroutes.sort((a, b) => a.linkIds.size - b.linkIds.size)
|
||||
for (const reroute of visibleReroutes) {
|
||||
// Respect hidden reroutes while dragging existing links
|
||||
if (this.linkConnector?.hiddenReroutes.has(reroute)) continue
|
||||
rerouteSet.add(reroute)
|
||||
|
||||
if (
|
||||
@@ -5439,10 +5476,16 @@ export class LGraphCanvas
|
||||
) {
|
||||
this.drawSnapGuide(ctx, reroute, RenderShape.CIRCLE)
|
||||
}
|
||||
reroute.draw(ctx, this._pattern)
|
||||
{
|
||||
const colour = rerouteColours.get(reroute.id) ?? this.default_link_color
|
||||
drawReroute(ctx, reroute, this._pattern, colour)
|
||||
|
||||
// Never draw slots when the pointer is down
|
||||
if (!this.pointer.isDown) reroute.drawSlots(ctx)
|
||||
// Never draw slots when the pointer is down
|
||||
if (!this.pointer.isDown) {
|
||||
const state = computeRerouteHoverState(reroute, this.graph_mouse)
|
||||
drawRerouteSlots(ctx, reroute, state, colour)
|
||||
}
|
||||
}
|
||||
}
|
||||
ctx.globalAlpha = 1
|
||||
}
|
||||
@@ -5451,7 +5494,9 @@ export class LGraphCanvas
|
||||
ctx: CanvasRenderingContext2D,
|
||||
graph: LGraph,
|
||||
visibleReroutes: Reroute[],
|
||||
now: number
|
||||
now: number,
|
||||
visibleRerouteIds: Set<RerouteId>,
|
||||
rerouteColours: Map<RerouteId, CanvasColour>
|
||||
) {
|
||||
// Render floating links with 3/4 current alpha
|
||||
const { globalAlpha } = ctx
|
||||
@@ -5473,7 +5518,6 @@ export class LGraphCanvas
|
||||
const endPos = node.getInputPos(link.target_slot)
|
||||
const endDirection = node.inputs[link.target_slot]?.dir
|
||||
|
||||
firstReroute._dragging = true
|
||||
this.#renderAllLinkSegments(
|
||||
ctx,
|
||||
link,
|
||||
@@ -5483,6 +5527,8 @@ export class LGraphCanvas
|
||||
now,
|
||||
LinkDirection.CENTER,
|
||||
endDirection,
|
||||
visibleRerouteIds,
|
||||
rerouteColours,
|
||||
true
|
||||
)
|
||||
} else {
|
||||
@@ -5493,7 +5539,6 @@ export class LGraphCanvas
|
||||
const endPos = reroute.pos
|
||||
const startDirection = node.outputs[link.origin_slot]?.dir
|
||||
|
||||
link._dragging = true
|
||||
this.#renderAllLinkSegments(
|
||||
ctx,
|
||||
link,
|
||||
@@ -5503,6 +5548,8 @@ export class LGraphCanvas
|
||||
now,
|
||||
startDirection,
|
||||
LinkDirection.CENTER,
|
||||
visibleRerouteIds,
|
||||
rerouteColours,
|
||||
true
|
||||
)
|
||||
}
|
||||
@@ -5519,6 +5566,8 @@ export class LGraphCanvas
|
||||
now: number,
|
||||
startDirection?: LinkDirection,
|
||||
endDirection?: LinkDirection,
|
||||
seenRerouteIds?: Set<RerouteId>,
|
||||
rerouteColours?: Map<RerouteId, CanvasColour>,
|
||||
disabled: boolean = false
|
||||
) {
|
||||
const { graph, renderedPaths } = this
|
||||
@@ -5551,29 +5600,48 @@ export class LGraphCanvas
|
||||
const start_dir = startDirection || LinkDirection.RIGHT
|
||||
const end_dir = endDirection || LinkDirection.LEFT
|
||||
|
||||
const baseColour =
|
||||
link.color ||
|
||||
LGraphCanvas.link_type_colors[link.type] ||
|
||||
this.default_link_color
|
||||
|
||||
// Has reroutes
|
||||
if (reroutes.length) {
|
||||
const lastReroute = reroutes[reroutes.length - 1]
|
||||
const floatingType = lastReroute?.floating?.slotType
|
||||
const skipFirstSegment = floatingType === 'input'
|
||||
const skipLastSegment = floatingType === 'output'
|
||||
let startControl: Point | undefined
|
||||
|
||||
const l = reroutes.length
|
||||
for (let j = 0; j < l; j++) {
|
||||
const reroute = reroutes[j]
|
||||
|
||||
// Only render once
|
||||
if (!renderedPaths.has(reroute)) {
|
||||
renderedPaths.add(reroute)
|
||||
visibleReroutes.push(reroute)
|
||||
reroute._colour =
|
||||
link.color ||
|
||||
LGraphCanvas.link_type_colors[link.type] ||
|
||||
this.default_link_color
|
||||
// Lazily compute render params only if needed, and reuse for both purposes
|
||||
const prevReroute = graph.getReroute(reroute.parentId)
|
||||
const rerouteStartPos = prevReroute?.pos ?? startPos
|
||||
let params:
|
||||
| { cos: number; sin: number; controlPoint: Point }
|
||||
| undefined
|
||||
const getParams = () =>
|
||||
(params ??= reroute.computeRenderParams(graph, rerouteStartPos))
|
||||
|
||||
const prevReroute = graph.getReroute(reroute.parentId)
|
||||
const rerouteStartPos = prevReroute?.pos ?? startPos
|
||||
reroute.calculateAngle(this.last_draw_time, graph, rerouteStartPos)
|
||||
// Only render once per reroute
|
||||
if (!seenRerouteIds?.has(reroute.id)) {
|
||||
visibleReroutes.push(reroute)
|
||||
seenRerouteIds?.add(reroute.id)
|
||||
if (rerouteColours && !rerouteColours.has(reroute.id))
|
||||
rerouteColours.set(reroute.id, baseColour)
|
||||
|
||||
// Skip the first segment if it is being dragged
|
||||
if (!reroute._dragging) {
|
||||
if (!(skipFirstSegment && j === 0)) {
|
||||
const rendered = new RenderedLinkSegment({
|
||||
id: reroute.id,
|
||||
origin_id: link.origin_id,
|
||||
origin_slot: link.origin_slot,
|
||||
parentId: reroute.parentId
|
||||
})
|
||||
rendered.colour = baseColour
|
||||
this.renderLink(
|
||||
ctx,
|
||||
rerouteStartPos,
|
||||
@@ -5586,15 +5654,16 @@ export class LGraphCanvas
|
||||
LinkDirection.CENTER,
|
||||
{
|
||||
startControl,
|
||||
endControl: reroute.controlPoint,
|
||||
reroute,
|
||||
disabled
|
||||
endControl: getParams().controlPoint,
|
||||
disabled,
|
||||
renderTarget: rendered
|
||||
}
|
||||
)
|
||||
renderedPaths.add(rendered)
|
||||
}
|
||||
}
|
||||
|
||||
if (!startControl && reroutes.at(-1)?.floating?.slotType === 'input') {
|
||||
if (!startControl && skipFirstSegment) {
|
||||
// Floating link connected to an input
|
||||
startControl = [0, 0]
|
||||
} else {
|
||||
@@ -5604,17 +5673,25 @@ export class LGraphCanvas
|
||||
Reroute.maxSplineOffset,
|
||||
distance(reroute.pos, nextPos) * 0.25
|
||||
)
|
||||
startControl = [dist * reroute.cos, dist * reroute.sin]
|
||||
const p = getParams()
|
||||
startControl = [dist * p.cos, dist * p.sin]
|
||||
}
|
||||
}
|
||||
|
||||
// Skip the last segment if it is being dragged
|
||||
if (link._dragging) return
|
||||
// For floating links from output, skip the last segment
|
||||
if (skipLastSegment) return
|
||||
|
||||
// Use runtime fallback; TypeScript cannot evaluate this correctly.
|
||||
const segmentStartPos = points.at(-2) ?? startPos
|
||||
|
||||
// Render final link segment
|
||||
const rendered = new RenderedLinkSegment({
|
||||
id: link.id,
|
||||
origin_id: link.origin_id,
|
||||
origin_slot: link.origin_slot,
|
||||
parentId: link.parentId
|
||||
})
|
||||
rendered.colour = baseColour
|
||||
this.renderLink(
|
||||
ctx,
|
||||
segmentStartPos,
|
||||
@@ -5625,10 +5702,18 @@ export class LGraphCanvas
|
||||
null,
|
||||
LinkDirection.CENTER,
|
||||
end_dir,
|
||||
{ startControl, disabled }
|
||||
{ startControl, disabled, renderTarget: rendered }
|
||||
)
|
||||
renderedPaths.add(rendered)
|
||||
// Skip normal render when link is being dragged
|
||||
} else if (!link._dragging) {
|
||||
} else if (!this.linkConnector?.isLinkBeingDragged(link.id)) {
|
||||
const rendered = new RenderedLinkSegment({
|
||||
id: link.id,
|
||||
origin_id: link.origin_id,
|
||||
origin_slot: link.origin_slot,
|
||||
parentId: link.parentId
|
||||
})
|
||||
rendered.colour = baseColour
|
||||
this.renderLink(
|
||||
ctx,
|
||||
startPos,
|
||||
@@ -5638,10 +5723,11 @@ export class LGraphCanvas
|
||||
0,
|
||||
null,
|
||||
start_dir,
|
||||
end_dir
|
||||
end_dir,
|
||||
{ renderTarget: rendered }
|
||||
)
|
||||
renderedPaths.add(rendered)
|
||||
}
|
||||
renderedPaths.add(link)
|
||||
|
||||
// event triggered rendered on top
|
||||
if (link?._last_time && now - link._last_time < 1000) {
|
||||
@@ -5688,12 +5774,10 @@ export class LGraphCanvas
|
||||
{
|
||||
startControl,
|
||||
endControl,
|
||||
reroute,
|
||||
num_sublines = 1,
|
||||
disabled = false
|
||||
disabled = false,
|
||||
renderTarget
|
||||
}: {
|
||||
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
|
||||
reroute?: Reroute
|
||||
/** Offset of the bezier curve control point from {@link a point a} (output side) */
|
||||
startControl?: ReadOnlyPoint
|
||||
/** Offset of the bezier curve control point from {@link b point b} (input side) */
|
||||
@@ -5702,6 +5786,8 @@ export class LGraphCanvas
|
||||
num_sublines?: number
|
||||
/** Whether this is a floating link segment */
|
||||
disabled?: boolean
|
||||
/** Where to store the drawn path for hit testing */
|
||||
renderTarget?: RenderedLinkSegment
|
||||
} = {}
|
||||
): void {
|
||||
const linkColour =
|
||||
@@ -5731,14 +5817,14 @@ export class LGraphCanvas
|
||||
// begin line shape
|
||||
const path = new Path2D()
|
||||
|
||||
/** The link or reroute we're currently rendering */
|
||||
const linkSegment = reroute ?? link
|
||||
/** The segment we're currently rendering */
|
||||
const linkSegment = renderTarget
|
||||
if (linkSegment) linkSegment.path = path
|
||||
|
||||
const innerA = LGraphCanvas.#lTempA
|
||||
const innerB = LGraphCanvas.#lTempB
|
||||
|
||||
/** Reference to {@link reroute._pos} if present, or {@link link._pos} if present. Caches the centre point of the link. */
|
||||
/** Reference to render-time centre point of this segment. */
|
||||
const pos: Point = linkSegment?._pos ?? [0, 0]
|
||||
|
||||
for (let i = 0; i < num_sublines; i++) {
|
||||
|
||||
@@ -34,9 +34,12 @@ import type {
|
||||
} from './interfaces'
|
||||
import {
|
||||
type LGraphNodeConstructor,
|
||||
LabelPosition,
|
||||
LiteGraph,
|
||||
type Subgraph,
|
||||
type SubgraphNode
|
||||
type SubgraphNode,
|
||||
drawCollapsedSlot,
|
||||
drawSlot
|
||||
} from './litegraph'
|
||||
import {
|
||||
createBounds,
|
||||
@@ -2819,7 +2822,6 @@ export class LGraphNode
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds.add(link.id)
|
||||
if (reroute.floating) delete reroute.floating
|
||||
reroute._dragging = undefined
|
||||
}
|
||||
|
||||
// If this is the terminus of a floating link, remove it
|
||||
@@ -3798,13 +3800,13 @@ export class LGraphNode
|
||||
// Render the first connected slot only.
|
||||
for (const slot of this.#concreteInputs) {
|
||||
if (slot.link != null) {
|
||||
slot.drawCollapsed(ctx)
|
||||
drawCollapsedSlot(ctx, slot, slot.collapsedPos)
|
||||
break
|
||||
}
|
||||
}
|
||||
for (const slot of this.#concreteOutputs) {
|
||||
if (slot.links?.length) {
|
||||
slot.drawCollapsed(ctx)
|
||||
drawCollapsedSlot(ctx, slot, slot.collapsedPos)
|
||||
break
|
||||
}
|
||||
}
|
||||
@@ -3917,11 +3919,32 @@ export class LGraphNode
|
||||
slot.isConnected
|
||||
) {
|
||||
ctx.globalAlpha = isValid ? editorAlpha : 0.4 * editorAlpha
|
||||
slot.draw(ctx, {
|
||||
colorContext,
|
||||
lowQuality,
|
||||
highlight
|
||||
})
|
||||
|
||||
// - Inputs: label on the right, no stroke, left textAlign
|
||||
// - Outputs: label on the left, black stroke, right textAlign
|
||||
const isInput = slot instanceof NodeInputSlot
|
||||
const labelPosition = isInput ? LabelPosition.Right : LabelPosition.Left
|
||||
|
||||
const { strokeStyle, textAlign } = ctx
|
||||
if (isInput) {
|
||||
ctx.textAlign = 'left'
|
||||
} else {
|
||||
ctx.textAlign = 'right'
|
||||
ctx.strokeStyle = 'black'
|
||||
}
|
||||
|
||||
try {
|
||||
drawSlot(ctx, slot as any, {
|
||||
colorContext,
|
||||
lowQuality,
|
||||
highlight,
|
||||
labelPosition,
|
||||
doStroke: !isInput
|
||||
})
|
||||
} finally {
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.textAlign = textAlign
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import type {
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
LinkNetwork,
|
||||
LinkSegment,
|
||||
ReadonlyLinkNetwork
|
||||
} from './interfaces'
|
||||
import { Subgraph } from './litegraph'
|
||||
@@ -87,7 +86,7 @@ type BasicReadonlyNetwork = Pick<
|
||||
>
|
||||
|
||||
// this is the class in charge of storing link information
|
||||
export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
export class LLink implements Serialisable<SerialisableLLink> {
|
||||
static _drawDebug = false
|
||||
|
||||
/** Link ID */
|
||||
@@ -105,17 +104,11 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
|
||||
data?: number | string | boolean | { toToolTip?(): string }
|
||||
_data?: unknown
|
||||
/** Centre point of the link, calculated during render only - can be inaccurate */
|
||||
_pos: Float32Array
|
||||
/** @todo Clean up - never implemented in comfy. */
|
||||
_last_time?: number
|
||||
/** The last canvas 2D path that was used to render this link */
|
||||
path?: Path2D
|
||||
/** @inheritdoc */
|
||||
_centreAngle?: number
|
||||
|
||||
/** @inheritdoc */
|
||||
_dragging?: boolean
|
||||
// Note: Render-time dragging state is tracked externally (LinkConnector), not on the model.
|
||||
|
||||
#color?: CanvasColour | null
|
||||
/** Custom colour for this link only */
|
||||
@@ -167,8 +160,6 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
this.parentId = parentId
|
||||
|
||||
this._data = null
|
||||
// center
|
||||
this._pos = new Float32Array(2)
|
||||
}
|
||||
|
||||
/** @deprecated Use {@link LLink.create} */
|
||||
@@ -200,7 +191,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
*/
|
||||
static getReroutes(
|
||||
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
||||
linkSegment: LinkSegment
|
||||
linkSegment: { parentId?: RerouteId }
|
||||
): Reroute[] {
|
||||
if (!linkSegment.parentId) return []
|
||||
return network.reroutes.get(linkSegment.parentId)?.getReroutes() ?? []
|
||||
@@ -208,7 +199,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
|
||||
static getFirstReroute(
|
||||
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
||||
linkSegment: LinkSegment
|
||||
linkSegment: { parentId?: RerouteId }
|
||||
): Reroute | undefined {
|
||||
return LLink.getReroutes(network, linkSegment).at(0)
|
||||
}
|
||||
@@ -223,7 +214,7 @@ export class LLink implements LinkSegment, Serialisable<SerialisableLLink> {
|
||||
*/
|
||||
static findNextReroute(
|
||||
network: Pick<ReadonlyLinkNetwork, 'reroutes'>,
|
||||
linkSegment: LinkSegment,
|
||||
linkSegment: { parentId?: RerouteId },
|
||||
rerouteId: RerouteId
|
||||
): Reroute | null | undefined {
|
||||
if (!linkSegment.parentId) return
|
||||
|
||||
@@ -6,7 +6,6 @@ import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
LinkNetwork,
|
||||
LinkSegment,
|
||||
Point,
|
||||
Positionable,
|
||||
ReadOnlyRect,
|
||||
@@ -31,7 +30,7 @@ export interface FloatingRerouteSlot {
|
||||
* and a `WeakRef` to a {@link LinkNetwork} to resolve them.
|
||||
*/
|
||||
export class Reroute
|
||||
implements Positionable, LinkSegment, Serialisable<SerialisableReroute>
|
||||
implements Positionable, Serialisable<SerialisableReroute>
|
||||
{
|
||||
static radius: number = 10
|
||||
/** Maximum distance from reroutes to their bezier curve control points. */
|
||||
@@ -123,24 +122,6 @@ export class Reroute
|
||||
/** Bezier curve control point for the "target" (input) side of the link */
|
||||
controlPoint: Point = this.#malloc.subarray(4, 6)
|
||||
|
||||
/** @inheritdoc */
|
||||
path?: Path2D
|
||||
/** @inheritdoc */
|
||||
_centreAngle?: number
|
||||
/** @inheritdoc */
|
||||
_pos: Float32Array = this.#malloc.subarray(6, 8)
|
||||
|
||||
/** @inheritdoc */
|
||||
_dragging?: boolean
|
||||
|
||||
/** Colour of the first link that rendered this reroute */
|
||||
_colour?: CanvasColour
|
||||
|
||||
/** Colour of the first link that rendered this reroute */
|
||||
get colour(): CanvasColour {
|
||||
return this._colour ?? '#18184d'
|
||||
}
|
||||
|
||||
/**
|
||||
* Used to ensure reroute angles are only executed once per frame.
|
||||
* @todo Calculate on change instead.
|
||||
@@ -150,18 +131,6 @@ export class Reroute
|
||||
#inputSlot = new RerouteSlot(this, true)
|
||||
#outputSlot = new RerouteSlot(this, false)
|
||||
|
||||
get isSlotHovered(): boolean {
|
||||
return this.isInputHovered || this.isOutputHovered
|
||||
}
|
||||
|
||||
get isInputHovered(): boolean {
|
||||
return this.#inputSlot.hovering
|
||||
}
|
||||
|
||||
get isOutputHovered(): boolean {
|
||||
return this.#outputSlot.hovering
|
||||
}
|
||||
|
||||
get firstLink(): LLink | undefined {
|
||||
const linkId = this.linkIds.values().next().value
|
||||
return linkId === undefined
|
||||
@@ -537,13 +506,75 @@ export class Reroute
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Computes render-time parameters for this reroute without mutating the model.
|
||||
* Returns the bezier end control-point offset and the direction cos/sin.
|
||||
*/
|
||||
computeRenderParams(
|
||||
network: ReadonlyLinkNetwork,
|
||||
linkStart: Point
|
||||
): { cos: number; sin: number; controlPoint: Point } {
|
||||
const thisPos = this.#pos
|
||||
const { id } = this
|
||||
const angles: number[] = []
|
||||
let sum = 0
|
||||
|
||||
// Collect angles of all links passing through this reroute
|
||||
addAngles(this.linkIds, network.links)
|
||||
addAngles(this.floatingLinkIds, network.floatingLinks)
|
||||
|
||||
// Default values when invalid
|
||||
if (!angles.length) {
|
||||
return { cos: 0, sin: 0, controlPoint: [0, 0] }
|
||||
}
|
||||
|
||||
sum /= angles.length
|
||||
|
||||
const originToReroute = Math.atan2(
|
||||
thisPos[1] - linkStart[1],
|
||||
thisPos[0] - linkStart[0]
|
||||
)
|
||||
let diff = (originToReroute - sum) * 0.5
|
||||
if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI
|
||||
const dist = Math.min(
|
||||
Reroute.maxSplineOffset,
|
||||
distance(linkStart, thisPos) * 0.25
|
||||
)
|
||||
|
||||
const originDiff = originToReroute - diff
|
||||
const cos = Math.cos(originDiff)
|
||||
const sin = Math.sin(originDiff)
|
||||
const controlPoint: Point = [dist * -cos, dist * -sin]
|
||||
|
||||
return { cos, sin, controlPoint }
|
||||
|
||||
function addAngles(
|
||||
linkIds: Iterable<LinkId>,
|
||||
links: ReadonlyMap<LinkId, LLink>
|
||||
) {
|
||||
for (const linkId of linkIds) {
|
||||
const link = links.get(linkId)
|
||||
const pos = getNextPos(network, link, id)
|
||||
if (!pos) continue
|
||||
const angle = getDirection(thisPos, pos)
|
||||
angles.push(angle)
|
||||
sum += angle
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Renders the reroute on the canvas.
|
||||
* @param ctx Canvas context to draw on
|
||||
* @param backgroundPattern The canvas background pattern; used to make floating reroutes appear washed out.
|
||||
* @param ctx Canvas context to draw on.
|
||||
* @param backgroundPattern Canvas background pattern; used to wash out floating reroutes.
|
||||
* @param colour Fill/stroke colour to use for this reroute (provided by renderer per-frame).
|
||||
* @remarks Leaves {@link ctx}.fillStyle, strokeStyle, and lineWidth dirty (perf.).
|
||||
*/
|
||||
draw(ctx: CanvasRenderingContext2D, backgroundPattern?: CanvasPattern): void {
|
||||
draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
backgroundPattern: CanvasPattern | undefined,
|
||||
colour: CanvasColour
|
||||
): void {
|
||||
const { globalAlpha } = ctx
|
||||
const { pos } = this
|
||||
|
||||
@@ -556,7 +587,7 @@ export class Reroute
|
||||
ctx.globalAlpha = globalAlpha * 0.33
|
||||
}
|
||||
|
||||
ctx.fillStyle = this.colour
|
||||
ctx.fillStyle = colour
|
||||
ctx.lineWidth = Reroute.radius * 0.1
|
||||
ctx.strokeStyle = 'rgb(0,0,0,0.5)'
|
||||
ctx.fill()
|
||||
@@ -587,64 +618,23 @@ export class Reroute
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the input and output slots on the canvas, if the slots are visible.
|
||||
* @param ctx The canvas context to draw on.
|
||||
* Draws the input and output slots for this reroute.
|
||||
* @param ctx Canvas context to draw on.
|
||||
* @param state Ephemeral UI state for this frame: slot hover/outline flags.
|
||||
* @param colour Colour to use when a slot is hovered (renderer-provided).
|
||||
*/
|
||||
drawSlots(ctx: CanvasRenderingContext2D): void {
|
||||
this.#inputSlot.draw(ctx)
|
||||
this.#outputSlot.draw(ctx)
|
||||
}
|
||||
|
||||
drawHighlight(ctx: CanvasRenderingContext2D, colour: CanvasColour): void {
|
||||
const { pos } = this
|
||||
|
||||
const { strokeStyle, lineWidth } = ctx
|
||||
ctx.strokeStyle = colour
|
||||
ctx.lineWidth = 1
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], Reroute.radius * 1.5, 0, 2 * Math.PI)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates visibility of the input and output slots, based on the position of the pointer.
|
||||
* @param pos The position of the pointer.
|
||||
* @returns `true` if any changes require a redraw.
|
||||
*/
|
||||
updateVisibility(pos: Point): boolean {
|
||||
const input = this.#inputSlot
|
||||
const output = this.#outputSlot
|
||||
input.dirty = false
|
||||
output.dirty = false
|
||||
|
||||
const { firstFloatingLink } = this
|
||||
const hasLink = !!this.firstLink
|
||||
|
||||
const showInput = hasLink || firstFloatingLink?.isFloatingOutput
|
||||
const showOutput = hasLink || firstFloatingLink?.isFloatingInput
|
||||
const showEither = showInput || showOutput
|
||||
|
||||
// Check if even in the vicinity
|
||||
if (showEither && isPointInRect(pos, this.#hoverArea)) {
|
||||
const outlineOnly = this.#contains(pos)
|
||||
|
||||
if (showInput) input.update(pos, outlineOnly)
|
||||
if (showOutput) output.update(pos, outlineOnly)
|
||||
} else {
|
||||
this.hideSlots()
|
||||
}
|
||||
|
||||
return input.dirty || output.dirty
|
||||
}
|
||||
|
||||
/** Prevents rendering of the input and output slots. */
|
||||
hideSlots() {
|
||||
this.#inputSlot.hide()
|
||||
this.#outputSlot.hide()
|
||||
drawSlots(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
state: {
|
||||
inputHover: boolean
|
||||
inputOutline: boolean
|
||||
outputHover: boolean
|
||||
outputOutline: boolean
|
||||
},
|
||||
colour: CanvasColour
|
||||
): void {
|
||||
this.#inputSlot.draw(ctx, state.inputOutline, state.inputHover, colour)
|
||||
this.#outputSlot.draw(ctx, state.outputOutline, state.outputHover, colour)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -688,77 +678,30 @@ class RerouteSlot {
|
||||
return [x + Reroute.slotOffset * this.#offsetMultiplier, y]
|
||||
}
|
||||
|
||||
/** Whether any changes require a redraw. */
|
||||
dirty: boolean = false
|
||||
|
||||
#hovering = false
|
||||
/** Whether the pointer is hovering over the slot itself. */
|
||||
get hovering() {
|
||||
return this.#hovering
|
||||
}
|
||||
|
||||
set hovering(value) {
|
||||
if (!Object.is(this.#hovering, value)) {
|
||||
this.#hovering = value
|
||||
this.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
#showOutline = false
|
||||
/** Whether the slot outline / faint background is visible. */
|
||||
get showOutline() {
|
||||
return this.#showOutline
|
||||
}
|
||||
|
||||
set showOutline(value) {
|
||||
if (!Object.is(this.#showOutline, value)) {
|
||||
this.#showOutline = value
|
||||
this.dirty = true
|
||||
}
|
||||
}
|
||||
|
||||
constructor(reroute: Reroute, isInput: boolean) {
|
||||
this.#reroute = reroute
|
||||
this.#offsetMultiplier = isInput ? -1 : 1
|
||||
}
|
||||
|
||||
/**
|
||||
* Updates the slot's visibility based on the position of the pointer.
|
||||
* @param pos The position of the pointer.
|
||||
* @param outlineOnly If `true`, slot will display with the faded outline only ({@link showOutline}).
|
||||
*/
|
||||
update(pos: Point, outlineOnly?: boolean) {
|
||||
if (outlineOnly) {
|
||||
this.hovering = false
|
||||
this.showOutline = true
|
||||
} else {
|
||||
const dist = distance(this.pos, pos)
|
||||
this.hovering = dist <= 2 * Reroute.slotRadius
|
||||
this.showOutline = dist <= 5 * Reroute.slotRadius
|
||||
}
|
||||
}
|
||||
|
||||
/** Hides the slot. */
|
||||
hide() {
|
||||
this.hovering = false
|
||||
this.showOutline = false
|
||||
}
|
||||
|
||||
/**
|
||||
* Draws the slot on the canvas.
|
||||
* @param ctx The canvas context to draw on.
|
||||
* @param ctx Canvas 2D context to draw on.
|
||||
* @param showOutline Whether to render the faint slot outline/background.
|
||||
* @param hovering Whether the pointer is close enough to treat the slot as hovered.
|
||||
* @param colour The colour to use when hovered (provided by the renderer per-frame).
|
||||
*/
|
||||
draw(ctx: CanvasRenderingContext2D): void {
|
||||
draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
showOutline: boolean,
|
||||
hovering: boolean,
|
||||
colour: CanvasColour
|
||||
): void {
|
||||
const { fillStyle, strokeStyle, lineWidth } = ctx
|
||||
const {
|
||||
showOutline,
|
||||
hovering,
|
||||
pos: [x, y]
|
||||
} = this
|
||||
const [x, y] = this.pos
|
||||
if (!showOutline) return
|
||||
|
||||
try {
|
||||
ctx.fillStyle = hovering ? this.#reroute.colour : 'rgba(127,127,127,0.3)'
|
||||
ctx.fillStyle = hovering ? colour : 'rgba(127,127,127,0.3)'
|
||||
ctx.strokeStyle = 'rgb(0,0,0,0.5)'
|
||||
ctx.lineWidth = 1
|
||||
|
||||
|
||||
@@ -108,6 +108,8 @@ export class LinkConnector {
|
||||
readonly floatingLinks: LLink[] = []
|
||||
|
||||
readonly hiddenReroutes: Set<Reroute> = new Set()
|
||||
/** IDs of existing links currently being dragged (for render suppression). */
|
||||
readonly draggingLinkIds: Set<number> = new Set()
|
||||
|
||||
/** The widget beneath the pointer, if it is a valid connection target. */
|
||||
overWidget?: IBaseWidget
|
||||
@@ -131,6 +133,11 @@ export class LinkConnector {
|
||||
return this.state.draggingExistingLinks
|
||||
}
|
||||
|
||||
/** Returns true if the given link id is being dragged (existing link relocation). */
|
||||
isLinkBeingDragged(id: number | null | undefined): boolean {
|
||||
return id != null && this.draggingLinkIds.has(id)
|
||||
}
|
||||
|
||||
/** Drag an existing link to a different input. */
|
||||
moveInputLink(network: LinkNetwork, input: INodeInputSlot): void {
|
||||
if (this.isConnecting) throw new Error('Already dragging links.')
|
||||
@@ -171,7 +178,8 @@ export class LinkConnector {
|
||||
)
|
||||
}
|
||||
|
||||
floatingLink._dragging = true
|
||||
// Track floating link being dragged (existing link relocation)
|
||||
this.draggingLinkIds.add(floatingLink.id)
|
||||
this.floatingLinks.push(floatingLink)
|
||||
} else {
|
||||
const link = network.links.get(linkId)
|
||||
@@ -217,7 +225,7 @@ export class LinkConnector {
|
||||
return
|
||||
}
|
||||
|
||||
link._dragging = true
|
||||
this.draggingLinkIds.add(link.id)
|
||||
inputLinks.push(link)
|
||||
} else {
|
||||
// Regular node links
|
||||
@@ -247,7 +255,7 @@ export class LinkConnector {
|
||||
return
|
||||
}
|
||||
|
||||
link._dragging = true
|
||||
this.draggingLinkIds.add(link.id)
|
||||
inputLinks.push(link)
|
||||
}
|
||||
}
|
||||
@@ -288,6 +296,7 @@ export class LinkConnector {
|
||||
|
||||
renderLinks.push(renderLink)
|
||||
this.floatingLinks.push(floatingLink)
|
||||
this.draggingLinkIds.add(floatingLink.id)
|
||||
} catch (error) {
|
||||
console.warn(
|
||||
`Could not create render link for link id: [${floatingLink.id}].`,
|
||||
@@ -306,10 +315,9 @@ export class LinkConnector {
|
||||
|
||||
const firstReroute = LLink.getFirstReroute(network, link)
|
||||
if (firstReroute) {
|
||||
firstReroute._dragging = true
|
||||
this.hiddenReroutes.add(firstReroute)
|
||||
} else {
|
||||
link._dragging = true
|
||||
this.draggingLinkIds.add(link.id)
|
||||
}
|
||||
this.outputLinks.push(link)
|
||||
|
||||
@@ -1053,10 +1061,10 @@ export class LinkConnector {
|
||||
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 link of floatingLinks) delete link._dragging
|
||||
for (const reroute of hiddenReroutes) delete reroute._dragging
|
||||
// Clear tracked dragging links
|
||||
this.draggingLinkIds.clear()
|
||||
// Clear tracked reroutes hidden during drag
|
||||
hiddenReroutes.clear()
|
||||
|
||||
renderLinks.length = 0
|
||||
inputLinks.length = 0
|
||||
|
||||
33
src/lib/litegraph/src/canvas/RenderedLinkSegment.ts
Normal file
33
src/lib/litegraph/src/canvas/RenderedLinkSegment.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LinkId } from '@/lib/litegraph/src/LLink'
|
||||
import type { RerouteId } from '@/lib/litegraph/src/Reroute'
|
||||
import type { CanvasColour, LinkSegment } from '@/lib/litegraph/src/interfaces'
|
||||
|
||||
/**
|
||||
* Lightweight, render-only representation of a link segment used for hit testing and tooltips.
|
||||
* Decouples canvas state from the LLink data model.
|
||||
*/
|
||||
export class RenderedLinkSegment implements LinkSegment {
|
||||
readonly id: LinkId | RerouteId
|
||||
readonly origin_id: NodeId
|
||||
readonly origin_slot: number
|
||||
readonly parentId?: RerouteId
|
||||
|
||||
path?: Path2D
|
||||
readonly _pos: Float32Array = new Float32Array(2)
|
||||
_centreAngle?: number
|
||||
_dragging?: boolean
|
||||
colour?: CanvasColour
|
||||
|
||||
constructor(args: {
|
||||
id: LinkId | RerouteId
|
||||
origin_id: NodeId
|
||||
origin_slot: number
|
||||
parentId?: RerouteId
|
||||
}) {
|
||||
this.id = args.id
|
||||
this.origin_id = args.origin_id
|
||||
this.origin_slot = args.origin_slot
|
||||
this.parentId = args.parentId
|
||||
}
|
||||
}
|
||||
55
src/lib/litegraph/src/canvas/RerouteHover.ts
Normal file
55
src/lib/litegraph/src/canvas/RerouteHover.ts
Normal file
@@ -0,0 +1,55 @@
|
||||
import { Reroute } from '../Reroute'
|
||||
import type { Point } from '../interfaces'
|
||||
import type { RerouteSlotUiState } from './RerouteRenderer'
|
||||
|
||||
export function computeRerouteHoverState(
|
||||
reroute: Reroute,
|
||||
mouse: Point
|
||||
): RerouteSlotUiState {
|
||||
const [mx, my] = mouse
|
||||
const state: RerouteSlotUiState = {
|
||||
inputHover: false,
|
||||
inputOutline: false,
|
||||
outputHover: false,
|
||||
outputOutline: false
|
||||
}
|
||||
|
||||
const hasLink = reroute.firstLink != null
|
||||
const firstFloating = reroute.firstFloatingLink
|
||||
const showInput = hasLink || !!firstFloating?.isFloatingOutput
|
||||
const showOutput = hasLink || !!firstFloating?.isFloatingInput
|
||||
|
||||
if (!showInput && !showOutput) return state
|
||||
|
||||
const overBody = reroute.containsPoint([mx, my])
|
||||
|
||||
if (showInput) {
|
||||
if (overBody) {
|
||||
state.inputOutline = true
|
||||
} else {
|
||||
const ix = reroute.pos[0] - Reroute.slotOffset
|
||||
const iy = reroute.pos[1]
|
||||
const dx = mx - ix
|
||||
const dy = my - iy
|
||||
const dist = Math.hypot(dx, dy)
|
||||
state.inputHover = dist <= 2 * Reroute.slotRadius
|
||||
state.inputOutline = dist <= 5 * Reroute.slotRadius
|
||||
}
|
||||
}
|
||||
|
||||
if (showOutput) {
|
||||
if (overBody) {
|
||||
state.outputOutline = true
|
||||
} else {
|
||||
const ox = reroute.pos[0] + Reroute.slotOffset
|
||||
const oy = reroute.pos[1]
|
||||
const dx = mx - ox
|
||||
const dy = my - oy
|
||||
const dist = Math.hypot(dx, dy)
|
||||
state.outputHover = dist <= 2 * Reroute.slotRadius
|
||||
state.outputOutline = dist <= 5 * Reroute.slotRadius
|
||||
}
|
||||
}
|
||||
|
||||
return state
|
||||
}
|
||||
122
src/lib/litegraph/src/canvas/RerouteRenderer.ts
Normal file
122
src/lib/litegraph/src/canvas/RerouteRenderer.ts
Normal file
@@ -0,0 +1,122 @@
|
||||
import { Reroute } from '../Reroute'
|
||||
import type { CanvasColour, Point } from '../interfaces'
|
||||
|
||||
export interface RerouteSlotUiState {
|
||||
inputHover: boolean
|
||||
inputOutline: boolean
|
||||
outputHover: boolean
|
||||
outputOutline: boolean
|
||||
}
|
||||
|
||||
const DEFAULT_REROUTE_COLOUR: CanvasColour = '#18184d'
|
||||
|
||||
export function drawReroute(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
reroute: Reroute,
|
||||
backgroundPattern: CanvasPattern | undefined,
|
||||
colour: CanvasColour | undefined
|
||||
): void {
|
||||
const { globalAlpha } = ctx
|
||||
const { pos } = reroute
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], Reroute.radius, 0, 2 * Math.PI)
|
||||
|
||||
if (reroute.linkIds.size === 0) {
|
||||
ctx.fillStyle = backgroundPattern ?? '#797979'
|
||||
ctx.fill()
|
||||
ctx.globalAlpha = globalAlpha * 0.33
|
||||
}
|
||||
|
||||
ctx.fillStyle = colour ?? DEFAULT_REROUTE_COLOUR
|
||||
ctx.lineWidth = Reroute.radius * 0.1
|
||||
ctx.strokeStyle = 'rgb(0,0,0,0.5)'
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
|
||||
ctx.fillStyle = '#ffffff55'
|
||||
ctx.strokeStyle = 'rgb(0,0,0,0.3)'
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], Reroute.radius * 0.8, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
|
||||
if (reroute.selected) {
|
||||
ctx.strokeStyle = '#fff'
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], Reroute.radius * 1.2, 0, 2 * Math.PI)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
ctx.globalAlpha = globalAlpha
|
||||
}
|
||||
|
||||
export function drawRerouteHighlight(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
reroute: Reroute,
|
||||
colour: CanvasColour
|
||||
): void {
|
||||
const { pos } = reroute
|
||||
|
||||
const { strokeStyle, lineWidth } = ctx
|
||||
ctx.strokeStyle = colour
|
||||
ctx.lineWidth = 1
|
||||
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], Reroute.radius * 1.5, 0, 2 * Math.PI)
|
||||
ctx.stroke()
|
||||
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineWidth = lineWidth
|
||||
}
|
||||
|
||||
export function drawRerouteSlots(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
reroute: Reroute,
|
||||
state: RerouteSlotUiState,
|
||||
colour: CanvasColour | undefined
|
||||
): void {
|
||||
const c = colour ?? DEFAULT_REROUTE_COLOUR
|
||||
drawSlot(ctx, getInputPos(reroute), state.inputOutline, state.inputHover, c)
|
||||
drawSlot(
|
||||
ctx,
|
||||
getOutputPos(reroute),
|
||||
state.outputOutline,
|
||||
state.outputHover,
|
||||
c
|
||||
)
|
||||
}
|
||||
|
||||
function drawSlot(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
[x, y]: Point,
|
||||
showOutline: boolean,
|
||||
hovering: boolean,
|
||||
colour: CanvasColour
|
||||
) {
|
||||
if (!showOutline) return
|
||||
const { fillStyle, strokeStyle, lineWidth } = ctx
|
||||
try {
|
||||
ctx.fillStyle = hovering ? colour : 'rgba(127,127,127,0.3)'
|
||||
ctx.strokeStyle = 'rgb(0,0,0,0.5)'
|
||||
ctx.lineWidth = 1
|
||||
ctx.beginPath()
|
||||
ctx.arc(x, y, Reroute.slotRadius, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
ctx.stroke()
|
||||
} finally {
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.strokeStyle = strokeStyle
|
||||
ctx.lineWidth = lineWidth
|
||||
}
|
||||
}
|
||||
|
||||
export function getInputPos(reroute: Reroute): Point {
|
||||
const [x, y] = reroute.pos
|
||||
return [x - Reroute.slotOffset, y]
|
||||
}
|
||||
|
||||
export function getOutputPos(reroute: Reroute): Point {
|
||||
const [x, y] = reroute.pos
|
||||
return [x + Reroute.slotOffset, y]
|
||||
}
|
||||
175
src/lib/litegraph/src/canvas/SlotRenderer.ts
Normal file
175
src/lib/litegraph/src/canvas/SlotRenderer.ts
Normal file
@@ -0,0 +1,175 @@
|
||||
import { LabelPosition } from '@/lib/litegraph/src/draw'
|
||||
import type {
|
||||
DefaultConnectionColors,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import type { ReadOnlyPoint } from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { getCentre } from '@/lib/litegraph/src/measure'
|
||||
import { NodeInputSlot } from '@/lib/litegraph/src/node/NodeInputSlot'
|
||||
import type { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
|
||||
import { RenderShape } from '@/lib/litegraph/src/types/globalEnums'
|
||||
|
||||
export interface SlotDrawOptions {
|
||||
colorContext: DefaultConnectionColors
|
||||
labelPosition?: LabelPosition
|
||||
lowQuality?: boolean
|
||||
doStroke?: boolean
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
/** Draw a node input or output slot without coupling to the model class. */
|
||||
export function drawSlot(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
slot: NodeSlot,
|
||||
{
|
||||
colorContext,
|
||||
labelPosition = LabelPosition.Right,
|
||||
lowQuality = false,
|
||||
highlight = false,
|
||||
doStroke = false
|
||||
}: SlotDrawOptions
|
||||
) {
|
||||
// Save the current fillStyle and strokeStyle
|
||||
const originalFillStyle = ctx.fillStyle
|
||||
const originalStrokeStyle = ctx.strokeStyle
|
||||
const originalLineWidth = ctx.lineWidth
|
||||
|
||||
const labelColor = highlight ? slot.highlightColor : LiteGraph.NODE_TEXT_COLOR
|
||||
|
||||
const nodePos = slot.node.pos
|
||||
const { boundingRect } = slot
|
||||
const diameter = boundingRect[3]
|
||||
const [cx, cy] = getCentre([
|
||||
boundingRect[0] - nodePos[0],
|
||||
boundingRect[1] - nodePos[1],
|
||||
diameter,
|
||||
diameter
|
||||
])
|
||||
|
||||
const slot_type = slot.type
|
||||
const slot_shape = (slot_type === 'array' ? RenderShape.GRID : slot.shape) as
|
||||
| RenderShape
|
||||
| undefined
|
||||
|
||||
ctx.beginPath()
|
||||
let doFill = true
|
||||
|
||||
ctx.fillStyle = slot.renderingColor(colorContext)
|
||||
ctx.lineWidth = 1
|
||||
if (slot_type === LiteGraph.EVENT || slot_shape === RenderShape.BOX) {
|
||||
ctx.rect(cx - 6 + 0.5, cy - 5 + 0.5, 14, 10)
|
||||
} else if (slot_shape === RenderShape.ARROW) {
|
||||
ctx.moveTo(cx + 8, cy + 0.5)
|
||||
ctx.lineTo(cx - 4, cy + 6 + 0.5)
|
||||
ctx.lineTo(cx - 4, cy - 6 + 0.5)
|
||||
ctx.closePath()
|
||||
} else if (slot_shape === RenderShape.GRID) {
|
||||
const gridSize = 3
|
||||
const cellSize = 2
|
||||
const spacing = 3
|
||||
|
||||
for (let x = 0; x < gridSize; x++) {
|
||||
for (let y = 0; y < gridSize; y++) {
|
||||
ctx.rect(cx - 4 + x * spacing, cy - 4 + y * spacing, cellSize, cellSize)
|
||||
}
|
||||
}
|
||||
doStroke = false
|
||||
} else {
|
||||
// Default rendering for circle, hollow circle.
|
||||
if (lowQuality) {
|
||||
ctx.rect(cx - 4, cy - 4, 8, 8)
|
||||
} else {
|
||||
let radius: number
|
||||
if (slot_shape === RenderShape.HollowCircle) {
|
||||
doFill = false
|
||||
doStroke = true
|
||||
ctx.lineWidth = 3
|
||||
ctx.strokeStyle = ctx.fillStyle
|
||||
radius = highlight ? 4 : 3
|
||||
} else {
|
||||
// Normal circle
|
||||
radius = highlight ? 5 : 4
|
||||
}
|
||||
ctx.arc(cx, cy, radius, 0, Math.PI * 2)
|
||||
}
|
||||
}
|
||||
|
||||
if (doFill) ctx.fill()
|
||||
if (!lowQuality && doStroke) ctx.stroke()
|
||||
|
||||
// render slot label
|
||||
const hideLabel = lowQuality || slot.isWidgetInputSlot
|
||||
if (!hideLabel) {
|
||||
const text = slot.renderingLabel
|
||||
if (text) {
|
||||
ctx.fillStyle = labelColor
|
||||
if (labelPosition === LabelPosition.Right) {
|
||||
if (slot.dir == LiteGraph.UP) {
|
||||
ctx.fillText(text, cx, cy - 10)
|
||||
} else {
|
||||
ctx.fillText(text, cx + 10, cy + 5)
|
||||
}
|
||||
} else {
|
||||
if (slot.dir == LiteGraph.DOWN) {
|
||||
ctx.fillText(text, cx, cy - 8)
|
||||
} else {
|
||||
ctx.fillText(text, cx - 10, cy + 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a red circle if the slot has errors.
|
||||
if (slot.hasErrors) {
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = 'red'
|
||||
ctx.beginPath()
|
||||
ctx.arc(cx, cy, 12, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Restore the original fillStyle and strokeStyle
|
||||
ctx.fillStyle = originalFillStyle
|
||||
ctx.strokeStyle = originalStrokeStyle
|
||||
ctx.lineWidth = originalLineWidth
|
||||
}
|
||||
|
||||
/** Draw a minimal collapsed representation for the first connected slot. */
|
||||
export function drawCollapsedSlot(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
slot: INodeInputSlot | INodeOutputSlot,
|
||||
collapsedPos: ReadOnlyPoint
|
||||
) {
|
||||
const x = collapsedPos[0]
|
||||
const y = collapsedPos[1]
|
||||
|
||||
// Save original styles
|
||||
const { fillStyle } = ctx
|
||||
|
||||
ctx.fillStyle = '#686'
|
||||
ctx.beginPath()
|
||||
|
||||
if (slot.type === LiteGraph.EVENT || slot.shape === RenderShape.BOX) {
|
||||
ctx.rect(x - 7 + 0.5, y - 4, 14, 8)
|
||||
} else if (slot.shape === RenderShape.ARROW) {
|
||||
const isInput = slot instanceof NodeInputSlot
|
||||
if (isInput) {
|
||||
ctx.moveTo(x + 8, y)
|
||||
ctx.lineTo(x - 4, y - 4)
|
||||
ctx.lineTo(x - 4, y + 4)
|
||||
} else {
|
||||
ctx.moveTo(x + 6, y)
|
||||
ctx.lineTo(x - 6, y - 4)
|
||||
ctx.lineTo(x - 6, y + 4)
|
||||
}
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2)
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
// Restore original styles
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
@@ -87,6 +87,7 @@ export interface LGraphNodeConstructor<T extends LGraphNode = LGraphNode> {
|
||||
export { InputIndicators } from './canvas/InputIndicators'
|
||||
export { LinkConnector } from './canvas/LinkConnector'
|
||||
export { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
|
||||
export { drawSlot, drawCollapsedSlot } from './canvas/SlotRenderer'
|
||||
export { CanvasPointer } from './CanvasPointer'
|
||||
export * as Constants from './constants'
|
||||
export { ContextMenu } from './ContextMenu'
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LinkId } from '@/lib/litegraph/src/LLink'
|
||||
import { LabelPosition } from '@/lib/litegraph/src/draw'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
@@ -8,7 +7,7 @@ import type {
|
||||
ReadOnlyPoint
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
|
||||
import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
|
||||
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
||||
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
|
||||
import { isSubgraphInput } from '@/lib/litegraph/src/subgraph/subgraphUtils'
|
||||
@@ -61,20 +60,4 @@ export class NodeInputSlot extends NodeSlot implements INodeInputSlot {
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
override draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
options: Omit<IDrawOptions, 'doStroke' | 'labelPosition'>
|
||||
) {
|
||||
const { textAlign } = ctx
|
||||
ctx.textAlign = 'left'
|
||||
|
||||
super.draw(ctx, {
|
||||
...options,
|
||||
labelPosition: LabelPosition.Right,
|
||||
doStroke: false
|
||||
})
|
||||
|
||||
ctx.textAlign = textAlign
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LinkId } from '@/lib/litegraph/src/LLink'
|
||||
import { LabelPosition } from '@/lib/litegraph/src/draw'
|
||||
import type {
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
@@ -8,7 +7,7 @@ import type {
|
||||
ReadOnlyPoint
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { type IDrawOptions, NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
|
||||
import { NodeSlot } from '@/lib/litegraph/src/node/NodeSlot'
|
||||
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
||||
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
|
||||
import { isSubgraphOutput } from '@/lib/litegraph/src/subgraph/subgraphUtils'
|
||||
@@ -59,22 +58,4 @@ export class NodeOutputSlot extends NodeSlot implements INodeOutputSlot {
|
||||
override get isConnected(): boolean {
|
||||
return this.links != null && this.links.length > 0
|
||||
}
|
||||
|
||||
override draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
options: Omit<IDrawOptions, 'doStroke' | 'labelPosition'>
|
||||
) {
|
||||
const { textAlign, strokeStyle } = ctx
|
||||
ctx.textAlign = 'right'
|
||||
ctx.strokeStyle = 'black'
|
||||
|
||||
super.draw(ctx, {
|
||||
...options,
|
||||
labelPosition: LabelPosition.Left,
|
||||
doStroke: true
|
||||
})
|
||||
|
||||
ctx.textAlign = textAlign
|
||||
ctx.strokeStyle = strokeStyle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LabelPosition, SlotShape, SlotType } from '@/lib/litegraph/src/draw'
|
||||
import type {
|
||||
CanvasColour,
|
||||
DefaultConnectionColors,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
INodeSlot,
|
||||
@@ -12,45 +10,15 @@ import type {
|
||||
ReadOnlyPoint
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph, Rectangle } from '@/lib/litegraph/src/litegraph'
|
||||
import { getCentre } from '@/lib/litegraph/src/measure'
|
||||
import type { SubgraphInput } from '@/lib/litegraph/src/subgraph/SubgraphInput'
|
||||
import type { SubgraphOutput } from '@/lib/litegraph/src/subgraph/SubgraphOutput'
|
||||
import {
|
||||
LinkDirection,
|
||||
RenderShape
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
|
||||
import { NodeInputSlot } from './NodeInputSlot'
|
||||
import { SlotBase } from './SlotBase'
|
||||
|
||||
export interface IDrawOptions {
|
||||
colorContext: DefaultConnectionColors
|
||||
labelPosition?: LabelPosition
|
||||
lowQuality?: boolean
|
||||
doStroke?: boolean
|
||||
highlight?: boolean
|
||||
}
|
||||
|
||||
/** Shared base class for {@link LGraphNode} input and output slots. */
|
||||
export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||
pos?: Point
|
||||
|
||||
/** The offset from the parent node to the centre point of this slot. */
|
||||
get #centreOffset(): ReadOnlyPoint {
|
||||
const nodePos = this.node.pos
|
||||
const { boundingRect } = this
|
||||
|
||||
// Use height; widget input slots may be thinner.
|
||||
const diameter = boundingRect[3]
|
||||
|
||||
return getCentre([
|
||||
boundingRect[0] - nodePos[0],
|
||||
boundingRect[1] - nodePos[1],
|
||||
diameter,
|
||||
diameter
|
||||
])
|
||||
}
|
||||
|
||||
/** The center point of this slot when the node is collapsed. */
|
||||
abstract get collapsedPos(): ReadOnlyPoint
|
||||
|
||||
@@ -105,152 +73,4 @@ export abstract class NodeSlot extends SlotBase implements INodeSlot {
|
||||
get renderingLabel(): string {
|
||||
return this.label || this.localized_name || this.name || ''
|
||||
}
|
||||
|
||||
draw(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
{
|
||||
colorContext,
|
||||
labelPosition = LabelPosition.Right,
|
||||
lowQuality = false,
|
||||
highlight = false,
|
||||
doStroke = false
|
||||
}: IDrawOptions
|
||||
) {
|
||||
// Save the current fillStyle and strokeStyle
|
||||
const originalFillStyle = ctx.fillStyle
|
||||
const originalStrokeStyle = ctx.strokeStyle
|
||||
const originalLineWidth = ctx.lineWidth
|
||||
|
||||
const labelColor = highlight
|
||||
? this.highlightColor
|
||||
: LiteGraph.NODE_TEXT_COLOR
|
||||
|
||||
const pos = this.#centreOffset
|
||||
const slot_type = this.type
|
||||
const slot_shape = (
|
||||
slot_type === SlotType.Array ? SlotShape.Grid : this.shape
|
||||
) as SlotShape
|
||||
|
||||
ctx.beginPath()
|
||||
let doFill = true
|
||||
|
||||
ctx.fillStyle = this.renderingColor(colorContext)
|
||||
ctx.lineWidth = 1
|
||||
if (slot_type === SlotType.Event || slot_shape === SlotShape.Box) {
|
||||
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
||||
} else if (slot_shape === SlotShape.Arrow) {
|
||||
ctx.moveTo(pos[0] + 8, pos[1] + 0.5)
|
||||
ctx.lineTo(pos[0] - 4, pos[1] + 6 + 0.5)
|
||||
ctx.lineTo(pos[0] - 4, pos[1] - 6 + 0.5)
|
||||
ctx.closePath()
|
||||
} else if (slot_shape === SlotShape.Grid) {
|
||||
const gridSize = 3
|
||||
const cellSize = 2
|
||||
const spacing = 3
|
||||
|
||||
for (let x = 0; x < gridSize; x++) {
|
||||
for (let y = 0; y < gridSize; y++) {
|
||||
ctx.rect(
|
||||
pos[0] - 4 + x * spacing,
|
||||
pos[1] - 4 + y * spacing,
|
||||
cellSize,
|
||||
cellSize
|
||||
)
|
||||
}
|
||||
}
|
||||
doStroke = false
|
||||
} else {
|
||||
// Default rendering for circle, hollow circle.
|
||||
if (lowQuality) {
|
||||
ctx.rect(pos[0] - 4, pos[1] - 4, 8, 8)
|
||||
} else {
|
||||
let radius: number
|
||||
if (slot_shape === SlotShape.HollowCircle) {
|
||||
doFill = false
|
||||
doStroke = true
|
||||
ctx.lineWidth = 3
|
||||
ctx.strokeStyle = ctx.fillStyle
|
||||
radius = highlight ? 4 : 3
|
||||
} else {
|
||||
// Normal circle
|
||||
radius = highlight ? 5 : 4
|
||||
}
|
||||
ctx.arc(pos[0], pos[1], radius, 0, Math.PI * 2)
|
||||
}
|
||||
}
|
||||
|
||||
if (doFill) ctx.fill()
|
||||
if (!lowQuality && doStroke) ctx.stroke()
|
||||
|
||||
// render slot label
|
||||
const hideLabel = lowQuality || this.isWidgetInputSlot
|
||||
if (!hideLabel) {
|
||||
const text = this.renderingLabel
|
||||
if (text) {
|
||||
// TODO: Finish impl. Highlight text on mouseover unless we're connecting links.
|
||||
ctx.fillStyle = labelColor
|
||||
|
||||
if (labelPosition === LabelPosition.Right) {
|
||||
if (this.dir == LinkDirection.UP) {
|
||||
ctx.fillText(text, pos[0], pos[1] - 10)
|
||||
} else {
|
||||
ctx.fillText(text, pos[0] + 10, pos[1] + 5)
|
||||
}
|
||||
} else {
|
||||
if (this.dir == LinkDirection.DOWN) {
|
||||
ctx.fillText(text, pos[0], pos[1] - 8)
|
||||
} else {
|
||||
ctx.fillText(text, pos[0] - 10, pos[1] + 5)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Draw a red circle if the slot has errors.
|
||||
if (this.hasErrors) {
|
||||
ctx.lineWidth = 2
|
||||
ctx.strokeStyle = 'red'
|
||||
ctx.beginPath()
|
||||
ctx.arc(pos[0], pos[1], 12, 0, Math.PI * 2)
|
||||
ctx.stroke()
|
||||
}
|
||||
|
||||
// Restore the original fillStyle and strokeStyle
|
||||
ctx.fillStyle = originalFillStyle
|
||||
ctx.strokeStyle = originalStrokeStyle
|
||||
ctx.lineWidth = originalLineWidth
|
||||
}
|
||||
|
||||
drawCollapsed(ctx: CanvasRenderingContext2D) {
|
||||
const [x, y] = this.collapsedPos
|
||||
|
||||
// Save original styles
|
||||
const { fillStyle } = ctx
|
||||
|
||||
ctx.fillStyle = '#686'
|
||||
ctx.beginPath()
|
||||
|
||||
if (this.type === SlotType.Event || this.shape === RenderShape.BOX) {
|
||||
ctx.rect(x - 7 + 0.5, y - 4, 14, 8)
|
||||
} else if (this.shape === RenderShape.ARROW) {
|
||||
// Adjust arrow direction based on whether this is an input or output slot
|
||||
const isInput = this instanceof NodeInputSlot
|
||||
if (isInput) {
|
||||
ctx.moveTo(x + 8, y)
|
||||
ctx.lineTo(x - 4, y - 4)
|
||||
ctx.lineTo(x - 4, y + 4)
|
||||
} else {
|
||||
ctx.moveTo(x + 6, y)
|
||||
ctx.lineTo(x - 6, y - 4)
|
||||
ctx.lineTo(x - 6, y + 4)
|
||||
}
|
||||
ctx.closePath()
|
||||
} else {
|
||||
ctx.arc(x, y, 4, 0, Math.PI * 2)
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
// Restore original styles
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
}
|
||||
|
||||
@@ -116,7 +116,6 @@ export class SubgraphInput extends SubgraphSlot {
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds.add(link.id)
|
||||
if (reroute.floating) delete reroute.floating
|
||||
reroute._dragging = undefined
|
||||
}
|
||||
|
||||
// If this is the terminus of a floating link, remove it
|
||||
|
||||
@@ -86,7 +86,6 @@ export class SubgraphOutput extends SubgraphSlot {
|
||||
for (const reroute of reroutes) {
|
||||
reroute.linkIds.add(link.id)
|
||||
if (reroute.floating) delete reroute.floating
|
||||
reroute._dragging = undefined
|
||||
}
|
||||
|
||||
// If this is the terminus of a floating link, remove it
|
||||
|
||||
@@ -124,7 +124,7 @@ describe('LinkConnector', () => {
|
||||
expect(connector.state.connectingTo).toBe('input')
|
||||
expect(connector.state.draggingExistingLinks).toBe(true)
|
||||
expect(connector.inputLinks).toContain(link)
|
||||
expect(link._dragging).toBe(true)
|
||||
expect(connector.isLinkBeingDragged(link.id)).toBe(true)
|
||||
})
|
||||
|
||||
test('should not move input link if already connecting', ({
|
||||
@@ -162,7 +162,7 @@ describe('LinkConnector', () => {
|
||||
expect(connector.state.draggingExistingLinks).toBe(true)
|
||||
expect(connector.state.multi).toBe(true)
|
||||
expect(connector.outputLinks).toContain(link)
|
||||
expect(link._dragging).toBe(true)
|
||||
expect(connector.isLinkBeingDragged(link.id)).toBe(true)
|
||||
})
|
||||
|
||||
test('should not move output link if already connecting', ({
|
||||
@@ -253,12 +253,11 @@ describe('LinkConnector', () => {
|
||||
connector.state.draggingExistingLinks = true
|
||||
|
||||
const link = new LLink(1, 'number', 1, 0, 2, 0)
|
||||
link._dragging = true
|
||||
connector.inputLinks.push(link)
|
||||
connector.draggingLinkIds.add(link.id)
|
||||
|
||||
const reroute = new Reroute(1, network)
|
||||
reroute.pos = [0, 0]
|
||||
reroute._dragging = true
|
||||
connector.hiddenReroutes.add(reroute)
|
||||
|
||||
connector.reset()
|
||||
@@ -272,8 +271,7 @@ describe('LinkConnector', () => {
|
||||
expect(connector.inputLinks).toEqual([])
|
||||
expect(connector.outputLinks).toEqual([])
|
||||
expect(connector.hiddenReroutes.size).toBe(0)
|
||||
expect(link._dragging).toBeUndefined()
|
||||
expect(reroute._dragging).toBeUndefined()
|
||||
expect(connector.draggingLinkIds.size).toBe(0)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
@@ -159,8 +159,8 @@ describe('Subgraph slot connections', () => {
|
||||
expect(connector.inputLinks).toHaveLength(1)
|
||||
expect(connector.inputLinks[0]).toBe(link)
|
||||
|
||||
// Verify the link is marked as dragging
|
||||
expect(link!._dragging).toBe(true)
|
||||
// Verify the link is marked as dragging (via connector state)
|
||||
expect(connector.isLinkBeingDragged(link!.id)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
|
||||
Reference in New Issue
Block a user