function arrangeEvenly()

in src/state/layout/hierarchicalLayout.ts [199:309]


function arrangeEvenly(nodesCount: number, fixtures: NodeDirection[], defaultAngle: number): NodeDirection[] {
  const arrangement: NodeDirection[] = [];

  // No nodes to arrange? Return fixtures unchanged.
  if (nodesCount <= 0) {
    return clone(fixtures);
  }

  // If there is no fixtures, just arrange nodes evenly.
  if (!fixtures.length) {
    const angleIncrement = (Math.PI * 2) / nodesCount;

    // General orientation should point to `defaultAngle`:
    // - For odd number of nodes, one of the nodes points to `defaultAngle`.
    // - For even number of nodes, `defaultAngle` is in the middle between two consecutive nodes.
    let angle = (nodesCount % 2 === 0)
      ? normalizeAngle(defaultAngle + angleIncrement * 0.5)
      : defaultAngle;

    for (let i = 0; i < nodesCount; i += 1) {
      arrangement.push({ angle, isFixed: false });

      angle = normalizeAngle(angle + angleIncrement);
    }

    sortNodeDirections(arrangement);
    return arrangement;
  }

  // Calculate the segments bewteen the fixtures (in angles).
  const segments = [];
  for (let i = 1; i < fixtures.length; i += 1) {
    segments.push(fixtures[i].angle - fixtures[i - 1].angle);
  }
  segments.push((fixtures[0].angle + Math.PI * 2) - fixtures[fixtures.length - 1].angle);

  // Use dynamic programming to figure out the best arrangement.
  // `minAngle[x][y]` equals to the minimum angle in optimal arrangement
  // of Y number nodes in the 0..X segments.
  //
  // As example `minAngle[5][3]` equals to the minimal angle between any nodes,
  // when we arrange 5 nodes in segments 0, 1 and 2.

  // Initialize the table with 0 in each position. That's the worst outcome for optimal arrangement.
  const minAngle: number[][] = [];
  const bestNodesCount: number[][] = [];
  for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
    minAngle.push([]);
    bestNodesCount.push([]);
    for (let nodes = 0; nodes <= nodesCount; nodes += 1) {
      minAngle[segmentIndex].push(0);
      bestNodesCount[segmentIndex].push(0);
    }
  }

  // Initialize `minAngle[X][0]`. This is when we don't have any nodes to arrange.
  minAngle[0][0] = segments[0];
  bestNodesCount[0][0] = 0;
  for (let segmentIndex = 1; segmentIndex < segments.length; segmentIndex += 1) {
    minAngle[segmentIndex][0] = Math.min(segments[segmentIndex], minAngle[segmentIndex - 1][0]);
    bestNodesCount[segmentIndex][0] = 0;
  }

  // Initialize `minAngle[0][X]`. This is when we arrange X nodes evenly in the first segment.
  for (let nodes = 1; nodes <= nodesCount; nodes += 1) {
    // Squeezing X nodes in a segment splits it into (X + 1) equal subsegments.
    minAngle[0][nodes] = segments[0] / (nodes + 1);
    bestNodesCount[0][nodes] = nodes;
  }

  // Calculate the minimal angle for each remaining cell.
  for (let segmentIndex = 1; segmentIndex < segments.length; segmentIndex += 1) {
    for (let nodes = 1; nodes <= nodesCount; nodes += 1) {
      let best = Math.min(minAngle[segmentIndex - 1][nodes], segments[segmentIndex]);
      let bestNodes = 0;
      for (let x = 1; x <= nodes; x += 1) {
        const current = Math.min(minAngle[segmentIndex - 1][nodes - x], segments[segmentIndex] / (x + 1));
        if (current >= best) {
          best = current;
          bestNodes = x;
        }
      }

      minAngle[segmentIndex][nodes] = best;
      bestNodesCount[segmentIndex][nodes] = bestNodes;
    }
  }

  // Based on the results, extract the optimal number of nodes that should go in each segment.
  const segmentNodesCount: number[] = segments.map(segment => 0);
  let remaining = nodesCount;
  for (let segmentIndex = segments.length - 1; segmentIndex >= 0; segmentIndex -= 1) {
    segmentNodesCount[segmentIndex] = bestNodesCount[segmentIndex][remaining];
    remaining -= segmentNodesCount[segmentIndex];
  }

  // Finally, calculate and return the actual angles.
  for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex += 1) {
    arrangement.push(fixtures[segmentIndex]);
    let angle = fixtures[segmentIndex].angle;
    const angleIncrement = segments[segmentIndex] / (segmentNodesCount[segmentIndex] + 1);
    for (let node = 0; node < segmentNodesCount[segmentIndex]; node += 1) {
      angle += angleIncrement;
      angle = normalizeAngle(angle);
      arrangement.push({ angle, isFixed: false });
    }
  }

  sortNodeDirections(arrangement);
  return arrangement;
}