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 { useCallback } from 'react'
import { useStoreApi } from 'reactflow' import { useStoreApi } from 'reactflow'
import type { Node } from '../types' import type { Node } from '../types'
import { BlockEnum, TRIGGER_NODE_TYPES } from '../types'
import { useWorkflowStore } from '../store' 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 = () => { export const useHelpline = () => {
const store = useStoreApi() const store = useStoreApi()
const workflowStore = useWorkflowStore() 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 handleSetHelpline = useCallback((node: Node) => {
const { getNodes } = store.getState() const { getNodes } = store.getState()
const nodes = getNodes() 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) => { const showHorizontalHelpLineNodes = nodes.filter((n) => {
if (n.id === node.id) if (n.id === node.id)
return false return false
@ -39,33 +70,52 @@ export const useHelpline = () => {
if (n.data.isInLoop) if (n.data.isInLoop)
return false return false
const nY = Math.ceil(n.position.y) // Get actual alignment position for comparison node
const nodeY = Math.ceil(node.position.y) const nAlignPos = getNodeAlignPosition(n)
const nY = Math.ceil(nAlignPos.y)
const nodeY = Math.ceil(nodeAlignPos.y)
if (nY - nodeY < 5 && nY - nodeY > -5) if (nY - nodeY < 5 && nY - nodeY > -5)
return true return true
return false 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 const showHorizontalHelpLineNodesLength = showHorizontalHelpLineNodes.length
if (showHorizontalHelpLineNodesLength > 0) { if (showHorizontalHelpLineNodesLength > 0) {
const first = showHorizontalHelpLineNodes[0] const first = showHorizontalHelpLineNodes[0]
const last = showHorizontalHelpLineNodes[showHorizontalHelpLineNodesLength - 1] 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 = { const helpLine = {
top: first.position.y, top: firstPos.y,
left: first.position.x, left: firstPos.x,
width: last.position.x + last.width! - first.position.x, width: lastPos.x + lastNodeWidth - firstPos.x,
} }
if (node.position.x < first.position.x) { if (nodeAlignPos.x < firstPos.x) {
helpLine.left = node.position.x const firstIsEntryNode = isEntryNode(first)
helpLine.width = first.position.x + first.width! - node.position.x 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) if (nodeAlignPos.x > lastPos.x) {
helpLine.width = node.position.x + node.width! - first.position.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) setHelpLineHorizontal(helpLine)
} }
@ -81,33 +131,52 @@ export const useHelpline = () => {
if (n.data.isInLoop) if (n.data.isInLoop)
return false return false
const nX = Math.ceil(n.position.x) // Get actual alignment position for comparison node
const nodeX = Math.ceil(node.position.x) const nAlignPos = getNodeAlignPosition(n)
const nX = Math.ceil(nAlignPos.x)
const nodeX = Math.ceil(nodeAlignPos.x)
if (nX - nodeX < 5 && nX - nodeX > -5) if (nX - nodeX < 5 && nX - nodeX > -5)
return true return true
return false 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 const showVerticalHelpLineNodesLength = showVerticalHelpLineNodes.length
if (showVerticalHelpLineNodesLength > 0) { if (showVerticalHelpLineNodesLength > 0) {
const first = showVerticalHelpLineNodes[0] const first = showVerticalHelpLineNodes[0]
const last = showVerticalHelpLineNodes[showVerticalHelpLineNodesLength - 1] 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 = { const helpLine = {
top: first.position.y, top: firstPos.y,
left: first.position.x, left: firstPos.x,
height: last.position.y + last.height! - first.position.y, height: lastPos.y + lastNodeHeight - firstPos.y,
} }
if (node.position.y < first.position.y) { if (nodeAlignPos.y < firstPos.y) {
helpLine.top = node.position.y const firstIsEntryNode = isEntryNode(first)
helpLine.height = first.position.y + first.height! - node.position.y 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) if (nodeAlignPos.y > lastPos.y) {
helpLine.height = node.position.y + node.height! - first.position.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) setHelpLineVertical(helpLine)
} }
@ -119,7 +188,7 @@ export const useHelpline = () => {
showHorizontalHelpLineNodes, showHorizontalHelpLineNodes,
showVerticalHelpLineNodes, showVerticalHelpLineNodes,
} }
}, [store, workflowStore]) }, [store, workflowStore, getNodeAlignPosition])
return { return {
handleSetHelpline, handleSetHelpline,

View File

@ -18,7 +18,7 @@ import {
} from 'reactflow' } from 'reactflow'
import type { PluginDefaultValue } from '../block-selector/types' import type { PluginDefaultValue } from '../block-selector/types'
import type { Edge, Node, OnNodeAdd } from '../types' import type { Edge, Node, OnNodeAdd } from '../types'
import { BlockEnum } from '../types' import { BlockEnum, TRIGGER_NODE_TYPES } from '../types'
import { useWorkflowStore } from '../store' import { useWorkflowStore } from '../store'
import { import {
CUSTOM_EDGE, 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 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 = () => { export const useNodesInteractions = () => {
const { t } = useTranslation() const { t } = useTranslation()
const store = useStoreApi() const store = useStoreApi()
@ -140,21 +147,51 @@ export const useNodesInteractions = () => {
const newNodes = produce(nodes, (draft) => { const newNodes = produce(nodes, (draft) => {
const currentNode = draft.find(n => n.id === node.id)! const currentNode = draft.find(n => n.id === node.id)!
if (showVerticalHelpLineNodesLength > 0) // Check if current dragging node is an entry node
currentNode.position.x = showVerticalHelpLineNodes[0].position.x const isCurrentEntryNode = TRIGGER_NODE_TYPES.includes(node.data.type as any) || node.data.type === BlockEnum.Start
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
if (showHorizontalHelpLineNodesLength > 0) // X-axis alignment with offset consideration
currentNode.position.y = showHorizontalHelpLineNodes[0].position.y if (showVerticalHelpLineNodesLength > 0) {
else if (restrictPosition.y !== undefined) 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 currentNode.position.y = restrictPosition.y
else if (restrictLoopPosition.y !== undefined) }
else if (restrictLoopPosition.y !== undefined) {
currentNode.position.y = restrictLoopPosition.y currentNode.position.y = restrictLoopPosition.y
else currentNode.position.y = node.position.y }
else {
currentNode.position.y = node.position.y
}
}) })
setNodes(newNodes) setNodes(newNodes)
}, },