diff --git a/.gitattributes b/.gitattributes index 0f538ae76..39d7f722c 100644 --- a/.gitattributes +++ b/.gitattributes @@ -7,6 +7,7 @@ *.json text eol=lf *.mjs text eol=lf *.mts text eol=lf +*.snap text eol=lf *.ts text eol=lf *.vue text eol=lf *.yaml text eol=lf diff --git a/knip.config.ts b/knip.config.ts index 3bc025cad..a77574f97 100644 --- a/knip.config.ts +++ b/knip.config.ts @@ -14,7 +14,7 @@ const config: KnipConfig = { }, 'apps/desktop-ui': { entry: ['src/main.ts', 'src/i18n.ts'], - project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}'] + project: ['src/**/*.{js,ts,vue}'] }, 'packages/tailwind-utils': { project: ['src/**/*.{js,ts}'] diff --git a/src/composables/graph/useVueNodeLifecycle.ts b/src/composables/graph/useVueNodeLifecycle.ts index cfcee1d44..2b32e51bf 100644 --- a/src/composables/graph/useVueNodeLifecycle.ts +++ b/src/composables/graph/useVueNodeLifecycle.ts @@ -4,13 +4,11 @@ import { shallowRef, watch } from 'vue' import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager' import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager' import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags' -import type { LGraphCanvas, LGraphNode } from '@/lib/litegraph/src/litegraph' +import type { LGraphNode } from '@/lib/litegraph/src/litegraph' import { useCanvasStore } from '@/renderer/core/canvas/canvasStore' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync' -import { useLinkLayoutSync } from '@/renderer/core/layout/sync/useLinkLayoutSync' -import { useSlotLayoutSync } from '@/renderer/core/layout/sync/useSlotLayoutSync' import { app as comfyApp } from '@/scripts/app' function useVueNodeLifecycleIndividual() { @@ -21,8 +19,6 @@ function useVueNodeLifecycleIndividual() { const nodeManager = shallowRef(null) const { startSync } = useLayoutSync() - const linkSyncManager = useLinkLayoutSync() - const slotSyncManager = useSlotLayoutSync() const initializeNodeManager = () => { // Use canvas graph if available (handles subgraph contexts), fallback to app graph @@ -62,10 +58,6 @@ function useVueNodeLifecycleIndividual() { // Initialize layout sync (one-way: Layout Store → LiteGraph) startSync(canvasStore.canvas) - - if (comfyApp.canvas) { - linkSyncManager.start(comfyApp.canvas) - } } const disposeNodeManagerAndSyncs = () => { @@ -77,8 +69,6 @@ function useVueNodeLifecycleIndividual() { /* empty */ } nodeManager.value = null - - linkSyncManager.stop() } // Watch for Vue nodes enabled state changes @@ -96,25 +86,14 @@ function useVueNodeLifecycleIndividual() { // Consolidated watch for slot layout sync management watch( - [() => canvasStore.canvas, () => shouldRenderVueNodes.value], - ([canvas, vueMode], [, oldVueMode]) => { + () => shouldRenderVueNodes.value, + (vueMode, oldVueMode) => { const modeChanged = vueMode !== oldVueMode // Clear stale slot layouts when switching modes if (modeChanged) { layoutStore.clearAllSlotLayouts() } - - // Switching to Vue - if (vueMode) { - slotSyncManager.stop() - } - - // Switching to LG - const shouldRun = Boolean(canvas?.graph) && !vueMode - if (shouldRun && canvas) { - slotSyncManager.attemptStart(canvas as LGraphCanvas) - } }, { immediate: true, flush: 'sync' } ) @@ -152,8 +131,6 @@ function useVueNodeLifecycleIndividual() { nodeManager.value.cleanup() nodeManager.value = null } - slotSyncManager.stop() - linkSyncManager.stop() } return { diff --git a/src/lib/litegraph/src/LGraph.ts b/src/lib/litegraph/src/LGraph.ts index b588baa74..4c7b1ff2a 100644 --- a/src/lib/litegraph/src/LGraph.ts +++ b/src/lib/litegraph/src/LGraph.ts @@ -223,15 +223,15 @@ export class LGraph /** Internal only. Not required for serialisation; calculated on deserialise. */ #lastFloatingLinkId: number = 0 - #floatingLinks: Map = new Map() + private readonly floatingLinksInternal: Map = new Map() get floatingLinks(): ReadonlyMap { - return this.#floatingLinks + return this.floatingLinksInternal } - #reroutes = new Map() + private readonly reroutesInternal = new Map() /** All reroutes in this graph. */ public get reroutes(): Map { - return this.#reroutes + return this.reroutesInternal } get rootGraph(): LGraph { @@ -340,7 +340,7 @@ export class LGraph this._links.clear() this.reroutes.clear() - this.#floatingLinks.clear() + this.floatingLinksInternal.clear() this.#lastFloatingLinkId = 0 @@ -1268,7 +1268,7 @@ export class LGraph if (link.id === -1) { link.id = ++this.#lastFloatingLinkId } - this.#floatingLinks.set(link.id, link) + this.floatingLinksInternal.set(link.id, link) const slot = link.target_id !== -1 @@ -1291,7 +1291,7 @@ export class LGraph } removeFloatingLink(link: LLink): void { - this.#floatingLinks.delete(link.id) + this.floatingLinksInternal.delete(link.id) const slot = link.target_id !== -1 diff --git a/src/lib/litegraph/src/Reroute.ts b/src/lib/litegraph/src/Reroute.ts index 348289709..6436df532 100644 --- a/src/lib/litegraph/src/Reroute.ts +++ b/src/lib/litegraph/src/Reroute.ts @@ -51,31 +51,31 @@ export class Reroute } /** The network this reroute belongs to. Contains all valid links and reroutes. */ - #network: WeakRef + private readonly network: WeakRef - #parentId?: RerouteId + private parentIdInternal?: RerouteId public get parentId(): RerouteId | undefined { - return this.#parentId + return this.parentIdInternal } /** Ignores attempts to create an infinite loop. @inheritdoc */ public set parentId(value) { if (value === this.id) return if (this.getReroutes() === null) return - this.#parentId = value + this.parentIdInternal = value } public get parent(): Reroute | undefined { - return this.#network.deref()?.getReroute(this.#parentId) + return this.network.deref()?.getReroute(this.parentIdInternal) } /** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */ floating?: FloatingRerouteSlot - #pos: Point = [0, 0] + private readonly posInternal: Point = [0, 0] /** @inheritdoc */ get pos(): Point { - return this.#pos + return this.posInternal } set pos(value: Point) { @@ -83,14 +83,14 @@ export class Reroute throw new TypeError( 'Reroute.pos is an x,y point, and expects an indexable with at least two values.' ) - this.#pos[0] = value[0] - this.#pos[1] = value[1] + this.posInternal[0] = value[0] + this.posInternal[1] = value[1] } /** @inheritdoc */ get boundingRect(): ReadOnlyRect { const { radius } = Reroute - const [x, y] = this.#pos + const [x, y] = this.posInternal return [x - radius, y - radius, 2 * radius, 2 * radius] } @@ -98,11 +98,11 @@ export class Reroute * Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection. * Eliminates most hover positions using an extremely cheap check. */ - get #hoverArea(): ReadOnlyRect { + private get hoverArea(): ReadOnlyRect { const xOffset = 2 * Reroute.slotOffset const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius) - const [x, y] = this.#pos + const [x, y] = this.posInternal return [x - xOffset, y - yOffset, 2 * xOffset, 2 * yOffset] } @@ -149,35 +149,35 @@ export class Reroute * Used to ensure reroute angles are only executed once per frame. * @todo Calculate on change instead. */ - #lastRenderTime: number = -Infinity + private lastRenderTime: number = -Infinity - #inputSlot = new RerouteSlot(this, true) - #outputSlot = new RerouteSlot(this, false) + private readonly inputSlot = new RerouteSlot(this, true) + private readonly outputSlot = new RerouteSlot(this, false) get isSlotHovered(): boolean { return this.isInputHovered || this.isOutputHovered } get isInputHovered(): boolean { - return this.#inputSlot.hovering + return this.inputSlot.hovering } get isOutputHovered(): boolean { - return this.#outputSlot.hovering + return this.outputSlot.hovering } get firstLink(): LLink | undefined { const linkId = this.linkIds.values().next().value return linkId === undefined ? undefined - : this.#network.deref()?.links.get(linkId) + : this.network.deref()?.links.get(linkId) } get firstFloatingLink(): LLink | undefined { const linkId = this.floatingLinkIds.values().next().value return linkId === undefined ? undefined - : this.#network.deref()?.floatingLinks.get(linkId) + : this.network.deref()?.floatingLinks.get(linkId) } /** @inheritdoc */ @@ -205,7 +205,7 @@ export class Reroute linkIds?: Iterable, floatingLinkIds?: Iterable ) { - this.#network = new WeakRef(network) + this.network = new WeakRef(network) this.parentId = parentId if (pos) this.pos = pos this.linkIds = new Set(linkIds) @@ -261,15 +261,15 @@ export class Reroute */ getReroutes(visited = new Set()): Reroute[] | null { // No parentId - last in the chain - if (this.#parentId === undefined) return [this] + if (this.parentIdInternal === undefined) return [this] // Invalid chain - looped if (visited.has(this)) return null visited.add(this) - const parent = this.#network.deref()?.reroutes.get(this.#parentId) + const parent = this.network.deref()?.reroutes.get(this.parentIdInternal) // Invalid parent (or network) - drop silently to recover if (!parent) { - this.#parentId = undefined + this.parentIdInternal = undefined return [this] } @@ -288,14 +288,14 @@ export class Reroute withParentId: RerouteId, visited = new Set() ): Reroute | null | undefined { - if (this.#parentId === withParentId) return this + if (this.parentIdInternal === withParentId) return this if (visited.has(this)) return null visited.add(this) - if (this.#parentId === undefined) return + if (this.parentIdInternal === undefined) return - return this.#network + return this.network .deref() - ?.reroutes.get(this.#parentId) + ?.reroutes.get(this.parentIdInternal) ?.findNextReroute(withParentId, visited) } @@ -309,7 +309,7 @@ export class Reroute const link = this.firstLink ?? this.firstFloatingLink if (!link) return - const node = this.#network.deref()?.getNodeById(link.origin_id) + const node = this.network.deref()?.getNodeById(link.origin_id) if (!node) return return { @@ -325,7 +325,7 @@ export class Reroute findTargetInputs(): | { node: LGraphNode; input: INodeInputSlot; link: LLink }[] | undefined { - const network = this.#network.deref() + const network = this.network.deref() if (!network) return const results: { @@ -363,7 +363,7 @@ export class Reroute * @returns An array of floating links */ getFloatingLinks(from: 'input' | 'output'): LLink[] | undefined { - const floatingLinks = this.#network.deref()?.floatingLinks + const floatingLinks = this.network.deref()?.floatingLinks if (!floatingLinks) return const idProp = from === 'input' ? 'origin_id' : 'target_id' @@ -387,7 +387,7 @@ export class Reroute output: INodeOutputSlot, index: number ) { - const network = this.#network.deref() + const network = this.network.deref() const floatingOutLinks = this.getFloatingLinks('output') if (!floatingOutLinks) throw new Error('[setFloatingLinkOrigin]: Invalid network.') @@ -411,15 +411,15 @@ export class Reroute /** @inheritdoc */ move(deltaX: number, deltaY: number) { - const previousPos = { x: this.#pos[0], y: this.#pos[1] } - this.#pos[0] += deltaX - this.#pos[1] += deltaY + const previousPos = { x: this.posInternal[0], y: this.posInternal[1] } + this.posInternal[0] += deltaX + this.posInternal[1] += deltaY // Update Layout Store with new position layoutMutations.setSource(LayoutSource.Canvas) layoutMutations.moveReroute( this.id, - { x: this.#pos[0], y: this.#pos[1] }, + { x: this.posInternal[0], y: this.posInternal[1] }, previousPos ) } @@ -441,7 +441,7 @@ export class Reroute } removeFloatingLink(linkId: LinkId) { - const network = this.#network.deref() + const network = this.network.deref() if (!network) return const floatingLink = network.floatingLinks.get(linkId) @@ -462,7 +462,7 @@ export class Reroute * @remarks Does not remove the link from the network. */ removeLink(link: LLink) { - const network = this.#network.deref() + const network = this.network.deref() if (!network) return const floatingLink = network.floatingLinks.get(link.id) @@ -474,7 +474,7 @@ export class Reroute } remove() { - const network = this.#network.deref() + const network = this.network.deref() if (!network) return network.removeReroute(this.id) @@ -486,8 +486,8 @@ export class Reroute linkStart: Point ): void { // Ensure we run once per render - if (!(lastRenderTime > this.#lastRenderTime)) return - this.#lastRenderTime = lastRenderTime + if (!(lastRenderTime > this.lastRenderTime)) return + this.lastRenderTime = lastRenderTime const { id, pos: thisPos } = this @@ -509,14 +509,14 @@ export class Reroute sum /= angles.length const originToReroute = Math.atan2( - this.#pos[1] - linkStart[1], - this.#pos[0] - linkStart[0] + this.posInternal[1] - linkStart[1], + this.posInternal[0] - linkStart[0] ) let diff = (originToReroute - sum) * 0.5 if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI const dist = Math.min( Reroute.maxSplineOffset, - distance(linkStart, this.#pos) * 0.25 + distance(linkStart, this.posInternal) * 0.25 ) // Store results @@ -604,8 +604,8 @@ export class Reroute * @param ctx The canvas context to draw on. */ drawSlots(ctx: CanvasRenderingContext2D): void { - this.#inputSlot.draw(ctx) - this.#outputSlot.draw(ctx) + this.inputSlot.draw(ctx) + this.outputSlot.draw(ctx) } drawHighlight(ctx: CanvasRenderingContext2D, colour: CanvasColour): void { @@ -629,8 +629,8 @@ export class Reroute * @returns `true` if any changes require a redraw. */ updateVisibility(pos: Point): boolean { - const input = this.#inputSlot - const output = this.#outputSlot + const input = this.inputSlot + const output = this.outputSlot input.dirty = false output.dirty = false @@ -642,8 +642,8 @@ export class Reroute const showEither = showInput || showOutput // Check if even in the vicinity - if (showEither && isPointInRect(pos, this.#hoverArea)) { - const outlineOnly = this.#contains(pos) + if (showEither && isPointInRect(pos, this.hoverArea)) { + const outlineOnly = this.contains(pos) if (showInput) input.update(pos, outlineOnly) if (showOutput) output.update(pos, outlineOnly) @@ -656,8 +656,8 @@ export class Reroute /** Prevents rendering of the input and output slots. */ hideSlots() { - this.#inputSlot.hide() - this.#outputSlot.hide() + this.inputSlot.hide() + this.outputSlot.hide() } /** @@ -666,10 +666,10 @@ export class Reroute * @returns `true` if {@link pos} is within the reroute's radius. */ containsPoint(pos: Point): boolean { - return isPointInRect(pos, this.#hoverArea) && this.#contains(pos) + return isPointInRect(pos, this.hoverArea) && this.contains(pos) } - #contains(pos: Point): boolean { + private contains(pos: Point): boolean { return distance(this.pos, pos) <= Reroute.radius } @@ -692,47 +692,47 @@ export class Reroute */ class RerouteSlot { /** The reroute that the slot belongs to. */ - readonly #reroute: Reroute + private readonly reroute: Reroute - readonly #offsetMultiplier: 1 | -1 + private readonly offsetMultiplier: 1 | -1 /** Centre point of this slot. */ get pos(): Point { - const [x, y] = this.#reroute.pos - return [x + Reroute.slotOffset * this.#offsetMultiplier, y] + const [x, y] = this.reroute.pos + return [x + Reroute.slotOffset * this.offsetMultiplier, y] } /** Whether any changes require a redraw. */ dirty: boolean = false - #hovering = false + private hoveringInternal = false /** Whether the pointer is hovering over the slot itself. */ get hovering() { - return this.#hovering + return this.hoveringInternal } set hovering(value) { - if (!Object.is(this.#hovering, value)) { - this.#hovering = value + if (!Object.is(this.hoveringInternal, value)) { + this.hoveringInternal = value this.dirty = true } } - #showOutline = false + private showOutlineInternal = false /** Whether the slot outline / faint background is visible. */ get showOutline() { - return this.#showOutline + return this.showOutlineInternal } set showOutline(value) { - if (!Object.is(this.#showOutline, value)) { - this.#showOutline = value + if (!Object.is(this.showOutlineInternal, value)) { + this.showOutlineInternal = value this.dirty = true } } constructor(reroute: Reroute, isInput: boolean) { - this.#reroute = reroute - this.#offsetMultiplier = isInput ? -1 : 1 + this.reroute = reroute + this.offsetMultiplier = isInput ? -1 : 1 } /** @@ -771,7 +771,7 @@ class RerouteSlot { if (!showOutline) return try { - ctx.fillStyle = hovering ? this.#reroute.colour : 'rgba(127,127,127,0.3)' + ctx.fillStyle = hovering ? this.reroute.colour : 'rgba(127,127,127,0.3)' ctx.strokeStyle = 'rgb(0,0,0,0.5)' ctx.lineWidth = 1 diff --git a/src/renderer/core/layout/slots/register.ts b/src/renderer/core/layout/slots/register.ts deleted file mode 100644 index 747ae4543..000000000 --- a/src/renderer/core/layout/slots/register.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * Slot Registration - * - * Handles registration of slot layouts with the layout store for hit testing. - * This module manages the state mutation side of slot layout management, - * while pure calculations are handled separately in SlotCalculations.ts. - */ -import type { Point } from '@/lib/litegraph/src/interfaces' -import { LiteGraph } from '@/lib/litegraph/src/litegraph' -import { - calculateInputSlotPos, - calculateOutputSlotPos -} from '@/renderer/core/canvas/litegraph/slotCalculations' -import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations' -import { layoutStore } from '@/renderer/core/layout/store/layoutStore' -import type { SlotLayout } from '@/renderer/core/layout/types' - -import { getSlotKey } from './slotIdentifier' - -/** - * Register slot layout with the layout store for hit testing - * @param nodeId The node ID - * @param slotIndex The slot index - * @param isInput Whether this is an input slot - * @param position The slot position in graph coordinates - */ -function registerSlotLayout( - nodeId: string, - slotIndex: number, - isInput: boolean, - position: Point -): void { - const slotKey = getSlotKey(nodeId, slotIndex, isInput) - - // Calculate bounds for the slot using LiteGraph's standard slot height - const slotSize = LiteGraph.NODE_SLOT_HEIGHT - const halfSize = slotSize / 2 - - const slotLayout: SlotLayout = { - nodeId, - index: slotIndex, - type: isInput ? 'input' : 'output', - position: { x: position[0], y: position[1] }, - bounds: { - x: position[0] - halfSize, - y: position[1] - halfSize, - width: slotSize, - height: slotSize - } - } - - layoutStore.updateSlotLayout(slotKey, slotLayout) -} - -/** - * Register all slots for a node - * @param nodeId The node ID - * @param context The slot position context - */ -export function registerNodeSlots( - nodeId: string, - context: SlotPositionContext -): void { - // Register input slots - context.inputs.forEach((_, index) => { - const position = calculateInputSlotPos(context, index) - registerSlotLayout(nodeId, index, true, position) - }) - - // Register output slots - context.outputs.forEach((_, index) => { - const position = calculateOutputSlotPos(context, index) - registerSlotLayout(nodeId, index, false, position) - }) -} diff --git a/src/renderer/core/layout/sync/useLinkLayoutSync.ts b/src/renderer/core/layout/sync/useLinkLayoutSync.ts deleted file mode 100644 index b1cbc5fa0..000000000 --- a/src/renderer/core/layout/sync/useLinkLayoutSync.ts +++ /dev/null @@ -1,310 +0,0 @@ -import { tryOnScopeDispose } from '@vueuse/core' -import { computed, ref, toValue } from 'vue' - -import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' -import { LLink } from '@/lib/litegraph/src/LLink' -import { Reroute } from '@/lib/litegraph/src/Reroute' -import type { Point } from '@/lib/litegraph/src/interfaces' -import { LinkDirection } from '@/lib/litegraph/src/types/globalEnums' -import { LitegraphLinkAdapter } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' -import type { LinkRenderContext } from '@/renderer/core/canvas/litegraph/litegraphLinkAdapter' -import { getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' -import { layoutStore } from '@/renderer/core/layout/store/layoutStore' -import type { LayoutChange } from '@/renderer/core/layout/types' - -export function useLinkLayoutSync() { - const canvasRef = ref() - const graphRef = computed(() => canvasRef.value?.graph) - const unsubscribeLayoutChange = ref<() => void>() - const adapter = new LitegraphLinkAdapter() - - /** - * Build link render context from canvas properties - */ - function buildLinkRenderContext(): LinkRenderContext { - const canvas = toValue(canvasRef) - if (!canvas) { - throw new Error('Canvas not initialized') - } - - return { - // Canvas settings - renderMode: canvas.links_render_mode, - connectionWidth: canvas.connections_width, - renderBorder: canvas.render_connections_border, - lowQuality: canvas.low_quality, - highQualityRender: canvas.highquality_render, - scale: canvas.ds.scale, - linkMarkerShape: canvas.linkMarkerShape, - renderConnectionArrows: canvas.render_connection_arrows, - - // State - highlightedLinks: new Set(Object.keys(canvas.highlighted_links)), - - // Colors - defaultLinkColor: canvas.default_link_color, - linkTypeColors: (canvas.constructor as any).link_type_colors || {}, - - // Pattern for disabled links - disabledPattern: canvas._pattern - } - } - - /** - * Recompute a single link and all its segments - * - * Note: This logic mirrors LGraphCanvas#renderAllLinkSegments but: - * - Works with offscreen context for event-driven updates - * - No visibility checks (always computes full geometry) - * - No dragging state handling (pure geometry computation) - */ - function recomputeLinkById(linkId: number): void { - const canvas = toValue(canvasRef) - const graph = toValue(graphRef) - if (!graph || !canvas) return - - const link = graph.links.get(linkId) - if (!link || link.id === -1) return // Skip floating/temp links - - // Get source and target nodes - const sourceNode = graph.getNodeById(link.origin_id) - const targetNode = graph.getNodeById(link.target_id) - if (!sourceNode || !targetNode) return - - // Get slots - const sourceSlot = sourceNode.outputs?.[link.origin_slot] - const targetSlot = targetNode.inputs?.[link.target_slot] - if (!sourceSlot || !targetSlot) return - - // Get positions - const startPos = getSlotPosition(sourceNode, link.origin_slot, false) - const endPos = getSlotPosition(targetNode, link.target_slot, true) - - // Get directions - const startDir = sourceSlot.dir || LinkDirection.RIGHT - const endDir = targetSlot.dir || LinkDirection.LEFT - - // Get reroutes for this link - const reroutes = LLink.getReroutes(graph, link) - - // Build render context - const context = buildLinkRenderContext() - - if (reroutes.length > 0) { - // Render segmented link with reroutes - let segmentStartPos = startPos - let segmentStartDir = startDir - - for (let i = 0; i < reroutes.length; i++) { - const reroute = reroutes[i] - - // Calculate reroute angle - reroute.calculateAngle(Date.now(), graph, [ - segmentStartPos[0], - segmentStartPos[1] - ]) - - // Calculate control points - const distance = Math.sqrt( - (reroute.pos[0] - segmentStartPos[0]) ** 2 + - (reroute.pos[1] - segmentStartPos[1]) ** 2 - ) - const dist = Math.min(Reroute.maxSplineOffset, distance * 0.25) - - // Special handling for floating input chain - const isFloatingInputChain = !sourceNode && targetNode - const startControl: Readonly = isFloatingInputChain - ? [0, 0] - : [dist * reroute.cos, dist * reroute.sin] - - // Render segment to this reroute - adapter.renderLinkDirect( - canvas.ctx, - segmentStartPos, - reroute.pos, - link, - true, // skip_border - 0, // flow - null, // color - segmentStartDir, - LinkDirection.CENTER, - context, - { - startControl, - endControl: reroute.controlPoint, - reroute, - disabled: false - } - ) - - // Prepare for next segment - segmentStartPos = reroute.pos - segmentStartDir = LinkDirection.CENTER - } - - // Render final segment from last reroute to target - const lastReroute = reroutes[reroutes.length - 1] - const finalDistance = Math.sqrt( - (endPos[0] - lastReroute.pos[0]) ** 2 + - (endPos[1] - lastReroute.pos[1]) ** 2 - ) - const finalDist = Math.min(Reroute.maxSplineOffset, finalDistance * 0.25) - const finalStartControl: Readonly = [ - finalDist * lastReroute.cos, - finalDist * lastReroute.sin - ] - - adapter.renderLinkDirect( - canvas.ctx, - lastReroute.pos, - endPos, - link, - true, // skip_border - 0, // flow - null, // color - LinkDirection.CENTER, - endDir, - context, - { - startControl: finalStartControl, - disabled: false - } - ) - } else { - // No reroutes - render direct link - adapter.renderLinkDirect( - canvas.ctx, - startPos, - endPos, - link, - true, // skip_border - 0, // flow - null, // color - startDir, - endDir, - context, - { - disabled: false - } - ) - } - } - - /** - * Recompute all links connected to a node - */ - function recomputeLinksForNode(nodeId: number): void { - const graph = toValue(graphRef) - if (!graph) return - - const node = graph.getNodeById(nodeId) - if (!node) return - - const linkIds = new Set() - - // Collect output links - if (node.outputs) { - for (const output of node.outputs) { - if (output.links) { - for (const linkId of output.links) { - linkIds.add(linkId) - } - } - } - } - - // Collect input links - if (node.inputs) { - for (const input of node.inputs) { - if (input.link !== null && input.link !== undefined) { - linkIds.add(input.link) - } - } - } - - // Recompute each link - for (const linkId of linkIds) { - recomputeLinkById(linkId) - } - } - - /** - * Recompute all links associated with a reroute - */ - function recomputeLinksForReroute(rerouteId: number): void { - const graph = toValue(graphRef) - if (!graph) return - - const reroute = graph.reroutes.get(rerouteId) - if (!reroute) return - - // Recompute all links that pass through this reroute - for (const linkId of reroute.linkIds) { - recomputeLinkById(linkId) - } - } - - /** - * Start link layout sync with event-driven functionality - */ - function start(canvasInstance: LGraphCanvas): void { - canvasRef.value = canvasInstance - if (!canvasInstance.graph) return - - // Initial computation for all existing links - for (const link of canvasInstance.graph._links.values()) { - if (link.id !== -1) { - recomputeLinkById(link.id) - } - } - - // Subscribe to layout store changes - unsubscribeLayoutChange.value?.() - unsubscribeLayoutChange.value = layoutStore.onChange( - (change: LayoutChange) => { - switch (change.operation.type) { - case 'moveNode': - case 'resizeNode': - recomputeLinksForNode(parseInt(change.operation.nodeId)) - break - case 'batchUpdateBounds': - for (const nodeId of change.operation.nodeIds) { - recomputeLinksForNode(parseInt(nodeId)) - } - break - case 'createLink': - recomputeLinkById(change.operation.linkId) - break - case 'deleteLink': - // No-op - store already cleaned by existing code - break - case 'createReroute': - case 'deleteReroute': - // Recompute all affected links - if ('linkIds' in change.operation) { - for (const linkId of change.operation.linkIds) { - recomputeLinkById(linkId) - } - } - break - case 'moveReroute': - recomputeLinksForReroute(change.operation.rerouteId) - break - } - } - ) - } - - function stop(): void { - unsubscribeLayoutChange.value?.() - unsubscribeLayoutChange.value = undefined - canvasRef.value = undefined - } - - tryOnScopeDispose(stop) - - return { - start, - stop - } -} diff --git a/src/renderer/core/layout/sync/useSlotLayoutSync.ts b/src/renderer/core/layout/sync/useSlotLayoutSync.ts deleted file mode 100644 index 6d7812dd7..000000000 --- a/src/renderer/core/layout/sync/useSlotLayoutSync.ts +++ /dev/null @@ -1,150 +0,0 @@ -import { tryOnScopeDispose } from '@vueuse/core' -import { ref } from 'vue' - -import type { LGraphCanvas } from '@/lib/litegraph/src/LGraphCanvas' -import type { LGraphNode } from '@/lib/litegraph/src/litegraph' -import { LiteGraph } from '@/lib/litegraph/src/litegraph' -import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotCalculations' -import { registerNodeSlots } from '@/renderer/core/layout/slots/register' -import { layoutStore } from '@/renderer/core/layout/store/layoutStore' - -function computeAndRegisterSlots(node: LGraphNode): void { - const nodeId = String(node.id) - const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value - - // Fallback to live node values if layout not ready - const nodeX = nodeLayout?.position.x ?? node.pos[0] - const nodeY = nodeLayout?.position.y ?? node.pos[1] - const nodeWidth = nodeLayout?.size.width ?? node.size[0] - const nodeHeight = nodeLayout?.size.height ?? node.size[1] - - // Ensure concrete slots & arrange when needed for accurate positions - node._setConcreteSlots() - const collapsed = node.flags.collapsed ?? false - if (!collapsed) { - node.arrange() - } - - const context: SlotPositionContext = { - nodeX, - nodeY, - nodeWidth, - nodeHeight, - collapsed, - collapsedWidth: node._collapsed_width, - slotStartY: node.constructor.slot_start_y, - inputs: node.inputs, - outputs: node.outputs, - widgets: node.widgets - } - - registerNodeSlots(nodeId, context) -} - -export function useSlotLayoutSync() { - const unsubscribeLayoutChange = ref<() => void>() - const restoreHandlers = ref<() => void>() - - /** - * Attempt to start slot layout sync with full event-driven functionality - * @param canvas LiteGraph canvas instance - * @returns true if sync was actually started, false if early-returned - */ - function attemptStart(canvas: LGraphCanvas): boolean { - // When Vue nodes are enabled, slot DOM registers exact positions. - // Skip calculated registration to avoid conflicts. - if (LiteGraph.vueNodesMode) { - return false - } - const graph = canvas?.graph - if (!graph) return false - - // Initial registration for all nodes in the current graph - for (const node of graph.nodes) { - computeAndRegisterSlots(node) - } - - // Layout changes → recompute slots for changed nodes - unsubscribeLayoutChange.value?.() - unsubscribeLayoutChange.value = layoutStore.onChange((change) => { - for (const nodeId of change.nodeIds) { - const node = graph.getNodeById(parseInt(nodeId)) - if (node) { - computeAndRegisterSlots(node) - } - } - }) - - // LiteGraph event hooks - const origNodeAdded = graph.onNodeAdded - const origNodeRemoved = graph.onNodeRemoved - const origTrigger = graph.onTrigger - const origAfterChange = graph.onAfterChange - - graph.onNodeAdded = (node: LGraphNode) => { - computeAndRegisterSlots(node) - if (origNodeAdded) { - origNodeAdded.call(graph, node) - } - } - - graph.onNodeRemoved = (node: LGraphNode) => { - layoutStore.deleteNodeSlotLayouts(String(node.id)) - if (origNodeRemoved) { - origNodeRemoved.call(graph, node) - } - } - - graph.onTrigger = (event) => { - if ( - event.type === 'node:property:changed' && - event.property === 'flags.collapsed' - ) { - const node = graph.getNodeById(parseInt(String(event.nodeId))) - if (node) { - computeAndRegisterSlots(node) - } - } - - // Chain to original handler - origTrigger?.(event) - } - - graph.onAfterChange = (graph: any, node?: any) => { - if (node && node.id) { - computeAndRegisterSlots(node) - } - if (origAfterChange) { - origAfterChange.call(graph, graph, node) - } - } - - // Store cleanup function - restoreHandlers.value = () => { - graph.onNodeAdded = origNodeAdded || undefined - graph.onNodeRemoved = origNodeRemoved || undefined - // Only restore onTrigger if Vue nodes are not active - // Vue node manager sets its own onTrigger handler - if (!LiteGraph.vueNodesMode) { - graph.onTrigger = origTrigger || undefined - } - graph.onAfterChange = origAfterChange || undefined - } - - return true - } - - function stop(): void { - unsubscribeLayoutChange.value?.() - unsubscribeLayoutChange.value = undefined - restoreHandlers.value?.() - restoreHandlers.value = undefined - } - - tryOnScopeDispose(stop) - - return { - attemptStart, - stop - } -} diff --git a/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap b/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap index 18cc5c713..1a92319bb 100644 --- a/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap +++ b/tests-ui/tests/litegraph/core/__snapshots__/LGraph.test.ts.snap @@ -274,6 +274,7 @@ LGraph { "filter": undefined, "fixedtime": 0, "fixedtime_lapse": 0.01, + "floatingLinksInternal": Map {}, "globaltime": 0, "id": "b4e984f1-b421-4d24-b8b4-ff895793af13", "iteration": 0, @@ -284,6 +285,7 @@ LGraph { "nodes_executedAction": [], "nodes_executing": [], "onTrigger": undefined, + "reroutesInternal": Map {}, "revision": 0, "runningtime": 0, "starttime": 0,