mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +00:00
refactor(litegraph): decouple render-time state from models for reroutes and links\n\nIntroduce RenderedLinkSegment; compute reroute render params without mutating model; render into ephemeral segments instead of writing to Reroute/LLink.
This commit is contained in:
@@ -8,6 +8,7 @@ import { LGraphGroup } from './LGraphGroup'
|
|||||||
import { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode'
|
import { LGraphNode, type NodeId, type NodeProperty } from './LGraphNode'
|
||||||
import { LLink, type LinkId } from './LLink'
|
import { LLink, type LinkId } from './LLink'
|
||||||
import { Reroute, type RerouteId } from './Reroute'
|
import { Reroute, type RerouteId } from './Reroute'
|
||||||
|
import { RenderedLinkSegment } from './canvas/RenderedLinkSegment'
|
||||||
import { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
|
import { isOverNodeInput, isOverNodeOutput } from './canvas/measureSlots'
|
||||||
import { strokeShape } from './draw'
|
import { strokeShape } from './draw'
|
||||||
import type {
|
import type {
|
||||||
@@ -5470,7 +5471,6 @@ export class LGraphCanvas
|
|||||||
const endPos = node.getInputPos(link.target_slot)
|
const endPos = node.getInputPos(link.target_slot)
|
||||||
const endDirection = node.inputs[link.target_slot]?.dir
|
const endDirection = node.inputs[link.target_slot]?.dir
|
||||||
|
|
||||||
firstReroute._dragging = true
|
|
||||||
this.#renderAllLinkSegments(
|
this.#renderAllLinkSegments(
|
||||||
ctx,
|
ctx,
|
||||||
link,
|
link,
|
||||||
@@ -5490,7 +5490,6 @@ export class LGraphCanvas
|
|||||||
const endPos = reroute.pos
|
const endPos = reroute.pos
|
||||||
const startDirection = node.outputs[link.origin_slot]?.dir
|
const startDirection = node.outputs[link.origin_slot]?.dir
|
||||||
|
|
||||||
link._dragging = true
|
|
||||||
this.#renderAllLinkSegments(
|
this.#renderAllLinkSegments(
|
||||||
ctx,
|
ctx,
|
||||||
link,
|
link,
|
||||||
@@ -5550,6 +5549,10 @@ export class LGraphCanvas
|
|||||||
|
|
||||||
// Has reroutes
|
// Has reroutes
|
||||||
if (reroutes.length) {
|
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
|
let startControl: Point | undefined
|
||||||
|
|
||||||
const l = reroutes.length
|
const l = reroutes.length
|
||||||
@@ -5558,7 +5561,6 @@ export class LGraphCanvas
|
|||||||
|
|
||||||
// Only render once
|
// Only render once
|
||||||
if (!renderedPaths.has(reroute)) {
|
if (!renderedPaths.has(reroute)) {
|
||||||
renderedPaths.add(reroute)
|
|
||||||
visibleReroutes.push(reroute)
|
visibleReroutes.push(reroute)
|
||||||
reroute._colour =
|
reroute._colour =
|
||||||
link.color ||
|
link.color ||
|
||||||
@@ -5567,10 +5569,16 @@ export class LGraphCanvas
|
|||||||
|
|
||||||
const prevReroute = graph.getReroute(reroute.parentId)
|
const prevReroute = graph.getReroute(reroute.parentId)
|
||||||
const rerouteStartPos = prevReroute?.pos ?? startPos
|
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
|
// 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(
|
this.renderLink(
|
||||||
ctx,
|
ctx,
|
||||||
rerouteStartPos,
|
rerouteStartPos,
|
||||||
@@ -5583,35 +5591,45 @@ export class LGraphCanvas
|
|||||||
LinkDirection.CENTER,
|
LinkDirection.CENTER,
|
||||||
{
|
{
|
||||||
startControl,
|
startControl,
|
||||||
endControl: reroute.controlPoint,
|
endControl: params.controlPoint,
|
||||||
reroute,
|
disabled,
|
||||||
disabled
|
renderTarget: rendered
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
renderedPaths.add(rendered)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!startControl && reroutes.at(-1)?.floating?.slotType === 'input') {
|
if (!startControl && skipFirstSegment) {
|
||||||
// Floating link connected to an input
|
// Floating link connected to an input
|
||||||
startControl = [0, 0]
|
startControl = [0, 0]
|
||||||
} else {
|
} else {
|
||||||
// Calculate start control for the next iter control point
|
// Calculate start control for the next iter control point
|
||||||
const nextPos = reroutes[j + 1]?.pos ?? endPos
|
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(
|
const dist = Math.min(
|
||||||
Reroute.maxSplineOffset,
|
Reroute.maxSplineOffset,
|
||||||
distance(reroute.pos, nextPos) * 0.25
|
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
|
// For floating links from output, skip the last segment
|
||||||
if (link._dragging) return
|
if (skipLastSegment) return
|
||||||
|
|
||||||
// Use runtime fallback; TypeScript cannot evaluate this correctly.
|
// Use runtime fallback; TypeScript cannot evaluate this correctly.
|
||||||
const segmentStartPos = points.at(-2) ?? startPos
|
const segmentStartPos = points.at(-2) ?? startPos
|
||||||
|
|
||||||
// Render final link segment
|
// 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(
|
this.renderLink(
|
||||||
ctx,
|
ctx,
|
||||||
segmentStartPos,
|
segmentStartPos,
|
||||||
@@ -5622,10 +5640,17 @@ export class LGraphCanvas
|
|||||||
null,
|
null,
|
||||||
LinkDirection.CENTER,
|
LinkDirection.CENTER,
|
||||||
end_dir,
|
end_dir,
|
||||||
{ startControl, disabled }
|
{ startControl, disabled, renderTarget: rendered }
|
||||||
)
|
)
|
||||||
|
renderedPaths.add(rendered)
|
||||||
// Skip normal render when link is being dragged
|
// Skip normal render when link is being dragged
|
||||||
} else if (!link._dragging) {
|
} 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(
|
this.renderLink(
|
||||||
ctx,
|
ctx,
|
||||||
startPos,
|
startPos,
|
||||||
@@ -5635,10 +5660,11 @@ export class LGraphCanvas
|
|||||||
0,
|
0,
|
||||||
null,
|
null,
|
||||||
start_dir,
|
start_dir,
|
||||||
end_dir
|
end_dir,
|
||||||
|
{ renderTarget: rendered }
|
||||||
)
|
)
|
||||||
|
renderedPaths.add(rendered)
|
||||||
}
|
}
|
||||||
renderedPaths.add(link)
|
|
||||||
|
|
||||||
// event triggered rendered on top
|
// event triggered rendered on top
|
||||||
if (link?._last_time && now - link._last_time < 1000) {
|
if (link?._last_time && now - link._last_time < 1000) {
|
||||||
@@ -5687,7 +5713,8 @@ export class LGraphCanvas
|
|||||||
endControl,
|
endControl,
|
||||||
reroute,
|
reroute,
|
||||||
num_sublines = 1,
|
num_sublines = 1,
|
||||||
disabled = false
|
disabled = false,
|
||||||
|
renderTarget
|
||||||
}: {
|
}: {
|
||||||
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
|
/** When defined, render data will be saved to this reroute instead of the {@link link}. */
|
||||||
reroute?: Reroute
|
reroute?: Reroute
|
||||||
@@ -5699,6 +5726,8 @@ export class LGraphCanvas
|
|||||||
num_sublines?: number
|
num_sublines?: number
|
||||||
/** Whether this is a floating link segment */
|
/** Whether this is a floating link segment */
|
||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
|
/** Where to store the drawn path for hit testing if not using a reroute */
|
||||||
|
renderTarget?: RenderedLinkSegment
|
||||||
} = {}
|
} = {}
|
||||||
): void {
|
): void {
|
||||||
const linkColour =
|
const linkColour =
|
||||||
@@ -5729,13 +5758,13 @@ export class LGraphCanvas
|
|||||||
const path = new Path2D()
|
const path = new Path2D()
|
||||||
|
|
||||||
/** The link or reroute we're currently rendering */
|
/** The link or reroute we're currently rendering */
|
||||||
const linkSegment = reroute ?? link
|
const linkSegment = reroute ?? renderTarget
|
||||||
if (linkSegment) linkSegment.path = path
|
if (linkSegment) linkSegment.path = path
|
||||||
|
|
||||||
const innerA = LGraphCanvas.#lTempA
|
const innerA = LGraphCanvas.#lTempA
|
||||||
const innerB = LGraphCanvas.#lTempB
|
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]
|
const pos: Point = linkSegment?._pos ?? [0, 0]
|
||||||
|
|
||||||
for (let i = 0; i < num_sublines; i++) {
|
for (let i = 0; i < num_sublines; i++) {
|
||||||
|
|||||||
@@ -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.
|
* Renders the reroute on the canvas.
|
||||||
* @param ctx Canvas context to draw on
|
* @param ctx Canvas context to draw on
|
||||||
|
|||||||
32
src/lib/litegraph/src/canvas/RenderedLinkSegment.ts
Normal file
32
src/lib/litegraph/src/canvas/RenderedLinkSegment.ts
Normal 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
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user