feat(math/polygon_tangents): Implement polygon handover

Fixes #240
This commit is contained in:
Ellen Emilia Anna Zscheile 2025-06-04 22:23:02 +02:00 committed by mikolaj
parent 371d13e7e1
commit 03d85b8566
2 changed files with 195 additions and 22 deletions

View File

@ -15,12 +15,34 @@ pub use polygon_tangents::*;
mod tangents; mod tangents;
pub use tangents::*; pub use tangents::*;
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum RotationSense { pub enum RotationSense {
Counterclockwise, Counterclockwise,
Clockwise, Clockwise,
} }
impl core::ops::Neg for RotationSense {
type Output = Self;
fn neg(self) -> Self {
match self {
RotationSense::Counterclockwise => RotationSense::Clockwise,
RotationSense::Clockwise => RotationSense::Counterclockwise,
}
}
}
impl RotationSense {
/// move `pos` by `step` along `self` assuming the list of positions is ordered CCW.
pub fn step_ccw(self, pos: usize, len: usize, mut step: usize) -> usize {
step %= len;
(match self {
RotationSense::Counterclockwise => pos + step,
RotationSense::Clockwise => len + pos - step,
}) % len
}
}
#[derive(Clone, Copy, Debug, PartialEq)] #[derive(Clone, Copy, Debug, PartialEq)]
pub enum LineIntersection { pub enum LineIntersection {
Empty, Empty,

View File

@ -4,8 +4,9 @@
use super::{ use super::{
between_vectors_cached, cyclic_breadth_partition_search, dot_product, perp_dot_product, between_vectors_cached, cyclic_breadth_partition_search, dot_product, perp_dot_product,
RotationSense,
}; };
use geo::Point; use geo::{algorithm::Centroid, Point, Polygon};
#[derive(Clone, Debug, thiserror::Error, PartialEq)] #[derive(Clone, Debug, thiserror::Error, PartialEq)]
pub enum PolyTangentException<I> { pub enum PolyTangentException<I> {
@ -49,8 +50,36 @@ impl<I: Copy> CachedPolyExt<I> {
) )
} }
pub fn centroid(&self) -> Point {
Polygon::new(self.0.iter().map(|(pt, _, _)| *pt).collect(), Vec::new())
.centroid()
.unwrap()
}
fn is_outside(&self, i: usize, origin: Point) -> bool {
let poly_ext = &self.0;
let len = poly_ext.len();
let prev = &poly_ext[(len + i - 1) % len];
let cur = &poly_ext[i];
let next = &poly_ext[(i + 1) % len];
// local coordinate system with origin at `cur.0`.
between_vectors_cached(cur.0 - origin, cur.0 - prev.0, cur.0 - next.0, cur.2)
}
fn is_rev_outside(&self, i: usize, origin: Point) -> bool {
let poly_ext = &self.0;
let len = poly_ext.len();
let prev = &poly_ext[(len + i - 1) % len];
let cur = &poly_ext[i];
let next = &poly_ext[(i + 1) % len];
// local coordinate system with origin at `cur.0`.
between_vectors_cached(origin - cur.0, cur.0 - prev.0, cur.0 - next.0, cur.2)
}
/// Calculates the tangents to the polygon exterior going through point `origin`. /// Calculates the tangents to the polygon exterior going through point `origin`.
pub fn tangent_points(&self, origin: Point) -> Option<(I, I)> { fn tangent_points_intern(&self, origin: Point) -> Option<(usize, usize)> {
let poly_ext = &self.0; let poly_ext = &self.0;
let len = poly_ext.len(); let len = poly_ext.len();
debug_assert!(len > 1); debug_assert!(len > 1);
@ -74,24 +103,11 @@ impl<I: Copy> CachedPolyExt<I> {
// In `Self::new` we force CCw. // In `Self::new` we force CCw.
let (pos_false, pos_true) = if let (Some(pos_false), Some(pos_true)) = let (pos_false, pos_true) = if let (Some(pos_false), Some(pos_true)) =
cyclic_breadth_partition_search(0..len, |i: usize| { cyclic_breadth_partition_search(0..len, |i: usize| self.is_outside(i, origin))
let prev = &poly_ext[(len + i - 1) % len]; {
let cur = &poly_ext[i];
let next = &poly_ext[(i + 1) % len];
// local coordinate system with origin at `cur.0`.
between_vectors_cached(cur.0 - origin, cur.0 - prev.0, cur.0 - next.0, cur.2)
}) {
((pos_false + 1) % len, pos_true) ((pos_false + 1) % len, pos_true)
} else if let (Some(mut rev_pos_false), Some(mut rev_pos_true)) = } else if let (Some(rev_pos_false), Some(rev_pos_true)) =
cyclic_breadth_partition_search(0..len, |i: usize| { cyclic_breadth_partition_search(0..len, |i: usize| self.is_rev_outside(i, origin))
let prev = &poly_ext[(len + i - 1) % len];
let cur = &poly_ext[i];
let next = &poly_ext[(i + 1) % len];
// local coordinate system with origin at `cur.0`.
between_vectors_cached(origin - cur.0, cur.0 - prev.0, cur.0 - next.0, cur.2)
})
{ {
// the following is necessary to find the "furthest" tangent points // the following is necessary to find the "furthest" tangent points
let same_direction = |pos: usize, vec: Point| { let same_direction = |pos: usize, vec: Point| {
@ -123,7 +139,13 @@ impl<I: Copy> CachedPolyExt<I> {
return None; return None;
}; };
Some((poly_ext[pos_true].1, poly_ext[pos_false].1)) Some((pos_true, pos_false))
}
/// Calculates the tangents to the polygon exterior going through point `origin`.
pub fn tangent_points(&self, origin: Point) -> Option<(I, I)> {
self.tangent_points_intern(origin)
.map(|(pos_min, pos_max)| (self.0[pos_min].1, self.0[pos_max].1))
} }
} }
@ -146,9 +168,98 @@ pub fn poly_ext_tangent_points<I: Copy>(
}) })
} }
/// Calculates the tangent between the polygons `source` and `target`,
/// according to their intended [`RotationSense`].
pub fn poly_ext_handover<I: Copy>(
source: &CachedPolyExt<I>,
source_sense: RotationSense,
target: &CachedPolyExt<I>,
target_sense: RotationSense,
) -> Option<(I, I)> {
use RotationSense::{Clockwise as Cw, Counterclockwise as CoCw};
let inv_source_sense = match source_sense {
CoCw => Cw,
Cw => CoCw,
};
let inv_target_sense = match target_sense {
CoCw => Cw,
Cw => CoCw,
};
// initialization
let mut pos_trg = {
let pos = target.tangent_points_intern(source.centroid())?;
match target_sense {
CoCw => pos.0,
Cw => pos.1,
}
};
let mut pos_src = {
let pos = source.tangent_points_intern(target.centroid())?;
match inv_source_sense {
CoCw => pos.0,
Cw => pos.1,
}
};
// compute flow (direction toward which to shift the positions)
/*
let (flow_src, flow_trg) = match (source_sense, target_sense) {
(CoCw, CoCw) => (Cw, CoCw),
(Cw, CoCw) => (Cw, Cw),
(CoCw, Cw) => (CoCw, CoCw),
(Cw, Cw) => (CoCw, Cw),
};
*/
let (flow_src, flow_trg) = (inv_target_sense, source_sense);
let mut modified = true;
while modified {
modified = false;
if !source.is_outside(pos_src, target.0[pos_trg].0)
|| !source.is_rev_outside(pos_src, target.0[pos_trg].0)
{
pos_src = flow_src.step_ccw(pos_src, source.0.len(), 1);
modified = true;
}
if !target.is_outside(pos_trg, source.0[pos_src].0)
|| !target.is_rev_outside(pos_trg, source.0[pos_src].0)
{
pos_trg = flow_trg.step_ccw(pos_trg, target.0.len(), 1);
modified = true;
}
}
// make extremal
let (xtflow_src, xtflow_trg) = (inv_source_sense, target_sense);
while modified {
modified = false;
let next_pos_src = xtflow_src.step_ccw(pos_src, source.0.len(), 1);
if source.is_outside(next_pos_src, target.0[pos_trg].0)
&& source.is_rev_outside(next_pos_src, target.0[pos_trg].0)
{
pos_src = next_pos_src;
modified = true;
}
let next_pos_trg = xtflow_trg.step_ccw(pos_trg, target.0.len(), 1);
if target.is_outside(next_pos_trg, source.0[pos_src].0)
&& target.is_rev_outside(next_pos_trg, source.0[pos_src].0)
{
pos_trg = next_pos_trg;
modified = true;
}
}
Some((source.0[pos_src].1, target.0[pos_trg].1))
}
#[cfg(test)] #[cfg(test)]
mod tests { mod tests {
use super::poly_ext_tangent_points as petp; use super::{
poly_ext_handover as pehov, poly_ext_tangent_points as petp, CachedPolyExt,
RotationSense::{Clockwise as Cw, Counterclockwise as CoCw},
};
use crate::drawing::dot::FixedDotIndex; use crate::drawing::dot::FixedDotIndex;
use geo::point; use geo::point;
@ -209,4 +320,44 @@ mod tests {
Ok((FixedDotIndex::new(2.into()), FixedDotIndex::new(0.into()))) Ok((FixedDotIndex::new(2.into()), FixedDotIndex::new(0.into())))
); );
} }
#[test]
fn handover00() {
let poly_ext_src = &[
(point! { x: 4., y: 0. }, FixedDotIndex::new(0.into())),
(point! { x: 3., y: 3. }, FixedDotIndex::new(1.into())),
(point! { x: 1., y: 2. }, FixedDotIndex::new(2.into())),
(point! { x: 1., y: -2. }, FixedDotIndex::new(3.into())),
(point! { x: 3., y: -3. }, FixedDotIndex::new(4.into())),
];
let source = CachedPolyExt::new(poly_ext_src, false);
let source = &source;
let poly_ext_trg = &[
(point! { x: -4., y: 0. }, FixedDotIndex::new(10.into())),
(point! { x: -3., y: 3. }, FixedDotIndex::new(11.into())),
(point! { x: -1., y: 2. }, FixedDotIndex::new(12.into())),
(point! { x: -1., y: -2. }, FixedDotIndex::new(13.into())),
(point! { x: -3., y: -3. }, FixedDotIndex::new(14.into())),
];
let target = CachedPolyExt::new(poly_ext_trg, true);
let target = &target;
assert_eq!(
pehov(source, CoCw, target, CoCw),
Some((FixedDotIndex::new(1.into()), FixedDotIndex::new(11.into())))
);
assert_eq!(
pehov(source, CoCw, target, Cw),
Some((FixedDotIndex::new(2.into()), FixedDotIndex::new(13.into())))
);
assert_eq!(
pehov(source, Cw, target, CoCw),
Some((FixedDotIndex::new(3.into()), FixedDotIndex::new(12.into())))
);
assert_eq!(
pehov(source, Cw, target, Cw),
Some((FixedDotIndex::new(4.into()), FixedDotIndex::new(14.into())))
);
}
} }