Decouple link/reroute rendering from models: use RenderedLinkSegment; compute reroute controls without mutations; remove render-time dragging flags

This commit is contained in:
Benjamin Lu
2025-08-08 16:45:16 -04:00
parent 2c121a4a3c
commit d95f177818
10 changed files with 353 additions and 257 deletions

View File

@@ -8,6 +8,7 @@ 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 { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
import { strokeShape } from './draw'
import type {
@@ -5470,7 +5471,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,
@@ -5490,7 +5490,6 @@ export class LGraphCanvas
const endPos = reroute.pos
const startDirection = node.outputs[link.origin_slot]?.dir
link._dragging = true
this.#renderAllLinkSegments(
ctx,
link,
@@ -5550,6 +5549,10 @@ export class LGraphCanvas
// 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
@@ -5558,7 +5561,6 @@ export class LGraphCanvas
// Only render once
if (!renderedPaths.has(reroute)) {
renderedPaths.add(reroute)
visibleReroutes.push(reroute)
reroute._colour =
link.color ||
@@ -5567,10 +5569,16 @@ export class LGraphCanvas
const prevReroute = graph.getReroute(reroute.parentId)
const rerouteStartPos = prevReroute?.pos ?? startPos
reroute.calculateAngle(this.last_draw_time, graph, rerouteStartPos)
const params = reroute.computeRenderParams(graph, rerouteStartPos)
// 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
})
this.renderLink(
ctx,
rerouteStartPos,
@@ -5583,35 +5591,45 @@ export class LGraphCanvas
LinkDirection.CENTER,
{
startControl,
endControl: reroute.controlPoint,
reroute,
disabled
endControl: params.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 {
// Calculate start control for the next iter control point
const nextPos = reroutes[j + 1]?.pos ?? endPos
const prevR = graph.getReroute(reroute.parentId)
const startPosForParams = prevR?.pos ?? startPos
const params = reroute.computeRenderParams(graph, startPosForParams)
const dist = Math.min(
Reroute.maxSplineOffset,
distance(reroute.pos, nextPos) * 0.25
)
startControl = [dist * reroute.cos, dist * reroute.sin]
startControl = [dist * params.cos, dist * params.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
})
this.renderLink(
ctx,
segmentStartPos,
@@ -5622,10 +5640,17 @@ 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) {
const rendered = new RenderedLinkSegment({
id: link.id,
origin_id: link.origin_id,
origin_slot: link.origin_slot,
parentId: link.parentId
})
this.renderLink(
ctx,
startPos,
@@ -5635,10 +5660,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) {
@@ -5687,7 +5713,8 @@ export class LGraphCanvas
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
@@ -5699,6 +5726,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 if not using a reroute */
renderTarget?: RenderedLinkSegment
} = {}
): void {
const linkColour =
@@ -5729,13 +5758,13 @@ export class LGraphCanvas
const path = new Path2D()
/** The link or reroute we're currently rendering */
const linkSegment = reroute ?? link
const linkSegment = reroute ?? 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++) {

View File

@@ -36,9 +36,12 @@ import type {
} from './interfaces'
import {
type LGraphNodeConstructor,
LabelPosition,
LiteGraph,
type Subgraph,
type SubgraphNode
type SubgraphNode,
drawCollapsedSlot,
drawSlot
} from './litegraph'
import {
createBounds,
@@ -3894,13 +3897,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
}
}
@@ -4034,11 +4037,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
}
}
}
}

View File

@@ -11,7 +11,6 @@ import type {
INodeOutputSlot,
ISlotType,
LinkNetwork,
LinkSegment,
ReadonlyLinkNetwork
} from './interfaces'
import type {
@@ -86,7 +85,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 */
@@ -104,14 +103,8 @@ 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
@@ -166,8 +159,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} */
@@ -199,7 +190,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() ?? []
@@ -207,7 +198,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)
}
@@ -222,7 +213,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

View File

@@ -537,6 +537,66 @@ 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 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, thisObj.id)
if (!pos) continue
const angle = getDirection(thisPos, pos)
angles.push(angle)
sum += angle
}
}
// Preserve lexical `this` values inside helper
// eslint-disable-next-line @typescript-eslint/no-this-alias
const thisObj = this
}
/**
* Renders the reroute on the canvas.
* @param ctx Canvas context to draw on

View File

@@ -0,0 +1,32 @@
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 { 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
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
}
}

View 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
}

View File

@@ -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'

View File

@@ -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
}
}

View File

@@ -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
}
}

View File

@@ -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
}
}