mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-01-26 10:59:53 +00:00
Fix: Vue-Litegraph conversion bug with Reroutes (#6330)
## Summary The private fields triggered an error in intitializing the linkLayoutSync. Turns out that wasn't necessary anymore. > [!NOTE] > Edit: Doing some more investigation, it looks like the slot sync can also be removed? ## Changes - **What**: Converts JS private fields to typescript private, adds some readonly declarations - **What**: Removes the useLinkLayoutSync usage in useVueNodeLifecycle - **What**: Removes the useSlotLayoutSync usage in useVueNodeLifecycle ## Review Focus Was the sync doing something that wouldn't be caught in normal usage/testing? ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-6330-Fix-Vue-Litegraph-conversion-bug-with-Reroutes-2996d73d3650819ebb24e4aa2fc51c65) by [Unito](https://www.unito.io)
This commit is contained in:
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -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
|
||||
|
||||
@@ -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}']
|
||||
|
||||
@@ -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<GraphNodeManager | null>(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 {
|
||||
|
||||
@@ -223,15 +223,15 @@ export class LGraph
|
||||
/** Internal only. Not required for serialisation; calculated on deserialise. */
|
||||
#lastFloatingLinkId: number = 0
|
||||
|
||||
#floatingLinks: Map<LinkId, LLink> = new Map()
|
||||
private readonly floatingLinksInternal: Map<LinkId, LLink> = new Map()
|
||||
get floatingLinks(): ReadonlyMap<LinkId, LLink> {
|
||||
return this.#floatingLinks
|
||||
return this.floatingLinksInternal
|
||||
}
|
||||
|
||||
#reroutes = new Map<RerouteId, Reroute>()
|
||||
private readonly reroutesInternal = new Map<RerouteId, Reroute>()
|
||||
/** All reroutes in this graph. */
|
||||
public get reroutes(): Map<RerouteId, Reroute> {
|
||||
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
|
||||
|
||||
@@ -51,31 +51,31 @@ export class Reroute
|
||||
}
|
||||
|
||||
/** The network this reroute belongs to. Contains all valid links and reroutes. */
|
||||
#network: WeakRef<LinkNetwork>
|
||||
private readonly network: WeakRef<LinkNetwork>
|
||||
|
||||
#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<LinkId>,
|
||||
floatingLinkIds?: Iterable<LinkId>
|
||||
) {
|
||||
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>()): 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>()
|
||||
): 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
|
||||
|
||||
|
||||
@@ -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)
|
||||
})
|
||||
}
|
||||
@@ -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<LGraphCanvas>()
|
||||
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<Point> = 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<Point> = [
|
||||
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<number>()
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user