use std::collections::HashMap;
struct Seg {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
edge: u32,
pos0: f64,
}
pub struct Snap {
pub edge: usize,
pub dist: f64,
pub x: f64,
pub y: f64,
pub pos: f64,
}
pub struct SegmentIndex {
segs: Vec<Seg>,
grid: HashMap<(i64, i64), Vec<u32>>,
oversize: Vec<u32>,
cell: f64,
minx: f64,
miny: f64,
maxx: f64,
maxy: f64,
}
fn project(px: f64, py: f64, s: &Seg) -> (f64, f64, f64, f64, f64) {
let dx = s.x2 - s.x1;
let dy = s.y2 - s.y1;
let len2 = dx * dx + dy * dy;
let t = if len2 > 0.0 {
(((px - s.x1) * dx + (py - s.y1) * dy) / len2).clamp(0.0, 1.0)
} else {
0.0
};
let x = s.x1 + t * dx;
let y = s.y1 + t * dy;
let d2 = (px - x) * (px - x) + (py - y) * (py - y);
(d2, t, x, y, len2.sqrt())
}
impl SegmentIndex {
pub fn build(vx: &[f64], vy: &[f64], offsets: &[usize]) -> Self {
let n_edges = offsets.len().saturating_sub(1);
let mut segs: Vec<Seg> = Vec::new();
let (mut minx, mut miny) = (f64::INFINITY, f64::INFINITY);
let (mut maxx, mut maxy) = (f64::NEG_INFINITY, f64::NEG_INFINITY);
for e in 0..n_edges {
let mut pos0 = 0.0;
let hi = offsets[e + 1];
for i in offsets[e]..hi.saturating_sub(1) {
let (x1, y1, x2, y2) = (vx[i], vy[i], vx[i + 1], vy[i + 1]);
minx = minx.min(x1.min(x2));
miny = miny.min(y1.min(y2));
maxx = maxx.max(x1.max(x2));
maxy = maxy.max(y1.max(y2));
let len = ((x2 - x1).powi(2) + (y2 - y1).powi(2)).sqrt();
segs.push(Seg {
x1,
y1,
x2,
y2,
edge: e as u32,
pos0,
});
pos0 += len;
}
}
let w = (maxx - minx).max(0.0);
let h = (maxy - miny).max(0.0);
let m = segs.len().max(1) as f64;
let mut cell = (w * h / m).sqrt();
if !cell.is_finite() || cell <= 0.0 {
cell = w.max(h) / m.sqrt(); }
if !cell.is_finite() || cell <= 0.0 {
cell = 1.0; }
let mut grid: HashMap<(i64, i64), Vec<u32>> = HashMap::new();
let mut oversize: Vec<u32> = Vec::new();
for (i, s) in segs.iter().enumerate() {
let ix0 = (s.x1.min(s.x2) / cell).floor() as i64;
let ix1 = (s.x1.max(s.x2) / cell).floor() as i64;
let iy0 = (s.y1.min(s.y2) / cell).floor() as i64;
let iy1 = (s.y1.max(s.y2) / cell).floor() as i64;
let n_cells = (ix1 - ix0 + 1).saturating_mul(iy1 - iy0 + 1);
if n_cells > 1024 {
oversize.push(i as u32);
continue;
}
for ix in ix0..=ix1 {
for iy in iy0..=iy1 {
grid.entry((ix, iy)).or_default().push(i as u32);
}
}
}
SegmentIndex {
segs,
grid,
oversize,
cell,
minx,
miny,
maxx,
maxy,
}
}
#[cfg(test)]
pub fn is_empty(&self) -> bool {
self.segs.is_empty()
}
pub fn nearest(&self, px: f64, py: f64, tolerance: f64) -> Option<Snap> {
if self.segs.is_empty() {
return None;
}
let mut best: Option<(f64, u32, f64, f64, f64)> = None;
let consider = |si: u32, best: &mut Option<(f64, u32, f64, f64, f64)>| {
let s = &self.segs[si as usize];
let (d2, t, x, y, _) = project(px, py, s);
let better = match best {
None => true,
Some((bd2, bsi, ..)) => d2 < *bd2 || (d2 == *bd2 && si < *bsi),
};
if better {
*best = Some((d2, si, t, x, y));
}
};
for &si in &self.oversize {
consider(si, &mut best);
}
let cell = self.cell;
let lo_x = (self.minx / cell).floor() as i64;
let hi_x = (self.maxx / cell).floor() as i64;
let lo_y = (self.miny / cell).floor() as i64;
let hi_y = (self.maxy / cell).floor() as i64;
let cx = ((px / cell).floor() as i64).clamp(lo_x, hi_x);
let cy = ((py / cell).floor() as i64).clamp(lo_y, hi_y);
let max_ring = (hi_x - lo_x).max(hi_y - lo_y) + 1;
for r in 0..=max_ring {
let bound = if r <= 1 { 0.0 } else { (r - 1) as f64 * cell };
if let Some((bd2, ..)) = best {
if bd2 <= bound * bound {
break;
}
}
if bound > tolerance {
break;
}
let scan = |ix: i64, iy: i64, best: &mut Option<(f64, u32, f64, f64, f64)>| {
if let Some(cellv) = self.grid.get(&(ix, iy)) {
for &si in cellv {
consider(si, best);
}
}
};
if r == 0 {
scan(cx, cy, &mut best);
} else {
for ix in (cx - r)..=(cx + r) {
scan(ix, cy - r, &mut best);
scan(ix, cy + r, &mut best);
}
for iy in (cy - r + 1)..=(cy + r - 1) {
scan(cx - r, iy, &mut best);
scan(cx + r, iy, &mut best);
}
}
}
best.and_then(|(d2, si, t, x, y)| {
let dist = d2.sqrt();
if dist > tolerance {
return None;
}
let s = &self.segs[si as usize];
let seg_len = ((s.x2 - s.x1).powi(2) + (s.y2 - s.y1).powi(2)).sqrt();
Some(Snap {
edge: s.edge as usize,
dist,
x,
y,
pos: s.pos0 + t * seg_len,
})
})
}
}
#[cfg(test)]
mod tests {
use super::*;
fn brute(vx: &[f64], vy: &[f64], offsets: &[usize], px: f64, py: f64) -> (usize, f64, f64) {
let mut best: Option<(f64, usize, f64)> = None; for e in 0..offsets.len() - 1 {
let mut pos0 = 0.0;
for i in offsets[e]..offsets[e + 1] - 1 {
let s = Seg {
x1: vx[i],
y1: vy[i],
x2: vx[i + 1],
y2: vy[i + 1],
edge: e as u32,
pos0,
};
let (d2, t, _, _, seg_len) = project(px, py, &s);
if best.map_or(true, |(bd2, ..)| d2 < bd2) {
best = Some((d2, e, pos0 + t * seg_len));
}
pos0 += seg_len;
}
}
let (d2, e, pos) = best.unwrap();
(e, d2.sqrt(), pos)
}
struct Rng(u64);
impl Rng {
fn next(&mut self) -> u64 {
let mut x = self.0;
x ^= x << 13;
x ^= x >> 7;
x ^= x << 17;
self.0 = x;
x
}
fn unif(&mut self) -> f64 {
(self.next() % 1_000_000) as f64 / 1_000_000.0
}
}
#[test]
fn snaps_to_the_interior_of_a_segment() {
let vx = vec![0.0, 10.0];
let vy = vec![0.0, 0.0];
let idx = SegmentIndex::build(&vx, &vy, &[0, 2]);
let s = idx.nearest(3.0, 4.0, f64::INFINITY).unwrap();
assert_eq!(s.edge, 0);
assert!((s.dist - 4.0).abs() < 1e-12);
assert!((s.x - 3.0).abs() < 1e-12 && s.y.abs() < 1e-12);
assert!((s.pos - 3.0).abs() < 1e-12);
}
#[test]
fn clamps_beyond_the_ends() {
let vx = vec![0.0, 10.0];
let vy = vec![0.0, 0.0];
let idx = SegmentIndex::build(&vx, &vy, &[0, 2]);
let s = idx.nearest(-5.0, 1.0, f64::INFINITY).unwrap();
assert!((s.pos - 0.0).abs() < 1e-12);
let s = idx.nearest(15.0, -1.0, f64::INFINITY).unwrap();
assert!((s.pos - 10.0).abs() < 1e-12);
}
#[test]
fn respects_the_tolerance() {
let vx = vec![0.0, 10.0];
let vy = vec![0.0, 0.0];
let idx = SegmentIndex::build(&vx, &vy, &[0, 2]);
assert!(idx.nearest(5.0, 3.0, 2.0).is_none());
assert!(idx.nearest(5.0, 3.0, 3.0).is_some());
}
#[test]
fn position_accumulates_over_polyline_vertices() {
let vx = vec![0.0, 10.0, 10.0];
let vy = vec![0.0, 0.0, 10.0];
let idx = SegmentIndex::build(&vx, &vy, &[0, 3]);
let s = idx.nearest(12.0, 4.0, f64::INFINITY).unwrap();
assert_eq!(s.edge, 0);
assert!((s.pos - 14.0).abs() < 1e-12);
assert!((s.x - 10.0).abs() < 1e-12 && (s.y - 4.0).abs() < 1e-12);
}
#[test]
fn empty_index_returns_none() {
let idx = SegmentIndex::build(&[], &[], &[0]);
assert!(idx.is_empty());
assert!(idx.nearest(0.0, 0.0, f64::INFINITY).is_none());
}
#[test]
fn matches_brute_force_on_random_networks() {
let mut rng = Rng(42);
for _ in 0..5 {
let n_edges = 80;
let mut vx = Vec::new();
let mut vy = Vec::new();
let mut offsets = vec![0usize];
for _ in 0..n_edges {
let nv = 2 + (rng.next() % 3) as usize;
for _ in 0..nv {
vx.push(rng.unif());
vy.push(rng.unif());
}
offsets.push(vx.len());
}
let idx = SegmentIndex::build(&vx, &vy, &offsets);
for _ in 0..200 {
let px = rng.unif() * 1.6 - 0.3;
let py = rng.unif() * 1.6 - 0.3;
let got = idx.nearest(px, py, f64::INFINITY).unwrap();
let (we, wd, wp) = brute(&vx, &vy, &offsets, px, py);
assert!(
(got.dist - wd).abs() < 1e-12,
"dist mismatch at ({px}, {py}): got {} want {wd}",
got.dist
);
if got.edge == we {
assert!((got.pos - wp).abs() < 1e-9);
}
}
}
}
}