Skip to content

Commit a5fc834

Browse files
benceruleanluclaude
andcommitted
feat: Implement DOM-based slot registration with unified position system
- Add centralized getSlotPosition() function in SlotCalculations - Create SlotIdentifier utilities for consistent slot key generation - Implement DOM-based slot registration composable with performance optimizations: - Cache slot offsets to avoid DOM reads during drag operations - Batch measurements via requestAnimationFrame - Skip redundant updates when bounds unchanged - Update Vue slot components to register DOM positions - Fix widget-to-input index mapping in NodeWidgets - Prevent double registration when Vue nodes enabled This improves slot hit-detection accuracy by using actual DOM positions while maintaining performance through intelligent caching and batching. 🤖 Generated with Claude Code Co-Authored-By: Claude <[email protected]>
1 parent 77b7d05 commit a5fc834

File tree

11 files changed

+435
-93
lines changed

11 files changed

+435
-93
lines changed

src/renderer/core/canvas/litegraph/LitegraphLinkAdapter.ts

Lines changed: 4 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -32,11 +32,7 @@ import {
3232
type Point,
3333
type RenderMode
3434
} from '@/renderer/core/canvas/PathRenderer'
35-
import {
36-
type SlotPositionContext,
37-
calculateInputSlotPos,
38-
calculateOutputSlotPos
39-
} from '@/renderer/core/canvas/litegraph/SlotCalculations'
35+
import { getSlotPosition } from '@/renderer/core/canvas/litegraph/SlotCalculations'
4036
import { layoutStore } from '@/renderer/core/layout/store/LayoutStore'
4137
import type { Bounds } from '@/renderer/core/layout/types'
4238

@@ -108,12 +104,12 @@ export class LitegraphLinkAdapter {
108104
}
109105

110106
// Get positions using layout tree data if available
111-
const startPos = this.getSlotPosition(
107+
const startPos = getSlotPosition(
112108
sourceNode,
113109
link.origin_slot,
114110
false // output
115111
)
116-
const endPos = this.getSlotPosition(
112+
const endPos = getSlotPosition(
117113
targetNode,
118114
link.target_slot,
119115
true // input
@@ -503,42 +499,6 @@ export class LitegraphLinkAdapter {
503499
}
504500
}
505501

506-
/**
507-
* Get slot position using layout tree if available, fallback to node's position
508-
*/
509-
private getSlotPosition(
510-
node: LGraphNode,
511-
slotIndex: number,
512-
isInput: boolean
513-
): ReadOnlyPoint {
514-
// Try to get position from layout tree
515-
const nodeLayout = layoutStore.getNodeLayoutRef(String(node.id)).value
516-
517-
if (nodeLayout) {
518-
// Create context from layout tree data
519-
const context: SlotPositionContext = {
520-
nodeX: nodeLayout.position.x,
521-
nodeY: nodeLayout.position.y,
522-
nodeWidth: nodeLayout.size.width,
523-
nodeHeight: nodeLayout.size.height,
524-
collapsed: node.flags.collapsed || false,
525-
collapsedWidth: node._collapsed_width,
526-
slotStartY: node.constructor.slot_start_y,
527-
inputs: node.inputs,
528-
outputs: node.outputs,
529-
widgets: node.widgets
530-
}
531-
532-
// Use helper to calculate position
533-
return isInput
534-
? calculateInputSlotPos(context, slotIndex)
535-
: calculateOutputSlotPos(context, slotIndex)
536-
}
537-
538-
// Fallback to node's own methods if layout not available
539-
return isInput ? node.getInputPos(slotIndex) : node.getOutputPos(slotIndex)
540-
}
541-
542502
/**
543503
* Render a link being dragged from a slot to mouse position
544504
* Used during link creation/reconnection
@@ -559,7 +519,7 @@ export class LitegraphLinkAdapter {
559519
if (!fromNode) return
560520

561521
// Get slot position using layout tree if available
562-
const slotPos = this.getSlotPosition(
522+
const slotPos = getSlotPosition(
563523
fromNode,
564524
fromSlotIndex,
565525
options.fromInput || false

src/renderer/core/canvas/litegraph/SlotCalculations.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,18 @@
55
* This allows both litegraph nodes and the layout system to use the same
66
* calculation logic while providing their own position data.
77
*/
8+
import type { LGraphNode } from '@/lib/litegraph/src/LGraphNode'
89
import type {
910
INodeInputSlot,
1011
INodeOutputSlot,
1112
INodeSlot,
12-
Point
13+
Point,
14+
ReadOnlyPoint
1315
} from '@/lib/litegraph/src/interfaces'
1416
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
1517
import { isWidgetInputSlot } from '@/lib/litegraph/src/node/slotUtils'
18+
import { getSlotKey } from '@/renderer/core/layout/slots/SlotIdentifier'
19+
import { layoutStore } from '@/renderer/core/layout/store/LayoutStore'
1620

1721
export interface SlotPositionContext {
1822
/** Node's X position in graph coordinates */
@@ -147,6 +151,54 @@ export function calculateOutputSlotPos(
147151
return [nodeX + nodeWidth + 1 - offsetX, nodeY + slotY + nodeOffsetY]
148152
}
149153

154+
/**
155+
* Get slot position using layout tree if available, fallback to node's position
156+
* Unified implementation used by both LitegraphLinkAdapter and useLinkLayoutSync
157+
* @param node The LGraphNode
158+
* @param slotIndex The slot index
159+
* @param isInput Whether this is an input slot
160+
* @returns Position of the slot center in graph coordinates
161+
*/
162+
export function getSlotPosition(
163+
node: LGraphNode,
164+
slotIndex: number,
165+
isInput: boolean
166+
): ReadOnlyPoint {
167+
// Try to get precise position from slot layout (DOM-registered)
168+
const slotKey = getSlotKey(String(node.id), slotIndex, isInput)
169+
const slotLayout = layoutStore.getSlotLayout(slotKey)
170+
if (slotLayout) {
171+
return [slotLayout.position.x, slotLayout.position.y]
172+
}
173+
174+
// Fallback: derive position from node layout tree and slot model
175+
const nodeLayout = layoutStore.getNodeLayoutRef(String(node.id)).value
176+
177+
if (nodeLayout) {
178+
// Create context from layout tree data
179+
const context: SlotPositionContext = {
180+
nodeX: nodeLayout.position.x,
181+
nodeY: nodeLayout.position.y,
182+
nodeWidth: nodeLayout.size.width,
183+
nodeHeight: nodeLayout.size.height,
184+
collapsed: node.flags.collapsed || false,
185+
collapsedWidth: node._collapsed_width,
186+
slotStartY: node.constructor.slot_start_y,
187+
inputs: node.inputs,
188+
outputs: node.outputs,
189+
widgets: node.widgets
190+
}
191+
192+
// Use helper to calculate position
193+
return isInput
194+
? calculateInputSlotPos(context, slotIndex)
195+
: calculateOutputSlotPos(context, slotIndex)
196+
}
197+
198+
// Fallback to node's own methods if layout not available
199+
return isInput ? node.getInputPos(slotIndex) : node.getOutputPos(slotIndex)
200+
}
201+
150202
/**
151203
* Get the inputs that are not positioned with absolute coordinates
152204
*/
Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,76 @@
1+
/**
2+
* Slot identifier utilities for consistent slot key generation and parsing
3+
*
4+
* Provides a centralized interface for slot identification across the layout system
5+
*
6+
* @TODO Replace this concatenated string with root cause fix
7+
*/
8+
9+
export interface SlotIdentifier {
10+
nodeId: string
11+
index: number
12+
isInput: boolean
13+
}
14+
15+
/**
16+
* Generate a unique key for a slot
17+
* Format: "{nodeId}-{in|out}-{index}"
18+
*/
19+
export function getSlotKey(identifier: SlotIdentifier): string
20+
export function getSlotKey(
21+
nodeId: string,
22+
index: number,
23+
isInput: boolean
24+
): string
25+
export function getSlotKey(
26+
nodeIdOrIdentifier: string | SlotIdentifier,
27+
index?: number,
28+
isInput?: boolean
29+
): string {
30+
if (typeof nodeIdOrIdentifier === 'object') {
31+
const { nodeId, index, isInput } = nodeIdOrIdentifier
32+
return `${nodeId}-${isInput ? 'in' : 'out'}-${index}`
33+
}
34+
35+
if (index === undefined || isInput === undefined) {
36+
throw new Error('Missing required parameters for slot key generation')
37+
}
38+
39+
return `${nodeIdOrIdentifier}-${isInput ? 'in' : 'out'}-${index}`
40+
}
41+
42+
/**
43+
* Parse a slot key back into its components
44+
*/
45+
export function parseSlotKey(key: string): SlotIdentifier | null {
46+
const match = key.match(/^(.+)-(in|out)-(\d+)$/)
47+
if (!match) return null
48+
49+
return {
50+
nodeId: match[1],
51+
isInput: match[2] === 'in',
52+
index: parseInt(match[3], 10)
53+
}
54+
}
55+
56+
/**
57+
* Check if a key represents an input slot
58+
*/
59+
export function isInputSlotKey(key: string): boolean {
60+
return key.includes('-in-')
61+
}
62+
63+
/**
64+
* Check if a key represents an output slot
65+
*/
66+
export function isOutputSlotKey(key: string): boolean {
67+
return key.includes('-out-')
68+
}
69+
70+
/**
71+
* Get the node ID from a slot key
72+
*/
73+
export function getNodeIdFromSlotKey(key: string): string | null {
74+
const parsed = parseSlotKey(key)
75+
return parsed?.nodeId ?? null
76+
}

src/renderer/core/layout/slots/register.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,8 @@ import {
1515
import { layoutStore } from '@/renderer/core/layout/store/LayoutStore'
1616
import type { SlotLayout } from '@/renderer/core/layout/types'
1717

18+
import { getSlotKey } from './SlotIdentifier'
19+
1820
/**
1921
* Register slot layout with the layout store for hit testing
2022
* @param nodeId The node ID
@@ -28,7 +30,7 @@ export function registerSlotLayout(
2830
isInput: boolean,
2931
position: Point
3032
): void {
31-
const slotKey = `${nodeId}-${isInput ? 'in' : 'out'}-${slotIndex}`
33+
const slotKey = getSlotKey(nodeId, slotIndex, isInput)
3234

3335
// Calculate bounds for the slot using LiteGraph's standard slot height
3436
const slotSize = LiteGraph.NODE_SLOT_HEIGHT

0 commit comments

Comments
 (0)