From f7ea0360ef6a45a34e0777bc311194fe396ede48 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Fri, 3 Oct 2025 00:55:10 -0700 Subject: [PATCH 1/5] feat(litegraph): use layoutStore slot positions for node slot queries\n\n- Route getInputPos/getInputSlotPos/getOutputPos through getSlotPosition(), which prefers layoutStore DOM-tracked positions and falls back to geometry.\n- Ensures reroute-origin drags snap to visually tracked node slots when hovering compatible nodes in Vue nodes mode.\n- No change for non-Vue nodes mode or when no tracked slot layout is present. --- src/lib/litegraph/src/LGraphNode.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index a1eeb8ae27..a4604c9a16 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -1,9 +1,8 @@ import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties' import { type SlotPositionContext, - calculateInputSlotPos, calculateInputSlotPosFromSlot, - calculateOutputSlotPos + getSlotPosition } from '@/renderer/core/canvas/litegraph/slotCalculations' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' @@ -3267,7 +3266,7 @@ export class LGraphNode * @returns Position of the input slot */ getInputPos(slot: number): Point { - return calculateInputSlotPos(this.#getSlotPositionContext(), slot) + return getSlotPosition(this, slot, true) } /** @@ -3276,6 +3275,9 @@ export class LGraphNode * @returns Position of the centre of the input slot in graph co-ordinates. */ getInputSlotPos(input: INodeInputSlot): Point { + const idx = this.inputs.indexOf(input) + if (idx !== -1) return getSlotPosition(this, idx, true) + // Fallback when slot instance is not found in inputs return calculateInputSlotPosFromSlot(this.#getSlotPositionContext(), input) } @@ -3283,11 +3285,11 @@ export class LGraphNode * Gets the position of an output slot, in graph co-ordinates. * * This method is preferred over the legacy {@link getConnectionPos} method. - * @param slot Output slot index + * @param outputSlotIndex Output slot index * @returns Position of the output slot */ - getOutputPos(slot: number): Point { - return calculateOutputSlotPos(this.#getSlotPositionContext(), slot) + getOutputPos(outputSlotIndex: number): Point { + return getSlotPosition(this, outputSlotIndex, false) } /** @inheritdoc */ From da874705155026c2fb71743198078f85368f706a Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Fri, 3 Oct 2025 04:22:11 -0700 Subject: [PATCH 2/5] From 027e06d8d107710eeea8a2e50599b6db63211036 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Fri, 3 Oct 2025 05:20:51 -0700 Subject: [PATCH 3/5] Revert my incredibly intelligent changes --- src/lib/litegraph/src/LGraphNode.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index a4604c9a16..11b4d1238a 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -1,8 +1,9 @@ import { LGraphNodeProperties } from '@/lib/litegraph/src/LGraphNodeProperties' import { type SlotPositionContext, + calculateInputSlotPos, calculateInputSlotPosFromSlot, - getSlotPosition + calculateOutputSlotPos } from '@/renderer/core/canvas/litegraph/slotCalculations' import { useLayoutMutations } from '@/renderer/core/layout/operations/layoutMutations' import { LayoutSource } from '@/renderer/core/layout/types' @@ -3266,7 +3267,7 @@ export class LGraphNode * @returns Position of the input slot */ getInputPos(slot: number): Point { - return getSlotPosition(this, slot, true) + return calculateInputSlotPos(this.#getSlotPositionContext(), slot) } /** @@ -3275,9 +3276,6 @@ export class LGraphNode * @returns Position of the centre of the input slot in graph co-ordinates. */ getInputSlotPos(input: INodeInputSlot): Point { - const idx = this.inputs.indexOf(input) - if (idx !== -1) return getSlotPosition(this, idx, true) - // Fallback when slot instance is not found in inputs return calculateInputSlotPosFromSlot(this.#getSlotPositionContext(), input) } @@ -3289,7 +3287,10 @@ export class LGraphNode * @returns Position of the output slot */ getOutputPos(outputSlotIndex: number): Point { - return getSlotPosition(this, outputSlotIndex, false) + return calculateOutputSlotPos( + this.#getSlotPositionContext(), + outputSlotIndex + ) } /** @inheritdoc */ From d70b7cc434ce9e2cda8db3ae65ea5ff11fa22db6 Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Fri, 3 Oct 2025 05:55:27 -0700 Subject: [PATCH 4/5] Move more getInputOutputPos to getSlotPosition --- src/lib/litegraph/src/LGraphCanvas.ts | 34 +++++++++++++++++++++------ 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 22b71c66a3..f84643478f 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -3315,7 +3315,15 @@ export class LGraphCanvas if (slot && linkConnector.isInputValidDrop(node, slot)) { highlightInput = slot - highlightPos = node.getInputSlotPos(slot) + if (LiteGraph.vueNodesMode) { + const idx = node.inputs.indexOf(slot) + highlightPos = + idx !== -1 + ? getSlotPosition(node, idx, true) + : node.getInputSlotPos(slot) + } else { + highlightPos = node.getInputSlotPos(slot) + } linkConnector.overWidget = overWidget } } @@ -3327,7 +3335,9 @@ export class LGraphCanvas const result = node.findInputByType(firstLink.fromSlot.type) if (result) { highlightInput = result.slot - highlightPos = node.getInputSlotPos(result.slot) + highlightPos = LiteGraph.vueNodesMode + ? getSlotPosition(node, result.index, true) + : node.getInputSlotPos(result.slot) } } else if ( inputId != -1 && @@ -3352,7 +3362,9 @@ export class LGraphCanvas if (inputId === -1 && outputId === -1) { const result = node.findOutputByType(firstLink.fromSlot.type) if (result) { - highlightPos = node.getOutputPos(result.index) + highlightPos = LiteGraph.vueNodesMode + ? getSlotPosition(node, result.index, false) + : node.getOutputPos(result.index) } } else { // check if I have a slot below de mouse @@ -5617,7 +5629,9 @@ export class LGraphCanvas const { link, inputNode, input } = resolved if (!inputNode || !input) continue - const endPos = inputNode.getInputPos(link.target_slot) + const endPos: Point = LiteGraph.vueNodesMode + ? getSlotPosition(inputNode, link.target_slot, true) + : inputNode.getInputPos(link.target_slot) this.#renderAllLinkSegments( ctx, @@ -5642,7 +5656,9 @@ export class LGraphCanvas const { link, outputNode, output } = resolved if (!outputNode || !output) continue - const startPos = outputNode.getOutputPos(link.origin_slot) + const startPos: Point = LiteGraph.vueNodesMode + ? getSlotPosition(outputNode, link.origin_slot, false) + : outputNode.getOutputPos(link.origin_slot) this.#renderAllLinkSegments( ctx, @@ -5707,7 +5723,9 @@ export class LGraphCanvas if (!node) continue const startPos = firstReroute.pos - const endPos = node.getInputPos(link.target_slot) + const endPos: Point = LiteGraph.vueNodesMode + ? getSlotPosition(node, link.target_slot, true) + : node.getInputPos(link.target_slot) const endDirection = node.inputs[link.target_slot]?.dir firstReroute._dragging = true @@ -5726,7 +5744,9 @@ export class LGraphCanvas const node = graph.getNodeById(link.origin_id) if (!node) continue - const startPos = node.getOutputPos(link.origin_slot) + const startPos: Point = LiteGraph.vueNodesMode + ? getSlotPosition(node, link.origin_slot, false) + : node.getOutputPos(link.origin_slot) const endPos = reroute.pos const startDirection = node.outputs[link.origin_slot]?.dir From 8e8a3372f696725fee951a97ea328208f2b9f63c Mon Sep 17 00:00:00 2001 From: Benjamin Lu Date: Tue, 7 Oct 2025 11:00:23 -0700 Subject: [PATCH 5/5] Add slot dimming (#5937) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 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) --- .../core/canvas/links/linkDropOrchestrator.ts | 15 ++++--- .../core/canvas/links/slotLinkDragState.ts | 16 ++++++- src/renderer/core/layout/store/layoutStore.ts | 8 ++++ src/renderer/core/layout/types.ts | 3 ++ .../vueNodes/components/InputSlot.vue | 14 ++++++- .../vueNodes/components/OutputSlot.vue | 14 ++++++- .../composables/slotLinkDragSession.ts | 3 -- .../composables/useSlotLinkInteraction.ts | 42 ++++++++++++++++++- 8 files changed, 101 insertions(+), 14 deletions(-) diff --git a/src/renderer/core/canvas/links/linkDropOrchestrator.ts b/src/renderer/core/canvas/links/linkDropOrchestrator.ts index 7399ec4014..e3db2b6f0e 100644 --- a/src/renderer/core/canvas/links/linkDropOrchestrator.ts +++ b/src/renderer/core/canvas/links/linkDropOrchestrator.ts @@ -1,6 +1,9 @@ import type { LGraph } from '@/lib/litegraph/src/LGraph' import type { LinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter' -import type { SlotDropCandidate } from '@/renderer/core/canvas/links/slotLinkDragState' +import { + type SlotDropCandidate, + useSlotLinkDragState +} from '@/renderer/core/canvas/links/slotLinkDragState' import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' import { layoutStore } from '@/renderer/core/layout/store/layoutStore' import type { SlotLinkDragSession } from '@/renderer/extensions/vueNodes/composables/slotLinkDragSession' @@ -13,8 +16,9 @@ interface DropResolutionContext { export const resolveSlotTargetCandidate = ( target: EventTarget | null, - { adapter, graph, session }: DropResolutionContext + { adapter, graph }: DropResolutionContext ): SlotDropCandidate | null => { + const { state: dragState, setCompatibleForKey } = useSlotLinkDragState() if (!(target instanceof HTMLElement)) return null const elWithKey = target.closest('[data-slot-key]') @@ -27,7 +31,7 @@ export const resolveSlotTargetCandidate = ( const candidate: SlotDropCandidate = { layout, compatible: false } if (adapter && graph) { - const cached = session.compatCache.get(key) + const cached = dragState.compatible.get(key) if (cached != null) { candidate.compatible = cached } else { @@ -37,7 +41,7 @@ export const resolveSlotTargetCandidate = ( ? adapter.isInputValidDrop(nodeId, layout.index) : adapter.isOutputValidDrop(nodeId, layout.index) - session.compatCache.set(key, compatible) + setCompatibleForKey(key, compatible) candidate.compatible = compatible } } @@ -49,6 +53,7 @@ export const resolveNodeSurfaceCandidate = ( target: EventTarget | null, { adapter, graph, session }: DropResolutionContext ): SlotDropCandidate | null => { + const { setCompatibleForKey } = useSlotLinkDragState() if (!(target instanceof HTMLElement)) return null const elWithNode = target.closest('[data-node-id]') @@ -99,7 +104,7 @@ export const resolveNodeSurfaceCandidate = ( ? adapter.isInputValidDrop(nodeId, index) : adapter.isOutputValidDrop(nodeId, index) - session.compatCache.set(key, compatible) + setCompatibleForKey(key, compatible) if (!compatible) { session.nodePreferred.set(nodeId, null) diff --git a/src/renderer/core/canvas/links/slotLinkDragState.ts b/src/renderer/core/canvas/links/slotLinkDragState.ts index 33c47f0f50..81dfde059d 100644 --- a/src/renderer/core/canvas/links/slotLinkDragState.ts +++ b/src/renderer/core/canvas/links/slotLinkDragState.ts @@ -33,6 +33,7 @@ interface SlotDragState { source: SlotDragSource | null pointer: PointerPosition candidate: SlotDropCandidate | null + compatible: Map } const state = reactive({ @@ -43,7 +44,8 @@ const state = reactive({ client: { x: 0, y: 0 }, canvas: { x: 0, y: 0 } }, - candidate: null + candidate: null, + compatible: new Map() }) function updatePointerPosition( @@ -67,6 +69,7 @@ function beginDrag(source: SlotDragSource, pointerId: number) { state.source = source state.pointerId = pointerId state.candidate = null + state.compatible.clear() } function endDrag() { @@ -78,6 +81,7 @@ function endDrag() { state.pointer.canvas.x = 0 state.pointer.canvas.y = 0 state.candidate = null + state.compatible.clear() } function getSlotLayout(nodeId: string, slotIndex: number, isInput: boolean) { @@ -92,6 +96,14 @@ export function useSlotLinkDragState() { endDrag, updatePointerPosition, setCandidate, - getSlotLayout + getSlotLayout, + setCompatibleMap: (entries: Iterable<[string, boolean]>) => { + state.compatible.clear() + for (const [key, value] of entries) state.compatible.set(key, value) + }, + setCompatibleForKey: (key: string, value: boolean) => { + state.compatible.set(key, value) + }, + clearCompatible: () => state.compatible.clear() } } diff --git a/src/renderer/core/layout/store/layoutStore.ts b/src/renderer/core/layout/store/layoutStore.ts index 15d4128142..45d1122e82 100644 --- a/src/renderer/core/layout/store/layoutStore.ts +++ b/src/renderer/core/layout/store/layoutStore.ts @@ -578,6 +578,14 @@ class LayoutStoreImpl implements LayoutStore { return this.rerouteLayouts.get(rerouteId) || null } + /** + * Returns all slot layout keys currently tracked by the store. + * Useful for global passes without relying on spatial queries. + */ + getAllSlotKeys(): string[] { + return Array.from(this.slotLayouts.keys()) + } + /** * Update link segment layout data */ diff --git a/src/renderer/core/layout/types.ts b/src/renderer/core/layout/types.ts index ae2b761398..176b64396b 100644 --- a/src/renderer/core/layout/types.ts +++ b/src/renderer/core/layout/types.ts @@ -309,6 +309,9 @@ export interface LayoutStore { getSlotLayout(key: string): SlotLayout | null getRerouteLayout(rerouteId: RerouteId): RerouteLayout | null + // Returns all slot layout keys currently tracked by the store + getAllSlotKeys(): string[] + // Direct mutation API (CRDT-ready) applyOperation(operation: LayoutOperation): void diff --git a/src/renderer/extensions/vueNodes/components/InputSlot.vue b/src/renderer/extensions/vueNodes/components/InputSlot.vue index e746881814..a62acdb674 100644 --- a/src/renderer/extensions/vueNodes/components/InputSlot.vue +++ b/src/renderer/extensions/vueNodes/components/InputSlot.vue @@ -38,6 +38,8 @@ import { import { useErrorHandling } from '@/composables/useErrorHandling' import { getSlotColor } from '@/constants/slotColors' import type { INodeSlot } from '@/lib/litegraph/src/litegraph' +import { useSlotLinkDragState } from '@/renderer/core/canvas/links/slotLinkDragState' +import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips' import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking' import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction' @@ -113,6 +115,15 @@ const slotColor = computed(() => { return getSlotColor(props.slotData.type) }) +const { state: dragState } = useSlotLinkDragState() +const slotKey = computed(() => + getSlotKey(props.nodeId ?? '', props.index, true) +) +const shouldDim = computed(() => { + if (!dragState.active) return false + return !dragState.compatible.get(slotKey.value) +}) + const slotWrapperClass = computed(() => cn( 'lg-slot lg-slot--input flex items-center group rounded-r-lg h-6', @@ -122,7 +133,8 @@ const slotWrapperClass = computed(() => : 'pr-6 hover:bg-black/5 hover:dark:bg-white/5', { 'lg-slot--connected': props.connected, - 'lg-slot--compatible': props.compatible + 'lg-slot--compatible': props.compatible, + 'opacity-40': shouldDim.value } ) ) diff --git a/src/renderer/extensions/vueNodes/components/OutputSlot.vue b/src/renderer/extensions/vueNodes/components/OutputSlot.vue index 12db0a6a48..18f3c6841a 100644 --- a/src/renderer/extensions/vueNodes/components/OutputSlot.vue +++ b/src/renderer/extensions/vueNodes/components/OutputSlot.vue @@ -35,6 +35,8 @@ import { import { useErrorHandling } from '@/composables/useErrorHandling' import { getSlotColor } from '@/constants/slotColors' import type { INodeSlot } from '@/lib/litegraph/src/litegraph' +import { useSlotLinkDragState } from '@/renderer/core/canvas/links/slotLinkDragState' +import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier' import { useNodeTooltips } from '@/renderer/extensions/vueNodes/composables/useNodeTooltips' import { useSlotElementTracking } from '@/renderer/extensions/vueNodes/composables/useSlotElementTracking' import { useSlotLinkInteraction } from '@/renderer/extensions/vueNodes/composables/useSlotLinkInteraction' @@ -83,6 +85,15 @@ onErrorCaptured((error) => { // Get slot color based on type const slotColor = computed(() => getSlotColor(props.slotData.type)) +const { state: dragState } = useSlotLinkDragState() +const slotKey = computed(() => + getSlotKey(props.nodeId ?? '', props.index, false) +) +const shouldDim = computed(() => { + if (!dragState.active) return false + return !dragState.compatible.get(slotKey.value) +}) + const slotWrapperClass = computed(() => cn( 'lg-slot lg-slot--output flex items-center justify-end group rounded-l-lg h-6', @@ -92,7 +103,8 @@ const slotWrapperClass = computed(() => : 'pl-6 hover:bg-black/5 hover:dark:bg-white/5', { 'lg-slot--connected': props.connected, - 'lg-slot--compatible': props.compatible + 'lg-slot--compatible': props.compatible, + 'opacity-40': shouldDim.value } ) ) diff --git a/src/renderer/extensions/vueNodes/composables/slotLinkDragSession.ts b/src/renderer/extensions/vueNodes/composables/slotLinkDragSession.ts index 1929526ce9..1133dd1f3d 100644 --- a/src/renderer/extensions/vueNodes/composables/slotLinkDragSession.ts +++ b/src/renderer/extensions/vueNodes/composables/slotLinkDragSession.ts @@ -7,7 +7,6 @@ interface PendingMoveData { } export interface SlotLinkDragSession { - compatCache: Map nodePreferred: Map< number, { index: number; key: string; layout: SlotLayout } | null @@ -22,14 +21,12 @@ export interface SlotLinkDragSession { export function createSlotLinkDragSession(): SlotLinkDragSession { const state: SlotLinkDragSession = { - compatCache: new Map(), nodePreferred: new Map(), lastHoverSlotKey: null, lastHoverNodeId: null, lastCandidateKey: null, pendingMove: null, reset: () => { - state.compatCache = new Map() state.nodePreferred = new Map() state.lastHoverSlotKey = null state.lastHoverNodeId = null diff --git a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts index 504c035c8c..b3afc18537 100644 --- a/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts +++ b/src/renderer/extensions/vueNodes/composables/useSlotLinkInteraction.ts @@ -89,8 +89,15 @@ export function useSlotLinkInteraction({ index, type }: SlotInteractionOptions): SlotInteractionHandlers { - const { state, beginDrag, endDrag, updatePointerPosition, setCandidate } = - useSlotLinkDragState() + const { + state, + beginDrag, + endDrag, + updatePointerPosition, + setCandidate, + setCompatibleForKey, + clearCompatible + } = useSlotLinkDragState() const conversion = useSharedCanvasPositionConversion() const pointerSession = createPointerSession() let activeAdapter: LinkConnectorAdapter | null = null @@ -262,6 +269,7 @@ export function useSlotLinkInteraction({ activeAdapter = null raf.cancel() dragSession.dispose() + clearCompatible() } const updatePointerState = (event: PointerEvent) => { @@ -319,6 +327,22 @@ export function useSlotLinkInteraction({ candidate = slotCandidate ?? nodeCandidate dragSession.lastHoverSlotKey = hoveredSlotKey dragSession.lastHoverNodeId = hoveredNodeId + + if (slotCandidate) { + const key = getSlotKey( + slotCandidate.layout.nodeId, + slotCandidate.layout.index, + slotCandidate.layout.type === 'input' + ) + setCompatibleForKey(key, !!slotCandidate.compatible) + } else if (nodeCandidate) { + const key = getSlotKey( + nodeCandidate.layout.nodeId, + nodeCandidate.layout.index, + nodeCandidate.layout.type === 'input' + ) + setCompatibleForKey(key, !!nodeCandidate.compatible) + } } const newCandidate = candidate?.compatible ? candidate : null @@ -637,6 +661,20 @@ export function useSlotLinkInteraction({ capture: true }) ) + const targetType: 'input' | 'output' = type === 'input' ? 'output' : 'input' + const allKeys = layoutStore.getAllSlotKeys() + clearCompatible() + for (const key of allKeys) { + const slotLayout = layoutStore.getSlotLayout(key) + if (!slotLayout) continue + if (slotLayout.type !== targetType) continue + const idx = slotLayout.index + const ok = + targetType === 'input' + ? activeAdapter.isInputValidDrop(slotLayout.nodeId, idx) + : activeAdapter.isOutputValidDrop(slotLayout.nodeId, idx) + setCompatibleForKey(key, ok) + } app.canvas?.setDirty(true, true) event.preventDefault() event.stopPropagation()