diff --git a/src/lib/litegraph/src/LGraphCanvas.ts b/src/lib/litegraph/src/LGraphCanvas.ts index 13f5c9caf9..ecba62e3d2 100644 --- a/src/lib/litegraph/src/LGraphCanvas.ts +++ b/src/lib/litegraph/src/LGraphCanvas.ts @@ -3318,7 +3318,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 } } @@ -3330,7 +3338,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 && @@ -3355,7 +3365,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 @@ -5720,7 +5732,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 @@ -5739,7 +5753,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 diff --git a/src/lib/litegraph/src/LGraphNode.ts b/src/lib/litegraph/src/LGraphNode.ts index 2de06cd911..d80ef4e3bd 100644 --- a/src/lib/litegraph/src/LGraphNode.ts +++ b/src/lib/litegraph/src/LGraphNode.ts @@ -3277,11 +3277,14 @@ 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 calculateOutputSlotPos( + this.#getSlotPositionContext(), + outputSlotIndex + ) } /** @inheritdoc */ 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()