Files
OneCAD-Simulation/crates/mesh/src/predicates/mod.rs
T
02cc9dab57 Boundary surface preparation (weld, manifold/orientation, exact self-intersection, feature edges) and mesh.surface.check (#15)
* mesh: boundary surface preparation (weld, manifold and orientation checks, exact self-intersection, feature edges)

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: mesh.surface.check on an open design's body or an imported surface

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* engine: struct-init the test session

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* surface: weld at an absolute distance too; the engine welds CAD tessellations at twice SolidCraft's linear tolerance

Faces of a SolidCraft tessellation carry their own copies of shared boundary points, which agree
only to within the kernel's tolerance (up to ~2e-6 mm on the test corpus).

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* docs: surface preparation in the literature survey, architecture and roadmap

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* surface: simplify after review

- The weld distance travels with the mesh (SurfaceMesh::weld_distance, set by Shape::tessellate),
  so every consumer welds a tessellation correctly without opting in.
- Exact tests are infallible on predicates::orient2d_projected (shared with generic_orient2d), and
  compute each vertex's plane side once per pair with an early reject.
- One Box3 for the weld, the BVH and shells; a Half struct for edges; the weld's two lookups share
  one loop, with chained grid cells; cached normals for feature edges; shared parameter helpers.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

* corpus: boundary surface preparation as a corpus stage

20 of 21 parts are valid boundaries with their expected genus; cross-cylinders' tessellation has
cracks and flipped triangles where its two intersection curves meet.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm

---------

Co-authored-by: jyviko <[email protected]>
Co-authored-by: Claude Opus 5.5 <[email protected]>
2026-10-13 16:01:42 -04:00

177 lines
5.8 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Exact geometric predicates: the sign of `orient2d`, `orient3d`, `incircle` and `insphere`
//! for every admissible input, symbolic perturbation for the Delaunay tie-breaks, and indirect
//! predicates on points constructed as line–plane and three-plane intersections.
//!
//! # Sources
//! - J. R. Shewchuk, *Adaptive Precision Floating-Point Arithmetic and Fast Robust Geometric
//! Predicates*, DCG 18:305–363, 1997: expansions (§2), adaptive evaluation (§3), the four
//! predicates and their error bounds (§4). [`orient2d`], [`orient3d`], [`incircle`],
//! [`insphere`] and their `*_filter` stage-A variants.
//! - O. Devillers, M. Teillaud, *Perturbations for Delaunay and weighted Delaunay 3D
//! triangulations*, CGTA 44(3):160–168, 2011; H. Edelsbrunner, E. P. Mücke, *Simulation of
//! Simplicity*, ACM TOG 9(1), 1990: [`insphere_sos`], [`incircle_sos`].
//! - M. Attene, *Indirect Predicates for Geometric Constructions*, CAD 126:102856, 2020 (paper
//! only; his code is LGPL and is not read): [`Lpi`], [`Tpi`], [`GenericPoint`] and the
//! `generic_*` predicates.
//!
//! # Arithmetic assumptions
//! IEEE 754 binary64, round-to-nearest-even, no extended intermediate precision, no FMA
//! contraction. Rust's `f64` gives exactly that on x86_64 (SSE2), aarch64 and
//! wasm32-unknown-unknown: rustc never fuses `a * b + c` and never uses x87 on these targets, and
//! the code uses no platform intrinsics, so every predicate returns the same sign on every
//! platform. The results are deterministic functions of the inputs.
//!
//! # Domain
//! Shewchuk's error bounds and the exactness of expansion arithmetic need no overflow and no
//! underflow (§5). Every coordinate of a [`Point2`] / [`Point3`] is therefore either zero or has
//! magnitude in [[`MIN_MAGNITUDE`], [`MAX_MAGNITUDE`]] = [2⁻¹²⁰, 2¹⁶⁰]: then every intermediate
//! value of the degree-5 `insphere` is either zero or above 2⁻⁸⁶⁰ and below 2⁸¹², all normal
//! doubles. The constructors reject anything else (NaN, infinities, out-of-range magnitudes) with
//! [`crate::Error::Invalid`]; the predicates themselves are total and never fail. Callers snap
//! tessellation noise (e.g. `1e-40`) to zero before building points.
mod adaptive;
mod dyadic;
mod eft;
mod expansion;
mod indirect;
mod interval;
mod ring;
mod sos;
pub use adaptive::{incircle, incircle_filter, insphere, insphere_filter, orient2d, orient2d_filter, orient3d, orient3d_filter};
pub use indirect::{Axis, GenericPoint, Lpi, Tpi, generic_compare, generic_orient2d, generic_orient3d, orient2d_projected};
pub use sos::{incircle_sos, insphere_sos};
use crate::{Error, Result};
/// The smallest nonzero coordinate magnitude a point may have: 2⁻¹²⁰ ≈ 7.5e-37.
pub const MIN_MAGNITUDE: f64 = f64::from_bits((1023 - 120) << 52);
/// The largest coordinate magnitude a point may have: 2¹⁶⁰ ≈ 1.5e48.
pub const MAX_MAGNITUDE: f64 = f64::from_bits((1023 + 160) << 52);
/// The sign of a predicate.
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub enum Sign {
Negative = -1,
Zero = 0,
Positive = 1,
}
impl Sign {
/// The sign of a double (`Zero` for both zeros; NaN, which the domain excludes, maps to
/// `Zero`).
#[inline(always)]
pub fn of(x: f64) -> Sign {
if x > 0.0 {
Sign::Positive
} else if x < 0.0 {
Sign::Negative
} else {
Sign::Zero
}
}
#[inline(always)]
pub fn is_zero(self) -> bool {
self == Sign::Zero
}
}
impl std::ops::Neg for Sign {
type Output = Sign;
#[inline(always)]
fn neg(self) -> Sign {
match self {
Sign::Negative => Sign::Positive,
Sign::Zero => Sign::Zero,
Sign::Positive => Sign::Negative,
}
}
}
impl std::ops::Mul for Sign {
type Output = Sign;
#[inline(always)]
fn mul(self, rhs: Sign) -> Sign {
match (self, rhs) {
(Sign::Zero, _) | (_, Sign::Zero) => Sign::Zero,
(a, b) if a == b => Sign::Positive,
_ => Sign::Negative,
}
}
}
/// The sign of the volume `det[b − a, c − a, d − a]`: positive when `d` lies on the side of
/// face `a, b, c`'s right-hand normal (the tet4 node order). It is [`orient3d`] negated.
#[inline]
pub fn tet_volume_sign(a: &Point3, b: &Point3, c: &Point3, d: &Point3) -> Sign {
-orient3d(a, b, c, d)
}
/// Whether `x` may be a point coordinate (see the module docs).
#[inline]
pub fn is_admissible(x: f64) -> bool {
x == 0.0 || (x.abs() >= MIN_MAGNITUDE && x.abs() <= MAX_MAGNITUDE)
}
fn check<const N: usize>(c: [f64; N]) -> Result<[f64; N]> {
match c.iter().find(|x| !is_admissible(**x)) {
None => Ok(c),
Some(x) => {
Err(Error::Invalid(format!("point coordinate {x:e} is outside the exact-predicate domain (zero, or magnitude in [2^-120, 2^160])")))
}
}
}
/// A 2D point whose coordinates are admissible for the exact predicates.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point2([f64; 2]);
impl Point2 {
pub fn new(x: f64, y: f64) -> Result<Self> {
check([x, y]).map(Self)
}
#[inline(always)]
pub fn coords(&self) -> [f64; 2] {
self.0
}
}
impl TryFrom<[f64; 2]> for Point2 {
type Error = Error;
fn try_from(c: [f64; 2]) -> Result<Self> {
check(c).map(Self)
}
}
/// A 3D point whose coordinates are admissible for the exact predicates.
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Point3([f64; 3]);
impl Point3 {
pub fn new(x: f64, y: f64, z: f64) -> Result<Self> {
check([x, y, z]).map(Self)
}
#[inline(always)]
pub fn coords(&self) -> [f64; 3] {
self.0
}
}
impl TryFrom<[f64; 3]> for Point3 {
type Error = Error;
fn try_from(c: [f64; 3]) -> Result<Self> {
check(c).map(Self)
}
}
#[cfg(test)]
mod tests;