From 027f0e643776bdf0b152f1f994a6cd77904dda83 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Thu, 14 Aug 2025 21:39:27 -0400 Subject: [PATCH] Revert "Totally not scuffed renderer and adapter" This reverts commit 2b9d83efb81d9b9800f868e8804601ce7983c0d8. --- src/lib/litegraph/src/LGraphCanvas.ts | 424 +++++++++--- .../adapters/LitegraphLinkAdapter.ts | 531 --------------- src/rendering/canvas/PathRenderer.ts | 630 ------------------ 3 files changed, 341 insertions(+), 1244 deletions(-) delete mode 100644 src/rendering/adapters/LitegraphLinkAdapter.ts delete mode 100644 src/rendering/canvas/PathRenderer.ts diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 0136d0646..cbb644cbf 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -1,9 +1,4 @@ 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' @@ -52,6 +47,7 @@ import { containsRect, createBounds, distance, + findPointOnCurve, isInRect, isInRectangle, isPointInRect, @@ -236,6 +232,9 @@ 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=' @@ -637,9 +636,6 @@ 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. */ @@ -701,11 +697,6 @@ 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() ) @@ -1799,9 +1790,6 @@ 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() } @@ -4550,26 +4538,18 @@ export class LGraphCanvas : LiteGraph.CONNECTING_LINK_COLOR // the connection being dragged by the mouse - if (this.linkRenderer) { - const context = this.buildLinkRenderContext() - this.linkRenderer.renderLinkDirect( - ctx, - pos, - highlightPos, - null, - false, - null, - colour, - fromDirection, - dragDirection, - context, - { - disabled: false - } - ) - } + this.renderLink( + ctx, + pos, + highlightPos, + null, + false, + null, + colour, + fromDirection, + dragDirection + ) - 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) @@ -5680,34 +5660,6 @@ 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 @@ -5749,27 +5701,333 @@ export class LGraphCanvas disabled?: boolean } = {} ): void { - 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 + 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 (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 } } diff --git a/src/rendering/adapters/LitegraphLinkAdapter.ts b/src/rendering/adapters/LitegraphLinkAdapter.ts deleted file mode 100644 index b957d3cc5..000000000 --- a/src/rendering/adapters/LitegraphLinkAdapter.ts +++ /dev/null @@ -1,531 +0,0 @@ -/** - * 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 - - // Colors - defaultLinkColor: CanvasColour - linkTypeColors: Record - - // 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 - ): Record { - const result: Record = {} - 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] - } - } -} diff --git a/src/rendering/canvas/PathRenderer.ts b/src/rendering/canvas/PathRenderer.ts deleted file mode 100644 index 47fd16453..000000000 --- a/src/rendering/canvas/PathRenderer.ts +++ /dev/null @@ -1,630 +0,0 @@ -/** - * 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 - 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 -} - -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() - } - } -}