mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-05 23:50:08 +00:00
- Move layout system to renderer/core/layout/ - Store, operations, adapters, and sync modules organized clearly - Merged layoutTypes.ts and layoutOperations.ts into single types.ts - Move canvas rendering to renderer/core/canvas/ - LiteGraph-specific code in litegraph/ subdirectory - PathRenderer at canvas level - Move spatial indexing to renderer/core/spatial/ - Move Vue node composables to renderer/extensions/vue-nodes/ - Update all import paths throughout codebase - Apply consistent naming (renderer vs rendering) This establishes clearer separation between core rendering concerns and optional extensions, making the architecture more maintainable. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-authored-by: Claude <noreply@anthropic.com>
821 lines
21 KiB
TypeScript
821 lines
21 KiB
TypeScript
/**
|
|
* 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,
|
|
link.startDirection,
|
|
link.endDirection
|
|
)
|
|
} 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,
|
|
startDir: Direction,
|
|
endDir: Direction
|
|
): void {
|
|
// Match original litegraph LINEAR_LINK mode with 4-point path
|
|
const l = 15 // offset distance for control points
|
|
|
|
const innerA = { x: start.x, y: start.y }
|
|
const innerB = { x: end.x, y: end.y }
|
|
|
|
// Apply directional offsets to create control points
|
|
switch (startDir) {
|
|
case 'left':
|
|
innerA.x -= l
|
|
break
|
|
case 'right':
|
|
innerA.x += l
|
|
break
|
|
case 'up':
|
|
innerA.y -= l
|
|
break
|
|
case 'down':
|
|
innerA.y += l
|
|
break
|
|
}
|
|
|
|
switch (endDir) {
|
|
case 'left':
|
|
innerB.x -= l
|
|
break
|
|
case 'right':
|
|
innerB.x += l
|
|
break
|
|
case 'up':
|
|
innerB.y -= l
|
|
break
|
|
case 'down':
|
|
innerB.y += l
|
|
break
|
|
}
|
|
|
|
// Draw 4-point path: start -> innerA -> innerB -> end
|
|
path.moveTo(start.x, start.y)
|
|
path.lineTo(innerA.x, innerA.y)
|
|
path.lineTo(innerB.x, innerB.y)
|
|
path.lineTo(end.x, end.y)
|
|
}
|
|
|
|
private buildStraightPath(
|
|
path: Path2D,
|
|
start: Point,
|
|
end: Point,
|
|
startDir: Direction,
|
|
endDir: Direction
|
|
): void {
|
|
// Match original STRAIGHT_LINK implementation with l=10 offset
|
|
const l = 10 // offset distance matching original
|
|
|
|
const innerA = { x: start.x, y: start.y }
|
|
const innerB = { x: end.x, y: end.y }
|
|
|
|
// Apply directional offsets to match original behavior
|
|
switch (startDir) {
|
|
case 'left':
|
|
innerA.x -= l
|
|
break
|
|
case 'right':
|
|
innerA.x += l
|
|
break
|
|
case 'up':
|
|
innerA.y -= l
|
|
break
|
|
case 'down':
|
|
innerA.y += l
|
|
break
|
|
}
|
|
|
|
switch (endDir) {
|
|
case 'left':
|
|
innerB.x -= l
|
|
break
|
|
case 'right':
|
|
innerB.x += l
|
|
break
|
|
case 'up':
|
|
innerB.y -= l
|
|
break
|
|
case 'down':
|
|
innerB.y += l
|
|
break
|
|
}
|
|
|
|
// Calculate midpoint using innerA/innerB positions (matching original)
|
|
const midX = (innerA.x + innerB.x) * 0.5
|
|
|
|
// Build path: start -> innerA -> (midX, innerA.y) -> (midX, innerB.y) -> innerB -> end
|
|
path.moveTo(start.x, start.y)
|
|
path.lineTo(innerA.x, innerA.y)
|
|
path.lineTo(midX, innerA.y)
|
|
path.lineTo(midX, innerB.y)
|
|
path.lineTo(innerB.x, innerB.y)
|
|
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
|
|
|
|
// Render arrows at 0.25 and 0.75 positions along the path (matching original)
|
|
const positions = [0.25, 0.75]
|
|
|
|
for (const t of positions) {
|
|
// Compute arrow position and angle
|
|
const posA = this.computeConnectionPoint(link, t, context)
|
|
const posB = this.computeConnectionPoint(link, t + 0.01, context) // slightly ahead for angle
|
|
|
|
const angle = Math.atan2(posB.y - posA.y, posB.x - posA.x)
|
|
|
|
// Draw arrow triangle (matching original shape)
|
|
const transform = ctx.getTransform()
|
|
ctx.translate(posA.x, posA.y)
|
|
ctx.rotate(angle)
|
|
ctx.fillStyle = color
|
|
ctx.beginPath()
|
|
ctx.moveTo(-5, -3)
|
|
ctx.lineTo(0, +7)
|
|
ctx.lineTo(+5, -3)
|
|
ctx.fill()
|
|
ctx.setTransform(transform)
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Compute a point along the link path at position t (0 to 1)
|
|
* For backward compatibility with original litegraph, this always uses
|
|
* bezier calculation with spline offsets, regardless of render mode.
|
|
* This ensures arrow positions match the original implementation.
|
|
*/
|
|
private computeConnectionPoint(
|
|
link: LinkRenderData,
|
|
t: number,
|
|
_context: RenderContext
|
|
): Point {
|
|
const { startPoint, endPoint, startDirection, endDirection } = link
|
|
|
|
// Match original behavior: always use bezier math with spline offsets
|
|
// regardless of render mode (for arrow position compatibility)
|
|
const dist = Math.sqrt(
|
|
Math.pow(endPoint.x - startPoint.x, 2) +
|
|
Math.pow(endPoint.y - startPoint.y, 2)
|
|
)
|
|
const factor = 0.25
|
|
|
|
// Create control points with spline offsets (matching original #addSplineOffset)
|
|
const pa = { x: startPoint.x, y: startPoint.y }
|
|
const pb = { x: endPoint.x, y: endPoint.y }
|
|
|
|
// Apply spline offsets based on direction
|
|
switch (startDirection) {
|
|
case 'left':
|
|
pa.x -= dist * factor
|
|
break
|
|
case 'right':
|
|
pa.x += dist * factor
|
|
break
|
|
case 'up':
|
|
pa.y -= dist * factor
|
|
break
|
|
case 'down':
|
|
pa.y += dist * factor
|
|
break
|
|
}
|
|
|
|
switch (endDirection) {
|
|
case 'left':
|
|
pb.x -= dist * factor
|
|
break
|
|
case 'right':
|
|
pb.x += dist * factor
|
|
break
|
|
case 'up':
|
|
pb.y -= dist * factor
|
|
break
|
|
case 'down':
|
|
pb.y += dist * factor
|
|
break
|
|
}
|
|
|
|
// Calculate bezier point (matching original computeConnectionPoint)
|
|
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
|
|
|
|
return {
|
|
x: c1 * startPoint.x + c2 * pa.x + c3 * pb.x + c4 * endPoint.x,
|
|
y: c1 * startPoint.y + c2 * pa.y + c3 * pb.y + c4 * endPoint.y
|
|
}
|
|
}
|
|
|
|
private drawFlowAnimation(
|
|
ctx: CanvasRenderingContext2D,
|
|
_path: Path2D,
|
|
link: LinkRenderData,
|
|
context: RenderContext
|
|
): void {
|
|
if (!context.animation) return
|
|
|
|
// Match original implementation: render 5 moving circles along the path
|
|
const time = context.animation.time
|
|
const linkColor = this.determineLinkColor(link, context, false)
|
|
|
|
ctx.save()
|
|
ctx.fillStyle = linkColor
|
|
|
|
// Draw 5 circles at different positions along the path
|
|
for (let i = 0; i < 5; ++i) {
|
|
// Calculate position along path (0 to 1), with time-based animation
|
|
const f = (time + i * 0.2) % 1
|
|
const flowPos = this.computeConnectionPoint(link, f, context)
|
|
|
|
// Draw circle at this position
|
|
ctx.beginPath()
|
|
ctx.arc(flowPos.x, flowPos.y, 5, 0, 2 * Math.PI)
|
|
ctx.fill()
|
|
}
|
|
|
|
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 (matching original)
|
|
const l = 15 // Same offset as buildLinearPath
|
|
const innerA = { x: startPoint.x, y: startPoint.y }
|
|
const innerB = { x: endPoint.x, y: endPoint.y }
|
|
|
|
// Apply same directional offsets as buildLinearPath
|
|
switch (link.startDirection) {
|
|
case 'left':
|
|
innerA.x -= l
|
|
break
|
|
case 'right':
|
|
innerA.x += l
|
|
break
|
|
case 'up':
|
|
innerA.y -= l
|
|
break
|
|
case 'down':
|
|
innerA.y += l
|
|
break
|
|
}
|
|
|
|
switch (link.endDirection) {
|
|
case 'left':
|
|
innerB.x -= l
|
|
break
|
|
case 'right':
|
|
innerB.x += l
|
|
break
|
|
case 'up':
|
|
innerB.y -= l
|
|
break
|
|
case 'down':
|
|
innerB.y += l
|
|
break
|
|
}
|
|
|
|
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, match original STRAIGHT_LINK center calculation
|
|
const l = 10 // Same offset as buildStraightPath
|
|
const innerA = { x: startPoint.x, y: startPoint.y }
|
|
const innerB = { x: endPoint.x, y: endPoint.y }
|
|
|
|
// Apply same directional offsets as buildStraightPath
|
|
switch (link.startDirection) {
|
|
case 'left':
|
|
innerA.x -= l
|
|
break
|
|
case 'right':
|
|
innerA.x += l
|
|
break
|
|
case 'up':
|
|
innerA.y -= l
|
|
break
|
|
case 'down':
|
|
innerA.y += l
|
|
break
|
|
}
|
|
|
|
switch (link.endDirection) {
|
|
case 'left':
|
|
innerB.x -= l
|
|
break
|
|
case 'right':
|
|
innerB.x += l
|
|
break
|
|
case 'up':
|
|
innerB.y -= l
|
|
break
|
|
case 'down':
|
|
innerB.y += l
|
|
break
|
|
}
|
|
|
|
// Calculate center using midX and average of innerA/innerB y positions
|
|
const midX = (innerA.x + innerB.x) * 0.5
|
|
link.centerPos = {
|
|
x: midX,
|
|
y: (innerA.y + innerB.y) * 0.5
|
|
}
|
|
|
|
if (context.style.centerMarkerShape === 'arrow') {
|
|
const diff = innerB.y - innerA.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()
|
|
}
|
|
}
|
|
}
|