use std::collections::BTreeMap;
use axiolid_contracts::{GeomResult, Sign};
use axiolid_core::{Point2, Point3};
use axiolid_exact::{Arith, Dyadic};
use axiolid_exact_arrangement::{ExactPoints, PointId};
pub(super) use axiolid_exact_arrangement::{Location, RingBox};
use axiolid_mesh::TriMesh;
use super::regions::Projection;
use super::{
internal_topology_error, mesh_index, Classification, EdgeKey, ExactSectionPlane, NodeKey,
};
use crate::coplanar::project;
pub(super) struct PlanePoints {
origin: [Dyadic; 3],
normal: [Dyadic; 3],
axis: usize,
points: ExactPoints,
by_node: BTreeMap<NodeKey, usize>,
values: BTreeMap<u32, Dyadic>,
}
fn exact(value: f64) -> GeomResult<Dyadic> {
Dyadic::try_from_f64(value).ok_or_else(|| {
axiolid_contracts::GeomError::Degenerate("cut ring point is not finite".into())
})
}
fn exact_point(point: Point3) -> GeomResult<[Dyadic; 3]> {
Ok([exact(point.x)?, exact(point.y)?, exact(point.z)?])
}
fn dot(left: &[Dyadic; 3], right: &[Dyadic; 3]) -> Dyadic {
left[0]
.mul(&right[0])
.add(&left[1].mul(&right[1]))
.add(&left[2].mul(&right[2]))
}
fn minor(p: &[Dyadic; 3], q: &[Dyadic; 3], i: usize, j: usize) -> Dyadic {
p[i].mul(&q[j]).sub(&q[i].mul(&p[j]))
}
fn sub3(left: &[Dyadic; 3], right: &[Dyadic; 3]) -> [Dyadic; 3] {
[
left[0].sub(&right[0]),
left[1].sub(&right[1]),
left[2].sub(&right[2]),
]
}
impl PlanePoints {
pub(super) fn new(plane: &ExactSectionPlane, projection: Projection) -> GeomResult<Self> {
let origin = exact_point(plane.origin)?;
let to_x = sub3(&exact_point(plane.x_point)?, &origin);
let to_y = sub3(&exact_point(plane.y_point)?, &origin);
let normal = [
minor(&to_x, &to_y, 1, 2),
minor(&to_x, &to_y, 2, 0),
minor(&to_x, &to_y, 0, 1),
];
Ok(Self {
origin,
normal,
axis: projection.axis(),
points: ExactPoints::with_frame_sign(projection.frame_sign()),
by_node: BTreeMap::new(),
values: BTreeMap::new(),
})
}
pub(super) fn store(&self) -> &ExactPoints {
&self.points
}
pub(super) fn store_mut(&mut self) -> &mut ExactPoints {
&mut self.points
}
fn plane_value(&self, point: Point3) -> GeomResult<Dyadic> {
let offset = sub3(&exact_point(point)?, &self.origin);
Ok(dot(&self.normal, &offset))
}
fn vertex_value(&mut self, index: u32, point: Point3) -> GeomResult<Dyadic> {
if let Some(value) = self.values.get(&index) {
return Ok(value.clone());
}
let value = self.plane_value(point)?;
self.values.insert(index, value.clone());
Ok(value)
}
pub(super) fn node(
&mut self,
mesh: &TriMesh,
classifications: &[Classification],
node: NodeKey,
) -> GeomResult<usize> {
if let Some(&index) = self.by_node.get(&node) {
return Ok(index);
}
let position = |index: u32| {
mesh.positions
.get(mesh_index(index)?)
.copied()
.ok_or_else(internal_topology_error)
};
let index = match node {
NodeKey::Vertex(vertex) => {
let certified = classifications
.get(mesh_index(vertex)?)
.ok_or_else(internal_topology_error)?;
if certified.sign != Sign::Zero {
return Err(internal_topology_error());
}
let point = position(vertex)?;
debug_assert_eq!(self.plane_value(point)?.sign(), Some(Sign::Zero));
self.push_vertex(point)?
}
NodeKey::Edge(EdgeKey(left, right)) => {
let (a, b) = (position(left)?, position(right)?);
let (da, db) = (self.vertex_value(left, a)?, self.vertex_value(right, b)?);
self.crossing_of(&da, &db, a, b)?
}
};
self.by_node.insert(node, index);
Ok(index)
}
#[cfg(test)]
pub(super) fn vertex(&mut self, point: Point3) -> GeomResult<usize> {
if self.plane_value(point)?.sign() != Some(Sign::Zero) {
return Err(internal_topology_error());
}
self.push_vertex(point)
}
fn push_vertex(&mut self, point: Point3) -> GeomResult<usize> {
Ok(self.points.push_flat(project(point, self.axis))?.0)
}
#[cfg(test)]
pub(super) fn crossing(&mut self, a: Point3, b: Point3) -> GeomResult<usize> {
let (da, db) = (self.plane_value(a)?, self.plane_value(b)?);
self.crossing_of(&da, &db, a, b)
}
fn crossing_of(&mut self, da: &Dyadic, db: &Dyadic, a: Point3, b: Point3) -> GeomResult<usize> {
let opposite = matches!(
(da.sign(), db.sign()),
(Some(Sign::Positive), Some(Sign::Negative))
| (Some(Sign::Negative), Some(Sign::Positive))
);
if !opposite {
return Err(internal_topology_error());
}
let (a, b): (Point2, Point2) = (project(a, self.axis), project(b, self.axis));
let point = [
da.mul(&exact(b.x)?).sub(&db.mul(&exact(a.x)?)),
da.mul(&exact(b.y)?).sub(&db.mul(&exact(a.y)?)),
da.sub(db),
];
let fixed = |from: f64, to: f64| (from == to).then_some(from);
let hints = [fixed(a.x, b.x), fixed(a.y, b.y)];
Ok(self.points.push_hinted(point, hints)?.0)
}
pub(super) fn segment_parameter(&self, ends: [usize; 4]) -> GeomResult<f64> {
self.points.segment_parameter(ends.map(PointId))
}
pub(super) fn bounds(&self, index: usize) -> [f64; 4] {
self.points.bounds(PointId(index))
}
#[cfg(test)]
pub(super) fn flat(&self, index: usize) -> Option<Point2> {
self.points.flat(PointId(index))
}
pub(super) fn exact(&self, index: usize) -> &[Dyadic; 3] {
self.points.exact(PointId(index))
}
pub(super) fn orient(&self, a: usize, b: usize, c: usize) -> GeomResult<Sign> {
self.points.orient(PointId(a), PointId(b), PointId(c))
}
pub(super) fn compare(
&self,
a: usize,
b: usize,
axis: usize,
) -> GeomResult<std::cmp::Ordering> {
self.points.compare(PointId(a), PointId(b), axis)
}
pub(super) fn same(&self, a: usize, b: usize) -> GeomResult<bool> {
self.points.same(PointId(a), PointId(b))
}
pub(super) fn lexicographic(&self, a: usize, b: usize) -> GeomResult<std::cmp::Ordering> {
self.points.lexicographic(PointId(a), PointId(b))
}
pub(super) fn dominant(&self, a: usize, b: usize) -> GeomResult<usize> {
self.points.dominant(PointId(a), PointId(b))
}
pub(super) fn ordered(&self, a: usize, b: usize, axis: usize) -> GeomResult<(usize, usize)> {
let (low, high) = self.points.ordered(PointId(a), PointId(b), axis)?;
Ok((low.0, high.0))
}
pub(super) fn strictly_between(
&self,
before: usize,
at: usize,
after: usize,
) -> GeomResult<bool> {
self.points
.strictly_between(PointId(before), PointId(at), PointId(after))
}
pub(super) fn within_segment(
&self,
point: usize,
start: usize,
end: usize,
) -> GeomResult<bool> {
self.points
.within_segment(PointId(point), PointId(start), PointId(end))
}
pub(super) fn locate(&self, point: usize, ring: &[usize]) -> GeomResult<Location> {
let ring: Vec<PointId> = ring.iter().copied().map(PointId).collect();
self.points.locate(PointId(point), &ring)
}
pub(super) fn ring_box(&self, ring: &[usize]) -> RingBox {
let ring: Vec<PointId> = ring.iter().copied().map(PointId).collect();
self.points.ring_box(&ring)
}
}
#[cfg(test)]
mod tests {
use std::cmp::Ordering;
use axiolid_contracts::GeomError;
use axiolid_core::{Frame3, Point2, Vec3};
use super::super::{classify_vertices, node_point, orient_sign, project_local};
use super::*;
fn plane_of(frame: Frame3) -> (ExactSectionPlane, Projection) {
let plane = ExactSectionPlane::new(frame).expect("plane");
let projection = Projection::new(&plane).expect("projection");
(plane, projection)
}
fn flat_frame() -> Frame3 {
Frame3 {
origin: Point3::new(0.0, 0.0, 0.5),
x: Vec3::X,
y: Vec3::Y,
z: Vec3::Z,
}
}
fn turned(k: u32) -> Frame3 {
let (s, c) = (std::f64::consts::PI * f64::from(k) / 64.0).sin_cos();
Frame3 {
origin: Point3::new(0.0, 0.0, 0.5),
x: Vec3::new(c, s, 0.0),
y: Vec3::new(-s, c, 0.0),
z: Vec3::Z,
}
}
fn column(x: f64) -> (Point3, Point3) {
(Point3::new(x, 0.1, 0.0), Point3::new(x, 0.1, 1.0))
}
#[test]
fn crossings_on_one_line_stay_collinear_where_rounded_points_do_not() {
let xs = [0.3, 0.9, 1.7];
let mut premise = false;
for k in 1..64 {
let frame = turned(k);
let (plane, projection) = plane_of(frame);
let mut points = PlanePoints::new(&plane, projection).expect("points");
let nodes: Vec<usize> = xs
.iter()
.map(|&x| {
let (a, b) = column(x);
points.crossing(a, b).expect("crossing")
})
.collect();
assert_eq!(
points.orient(nodes[0], nodes[1], nodes[2]).expect("orient"),
Sign::Zero,
"frame {k}"
);
let positions: Vec<Point3> = xs
.iter()
.flat_map(|&x| {
let (a, b) = column(x);
[a, b]
})
.collect();
let mesh = TriMesh::new(positions, vec![]);
let classifications = classify_vertices(&mesh, plane).expect("classes");
let rounded: Vec<_> = (0..3u32)
.map(|i| {
let node = NodeKey::Edge(EdgeKey::new(2 * i, 2 * i + 1));
let world = node_point(&mesh, &classifications, node).expect("point");
project_local(frame, world).expect("local")
})
.collect();
premise |= orient_sign(rounded[0], rounded[1], rounded[2]).expect("sign") != Sign::Zero;
}
assert!(premise, "no frame rounds the collinear points off the line");
}
#[test]
fn a_vertex_and_a_crossing_at_the_same_point_are_the_same() {
let (plane, projection) = plane_of(flat_frame());
let mut points = PlanePoints::new(&plane, projection).expect("points");
let crossing = points
.crossing(Point3::new(0.25, 0.5, 1.0), Point3::new(0.25, 0.5, -1.0))
.expect("crossing");
let vertex = points.vertex(Point3::new(0.25, 0.5, 0.5)).expect("vertex");
assert!(points.same(crossing, vertex).expect("same"));
let slanted = points
.crossing(Point3::new(0.0, 0.0, -1.0), Point3::new(1.0, 2.0, 3.0))
.expect("crossing");
let other = points
.vertex(Point3::new(0.375, 0.75, 0.5))
.expect("vertex");
assert!(points.same(slanted, other).expect("same"));
assert!(!points.same(slanted, vertex).expect("same"));
}
#[test]
fn a_crossing_with_a_binary64_position_takes_the_fast_path() {
let (plane, projection) = plane_of(flat_frame());
let mut points = PlanePoints::new(&plane, projection).expect("points");
let crossing = points
.crossing(Point3::new(0.0, 0.0, -1.0), Point3::new(1.0, 2.0, 3.0))
.expect("crossing");
assert_eq!(points.flat(crossing), Some(Point2::new(0.375, 0.75)));
let vertex = points
.vertex(Point3::new(0.375, 0.75, 0.5))
.expect("vertex");
assert!(points.same(crossing, vertex).expect("same"));
let third = points
.crossing(Point3::new(0.0, 0.0, 0.0), Point3::new(1.0, 1.0, 1.5))
.expect("crossing");
assert_eq!(points.flat(third), None);
assert!(!points.same(third, vertex).expect("same"));
let near = points
.vertex(Point3::new(0.375_f64.next_up(), 0.75, 0.5))
.expect("vertex");
assert!(!points.same(crossing, near).expect("same"));
assert_eq!(
points.lexicographic(crossing, near).expect("order"),
Ordering::Less
);
}
#[test]
fn a_crossing_does_not_depend_on_the_direction_of_its_edge() {
let (plane, projection) = plane_of(turned(5));
let mut points = PlanePoints::new(&plane, projection).expect("points");
let (a, b) = (Point3::new(0.1, 0.7, -0.3), Point3::new(-0.9, 0.2, 1.4));
let forward = points.crossing(a, b).expect("crossing");
let backward = points.crossing(b, a).expect("crossing");
assert!(points.same(forward, backward).expect("same"));
assert_eq!(points.exact(forward), points.exact(backward));
}
#[test]
fn the_box_of_a_crossing_contains_its_position() {
let frame = Frame3 {
origin: Point3::ZERO,
..flat_frame()
};
let (plane, projection) = plane_of(frame);
let mut points = PlanePoints::new(&plane, projection).expect("points");
let crossing = points
.crossing(Point3::new(0.0, 0.0, -1.0), Point3::new(1.0, 1.0, 2.0))
.expect("crossing");
let [u_lo, v_lo, u_hi, v_hi] = points.bounds(crossing);
let third = 1.0 / 3.0;
assert!(u_lo <= third && third <= u_hi && v_lo <= third && third <= v_hi);
assert!(u_hi - u_lo < 1e-12);
}
#[test]
fn a_mirrored_frame_reverses_the_orientation() {
let counter = flat_frame();
let mirrored = Frame3 {
y: -Vec3::Y,
z: -Vec3::Z,
..counter
};
for (frame, expected) in [(counter, Sign::Positive), (mirrored, Sign::Negative)] {
let (plane, projection) = plane_of(frame);
let mut points = PlanePoints::new(&plane, projection).expect("points");
let nodes: Vec<usize> = [(0.0, 0.0), (1.0, 0.0), (0.0, 1.0)]
.into_iter()
.map(|(x, y)| points.vertex(Point3::new(x, y, 0.5)).expect("vertex"))
.collect();
assert_eq!(
points.orient(nodes[0], nodes[1], nodes[2]).expect("orient"),
expected
);
}
}
fn slanted(points: &mut PlanePoints, thirds: &[f64]) -> Vec<usize> {
thirds
.iter()
.map(|&x| {
points
.crossing(Point3::new(0.0, 0.1, 0.0), Point3::new(x, 0.1, 1.5))
.expect("crossing")
})
.collect()
}
#[test]
fn homogeneous_points_decide_collinearity_exactly_in_either_frame_sense() {
let counter = flat_frame();
let mirrored = Frame3 {
y: -Vec3::Y,
z: -Vec3::Z,
..counter
};
for (frame, turn) in [(counter, Sign::Positive), (mirrored, Sign::Negative)] {
let (plane, projection) = plane_of(frame);
let mut points = PlanePoints::new(&plane, projection).expect("points");
let line = slanted(&mut points, &[1.0, 2.0, 4.0]);
assert!(line.iter().all(|&i| points.flat(i).is_none()));
assert_eq!(
points.orient(line[0], line[1], line[2]).expect("orient"),
Sign::Zero
);
let above = points.vertex(Point3::new(0.5, 0.5, 0.5)).expect("vertex");
assert_eq!(
points.orient(line[0], line[1], above).expect("orient"),
turn
);
}
}
#[test]
fn equal_homogeneous_points_compare_equal() {
let (plane, projection) = plane_of(flat_frame());
let mut points = PlanePoints::new(&plane, projection).expect("points");
let first = slanted(&mut points, &[1.0])[0];
let second = points
.crossing(Point3::new(0.0, 0.1, 0.0), Point3::new(2.0, 0.1, 3.0))
.expect("crossing");
assert!(points.same(first, second).expect("same"));
assert_eq!(
points.lexicographic(first, second).expect("order"),
Ordering::Equal
);
}
#[test]
fn an_edge_that_does_not_move_crosses_at_its_own_coordinates() {
let (plane, projection) = plane_of(flat_frame());
let mut points = PlanePoints::new(&plane, projection).expect("points");
let column = points
.crossing(Point3::new(0.25, 0.5, 1.0), Point3::new(0.25, 0.5, -1.0))
.expect("crossing");
assert_eq!(points.flat(column), Some(Point2::new(0.25, 0.5)));
}
#[test]
fn a_vertex_node_needs_a_certified_plane_side_of_zero() {
let (plane, projection) = plane_of(flat_frame());
let mut points = PlanePoints::new(&plane, projection).expect("points");
let mesh = TriMesh::new(vec![Point3::new(0.0, 0.0, 1.0)], vec![]);
let classifications = classify_vertices(&mesh, plane).expect("classes");
let off = points.node(&mesh, &classifications, NodeKey::Vertex(0));
assert!(matches!(
off,
Err(GeomError::BackendContractViolation { .. })
));
}
#[test]
fn a_vertex_off_the_plane_is_a_contract_violation() {
let (plane, projection) = plane_of(flat_frame());
let mut points = PlanePoints::new(&plane, projection).expect("points");
let off = points.vertex(Point3::new(0.0, 0.0, 0.5 + f64::EPSILON));
assert!(matches!(
off,
Err(GeomError::BackendContractViolation { .. })
));
let same_side = points.crossing(Point3::new(0.0, 0.0, 1.0), Point3::new(1.0, 0.0, 2.0));
assert!(matches!(
same_side,
Err(GeomError::BackendContractViolation { .. })
));
}
}