mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 06:47:33 +00:00
Refactor slotkey from bespoke string key
This commit is contained in:
@@ -1,7 +1,6 @@
|
||||
import type { Locator, Page } from '@playwright/test'
|
||||
|
||||
import type { NodeId } from '../../../../../src/platform/workflow/validation/schemas/workflowSchema'
|
||||
import { getSlotKey } from '../../../../../src/renderer/core/layout/slots/slotIdentifier'
|
||||
import {
|
||||
comfyExpect as expect,
|
||||
comfyPageFixture as test
|
||||
@@ -67,8 +66,11 @@ function slotLocator(
|
||||
slotIndex: number,
|
||||
isInput: boolean
|
||||
) {
|
||||
const key = getSlotKey(String(nodeId), slotIndex, isInput)
|
||||
return page.locator(`[data-slot-key="${key}"]`)
|
||||
const type = isInput ? 'input' : 'output'
|
||||
const id = String(nodeId)
|
||||
return page.locator(
|
||||
`[data-node-id="${id}"][data-slot-type="${type}"][data-slot-index="${slotIndex}"]`
|
||||
)
|
||||
}
|
||||
|
||||
async function expectVisibleAll(...locators: Locator[]) {
|
||||
|
||||
@@ -3,7 +3,6 @@ import type { NodeId } from '@/lib/litegraph/src/LGraphNode'
|
||||
import type { LinkConnectorAdapter } from '@/renderer/core/canvas/links/linkConnectorAdapter'
|
||||
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 { SlotLinkDragContext } from '@/renderer/extensions/vueNodes/composables/slotLinkDragContext'
|
||||
|
||||
@@ -17,30 +16,39 @@ export const resolveSlotTargetCandidate = (
|
||||
target: EventTarget | null,
|
||||
{ adapter, graph }: DropResolutionContext
|
||||
): SlotDropCandidate | null => {
|
||||
const { state: dragState, setCompatibleForKey } = useSlotLinkDragUIState()
|
||||
const { setCompatibleFor, getCompatible } = useSlotLinkDragUIState()
|
||||
if (!(target instanceof HTMLElement)) return null
|
||||
|
||||
const elWithKey = target.closest<HTMLElement>('[data-slot-key]')
|
||||
const key = elWithKey?.dataset['slotKey']
|
||||
if (!key) return null
|
||||
const elWithSlot = target.closest<HTMLElement>(
|
||||
'[data-node-id][data-slot-type][data-slot-index]'
|
||||
)
|
||||
const nodeId = elWithSlot?.dataset['nodeId']
|
||||
const typeAttr = elWithSlot?.dataset['slotType'] as
|
||||
| 'input'
|
||||
| 'output'
|
||||
| undefined
|
||||
const indexAttr = elWithSlot?.dataset['slotIndex']
|
||||
if (!nodeId || !typeAttr || indexAttr == null) return null
|
||||
const index = Number.parseInt(indexAttr, 10)
|
||||
if (!Number.isFinite(index)) return null
|
||||
|
||||
const layout = layoutStore.getSlotLayout(key)
|
||||
const layout = layoutStore.getSlotLayoutBy(nodeId, typeAttr, index)
|
||||
if (!layout) return null
|
||||
|
||||
const candidate: SlotDropCandidate = { layout, compatible: false }
|
||||
|
||||
if (adapter && graph) {
|
||||
const cached = dragState.compatible.get(key)
|
||||
const cached = getCompatible(nodeId, layout.type, layout.index)
|
||||
if (cached != null) {
|
||||
candidate.compatible = cached
|
||||
} else {
|
||||
const nodeId: NodeId = layout.nodeId
|
||||
const layoutNodeId: NodeId = layout.nodeId
|
||||
const compatible =
|
||||
layout.type === 'input'
|
||||
? adapter.isInputValidDrop(nodeId, layout.index)
|
||||
: adapter.isOutputValidDrop(nodeId, layout.index)
|
||||
? adapter.isInputValidDrop(layoutNodeId, layout.index)
|
||||
: adapter.isOutputValidDrop(layoutNodeId, layout.index)
|
||||
|
||||
setCompatibleForKey(key, compatible)
|
||||
setCompatibleFor(layoutNodeId, layout.type, layout.index, compatible)
|
||||
candidate.compatible = compatible
|
||||
}
|
||||
}
|
||||
@@ -52,7 +60,7 @@ export const resolveNodeSurfaceSlotCandidate = (
|
||||
target: EventTarget | null,
|
||||
{ adapter, graph, session }: DropResolutionContext
|
||||
): SlotDropCandidate | null => {
|
||||
const { setCompatibleForKey } = useSlotLinkDragUIState()
|
||||
const { setCompatibleFor } = useSlotLinkDragUIState()
|
||||
if (!(target instanceof HTMLElement)) return null
|
||||
|
||||
const elWithNode = target.closest<HTMLElement>('[data-node-id]')
|
||||
@@ -92,8 +100,11 @@ export const resolveNodeSurfaceSlotCandidate = (
|
||||
return null
|
||||
}
|
||||
|
||||
const key = getSlotKey(String(nodeId), index, isInput)
|
||||
const layout = layoutStore.getSlotLayout(key)
|
||||
const layout = layoutStore.getSlotLayoutBy(
|
||||
String(nodeId),
|
||||
isInput ? 'input' : 'output',
|
||||
index
|
||||
)
|
||||
if (!layout) {
|
||||
session.preferredSlotForNode.set(nodeId, null)
|
||||
return null
|
||||
@@ -103,14 +114,18 @@ export const resolveNodeSurfaceSlotCandidate = (
|
||||
? adapter.isInputValidDrop(nodeId, index)
|
||||
: adapter.isOutputValidDrop(nodeId, index)
|
||||
|
||||
setCompatibleForKey(key, compatible)
|
||||
setCompatibleFor(layout.nodeId, layout.type, layout.index, compatible)
|
||||
|
||||
if (!compatible) {
|
||||
session.preferredSlotForNode.set(nodeId, null)
|
||||
return null
|
||||
}
|
||||
|
||||
const preferred = { index, key, layout }
|
||||
const preferred = {
|
||||
index,
|
||||
identity: { nodeId: layout.nodeId, type: layout.type, index },
|
||||
layout
|
||||
}
|
||||
session.preferredSlotForNode.set(nodeId, preferred)
|
||||
|
||||
return { layout, compatible: true }
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { reactive, readonly } from 'vue'
|
||||
|
||||
import type { LinkDirection } from '@/lib/litegraph/src/types/globalEnums'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { Point, SlotLayout } from '@/renderer/core/layout/types'
|
||||
|
||||
@@ -41,7 +40,10 @@ interface SlotDragState {
|
||||
source: SlotDragSource | null
|
||||
pointer: PointerPosition
|
||||
candidate: SlotDropCandidate | null
|
||||
compatible: Map<string, boolean>
|
||||
compatible: Map<
|
||||
string,
|
||||
{ input: Map<number, boolean>; output: Map<number, boolean> }
|
||||
>
|
||||
}
|
||||
|
||||
const state = reactive<SlotDragState>({
|
||||
@@ -53,7 +55,7 @@ const state = reactive<SlotDragState>({
|
||||
canvas: { x: 0, y: 0 }
|
||||
},
|
||||
candidate: null,
|
||||
compatible: new Map<string, boolean>()
|
||||
compatible: new Map()
|
||||
})
|
||||
|
||||
function updatePointerPosition(
|
||||
@@ -93,8 +95,45 @@ function endDrag() {
|
||||
}
|
||||
|
||||
function getSlotLayout(nodeId: string, slotIndex: number, isInput: boolean) {
|
||||
const slotKey = getSlotKey(nodeId, slotIndex, isInput)
|
||||
return layoutStore.getSlotLayout(slotKey)
|
||||
return layoutStore.getSlotLayoutBy(
|
||||
nodeId,
|
||||
isInput ? 'input' : 'output',
|
||||
slotIndex
|
||||
)
|
||||
}
|
||||
|
||||
function ensureCompatibleNode(nodeId: string) {
|
||||
let entry = state.compatible.get(nodeId)
|
||||
if (!entry) {
|
||||
entry = {
|
||||
input: reactive(new Map<number, boolean>()),
|
||||
output: reactive(new Map<number, boolean>())
|
||||
}
|
||||
state.compatible.set(nodeId, entry)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
function getCompatible(
|
||||
nodeId: string,
|
||||
type: 'input' | 'output',
|
||||
index: number
|
||||
): boolean | undefined {
|
||||
const nodeEntry = state.compatible.get(nodeId)
|
||||
if (!nodeEntry) return undefined
|
||||
const map = type === 'input' ? nodeEntry.input : nodeEntry.output
|
||||
return map.get(index)
|
||||
}
|
||||
|
||||
function setCompatibleFor(
|
||||
nodeId: string,
|
||||
type: 'input' | 'output',
|
||||
index: number,
|
||||
value: boolean
|
||||
) {
|
||||
const nodeEntry = ensureCompatibleNode(nodeId)
|
||||
const map = type === 'input' ? nodeEntry.input : nodeEntry.output
|
||||
map.set(index, value)
|
||||
}
|
||||
|
||||
export function useSlotLinkDragUIState() {
|
||||
@@ -105,13 +144,8 @@ export function useSlotLinkDragUIState() {
|
||||
updatePointerPosition,
|
||||
setCandidate,
|
||||
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)
|
||||
},
|
||||
getCompatible,
|
||||
setCompatibleFor,
|
||||
clearCompatible: () => state.compatible.clear()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@ import type {
|
||||
} from '@/lib/litegraph/src/interfaces'
|
||||
import { LiteGraph } from '@/lib/litegraph/src/litegraph'
|
||||
import { isWidgetInputSlot } from '@/lib/litegraph/src/node/slotUtils'
|
||||
import { getSlotKey } from '@/renderer/core/layout/slots/slotIdentifier'
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
|
||||
export interface SlotPositionContext {
|
||||
@@ -139,8 +138,11 @@ export function getSlotPosition(
|
||||
isInput: boolean
|
||||
): Point {
|
||||
// Try to get precise position from slot layout (DOM-registered)
|
||||
const slotKey = getSlotKey(String(node.id), slotIndex, isInput)
|
||||
const slotLayout = layoutStore.getSlotLayout(slotKey)
|
||||
const slotLayout = layoutStore.getSlotLayoutBy(
|
||||
String(node.id),
|
||||
isInput ? 'input' : 'output',
|
||||
slotIndex
|
||||
)
|
||||
if (slotLayout) {
|
||||
return [slotLayout.position.x, slotLayout.position.y]
|
||||
}
|
||||
|
||||
@@ -15,8 +15,6 @@ import type { SlotPositionContext } from '@/renderer/core/canvas/litegraph/slotC
|
||||
import { layoutStore } from '@/renderer/core/layout/store/layoutStore'
|
||||
import type { SlotLayout } from '@/renderer/core/layout/types'
|
||||
|
||||
import { getSlotKey } from './slotIdentifier'
|
||||
|
||||
/**
|
||||
* Register slot layout with the layout store for hit testing
|
||||
* @param nodeId The node ID
|
||||
@@ -30,8 +28,6 @@ function registerSlotLayout(
|
||||
isInput: boolean,
|
||||
position: Point
|
||||
): void {
|
||||
const slotKey = getSlotKey(nodeId, slotIndex, isInput)
|
||||
|
||||
// Calculate bounds for the slot using LiteGraph's standard slot height
|
||||
const slotSize = LiteGraph.NODE_SLOT_HEIGHT
|
||||
const halfSize = slotSize / 2
|
||||
@@ -49,7 +45,12 @@ function registerSlotLayout(
|
||||
}
|
||||
}
|
||||
|
||||
layoutStore.updateSlotLayout(slotKey, slotLayout)
|
||||
layoutStore.updateSlotLayoutBy(
|
||||
nodeId,
|
||||
isInput ? 'input' : 'output',
|
||||
slotIndex,
|
||||
slotLayout
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,40 +0,0 @@
|
||||
/**
|
||||
* Slot identifier utilities for consistent slot key generation and parsing
|
||||
*
|
||||
* Provides a centralized interface for slot identification across the layout system
|
||||
*
|
||||
* @TODO Replace this concatenated string with root cause fix
|
||||
*/
|
||||
|
||||
interface SlotIdentifier {
|
||||
nodeId: string
|
||||
index: number
|
||||
isInput: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate a unique key for a slot
|
||||
* Format: "{nodeId}-{in|out}-{index}"
|
||||
*/
|
||||
export function getSlotKey(identifier: SlotIdentifier): string
|
||||
export function getSlotKey(
|
||||
nodeId: string,
|
||||
index: number,
|
||||
isInput: boolean
|
||||
): string
|
||||
export function getSlotKey(
|
||||
nodeIdOrIdentifier: string | SlotIdentifier,
|
||||
index?: number,
|
||||
isInput?: boolean
|
||||
): string {
|
||||
if (typeof nodeIdOrIdentifier === 'object') {
|
||||
const { nodeId, index, isInput } = nodeIdOrIdentifier
|
||||
return `${nodeId}-${isInput ? 'in' : 'out'}-${index}`
|
||||
}
|
||||
|
||||
if (index === undefined || isInput === undefined) {
|
||||
throw new Error('Missing required parameters for slot key generation')
|
||||
}
|
||||
|
||||
return `${nodeIdOrIdentifier}-${isInput ? 'in' : 'out'}-${index}`
|
||||
}
|
||||
@@ -35,6 +35,7 @@ import type {
|
||||
RerouteLayout,
|
||||
ResizeNodeOperation,
|
||||
SetNodeZIndexOperation,
|
||||
SlotIdentity,
|
||||
SlotLayout
|
||||
} from '@/renderer/core/layout/types'
|
||||
import {
|
||||
@@ -124,7 +125,11 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
// New data structures for hit testing
|
||||
private linkLayouts = new Map<LinkId, LinkLayout>()
|
||||
private linkSegmentLayouts = new Map<string, LinkSegmentLayout>() // Internal string key: ${linkId}:${rerouteId ?? 'final'}
|
||||
private slotLayouts = new Map<string, SlotLayout>()
|
||||
private slotLayoutsByNode = new Map<
|
||||
NodeId,
|
||||
{ input: Map<number, SlotLayout>; output: Map<number, SlotLayout> }
|
||||
>()
|
||||
private slotIndexKeyToIdentity = new Map<string, SlotIdentity>()
|
||||
private rerouteLayouts = new Map<RerouteId, RerouteLayout>()
|
||||
|
||||
// Spatial index managers
|
||||
@@ -431,66 +436,82 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update slot layout data
|
||||
*/
|
||||
updateSlotLayout(key: string, layout: SlotLayout): void {
|
||||
const existing = this.slotLayouts.get(key)
|
||||
private makeSlotIndexKey(
|
||||
nodeId: NodeId,
|
||||
type: 'input' | 'output',
|
||||
index: number
|
||||
): string {
|
||||
return `${nodeId}::${type}::${index}`
|
||||
}
|
||||
|
||||
private getOrCreateNodeSlotMaps(nodeId: NodeId) {
|
||||
let entry = this.slotLayoutsByNode.get(nodeId)
|
||||
if (!entry) {
|
||||
entry = { input: new Map(), output: new Map() }
|
||||
this.slotLayoutsByNode.set(nodeId, entry)
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
updateSlotLayoutBy(
|
||||
nodeId: NodeId,
|
||||
type: 'input' | 'output',
|
||||
index: number,
|
||||
layout: SlotLayout
|
||||
): void {
|
||||
const indexKey = this.makeSlotIndexKey(nodeId, type, index)
|
||||
|
||||
const nodeMaps = this.getOrCreateNodeSlotMaps(nodeId)
|
||||
const map = type === 'input' ? nodeMaps.input : nodeMaps.output
|
||||
const existing = map.get(index)
|
||||
|
||||
if (existing) {
|
||||
// Short-circuit if geometry is unchanged
|
||||
if (
|
||||
isPointEqual(existing.position, layout.position) &&
|
||||
isBoundsEqual(existing.bounds, layout.bounds)
|
||||
) {
|
||||
return
|
||||
}
|
||||
// Update spatial index
|
||||
this.slotSpatialIndex.update(key, layout.bounds)
|
||||
this.slotSpatialIndex.update(indexKey, layout.bounds)
|
||||
} else {
|
||||
// Insert into spatial index
|
||||
this.slotSpatialIndex.insert(key, layout.bounds)
|
||||
this.slotSpatialIndex.insert(indexKey, layout.bounds)
|
||||
}
|
||||
|
||||
this.slotLayouts.set(key, layout)
|
||||
map.set(index, layout)
|
||||
this.slotIndexKeyToIdentity.set(indexKey, { nodeId, type, index })
|
||||
}
|
||||
|
||||
/**
|
||||
* Batch update slot layouts and spatial index in one pass
|
||||
*/
|
||||
batchUpdateSlotLayouts(
|
||||
updates: Array<{ key: string; layout: SlotLayout }>
|
||||
|
||||
batchUpdateSlotLayoutsBy(
|
||||
updates: Array<{
|
||||
nodeId: NodeId
|
||||
type: 'input' | 'output'
|
||||
index: number
|
||||
layout: SlotLayout
|
||||
}>
|
||||
): void {
|
||||
if (!updates.length) return
|
||||
|
||||
// Update spatial index and map entries (skip unchanged)
|
||||
for (const { key, layout } of updates) {
|
||||
const existing = this.slotLayouts.get(key)
|
||||
|
||||
if (existing) {
|
||||
// Short-circuit if geometry is unchanged
|
||||
if (
|
||||
isPointEqual(existing.position, layout.position) &&
|
||||
isBoundsEqual(existing.bounds, layout.bounds)
|
||||
) {
|
||||
continue
|
||||
}
|
||||
this.slotSpatialIndex.update(key, layout.bounds)
|
||||
} else {
|
||||
this.slotSpatialIndex.insert(key, layout.bounds)
|
||||
}
|
||||
this.slotLayouts.set(key, layout)
|
||||
for (const { nodeId, type, index, layout } of updates) {
|
||||
this.updateSlotLayoutBy(nodeId, type, index, layout)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete slot layout data
|
||||
*/
|
||||
deleteSlotLayout(key: string): void {
|
||||
const deleted = this.slotLayouts.delete(key)
|
||||
deleteSlotLayoutBy(
|
||||
nodeId: NodeId,
|
||||
type: 'input' | 'output',
|
||||
index: number
|
||||
): void {
|
||||
const indexKey = this.makeSlotIndexKey(nodeId, type, index)
|
||||
const nodeMaps = this.slotLayoutsByNode.get(nodeId)
|
||||
if (!nodeMaps) return
|
||||
const map = type === 'input' ? nodeMaps.input : nodeMaps.output
|
||||
const deleted = map.delete(index)
|
||||
if (deleted) {
|
||||
// Remove from spatial index
|
||||
this.slotSpatialIndex.remove(key)
|
||||
this.slotSpatialIndex.remove(indexKey)
|
||||
this.slotIndexKeyToIdentity.delete(indexKey)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -498,17 +519,21 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
* Delete all slot layouts for a node
|
||||
*/
|
||||
deleteNodeSlotLayouts(nodeId: NodeId): void {
|
||||
const keysToDelete: string[] = []
|
||||
for (const [key, layout] of this.slotLayouts) {
|
||||
if (layout.nodeId === nodeId) {
|
||||
keysToDelete.push(key)
|
||||
}
|
||||
const nodeMaps = this.slotLayoutsByNode.get(nodeId)
|
||||
if (!nodeMaps) return
|
||||
for (const [idx] of nodeMaps.input) {
|
||||
const indexKey = this.makeSlotIndexKey(nodeId, 'input', idx)
|
||||
this.slotSpatialIndex.remove(indexKey)
|
||||
this.slotIndexKeyToIdentity.delete(indexKey)
|
||||
}
|
||||
for (const key of keysToDelete) {
|
||||
this.slotLayouts.delete(key)
|
||||
// Remove from spatial index
|
||||
this.slotSpatialIndex.remove(key)
|
||||
for (const [idx] of nodeMaps.output) {
|
||||
const indexKey = this.makeSlotIndexKey(nodeId, 'output', idx)
|
||||
this.slotSpatialIndex.remove(indexKey)
|
||||
this.slotIndexKeyToIdentity.delete(indexKey)
|
||||
}
|
||||
nodeMaps.input.clear()
|
||||
nodeMaps.output.clear()
|
||||
this.slotLayoutsByNode.delete(nodeId)
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -516,7 +541,8 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
* Used when switching rendering modes (Vue ↔ LiteGraph)
|
||||
*/
|
||||
clearAllSlotLayouts(): void {
|
||||
this.slotLayouts.clear()
|
||||
this.slotLayoutsByNode.clear()
|
||||
this.slotIndexKeyToIdentity.clear()
|
||||
this.slotSpatialIndex.clear()
|
||||
}
|
||||
|
||||
@@ -563,11 +589,15 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
return this.linkLayouts.get(linkId) || null
|
||||
}
|
||||
|
||||
/**
|
||||
* Get slot layout data
|
||||
*/
|
||||
getSlotLayout(key: string): SlotLayout | null {
|
||||
return this.slotLayouts.get(key) || null
|
||||
getSlotLayoutBy(
|
||||
nodeId: NodeId,
|
||||
type: 'input' | 'output',
|
||||
index: number
|
||||
): SlotLayout | null {
|
||||
const nodeMaps = this.slotLayoutsByNode.get(nodeId)
|
||||
if (!nodeMaps) return null
|
||||
const map = type === 'input' ? nodeMaps.input : nodeMaps.output
|
||||
return map.get(index) || null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -581,8 +611,13 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
* 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())
|
||||
getAllSlots(): ReadonlyArray<SlotLayout> {
|
||||
const result: SlotLayout[] = []
|
||||
for (const [, maps] of this.slotLayoutsByNode) {
|
||||
for (const [, layout] of maps.input) result.push(layout)
|
||||
for (const [, layout] of maps.output) result.push(layout)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -740,9 +775,14 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
const candidateSlotKeys = this.slotSpatialIndex.query(searchArea)
|
||||
|
||||
// Check precise bounds for candidates
|
||||
for (const key of candidateSlotKeys) {
|
||||
const slotLayout = this.slotLayouts.get(key)
|
||||
const identity = this.slotIndexKeyToIdentity.get(key)
|
||||
if (!identity) continue
|
||||
const slotLayout = this.getSlotLayoutBy(
|
||||
identity.nodeId,
|
||||
identity.type,
|
||||
identity.index
|
||||
)
|
||||
if (slotLayout && pointInBounds(point, slotLayout.bounds)) {
|
||||
return slotLayout
|
||||
}
|
||||
@@ -799,7 +839,7 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
queryItemsInBounds(bounds: Bounds): {
|
||||
nodes: NodeId[]
|
||||
links: LinkId[]
|
||||
slots: string[]
|
||||
slots: SlotIdentity[]
|
||||
reroutes: RerouteId[]
|
||||
} {
|
||||
// Query segments and union their linkIds
|
||||
@@ -812,10 +852,15 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
}
|
||||
}
|
||||
|
||||
const slotIdentities: SlotIdentity[] = this.slotSpatialIndex
|
||||
.query(bounds)
|
||||
.map((key) => this.slotIndexKeyToIdentity.get(key))
|
||||
.filter((v): v is SlotIdentity => !!v)
|
||||
|
||||
return {
|
||||
nodes: this.queryNodesInBounds(bounds),
|
||||
links: Array.from(linkIds),
|
||||
slots: this.slotSpatialIndex.query(bounds),
|
||||
slots: slotIdentities,
|
||||
reroutes: this.rerouteSpatialIndex
|
||||
.query(bounds)
|
||||
.map((key) => asRerouteId(key))
|
||||
@@ -958,10 +1003,12 @@ class LayoutStoreImpl implements LayoutStore {
|
||||
this.spatialIndex.clear()
|
||||
this.linkSegmentSpatialIndex.clear()
|
||||
this.slotSpatialIndex.clear()
|
||||
// Clear slot identity maps to avoid stale entries when re-initializing
|
||||
this.slotLayoutsByNode.clear()
|
||||
this.slotIndexKeyToIdentity.clear()
|
||||
this.rerouteSpatialIndex.clear()
|
||||
this.linkLayouts.clear()
|
||||
this.linkSegmentLayouts.clear()
|
||||
this.slotLayouts.clear()
|
||||
this.rerouteLayouts.clear()
|
||||
|
||||
nodes.forEach((node, index) => {
|
||||
|
||||
@@ -59,6 +59,12 @@ export interface SlotLayout {
|
||||
bounds: Bounds
|
||||
}
|
||||
|
||||
export interface SlotIdentity {
|
||||
nodeId: NodeId
|
||||
type: 'input' | 'output'
|
||||
index: number
|
||||
}
|
||||
|
||||
export interface LinkLayout {
|
||||
id: LinkId
|
||||
path: Path2D
|
||||
@@ -282,7 +288,7 @@ export interface LayoutStore {
|
||||
queryItemsInBounds(bounds: Bounds): {
|
||||
nodes: NodeId[]
|
||||
links: LinkId[]
|
||||
slots: string[]
|
||||
slots: SlotIdentity[]
|
||||
reroutes: RerouteId[]
|
||||
}
|
||||
|
||||
@@ -293,24 +299,37 @@ export interface LayoutStore {
|
||||
rerouteId: RerouteId | null,
|
||||
layout: Omit<LinkSegmentLayout, 'linkId' | 'rerouteId'>
|
||||
): void
|
||||
updateSlotLayout(key: string, layout: SlotLayout): void
|
||||
updateSlotLayoutBy(
|
||||
nodeId: NodeId,
|
||||
type: 'input' | 'output',
|
||||
index: number,
|
||||
layout: SlotLayout
|
||||
): void
|
||||
updateRerouteLayout(rerouteId: RerouteId, layout: RerouteLayout): void
|
||||
|
||||
// Delete methods for cleanup
|
||||
deleteLinkLayout(linkId: LinkId): void
|
||||
deleteLinkSegmentLayout(linkId: LinkId, rerouteId: RerouteId | null): void
|
||||
deleteSlotLayout(key: string): void
|
||||
deleteSlotLayoutBy(
|
||||
nodeId: NodeId,
|
||||
type: 'input' | 'output',
|
||||
index: number
|
||||
): void
|
||||
deleteNodeSlotLayouts(nodeId: NodeId): void
|
||||
deleteRerouteLayout(rerouteId: RerouteId): void
|
||||
clearAllSlotLayouts(): void
|
||||
|
||||
// Get layout data
|
||||
getLinkLayout(linkId: LinkId): LinkLayout | null
|
||||
getSlotLayout(key: string): SlotLayout | null
|
||||
getSlotLayoutBy(
|
||||
nodeId: NodeId,
|
||||
type: 'input' | 'output',
|
||||
index: number
|
||||
): SlotLayout | null
|
||||
getRerouteLayout(rerouteId: RerouteId): RerouteLayout | null
|
||||
|
||||
// Returns all slot layout keys currently tracked by the store
|
||||
getAllSlotKeys(): string[]
|
||||
// Returns all slot layouts currently tracked by the store
|
||||
getAllSlots(): ReadonlyArray<SlotLayout>
|
||||
|
||||
// Direct mutation API (CRDT-ready)
|
||||
applyOperation(operation: LayoutOperation): void
|
||||
@@ -334,7 +353,12 @@ export interface LayoutStore {
|
||||
updates: Array<{ nodeId: NodeId; bounds: Bounds }>
|
||||
): void
|
||||
|
||||
batchUpdateSlotLayouts(
|
||||
updates: Array<{ key: string; layout: SlotLayout }>
|
||||
batchUpdateSlotLayoutsBy(
|
||||
updates: Array<{
|
||||
nodeId: NodeId
|
||||
type: 'input' | 'output'
|
||||
index: number
|
||||
layout: SlotLayout
|
||||
}>
|
||||
): void
|
||||
}
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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(() =>
|
||||
|
||||
@@ -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: () => {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user