pub(super) const WIDTH: f64 = 960.0;
pub(super) const HEIGHT: f64 = 600.0;
const MARGIN: f64 = 72.0;
const ITERATIONS: usize = 240;
const COOLING: f64 = 0.94;
const MAX_MAGNIFICATION: f64 = 4.0;
pub(super) type Link = (usize, usize, u64);
pub(super) fn place(count: usize, links: &[Link]) -> Vec<(f64, f64)> {
let mut positions = ring(count);
if count < 2 {
return positions;
}
let ideal = (WIDTH * HEIGHT / scale(count)).sqrt();
let mut temperature = WIDTH / 8.0;
for _ in 0..ITERATIONS {
let mut push = vec![(0.0_f64, 0.0_f64); count];
repel(&positions, ideal, &mut push);
attract(&positions, links, ideal, &mut push);
advance(&mut positions, &push, temperature);
temperature *= COOLING;
}
normalize(&mut positions);
positions
}
fn normalize(positions: &mut [(f64, f64)]) {
let (mut min_x, mut min_y) = (f64::MAX, f64::MAX);
let (mut max_x, mut max_y) = (f64::MIN, f64::MIN);
for (x, y) in &*positions {
min_x = min_x.min(*x);
max_x = max_x.max(*x);
min_y = min_y.min(*y);
max_y = max_y.max(*y);
}
let span_x = (max_x - min_x).max(0.01);
let span_y = (max_y - min_y).max(0.01);
let factor = ((WIDTH - 2.0 * MARGIN) / span_x)
.min((HEIGHT - 2.0 * MARGIN) / span_y)
.min(MAX_MAGNIFICATION);
let centre = (f64::midpoint(min_x, max_x), f64::midpoint(min_y, max_y));
for point in positions.iter_mut() {
point.0 = WIDTH / 2.0 + (point.0 - centre.0) * factor;
point.1 = HEIGHT / 2.0 + (point.1 - centre.1) * factor;
}
}
fn ring(count: usize) -> Vec<(f64, f64)> {
let centre = (WIDTH / 2.0, HEIGHT / 2.0);
let radius = (HEIGHT / 2.0) - MARGIN;
(0..count)
.map(|index| {
let angle = std::f64::consts::TAU * scale(index) / scale(count.max(1));
(
centre.0 + radius * angle.cos(),
centre.1 + radius * angle.sin(),
)
})
.collect()
}
fn repel(positions: &[(f64, f64)], ideal: f64, push: &mut [(f64, f64)]) {
for left in 0..positions.len() {
for right in (left + 1)..positions.len() {
let (dx, dy, distance) = delta(positions[left], positions[right]);
let force = ideal * ideal / distance;
let (ux, uy) = (dx / distance * force, dy / distance * force);
push[left].0 += ux;
push[left].1 += uy;
push[right].0 -= ux;
push[right].1 -= uy;
}
}
}
fn attract(positions: &[(f64, f64)], links: &[Link], ideal: f64, push: &mut [(f64, f64)]) {
for (left, right, weight) in links {
let (Some(from), Some(to)) = (positions.get(*left), positions.get(*right)) else {
continue;
};
let (dx, dy, distance) = delta(*from, *to);
let force = distance * distance / ideal * (1.0 + scale_u64(*weight)).ln();
let (ux, uy) = (dx / distance * force, dy / distance * force);
push[*left].0 -= ux;
push[*left].1 -= uy;
push[*right].0 += ux;
push[*right].1 += uy;
}
}
fn advance(positions: &mut [(f64, f64)], push: &[(f64, f64)], temperature: f64) {
for (position, (dx, dy)) in positions.iter_mut().zip(push.iter()) {
let length = (dx * dx + dy * dy).sqrt().max(0.01);
let step = length.min(temperature);
position.0 = (position.0 + dx / length * step).clamp(MARGIN, WIDTH - MARGIN);
position.1 = (position.1 + dy / length * step).clamp(MARGIN, HEIGHT - MARGIN);
}
}
fn delta(from: (f64, f64), to: (f64, f64)) -> (f64, f64, f64) {
let (dx, dy) = (from.0 - to.0, from.1 - to.1);
(dx, dy, (dx * dx + dy * dy).sqrt().max(0.01))
}
fn scale(count: usize) -> f64 {
f64::from(u32::try_from(count).unwrap_or(u32::MAX))
}
fn scale_u64(count: u64) -> f64 {
f64::from(u32::try_from(count).unwrap_or(u32::MAX))
}