Visually snap to node

This commit is contained in:
Benjamin Lu
2025-09-25 16:58:55 -07:00
parent 4f6eaea257
commit 76c718e2ee
2 changed files with 81 additions and 20 deletions

View File

@@ -40,7 +40,7 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
if (canvas.linkConnector?.isConnecting) return
if (!state.active || !state.source) return
const { pointer } = state
const { pointer, candidate } = state
const linkRenderer = canvas.linkRenderer
if (!linkRenderer) return
@@ -50,7 +50,9 @@ export function attachSlotLinkPreviewRenderer(canvas: LGraphCanvas) {
const renderLinks = adapter?.renderLinks
if (!adapter || !renderLinks || renderLinks.length === 0) return
const to: ReadOnlyPoint = [pointer.canvas.x, pointer.canvas.y]
const to: ReadOnlyPoint = candidate?.compatible
? [candidate.layout.position.x, candidate.layout.position.y]
: [pointer.canvas.x, pointer.canvas.y]
ctx.save()
for (const link of renderLinks) {
const startDir = link.fromDirection ?? LinkDirection.RIGHT

View File

@@ -87,14 +87,15 @@ export function useSlotLinkInteraction({
}
}
const { state, beginDrag, endDrag, updatePointerPosition } =
const { state, beginDrag, endDrag, updatePointerPosition, setCandidate } =
useSlotLinkDragState()
function candidateFromTarget(
target: EventTarget | null
): SlotDropCandidate | null {
if (!(target instanceof HTMLElement)) return null
const key = target.dataset['slotKey']
const elWithKey = target.closest<HTMLElement>('[data-slot-key]')
const key = elWithKey?.dataset['slotKey']
if (!key) return null
const layout = layoutStore.getSlotLayout(key)
@@ -102,28 +103,70 @@ export function useSlotLinkInteraction({
const candidate: SlotDropCandidate = { layout, compatible: false }
if (state.source) {
const canvas = app.canvas
const graph = canvas?.graph
const adapter = ensureActiveAdapter()
if (graph && adapter) {
if (layout.type === 'input') {
candidate.compatible = adapter.isInputValidDrop(
layout.nodeId,
layout.index
)
} else if (layout.type === 'output') {
candidate.compatible = adapter.isOutputValidDrop(
layout.nodeId,
layout.index
)
}
const graph = app.canvas?.graph
const adapter = ensureActiveAdapter()
if (graph && adapter) {
if (layout.type === 'input') {
candidate.compatible = adapter.isInputValidDrop(
layout.nodeId,
layout.index
)
} else if (layout.type === 'output') {
candidate.compatible = adapter.isOutputValidDrop(
layout.nodeId,
layout.index
)
}
}
return candidate
}
function candidateFromNodeTarget(
target: EventTarget | null
): SlotDropCandidate | null {
if (!(target instanceof HTMLElement)) return null
const elWithNode = target.closest<HTMLElement>('[data-node-id]')
const nodeIdStr = elWithNode?.dataset['nodeId']
if (!nodeIdStr) return null
const adapter = ensureActiveAdapter()
const graph = app.canvas?.graph
if (!adapter || !graph) return null
const nodeId = Number(nodeIdStr)
const node = graph.getNodeById(nodeId)
if (!node) return null
const firstLink = adapter.renderLinks[0]
if (!firstLink) return null
const connectingTo = adapter.linkConnector.state.connectingTo
if (connectingTo === 'input') {
const res = node.findInputByType(firstLink.fromSlot.type)
const index = res?.index
if (index == null) return null
const key = getSlotKey(String(nodeId), index, true)
const layout = layoutStore.getSlotLayout(key)
if (!layout) return null
const compatible = adapter.isInputValidDrop(nodeId, index)
if (!compatible) return null
return { layout, compatible: true }
} else if (connectingTo === 'output') {
const res = node.findOutputByType(firstLink.fromSlot.type)
const index = res?.index
if (index == null) return null
const key = getSlotKey(String(nodeId), index, false)
const layout = layoutStore.getSlotLayout(key)
if (!layout) return null
const compatible = adapter.isOutputValidDrop(nodeId, index)
if (!compatible) return null
return { layout, compatible: true }
}
return null
}
const conversion = useSharedCanvasPositionConversion()
const pointerSession = createPointerSession()
@@ -359,6 +402,22 @@ export function useSlotLinkInteraction({
const handlePointerMove = (event: PointerEvent) => {
if (!pointerSession.matches(event)) return
updatePointerState(event)
const adapter = ensureActiveAdapter()
// Resolve a candidate from slot under cursor, else from node
const slotCandidate = candidateFromTarget(event.target)
const nodeCandidate = slotCandidate
? null
: candidateFromNodeTarget(event.target)
const candidate = slotCandidate ?? nodeCandidate
// Update drag-state candidate; Vue preview renderer reads this
if (candidate?.compatible && adapter) {
setCandidate(candidate)
} else {
setCandidate(null)
}
app.canvas?.setDirty(true)
// Debug: Log hovered slot/node IDs using event.target.dataset for review