use std::fs::File;
use std::io::{self, Write};
use std::path::Path;
fn point_pairs(points: &[[f64; 2]]) -> impl Iterator<Item = (usize, usize)> {
(0..points.len()).flat_map(|i| ((i + 1)..points.len()).map(move |j| (i, j)))
}
fn unit_distance_bounds(tolerance: f64) -> (f64, f64) {
(
(1.0 - tolerance) * (1.0 - tolerance),
(1.0 + tolerance) * (1.0 + tolerance),
)
}
fn squared_distance(a: [f64; 2], b: [f64; 2]) -> f64 {
let dx = a[0] - b[0];
let dy = a[1] - b[1];
dx * dx + dy * dy
}
pub fn count_unit_distances(points: &[[f64; 2]], tolerance: f64) -> usize {
let (lo, hi) = unit_distance_bounds(tolerance);
point_pairs(points)
.filter(|&(i, j)| {
let dist_sq = squared_distance(points[i], points[j]);
dist_sq >= lo && dist_sq <= hi
})
.count()
}
pub fn find_unit_distance_edges(points: &[[f64; 2]], tolerance: f64) -> Vec<(usize, usize)> {
let (lo, hi) = unit_distance_bounds(tolerance);
point_pairs(points)
.filter(|&(i, j)| {
let dist_sq = squared_distance(points[i], points[j]);
dist_sq >= lo && dist_sq <= hi
})
.collect()
}
pub fn export_to_csv(points: &[[f64; 2]], path: impl AsRef<Path>) -> io::Result<()> {
let mut file = File::create(path)?;
writeln!(file, "x,y")?;
points
.iter()
.try_for_each(|p| writeln!(file, "{},{}", p[0], p[1]))
}
pub fn export_to_obj(points: &[[f64; 2]], path: impl AsRef<Path>) -> io::Result<()> {
let mut file = File::create(path)?;
writeln!(file, "# erdos-unit-distance OBJ export")?;
points
.iter()
.try_for_each(|p| writeln!(file, "v {} {} 0.0", p[0], p[1]))?;
find_unit_distance_edges(points, 1e-4)
.into_iter()
.try_for_each(|(i, j)| writeln!(file, "l {} {}", i + 1, j + 1))
}
pub fn export_to_svg(
points: &[[f64; 2]],
path: impl AsRef<Path>,
width: u32,
height: u32,
) -> io::Result<()> {
if points.is_empty() {
return Ok(());
}
let (min_x, max_x, min_y, max_y) = points.iter().skip(1).fold(
(points[0][0], points[0][0], points[0][1], points[0][1]),
|(min_x, max_x, min_y, max_y), p| {
(
min_x.min(p[0]),
max_x.max(p[0]),
min_y.min(p[1]),
max_y.max(p[1]),
)
},
);
let dx = max_x - min_x;
let dy = max_y - min_y;
let margin = 0.1 * (if dx > dy { dx } else { dy }).max(1.0);
let min_x = min_x - margin;
let max_x = max_x + margin;
let min_y = min_y - margin;
let max_y = max_y + margin;
let view_w = max_x - min_x;
let view_h = max_y - min_y;
let svg_width = f64::from(width);
let svg_height = f64::from(height);
let map_x = |x: f64| -> f64 { ((x - min_x) / view_w) * svg_width };
let map_y = |y: f64| -> f64 {
(1.0 - (y - min_y) / view_h) * svg_height
};
let mut file = File::create(path)?;
writeln!(
file,
r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 {} {}" width="{}" height="{}">"#,
width, height, width, height
)?;
writeln!(
file,
r##" <rect width="100%" height="100%" fill="#121214"/>"##
)?;
find_unit_distance_edges(points, 1e-4)
.into_iter()
.try_for_each(|(i, j)| {
writeln!(
file,
r##" <line x1="{:.2}" y1="{:.2}" x2="{:.2}" y2="{:.2}" stroke="#3b82f6" stroke-width="1.5" opacity="0.6"/>"##,
map_x(points[i][0]),
map_y(points[i][1]),
map_x(points[j][0]),
map_y(points[j][1])
)
})?;
points.iter().try_for_each(|p| {
writeln!(
file,
r##" <circle cx="{:.2}" cy="{:.2}" r="3.5" fill="#f43f5e" stroke="#121214" stroke-width="0.5"/>"##,
map_x(p[0]),
map_y(p[1])
)
})?;
writeln!(file, "</svg>")?;
Ok(())
}
pub fn prune_to_target_density(
points: &[[f64; 2]],
n_target: usize,
tolerance: f64,
) -> Vec<[f64; 2]> {
let n_initial = points.len();
if n_initial <= n_target {
return points.to_vec();
}
let mut adj = vec![Vec::new(); n_initial];
let mut degrees = vec![0; n_initial];
let mut active = vec![true; n_initial];
let (lo, hi) = unit_distance_bounds(tolerance);
point_pairs(points)
.filter(|&(i, j)| {
let dist_sq = squared_distance(points[i], points[j]);
dist_sq >= lo && dist_sq <= hi
})
.for_each(|(i, j)| {
adj[i].push(j);
adj[j].push(i);
degrees[i] += 1;
degrees[j] += 1;
});
let mut active_count = n_initial;
while active_count > n_target {
let Some(min_idx) = (0..n_initial)
.filter(|&i| active[i])
.min_by_key(|&i| degrees[i])
else {
break;
};
active[min_idx] = false;
active_count -= 1;
let neighbors_to_decrement: Vec<usize> = adj[min_idx]
.iter()
.copied()
.filter(|&neighbor| active[neighbor] && degrees[neighbor] > 0)
.collect();
neighbors_to_decrement
.into_iter()
.for_each(|neighbor| degrees[neighbor] -= 1);
}
points
.iter()
.zip(active)
.filter_map(|(&point, is_active)| is_active.then_some(point))
.collect()
}