diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index a4ff279221..7769857b98 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -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() + // Per-frame reroute colours computed while building segments + const rerouteColours = new Map() + // 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, + rerouteColours: Map ) { // 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, + rerouteColours?: Map, 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++) { diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 14bfee4859..cbd357afa3 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -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 + } } } } diff --git a/src/lib/litegraph/src/LLink.ts b/src/lib/litegraph/src/LLink.ts index b8de0f9304..5c99b6f7c8 100644 --- a/src/lib/litegraph/src/LLink.ts +++ b/src/lib/litegraph/src/LLink.ts @@ -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 { +export class LLink implements Serialisable { static _drawDebug = false /** Link ID */ @@ -105,17 +104,11 @@ export class LLink implements LinkSegment, Serialisable { 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 { 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 { */ static getReroutes( network: Pick, - 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 { static getFirstReroute( network: Pick, - linkSegment: LinkSegment + linkSegment: { parentId?: RerouteId } ): Reroute | undefined { return LLink.getReroutes(network, linkSegment).at(0) } @@ -223,7 +214,7 @@ export class LLink implements LinkSegment, Serialisable { */ static findNextReroute( network: Pick, - linkSegment: LinkSegment, + linkSegment: { parentId?: RerouteId }, rerouteId: RerouteId ): Reroute | null | undefined { if (!linkSegment.parentId) return diff --git a/src/lib/litegraph/src/Reroute.ts b/src/lib/litegraph/src/Reroute.ts index 886930227c..e020320f36 100644 --- a/src/lib/litegraph/src/Reroute.ts +++ b/src/lib/litegraph/src/Reroute.ts @@ -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 + implements Positionable, Serialisable { 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, + links: ReadonlyMap + ) { + 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 diff --git a/src/lib/litegraph/src/canvas/LinkConnector.ts b/src/lib/litegraph/src/canvas/LinkConnector.ts index 8f925bbc7c..4afacba530 100644 --- a/src/lib/litegraph/src/canvas/LinkConnector.ts +++ b/src/lib/litegraph/src/canvas/LinkConnector.ts @@ -108,6 +108,8 @@ export class LinkConnector { readonly floatingLinks: LLink[] = [] readonly hiddenReroutes: Set = new Set() + /** IDs of existing links currently being dragged (for render suppression). */ + readonly draggingLinkIds: Set = 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 diff --git a/src/lib/litegraph/src/canvas/RenderedLinkSegment.ts b/src/lib/litegraph/src/canvas/RenderedLinkSegment.ts new file mode 100644 index 0000000000..8bc5b9f332 --- /dev/null +++ b/src/lib/litegraph/src/canvas/RenderedLinkSegment.ts @@ -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 + } +} diff --git a/src/lib/litegraph/src/canvas/RerouteHover.ts b/src/lib/litegraph/src/canvas/RerouteHover.ts new file mode 100644 index 0000000000..31b4a1e8b9 --- /dev/null +++ b/src/lib/litegraph/src/canvas/RerouteHover.ts @@ -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 +} diff --git a/src/lib/litegraph/src/canvas/RerouteRenderer.ts b/src/lib/litegraph/src/canvas/RerouteRenderer.ts new file mode 100644 index 0000000000..a4a63f7247 --- /dev/null +++ b/src/lib/litegraph/src/canvas/RerouteRenderer.ts @@ -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] +} diff --git a/src/lib/litegraph/src/canvas/SlotRenderer.ts b/src/lib/litegraph/src/canvas/SlotRenderer.ts new file mode 100644 index 0000000000..1c338fbb34 --- /dev/null +++ b/src/lib/litegraph/src/canvas/SlotRenderer.ts @@ -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 +} diff --git a/src/lib/litegraph/src/litegraph.ts b/src/lib/litegraph/src/litegraph.ts index ee6f124a41..11c9a79740 100644 --- a/src/lib/litegraph/src/litegraph.ts +++ b/src/lib/litegraph/src/litegraph.ts @@ -87,6 +87,7 @@ export interface LGraphNodeConstructor { 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' diff --git a/src/lib/litegraph/src/node/NodeInputSlot.ts b/src/lib/litegraph/src/node/NodeInputSlot.ts index 8a6af55e9d..7cb732cea9 100644 --- a/src/lib/litegraph/src/node/NodeInputSlot.ts +++ b/src/lib/litegraph/src/node/NodeInputSlot.ts @@ -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 - ) { - const { textAlign } = ctx - ctx.textAlign = 'left' - - super.draw(ctx, { - ...options, - labelPosition: LabelPosition.Right, - doStroke: false - }) - - ctx.textAlign = textAlign - } } diff --git a/src/lib/litegraph/src/node/NodeOutputSlot.ts b/src/lib/litegraph/src/node/NodeOutputSlot.ts index a1120dd8ec..6d4cdd869f 100644 --- a/src/lib/litegraph/src/node/NodeOutputSlot.ts +++ b/src/lib/litegraph/src/node/NodeOutputSlot.ts @@ -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 - ) { - 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 - } } diff --git a/src/lib/litegraph/src/node/NodeSlot.ts b/src/lib/litegraph/src/node/NodeSlot.ts index 527c1dfac2..910727ea4e 100644 --- a/src/lib/litegraph/src/node/NodeSlot.ts +++ b/src/lib/litegraph/src/node/NodeSlot.ts @@ -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 - } } diff --git a/src/lib/litegraph/src/subgraph/SubgraphInput.ts b/src/lib/litegraph/src/subgraph/SubgraphInput.ts index 63465d2b53..4094ad1545 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphInput.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphInput.ts @@ -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 diff --git a/src/lib/litegraph/src/subgraph/SubgraphOutput.ts b/src/lib/litegraph/src/subgraph/SubgraphOutput.ts index 1c3479b842..08fa4a8118 100644 --- a/src/lib/litegraph/src/subgraph/SubgraphOutput.ts +++ b/src/lib/litegraph/src/subgraph/SubgraphOutput.ts @@ -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 diff --git a/src/lib/litegraph/test/LinkConnector.test.ts b/src/lib/litegraph/test/LinkConnector.test.ts index b21d8a55a7..8ff7296ac7 100644 --- a/src/lib/litegraph/test/LinkConnector.test.ts +++ b/src/lib/litegraph/test/LinkConnector.test.ts @@ -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) }) }) diff --git a/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts b/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts index 05cf543a23..720f254b7b 100644 --- a/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts +++ b/src/lib/litegraph/test/subgraph/SubgraphSlotConnections.test.ts @@ -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) }) })