All files / src/layout layer-assign.ts

96% Statements 48/50
77.77% Branches 14/18
100% Functions 5/5
95.83% Lines 46/48

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103                24x     24x 24x 24x     24x 160x 160x 46x         24x 160x 160x   160x 160x 204x 204x 204x 114x           24x 160x           24x 160x   160x   46x     114x 114x 204x 204x 204x     114x         24x 160x           24x 24x 160x 160x 160x       142x 24x   24x 118x   118x 118x 160x     118x       160x       24x    
import type { LayoutState, LayoutNode, Layer } from "./types";
 
/**
 * Assign layers to nodes using longest-path algorithm
 * Sources (no predecessors) get layer 0
 * Each node gets max(predecessor layers) + 1
 */
export function assignLayers(state: LayoutState): void {
  const { nodes } = state;
 
  // Topological sort via Kahn's algorithm
  const inDegree = new Map<number, number>();
  const queue: number[] = [];
  const order: number[] = [];
 
  // Initialize in-degrees
  for (const [id, node] of nodes) {
    inDegree.set(id, node.predecessors.length);
    if (node.predecessors.length === 0) {
      queue.push(id);
    }
  }
 
  // Process nodes in topological order
  while (queue.length > 0) {
    const nodeId = queue.shift()!;
    order.push(nodeId);
 
    const node = nodes.get(nodeId)!;
    for (const succId of node.successors) {
      const deg = (inDegree.get(succId) ?? 1) - 1;
      inDegree.set(succId, deg);
      if (deg === 0) {
        queue.push(succId);
      }
    }
  }
 
  // Handle cycles: add remaining nodes (shouldn't happen in valid DAG)
  for (const [id] of nodes) {
    Iif (!order.includes(id)) {
      order.push(id);
    }
  }
 
  // Assign layers via longest path from sources
  for (const nodeId of order) {
    const node = nodes.get(nodeId)!;
 
    if (node.predecessors.length === 0) {
      // Source node: layer 0
      node.layer = 0;
    } else {
      // Max layer of predecessors + 1
      let maxPredLayer = 0;
      for (const predId of node.predecessors) {
        const pred = nodes.get(predId);
        Eif (pred && pred.layer >= 0) {
          maxPredLayer = Math.max(maxPredLayer, pred.layer + 1);
        }
      }
      node.layer = maxPredLayer;
    }
  }
 
  // Ensure all nodes have valid layer (handle edge cases)
  for (const node of nodes.values()) {
    Iif (node.layer < 0) {
      node.layer = 0;
    }
  }
 
  // Build layer structures
  const layerMap = new Map<number, LayoutNode[]>();
  for (const node of nodes.values()) {
    const arr = layerMap.get(node.layer) ?? [];
    arr.push(node);
    layerMap.set(node.layer, arr);
  }
 
  // Sort layers by index and create Layer objects
  const sortedLayerIndices = [...layerMap.keys()].sort((a, b) => a - b);
  const layers: Layer[] = [];
 
  for (const index of sortedLayerIndices) {
    const layerNodes = layerMap.get(index)!;
    // Sort nodes in layer by ID for initial deterministic order
    layerNodes.sort((a, b) => a.id - b.id);
    layerNodes.forEach((n, i) => {
      n.orderInLayer = i;
    });
 
    layers.push({
      index,
      nodes: layerNodes,
      x: 0,
      maxWidth: Math.max(...layerNodes.map((n) => n.width), 100),
    });
  }
 
  state.layers = layers;
}