Skip to content

Commit 8f438d6

Browse files
Fix reroute links not snapping correctly (#5903)
Summary - Route LGraphNode slot position queries through a single source of truth that prefers layoutStore’s DOM‑tracked slot positions and falls back to geometry when unavailable. - Ensures that reroute‑origin drags snap to the same visual slot centers used by Vue nodes when hovering compatible nodes. What changed - LGraphNode.getInputPos/getOutputPos now resolve via getSlotPosition(), which: - Returns the Vue nodes layoutStore position if the slot has been tracked. - Otherwise, computes from node geometry (unchanged fallback behavior). - LGraphNode.getInputSlotPos(input) resolves index→getSlotPosition() and retains a safe fallback when an input slot object doesn’t map to an index. Why - Previously, when starting a drag from a reroute and hovering a node, the temporary link endpoint would render toward LiteGraph’s calculated slot position, not the Vue‑tracked slot position, causing a visible mismatch. - By making all slot position lookups consult the layout store first, node hover snap rendering is consistent across slot‑origin and reroute‑origin drags. Impact - No behavior change for non‑Vue nodes mode or when no tracked layout exists — the legacy calculated positions are still used. - Centralizes slot positioning in one place, reducing special‑case logic and making hover/drag rendering more predictable. #5780 (snapping) <-- #5898 (drop on canvas + linkconnectoradapter refactor) <-- #5903 (fix reroute snapping) ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5903-Use-layoutStore-slot-positions-for-node-slot-queries-fix-reroute-origin-node-snap-2816d73d36508184b297f46b39105545) by [Unito](https://www.unito.io)
1 parent 89a2315 commit 8f438d6

File tree

10 files changed

+128
-22
lines changed

10 files changed

+128
-22
lines changed

src/lib/litegraph/src/LGraphCanvas.ts

Lines changed: 21 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3318,7 +3318,15 @@ export class LGraphCanvas
33183318

33193319
if (slot && linkConnector.isInputValidDrop(node, slot)) {
33203320
highlightInput = slot
3321-
highlightPos = node.getInputSlotPos(slot)
3321+
if (LiteGraph.vueNodesMode) {
3322+
const idx = node.inputs.indexOf(slot)
3323+
highlightPos =
3324+
idx !== -1
3325+
? getSlotPosition(node, idx, true)
3326+
: node.getInputSlotPos(slot)
3327+
} else {
3328+
highlightPos = node.getInputSlotPos(slot)
3329+
}
33223330
linkConnector.overWidget = overWidget
33233331
}
33243332
}
@@ -3330,7 +3338,9 @@ export class LGraphCanvas
33303338
const result = node.findInputByType(firstLink.fromSlot.type)
33313339
if (result) {
33323340
highlightInput = result.slot
3333-
highlightPos = node.getInputSlotPos(result.slot)
3341+
highlightPos = LiteGraph.vueNodesMode
3342+
? getSlotPosition(node, result.index, true)
3343+
: node.getInputSlotPos(result.slot)
33343344
}
33353345
} else if (
33363346
inputId != -1 &&
@@ -3355,7 +3365,9 @@ export class LGraphCanvas
33553365
if (inputId === -1 && outputId === -1) {
33563366
const result = node.findOutputByType(firstLink.fromSlot.type)
33573367
if (result) {
3358-
highlightPos = node.getOutputPos(result.index)
3368+
highlightPos = LiteGraph.vueNodesMode
3369+
? getSlotPosition(node, result.index, false)
3370+
: node.getOutputPos(result.index)
33593371
}
33603372
} else {
33613373
// check if I have a slot below de mouse
@@ -5720,7 +5732,9 @@ export class LGraphCanvas
57205732
if (!node) continue
57215733

57225734
const startPos = firstReroute.pos
5723-
const endPos = node.getInputPos(link.target_slot)
5735+
const endPos: Point = LiteGraph.vueNodesMode
5736+
? getSlotPosition(node, link.target_slot, true)
5737+
: node.getInputPos(link.target_slot)
57245738
const endDirection = node.inputs[link.target_slot]?.dir
57255739

57265740
firstReroute._dragging = true
@@ -5739,7 +5753,9 @@ export class LGraphCanvas
57395753
const node = graph.getNodeById(link.origin_id)
57405754
if (!node) continue
57415755

5742-
const startPos = node.getOutputPos(link.origin_slot)
5756+
const startPos: Point = LiteGraph.vueNodesMode
5757+
? getSlotPosition(node, link.origin_slot, false)
5758+
: node.getOutputPos(link.origin_slot)
57435759
const endPos = reroute.pos
57445760
const startDirection = node.outputs[link.origin_slot]?.dir
57455761

src/lib/litegraph/src/LGraphNode.ts

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3277,11 +3277,14 @@ export class LGraphNode
32773277
* Gets the position of an output slot, in graph co-ordinates.
32783278
*
32793279
* This method is preferred over the legacy {@link getConnectionPos} method.
3280-
* @param slot Output slot index
3280+
* @param outputSlotIndex Output slot index
32813281
* @returns Position of the output slot
32823282
*/
3283-
getOutputPos(slot: number): Point {
3284-
return calculateOutputSlotPos(this.#getSlotPositionContext(), slot)
3283+
getOutputPos(outputSlotIndex: number): Point {
3284+
return calculateOutputSlotPos(
3285+
this.#getSlotPositionContext(),
3286+
outputSlotIndex
3287+
)
32853288
}
32863289

32873290
/** @inheritdoc */

src/renderer/core/canvas/links/linkDropOrchestrator.ts

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,9 @@
11
import type { LGraph } from '@/lib/litegraph/src/LGraph'
22
import type { LinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
3-
import type { SlotDropCandidate } from '@/renderer/core/canvas/links/slotLinkDragState'
3+
import {
4+
type SlotDropCandidate,
5+
useSlotLinkDragState
6+
} from '@/renderer/core/canvas/links/slotLinkDragState'
47
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
58
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
69
import type { SlotLinkDragSession } from '@/renderer/extensions/vueNodes/composables/slotLinkDragSession'
@@ -13,8 +16,9 @@ interface DropResolutionContext {
1316

1417
export const resolveSlotTargetCandidate = (
1518
target: EventTarget | null,
16-
{ adapter, graph, session }: DropResolutionContext
19+
{ adapter, graph }: DropResolutionContext
1720
): SlotDropCandidate | null => {
21+
const { state: dragState, setCompatibleForKey } = useSlotLinkDragState()
1822
if (!(target instanceof HTMLElement)) return null
1923

2024
const elWithKey = target.closest<HTMLElement>('[data-slot-key]')
@@ -27,7 +31,7 @@ export const resolveSlotTargetCandidate = (
2731
const candidate: SlotDropCandidate = { layout, compatible: false }
2832

2933
if (adapter && graph) {
30-
const cached = session.compatCache.get(key)
34+
const cached = dragState.compatible.get(key)
3135
if (cached != null) {
3236
candidate.compatible = cached
3337
} else {
@@ -37,7 +41,7 @@ export const resolveSlotTargetCandidate = (
3741
? adapter.isInputValidDrop(nodeId, layout.index)
3842
: adapter.isOutputValidDrop(nodeId, layout.index)
3943

40-
session.compatCache.set(key, compatible)
44+
setCompatibleForKey(key, compatible)
4145
candidate.compatible = compatible
4246
}
4347
}
@@ -49,6 +53,7 @@ export const resolveNodeSurfaceCandidate = (
4953
target: EventTarget | null,
5054
{ adapter, graph, session }: DropResolutionContext
5155
): SlotDropCandidate | null => {
56+
const { setCompatibleForKey } = useSlotLinkDragState()
5257
if (!(target instanceof HTMLElement)) return null
5358

5459
const elWithNode = target.closest<HTMLElement>('[data-node-id]')
@@ -99,7 +104,7 @@ export const resolveNodeSurfaceCandidate = (
99104
? adapter.isInputValidDrop(nodeId, index)
100105
: adapter.isOutputValidDrop(nodeId, index)
101106

102-
session.compatCache.set(key, compatible)
107+
setCompatibleForKey(key, compatible)
103108

104109
if (!compatible) {
105110
session.nodePreferred.set(nodeId, null)

src/renderer/core/canvas/links/slotLinkDragState.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ interface SlotDragState {
3333
source: SlotDragSource | null
3434
pointer: PointerPosition
3535
candidate: SlotDropCandidate | null
36+
compatible: Map<string, boolean>
3637
}
3738

3839
const state = reactive<SlotDragState>({
@@ -43,7 +44,8 @@ const state = reactive<SlotDragState>({
4344
client: { x: 0, y: 0 },
4445
canvas: { x: 0, y: 0 }
4546
},
46-
candidate: null
47+
candidate: null,
48+
compatible: new Map<string, boolean>()
4749
})
4850

4951
function updatePointerPosition(
@@ -67,6 +69,7 @@ function beginDrag(source: SlotDragSource, pointerId: number) {
6769
state.source = source
6870
state.pointerId = pointerId
6971
state.candidate = null
72+
state.compatible.clear()
7073
}
7174

7275
function endDrag() {
@@ -78,6 +81,7 @@ function endDrag() {
7881
state.pointer.canvas.x = 0
7982
state.pointer.canvas.y = 0
8083
state.candidate = null
84+
state.compatible.clear()
8185
}
8286

8387
function getSlotLayout(nodeId: string, slotIndex: number, isInput: boolean) {
@@ -92,6 +96,14 @@ export function useSlotLinkDragState() {
9296
endDrag,
9397
updatePointerPosition,
9498
setCandidate,
95-
getSlotLayout
99+
getSlotLayout,
100+
setCompatibleMap: (entries: Iterable<[string, boolean]>) => {
101+
state.compatible.clear()
102+
for (const [key, value] of entries) state.compatible.set(key, value)
103+
},
104+
setCompatibleForKey: (key: string, value: boolean) => {
105+
state.compatible.set(key, value)
106+
},
107+
clearCompatible: () => state.compatible.clear()
96108
}
97109
}

src/renderer/core/layout/store/layoutStore.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -578,6 +578,14 @@ class LayoutStoreImpl implements LayoutStore {
578578
return this.rerouteLayouts.get(rerouteId) || null
579579
}
580580

581+
/**
582+
* Returns all slot layout keys currently tracked by the store.
583+
* Useful for global passes without relying on spatial queries.
584+
*/
585+
getAllSlotKeys(): string[] {
586+
return Array.from(this.slotLayouts.keys())
587+
}
588+
581589
/**
582590
* Update link segment layout data
583591
*/

src/renderer/core/layout/types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -309,6 +309,9 @@ export interface LayoutStore {
309309
getSlotLayout(key: string): SlotLayout | null
310310
getRerouteLayout(rerouteId: RerouteId): RerouteLayout | null
311311

312+
// Returns all slot layout keys currently tracked by the store
313+
getAllSlotKeys(): string[]
314+
312315
// Direct mutation API (CRDT-ready)
313316
applyOperation(operation: LayoutOperation): void
314317

src/renderer/extensions/vueNodes/components/InputSlot.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,8 @@ import {
3838
import { useErrorHandling } from '@/composables/useErrorHandling'
3939
import { getSlotColor } from '@/constants/slotColors'
4040
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
41+
import { useSlotLinkDragState } from '@/renderer/core/canvas/links/slotLinkDragState'
42+
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
4143
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
4244
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
4345
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
@@ -113,6 +115,15 @@ const slotColor = computed(() => {
113115
return getSlotColor(props.slotData.type)
114116
})
115117
118+
const { state: dragState } = useSlotLinkDragState()
119+
const slotKey = computed(() =>
120+
getSlotKey(props.nodeId ?? '', props.index, true)
121+
)
122+
const shouldDim = computed(() => {
123+
if (!dragState.active) return false
124+
return !dragState.compatible.get(slotKey.value)
125+
})
126+
116127
const slotWrapperClass = computed(() =>
117128
cn(
118129
'lg-slot lg-slot--input flex items-center group rounded-r-lg h-6',
@@ -122,7 +133,8 @@ const slotWrapperClass = computed(() =>
122133
: 'pr-6 hover:bg-black/5 hover:dark:bg-white/5',
123134
{
124135
'lg-slot--connected': props.connected,
125-
'lg-slot--compatible': props.compatible
136+
'lg-slot--compatible': props.compatible,
137+
'opacity-40': shouldDim.value
126138
}
127139
)
128140
)

src/renderer/extensions/vueNodes/components/OutputSlot.vue

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,8 @@ import {
3535
import { useErrorHandling } from '@/composables/useErrorHandling'
3636
import { getSlotColor } from '@/constants/slotColors'
3737
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
38+
import { useSlotLinkDragState } from '@/renderer/core/canvas/links/slotLinkDragState'
39+
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
3840
import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips'
3941
import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking'
4042
import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction'
@@ -83,6 +85,15 @@ onErrorCaptured((error) => {
8385
// Get slot color based on type
8486
const slotColor = computed(() => getSlotColor(props.slotData.type))
8587
88+
const { state: dragState } = useSlotLinkDragState()
89+
const slotKey = computed(() =>
90+
getSlotKey(props.nodeId ?? '', props.index, false)
91+
)
92+
const shouldDim = computed(() => {
93+
if (!dragState.active) return false
94+
return !dragState.compatible.get(slotKey.value)
95+
})
96+
8697
const slotWrapperClass = computed(() =>
8798
cn(
8899
'lg-slot lg-slot--output flex items-center justify-end group rounded-l-lg h-6',
@@ -92,7 +103,8 @@ const slotWrapperClass = computed(() =>
92103
: 'pl-6 hover:bg-black/5 hover:dark:bg-white/5',
93104
{
94105
'lg-slot--connected': props.connected,
95-
'lg-slot--compatible': props.compatible
106+
'lg-slot--compatible': props.compatible,
107+
'opacity-40': shouldDim.value
96108
}
97109
)
98110
)

src/renderer/extensions/vueNodes/composables/slotLinkDragSession.ts

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,6 @@ interface PendingMoveData {
77
}
88

99
export interface SlotLinkDragSession {
10-
compatCache: Map<string, boolean>
1110
nodePreferred: Map<
1211
number,
1312
{ index: number; key: string; layout: SlotLayout } | null
@@ -22,14 +21,12 @@ export interface SlotLinkDragSession {
2221

2322
export function createSlotLinkDragSession(): SlotLinkDragSession {
2423
const state: SlotLinkDragSession = {
25-
compatCache: new Map(),
2624
nodePreferred: new Map(),
2725
lastHoverSlotKey: null,
2826
lastHoverNodeId: null,
2927
lastCandidateKey: null,
3028
pendingMove: null,
3129
reset: () => {
32-
state.compatCache = new Map()
3330
state.nodePreferred = new Map()
3431
state.lastHoverSlotKey = null
3532
state.lastHoverNodeId = null

src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -89,8 +89,15 @@ export function useSlotLinkInteraction({
8989
index,
9090
type
9191
}: SlotInteractionOptions): SlotInteractionHandlers {
92-
const { state, beginDrag, endDrag, updatePointerPosition, setCandidate } =
93-
useSlotLinkDragState()
92+
const {
93+
state,
94+
beginDrag,
95+
endDrag,
96+
updatePointerPosition,
97+
setCandidate,
98+
setCompatibleForKey,
99+
clearCompatible
100+
} = useSlotLinkDragState()
94101
const conversion = useSharedCanvasPositionConversion()
95102
const pointerSession = createPointerSession()
96103
let activeAdapter: LinkConnectorAdapter | null = null
@@ -262,6 +269,7 @@ export function useSlotLinkInteraction({
262269
activeAdapter = null
263270
raf.cancel()
264271
dragSession.dispose()
272+
clearCompatible()
265273
}
266274

267275
const updatePointerState = (event: PointerEvent) => {
@@ -319,6 +327,22 @@ export function useSlotLinkInteraction({
319327
candidate = slotCandidate ?? nodeCandidate
320328
dragSession.lastHoverSlotKey = hoveredSlotKey
321329
dragSession.lastHoverNodeId = hoveredNodeId
330+
331+
if (slotCandidate) {
332+
const key = getSlotKey(
333+
slotCandidate.layout.nodeId,
334+
slotCandidate.layout.index,
335+
slotCandidate.layout.type === 'input'
336+
)
337+
setCompatibleForKey(key, !!slotCandidate.compatible)
338+
} else if (nodeCandidate) {
339+
const key = getSlotKey(
340+
nodeCandidate.layout.nodeId,
341+
nodeCandidate.layout.index,
342+
nodeCandidate.layout.type === 'input'
343+
)
344+
setCompatibleForKey(key, !!nodeCandidate.compatible)
345+
}
322346
}
323347

324348
const newCandidate = candidate?.compatible ? candidate : null
@@ -637,6 +661,20 @@ export function useSlotLinkInteraction({
637661
capture: true
638662
})
639663
)
664+
const targetType: 'input' | 'output' = type === 'input' ? 'output' : 'input'
665+
const allKeys = layoutStore.getAllSlotKeys()
666+
clearCompatible()
667+
for (const key of allKeys) {
668+
const slotLayout = layoutStore.getSlotLayout(key)
669+
if (!slotLayout) continue
670+
if (slotLayout.type !== targetType) continue
671+
const idx = slotLayout.index
672+
const ok =
673+
targetType === 'input'
674+
? activeAdapter.isInputValidDrop(slotLayout.nodeId, idx)
675+
: activeAdapter.isOutputValidDrop(slotLayout.nodeId, idx)
676+
setCompatibleForKey(key, ok)
677+
}
640678
app.canvas?.setDirty(true, true)
641679
event.preventDefault()
642680
event.stopPropagation()

0 commit comments

Comments
 (0)