mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 03:01:54 +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 { 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 { CanvasPointer } from './CanvasPointer'
|
||||||
import type { ContextMenu } from './ContextMenu'
|
import type { ContextMenu } from './ContextMenu'
|
||||||
@@ -47,7 +52,6 @@ import {
|
|||||||
containsRect,
|
containsRect,
|
||||||
createBounds,
|
createBounds,
|
||||||
distance,
|
distance,
|
||||||
findPointOnCurve,
|
|
||||||
isInRect,
|
isInRect,
|
||||||
isInRectangle,
|
isInRectangle,
|
||||||
isPointInRect,
|
isPointInRect,
|
||||||
@@ -232,9 +236,6 @@ export class LGraphCanvas
|
|||||||
static #tmp_area = new Float32Array(4)
|
static #tmp_area = new Float32Array(4)
|
||||||
static #margin_area = new Float32Array(4)
|
static #margin_area = new Float32Array(4)
|
||||||
static #link_bounding = 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 =
|
static DEFAULT_BACKGROUND_IMAGE =
|
||||||
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAGQAAABkCAIAAAD/gAIDAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAQBJREFUeNrs1rEKwjAUhlETUkj3vP9rdmr1Ysammk2w5wdxuLgcMHyptfawuZX4pJSWZTnfnu/lnIe/jNNxHHGNn//HNbbv+4dr6V+11uF527arU7+u63qfa/bnmh8sWLBgwYJlqRf8MEptXPBXJXa37BSl3ixYsGDBMliwFLyCV/DeLIMFCxYsWLBMwSt4Be/NggXLYMGCBUvBK3iNruC9WbBgwYJlsGApeAWv4L1ZBgsWLFiwYJmCV/AK3psFC5bBggULloJX8BpdwXuzYMGCBctgwVLwCl7Be7MMFixYsGDBsu8FH1FaSmExVfAxBa/gvVmwYMGCZbBg/W4vAQYA5tRF9QYlv/QAAAAASUVORK5CYII='
|
'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 */
|
/** Set on keydown, keyup. @todo */
|
||||||
#shiftDown: boolean = false
|
#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. */
|
/** If true, enable drag zoom. Ctrl+Shift+Drag Up/Down: zoom canvas. */
|
||||||
dragZoomEnabled: boolean = false
|
dragZoomEnabled: boolean = false
|
||||||
/** The start position of the drag zoom. */
|
/** The start position of the drag zoom. */
|
||||||
@@ -697,6 +701,11 @@ export class LGraphCanvas
|
|||||||
this.ds = new DragAndScale(canvas)
|
this.ds = new DragAndScale(canvas)
|
||||||
this.pointer = new CanvasPointer(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.linkConnector.events.addEventListener('link-created', () =>
|
||||||
this.#dirty()
|
this.#dirty()
|
||||||
)
|
)
|
||||||
@@ -1790,6 +1799,9 @@ export class LGraphCanvas
|
|||||||
this.clear()
|
this.clear()
|
||||||
newGraph.attachCanvas(this)
|
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.dispatch('litegraph:set-graph', { newGraph, oldGraph: graph })
|
||||||
this.#dirty()
|
this.#dirty()
|
||||||
}
|
}
|
||||||
@@ -4538,18 +4550,26 @@ export class LGraphCanvas
|
|||||||
: LiteGraph.CONNECTING_LINK_COLOR
|
: LiteGraph.CONNECTING_LINK_COLOR
|
||||||
|
|
||||||
// the connection being dragged by the mouse
|
// the connection being dragged by the mouse
|
||||||
this.renderLink(
|
if (this.linkRenderer) {
|
||||||
ctx,
|
const context = this.buildLinkRenderContext()
|
||||||
pos,
|
this.linkRenderer.renderLinkDirect(
|
||||||
highlightPos,
|
ctx,
|
||||||
null,
|
pos,
|
||||||
false,
|
highlightPos,
|
||||||
null,
|
null,
|
||||||
colour,
|
false,
|
||||||
fromDirection,
|
null,
|
||||||
dragDirection
|
colour,
|
||||||
)
|
fromDirection,
|
||||||
|
dragDirection,
|
||||||
|
context,
|
||||||
|
{
|
||||||
|
disabled: false
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fillStyle = colour
|
||||||
ctx.beginPath()
|
ctx.beginPath()
|
||||||
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
|
if (connType === LiteGraph.EVENT || connShape === RenderShape.BOX) {
|
||||||
ctx.rect(pos[0] - 6 + 0.5, pos[1] - 5 + 0.5, 14, 10)
|
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
|
* draws a link between two points
|
||||||
* @param ctx Canvas 2D rendering context
|
* @param ctx Canvas 2D rendering context
|
||||||
@@ -5701,333 +5749,27 @@ export class LGraphCanvas
|
|||||||
disabled?: boolean
|
disabled?: boolean
|
||||||
} = {}
|
} = {}
|
||||||
): void {
|
): void {
|
||||||
const linkColour =
|
if (this.linkRenderer) {
|
||||||
link != null && this.highlighted_links[link.id]
|
const context = this.buildLinkRenderContext()
|
||||||
? '#FFF'
|
this.linkRenderer.renderLinkDirect(
|
||||||
: color ||
|
ctx,
|
||||||
link?.color ||
|
a,
|
||||||
(link?.type != null && LGraphCanvas.link_type_colors[link.type]) ||
|
b,
|
||||||
this.default_link_color
|
link,
|
||||||
const startDir = start_dir || LinkDirection.RIGHT
|
skip_border,
|
||||||
const endDir = end_dir || LinkDirection.LEFT
|
flow,
|
||||||
|
color,
|
||||||
const dist =
|
start_dir,
|
||||||
this.links_render_mode == LinkRenderType.SPLINE_LINK &&
|
end_dir,
|
||||||
(!endControl || !startControl)
|
context,
|
||||||
? distance(a, b)
|
{
|
||||||
: 0
|
reroute,
|
||||||
|
startControl,
|
||||||
// TODO: Subline code below was inserted in the wrong place - should be before this statement
|
endControl,
|
||||||
if (this.render_connections_border && !this.low_quality) {
|
num_sublines,
|
||||||
ctx.lineWidth = this.connections_width + 4
|
disabled
|
||||||
}
|
|
||||||
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 (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