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;
}