143 lines
4.4 KiB
Rust
143 lines
4.4 KiB
Rust
//! 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<Vec<DVec2>> {
|
|
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<DVec2> {
|
|
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<Polygon> {
|
|
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::<Vec<_>>() {
|
|
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);
|
|
}
|
|
}
|