Skip to content

Commit 8e8a337

Browse files
Add slot dimming (#5937)
## Summary Add slot dimming by precomputing compatible targets during link drag start, improving clarity and responsiveness when connecting links. ## Changes - What: Centralize compatibility precompute on pointer down to dim incompatible slots across the canvas. - What: Derive target side from the source slot type; remove overly defensive try/catch and optional chaining. - What: Iterate LayoutStore slot keys; clear then set compatibility per key for simplicity. - What: Remove redundant numeric coercions and double-negation; rely on existing types. - Breaking: None - Dependencies: None ## Review Focus - Validate dimming correctness for both input→output and output→input drags. - Check performance on large graphs (single pass on start; no per-frame cost). - Ensure no regressions with reroute snapping and node-surface candidates. ## Screenshots (if applicable) N/A ┆Issue is synchronized with this [Notion page](https://www.notion.so/PR-5937-Add-slot-dimming-2846d73d3650816f932ed657f5526f5e) by [Unito](https://www.unito.io)
1 parent c73df4d commit 8e8a337

File tree

8 files changed

+101
-14
lines changed

8 files changed

+101
-14
lines changed

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)