mirror of
https://github.com/langgenius/dify.git
synced 2025-07-17 06:12:10 +00:00
247 lines
7.5 KiB
TypeScript
247 lines
7.5 KiB
TypeScript
import dagre from '@dagrejs/dagre'
|
|
import {
|
|
cloneDeep,
|
|
} from 'lodash-es'
|
|
import type {
|
|
Edge,
|
|
Node,
|
|
} from '../types'
|
|
import {
|
|
BlockEnum,
|
|
} from '../types'
|
|
import {
|
|
CUSTOM_NODE,
|
|
NODE_LAYOUT_HORIZONTAL_PADDING,
|
|
NODE_LAYOUT_MIN_DISTANCE,
|
|
NODE_LAYOUT_VERTICAL_PADDING,
|
|
} from '../constants'
|
|
import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
|
|
import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
|
|
|
|
export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
|
|
const dagreGraph = new dagre.graphlib.Graph({ compound: true })
|
|
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
|
|
|
const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
|
|
const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop))
|
|
|
|
// The default dagre layout algorithm often fails to correctly order the branches
|
|
// of an If/Else node, leading to crossed edges.
|
|
//
|
|
// To solve this, we employ a "virtual container" strategy:
|
|
// 1. A virtual, compound parent node (the "container") is created for each If/Else node's branches.
|
|
// 2. Each direct child of the If/Else node is preceded by a virtual dummy node. These dummies are placed inside the container.
|
|
// 3. A rigid, sequential chain of invisible edges is created between these dummy nodes (e.g., dummy_IF -> dummy_ELIF -> dummy_ELSE).
|
|
//
|
|
// This forces dagre to treat the ordered branches as an unbreakable, atomic group,
|
|
// ensuring their layout respects the intended logical sequence.
|
|
const ifElseNodes = nodes.filter(node => node.data.type === BlockEnum.IfElse)
|
|
let virtualLogicApplied = false
|
|
|
|
ifElseNodes.forEach((ifElseNode) => {
|
|
const childEdges = edges.filter(e => e.source === ifElseNode.id)
|
|
if (childEdges.length <= 1)
|
|
return
|
|
|
|
virtualLogicApplied = true
|
|
const sortedChildEdges = childEdges.sort((edgeA, edgeB) => {
|
|
const handleA = edgeA.sourceHandle
|
|
const handleB = edgeB.sourceHandle
|
|
|
|
if (handleA && handleB) {
|
|
const cases = (ifElseNode.data as any).cases || []
|
|
const isAElse = handleA === 'false'
|
|
const isBElse = handleB === 'false'
|
|
|
|
if (isAElse) return 1
|
|
if (isBElse) return -1
|
|
|
|
const indexA = cases.findIndex((c: any) => c.case_id === handleA)
|
|
const indexB = cases.findIndex((c: any) => c.case_id === handleB)
|
|
|
|
if (indexA !== -1 && indexB !== -1)
|
|
return indexA - indexB
|
|
}
|
|
return 0
|
|
})
|
|
|
|
const parentDummyId = `dummy-parent-${ifElseNode.id}`
|
|
dagreGraph.setNode(parentDummyId, { width: 1, height: 1 })
|
|
|
|
const dummyNodes: string[] = []
|
|
sortedChildEdges.forEach((edge) => {
|
|
const dummyNodeId = `dummy-${edge.source}-${edge.target}`
|
|
dummyNodes.push(dummyNodeId)
|
|
dagreGraph.setNode(dummyNodeId, { width: 1, height: 1 })
|
|
dagreGraph.setParent(dummyNodeId, parentDummyId)
|
|
|
|
const edgeIndex = edges.findIndex(e => e.id === edge.id)
|
|
if (edgeIndex > -1)
|
|
edges.splice(edgeIndex, 1)
|
|
|
|
edges.push({ id: `e-${edge.source}-${dummyNodeId}`, source: edge.source, target: dummyNodeId, sourceHandle: edge.sourceHandle } as Edge)
|
|
edges.push({ id: `e-${dummyNodeId}-${edge.target}`, source: dummyNodeId, target: edge.target, targetHandle: edge.targetHandle } as Edge)
|
|
})
|
|
|
|
for (let i = 0; i < dummyNodes.length - 1; i++) {
|
|
const sourceDummy = dummyNodes[i]
|
|
const targetDummy = dummyNodes[i + 1]
|
|
edges.push({ id: `e-dummy-${sourceDummy}-${targetDummy}`, source: sourceDummy, target: targetDummy } as Edge)
|
|
}
|
|
})
|
|
|
|
dagreGraph.setGraph({
|
|
rankdir: 'LR',
|
|
align: 'UL',
|
|
nodesep: 40,
|
|
ranksep: virtualLogicApplied ? 30 : 60,
|
|
ranker: 'tight-tree',
|
|
marginx: 30,
|
|
marginy: 200,
|
|
})
|
|
|
|
nodes.forEach((node) => {
|
|
dagreGraph.setNode(node.id, {
|
|
width: node.width!,
|
|
height: node.height!,
|
|
})
|
|
})
|
|
edges.forEach((edge) => {
|
|
dagreGraph.setEdge(edge.source, edge.target)
|
|
})
|
|
dagre.layout(dagreGraph)
|
|
return dagreGraph
|
|
}
|
|
|
|
export const getLayoutForChildNodes = (parentNodeId: string, originNodes: Node[], originEdges: Edge[]) => {
|
|
const dagreGraph = new dagre.graphlib.Graph()
|
|
dagreGraph.setDefaultEdgeLabel(() => ({}))
|
|
|
|
const nodes = cloneDeep(originNodes).filter(node => node.parentId === parentNodeId)
|
|
const edges = cloneDeep(originEdges).filter(edge =>
|
|
(edge.data?.isInIteration && edge.data?.iteration_id === parentNodeId)
|
|
|| (edge.data?.isInLoop && edge.data?.loop_id === parentNodeId),
|
|
)
|
|
|
|
const startNode = nodes.find(node =>
|
|
node.type === CUSTOM_ITERATION_START_NODE
|
|
|| node.type === CUSTOM_LOOP_START_NODE
|
|
|| node.data?.type === BlockEnum.LoopStart
|
|
|| node.data?.type === BlockEnum.IterationStart,
|
|
)
|
|
|
|
if (!startNode) {
|
|
dagreGraph.setGraph({
|
|
rankdir: 'LR',
|
|
align: 'UL',
|
|
nodesep: 40,
|
|
ranksep: 60,
|
|
marginx: NODE_LAYOUT_HORIZONTAL_PADDING,
|
|
marginy: NODE_LAYOUT_VERTICAL_PADDING,
|
|
})
|
|
|
|
nodes.forEach((node) => {
|
|
dagreGraph.setNode(node.id, {
|
|
width: node.width || 244,
|
|
height: node.height || 100,
|
|
})
|
|
})
|
|
|
|
edges.forEach((edge) => {
|
|
dagreGraph.setEdge(edge.source, edge.target)
|
|
})
|
|
|
|
dagre.layout(dagreGraph)
|
|
return dagreGraph
|
|
}
|
|
|
|
const startNodeOutEdges = edges.filter(edge => edge.source === startNode.id)
|
|
const firstConnectedNodes = startNodeOutEdges.map(edge =>
|
|
nodes.find(node => node.id === edge.target),
|
|
).filter(Boolean) as Node[]
|
|
|
|
const nonStartNodes = nodes.filter(node => node.id !== startNode.id)
|
|
const nonStartEdges = edges.filter(edge => edge.source !== startNode.id && edge.target !== startNode.id)
|
|
|
|
dagreGraph.setGraph({
|
|
rankdir: 'LR',
|
|
align: 'UL',
|
|
nodesep: 40,
|
|
ranksep: 60,
|
|
marginx: NODE_LAYOUT_HORIZONTAL_PADDING / 2,
|
|
marginy: NODE_LAYOUT_VERTICAL_PADDING / 2,
|
|
})
|
|
|
|
nonStartNodes.forEach((node) => {
|
|
dagreGraph.setNode(node.id, {
|
|
width: node.width || 244,
|
|
height: node.height || 100,
|
|
})
|
|
})
|
|
|
|
nonStartEdges.forEach((edge) => {
|
|
dagreGraph.setEdge(edge.source, edge.target)
|
|
})
|
|
|
|
dagre.layout(dagreGraph)
|
|
|
|
const startNodeSize = {
|
|
width: startNode.width || 44,
|
|
height: startNode.height || 48,
|
|
}
|
|
|
|
const startNodeX = NODE_LAYOUT_HORIZONTAL_PADDING / 1.5
|
|
let startNodeY = 100
|
|
|
|
let minFirstLayerX = Infinity
|
|
let avgFirstLayerY = 0
|
|
let firstLayerCount = 0
|
|
|
|
if (firstConnectedNodes.length > 0) {
|
|
firstConnectedNodes.forEach((node) => {
|
|
if (dagreGraph.node(node.id)) {
|
|
const nodePos = dagreGraph.node(node.id)
|
|
avgFirstLayerY += nodePos.y
|
|
firstLayerCount++
|
|
minFirstLayerX = Math.min(minFirstLayerX, nodePos.x - nodePos.width / 2)
|
|
}
|
|
})
|
|
|
|
if (firstLayerCount > 0) {
|
|
avgFirstLayerY /= firstLayerCount
|
|
startNodeY = avgFirstLayerY
|
|
}
|
|
|
|
const minRequiredX = startNodeX + startNodeSize.width + NODE_LAYOUT_MIN_DISTANCE
|
|
|
|
if (minFirstLayerX < minRequiredX) {
|
|
const shiftX = minRequiredX - minFirstLayerX
|
|
|
|
nonStartNodes.forEach((node) => {
|
|
if (dagreGraph.node(node.id)) {
|
|
const nodePos = dagreGraph.node(node.id)
|
|
dagreGraph.setNode(node.id, {
|
|
x: nodePos.x + shiftX,
|
|
y: nodePos.y,
|
|
width: nodePos.width,
|
|
height: nodePos.height,
|
|
})
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
dagreGraph.setNode(startNode.id, {
|
|
x: startNodeX + startNodeSize.width / 2,
|
|
y: startNodeY,
|
|
width: startNodeSize.width,
|
|
height: startNodeSize.height,
|
|
})
|
|
|
|
startNodeOutEdges.forEach((edge) => {
|
|
dagreGraph.setEdge(edge.source, edge.target)
|
|
})
|
|
|
|
return dagreGraph
|
|
}
|