fix: correct entry node alignment for wrapper offset

- Add ENTRY_NODE_WRAPPER_OFFSET constant (x: 0, y: 21) for Start/Trigger nodes
- Implement getNodeAlignPosition() to calculate actual inner node positions
- Fix horizontal/vertical helpline rendering to account for wrapper offset
- Fix snap-to-align logic to properly align inner nodes instead of wrapper
- Correct helpline width/height calculation by subtracting offset for entry nodes
- Ensure backward compatibility: only affects Start/Trigger nodes with EntryNodeContainer wrapper

This fix ensures that Start and Trigger nodes (which have an EntryNodeContainer wrapper
with status indicator) align based on their inner node boundaries rather than the wrapper
boundaries, matching the alignment behavior of regular nodes.
This commit is contained in:
lyzno1 2025-09-30 18:36:49 +08:00
parent bea11b08d7
commit 7c97ea4a9e
2 changed files with 142 additions and 36 deletions

View File

@ -1,12 +1,40 @@
import { useCallback } from 'react'
import { useStoreApi } from 'reactflow'
import type { Node } from '../types'
import { BlockEnum, TRIGGER_NODE_TYPES } from '../types'
import { useWorkflowStore } from '../store'
// Entry node (Start/Trigger) wrapper offsets
// The EntryNodeContainer adds a wrapper with status indicator above the actual node
// These offsets ensure alignment happens on the inner node, not the wrapper
const ENTRY_NODE_WRAPPER_OFFSET = {
x: 0, // No horizontal padding on wrapper (px-0)
y: 21, // Actual measured: pt-0.5 (2px) + status bar height (~19px)
} as const
export const useHelpline = () => {
const store = useStoreApi()
const workflowStore = useWorkflowStore()
// Check if a node is an entry node (Start or Trigger)
const isEntryNode = useCallback((node: Node): boolean => {
return TRIGGER_NODE_TYPES.includes(node.data.type as any) || node.data.type === BlockEnum.Start
}, [])
// Get the actual alignment position of a node (accounting for wrapper offset)
const getNodeAlignPosition = useCallback((node: Node) => {
if (isEntryNode(node)) {
return {
x: node.position.x + ENTRY_NODE_WRAPPER_OFFSET.x,
y: node.position.y + ENTRY_NODE_WRAPPER_OFFSET.y,
}
}
return {
x: node.position.x,
y: node.position.y,
}
}, [isEntryNode])
const handleSetHelpline = useCallback((node: Node) => {
const { getNodes } = store.getState()
const nodes = getNodes()
@ -29,6 +57,9 @@ export const useHelpline = () => {
}
}
// Get the actual alignment position for the dragging node
const nodeAlignPos = getNodeAlignPosition(node)
const showHorizontalHelpLineNodes = nodes.filter((n) => {
if (n.id === node.id)
return false
@ -39,33 +70,52 @@ export const useHelpline = () => {
if (n.data.isInLoop)
return false
const nY = Math.ceil(n.position.y)
const nodeY = Math.ceil(node.position.y)
// Get actual alignment position for comparison node
const nAlignPos = getNodeAlignPosition(n)
const nY = Math.ceil(nAlignPos.y)
const nodeY = Math.ceil(nodeAlignPos.y)
if (nY - nodeY < 5 && nY - nodeY > -5)
return true
return false
}).sort((a, b) => a.position.x - b.position.x)
}).sort((a, b) => {
const aPos = getNodeAlignPosition(a)
const bPos = getNodeAlignPosition(b)
return aPos.x - bPos.x
})
const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
if (showHorizontalHelpLineNodesLength > 0) {
const first = showHorizontalHelpLineNodes[0]
const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1]
// Use actual alignment positions for help line rendering
const firstPos = getNodeAlignPosition(first)
const lastPos = getNodeAlignPosition(last)
// For entry nodes, we need to subtract the offset from width since lastPos already includes it
const lastIsEntryNode = isEntryNode(last)
const lastNodeWidth = lastIsEntryNode ? last.width! - ENTRY_NODE_WRAPPER_OFFSET.x : last.width!
const helpLine = {
top: first.position.y,
left: first.position.x,
width: last.position.x + last.width! - first.position.x,
top: firstPos.y,
left: firstPos.x,
width: lastPos.x + lastNodeWidth - firstPos.x,
}
if (node.position.x < first.position.x) {
helpLine.left = node.position.x
helpLine.width = first.position.x + first.width! - node.position.x
if (nodeAlignPos.x < firstPos.x) {
const firstIsEntryNode = isEntryNode(first)
const firstNodeWidth = firstIsEntryNode ? first.width! - ENTRY_NODE_WRAPPER_OFFSET.x : first.width!
helpLine.left = nodeAlignPos.x
helpLine.width = firstPos.x + firstNodeWidth - nodeAlignPos.x
}
if (node.position.x > last.position.x)
helpLine.width = node.position.x + node.width! - first.position.x
if (nodeAlignPos.x > lastPos.x) {
const nodeIsEntryNode = isEntryNode(node)
const nodeWidth = nodeIsEntryNode ? node.width! - ENTRY_NODE_WRAPPER_OFFSET.x : node.width!
helpLine.width = nodeAlignPos.x + nodeWidth - firstPos.x
}
setHelpLineHorizontal(helpLine)
}
@ -81,33 +131,52 @@ export const useHelpline = () => {
if (n.data.isInLoop)
return false
const nX = Math.ceil(n.position.x)
const nodeX = Math.ceil(node.position.x)
// Get actual alignment position for comparison node
const nAlignPos = getNodeAlignPosition(n)
const nX = Math.ceil(nAlignPos.x)
const nodeX = Math.ceil(nodeAlignPos.x)
if (nX - nodeX < 5 && nX - nodeX > -5)
return true
return false
}).sort((a, b) => a.position.x - b.position.x)
}).sort((a, b) => {
const aPos = getNodeAlignPosition(a)
const bPos = getNodeAlignPosition(b)
return aPos.x - bPos.x
})
const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
if (showVerticalHelpLineNodesLength > 0) {
const first = showVerticalHelpLineNodes[0]
const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1]
// Use actual alignment positions for help line rendering
const firstPos = getNodeAlignPosition(first)
const lastPos = getNodeAlignPosition(last)
// For entry nodes, we need to subtract the offset from height since lastPos already includes it
const lastIsEntryNode = isEntryNode(last)
const lastNodeHeight = lastIsEntryNode ? last.height! - ENTRY_NODE_WRAPPER_OFFSET.y : last.height!
const helpLine = {
top: first.position.y,
left: first.position.x,
height: last.position.y + last.height! - first.position.y,
top: firstPos.y,
left: firstPos.x,
height: lastPos.y + lastNodeHeight - firstPos.y,
}
if (node.position.y < first.position.y) {
helpLine.top = node.position.y
helpLine.height = first.position.y + first.height! - node.position.y
if (nodeAlignPos.y < firstPos.y) {
const firstIsEntryNode = isEntryNode(first)
const firstNodeHeight = firstIsEntryNode ? first.height! - ENTRY_NODE_WRAPPER_OFFSET.y : first.height!
helpLine.top = nodeAlignPos.y
helpLine.height = firstPos.y + firstNodeHeight - nodeAlignPos.y
}
if (node.position.y > last.position.y)
helpLine.height = node.position.y + node.height! - first.position.y
if (nodeAlignPos.y > lastPos.y) {
const nodeIsEntryNode = isEntryNode(node)
const nodeHeight = nodeIsEntryNode ? node.height! - ENTRY_NODE_WRAPPER_OFFSET.y : node.height!
helpLine.height = nodeAlignPos.y + nodeHeight - firstPos.y
}
setHelpLineVertical(helpLine)
}
@ -119,7 +188,7 @@ export const useHelpline = () => {
showHorizontalHelpLineNodes,
showVerticalHelpLineNodes,
}
}, [store, workflowStore])
}, [store, workflowStore, getNodeAlignPosition])
return {
handleSetHelpline,

View File

@ -18,7 +18,7 @@ import {
} from 'reactflow'
import type { PluginDefaultValue } from '../block-selector/types'
import type { Edge, Node, OnNodeAdd } from '../types'
import { BlockEnum } from '../types'
import { BlockEnum, TRIGGER_NODE_TYPES } from '../types'
import { useWorkflowStore } from '../store'
import {
CUSTOM_EDGE,
@ -65,6 +65,13 @@ import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
// Entry node deletion restriction has been removed to allow empty workflows
// Entry node (Start/Trigger) wrapper offsets for alignment
// Must match the values in use-helpline.ts
const ENTRY_NODE_WRAPPER_OFFSET = {
x: 0,
y: 21, // Adjusted based on visual testing feedback
} as const
export const useNodesInteractions = () => {
const { t } = useTranslation()
const store = useStoreApi()
@ -140,21 +147,51 @@ export const useNodesInteractions = () => {
const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(n => n.id === node.id)!
if (showVerticalHelpLineNodesLength > 0)
currentNode.position.x = showVerticalHelpLineNodes[0].position.x
else if (restrictPosition.x !== undefined)
currentNode.position.x = restrictPosition.x
else if (restrictLoopPosition.x !== undefined)
currentNode.position.x = restrictLoopPosition.x
else currentNode.position.x = node.position.x
// Check if current dragging node is an entry node
const isCurrentEntryNode = TRIGGER_NODE_TYPES.includes(node.data.type as any) || node.data.type === BlockEnum.Start
if (showHorizontalHelpLineNodesLength > 0)
currentNode.position.y = showHorizontalHelpLineNodes[0].position.y
else if (restrictPosition.y !== undefined)
// X-axis alignment with offset consideration
if (showVerticalHelpLineNodesLength > 0) {
const targetNode = showVerticalHelpLineNodes[0]
const isTargetEntryNode = TRIGGER_NODE_TYPES.includes(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start
// Calculate the wrapper position needed to align the inner nodes
// Target inner position = target.position + target.offset
// Current inner position should equal target inner position
// So: current.position + current.offset = target.position + target.offset
// Therefore: current.position = target.position + target.offset - current.offset
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.x : 0
currentNode.position.x = targetNode.position.x + targetOffset - currentOffset
}
else if (restrictPosition.x !== undefined) {
currentNode.position.x = restrictPosition.x
}
else if (restrictLoopPosition.x !== undefined) {
currentNode.position.x = restrictLoopPosition.x
}
else {
currentNode.position.x = node.position.x
}
// Y-axis alignment with offset consideration
if (showHorizontalHelpLineNodesLength > 0) {
const targetNode = showHorizontalHelpLineNodes[0]
const isTargetEntryNode = TRIGGER_NODE_TYPES.includes(targetNode.data.type as any) || targetNode.data.type === BlockEnum.Start
const targetOffset = isTargetEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
const currentOffset = isCurrentEntryNode ? ENTRY_NODE_WRAPPER_OFFSET.y : 0
currentNode.position.y = targetNode.position.y + targetOffset - currentOffset
}
else if (restrictPosition.y !== undefined) {
currentNode.position.y = restrictPosition.y
else if (restrictLoopPosition.y !== undefined)
}
else if (restrictLoopPosition.y !== undefined) {
currentNode.position.y = restrictLoopPosition.y
else currentNode.position.y = node.position.y
}
else {
currentNode.position.y = node.position.y
}
})
setNodes(newNodes)
},