mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11: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
|
*.json text eol=lf
|
||||||
*.mjs text eol=lf
|
*.mjs text eol=lf
|
||||||
*.mts text eol=lf
|
*.mts text eol=lf
|
||||||
|
*.snap text eol=lf
|
||||||
*.ts text eol=lf
|
*.ts text eol=lf
|
||||||
*.vue text eol=lf
|
*.vue text eol=lf
|
||||||
*.yaml text eol=lf
|
*.yaml text eol=lf
|
||||||
|
|||||||
@@ -14,7 +14,7 @@ const config: KnipConfig = {
|
|||||||
},
|
},
|
||||||
'apps/desktop-ui': {
|
'apps/desktop-ui': {
|
||||||
entry: ['src/main.ts', 'src/i18n.ts'],
|
entry: ['src/main.ts', 'src/i18n.ts'],
|
||||||
project: ['src/**/*.{js,ts,vue}', '*.{js,ts,mts}']
|
project: ['src/**/*.{js,ts,vue}']
|
||||||
},
|
},
|
||||||
'packages/tailwind-utils': {
|
'packages/tailwind-utils': {
|
||||||
project: ['src/**/*.{js,ts}']
|
project: ['src/**/*.{js,ts}']
|
||||||
|
|||||||
@@ -4,13 +4,11 @@ import { shallowRef, watch } from 'vue'
|
|||||||
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
import { useGraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||||
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
import type { GraphNodeManager } from '@/composables/graph/useGraphNodeManager'
|
||||||
import { useVueFeatureFlags } from '@/composables/useVueFeatureFlags'
|
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 { useCanvasStore } from '@/renderer/core/canvas/canvasStore'
|
||||||
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations'
|
||||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||||
import { useLayoutSync } from '@/renderer/core/layout/sync/useLayoutSync'
|
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'
|
import { app as comfyApp } from '@/scripts/app'
|
||||||
|
|
||||||
function useVueNodeLifecycleIndividual() {
|
function useVueNodeLifecycleIndividual() {
|
||||||
@@ -21,8 +19,6 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
const nodeManager = shallowRef<GraphNodeManager | null>(null)
|
||||||
|
|
||||||
const { startSync } = useLayoutSync()
|
const { startSync } = useLayoutSync()
|
||||||
const linkSyncManager = useLinkLayoutSync()
|
|
||||||
const slotSyncManager = useSlotLayoutSync()
|
|
||||||
|
|
||||||
const initializeNodeManager = () => {
|
const initializeNodeManager = () => {
|
||||||
// Use canvas graph if available (handles subgraph contexts), fallback to app graph
|
// 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)
|
// Initialize layout sync (one-way: Layout Store → LiteGraph)
|
||||||
startSync(canvasStore.canvas)
|
startSync(canvasStore.canvas)
|
||||||
|
|
||||||
if (comfyApp.canvas) {
|
|
||||||
linkSyncManager.start(comfyApp.canvas)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const disposeNodeManagerAndSyncs = () => {
|
const disposeNodeManagerAndSyncs = () => {
|
||||||
@@ -77,8 +69,6 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
/* empty */
|
/* empty */
|
||||||
}
|
}
|
||||||
nodeManager.value = null
|
nodeManager.value = null
|
||||||
|
|
||||||
linkSyncManager.stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Watch for Vue nodes enabled state changes
|
// Watch for Vue nodes enabled state changes
|
||||||
@@ -96,25 +86,14 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
|
|
||||||
// Consolidated watch for slot layout sync management
|
// Consolidated watch for slot layout sync management
|
||||||
watch(
|
watch(
|
||||||
[() => canvasStore.canvas, () => shouldRenderVueNodes.value],
|
() => shouldRenderVueNodes.value,
|
||||||
([canvas, vueMode], [, oldVueMode]) => {
|
(vueMode, oldVueMode) => {
|
||||||
const modeChanged = vueMode !== oldVueMode
|
const modeChanged = vueMode !== oldVueMode
|
||||||
|
|
||||||
// Clear stale slot layouts when switching modes
|
// Clear stale slot layouts when switching modes
|
||||||
if (modeChanged) {
|
if (modeChanged) {
|
||||||
layoutStore.clearAllSlotLayouts()
|
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' }
|
{ immediate: true, flush: 'sync' }
|
||||||
)
|
)
|
||||||
@@ -152,8 +131,6 @@ function useVueNodeLifecycleIndividual() {
|
|||||||
nodeManager.value.cleanup()
|
nodeManager.value.cleanup()
|
||||||
nodeManager.value = null
|
nodeManager.value = null
|
||||||
}
|
}
|
||||||
slotSyncManager.stop()
|
|
||||||
linkSyncManager.stop()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
@@ -223,15 +223,15 @@ export class LGraph
|
|||||||
/** Internal only. Not required for serialisation; calculated on deserialise. */
|
/** Internal only. Not required for serialisation; calculated on deserialise. */
|
||||||
#lastFloatingLinkId: number = 0
|
#lastFloatingLinkId: number = 0
|
||||||
|
|
||||||
#floatingLinks: Map<LinkId, LLink> = new Map()
|
private readonly floatingLinksInternal: Map<LinkId, LLink> = new Map()
|
||||||
get floatingLinks(): ReadonlyMap<LinkId, LLink> {
|
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. */
|
/** All reroutes in this graph. */
|
||||||
public get reroutes(): Map<RerouteId, Reroute> {
|
public get reroutes(): Map<RerouteId, Reroute> {
|
||||||
return this.#reroutes
|
return this.reroutesInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
get rootGraph(): LGraph {
|
get rootGraph(): LGraph {
|
||||||
@@ -340,7 +340,7 @@ export class LGraph
|
|||||||
|
|
||||||
this._links.clear()
|
this._links.clear()
|
||||||
this.reroutes.clear()
|
this.reroutes.clear()
|
||||||
this.#floatingLinks.clear()
|
this.floatingLinksInternal.clear()
|
||||||
|
|
||||||
this.#lastFloatingLinkId = 0
|
this.#lastFloatingLinkId = 0
|
||||||
|
|
||||||
@@ -1268,7 +1268,7 @@ export class LGraph
|
|||||||
if (link.id === -1) {
|
if (link.id === -1) {
|
||||||
link.id = ++this.#lastFloatingLinkId
|
link.id = ++this.#lastFloatingLinkId
|
||||||
}
|
}
|
||||||
this.#floatingLinks.set(link.id, link)
|
this.floatingLinksInternal.set(link.id, link)
|
||||||
|
|
||||||
const slot =
|
const slot =
|
||||||
link.target_id !== -1
|
link.target_id !== -1
|
||||||
@@ -1291,7 +1291,7 @@ export class LGraph
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeFloatingLink(link: LLink): void {
|
removeFloatingLink(link: LLink): void {
|
||||||
this.#floatingLinks.delete(link.id)
|
this.floatingLinksInternal.delete(link.id)
|
||||||
|
|
||||||
const slot =
|
const slot =
|
||||||
link.target_id !== -1
|
link.target_id !== -1
|
||||||
|
|||||||
@@ -51,31 +51,31 @@ export class Reroute
|
|||||||
}
|
}
|
||||||
|
|
||||||
/** The network this reroute belongs to. Contains all valid links and reroutes. */
|
/** 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 {
|
public get parentId(): RerouteId | undefined {
|
||||||
return this.#parentId
|
return this.parentIdInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Ignores attempts to create an infinite loop. @inheritdoc */
|
/** Ignores attempts to create an infinite loop. @inheritdoc */
|
||||||
public set parentId(value) {
|
public set parentId(value) {
|
||||||
if (value === this.id) return
|
if (value === this.id) return
|
||||||
if (this.getReroutes() === null) return
|
if (this.getReroutes() === null) return
|
||||||
this.#parentId = value
|
this.parentIdInternal = value
|
||||||
}
|
}
|
||||||
|
|
||||||
public get parent(): Reroute | undefined {
|
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). */
|
/** This property is only defined on the last reroute of a floating reroute chain (closest to input end). */
|
||||||
floating?: FloatingRerouteSlot
|
floating?: FloatingRerouteSlot
|
||||||
|
|
||||||
#pos: Point = [0, 0]
|
private readonly posInternal: Point = [0, 0]
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
get pos(): Point {
|
get pos(): Point {
|
||||||
return this.#pos
|
return this.posInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
set pos(value: Point) {
|
set pos(value: Point) {
|
||||||
@@ -83,14 +83,14 @@ export class Reroute
|
|||||||
throw new TypeError(
|
throw new TypeError(
|
||||||
'Reroute.pos is an x,y point, and expects an indexable with at least two values.'
|
'Reroute.pos is an x,y point, and expects an indexable with at least two values.'
|
||||||
)
|
)
|
||||||
this.#pos[0] = value[0]
|
this.posInternal[0] = value[0]
|
||||||
this.#pos[1] = value[1]
|
this.posInternal[1] = value[1]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
get boundingRect(): ReadOnlyRect {
|
get boundingRect(): ReadOnlyRect {
|
||||||
const { radius } = Reroute
|
const { radius } = Reroute
|
||||||
const [x, y] = this.#pos
|
const [x, y] = this.posInternal
|
||||||
return [x - radius, y - radius, 2 * radius, 2 * radius]
|
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.
|
* Slightly over-sized rectangle, guaranteed to contain the entire surface area for hover detection.
|
||||||
* Eliminates most hover positions using an extremely cheap check.
|
* Eliminates most hover positions using an extremely cheap check.
|
||||||
*/
|
*/
|
||||||
get #hoverArea(): ReadOnlyRect {
|
private get hoverArea(): ReadOnlyRect {
|
||||||
const xOffset = 2 * Reroute.slotOffset
|
const xOffset = 2 * Reroute.slotOffset
|
||||||
const yOffset = 2 * Math.max(Reroute.radius, Reroute.slotRadius)
|
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]
|
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.
|
* Used to ensure reroute angles are only executed once per frame.
|
||||||
* @todo Calculate on change instead.
|
* @todo Calculate on change instead.
|
||||||
*/
|
*/
|
||||||
#lastRenderTime: number = -Infinity
|
private lastRenderTime: number = -Infinity
|
||||||
|
|
||||||
#inputSlot = new RerouteSlot(this, true)
|
private readonly inputSlot = new RerouteSlot(this, true)
|
||||||
#outputSlot = new RerouteSlot(this, false)
|
private readonly outputSlot = new RerouteSlot(this, false)
|
||||||
|
|
||||||
get isSlotHovered(): boolean {
|
get isSlotHovered(): boolean {
|
||||||
return this.isInputHovered || this.isOutputHovered
|
return this.isInputHovered || this.isOutputHovered
|
||||||
}
|
}
|
||||||
|
|
||||||
get isInputHovered(): boolean {
|
get isInputHovered(): boolean {
|
||||||
return this.#inputSlot.hovering
|
return this.inputSlot.hovering
|
||||||
}
|
}
|
||||||
|
|
||||||
get isOutputHovered(): boolean {
|
get isOutputHovered(): boolean {
|
||||||
return this.#outputSlot.hovering
|
return this.outputSlot.hovering
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstLink(): LLink | undefined {
|
get firstLink(): LLink | undefined {
|
||||||
const linkId = this.linkIds.values().next().value
|
const linkId = this.linkIds.values().next().value
|
||||||
return linkId === undefined
|
return linkId === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: this.#network.deref()?.links.get(linkId)
|
: this.network.deref()?.links.get(linkId)
|
||||||
}
|
}
|
||||||
|
|
||||||
get firstFloatingLink(): LLink | undefined {
|
get firstFloatingLink(): LLink | undefined {
|
||||||
const linkId = this.floatingLinkIds.values().next().value
|
const linkId = this.floatingLinkIds.values().next().value
|
||||||
return linkId === undefined
|
return linkId === undefined
|
||||||
? undefined
|
? undefined
|
||||||
: this.#network.deref()?.floatingLinks.get(linkId)
|
: this.network.deref()?.floatingLinks.get(linkId)
|
||||||
}
|
}
|
||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
@@ -205,7 +205,7 @@ export class Reroute
|
|||||||
linkIds?: Iterable<LinkId>,
|
linkIds?: Iterable<LinkId>,
|
||||||
floatingLinkIds?: Iterable<LinkId>
|
floatingLinkIds?: Iterable<LinkId>
|
||||||
) {
|
) {
|
||||||
this.#network = new WeakRef(network)
|
this.network = new WeakRef(network)
|
||||||
this.parentId = parentId
|
this.parentId = parentId
|
||||||
if (pos) this.pos = pos
|
if (pos) this.pos = pos
|
||||||
this.linkIds = new Set(linkIds)
|
this.linkIds = new Set(linkIds)
|
||||||
@@ -261,15 +261,15 @@ export class Reroute
|
|||||||
*/
|
*/
|
||||||
getReroutes(visited = new Set<Reroute>()): Reroute[] | null {
|
getReroutes(visited = new Set<Reroute>()): Reroute[] | null {
|
||||||
// No parentId - last in the chain
|
// No parentId - last in the chain
|
||||||
if (this.#parentId === undefined) return [this]
|
if (this.parentIdInternal === undefined) return [this]
|
||||||
// Invalid chain - looped
|
// Invalid chain - looped
|
||||||
if (visited.has(this)) return null
|
if (visited.has(this)) return null
|
||||||
visited.add(this)
|
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
|
// Invalid parent (or network) - drop silently to recover
|
||||||
if (!parent) {
|
if (!parent) {
|
||||||
this.#parentId = undefined
|
this.parentIdInternal = undefined
|
||||||
return [this]
|
return [this]
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -288,14 +288,14 @@ export class Reroute
|
|||||||
withParentId: RerouteId,
|
withParentId: RerouteId,
|
||||||
visited = new Set<Reroute>()
|
visited = new Set<Reroute>()
|
||||||
): Reroute | null | undefined {
|
): Reroute | null | undefined {
|
||||||
if (this.#parentId === withParentId) return this
|
if (this.parentIdInternal === withParentId) return this
|
||||||
if (visited.has(this)) return null
|
if (visited.has(this)) return null
|
||||||
visited.add(this)
|
visited.add(this)
|
||||||
if (this.#parentId === undefined) return
|
if (this.parentIdInternal === undefined) return
|
||||||
|
|
||||||
return this.#network
|
return this.network
|
||||||
.deref()
|
.deref()
|
||||||
?.reroutes.get(this.#parentId)
|
?.reroutes.get(this.parentIdInternal)
|
||||||
?.findNextReroute(withParentId, visited)
|
?.findNextReroute(withParentId, visited)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -309,7 +309,7 @@ export class Reroute
|
|||||||
const link = this.firstLink ?? this.firstFloatingLink
|
const link = this.firstLink ?? this.firstFloatingLink
|
||||||
if (!link) return
|
if (!link) return
|
||||||
|
|
||||||
const node = this.#network.deref()?.getNodeById(link.origin_id)
|
const node = this.network.deref()?.getNodeById(link.origin_id)
|
||||||
if (!node) return
|
if (!node) return
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@@ -325,7 +325,7 @@ export class Reroute
|
|||||||
findTargetInputs():
|
findTargetInputs():
|
||||||
| { node: LGraphNode; input: INodeInputSlot; link: LLink }[]
|
| { node: LGraphNode; input: INodeInputSlot; link: LLink }[]
|
||||||
| undefined {
|
| undefined {
|
||||||
const network = this.#network.deref()
|
const network = this.network.deref()
|
||||||
if (!network) return
|
if (!network) return
|
||||||
|
|
||||||
const results: {
|
const results: {
|
||||||
@@ -363,7 +363,7 @@ export class Reroute
|
|||||||
* @returns An array of floating links
|
* @returns An array of floating links
|
||||||
*/
|
*/
|
||||||
getFloatingLinks(from: 'input' | 'output'): LLink[] | undefined {
|
getFloatingLinks(from: 'input' | 'output'): LLink[] | undefined {
|
||||||
const floatingLinks = this.#network.deref()?.floatingLinks
|
const floatingLinks = this.network.deref()?.floatingLinks
|
||||||
if (!floatingLinks) return
|
if (!floatingLinks) return
|
||||||
|
|
||||||
const idProp = from === 'input' ? 'origin_id' : 'target_id'
|
const idProp = from === 'input' ? 'origin_id' : 'target_id'
|
||||||
@@ -387,7 +387,7 @@ export class Reroute
|
|||||||
output: INodeOutputSlot,
|
output: INodeOutputSlot,
|
||||||
index: number
|
index: number
|
||||||
) {
|
) {
|
||||||
const network = this.#network.deref()
|
const network = this.network.deref()
|
||||||
const floatingOutLinks = this.getFloatingLinks('output')
|
const floatingOutLinks = this.getFloatingLinks('output')
|
||||||
if (!floatingOutLinks)
|
if (!floatingOutLinks)
|
||||||
throw new Error('[setFloatingLinkOrigin]: Invalid network.')
|
throw new Error('[setFloatingLinkOrigin]: Invalid network.')
|
||||||
@@ -411,15 +411,15 @@ export class Reroute
|
|||||||
|
|
||||||
/** @inheritdoc */
|
/** @inheritdoc */
|
||||||
move(deltaX: number, deltaY: number) {
|
move(deltaX: number, deltaY: number) {
|
||||||
const previousPos = { x: this.#pos[0], y: this.#pos[1] }
|
const previousPos = { x: this.posInternal[0], y: this.posInternal[1] }
|
||||||
this.#pos[0] += deltaX
|
this.posInternal[0] += deltaX
|
||||||
this.#pos[1] += deltaY
|
this.posInternal[1] += deltaY
|
||||||
|
|
||||||
// Update Layout Store with new position
|
// Update Layout Store with new position
|
||||||
layoutMutations.setSource(LayoutSource.Canvas)
|
layoutMutations.setSource(LayoutSource.Canvas)
|
||||||
layoutMutations.moveReroute(
|
layoutMutations.moveReroute(
|
||||||
this.id,
|
this.id,
|
||||||
{ x: this.#pos[0], y: this.#pos[1] },
|
{ x: this.posInternal[0], y: this.posInternal[1] },
|
||||||
previousPos
|
previousPos
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
@@ -441,7 +441,7 @@ export class Reroute
|
|||||||
}
|
}
|
||||||
|
|
||||||
removeFloatingLink(linkId: LinkId) {
|
removeFloatingLink(linkId: LinkId) {
|
||||||
const network = this.#network.deref()
|
const network = this.network.deref()
|
||||||
if (!network) return
|
if (!network) return
|
||||||
|
|
||||||
const floatingLink = network.floatingLinks.get(linkId)
|
const floatingLink = network.floatingLinks.get(linkId)
|
||||||
@@ -462,7 +462,7 @@ export class Reroute
|
|||||||
* @remarks Does not remove the link from the network.
|
* @remarks Does not remove the link from the network.
|
||||||
*/
|
*/
|
||||||
removeLink(link: LLink) {
|
removeLink(link: LLink) {
|
||||||
const network = this.#network.deref()
|
const network = this.network.deref()
|
||||||
if (!network) return
|
if (!network) return
|
||||||
|
|
||||||
const floatingLink = network.floatingLinks.get(link.id)
|
const floatingLink = network.floatingLinks.get(link.id)
|
||||||
@@ -474,7 +474,7 @@ export class Reroute
|
|||||||
}
|
}
|
||||||
|
|
||||||
remove() {
|
remove() {
|
||||||
const network = this.#network.deref()
|
const network = this.network.deref()
|
||||||
if (!network) return
|
if (!network) return
|
||||||
|
|
||||||
network.removeReroute(this.id)
|
network.removeReroute(this.id)
|
||||||
@@ -486,8 +486,8 @@ export class Reroute
|
|||||||
linkStart: Point
|
linkStart: Point
|
||||||
): void {
|
): void {
|
||||||
// Ensure we run once per render
|
// Ensure we run once per render
|
||||||
if (!(lastRenderTime > this.#lastRenderTime)) return
|
if (!(lastRenderTime > this.lastRenderTime)) return
|
||||||
this.#lastRenderTime = lastRenderTime
|
this.lastRenderTime = lastRenderTime
|
||||||
|
|
||||||
const { id, pos: thisPos } = this
|
const { id, pos: thisPos } = this
|
||||||
|
|
||||||
@@ -509,14 +509,14 @@ export class Reroute
|
|||||||
sum /= angles.length
|
sum /= angles.length
|
||||||
|
|
||||||
const originToReroute = Math.atan2(
|
const originToReroute = Math.atan2(
|
||||||
this.#pos[1] - linkStart[1],
|
this.posInternal[1] - linkStart[1],
|
||||||
this.#pos[0] - linkStart[0]
|
this.posInternal[0] - linkStart[0]
|
||||||
)
|
)
|
||||||
let diff = (originToReroute - sum) * 0.5
|
let diff = (originToReroute - sum) * 0.5
|
||||||
if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI
|
if (Math.abs(diff) > Math.PI * 0.5) diff += Math.PI
|
||||||
const dist = Math.min(
|
const dist = Math.min(
|
||||||
Reroute.maxSplineOffset,
|
Reroute.maxSplineOffset,
|
||||||
distance(linkStart, this.#pos) * 0.25
|
distance(linkStart, this.posInternal) * 0.25
|
||||||
)
|
)
|
||||||
|
|
||||||
// Store results
|
// Store results
|
||||||
@@ -604,8 +604,8 @@ export class Reroute
|
|||||||
* @param ctx The canvas context to draw on.
|
* @param ctx The canvas context to draw on.
|
||||||
*/
|
*/
|
||||||
drawSlots(ctx: CanvasRenderingContext2D): void {
|
drawSlots(ctx: CanvasRenderingContext2D): void {
|
||||||
this.#inputSlot.draw(ctx)
|
this.inputSlot.draw(ctx)
|
||||||
this.#outputSlot.draw(ctx)
|
this.outputSlot.draw(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
drawHighlight(ctx: CanvasRenderingContext2D, colour: CanvasColour): void {
|
drawHighlight(ctx: CanvasRenderingContext2D, colour: CanvasColour): void {
|
||||||
@@ -629,8 +629,8 @@ export class Reroute
|
|||||||
* @returns `true` if any changes require a redraw.
|
* @returns `true` if any changes require a redraw.
|
||||||
*/
|
*/
|
||||||
updateVisibility(pos: Point): boolean {
|
updateVisibility(pos: Point): boolean {
|
||||||
const input = this.#inputSlot
|
const input = this.inputSlot
|
||||||
const output = this.#outputSlot
|
const output = this.outputSlot
|
||||||
input.dirty = false
|
input.dirty = false
|
||||||
output.dirty = false
|
output.dirty = false
|
||||||
|
|
||||||
@@ -642,8 +642,8 @@ export class Reroute
|
|||||||
const showEither = showInput || showOutput
|
const showEither = showInput || showOutput
|
||||||
|
|
||||||
// Check if even in the vicinity
|
// Check if even in the vicinity
|
||||||
if (showEither && isPointInRect(pos, this.#hoverArea)) {
|
if (showEither && isPointInRect(pos, this.hoverArea)) {
|
||||||
const outlineOnly = this.#contains(pos)
|
const outlineOnly = this.contains(pos)
|
||||||
|
|
||||||
if (showInput) input.update(pos, outlineOnly)
|
if (showInput) input.update(pos, outlineOnly)
|
||||||
if (showOutput) output.update(pos, outlineOnly)
|
if (showOutput) output.update(pos, outlineOnly)
|
||||||
@@ -656,8 +656,8 @@ export class Reroute
|
|||||||
|
|
||||||
/** Prevents rendering of the input and output slots. */
|
/** Prevents rendering of the input and output slots. */
|
||||||
hideSlots() {
|
hideSlots() {
|
||||||
this.#inputSlot.hide()
|
this.inputSlot.hide()
|
||||||
this.#outputSlot.hide()
|
this.outputSlot.hide()
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -666,10 +666,10 @@ export class Reroute
|
|||||||
* @returns `true` if {@link pos} is within the reroute's radius.
|
* @returns `true` if {@link pos} is within the reroute's radius.
|
||||||
*/
|
*/
|
||||||
containsPoint(pos: Point): boolean {
|
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
|
return distance(this.pos, pos) <= Reroute.radius
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -692,47 +692,47 @@ export class Reroute
|
|||||||
*/
|
*/
|
||||||
class RerouteSlot {
|
class RerouteSlot {
|
||||||
/** The reroute that the slot belongs to. */
|
/** 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. */
|
/** Centre point of this slot. */
|
||||||
get pos(): Point {
|
get pos(): Point {
|
||||||
const [x, y] = this.#reroute.pos
|
const [x, y] = this.reroute.pos
|
||||||
return [x + Reroute.slotOffset * this.#offsetMultiplier, y]
|
return [x + Reroute.slotOffset * this.offsetMultiplier, y]
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Whether any changes require a redraw. */
|
/** Whether any changes require a redraw. */
|
||||||
dirty: boolean = false
|
dirty: boolean = false
|
||||||
|
|
||||||
#hovering = false
|
private hoveringInternal = false
|
||||||
/** Whether the pointer is hovering over the slot itself. */
|
/** Whether the pointer is hovering over the slot itself. */
|
||||||
get hovering() {
|
get hovering() {
|
||||||
return this.#hovering
|
return this.hoveringInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
set hovering(value) {
|
set hovering(value) {
|
||||||
if (!Object.is(this.#hovering, value)) {
|
if (!Object.is(this.hoveringInternal, value)) {
|
||||||
this.#hovering = value
|
this.hoveringInternal = value
|
||||||
this.dirty = true
|
this.dirty = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#showOutline = false
|
private showOutlineInternal = false
|
||||||
/** Whether the slot outline / faint background is visible. */
|
/** Whether the slot outline / faint background is visible. */
|
||||||
get showOutline() {
|
get showOutline() {
|
||||||
return this.#showOutline
|
return this.showOutlineInternal
|
||||||
}
|
}
|
||||||
|
|
||||||
set showOutline(value) {
|
set showOutline(value) {
|
||||||
if (!Object.is(this.#showOutline, value)) {
|
if (!Object.is(this.showOutlineInternal, value)) {
|
||||||
this.#showOutline = value
|
this.showOutlineInternal = value
|
||||||
this.dirty = true
|
this.dirty = true
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
constructor(reroute: Reroute, isInput: boolean) {
|
constructor(reroute: Reroute, isInput: boolean) {
|
||||||
this.#reroute = reroute
|
this.reroute = reroute
|
||||||
this.#offsetMultiplier = isInput ? -1 : 1
|
this.offsetMultiplier = isInput ? -1 : 1
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -771,7 +771,7 @@ class RerouteSlot {
|
|||||||
if (!showOutline) return
|
if (!showOutline) return
|
||||||
|
|
||||||
try {
|
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.strokeStyle = 'rgb(0,0,0,0.5)'
|
||||||
ctx.lineWidth = 1
|
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,
|
"filter": undefined,
|
||||||
"fixedtime": 0,
|
"fixedtime": 0,
|
||||||
"fixedtime_lapse": 0.01,
|
"fixedtime_lapse": 0.01,
|
||||||
|
"floatingLinksInternal": Map {},
|
||||||
"globaltime": 0,
|
"globaltime": 0,
|
||||||
"id": "b4e984f1-b421-4d24-b8b4-ff895793af13",
|
"id": "b4e984f1-b421-4d24-b8b4-ff895793af13",
|
||||||
"iteration": 0,
|
"iteration": 0,
|
||||||
@@ -284,6 +285,7 @@ LGraph {
|
|||||||
"nodes_executedAction": [],
|
"nodes_executedAction": [],
|
||||||
"nodes_executing": [],
|
"nodes_executing": [],
|
||||||
"onTrigger": undefined,
|
"onTrigger": undefined,
|
||||||
|
"reroutesInternal": Map {},
|
||||||
"revision": 0,
|
"revision": 0,
|
||||||
"runningtime": 0,
|
"runningtime": 0,
|
||||||
"starttime": 0,
|
"starttime": 0,
|
||||||
|
|||||||
Reference in New Issue
Block a user