mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
Totally not scuffed renderer and adapter
This commit is contained in:
@@ -1,4 +1,9 @@
|
||||
import { LinkConnector } from '@/lib/litegraph/src/canvas/LinkConnector'
|
||||
import {
|
||||
type LinkRenderContext,
|
||||
LitegraphLinkAdapter
|
||||
} from '@/rendering/adapters/LitegraphLinkAdapter'
|
||||
import { layoutStore } from '@/stores/layoutStore'
|
||||
|
||||
import { CanvasPointer } from './CanvasPointer'
|
||||
import type { ContextMenu } from './ContextMenu'
|
||||
@@ -47,7 +52,6 @@ import {
|
||||
containsRect,
|
||||
createBounds,
|
||||
distance,
|
||||
findPointOnCurve,
|
||||
isInRect,
|
||||
isInRectangle,
|
||||
isPointInRect,
|
||||
@@ -232,9 +236,6 @@ export class LGraphCanvas
|
||||
static #tmp_area = new Float32Array(4)
|
||||
static #margin_area = new Float32Array(4)
|
||||
static #link_bounding = new Float32Array(4)
|
||||
static #lTempA: Point = new Float32Array(2)
|
||||
static #lTempB: Point = new Float32Array(2)
|
||||
static #lTempC: Point = new Float32Array(2)
|
||||
|
||||
static DEFAULT_BACKGROUND_IMAGE =
|
||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
|
||||
@@ -636,6 +637,9 @@ export class LGraphCanvas
|
||||
/** Set on keydown, keyup. @todo */
|
||||
#shiftDown: boolean = false
|
||||
|
||||
/** Link rendering adapter for litegraph-to-canvas integration */
|
||||
linkRenderer: LitegraphLinkAdapter | null = null
|
||||
|
||||
/** If true, enable drag zoom. Ctrl+Shift+Drag Up/Down: zoom canvas. */
|
||||
dragZoomEnabled: boolean = false
|
||||
/** The start position of the drag zoom. */
|
||||
@@ -697,6 +701,11 @@ export class LGraphCanvas
|
||||
this.ds = new DragAndScale(canvas)
|
||||
this.pointer = new CanvasPointer(canvas)
|
||||
|
||||
// Initialize link renderer if graph is available
|
||||
if (graph) {
|
||||
this.linkRenderer = new LitegraphLinkAdapter(layoutStore, graph)
|
||||
}
|
||||
|
||||
this.linkConnector.events.addEventListener('link-created', () =>
|
||||
this.#dirty()
|
||||
)
|
||||
@@ -1790,6 +1799,9 @@ export class LGraphCanvas
|
||||
this.clear()
|
||||
newGraph.attachCanvas(this)
|
||||
|
||||
// Re-initialize link renderer with new graph
|
||||
this.linkRenderer = new LitegraphLinkAdapter(layoutStore, newGraph)
|
||||
|
||||
this.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
|
||||
this.#dirty()
|
||||
}
|
||||
@@ -4538,18 +4550,26 @@ export class LGraphCanvas
|
||||
: LiteGraph.CONNECTING_LINK_COLOR
|
||||
|
||||
// the connection being dragged by the mouse
|
||||
this.renderLink(
|
||||
ctx,
|
||||
pos,
|
||||
highlightPos,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
colour,
|
||||
fromDirection,
|
||||
dragDirection
|
||||
)
|
||||
if (this.linkRenderer) {
|
||||
const context = this.buildLinkRenderContext()
|
||||
this.linkRenderer.renderLinkDirect(
|
||||
ctx,
|
||||
pos,
|
||||
highlightPos,
|
||||
null,
|
||||
false,
|
||||
null,
|
||||
colour,
|
||||
fromDirection,
|
||||
dragDirection,
|
||||
context,
|
||||
{
|
||||
disabled: false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
ctx.fillStyle = colour
|
||||
ctx.beginPath()
|
||||
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
|
||||
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
||||
@@ -5660,6 +5680,34 @@ export class LGraphCanvas
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Build LinkRenderContext from canvas properties
|
||||
* Helper method for using LitegraphLinkAdapter
|
||||
*/
|
||||
private buildLinkRenderContext(): LinkRenderContext {
|
||||
return {
|
||||
// Canvas settings
|
||||
renderMode: this.links_render_mode,
|
||||
connectionWidth: this.connections_width,
|
||||
renderBorder: this.render_connections_border,
|
||||
lowQuality: this.low_quality,
|
||||
highQualityRender: this.highquality_render,
|
||||
scale: this.ds.scale,
|
||||
linkMarkerShape: this.linkMarkerShape,
|
||||
renderConnectionArrows: this.render_connection_arrows,
|
||||
|
||||
// State
|
||||
highlightedLinks: new Set(Object.keys(this.highlighted_links)),
|
||||
|
||||
// Colors
|
||||
defaultLinkColor: this.default_link_color,
|
||||
linkTypeColors: LGraphCanvas.link_type_colors,
|
||||
|
||||
// Pattern for disabled links
|
||||
disabledPattern: this._pattern
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* draws a link between two points
|
||||
* @param ctx Canvas 2D rendering context
|
||||
@@ -5701,333 +5749,27 @@ export class LGraphCanvas
|
||||
disabled?: boolean
|
||||
} = {}
|
||||
): void {
|
||||
const linkColour =
|
||||
link != null && this.highlighted_links[link.id]
|
||||
? '#FFF'
|
||||
: color ||
|
||||
link?.color ||
|
||||
(link?.type != null && LGraphCanvas.link_type_colors[link.type]) ||
|
||||
this.default_link_color
|
||||
const startDir = start_dir || LinkDirection.RIGHT
|
||||
const endDir = end_dir || LinkDirection.LEFT
|
||||
|
||||
const dist =
|
||||
this.links_render_mode == LinkRenderType.SPLINE_LINK &&
|
||||
(!endControl || !startControl)
|
||||
? distance(a, b)
|
||||
: 0
|
||||
|
||||
// TODO: Subline code below was inserted in the wrong place - should be before this statement
|
||||
if (this.render_connections_border && !this.low_quality) {
|
||||
ctx.lineWidth = this.connections_width + 4
|
||||
}
|
||||
ctx.lineJoin = 'round'
|
||||
num_sublines ||= 1
|
||||
if (num_sublines > 1) ctx.lineWidth = 0.5
|
||||
|
||||
// begin line shape
|
||||
const path = new Path2D()
|
||||
|
||||
/** The link or reroute we're currently rendering */
|
||||
const linkSegment = reroute ?? link
|
||||
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. */
|
||||
const pos: Point = linkSegment?._pos ?? [0, 0]
|
||||
|
||||
for (let i = 0; i < num_sublines; i++) {
|
||||
const offsety = (i - (num_sublines - 1) * 0.5) * 5
|
||||
innerA[0] = a[0]
|
||||
innerA[1] = a[1]
|
||||
innerB[0] = b[0]
|
||||
innerB[1] = b[1]
|
||||
|
||||
if (this.links_render_mode == LinkRenderType.SPLINE_LINK) {
|
||||
if (endControl) {
|
||||
innerB[0] = b[0] + endControl[0]
|
||||
innerB[1] = b[1] + endControl[1]
|
||||
} else {
|
||||
this.#addSplineOffset(innerB, endDir, dist)
|
||||
if (this.linkRenderer) {
|
||||
const context = this.buildLinkRenderContext()
|
||||
this.linkRenderer.renderLinkDirect(
|
||||
ctx,
|
||||
a,
|
||||
b,
|
||||
link,
|
||||
skip_border,
|
||||
flow,
|
||||
color,
|
||||
start_dir,
|
||||
end_dir,
|
||||
context,
|
||||
{
|
||||
reroute,
|
||||
startControl,
|
||||
endControl,
|
||||
num_sublines,
|
||||
disabled
|
||||
}
|
||||
if (startControl) {
|
||||
innerA[0] = a[0] + startControl[0]
|
||||
innerA[1] = a[1] + startControl[1]
|
||||
} else {
|
||||
this.#addSplineOffset(innerA, startDir, dist)
|
||||
}
|
||||
path.moveTo(a[0], a[1] + offsety)
|
||||
path.bezierCurveTo(
|
||||
innerA[0],
|
||||
innerA[1] + offsety,
|
||||
innerB[0],
|
||||
innerB[1] + offsety,
|
||||
b[0],
|
||||
b[1] + offsety
|
||||
)
|
||||
|
||||
// Calculate centre point
|
||||
findPointOnCurve(pos, a, b, innerA, innerB, 0.5)
|
||||
|
||||
if (linkSegment && this.linkMarkerShape === LinkMarkerShape.Arrow) {
|
||||
const justPastCentre = LGraphCanvas.#lTempC
|
||||
findPointOnCurve(justPastCentre, a, b, innerA, innerB, 0.51)
|
||||
|
||||
linkSegment._centreAngle = Math.atan2(
|
||||
justPastCentre[1] - pos[1],
|
||||
justPastCentre[0] - pos[0]
|
||||
)
|
||||
}
|
||||
} else {
|
||||
const l = this.links_render_mode == LinkRenderType.LINEAR_LINK ? 15 : 10
|
||||
switch (startDir) {
|
||||
case LinkDirection.LEFT:
|
||||
innerA[0] += -l
|
||||
break
|
||||
case LinkDirection.RIGHT:
|
||||
innerA[0] += l
|
||||
break
|
||||
case LinkDirection.UP:
|
||||
innerA[1] += -l
|
||||
break
|
||||
case LinkDirection.DOWN:
|
||||
innerA[1] += l
|
||||
break
|
||||
}
|
||||
switch (endDir) {
|
||||
case LinkDirection.LEFT:
|
||||
innerB[0] += -l
|
||||
break
|
||||
case LinkDirection.RIGHT:
|
||||
innerB[0] += l
|
||||
break
|
||||
case LinkDirection.UP:
|
||||
innerB[1] += -l
|
||||
break
|
||||
case LinkDirection.DOWN:
|
||||
innerB[1] += l
|
||||
break
|
||||
}
|
||||
if (this.links_render_mode == LinkRenderType.LINEAR_LINK) {
|
||||
path.moveTo(a[0], a[1] + offsety)
|
||||
path.lineTo(innerA[0], innerA[1] + offsety)
|
||||
path.lineTo(innerB[0], innerB[1] + offsety)
|
||||
path.lineTo(b[0], b[1] + offsety)
|
||||
|
||||
// Calculate centre point
|
||||
pos[0] = (innerA[0] + innerB[0]) * 0.5
|
||||
pos[1] = (innerA[1] + innerB[1]) * 0.5
|
||||
|
||||
if (linkSegment && this.linkMarkerShape === LinkMarkerShape.Arrow) {
|
||||
linkSegment._centreAngle = Math.atan2(
|
||||
innerB[1] - innerA[1],
|
||||
innerB[0] - innerA[0]
|
||||
)
|
||||
}
|
||||
} else if (this.links_render_mode == LinkRenderType.STRAIGHT_LINK) {
|
||||
const midX = (innerA[0] + innerB[0]) * 0.5
|
||||
|
||||
path.moveTo(a[0], a[1])
|
||||
path.lineTo(innerA[0], innerA[1])
|
||||
path.lineTo(midX, innerA[1])
|
||||
path.lineTo(midX, innerB[1])
|
||||
path.lineTo(innerB[0], innerB[1])
|
||||
path.lineTo(b[0], b[1])
|
||||
|
||||
// Calculate centre point
|
||||
pos[0] = midX
|
||||
pos[1] = (innerA[1] + innerB[1]) * 0.5
|
||||
|
||||
if (linkSegment && this.linkMarkerShape === LinkMarkerShape.Arrow) {
|
||||
const diff = innerB[1] - innerA[1]
|
||||
if (Math.abs(diff) < 4) linkSegment._centreAngle = 0
|
||||
else if (diff > 0) linkSegment._centreAngle = Math.PI * 0.5
|
||||
else linkSegment._centreAngle = -(Math.PI * 0.5)
|
||||
}
|
||||
} else {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// rendering the outline of the connection can be a little bit slow
|
||||
if (this.render_connections_border && !this.low_quality && !skip_border) {
|
||||
ctx.strokeStyle = 'rgba(0,0,0,0.5)'
|
||||
ctx.stroke(path)
|
||||
}
|
||||
|
||||
ctx.lineWidth = this.connections_width
|
||||
ctx.fillStyle = ctx.strokeStyle = linkColour
|
||||
ctx.stroke(path)
|
||||
|
||||
// render arrow in the middle
|
||||
if (this.ds.scale >= 0.6 && this.highquality_render && linkSegment) {
|
||||
// render arrow
|
||||
if (this.render_connection_arrows) {
|
||||
// compute two points in the connection
|
||||
const posA = this.computeConnectionPoint(a, b, 0.25, startDir, endDir)
|
||||
const posB = this.computeConnectionPoint(a, b, 0.26, startDir, endDir)
|
||||
const posC = this.computeConnectionPoint(a, b, 0.75, startDir, endDir)
|
||||
const posD = this.computeConnectionPoint(a, b, 0.76, startDir, endDir)
|
||||
|
||||
// compute the angle between them so the arrow points in the right direction
|
||||
let angleA = 0
|
||||
let angleB = 0
|
||||
if (this.render_curved_connections) {
|
||||
angleA = -Math.atan2(posB[0] - posA[0], posB[1] - posA[1])
|
||||
angleB = -Math.atan2(posD[0] - posC[0], posD[1] - posC[1])
|
||||
} else {
|
||||
angleB = angleA = b[1] > a[1] ? 0 : Math.PI
|
||||
}
|
||||
|
||||
// render arrow
|
||||
const transform = ctx.getTransform()
|
||||
ctx.translate(posA[0], posA[1])
|
||||
ctx.rotate(angleA)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(-5, -3)
|
||||
ctx.lineTo(0, +7)
|
||||
ctx.lineTo(+5, -3)
|
||||
ctx.fill()
|
||||
ctx.setTransform(transform)
|
||||
|
||||
ctx.translate(posC[0], posC[1])
|
||||
ctx.rotate(angleB)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(-5, -3)
|
||||
ctx.lineTo(0, +7)
|
||||
ctx.lineTo(+5, -3)
|
||||
ctx.fill()
|
||||
ctx.setTransform(transform)
|
||||
}
|
||||
|
||||
// Draw link centre marker
|
||||
ctx.beginPath()
|
||||
if (this.linkMarkerShape === LinkMarkerShape.Arrow) {
|
||||
const transform = ctx.getTransform()
|
||||
ctx.translate(pos[0], pos[1])
|
||||
if (linkSegment._centreAngle) ctx.rotate(linkSegment._centreAngle)
|
||||
// The math is off, but it currently looks better in chromium
|
||||
ctx.moveTo(-3.2, -5)
|
||||
ctx.lineTo(+7, 0)
|
||||
ctx.lineTo(-3.2, +5)
|
||||
ctx.setTransform(transform)
|
||||
} else if (
|
||||
this.linkMarkerShape == null ||
|
||||
this.linkMarkerShape === LinkMarkerShape.Circle
|
||||
) {
|
||||
ctx.arc(pos[0], pos[1], 5, 0, Math.PI * 2)
|
||||
}
|
||||
if (disabled) {
|
||||
const { fillStyle, globalAlpha } = ctx
|
||||
ctx.fillStyle = this._pattern ?? '#797979'
|
||||
ctx.globalAlpha = 0.75
|
||||
ctx.fill()
|
||||
ctx.globalAlpha = globalAlpha
|
||||
ctx.fillStyle = fillStyle
|
||||
}
|
||||
ctx.fill()
|
||||
|
||||
if (LLink._drawDebug) {
|
||||
const { fillStyle, font, globalAlpha, lineWidth, strokeStyle } = ctx
|
||||
ctx.globalAlpha = 1
|
||||
ctx.lineWidth = 4
|
||||
ctx.fillStyle = 'white'
|
||||
ctx.strokeStyle = 'black'
|
||||
ctx.font = '16px Arial'
|
||||
|
||||
const text = String(linkSegment.id)
|
||||
const { width, actualBoundingBoxAscent } = ctx.measureText(text)
|
||||
const x = pos[0] - width * 0.5
|
||||
const y = pos[1] + actualBoundingBoxAscent * 0.5
|
||||
ctx.strokeText(text, x, y)
|
||||
ctx.fillText(text, x, y)
|
||||
|
||||
ctx.font = font
|
||||
ctx.globalAlpha = globalAlpha
|
||||
ctx.lineWidth = lineWidth
|
||||
ctx.fillStyle = fillStyle
|
||||
ctx.strokeStyle = strokeStyle
|
||||
}
|
||||
}
|
||||
|
||||
// render flowing points
|
||||
if (flow) {
|
||||
ctx.fillStyle = linkColour
|
||||
for (let i = 0; i < 5; ++i) {
|
||||
const f = (LiteGraph.getTime() * 0.001 + i * 0.2) % 1
|
||||
const flowPos = this.computeConnectionPoint(a, b, f, startDir, endDir)
|
||||
ctx.beginPath()
|
||||
ctx.arc(flowPos[0], flowPos[1], 5, 0, 2 * Math.PI)
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds a point along a spline represented by a to b, with spline endpoint directions dictacted by start_dir and end_dir.
|
||||
* @param a Start point
|
||||
* @param b End point
|
||||
* @param t Time: distance between points (e.g 0.25 is 25% along the line)
|
||||
* @param start_dir Spline start direction
|
||||
* @param end_dir Spline end direction
|
||||
* @returns The point at {@link t} distance along the spline a-b.
|
||||
*/
|
||||
computeConnectionPoint(
|
||||
a: ReadOnlyPoint,
|
||||
b: ReadOnlyPoint,
|
||||
t: number,
|
||||
start_dir: LinkDirection,
|
||||
end_dir: LinkDirection
|
||||
): Point {
|
||||
start_dir ||= LinkDirection.RIGHT
|
||||
end_dir ||= LinkDirection.LEFT
|
||||
|
||||
const dist = distance(a, b)
|
||||
const pa: Point = [a[0], a[1]]
|
||||
const pb: Point = [b[0], b[1]]
|
||||
|
||||
this.#addSplineOffset(pa, start_dir, dist)
|
||||
this.#addSplineOffset(pb, end_dir, dist)
|
||||
|
||||
const c1 = (1 - t) * (1 - t) * (1 - t)
|
||||
const c2 = 3 * ((1 - t) * (1 - t)) * t
|
||||
const c3 = 3 * (1 - t) * (t * t)
|
||||
const c4 = t * t * t
|
||||
|
||||
const x = c1 * a[0] + c2 * pa[0] + c3 * pb[0] + c4 * b[0]
|
||||
const y = c1 * a[1] + c2 * pa[1] + c3 * pb[1] + c4 * b[1]
|
||||
return [x, y]
|
||||
}
|
||||
|
||||
/**
|
||||
* Modifies an existing point, adding a single-axis offset.
|
||||
* @param point The point to add the offset to
|
||||
* @param direction The direction to add the offset in
|
||||
* @param dist Distance to offset
|
||||
* @param factor Distance is mulitplied by this value. Default: 0.25
|
||||
*/
|
||||
#addSplineOffset(
|
||||
point: Point,
|
||||
direction: LinkDirection,
|
||||
dist: number,
|
||||
factor = 0.25
|
||||
): void {
|
||||
switch (direction) {
|
||||
case LinkDirection.LEFT:
|
||||
point[0] += dist * -factor
|
||||
break
|
||||
case LinkDirection.RIGHT:
|
||||
point[0] += dist * factor
|
||||
break
|
||||
case LinkDirection.UP:
|
||||
point[1] += dist * -factor
|
||||
break
|
||||
case LinkDirection.DOWN:
|
||||
point[1] += dist * factor
|
||||
break
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
531
src/rendering/adapters/LitegraphLinkAdapter.ts
Normal file
531
src/rendering/adapters/LitegraphLinkAdapter.ts
Normal file
@@ -0,0 +1,531 @@
|
||||
/**
|
||||
* Litegraph Link Adapter
|
||||
*
|
||||
* Bridges the gap between litegraph's data model and the pure canvas renderer.
|
||||
* Converts litegraph-specific types (LLink, LGraphNode, slots) into generic
|
||||
* rendering data that can be consumed by the PathRenderer.
|
||||
* Maintains backward compatibility with existing litegraph integration.
|
||||
*/
|
||||
import type { LGraph } from '@/lib/litegraph/src/LGraph'
|
||||
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
|
||||
import { LLink } from '@/lib/litegraph/src/LLink'
|
||||
import type { Reroute } from '@/lib/litegraph/src/Reroute'
|
||||
import type {
|
||||
CanvasColour,
|
||||
INodeInputSlot,
|
||||
INodeOutputSlot,
|
||||
ISlotType,
|
||||
ReadOnlyPoint
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import {
|
||||
LinkDirection,
|
||||
LinkMarkerShape,
|
||||
LinkRenderType
|
||||
} from '@/lib/litegraph/src/types/globalEnums'
|
||||
import {
|
||||
type ArrowShape,
|
||||
CanvasPathRenderer,
|
||||
type Direction,
|
||||
type DragLinkData,
|
||||
type LinkRenderData,
|
||||
type RenderContext as PathRenderContext,
|
||||
type RenderMode
|
||||
} from '@/rendering/canvas/PathRenderer'
|
||||
import type { LayoutStore, Point } from '@/types/layoutTypes'
|
||||
|
||||
export interface LinkRenderContext {
|
||||
// Canvas settings
|
||||
renderMode: LinkRenderType
|
||||
connectionWidth: number
|
||||
renderBorder: boolean
|
||||
lowQuality: boolean
|
||||
highQualityRender: boolean
|
||||
scale: number
|
||||
linkMarkerShape: LinkMarkerShape
|
||||
renderConnectionArrows: boolean
|
||||
|
||||
// State
|
||||
highlightedLinks: Set<string | number>
|
||||
|
||||
// Colors
|
||||
defaultLinkColor: CanvasColour
|
||||
linkTypeColors: Record<string, CanvasColour>
|
||||
|
||||
// Pattern for disabled links (optional)
|
||||
disabledPattern?: CanvasPattern | null
|
||||
}
|
||||
|
||||
export interface LinkRenderOptions {
|
||||
color?: CanvasColour
|
||||
flow?: boolean
|
||||
skipBorder?: boolean
|
||||
disabled?: boolean
|
||||
}
|
||||
|
||||
export class LitegraphLinkAdapter {
|
||||
private layoutStore: LayoutStore
|
||||
private graph: LGraph
|
||||
private pathRenderer: CanvasPathRenderer
|
||||
|
||||
constructor(layoutStore: LayoutStore, graph: LGraph) {
|
||||
this.layoutStore = layoutStore
|
||||
this.graph = graph
|
||||
this.pathRenderer = new CanvasPathRenderer()
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a single link with all necessary data properly fetched
|
||||
* Populates link.path for hit detection
|
||||
*/
|
||||
renderLink(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
link: LLink,
|
||||
context: LinkRenderContext,
|
||||
options: LinkRenderOptions = {}
|
||||
): void {
|
||||
// Get nodes from graph
|
||||
const sourceNode = this.graph.getNodeById(link.origin_id)
|
||||
const targetNode = this.graph.getNodeById(link.target_id)
|
||||
|
||||
if (!sourceNode || !targetNode) {
|
||||
console.warn(`Cannot render link ${link.id}: missing nodes`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get slots from nodes
|
||||
const sourceSlot = sourceNode.outputs?.[link.origin_slot]
|
||||
const targetSlot = targetNode.inputs?.[link.target_slot]
|
||||
|
||||
if (!sourceSlot || !targetSlot) {
|
||||
console.warn(`Cannot render link ${link.id}: missing slots`)
|
||||
return
|
||||
}
|
||||
|
||||
// Get positions from nodes
|
||||
const startPos = sourceNode.getOutputPos(link.origin_slot)
|
||||
const endPos = targetNode.getInputPos(link.target_slot)
|
||||
|
||||
// Get directions from slots
|
||||
const startDir = sourceSlot.dir || LinkDirection.RIGHT
|
||||
const endDir = targetSlot.dir || LinkDirection.LEFT
|
||||
|
||||
// Convert to pure render data
|
||||
const linkData = this.convertToLinkRenderData(
|
||||
link,
|
||||
{ x: startPos[0], y: startPos[1] },
|
||||
{ x: endPos[0], y: endPos[1] },
|
||||
startDir,
|
||||
endDir,
|
||||
options
|
||||
)
|
||||
|
||||
// Convert context
|
||||
const pathContext = this.convertToPathRenderContext(context)
|
||||
|
||||
// Render using pure renderer
|
||||
const path = this.pathRenderer.drawLink(ctx, linkData, pathContext)
|
||||
|
||||
// Store path for hit detection
|
||||
link.path = path
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert litegraph link data to pure render format
|
||||
*/
|
||||
private convertToLinkRenderData(
|
||||
link: LLink,
|
||||
startPoint: Point,
|
||||
endPoint: Point,
|
||||
startDir: LinkDirection,
|
||||
endDir: LinkDirection,
|
||||
options: LinkRenderOptions
|
||||
): LinkRenderData {
|
||||
return {
|
||||
id: String(link.id),
|
||||
startPoint,
|
||||
endPoint,
|
||||
startDirection: this.convertDirection(startDir),
|
||||
endDirection: this.convertDirection(endDir),
|
||||
color: options.color
|
||||
? String(options.color)
|
||||
: link.color
|
||||
? String(link.color)
|
||||
: undefined,
|
||||
type: link.type !== undefined ? String(link.type) : undefined,
|
||||
flow: options.flow || false,
|
||||
disabled: options.disabled || false
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LinkDirection enum to Direction string
|
||||
*/
|
||||
private convertDirection(dir: LinkDirection): Direction {
|
||||
switch (dir) {
|
||||
case LinkDirection.LEFT:
|
||||
return 'left'
|
||||
case LinkDirection.RIGHT:
|
||||
return 'right'
|
||||
case LinkDirection.UP:
|
||||
return 'up'
|
||||
case LinkDirection.DOWN:
|
||||
return 'down'
|
||||
default:
|
||||
return 'right'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LinkRenderContext to PathRenderContext
|
||||
*/
|
||||
private convertToPathRenderContext(
|
||||
context: LinkRenderContext
|
||||
): PathRenderContext {
|
||||
return {
|
||||
style: {
|
||||
mode: this.convertRenderMode(context.renderMode),
|
||||
connectionWidth: context.connectionWidth,
|
||||
borderWidth: context.renderBorder ? 4 : undefined,
|
||||
arrowShape: this.convertArrowShape(context.linkMarkerShape),
|
||||
showArrows: context.renderConnectionArrows,
|
||||
lowQuality: context.lowQuality,
|
||||
// Center marker settings (matches original litegraph behavior)
|
||||
showCenterMarker: true,
|
||||
centerMarkerShape:
|
||||
context.linkMarkerShape === LinkMarkerShape.Arrow
|
||||
? 'arrow'
|
||||
: 'circle',
|
||||
highQuality: context.highQualityRender
|
||||
},
|
||||
colors: {
|
||||
default: String(context.defaultLinkColor),
|
||||
byType: this.convertColorMap(context.linkTypeColors),
|
||||
highlighted: '#FFF'
|
||||
},
|
||||
patterns: {
|
||||
disabled: context.disabledPattern
|
||||
},
|
||||
animation: {
|
||||
time: LiteGraph.getTime() * 0.001
|
||||
},
|
||||
scale: context.scale,
|
||||
highlightedIds: new Set(Array.from(context.highlightedLinks).map(String))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LinkRenderType to RenderMode
|
||||
*/
|
||||
private convertRenderMode(mode: LinkRenderType): RenderMode {
|
||||
switch (mode) {
|
||||
case LinkRenderType.LINEAR_LINK:
|
||||
return 'linear'
|
||||
case LinkRenderType.STRAIGHT_LINK:
|
||||
return 'straight'
|
||||
case LinkRenderType.SPLINE_LINK:
|
||||
default:
|
||||
return 'spline'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert LinkMarkerShape to ArrowShape
|
||||
*/
|
||||
private convertArrowShape(shape: LinkMarkerShape): ArrowShape {
|
||||
switch (shape) {
|
||||
case LinkMarkerShape.Circle:
|
||||
return 'circle'
|
||||
case LinkMarkerShape.Arrow:
|
||||
default:
|
||||
return 'triangle'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert color map to ensure all values are strings
|
||||
*/
|
||||
private convertColorMap(
|
||||
colors: Record<string, CanvasColour>
|
||||
): Record<string, string> {
|
||||
const result: Record<string, string> = {}
|
||||
for (const [key, value] of Object.entries(colors)) {
|
||||
result[key] = String(value)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply spline offset to a point, mimicking original #addSplineOffset behavior
|
||||
* Critically: does nothing for CENTER/NONE directions (no case for them)
|
||||
*/
|
||||
private applySplineOffset(
|
||||
point: Point,
|
||||
direction: LinkDirection,
|
||||
distance: number
|
||||
): void {
|
||||
switch (direction) {
|
||||
case LinkDirection.LEFT:
|
||||
point.x -= distance
|
||||
break
|
||||
case LinkDirection.RIGHT:
|
||||
point.x += distance
|
||||
break
|
||||
case LinkDirection.UP:
|
||||
point.y -= distance
|
||||
break
|
||||
case LinkDirection.DOWN:
|
||||
point.y += distance
|
||||
break
|
||||
// CENTER and NONE: no offset applied (original behavior)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Direct rendering method compatible with LGraphCanvas
|
||||
* Converts data and delegates to pure renderer
|
||||
*/
|
||||
renderLinkDirect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
a: ReadOnlyPoint,
|
||||
b: ReadOnlyPoint,
|
||||
link: LLink | null,
|
||||
skip_border: boolean,
|
||||
flow: number | boolean | null,
|
||||
color: CanvasColour | null,
|
||||
start_dir: LinkDirection,
|
||||
end_dir: LinkDirection,
|
||||
context: LinkRenderContext,
|
||||
extras: {
|
||||
reroute?: Reroute
|
||||
startControl?: ReadOnlyPoint
|
||||
endControl?: ReadOnlyPoint
|
||||
num_sublines?: number
|
||||
disabled?: boolean
|
||||
} = {}
|
||||
): void {
|
||||
// Apply same defaults as original renderLink
|
||||
const startDir = start_dir || LinkDirection.RIGHT
|
||||
const endDir = end_dir || LinkDirection.LEFT
|
||||
|
||||
// Convert flow to boolean
|
||||
const flowBool = flow === true || (typeof flow === 'number' && flow > 0)
|
||||
|
||||
// Create LinkRenderData from direct parameters
|
||||
const linkData: LinkRenderData = {
|
||||
id: link ? String(link.id) : 'temp',
|
||||
startPoint: { x: a[0], y: a[1] },
|
||||
endPoint: { x: b[0], y: b[1] },
|
||||
startDirection: this.convertDirection(startDir),
|
||||
endDirection: this.convertDirection(endDir),
|
||||
color: color !== null && color !== undefined ? String(color) : undefined,
|
||||
type: link?.type !== undefined ? String(link.type) : undefined,
|
||||
flow: flowBool,
|
||||
disabled: extras.disabled || false
|
||||
}
|
||||
|
||||
// Control points handling (spline mode):
|
||||
// - Pre-refactor, the old renderLink honored a single provided control and
|
||||
// derived the missing side via #addSplineOffset (CENTER => no offset).
|
||||
// - Restore that behavior here so reroute segments render identically.
|
||||
if (context.renderMode === LinkRenderType.SPLINE_LINK) {
|
||||
const hasStartCtrl = !!extras.startControl
|
||||
const hasEndCtrl = !!extras.endControl
|
||||
|
||||
// Compute distance once for offsets
|
||||
const dist = Math.sqrt(
|
||||
(b[0] - a[0]) * (b[0] - a[0]) + (b[1] - a[1]) * (b[1] - a[1])
|
||||
)
|
||||
const factor = 0.25
|
||||
|
||||
const cps: Point[] = []
|
||||
|
||||
if (hasStartCtrl && hasEndCtrl) {
|
||||
// Both provided explicitly
|
||||
cps.push(
|
||||
{
|
||||
x: a[0] + (extras.startControl![0] || 0),
|
||||
y: a[1] + (extras.startControl![1] || 0)
|
||||
},
|
||||
{
|
||||
x: b[0] + (extras.endControl![0] || 0),
|
||||
y: b[1] + (extras.endControl![1] || 0)
|
||||
}
|
||||
)
|
||||
linkData.controlPoints = cps
|
||||
} else if (hasStartCtrl && !hasEndCtrl) {
|
||||
// Start provided, derive end via direction offset (CENTER => no offset)
|
||||
const start = {
|
||||
x: a[0] + (extras.startControl![0] || 0),
|
||||
y: a[1] + (extras.startControl![1] || 0)
|
||||
}
|
||||
const end = { x: b[0], y: b[1] }
|
||||
this.applySplineOffset(end, endDir, dist * factor)
|
||||
cps.push(start, end)
|
||||
linkData.controlPoints = cps
|
||||
} else if (!hasStartCtrl && hasEndCtrl) {
|
||||
// End provided, derive start via direction offset (CENTER => no offset)
|
||||
const start = { x: a[0], y: a[1] }
|
||||
this.applySplineOffset(start, startDir, dist * factor)
|
||||
const end = {
|
||||
x: b[0] + (extras.endControl![0] || 0),
|
||||
y: b[1] + (extras.endControl![1] || 0)
|
||||
}
|
||||
cps.push(start, end)
|
||||
linkData.controlPoints = cps
|
||||
} else {
|
||||
// Neither provided: derive both from directions (CENTER => no offset)
|
||||
const start = { x: a[0], y: a[1] }
|
||||
const end = { x: b[0], y: b[1] }
|
||||
this.applySplineOffset(start, startDir, dist * factor)
|
||||
this.applySplineOffset(end, endDir, dist * factor)
|
||||
cps.push(start, end)
|
||||
linkData.controlPoints = cps
|
||||
}
|
||||
}
|
||||
|
||||
// Convert context
|
||||
const pathContext = this.convertToPathRenderContext(context)
|
||||
|
||||
// Override skip_border if needed
|
||||
if (skip_border) {
|
||||
pathContext.style.borderWidth = undefined
|
||||
}
|
||||
|
||||
// Render using pure renderer
|
||||
const path = this.pathRenderer.drawLink(ctx, linkData, pathContext)
|
||||
|
||||
// Store path for hit detection
|
||||
const linkSegment = extras.reroute ?? link
|
||||
if (linkSegment) {
|
||||
linkSegment.path = path
|
||||
|
||||
// Copy calculated center position back to litegraph object
|
||||
// This is needed for hit detection and menu interaction
|
||||
if (linkData.centerPos) {
|
||||
linkSegment._pos = linkSegment._pos || new Float32Array(2)
|
||||
linkSegment._pos[0] = linkData.centerPos.x
|
||||
linkSegment._pos[1] = linkData.centerPos.y
|
||||
|
||||
// Store center angle if calculated (for arrow markers)
|
||||
if (linkData.centerAngle !== undefined) {
|
||||
linkSegment._centreAngle = linkData.centerAngle
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a link being dragged from a slot to mouse position
|
||||
* Used during link creation/reconnection
|
||||
*/
|
||||
renderDraggingLink(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
fromNode: LGraphNode | null,
|
||||
fromSlot: INodeOutputSlot | INodeInputSlot,
|
||||
fromSlotIndex: number,
|
||||
toPosition: ReadOnlyPoint,
|
||||
context: LinkRenderContext,
|
||||
options: {
|
||||
fromInput?: boolean
|
||||
color?: CanvasColour
|
||||
disabled?: boolean
|
||||
} = {}
|
||||
): void {
|
||||
if (!fromNode) return
|
||||
|
||||
// Get slot position
|
||||
const slotPos = options.fromInput
|
||||
? fromNode.getInputPos(fromSlotIndex)
|
||||
: fromNode.getOutputPos(fromSlotIndex)
|
||||
if (!slotPos) return
|
||||
|
||||
// Get slot direction
|
||||
const slotDir =
|
||||
fromSlot.dir ||
|
||||
(options.fromInput ? LinkDirection.LEFT : LinkDirection.RIGHT)
|
||||
|
||||
// Create drag data
|
||||
const dragData: DragLinkData = {
|
||||
fixedPoint: { x: slotPos[0], y: slotPos[1] },
|
||||
fixedDirection: this.convertDirection(slotDir),
|
||||
dragPoint: { x: toPosition[0], y: toPosition[1] },
|
||||
color: options.color ? String(options.color) : undefined,
|
||||
type: fromSlot.type !== undefined ? String(fromSlot.type) : undefined,
|
||||
disabled: options.disabled || false,
|
||||
fromInput: options.fromInput || false
|
||||
}
|
||||
|
||||
// Convert context
|
||||
const pathContext = this.convertToPathRenderContext(context)
|
||||
|
||||
// Render using pure renderer
|
||||
this.pathRenderer.drawDraggingLink(ctx, dragData, pathContext)
|
||||
}
|
||||
|
||||
/**
|
||||
* Render a dragging link with direct position data
|
||||
* More flexible version for complex drag scenarios
|
||||
*/
|
||||
renderDraggingLinkDirect(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
fromPos: ReadOnlyPoint,
|
||||
fromDir: LinkDirection,
|
||||
toPos: ReadOnlyPoint,
|
||||
context: LinkRenderContext,
|
||||
options: {
|
||||
fromInput?: boolean
|
||||
toDir?: LinkDirection
|
||||
color?: CanvasColour
|
||||
type?: ISlotType
|
||||
disabled?: boolean
|
||||
} = {}
|
||||
): void {
|
||||
// Use renderLinkDirect which already handles CENTER/NONE correctly
|
||||
this.renderLinkDirect(
|
||||
ctx,
|
||||
fromPos,
|
||||
toPos,
|
||||
null, // no link
|
||||
false, // skip_border
|
||||
null, // flow
|
||||
options.color || null,
|
||||
fromDir,
|
||||
options.toDir || LinkDirection.CENTER, // Default to CENTER for drag end
|
||||
context,
|
||||
{
|
||||
disabled: options.disabled || false
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slot position helper - may be useful for Vue components
|
||||
*/
|
||||
getSlotAbsolutePosition(
|
||||
nodeId: string,
|
||||
slotType: 'input' | 'output',
|
||||
slotIndex: number
|
||||
): Point | null {
|
||||
// Get node from graph
|
||||
const node = this.graph.getNodeById(nodeId)
|
||||
if (!node) return null
|
||||
|
||||
// Get node position from layout tree
|
||||
const nodeLayout = this.layoutStore.getNodeLayoutRef(nodeId).value
|
||||
if (!nodeLayout) return null
|
||||
|
||||
// Get relative slot position from node
|
||||
const relativePos =
|
||||
slotType === 'input'
|
||||
? node.getInputPos(slotIndex)
|
||||
: node.getOutputPos(slotIndex)
|
||||
if (!relativePos) return null
|
||||
|
||||
// Combine to get absolute position
|
||||
return {
|
||||
x: nodeLayout.position.x + relativePos[0],
|
||||
y: nodeLayout.position.y + relativePos[1]
|
||||
}
|
||||
}
|
||||
}
|
||||
630
src/rendering/canvas/PathRenderer.ts
Normal file
630
src/rendering/canvas/PathRenderer.ts
Normal file
@@ -0,0 +1,630 @@
|
||||
/**
|
||||
* Path Renderer
|
||||
*
|
||||
* Pure canvas2D rendering utility with no framework dependencies.
|
||||
* Renders bezier curves, straight lines, and linear connections between points.
|
||||
* Supports arrows, flow animations, and returns Path2D objects for hit detection.
|
||||
* Can be reused in any canvas-based project without modification.
|
||||
*/
|
||||
|
||||
export interface Point {
|
||||
x: number
|
||||
y: number
|
||||
}
|
||||
|
||||
export type Direction = 'left' | 'right' | 'up' | 'down'
|
||||
export type RenderMode = 'spline' | 'straight' | 'linear'
|
||||
export type ArrowShape = 'triangle' | 'circle' | 'square'
|
||||
|
||||
export interface LinkRenderData {
|
||||
id: string
|
||||
startPoint: Point
|
||||
endPoint: Point
|
||||
startDirection: Direction
|
||||
endDirection: Direction
|
||||
color?: string
|
||||
type?: string
|
||||
controlPoints?: Point[]
|
||||
flow?: boolean
|
||||
disabled?: boolean
|
||||
// Optional multi-segment support
|
||||
segments?: Array<{
|
||||
start: Point
|
||||
end: Point
|
||||
controlPoints?: Point[]
|
||||
}>
|
||||
// Center point storage (for hit detection and menu)
|
||||
centerPos?: Point
|
||||
centerAngle?: number
|
||||
}
|
||||
|
||||
export interface RenderStyle {
|
||||
mode: RenderMode
|
||||
connectionWidth: number
|
||||
borderWidth?: number
|
||||
arrowShape?: ArrowShape
|
||||
showArrows?: boolean
|
||||
lowQuality?: boolean
|
||||
// Center marker properties
|
||||
showCenterMarker?: boolean
|
||||
centerMarkerShape?: 'circle' | 'arrow'
|
||||
highQuality?: boolean
|
||||
}
|
||||
|
||||
export interface RenderColors {
|
||||
default: string
|
||||
byType: Record<string, string>
|
||||
highlighted: string
|
||||
}
|
||||
|
||||
export interface RenderContext {
|
||||
style: RenderStyle
|
||||
colors: RenderColors
|
||||
patterns?: {
|
||||
disabled?: CanvasPattern | null
|
||||
}
|
||||
animation?: {
|
||||
time: number // Seconds for flow animation
|
||||
}
|
||||
scale?: number // Canvas scale for quality adjustments
|
||||
highlightedIds?: Set<string>
|
||||
}
|
||||
|
||||
export interface DragLinkData {
|
||||
/** Fixed end - the slot being dragged from */
|
||||
fixedPoint: Point
|
||||
fixedDirection: Direction
|
||||
/** Moving end - follows mouse */
|
||||
dragPoint: Point
|
||||
dragDirection?: Direction
|
||||
/** Visual properties */
|
||||
color?: string
|
||||
type?: string
|
||||
disabled?: boolean
|
||||
/** Whether dragging from input (reverse direction) */
|
||||
fromInput?: boolean
|
||||
}
|
||||
|
||||
export class CanvasPathRenderer {
|
||||
/**
|
||||
* Draw a link between two points
|
||||
* Returns a Path2D object for hit detection
|
||||
*/
|
||||
drawLink(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
link: LinkRenderData,
|
||||
context: RenderContext
|
||||
): Path2D {
|
||||
const path = new Path2D()
|
||||
|
||||
// Determine final color
|
||||
const isHighlighted = context.highlightedIds?.has(link.id) ?? false
|
||||
const color = this.determineLinkColor(link, context, isHighlighted)
|
||||
|
||||
// Save context state
|
||||
ctx.save()
|
||||
|
||||
// Apply disabled pattern if needed
|
||||
if (link.disabled && context.patterns?.disabled) {
|
||||
ctx.strokeStyle = context.patterns.disabled
|
||||
} else {
|
||||
ctx.strokeStyle = color
|
||||
}
|
||||
|
||||
// Set line properties
|
||||
ctx.lineWidth = context.style.connectionWidth
|
||||
ctx.lineJoin = 'round'
|
||||
|
||||
// Draw border if needed
|
||||
if (context.style.borderWidth && !context.style.lowQuality) {
|
||||
this.drawLinkPath(
|
||||
ctx,
|
||||
path,
|
||||
link,
|
||||
context,
|
||||
context.style.connectionWidth + context.style.borderWidth,
|
||||
'rgba(0,0,0,0.5)'
|
||||
)
|
||||
}
|
||||
|
||||
// Draw main link
|
||||
this.drawLinkPath(
|
||||
ctx,
|
||||
path,
|
||||
link,
|
||||
context,
|
||||
context.style.connectionWidth,
|
||||
color
|
||||
)
|
||||
|
||||
// Calculate and store center position
|
||||
this.calculateCenterPoint(link, context)
|
||||
|
||||
// Draw arrows if needed
|
||||
if (context.style.showArrows) {
|
||||
this.drawArrows(ctx, link, context, color)
|
||||
}
|
||||
|
||||
// Draw center marker if needed (for link menu interaction)
|
||||
if (
|
||||
context.style.showCenterMarker &&
|
||||
context.scale &&
|
||||
context.scale >= 0.6 &&
|
||||
context.style.highQuality
|
||||
) {
|
||||
this.drawCenterMarker(ctx, link, context, color)
|
||||
}
|
||||
|
||||
// Draw flow animation if needed
|
||||
if (link.flow && context.animation) {
|
||||
this.drawFlowAnimation(ctx, path, link, context)
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
|
||||
return path
|
||||
}
|
||||
|
||||
private determineLinkColor(
|
||||
link: LinkRenderData,
|
||||
context: RenderContext,
|
||||
isHighlighted: boolean
|
||||
): string {
|
||||
if (isHighlighted) {
|
||||
return context.colors.highlighted
|
||||
}
|
||||
if (link.color) {
|
||||
return link.color
|
||||
}
|
||||
if (link.type && context.colors.byType[link.type]) {
|
||||
return context.colors.byType[link.type]
|
||||
}
|
||||
return context.colors.default
|
||||
}
|
||||
|
||||
private drawLinkPath(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
path: Path2D,
|
||||
link: LinkRenderData,
|
||||
context: RenderContext,
|
||||
lineWidth: number,
|
||||
color: string
|
||||
): void {
|
||||
ctx.strokeStyle = color
|
||||
ctx.lineWidth = lineWidth
|
||||
|
||||
const start = link.startPoint
|
||||
const end = link.endPoint
|
||||
|
||||
// Build the path based on render mode
|
||||
if (context.style.mode === 'linear') {
|
||||
this.buildLinearPath(path, start, end)
|
||||
} else if (context.style.mode === 'straight') {
|
||||
this.buildStraightPath(
|
||||
path,
|
||||
start,
|
||||
end,
|
||||
link.startDirection,
|
||||
link.endDirection
|
||||
)
|
||||
} else {
|
||||
// Spline mode (default)
|
||||
this.buildSplinePath(
|
||||
path,
|
||||
start,
|
||||
end,
|
||||
link.startDirection,
|
||||
link.endDirection,
|
||||
link.controlPoints
|
||||
)
|
||||
}
|
||||
|
||||
ctx.stroke(path)
|
||||
}
|
||||
|
||||
private buildLinearPath(path: Path2D, start: Point, end: Point): void {
|
||||
path.moveTo(start.x, start.y)
|
||||
path.lineTo(end.x, end.y)
|
||||
}
|
||||
|
||||
private buildStraightPath(
|
||||
path: Path2D,
|
||||
start: Point,
|
||||
end: Point,
|
||||
_startDir: Direction,
|
||||
_endDir: Direction
|
||||
): void {
|
||||
path.moveTo(start.x, start.y)
|
||||
|
||||
const dx = end.x - start.x
|
||||
const dy = end.y - start.y
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
const midX = start.x + dx * 0.5
|
||||
path.lineTo(midX, start.y)
|
||||
path.lineTo(midX, end.y)
|
||||
} else {
|
||||
const midY = start.y + dy * 0.5
|
||||
path.lineTo(start.x, midY)
|
||||
path.lineTo(end.x, midY)
|
||||
}
|
||||
|
||||
path.lineTo(end.x, end.y)
|
||||
}
|
||||
|
||||
private buildSplinePath(
|
||||
path: Path2D,
|
||||
start: Point,
|
||||
end: Point,
|
||||
startDir: Direction,
|
||||
endDir: Direction,
|
||||
controlPoints?: Point[]
|
||||
): void {
|
||||
path.moveTo(start.x, start.y)
|
||||
|
||||
// Calculate control points if not provided
|
||||
const controls =
|
||||
controlPoints || this.calculateControlPoints(start, end, startDir, endDir)
|
||||
|
||||
if (controls.length >= 2) {
|
||||
// Cubic bezier
|
||||
path.bezierCurveTo(
|
||||
controls[0].x,
|
||||
controls[0].y,
|
||||
controls[1].x,
|
||||
controls[1].y,
|
||||
end.x,
|
||||
end.y
|
||||
)
|
||||
} else if (controls.length === 1) {
|
||||
// Quadratic bezier
|
||||
path.quadraticCurveTo(controls[0].x, controls[0].y, end.x, end.y)
|
||||
} else {
|
||||
// Fallback to linear
|
||||
path.lineTo(end.x, end.y)
|
||||
}
|
||||
}
|
||||
|
||||
private calculateControlPoints(
|
||||
start: Point,
|
||||
end: Point,
|
||||
startDir: Direction,
|
||||
endDir: Direction
|
||||
): Point[] {
|
||||
const dist = Math.sqrt(
|
||||
Math.pow(end.x - start.x, 2) + Math.pow(end.y - start.y, 2)
|
||||
)
|
||||
const controlDist = Math.max(30, dist * 0.25)
|
||||
|
||||
// Calculate control point offsets based on direction
|
||||
const startControl = this.getDirectionOffset(startDir, controlDist)
|
||||
const endControl = this.getDirectionOffset(endDir, controlDist)
|
||||
|
||||
return [
|
||||
{ x: start.x + startControl.x, y: start.y + startControl.y },
|
||||
{ x: end.x + endControl.x, y: end.y + endControl.y }
|
||||
]
|
||||
}
|
||||
|
||||
private getDirectionOffset(direction: Direction, distance: number): Point {
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
return { x: -distance, y: 0 }
|
||||
case 'right':
|
||||
return { x: distance, y: 0 }
|
||||
case 'up':
|
||||
return { x: 0, y: -distance }
|
||||
case 'down':
|
||||
return { x: 0, y: distance }
|
||||
}
|
||||
}
|
||||
|
||||
private drawArrows(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
link: LinkRenderData,
|
||||
context: RenderContext,
|
||||
color: string
|
||||
): void {
|
||||
if (!context.style.showArrows) return
|
||||
|
||||
const arrowSize = 5 * (context.scale || 1)
|
||||
const shape = context.style.arrowShape || 'triangle'
|
||||
|
||||
// Calculate arrow position (middle of link for now)
|
||||
const mid = {
|
||||
x: (link.startPoint.x + link.endPoint.x) / 2,
|
||||
y: (link.startPoint.y + link.endPoint.y) / 2
|
||||
}
|
||||
|
||||
// Calculate angle
|
||||
const angle = Math.atan2(
|
||||
link.endPoint.y - link.startPoint.y,
|
||||
link.endPoint.x - link.startPoint.x
|
||||
)
|
||||
|
||||
ctx.save()
|
||||
ctx.translate(mid.x, mid.y)
|
||||
ctx.rotate(angle)
|
||||
ctx.fillStyle = color
|
||||
|
||||
if (shape === 'circle') {
|
||||
ctx.beginPath()
|
||||
ctx.arc(0, 0, arrowSize, 0, Math.PI * 2)
|
||||
ctx.fill()
|
||||
} else if (shape === 'square') {
|
||||
ctx.fillRect(-arrowSize / 2, -arrowSize / 2, arrowSize, arrowSize)
|
||||
} else {
|
||||
// Triangle (default)
|
||||
ctx.beginPath()
|
||||
ctx.moveTo(arrowSize, 0)
|
||||
ctx.lineTo(-arrowSize, -arrowSize)
|
||||
ctx.lineTo(-arrowSize, arrowSize)
|
||||
ctx.closePath()
|
||||
ctx.fill()
|
||||
}
|
||||
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
private drawFlowAnimation(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
path: Path2D,
|
||||
_link: LinkRenderData,
|
||||
context: RenderContext
|
||||
): void {
|
||||
if (!context.animation) return
|
||||
|
||||
const time = context.animation.time
|
||||
const spacing = 24
|
||||
const speed = 48
|
||||
|
||||
ctx.save()
|
||||
ctx.strokeStyle = context.colors.highlighted
|
||||
ctx.lineWidth = Math.max(1, context.style.connectionWidth * 0.5)
|
||||
|
||||
// Create dashed line effect for flow
|
||||
const dashOffset = (time * speed) % spacing
|
||||
ctx.setLineDash([4, spacing - 4])
|
||||
ctx.lineDashOffset = -dashOffset
|
||||
|
||||
ctx.stroke(path)
|
||||
ctx.restore()
|
||||
}
|
||||
|
||||
/**
|
||||
* Utility to find a point on a bezier curve (for hit detection)
|
||||
*/
|
||||
findPointOnBezier(
|
||||
t: number,
|
||||
p0: Point,
|
||||
p1: Point,
|
||||
p2: Point,
|
||||
p3: Point
|
||||
): Point {
|
||||
const mt = 1 - t
|
||||
const mt2 = mt * mt
|
||||
const mt3 = mt2 * mt
|
||||
const t2 = t * t
|
||||
const t3 = t2 * t
|
||||
|
||||
return {
|
||||
x: mt3 * p0.x + 3 * mt2 * t * p1.x + 3 * mt * t2 * p2.x + t3 * p3.x,
|
||||
y: mt3 * p0.y + 3 * mt2 * t * p1.y + 3 * mt * t2 * p2.y + t3 * p3.y
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw a link being dragged from a slot to the mouse position
|
||||
* Returns a Path2D object for potential hit detection
|
||||
*/
|
||||
drawDraggingLink(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
dragData: DragLinkData,
|
||||
context: RenderContext
|
||||
): Path2D {
|
||||
// Create LinkRenderData from drag data
|
||||
// When dragging from input, swap the points/directions
|
||||
const linkData: LinkRenderData = dragData.fromInput
|
||||
? {
|
||||
id: 'dragging',
|
||||
startPoint: dragData.dragPoint,
|
||||
endPoint: dragData.fixedPoint,
|
||||
startDirection:
|
||||
dragData.dragDirection ||
|
||||
this.getOppositeDirection(dragData.fixedDirection),
|
||||
endDirection: dragData.fixedDirection,
|
||||
color: dragData.color,
|
||||
type: dragData.type,
|
||||
disabled: dragData.disabled
|
||||
}
|
||||
: {
|
||||
id: 'dragging',
|
||||
startPoint: dragData.fixedPoint,
|
||||
endPoint: dragData.dragPoint,
|
||||
startDirection: dragData.fixedDirection,
|
||||
endDirection:
|
||||
dragData.dragDirection ||
|
||||
this.getOppositeDirection(dragData.fixedDirection),
|
||||
color: dragData.color,
|
||||
type: dragData.type,
|
||||
disabled: dragData.disabled
|
||||
}
|
||||
|
||||
// Use standard link drawing
|
||||
return this.drawLink(ctx, linkData, context)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the opposite direction (for drag preview)
|
||||
*/
|
||||
private getOppositeDirection(direction: Direction): Direction {
|
||||
switch (direction) {
|
||||
case 'left':
|
||||
return 'right'
|
||||
case 'right':
|
||||
return 'left'
|
||||
case 'up':
|
||||
return 'down'
|
||||
case 'down':
|
||||
return 'up'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the center point of a link (useful for labels, debugging)
|
||||
*/
|
||||
getLinkCenter(link: LinkRenderData): Point {
|
||||
// For now, simple midpoint
|
||||
// Could be enhanced to find actual curve midpoint
|
||||
return {
|
||||
x: (link.startPoint.x + link.endPoint.x) / 2,
|
||||
y: (link.startPoint.y + link.endPoint.y) / 2
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate and store the center point and angle of a link
|
||||
* Mimics the original litegraph center point calculation
|
||||
*/
|
||||
private calculateCenterPoint(
|
||||
link: LinkRenderData,
|
||||
context: RenderContext
|
||||
): void {
|
||||
const { startPoint, endPoint, controlPoints } = link
|
||||
|
||||
if (
|
||||
context.style.mode === 'spline' &&
|
||||
controlPoints &&
|
||||
controlPoints.length >= 2
|
||||
) {
|
||||
// For spline mode, find point at t=0.5 on the bezier curve
|
||||
const centerPos = this.findPointOnBezier(
|
||||
0.5,
|
||||
startPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1],
|
||||
endPoint
|
||||
)
|
||||
link.centerPos = centerPos
|
||||
|
||||
// Calculate angle for arrow marker (point slightly past center)
|
||||
if (context.style.centerMarkerShape === 'arrow') {
|
||||
const justPastCenter = this.findPointOnBezier(
|
||||
0.51,
|
||||
startPoint,
|
||||
controlPoints[0],
|
||||
controlPoints[1],
|
||||
endPoint
|
||||
)
|
||||
link.centerAngle = Math.atan2(
|
||||
justPastCenter.y - centerPos.y,
|
||||
justPastCenter.x - centerPos.x
|
||||
)
|
||||
}
|
||||
} else if (context.style.mode === 'linear') {
|
||||
// For linear mode, calculate midpoint between control points
|
||||
const startControl = this.getDirectionOffset(link.startDirection, 15)
|
||||
const endControl = this.getDirectionOffset(link.endDirection, 15)
|
||||
const innerA = {
|
||||
x: startPoint.x + startControl.x,
|
||||
y: startPoint.y + startControl.y
|
||||
}
|
||||
const innerB = {
|
||||
x: endPoint.x + endControl.x,
|
||||
y: endPoint.y + endControl.y
|
||||
}
|
||||
|
||||
link.centerPos = {
|
||||
x: (innerA.x + innerB.x) * 0.5,
|
||||
y: (innerA.y + innerB.y) * 0.5
|
||||
}
|
||||
|
||||
if (context.style.centerMarkerShape === 'arrow') {
|
||||
link.centerAngle = Math.atan2(innerB.y - innerA.y, innerB.x - innerA.x)
|
||||
}
|
||||
} else if (context.style.mode === 'straight') {
|
||||
// For straight mode, calculate midpoint
|
||||
const dx = endPoint.x - startPoint.x
|
||||
const dy = endPoint.y - startPoint.y
|
||||
|
||||
if (Math.abs(dx) > Math.abs(dy)) {
|
||||
const midX = startPoint.x + dx * 0.5
|
||||
link.centerPos = {
|
||||
x: midX,
|
||||
y: (startPoint.y + endPoint.y) * 0.5
|
||||
}
|
||||
} else {
|
||||
const midY = startPoint.y + dy * 0.5
|
||||
link.centerPos = {
|
||||
x: (startPoint.x + endPoint.x) * 0.5,
|
||||
y: midY
|
||||
}
|
||||
}
|
||||
|
||||
if (context.style.centerMarkerShape === 'arrow') {
|
||||
const diff = endPoint.y - startPoint.y
|
||||
if (Math.abs(diff) < 4) {
|
||||
link.centerAngle = 0
|
||||
} else if (diff > 0) {
|
||||
link.centerAngle = Math.PI * 0.5
|
||||
} else {
|
||||
link.centerAngle = -(Math.PI * 0.5)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Fallback to simple midpoint
|
||||
link.centerPos = this.getLinkCenter(link)
|
||||
if (context.style.centerMarkerShape === 'arrow') {
|
||||
link.centerAngle = Math.atan2(
|
||||
endPoint.y - startPoint.y,
|
||||
endPoint.x - startPoint.x
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Draw the center marker on a link (for menu interaction)
|
||||
* Matches the original litegraph center marker rendering
|
||||
*/
|
||||
private drawCenterMarker(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
link: LinkRenderData,
|
||||
context: RenderContext,
|
||||
color: string
|
||||
): void {
|
||||
if (!link.centerPos) return
|
||||
|
||||
ctx.beginPath()
|
||||
|
||||
if (
|
||||
context.style.centerMarkerShape === 'arrow' &&
|
||||
link.centerAngle !== undefined
|
||||
) {
|
||||
const transform = ctx.getTransform()
|
||||
ctx.translate(link.centerPos.x, link.centerPos.y)
|
||||
ctx.rotate(link.centerAngle)
|
||||
// The math is off, but it currently looks better in chromium (from original)
|
||||
ctx.moveTo(-3.2, -5)
|
||||
ctx.lineTo(7, 0)
|
||||
ctx.lineTo(-3.2, 5)
|
||||
ctx.setTransform(transform)
|
||||
} else {
|
||||
// Default to circle
|
||||
ctx.arc(link.centerPos.x, link.centerPos.y, 5, 0, Math.PI * 2)
|
||||
}
|
||||
|
||||
// Apply disabled pattern or color
|
||||
if (link.disabled && context.patterns?.disabled) {
|
||||
const { fillStyle, globalAlpha } = ctx
|
||||
ctx.fillStyle = context.patterns.disabled
|
||||
ctx.globalAlpha = 0.75
|
||||
ctx.fill()
|
||||
ctx.globalAlpha = globalAlpha
|
||||
ctx.fillStyle = fillStyle
|
||||
} else {
|
||||
ctx.fillStyle = color
|
||||
ctx.fill()
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user