mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-02-03 14:54:37 +00:00
Visually snap to node
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user