elkjs use

92 阅读2分钟
import '@xyflow/react/dist/style.css';

import ELK from 'elkjs/lib/elk.bundled.js';

import { CARD_HEIGHT, CARD_WIDTH } from './getNodesEdges';

// 初始化ELK布局引擎
const elk = new ELK();

// 树形布局配置选项(完整配置)
const elkOptions = {
  'elk.algorithm': 'mrtree',                  // 使用树形布局算法
  'elk.direction': 'DOWN',                    // 从上到下排列
  'elk.spacing.nodeNode': '60',               // 同级节点间距
  'elk.spacing.componentComponent': '80',     // 不同分支间距
  'elk.mrtree.edgeRoutingMode': 'ORTHOGONAL', // 边路由模式
  'elk.mrtree.nodePlacement.strategy': 'SIMPLE', // 节点放置策略
  // 'elk.mrtree.aspectRatio': '1.6',            // 树形宽高比
  'elk.mrtree.fixedAlignment': 'BALANCED',    // 节点对齐方式
  'elk.mrtree.considerModelOrder': 'NODES_AND_EDGES', // 考虑节点和边顺序
  'elk.mrtree.compaction.post.compaction.strategy': 'EDGE_LENGTH', // 紧凑策略
  // 'elk.padding': '[top=20,left=20,bottom=20,right=20]', // 整体内边距
  'elk.random.seed': '12345',                 // 固定随机种子确保布局稳定
};

/**
 * 获取树形布局后的元素(节点和边)
 * @param nodes 节点数组
 * @param edges 边数组
 * @param options 额外的布局选项
 * @param previousPositions 上次生成的节点位置记录
 * @returns 包含布局后节点和边的Promise
 */
export const getLayoutedElements = async (
  nodes: any[],
  edges: any[],
  options = {},
  previousPositions?: Record<string, { x: number, y: number }>,
) => {
  // 如果有之前的位置且节点ID匹配,直接重用位置
  if (previousPositions && nodes.every(node => node.id in previousPositions)) {
    const positionedNodes = nodes.map((node) => {
      const prevPos = previousPositions[node.id];
      return {
        ...node,
        position: prevPos,
        x: prevPos.x,
        y: prevPos.y,
        // 确保保留所有必要属性
        width: node.width || CARD_WIDTH,
        height: node.height || CARD_HEIGHT,
        targetPosition: 'top',
        sourcePosition: 'bottom',
        data: {
          ...node.data,
          level: Math.floor(prevPos.y / 100),
        },
      };
    });

    return {
      nodes: positionedNodes,
      edges,
      meta: {
        layoutType: 'reused',
        reused: true,
        timestamp: new Date().toISOString(),
      },
    };
  }

  // 构建ELK布局所需的图形结构
  const graph = {
    id: 'root',
    layoutOptions: { ...elkOptions, ...options },
    children: nodes.map(node => ({
      ...node,
      width: node.width || CARD_WIDTH,
      height: node.height || CARD_HEIGHT,
      targetPosition: 'top',
      sourcePosition: 'bottom',
      // 'elk.padding': '[top=15,left=15,bottom=15,right=15]',
      'elk.mrtree.compaction': 'true',
      'elk.mrtree.considerModelOrder': 'true',
    })),
    edges,
  };

  try {
    // 执行ELK布局计算
    const layoutedGraph = await elk.layout(graph);

    // 处理布局后的节点
    const layoutedNodes = layoutedGraph?.children?.map((node) => {
      const newX = (node.x || 0) + (window.innerWidth * 0.1);
      const newY = (node.y || 0) + 50;

      return {
        ...node,
        position: { x: newX, y: newY },
        x: newX,
        y: newY,
        data: {
          ...(node as any)?.data,
          level: Math.floor((node.y || 0) / 100),
        },
        width: node.width || CARD_WIDTH,
        height: node.height || CARD_HEIGHT,
        targetPosition: 'top',
        sourcePosition: 'bottom',
      };
    }) || [];

    // 返回完整结果
    return {
      nodes: layoutedNodes,
      edges,
      meta: {
        layoutType: 'tree',
        direction: 'vertical',
        spacing: elkOptions['elk.spacing.nodeNode'],
        algorithm: 'mrtree',
        version: '1.0.0',
        timestamp: new Date().toISOString(),
      },
    };
  } catch (error) {
    console.error('ELK树形布局计算错误:', error);
    return {
      nodes: [],
      edges: [],
      meta: {
        error: error instanceof Error ? error.message : 'Unknown error',
        success: false,
      },
    };
  }
};

// 使用示例
/*
// 第一次生成布局
const firstLayout = await getLayoutedElements(nodes, edges);

// 保存位置信息
const nodePositions = Object.fromEntries(
  firstLayout.nodes.map(node => [node.id, node.position])
);

// 后续生成时传入之前的位置
const consistentLayout = await getLayoutedElements(
  updatedNodes,
  updatedEdges,
  {},
  nodePositions
);
*/