use core::ops::Range;
use axiolid_contracts::{GeomError, GeomResult};
use axiolid_core::Point2;
use super::{by_value, lexicographic, orient, segments_touch, within, Loop, PinchPolicy};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(super) struct Touch {
pub(super) ring: usize,
pub(super) index: usize,
pub(super) vertex: u32,
}
pub(super) fn check_rings(points: &[Point2], rings: &[Range<usize>]) -> GeomResult<()> {
for (r, ring) in rings.iter().enumerate() {
if ring.len() < 3 {
return refuse(format!(
"profile {} needs at least 3 vertices, got {}",
name(r),
ring.len()
));
}
if points[ring.clone()]
.iter()
.any(|p| !(p.x.is_finite() && p.y.is_finite()))
{
return refuse(format!("profile {} has a non-finite vertex", name(r)));
}
for k in 0..ring.len() {
let next = (k + 1) % ring.len();
if points[ring.start + k] == points[ring.start + next] {
return refuse(format!("profile {} repeats vertex {k}", name(r)));
}
}
for k in 0..ring.len() {
let at = |i: usize| points[ring.start + i % ring.len()];
if folds_back(at(k), at(k + 1), at(k + 2)) {
return refuse(format!(
"profile {} folds back on itself at vertex {}",
name(r),
(k + 1) % ring.len()
));
}
}
}
Ok(())
}
pub(super) fn orient_and_place(points: &[Point2], rings: &[Range<usize>]) -> GeomResult<Vec<Loop>> {
let mut loops = Vec::with_capacity(rings.len());
for (r, ring) in rings.iter().enumerate() {
let mut vertices: Vec<u32> = ring.clone().map(|i| i as u32).collect();
let counter_clockwise = ring_turns_left(&points[ring.clone()]);
if counter_clockwise != (r == 0) {
vertices.reverse();
}
loops.push(Loop { vertices });
}
let outer = &points[rings[0].clone()];
for (h, hole) in rings.iter().enumerate().skip(1) {
let probe = points[hole.start];
if !strictly_inside(outer, probe) {
return refuse(format!(
"profile hole {} lies outside the outer ring",
h - 1
));
}
for (g, other) in rings.iter().enumerate().skip(1) {
if g != h && strictly_inside(&points[other.clone()], probe) {
return refuse(format!("profile hole {} lies inside hole {}", h - 1, g - 1));
}
}
}
Ok(loops)
}
pub(super) fn refuse<T>(message: String) -> GeomResult<T> {
Err(GeomError::InvalidInput(message))
}
pub(super) fn name(ring: usize) -> String {
if ring == 0 {
"outer ring".to_owned()
} else {
format!("hole {}", ring - 1)
}
}
#[derive(Debug, Clone, Copy)]
struct Edge {
ring: usize,
source: usize,
index: usize,
a: Point2,
b: Point2,
ia: u32,
ib: u32,
}
pub(super) fn check_edges(
points: &[Point2],
rings: &[Range<usize>],
source: &[usize],
policy: PinchPolicy,
) -> GeomResult<Vec<Touch>> {
let mut edges = Vec::with_capacity(points.len());
for (r, ring) in rings.iter().enumerate() {
for k in 0..ring.len() {
let (ia, ib) = (ring.start + k, ring.start + (k + 1) % ring.len());
edges.push(Edge {
ring: r,
source: source[r],
index: k,
a: points[ia],
b: points[ib],
ia: ia as u32,
ib: ib as u32,
});
}
}
let mut touches = Vec::new();
edges.sort_by(|e, f| by_value(e.a.x.min(e.b.x), f.a.x.min(f.b.x)));
for (i, e) in edges.iter().enumerate() {
let right = e.a.x.max(e.b.x);
let (low, high) = (e.a.y.min(e.b.y), e.a.y.max(e.b.y));
for f in &edges[i + 1..] {
if f.a.x.min(f.b.x) > right {
break;
}
if f.a.y.max(f.b.y) < low || f.a.y.min(f.b.y) > high {
continue;
}
match policy {
PinchPolicy::Refuse => check_pair(rings, e, f)?,
PinchPolicy::Accept => check_pair_pinched(rings, e, f, &mut touches)?,
}
}
}
Ok(touches)
}
fn check_pair(rings: &[Range<usize>], e: &Edge, f: &Edge) -> GeomResult<()> {
if e.ring == f.ring {
let len = rings[e.ring].len();
if (e.index + 1) % len == f.index || (f.index + 1) % len == e.index {
return Ok(());
}
if segments_touch(e.a, e.b, f.a, f.b) {
return refuse(format!("profile {} intersects itself", name(e.ring)));
}
return Ok(());
}
if !segments_touch(e.a, e.b, f.a, f.b) {
return Ok(());
}
let (r, s) = (e.ring.min(f.ring), e.ring.max(f.ring));
if r == 0 {
refuse(format!(
"profile hole {} touches or crosses the outer ring",
s - 1
))
} else {
refuse(format!(
"profile holes {} and {} overlap or touch",
r - 1,
s - 1
))
}
}
fn check_pair_pinched(
rings: &[Range<usize>],
e: &Edge,
f: &Edge,
touches: &mut Vec<Touch>,
) -> GeomResult<()> {
if e.ring == f.ring {
let len = rings[e.ring].len();
if (e.index + 1) % len == f.index || (f.index + 1) % len == e.index {
return Ok(());
}
}
let touching = segments_touch(e.a, e.b, f.a, f.b);
if !touching {
return Ok(());
}
for (p, far_e) in [(e.a, e.b), (e.b, e.a)] {
for (q, far_f) in [(f.a, f.b), (f.b, f.a)] {
if p == q {
if far_e == far_f || folds_back(far_e, p, far_f) {
return overlap(e, f);
}
return Ok(());
}
}
}
let inside = |a: Point2, b: Point2, c: Point2| orient(a, b, c) == 0 && within(a, b, c);
let hits = [
(inside(e.a, e.b, f.a), e, f.ia),
(inside(e.a, e.b, f.b), e, f.ib),
(inside(f.a, f.b, e.a), f, e.ia),
(inside(f.a, f.b, e.b), f, e.ib),
];
let mut found = hits.iter().filter(|hit| hit.0);
match (found.next(), found.next()) {
(Some(&(_, edge, vertex)), None) => {
touches.push(Touch {
ring: edge.ring,
index: edge.index,
vertex,
});
Ok(())
}
(Some(_), Some(_)) => overlap(e, f),
_ => crossing(e, f),
}
}
fn overlap(e: &Edge, f: &Edge) -> GeomResult<()> {
let (r, s) = (e.source.min(f.source), e.source.max(f.source));
if r == s {
refuse(format!("profile {} overlaps itself", name(r)))
} else if r == 0 {
refuse(format!("profile hole {} overlaps the outer ring", s - 1))
} else {
refuse(format!("profile holes {} and {} overlap", r - 1, s - 1))
}
}
fn crossing(e: &Edge, f: &Edge) -> GeomResult<()> {
let (r, s) = (e.source.min(f.source), e.source.max(f.source));
if r == s {
refuse(format!("profile {} intersects itself", name(r)))
} else if r == 0 {
refuse(format!("profile hole {} crosses the outer ring", s - 1))
} else {
refuse(format!("profile holes {} and {} cross", r - 1, s - 1))
}
}
pub(super) fn folds_back(a: Point2, v: Point2, b: Point2) -> bool {
if orient(a, v, b) != 0 {
return false;
}
let same = |p: f64, q: f64, o: f64| (p < o && q < o) || (p > o && q > o);
same(a.x, b.x, v.x) || (a.x == v.x && same(a.y, b.y, v.y))
}
pub(super) fn ring_turns_left(ring: &[Point2]) -> bool {
let n = ring.len();
let lowest = (0..n)
.min_by(|&i, &j| lexicographic(ring[i], ring[j]))
.unwrap_or(0);
orient(
ring[(lowest + n - 1) % n],
ring[lowest],
ring[(lowest + 1) % n],
) > 0
}
pub(super) fn strictly_inside(ring: &[Point2], p: Point2) -> bool {
let mut inside = false;
for k in 0..ring.len() {
let (a, b) = (ring[k], ring[(k + 1) % ring.len()]);
if (a.y > p.y) == (b.y > p.y) {
continue;
}
let side = orient(a, b, p);
if (b.y > a.y && side > 0) || (b.y < a.y && side < 0) {
inside = !inside;
}
}
inside
}