use alloc::vec::Vec;
use geometry_coords::CoordinateScalar;
use geometry_model::Ring;
use geometry_trait::{Point, PointMut, Polygon as PolygonTrait, Ring as RingTrait};
use crate::predicate::orientation::{Sign, orientation_2d};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Join {
Convex,
Concave,
Continue,
Spike,
}
fn join_at<P>(before: &P, corner: &P, after: &P) -> Join
where
P: Point,
P::Scalar: CoordinateScalar + Into<f64>,
{
match orientation_2d(before, corner, after) {
Sign::Negative => Join::Convex,
Sign::Positive => Join::Concave,
Sign::Collinear => {
let dot = (corner.get::<0>().into() - before.get::<0>().into())
* (after.get::<0>().into() - corner.get::<0>().into())
+ (corner.get::<1>().into() - before.get::<1>().into())
* (after.get::<1>().into() - corner.get::<1>().into());
if dot > 0.0 {
Join::Continue
} else {
Join::Spike
}
}
}
}
fn same_point<P>(left: &P, right: &P) -> bool
where
P: Point,
P::Scalar: CoordinateScalar,
{
left.get::<0>().tolerant_eq(right.get::<0>()) && left.get::<1>().tolerant_eq(right.get::<1>())
}
fn clockwise_view<R, P>(ring: &R) -> Vec<P>
where
R: RingTrait<Point = P>,
P: Point + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
{
let mut points: Vec<P> = Vec::new();
for point in ring.points() {
if points.last().is_none_or(|last| !same_point(last, point)) {
points.push(*point);
}
}
while points.len() > 1 && same_point(&points[0], &points[points.len() - 1]) {
points.pop();
}
if points.len() < 3 {
return points;
}
points.push(points[0]);
points
}
fn offsetted_ring<P>(closed: &[P]) -> Vec<P>
where
P: Point + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
{
let count = closed.len() - 1;
let mut out: Vec<P> = Vec::with_capacity(count * 2);
out.push(closed[0]);
for index in 0..count {
if index > 0 {
let before = closed[index - 1];
let corner = closed[index];
let after = closed[index + 1];
if join_at(&before, &corner, &after) == Join::Concave {
out.push(corner);
out.push(corner);
}
}
out.push(closed[index + 1]);
}
if count >= 2 {
let before = closed[count - 1];
let corner = closed[0];
let after = closed[1];
if join_at(&before, &corner, &after) == Join::Concave {
out.push(corner);
out.push(corner);
}
}
out
}
pub(crate) fn zero_width_rings<G, P>(polygon: &G) -> Vec<Ring<P>>
where
G: PolygonTrait<Point = P>,
P: PointMut + Default + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
{
let mut rings = Vec::new();
for ring in core::iter::once(polygon.exterior()).chain(polygon.interiors()) {
let closed = clockwise_view(ring);
if closed.len() < 4 {
continue;
}
rings.push(Ring::from_vec(offsetted_ring(&closed)));
}
rings
}
struct Step {
ring: usize,
at: usize,
from: [f64; 2],
to: [f64; 2],
}
fn turns<P>(rings: &[Ring<P>]) -> Vec<[f64; 2]>
where
P: Point + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
{
let mut steps: Vec<Step> = Vec::new();
for (ring, points) in rings.iter().enumerate() {
let coords: Vec<[f64; 2]> = points
.points()
.map(|point| [point.get::<0>().into(), point.get::<1>().into()])
.collect();
for pair in coords.windows(2) {
#[expect(clippy::float_cmp, reason = "recognising a repeated vertex")]
let repeated = pair[0][0] == pair[1][0] && pair[0][1] == pair[1][1];
if repeated {
continue;
}
steps.push(Step {
ring,
at: steps.iter().filter(|step| step.ring == ring).count(),
from: pair[0],
to: pair[1],
});
}
}
let mut found = Vec::new();
for (index, one) in steps.iter().enumerate() {
let length = steps.iter().filter(|step| step.ring == one.ring).count();
for two in steps.iter().skip(index + 1) {
if one.ring == two.ring {
let apart = two.at - one.at;
if apart <= 1 || apart + 1 >= length {
continue;
}
}
if let Some(point) = meeting_point(one, two) {
found.push(point);
}
}
}
found
}
fn meeting_point(one: &Step, two: &Step) -> Option<[f64; 2]> {
let cross = |a: [f64; 2], b: [f64; 2]| a[0] * b[1] - a[1] * b[0];
let sub = |a: [f64; 2], b: [f64; 2]| [a[0] - b[0], a[1] - b[1]];
let run = sub(one.to, one.from);
let other = sub(two.to, two.from);
let denominator = cross(run, other);
if denominator == 0.0 {
return None;
}
let offset = sub(two.from, one.from);
let along = cross(offset, other) / denominator;
let across = cross(offset, run) / denominator;
if !(0.0..=1.0).contains(&along) || !(0.0..=1.0).contains(&across) {
return None;
}
Some([one.from[0] + along * run[0], one.from[1] + along * run[1]])
}
pub(crate) enum ZeroWidthOutcome {
RingsStand,
NeedsTraversal,
}
pub(crate) fn zero_width_outcome<P>(rings: &[Ring<P>]) -> ZeroWidthOutcome
where
P: Point + Copy,
P::Scalar: CoordinateScalar + Into<f64>,
{
if turns(rings).is_empty() {
ZeroWidthOutcome::RingsStand
} else {
ZeroWidthOutcome::NeedsTraversal
}
}
#[cfg(test)]
mod tests {
use super::{ZeroWidthOutcome, zero_width_outcome, zero_width_rings};
use geometry_cs::Cartesian;
use geometry_model::{Point2D, Polygon, Ring};
use geometry_trait::{Point as _, Ring as _};
type P = Point2D<f64, Cartesian>;
fn ring(points: &[(f64, f64)]) -> Ring<P> {
let mut ring = Ring::new();
for &(x, y) in points {
ring.push(P::new(x, y));
}
ring
}
fn points_of(ring: &Ring<P>) -> Vec<(f64, f64)> {
ring.points()
.map(|point| (point.get::<0>(), point.get::<1>()))
.collect()
}
#[test]
fn a_concave_corner_is_emitted_three_times() {
let polygon = Polygon::new(ring(&[
(3186.0, 2762.0),
(3063.0, 2666.0),
(2953.0, 2536.0),
(2965.0, 2762.0),
(3023.0, 2920.0),
(3074.0, 2999.0),
(3186.0, 2762.0),
]));
let rings = zero_width_rings(&polygon);
assert_eq!(rings.len(), 1);
assert_eq!(
points_of(&rings[0]),
vec![
(3186.0, 2762.0),
(3063.0, 2666.0),
(3063.0, 2666.0),
(3063.0, 2666.0),
(2953.0, 2536.0),
(2965.0, 2762.0),
(3023.0, 2920.0),
(3074.0, 2999.0),
(3186.0, 2762.0),
]
);
assert!(matches!(
zero_width_outcome(&rings),
ZeroWidthOutcome::RingsStand
));
}
#[test]
fn a_ring_that_meets_itself_is_declined() {
let polygon = Polygon::new(ring(&[
(0.0, 0.0),
(10.0, 10.0),
(10.0, 0.0),
(0.0, 10.0),
(0.0, 0.0),
]));
let rings = zero_width_rings(&polygon);
assert!(matches!(
zero_width_outcome(&rings),
ZeroWidthOutcome::NeedsTraversal
));
}
#[test]
fn a_collapsed_exterior_leaves_its_interior_standing() {
let polygon = Polygon::with_inners(
ring(&[
(11.230_769_230_770_715, 4_095.000_000_000_000_5),
(11.230_769_230_770_715, 4_095.000_000_000_000_5),
]),
vec![ring(&[
(-11.0, 4091.0),
(-10.0, 4091.0),
(-10.0, 4_083.000_000_000_000_5),
(-2.0, 4087.0),
(-1.0, 4089.0),
(-4.0, 4092.0),
(-8.0, 4092.0),
(-9.0, 4093.0),
(-11.0, 4091.0),
])],
);
let rings = zero_width_rings(&polygon);
assert_eq!(rings.len(), 1, "the collapsed exterior encloses nothing");
assert!(matches!(
zero_width_outcome(&rings),
ZeroWidthOutcome::RingsStand
));
assert_eq!(points_of(&rings[0]).len(), 21);
}
}