//! Inward offsetting of polygons (spec §2.5). //! //! Used to compute the developable polygon `B'` of a block: `B` //! shrunk by the road setback distance `d_s`. For convex inputs this //! is exact. For concave inputs the result is the intersection of the //! inward half-planes of every edge — a conservative approximation //! that may shave off concave outer-corner detail but never produces //! an invalid polygon. use glam::DVec2; use super::polygon::Polygon; use super::EPS_GEOM; /// Compute the inward offset of `poly` by `distance` while preserving /// a 1-1 correspondence between input edges and output vertices. /// /// Vertex `i` of the returned ring is the intersection of the inward /// offset lines of input edges `i-1` and `i`. This is well-defined /// for convex inputs and modest offsets; for highly non-convex inputs /// or large offsets the result may self-intersect, in which case /// the caller should fall back to [`offset_inward`]. #[must_use] pub fn correlated_offset_inward(poly: &Polygon, distance: f64) -> Option> { let n = poly.len(); if n < 3 || distance < 0.0 { return None; } let verts = poly.vertices(); let mut offset_lines: Vec<(DVec2, DVec2)> = Vec::with_capacity(n); for i in 0..n { let p = verts[i]; let q = verts[(i + 1) % n]; let edge = q - p; let len = edge.length(); if len < EPS_GEOM { return None; } let tangent = edge / len; let inward = DVec2::new(-tangent.y, tangent.x); let p_off = p + inward * distance; let q_off = q + inward * distance; offset_lines.push((p_off, q_off)); } let mut new_verts = Vec::with_capacity(n); for i in 0..n { let prev = offset_lines[(i + n - 1) % n]; let curr = offset_lines[i]; let isect = line_line_intersect(prev.0, prev.1, curr.0, curr.1)?; new_verts.push(isect); } Some(new_verts) } fn line_line_intersect(p1: DVec2, p2: DVec2, p3: DVec2, p4: DVec2) -> Option { let d1 = p2 - p1; let d2 = p4 - p3; let denom = d1.x * d2.y - d1.y * d2.x; if denom.abs() < EPS_GEOM { return None; } let dp = p3 - p1; let t = (dp.x * d2.y - dp.y * d2.x) / denom; Some(p1 + d1 * t) } /// Compute the inward offset of `poly` by `distance`. Returns `None` /// when the result is empty (block too narrow to offset) or invalid. /// /// Implementation: clip `poly` once by the inward-shifted half-plane /// of each edge and validate the result. #[must_use] pub fn offset_inward(poly: &Polygon, distance: f64) -> Option { if distance.abs() < EPS_GEOM { return Some(poly.clone()); } if distance < 0.0 { return None; } let mut working = poly.clone(); for (a, b) in poly.edges().collect::>() { let edge = b - a; let len = edge.length(); if len < EPS_GEOM { continue; } // Inward normal of a CCW polygon: rotate the tangent +90° // (left-perpendicular). let tangent = edge / len; let inward = DVec2::new(-tangent.y, tangent.x); let shifted = a + inward * distance; match working.clip_half_plane(shifted, inward) { Some(p) => working = p, None => return None, } } Some(working) } #[cfg(test)] mod tests { use super::*; #[test] fn rectangle_inward_offset_shrinks() { let p = Polygon::new(vec![ DVec2::new(0.0, 0.0), DVec2::new(100.0, 0.0), DVec2::new(100.0, 50.0), DVec2::new(0.0, 50.0), ]) .unwrap(); let off = offset_inward(&p, 5.0).unwrap(); // Result should be the rectangle (5,5)-(95,45), area = 90*40. assert!((off.area() - 90.0 * 40.0).abs() < 1e-6); } #[test] fn too_large_offset_returns_none() { let p = Polygon::new(vec![ DVec2::new(0.0, 0.0), DVec2::new(10.0, 0.0), DVec2::new(10.0, 10.0), DVec2::new(0.0, 10.0), ]) .unwrap(); assert!(offset_inward(&p, 50.0).is_none()); } #[test] fn zero_offset_is_identity() { let p = Polygon::new(vec![ DVec2::new(0.0, 0.0), DVec2::new(1.0, 0.0), DVec2::new(1.0, 1.0), DVec2::new(0.0, 1.0), ]) .unwrap(); let off = offset_inward(&p, 0.0).unwrap(); assert!((off.area() - p.area()).abs() < 1e-12); } }