Skip to content
Merged
Changes from 1 commit
Commits
Show all changes
15 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Prev Previous commit
Next Next commit
improvement(workflow): use DOM hit-testing for edge drop-on-block det…
…ection (#3851)
  • Loading branch information
waleedlatif1 authored Mar 30, 2026
commit 4ae5b1b6209b3f3b9a6cb6fc2c0df4156f6c22bb
57 changes: 21 additions & 36 deletions apps/sim/app/workspace/[workspaceId]/w/[workflowId]/workflow.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -263,7 +263,7 @@ const WorkflowContent = React.memo(
const params = useParams()
const router = useRouter()
const reactFlowInstance = useReactFlow()
const { screenToFlowPosition, getNodes, setNodes, getIntersectingNodes } = reactFlowInstance
const { screenToFlowPosition, getNodes, setNodes } = reactFlowInstance
const { fitViewToBounds, getViewportCenter } = useCanvasViewport(reactFlowInstance, {
embedded,
})
Expand Down Expand Up @@ -2849,38 +2849,29 @@ const WorkflowContent = React.memo(
)

/**
* Finds the best node at a given flow position for drop-on-block connection.
* Skips subflow containers as they have their own connection logic.
* Finds the node under the cursor using DOM hit-testing for pixel-perfect
* detection that matches exactly what the user sees on screen.
* Uses the same approach as ReactFlow's internal handle detection.
*/
const findNodeAtPosition = useCallback(
(position: { x: number; y: number }) => {
const cursorRect = {
x: position.x - 1,
y: position.y - 1,
width: 2,
height: 2,
}
const findNodeAtScreenPosition = useCallback(
(clientX: number, clientY: number) => {
const elements = document.elementsFromPoint(clientX, clientY)
const nodes = getNodes()

const intersecting = getIntersectingNodes(cursorRect, true).filter(
(node) => node.type !== 'subflowNode'
)
for (const el of elements) {
const nodeEl = el.closest('.react-flow__node') as HTMLElement | null
if (!nodeEl) continue

if (intersecting.length === 0) return undefined
if (intersecting.length === 1) return intersecting[0]
const nodeId = nodeEl.getAttribute('data-id')
if (!nodeId) continue

return intersecting.reduce((closest, node) => {
const getDistance = (n: Node) => {
const absPos = getNodeAbsolutePosition(n.id)
const dims = getBlockDimensions(n.id)
const centerX = absPos.x + dims.width / 2
const centerY = absPos.y + dims.height / 2
return Math.hypot(position.x - centerX, position.y - centerY)
}
const node = nodes.find((n) => n.id === nodeId)
if (node && node.type !== 'subflowNode') return node
}

return getDistance(node) < getDistance(closest) ? node : closest
})
return undefined
},
[getIntersectingNodes, getNodeAbsolutePosition, getBlockDimensions]
[getNodes]
)

/**
Expand Down Expand Up @@ -3005,15 +2996,9 @@ const WorkflowContent = React.memo(
return
}

// Get cursor position in flow coordinates
// Find node under cursor using DOM hit-testing
const clientPos = 'changedTouches' in event ? event.changedTouches[0] : event
const flowPosition = screenToFlowPosition({
x: clientPos.clientX,
y: clientPos.clientY,
})

// Find node under cursor
const targetNode = findNodeAtPosition(flowPosition)
const targetNode = findNodeAtScreenPosition(clientPos.clientX, clientPos.clientY)

// Create connection if valid target found (handle-to-body case)
if (targetNode && targetNode.id !== source.nodeId) {
Expand All @@ -3027,7 +3012,7 @@ const WorkflowContent = React.memo(

connectionSourceRef.current = null
},
[screenToFlowPosition, findNodeAtPosition, onConnect]
[findNodeAtScreenPosition, onConnect]
)

/** Handles node drag to detect container intersections and update highlighting. */
Expand Down
Loading