Refactor slotkey from bespoke string key

This commit is contained in:
Benjamin Lu
2025-10-10 12:58:25 -07:00
parent d7796fcda4
commit 799ab5e5aa
14 changed files with 430 additions and 231 deletions

View File

@@ -32,7 +32,6 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
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'
@@ -105,13 +104,12 @@ const slotColor = computed(() => {
return getSlotColor(props.slotData.type)
})
const { state: dragState } = useSlotLinkDragUIState()
const slotKey = computed(() =>
getSlotKey(props.nodeId ?? '', props.index, true)
)
const { state: dragState, getCompatible } = useSlotLinkDragUIState()
const shouldDim = computed(() => {
if (!dragState.active) return false
return !dragState.compatible.get(slotKey.value)
const nodeId = props.nodeId ?? ''
const value = getCompatible(nodeId, 'input', props.index)
return !value
})
const slotWrapperClass = computed(() =>

View File

@@ -29,7 +29,6 @@ import { useErrorHandling } from '@/composables/useErrorHandling'
import { getSlotColor } from '@/constants/slotColors'
import type { INodeSlot } from '@/lib/litegraph/src/litegraph'
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
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'
@@ -75,13 +74,12 @@ onErrorCaptured((error) => {
// Get slot color based on type
const slotColor = computed(() => getSlotColor(props.slotData.type))
const { state: dragState } = useSlotLinkDragUIState()
const slotKey = computed(() =>
getSlotKey(props.nodeId ?? '', props.index, false)
)
const { state: dragState, getCompatible } = useSlotLinkDragUIState()
const shouldDim = computed(() => {
if (!dragState.active) return false
return !dragState.compatible.get(slotKey.value)
const nodeId = props.nodeId ?? ''
const value = getCompatible(nodeId, 'output', props.index)
return !value
})
const slotWrapperClass = computed(() =>

View File

@@ -1,5 +1,5 @@
import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
import type { SlotLayout } from '@/renderer/core/layout/types'
import type { SlotIdentity, SlotLayout } from '@/renderer/core/layout/types'
/**
* Slot link drag context
@@ -17,14 +17,18 @@ interface PendingPointerMoveData {
export interface SlotLinkDragContext {
preferredSlotForNode: Map<
NodeId,
{ index: number; key: string; layout: SlotLayout } | null
{
index: number
identity: SlotIdentity
layout: SlotLayout
} | null
>
lastHoverSlotKey: string | null
lastHoverSlotIdentity: SlotIdentity | null
lastHoverNodeId: NodeId | null
lastCandidateKey: string | null
lastCandidateIdentity: SlotIdentity | null
pendingPointerMove: PendingPointerMoveData | null
lastPointerEventTarget: EventTarget | null
lastPointerTargetSlotKey: string | null
lastPointerTargetSlotIdentity: SlotIdentity | null
lastPointerTargetNodeId: NodeId | null
reset: () => void
dispose: () => void
@@ -33,21 +37,21 @@ export interface SlotLinkDragContext {
export function createSlotLinkDragContext(): SlotLinkDragContext {
const state: SlotLinkDragContext = {
preferredSlotForNode: new Map(),
lastHoverSlotKey: null,
lastHoverSlotIdentity: null,
lastHoverNodeId: null,
lastCandidateKey: null,
lastCandidateIdentity: null,
pendingPointerMove: null,
lastPointerEventTarget: null,
lastPointerTargetSlotKey: null,
lastPointerTargetSlotIdentity: null,
lastPointerTargetNodeId: null,
reset: () => {
state.preferredSlotForNode = new Map()
state.lastHoverSlotKey = null
state.lastHoverSlotIdentity = null
state.lastHoverNodeId = null
state.lastCandidateKey = null
state.lastCandidateIdentity = null
state.pendingPointerMove = null
state.lastPointerEventTarget = null
state.lastPointerTargetSlotKey = null
state.lastPointerTargetSlotIdentity = null
state.lastPointerTargetNodeId = null
},
dispose: () => {

View File

@@ -10,7 +10,7 @@ import type { Ref } from 'vue'
import { useSharedCanvasPositionConversion } from '@/composables/element/useCanvasPositionConversion'
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
// Slot keys migrated to identity attributes
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { SlotLayout } from '@/renderer/core/layout/types'
import {
@@ -50,9 +50,14 @@ export function syncNodeSlotLayoutsFromDOM(
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
if (!nodeLayout) return
const batch: Array<{ key: string; layout: SlotLayout }> = []
const batch: Array<{
nodeId: string
type: 'input' | 'output'
index: number
layout: SlotLayout
}> = []
for (const [slotKey, entry] of node.slots) {
for (const [index, entry] of node.slots.input) {
const rect = entry.el.getBoundingClientRect()
const screenCenter: [number, number] = [
rect.left + rect.width / 2,
@@ -73,7 +78,9 @@ export function syncNodeSlotLayoutsFromDOM(
const size = LiteGraph.NODE_SLOT_HEIGHT
const half = size / 2
batch.push({
key: slotKey,
nodeId,
type: 'input',
index,
layout: {
nodeId,
index: entry.index,
@@ -88,7 +95,43 @@ export function syncNodeSlotLayoutsFromDOM(
}
})
}
if (batch.length) layoutStore.batchUpdateSlotLayouts(batch)
for (const [index, entry] of node.slots.output) {
const rect = entry.el.getBoundingClientRect()
const screenCenter: [number, number] = [
rect.left + rect.width / 2,
rect.top + rect.height / 2
]
const [x, y] = (
conv ?? useSharedCanvasPositionConversion()
).clientPosToCanvasPos(screenCenter)
const centerCanvas = { x, y }
entry.cachedOffset = {
x: centerCanvas.x - nodeLayout.position.x,
y: centerCanvas.y - nodeLayout.position.y
}
const size = LiteGraph.NODE_SLOT_HEIGHT
const half = size / 2
batch.push({
nodeId,
type: 'output',
index,
layout: {
nodeId,
index: entry.index,
type: entry.type,
position: { x: centerCanvas.x, y: centerCanvas.y },
bounds: {
x: centerCanvas.x - half,
y: centerCanvas.y - half,
width: size,
height: size
}
}
})
}
if (batch.length) layoutStore.batchUpdateSlotLayoutsBy(batch)
}
function updateNodeSlotsFromCache(nodeId: string) {
@@ -98,9 +141,14 @@ function updateNodeSlotsFromCache(nodeId: string) {
const nodeLayout = layoutStore.getNodeLayoutRef(nodeId).value
if (!nodeLayout) return
const batch: Array<{ key: string; layout: SlotLayout }> = []
const batch: Array<{
nodeId: string
type: 'input' | 'output'
index: number
layout: SlotLayout
}> = []
for (const [slotKey, entry] of node.slots) {
for (const [index, entry] of node.slots.input) {
if (!entry.cachedOffset) {
// schedule a sync to seed offset
scheduleSlotLayoutSync(nodeId)
@@ -114,7 +162,39 @@ function updateNodeSlotsFromCache(nodeId: string) {
const size = LiteGraph.NODE_SLOT_HEIGHT
const half = size / 2
batch.push({
key: slotKey,
nodeId,
type: 'input',
index,
layout: {
nodeId,
index: entry.index,
type: entry.type,
position: { x: centerCanvas.x, y: centerCanvas.y },
bounds: {
x: centerCanvas.x - half,
y: centerCanvas.y - half,
width: size,
height: size
}
}
})
}
for (const [index, entry] of node.slots.output) {
if (!entry.cachedOffset) {
scheduleSlotLayoutSync(nodeId)
continue
}
const centerCanvas = {
x: nodeLayout.position.x + entry.cachedOffset.x,
y: nodeLayout.position.y + entry.cachedOffset.y
}
const size = LiteGraph.NODE_SLOT_HEIGHT
const half = size / 2
batch.push({
nodeId,
type: 'output',
index,
layout: {
nodeId,
index: entry.index,
@@ -130,7 +210,7 @@ function updateNodeSlotsFromCache(nodeId: string) {
})
}
if (batch.length) layoutStore.batchUpdateSlotLayouts(batch)
if (batch.length) layoutStore.batchUpdateSlotLayoutsBy(batch)
}
export function useSlotElementTracking(options: {
@@ -180,11 +260,14 @@ export function useSlotElementTracking(options: {
}
}
// Register slot
const slotKey = getSlotKey(nodeId, index, type === 'input')
el.dataset.slotKey = slotKey
node.slots.set(slotKey, { el, index, type })
el.dataset.nodeId = nodeId
el.dataset.slotType = type
el.dataset.slotIndex = String(index)
if (type === 'input') {
node.slots.input.set(index, { el, index, type })
} else {
node.slots.output.set(index, { el, index, type })
}
// Seed initial sync from DOM
scheduleSlotLayoutSync(nodeId)
@@ -201,17 +284,18 @@ export function useSlotElementTracking(options: {
const node = nodeSlotRegistryStore.getNode(nodeId)
if (!node) return
// Remove this slot from registry and layout
const slotKey = getSlotKey(nodeId, index, type === 'input')
const entry = node.slots.get(slotKey)
const collection = type === 'input' ? node.slots.input : node.slots.output
const entry = collection.get(index)
if (entry) {
delete entry.el.dataset.slotKey
node.slots.delete(slotKey)
delete entry.el.dataset.nodeId
delete entry.el.dataset.slotType
delete entry.el.dataset.slotIndex
collection.delete(index)
}
layoutStore.deleteSlotLayout(slotKey)
layoutStore.deleteSlotLayoutBy(nodeId, type, index)
// If node has no more slots, clean up
if (node.slots.size === 0) {
if (node.slots.input.size === 0 && node.slots.output.size === 0) {
if (node.stopWatch) node.stopWatch()
nodeSlotRegistryStore.deleteNode(nodeId)
}

View File

@@ -25,9 +25,8 @@ import {
} from '@/renderer/core/canvas/links/linkDropOrchestrator'
import { useSlotLinkDragUIState } from '@/renderer/core/canvas/links/slotLinkDragUIState'
import type { SlotDropCandidate } from '@/renderer/core/canvas/links/slotLinkDragUIState'
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
import type { Point } from '@/renderer/core/layout/types'
import type { Point, SlotIdentity } from '@/renderer/core/layout/types'
import { toPoint } from '@/renderer/core/layout/utils/geometry'
import { createSlotLinkDragContext } from '@/renderer/extensions/vueNodes/composables/slotLinkDragContext'
import { app } from '@/scripts/app'
@@ -94,7 +93,7 @@ export function useSlotLinkInteraction({
endDrag,
updatePointerPosition,
setCandidate,
setCompatibleForKey,
setCompatibleFor,
clearCompatible
} = useSlotLinkDragUIState()
const conversion = useSharedCanvasPositionConversion()
@@ -115,8 +114,11 @@ export function useSlotLinkInteraction({
const nodeId = link.node.id
if (nodeId != null) {
const isInputFrom = link.toType === 'output'
const key = getSlotKey(String(nodeId), link.fromSlotIndex, isInputFrom)
const layout = layoutStore.getSlotLayout(key)
const layout = layoutStore.getSlotLayoutBy(
String(nodeId),
isInputFrom ? 'input' : 'output',
link.fromSlotIndex
)
if (layout) return layout.position
}
@@ -192,8 +194,11 @@ export function useSlotLinkInteraction({
): { position: Point; direction: LinkDirection } | null => {
if (!link) return null
const slotKey = getSlotKey(String(link.origin_id), link.origin_slot, false)
const layout = layoutStore.getSlotLayout(slotKey)
const layout = layoutStore.getSlotLayoutBy(
String(link.origin_id),
'output',
link.origin_slot
)
if (!layout) return null
return { position: { ...layout.position }, direction: LinkDirection.NONE }
@@ -295,28 +300,50 @@ export function useSlotLinkInteraction({
syncRenderLinkOrigins()
let hoveredSlotKey: string | null = null
let hoveredSlotIdentity: SlotIdentity | null = null
let hoveredNodeId: NodeId | null = null
const target = data.target
if (target === dragContext.lastPointerEventTarget) {
hoveredSlotKey = dragContext.lastPointerTargetSlotKey
hoveredSlotIdentity = dragContext.lastPointerTargetSlotIdentity
hoveredNodeId = dragContext.lastPointerTargetNodeId
} else if (target instanceof HTMLElement) {
const elWithSlot = target.closest<HTMLElement>('[data-slot-key]')
const elWithSlot = target.closest<HTMLElement>(
'[data-node-id][data-slot-type][data-slot-index]'
)
const elWithNode = elWithSlot
? null
: target.closest<HTMLElement>('[data-node-id]')
hoveredSlotKey = elWithSlot?.dataset['slotKey'] ?? null
hoveredNodeId = hoveredSlotKey
if (elWithSlot) {
const nodeIdAttr = elWithSlot.dataset['nodeId']
const typeAttr = elWithSlot.dataset['slotType'] as
| 'input'
| 'output'
| undefined
const indexAttr = elWithSlot.dataset['slotIndex']
if (nodeIdAttr && typeAttr && indexAttr != null) {
const indexVal = Number.parseInt(indexAttr, 10)
if (Number.isFinite(indexVal)) {
hoveredSlotIdentity = {
nodeId: nodeIdAttr,
type: typeAttr,
index: indexVal
}
}
}
}
hoveredNodeId = hoveredSlotIdentity
? null
: elWithNode?.dataset['nodeId'] ?? null
dragContext.lastPointerEventTarget = target
dragContext.lastPointerTargetSlotKey = hoveredSlotKey
dragContext.lastPointerTargetSlotIdentity = hoveredSlotIdentity
dragContext.lastPointerTargetNodeId = hoveredNodeId
}
const hoverChanged =
hoveredSlotKey !== dragContext.lastHoverSlotKey ||
hoveredSlotIdentity?.nodeId !==
dragContext.lastHoverSlotIdentity?.nodeId ||
hoveredSlotIdentity?.type !== dragContext.lastHoverSlotIdentity?.type ||
hoveredSlotIdentity?.index !== dragContext.lastHoverSlotIdentity?.index ||
hoveredNodeId !== dragContext.lastHoverNodeId
let candidate: SlotDropCandidate | null = state.candidate
@@ -330,39 +357,43 @@ export function useSlotLinkInteraction({
? null
: resolveNodeSurfaceSlotCandidate(target, context)
candidate = slotCandidate ?? nodeCandidate
dragContext.lastHoverSlotKey = hoveredSlotKey
dragContext.lastHoverSlotIdentity = hoveredSlotIdentity
dragContext.lastHoverNodeId = hoveredNodeId
if (slotCandidate) {
const key = getSlotKey(
setCompatibleFor(
slotCandidate.layout.nodeId,
slotCandidate.layout.type,
slotCandidate.layout.index,
slotCandidate.layout.type === 'input'
!!slotCandidate.compatible
)
setCompatibleForKey(key, !!slotCandidate.compatible)
} else if (nodeCandidate) {
const key = getSlotKey(
setCompatibleFor(
nodeCandidate.layout.nodeId,
nodeCandidate.layout.type,
nodeCandidate.layout.index,
nodeCandidate.layout.type === 'input'
!!nodeCandidate.compatible
)
setCompatibleForKey(key, !!nodeCandidate.compatible)
}
}
const newCandidate = candidate?.compatible ? candidate : null
const newCandidateKey = newCandidate
? getSlotKey(
newCandidate.layout.nodeId,
newCandidate.layout.index,
newCandidate.layout.type === 'input'
)
const newCandidateIdentity: SlotIdentity | null = newCandidate
? {
nodeId: newCandidate.layout.nodeId,
type: newCandidate.layout.type,
index: newCandidate.layout.index
}
: null
const candidateChanged = newCandidateKey !== dragContext.lastCandidateKey
const candidateChanged =
newCandidateIdentity?.nodeId !==
dragContext.lastCandidateIdentity?.nodeId ||
newCandidateIdentity?.type !== dragContext.lastCandidateIdentity?.type ||
newCandidateIdentity?.index !== dragContext.lastCandidateIdentity?.index
if (candidateChanged) {
setCandidate(newCandidate)
dragContext.lastCandidateKey = newCandidateKey
dragContext.lastCandidateIdentity = newCandidateIdentity
}
let snapPosChanged = false
@@ -565,9 +596,7 @@ export function useSlotLinkInteraction({
raf.cancel()
dragContext.reset()
const layout = layoutStore.getSlotLayout(
getSlotKey(nodeId, index, type === 'input')
)
const layout = layoutStore.getSlotLayoutBy(nodeId, type, index)
if (!layout) return
const localNodeId: NodeId = nodeId
@@ -682,18 +711,16 @@ export function useSlotLinkInteraction({
})
)
const targetType: 'input' | 'output' = type === 'input' ? 'output' : 'input'
const allKeys = layoutStore.getAllSlotKeys()
const allSlots = layoutStore.getAllSlots()
clearCompatible()
for (const key of allKeys) {
const slotLayout = layoutStore.getSlotLayout(key)
if (!slotLayout) continue
for (const slotLayout of allSlots) {
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)
setCompatibleFor(slotLayout.nodeId, slotLayout.type, idx, ok)
}
app.canvas?.setDirty(true, true)
event.preventDefault()

View File

@@ -10,7 +10,7 @@ type SlotEntry = {
type NodeEntry = {
nodeId: string
slots: Map<string, SlotEntry>
slots: { input: Map<number, SlotEntry>; output: Map<number, SlotEntry> }
stopWatch?: () => void
}
@@ -26,7 +26,10 @@ export const useNodeSlotRegistryStore = defineStore('nodeSlotRegistry', () => {
if (!node) {
node = {
nodeId,
slots: markRaw(new Map<string, SlotEntry>())
slots: markRaw({
input: new Map<number, SlotEntry>(),
output: new Map<number, SlotEntry>()
})
}
registry.set(nodeId, node)
}