| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| use crate::model::{Edge, Pt, Vectors}; |
| use std::collections::HashMap; |
|
|
| |
| |
| fn push_unique(v: &mut Vec<f64>, x: f64) { |
| if !v.iter().any(|&y| y == x) { |
| v.push(x); |
| } |
| } |
|
|
| |
| #[inline] |
| fn add_piece(prow: &mut [f64], lrow: &mut [f64], xa: f64, xb: f64, dyp: f64, w: f64, width: usize) { |
| let xm = 0.5 * (xa + xb); |
| if xm <= 0.0 { |
| |
| return; |
| } |
| if xm >= width as f64 { |
| |
| lrow[width] += w * dyp; |
| return; |
| } |
| let c = xm as usize; |
| prow[c] += w * dyp * (xm - c as f64); |
| lrow[c] += w * dyp; |
| } |
|
|
| #[allow(clippy::too_many_arguments)] |
| fn add_subrow( |
| prow: &mut [f64], |
| lrow: &mut [f64], |
| xlo: f64, |
| ylo: f64, |
| xhi: f64, |
| yhi: f64, |
| w: f64, |
| width: usize, |
| ) { |
| let dy_total = yhi - ylo; |
| if dy_total <= 0.0 { |
| return; |
| } |
| if (xhi - xlo).abs() < 1e-12 { |
| add_piece(prow, lrow, xlo, xhi, dy_total, w, width); |
| return; |
| } |
| let (lo_x, hi_x) = if xlo < xhi { (xlo, xhi) } else { (xhi, xlo) }; |
|
|
| let mut breaks: Vec<f64> = Vec::with_capacity(8); |
| push_unique(&mut breaks, xlo); |
| push_unique(&mut breaks, xhi); |
| let k0 = lo_x.ceil() as i64; |
| let k1 = hi_x.floor() as i64; |
| for k in k0..=k1 { |
| let kf = k as f64; |
| if lo_x < kf && kf < hi_x && k > 0 && k < width as i64 { |
| push_unique(&mut breaks, kf); |
| } |
| } |
| for b in [0.0_f64, width as f64] { |
| if lo_x < b && b < hi_x { |
| push_unique(&mut breaks, b); |
| } |
| } |
|
|
| let inv = (yhi - ylo) / (xhi - xlo); |
| |
| let mut pieces: Vec<(f64, f64)> = breaks.iter().map(|&x| (ylo + (x - xlo) * inv, x)).collect(); |
| pieces.sort_by(|a, b| a.partial_cmp(b).expect("no NaN in break coordinates")); |
|
|
| for pair in pieces.windows(2) { |
| let (ya_, xa_) = pair[0]; |
| let (yb_, xb_) = pair[1]; |
| if yb_ > ya_ { |
| add_piece(prow, lrow, xa_, xb_, yb_ - ya_, w, width); |
| } |
| } |
| } |
|
|
| #[allow(clippy::too_many_arguments)] |
| fn add_edge( |
| partial: &mut [f64], |
| left: &mut [f64], |
| x0: f64, |
| y0: f64, |
| x1: f64, |
| y1: f64, |
| width: usize, |
| height: usize, |
| ) { |
| if y0 == y1 { |
| return; |
| } |
| let (w, xa, ya, xb, yb) = if y0 < y1 { |
| (1.0, x0, y0, x1, y1) |
| } else { |
| (-1.0, x1, y1, x0, y0) |
| }; |
| let dxdy = (xb - xa) / (yb - ya); |
| let y_top = ya.max(0.0); |
| let y_bot = yb.min(height as f64); |
| if y_bot <= y_top { |
| return; |
| } |
| let mut r = y_top.floor() as i64; |
| while (r as f64) < y_bot { |
| let ylo = y_top.max(r as f64); |
| let yhi = y_bot.min(r as f64 + 1.0); |
| if yhi > ylo { |
| let xr0 = xa + (ylo - ya) * dxdy; |
| let xr1 = xa + (yhi - ya) * dxdy; |
| let ri = r as usize; |
| let prow = &mut partial[ri * width..(ri + 1) * width]; |
| let lrow = &mut left[ri * (width + 1)..(ri + 1) * (width + 1)]; |
| add_subrow(prow, lrow, xr0, ylo, xr1, yhi, w, width); |
| } |
| r += 1; |
| } |
| } |
|
|
| |
| |
| pub fn polygon_coverage(loops: &[Vec<Pt>], width: usize, height: usize) -> Vec<f64> { |
| let mut partial = vec![0.0_f64; height * width]; |
| let mut left = vec![0.0_f64; height * (width + 1)]; |
|
|
| for poly in loops { |
| let n = poly.len(); |
| if n < 2 { |
| continue; |
| } |
| for i in 0..n { |
| let p0 = poly[i]; |
| let p1 = poly[(i + 1) % n]; |
| add_edge( |
| &mut partial, |
| &mut left, |
| p0[0], |
| p0[1], |
| p1[0], |
| p1[1], |
| width, |
| height, |
| ); |
| } |
| } |
|
|
| |
| |
| let mut out = vec![0.0_f64; height * width]; |
| for r in 0..height { |
| let lrow = &left[r * (width + 1)..(r + 1) * (width + 1)]; |
| let prow = &partial[r * width..(r + 1) * width]; |
| let orow = &mut out[r * width..(r + 1) * width]; |
| let mut acc = 0.0_f64; |
| for q in (0..width).rev() { |
| acc += lrow[q + 1]; |
| orow[q] = prow[q] + acc; |
| } |
| } |
| out |
| } |
|
|
| |
| fn allclose(a: Pt, b: Pt) -> bool { |
| const RTOL: f64 = 1e-5; |
| const ATOL: f64 = 1e-8; |
| (0..2).all(|i| (a[i] - b[i]).abs() <= ATOL + RTOL * b[i].abs()) |
| } |
|
|
| |
| |
| pub fn flatten_edge(e: &Edge) -> Vec<Pt> { |
| let mut out: Vec<Pt> = Vec::new(); |
| for (i, c) in e.cubics.iter().enumerate() { |
| if i == 0 { |
| out.extend_from_slice(c); |
| } else { |
| out.extend_from_slice(&c[1..]); |
| } |
| } |
| out |
| } |
|
|
| |
| pub fn flatten_loop(darts: &[[i64; 2]], by_id: &HashMap<i64, &Edge>) -> Vec<Pt> { |
| let mut out: Vec<Pt> = Vec::new(); |
| for d in darts { |
| let e = by_id |
| .get(&d[0]) |
| .unwrap_or_else(|| panic!("loop references edge {} absent from the export", d[0])); |
| let mut p = flatten_edge(e); |
| if d[1] < 0 { |
| p.reverse(); |
| } |
| if out.is_empty() { |
| out.extend_from_slice(&p); |
| } else { |
| out.extend_from_slice(&p[1..]); |
| } |
| } |
| if out.len() > 1 && allclose(out[0], out[out.len() - 1]) { |
| out.pop(); |
| } |
| out |
| } |
|
|
| |
| |
| pub fn region_coverages(v: &Vectors) -> Vec<(i64, Vec<f64>)> { |
| let by_id = v.edge_by_id(); |
| let mut out = Vec::with_capacity(v.faces.len()); |
| for f in &v.faces { |
| let loops: Vec<Vec<Pt>> = f |
| .loops |
| .iter() |
| .map(|l| flatten_loop(l, &by_id)) |
| .filter(|p| p.len() >= 3) |
| .collect(); |
| if loops.is_empty() { |
| continue; |
| } |
| let mut cov = polygon_coverage(&loops, v.width, v.height); |
| for x in cov.iter_mut() { |
| *x = x.abs(); |
| } |
| out.push((f.label, cov)); |
| } |
| out |
| } |
|
|
| #[cfg(test)] |
| mod tests { |
| use super::*; |
|
|
| |
| #[test] |
| fn axis_aligned_rectangle_is_exact() { |
| |
| let poly = vec![[2.25, 1.5], [7.75, 1.5], [7.75, 6.5], [2.25, 6.5]]; |
| let cov = polygon_coverage(&[poly], 10, 10); |
| let total: f64 = cov.iter().map(|x| x.abs()).sum(); |
| assert!( |
| (total - 27.5).abs() < 1e-12, |
| "rectangle area {total} != 27.5" |
| ); |
| } |
|
|
| #[test] |
| fn rectangle_interior_pixels_are_fully_covered() { |
| let poly = vec![[2.0, 2.0], [8.0, 2.0], [8.0, 8.0], [2.0, 8.0]]; |
| let cov = polygon_coverage(&[poly], 10, 10); |
| for r in 2..8 { |
| for c in 2..8 { |
| assert!( |
| (cov[r * 10 + c].abs() - 1.0).abs() < 1e-12, |
| "interior pixel ({r},{c}) = {}", |
| cov[r * 10 + c] |
| ); |
| } |
| } |
| |
| assert!(cov[0].abs() < 1e-12); |
| } |
|
|
| |
| #[test] |
| fn partial_column_is_exact() { |
| let poly = vec![[0.0, 0.0], [1.5, 0.0], [1.5, 1.0], [0.0, 1.0]]; |
| let cov = polygon_coverage(&[poly], 4, 1); |
| assert!((cov[0].abs() - 1.0).abs() < 1e-12, "col0 {}", cov[0]); |
| assert!((cov[1].abs() - 0.5).abs() < 1e-12, "col1 {}", cov[1]); |
| assert!(cov[2].abs() < 1e-12); |
| } |
|
|
| |
| #[test] |
| fn disk_area_matches_closed_form() { |
| let (cx, cy, rad) = (32.0_f64, 32.0_f64, 20.0_f64); |
| let n = 4096; |
| let poly: Vec<Pt> = (0..n) |
| .map(|i| { |
| let t = 2.0 * std::f64::consts::PI * (i as f64) / (n as f64); |
| [cx + rad * t.cos(), cy + rad * t.sin()] |
| }) |
| .collect(); |
| let cov = polygon_coverage(&[poly], 64, 64); |
| let total: f64 = cov.iter().map(|x| x.abs()).sum(); |
| let exact = std::f64::consts::PI * rad * rad; |
| |
| |
| let rel = (total - exact).abs() / exact; |
| assert!(rel < 1e-5, "disk area {total} vs {exact} (rel {rel:.2e})"); |
| } |
|
|
| |
| #[test] |
| fn complementary_regions_partition_to_one() { |
| |
| let left = vec![[0.0, 0.0], [2.4, 0.0], [2.4, 4.0], [0.0, 4.0]]; |
| let right = vec![[2.4, 0.0], [6.0, 0.0], [6.0, 4.0], [2.4, 4.0]]; |
| let a = polygon_coverage(&[left], 6, 4); |
| let b = polygon_coverage(&[right], 6, 4); |
| for i in 0..a.len() { |
| let s = a[i].abs() + b[i].abs(); |
| assert!((s - 1.0).abs() < 1e-12, "pixel {i} partition = {s}"); |
| } |
| } |
|
|
| #[test] |
| fn winding_sign_flips_with_orientation() { |
| let ccw = vec![[1.0, 1.0], [3.0, 1.0], [3.0, 3.0], [1.0, 3.0]]; |
| let cw: Vec<Pt> = ccw.iter().rev().copied().collect(); |
| let a = polygon_coverage(&[ccw], 5, 5); |
| let b = polygon_coverage(&[cw], 5, 5); |
| for i in 0..a.len() { |
| assert!((a[i] + b[i]).abs() < 1e-12, "signs did not mirror at {i}"); |
| } |
| } |
|
|
| #[test] |
| fn geometry_outside_the_raster_is_clipped_not_wrapped() { |
| |
| let poly = vec![[-5.0, -5.0], [3.0, -5.0], [3.0, 3.0], [-5.0, 3.0]]; |
| let cov = polygon_coverage(&[poly], 4, 4); |
| let total: f64 = cov.iter().map(|x| x.abs()).sum(); |
| assert!((total - 9.0).abs() < 1e-12, "clipped area {total} != 9"); |
| } |
| } |
|
|