Nelze vybrat více než 25 témat Téma musí začínat písmenem nebo číslem, může obsahovat pomlčky („-“) a může být dlouhé až 35 znaků.

dagre-layout.ts 7.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246
  1. import dagre from '@dagrejs/dagre'
  2. import {
  3. cloneDeep,
  4. } from 'lodash-es'
  5. import type {
  6. Edge,
  7. Node,
  8. } from '../types'
  9. import {
  10. BlockEnum,
  11. } from '../types'
  12. import {
  13. CUSTOM_NODE,
  14. NODE_LAYOUT_HORIZONTAL_PADDING,
  15. NODE_LAYOUT_MIN_DISTANCE,
  16. NODE_LAYOUT_VERTICAL_PADDING,
  17. } from '../constants'
  18. import { CUSTOM_ITERATION_START_NODE } from '../nodes/iteration-start/constants'
  19. import { CUSTOM_LOOP_START_NODE } from '../nodes/loop-start/constants'
  20. export const getLayoutByDagre = (originNodes: Node[], originEdges: Edge[]) => {
  21. const dagreGraph = new dagre.graphlib.Graph({ compound: true })
  22. dagreGraph.setDefaultEdgeLabel(() => ({}))
  23. const nodes = cloneDeep(originNodes).filter(node => !node.parentId && node.type === CUSTOM_NODE)
  24. const edges = cloneDeep(originEdges).filter(edge => (!edge.data?.isInIteration && !edge.data?.isInLoop))
  25. // The default dagre layout algorithm often fails to correctly order the branches
  26. // of an If/Else node, leading to crossed edges.
  27. //
  28. // To solve this, we employ a "virtual container" strategy:
  29. // 1. A virtual, compound parent node (the "container") is created for each If/Else node's branches.
  30. // 2. Each direct child of the If/Else node is preceded by a virtual dummy node. These dummies are placed inside the container.
  31. // 3. A rigid, sequential chain of invisible edges is created between these dummy nodes (e.g., dummy_IF -> dummy_ELIF -> dummy_ELSE).
  32. //
  33. // This forces dagre to treat the ordered branches as an unbreakable, atomic group,
  34. // ensuring their layout respects the intended logical sequence.
  35. const ifElseNodes = nodes.filter(node => node.data.type === BlockEnum.IfElse)
  36. let virtualLogicApplied = false
  37. ifElseNodes.forEach((ifElseNode) => {
  38. const childEdges = edges.filter(e => e.source === ifElseNode.id)
  39. if (childEdges.length <= 1)
  40. return
  41. virtualLogicApplied = true
  42. const sortedChildEdges = childEdges.sort((edgeA, edgeB) => {
  43. const handleA = edgeA.sourceHandle
  44. const handleB = edgeB.sourceHandle
  45. if (handleA && handleB) {
  46. const cases = (ifElseNode.data as any).cases || []
  47. const isAElse = handleA === 'false'
  48. const isBElse = handleB === 'false'
  49. if (isAElse) return 1
  50. if (isBElse) return -1
  51. const indexA = cases.findIndex((c: any) => c.case_id === handleA)
  52. const indexB = cases.findIndex((c: any) => c.case_id === handleB)
  53. if (indexA !== -1 && indexB !== -1)
  54. return indexA - indexB
  55. }
  56. return 0
  57. })
  58. const parentDummyId = `dummy-parent-${ifElseNode.id}`
  59. dagreGraph.setNode(parentDummyId, { width: 1, height: 1 })
  60. const dummyNodes: string[] = []
  61. sortedChildEdges.forEach((edge) => {
  62. const dummyNodeId = `dummy-${edge.source}-${edge.target}`
  63. dummyNodes.push(dummyNodeId)
  64. dagreGraph.setNode(dummyNodeId, { width: 1, height: 1 })
  65. dagreGraph.setParent(dummyNodeId, parentDummyId)
  66. const edgeIndex = edges.findIndex(e => e.id === edge.id)
  67. if (edgeIndex > -1)
  68. edges.splice(edgeIndex, 1)
  69. edges.push({ id: `e-${edge.source}-${dummyNodeId}`, source: edge.source, target: dummyNodeId, sourceHandle: edge.sourceHandle } as Edge)
  70. edges.push({ id: `e-${dummyNodeId}-${edge.target}`, source: dummyNodeId, target: edge.target, targetHandle: edge.targetHandle } as Edge)
  71. })
  72. for (let i = 0; i < dummyNodes.length - 1; i++) {
  73. const sourceDummy = dummyNodes[i]
  74. const targetDummy = dummyNodes[i + 1]
  75. edges.push({ id: `e-dummy-${sourceDummy}-${targetDummy}`, source: sourceDummy, target: targetDummy } as Edge)
  76. }
  77. })
  78. dagreGraph.setGraph({
  79. rankdir: 'LR',
  80. align: 'UL',
  81. nodesep: 40,
  82. ranksep: virtualLogicApplied ? 30 : 60,
  83. ranker: 'tight-tree',
  84. marginx: 30,
  85. marginy: 200,
  86. })
  87. nodes.forEach((node) => {
  88. dagreGraph.setNode(node.id, {
  89. width: node.width!,
  90. height: node.height!,
  91. })
  92. })
  93. edges.forEach((edge) => {
  94. dagreGraph.setEdge(edge.source, edge.target)
  95. })
  96. dagre.layout(dagreGraph)
  97. return dagreGraph
  98. }
  99. export const getLayoutForChildNodes = (parentNodeId: string, originNodes: Node[], originEdges: Edge[]) => {
  100. const dagreGraph = new dagre.graphlib.Graph()
  101. dagreGraph.setDefaultEdgeLabel(() => ({}))
  102. const nodes = cloneDeep(originNodes).filter(node => node.parentId === parentNodeId)
  103. const edges = cloneDeep(originEdges).filter(edge =>
  104. (edge.data?.isInIteration && edge.data?.iteration_id === parentNodeId)
  105. || (edge.data?.isInLoop && edge.data?.loop_id === parentNodeId),
  106. )
  107. const startNode = nodes.find(node =>
  108. node.type === CUSTOM_ITERATION_START_NODE
  109. || node.type === CUSTOM_LOOP_START_NODE
  110. || node.data?.type === BlockEnum.LoopStart
  111. || node.data?.type === BlockEnum.IterationStart,
  112. )
  113. if (!startNode) {
  114. dagreGraph.setGraph({
  115. rankdir: 'LR',
  116. align: 'UL',
  117. nodesep: 40,
  118. ranksep: 60,
  119. marginx: NODE_LAYOUT_HORIZONTAL_PADDING,
  120. marginy: NODE_LAYOUT_VERTICAL_PADDING,
  121. })
  122. nodes.forEach((node) => {
  123. dagreGraph.setNode(node.id, {
  124. width: node.width || 244,
  125. height: node.height || 100,
  126. })
  127. })
  128. edges.forEach((edge) => {
  129. dagreGraph.setEdge(edge.source, edge.target)
  130. })
  131. dagre.layout(dagreGraph)
  132. return dagreGraph
  133. }
  134. const startNodeOutEdges = edges.filter(edge => edge.source === startNode.id)
  135. const firstConnectedNodes = startNodeOutEdges.map(edge =>
  136. nodes.find(node => node.id === edge.target),
  137. ).filter(Boolean) as Node[]
  138. const nonStartNodes = nodes.filter(node => node.id !== startNode.id)
  139. const nonStartEdges = edges.filter(edge => edge.source !== startNode.id && edge.target !== startNode.id)
  140. dagreGraph.setGraph({
  141. rankdir: 'LR',
  142. align: 'UL',
  143. nodesep: 40,
  144. ranksep: 60,
  145. marginx: NODE_LAYOUT_HORIZONTAL_PADDING / 2,
  146. marginy: NODE_LAYOUT_VERTICAL_PADDING / 2,
  147. })
  148. nonStartNodes.forEach((node) => {
  149. dagreGraph.setNode(node.id, {
  150. width: node.width || 244,
  151. height: node.height || 100,
  152. })
  153. })
  154. nonStartEdges.forEach((edge) => {
  155. dagreGraph.setEdge(edge.source, edge.target)
  156. })
  157. dagre.layout(dagreGraph)
  158. const startNodeSize = {
  159. width: startNode.width || 44,
  160. height: startNode.height || 48,
  161. }
  162. const startNodeX = NODE_LAYOUT_HORIZONTAL_PADDING / 1.5
  163. let startNodeY = 100
  164. let minFirstLayerX = Infinity
  165. let avgFirstLayerY = 0
  166. let firstLayerCount = 0
  167. if (firstConnectedNodes.length > 0) {
  168. firstConnectedNodes.forEach((node) => {
  169. if (dagreGraph.node(node.id)) {
  170. const nodePos = dagreGraph.node(node.id)
  171. avgFirstLayerY += nodePos.y
  172. firstLayerCount++
  173. minFirstLayerX = Math.min(minFirstLayerX, nodePos.x - nodePos.width / 2)
  174. }
  175. })
  176. if (firstLayerCount > 0) {
  177. avgFirstLayerY /= firstLayerCount
  178. startNodeY = avgFirstLayerY
  179. }
  180. const minRequiredX = startNodeX + startNodeSize.width + NODE_LAYOUT_MIN_DISTANCE
  181. if (minFirstLayerX < minRequiredX) {
  182. const shiftX = minRequiredX - minFirstLayerX
  183. nonStartNodes.forEach((node) => {
  184. if (dagreGraph.node(node.id)) {
  185. const nodePos = dagreGraph.node(node.id)
  186. dagreGraph.setNode(node.id, {
  187. x: nodePos.x + shiftX,
  188. y: nodePos.y,
  189. width: nodePos.width,
  190. height: nodePos.height,
  191. })
  192. }
  193. })
  194. }
  195. }
  196. dagreGraph.setNode(startNode.id, {
  197. x: startNodeX + startNodeSize.width / 2,
  198. y: startNodeY,
  199. width: startNodeSize.width,
  200. height: startNodeSize.height,
  201. })
  202. startNodeOutEdges.forEach((edge) => {
  203. dagreGraph.setEdge(edge.source, edge.target)
  204. })
  205. return dagreGraph
  206. }