Skip to main content

brepkit_math/
lib.rs

1//! # brepkit-math
2//!
3//! Vectors, matrices, NURBS, analytic curves and surfaces, and exact
4//! geometric predicates. Layer L0 of the brepkit CAD kernel, with no
5//! workspace dependencies.
6//!
7//! # What is here
8//!
9//! | Area | Modules |
10//! |------|---------|
11//! | Linear algebra | [`mod@vec`], [`mat`], [`plane`], [`frame`] |
12//! | NURBS | [`nurbs`] (evaluation, knot operations, fitting, projection, intersection) |
13//! | Analytic curves | [`curves`], [`curves2d`] |
14//! | Analytic surfaces | [`surfaces`], [`analytic_intersection`] |
15//! | Robustness | [`tolerance`], [`predicates`], [`filtered`] |
16//! | Spatial structures | [`aabb`], [`obb`], [`bvh`], [`cdt`], [`convex_hull`] |
17//! | 2D polygons | [`polygon2d`], [`polygon_offset`] |
18//!
19//! # Points are not vectors
20//!
21//! [`Point3`](vec::Point3) (a position) and [`Vec3`](vec::Vec3) (a direction)
22//! are separate types rather than one three-float struct. Subtracting two
23//! points gives a vector; adding a vector to a point gives a point; adding two
24//! points is not defined. The distinction is load-bearing under transforms: a
25//! [`Mat4`](mat::Mat4) translates a point but must not translate a direction,
26//! and conflating the two is a classic source of silently wrong normals.
27//!
28//! # The tolerance model
29//!
30//! Floating-point coordinates never compare equal in the way geometry needs.
31//! A wire that closes to within a billionth of a millimetre has closed, and a
32//! kernel that insists on bit equality will reject every real model. So
33//! measured comparisons, distances, angles, and coordinates go through
34//! [`Tolerance`](tolerance::Tolerance), which bundles three thresholds.
35//! Orientation decisions are the deliberate exception, covered below.
36//!
37//! | Field | Default | Meaning |
38//! |-------|---------|---------|
39//! | `linear` | `1e-7` | Distance below which two points are the same point |
40//! | `angular` | `1e-12` rad | Angle below which two directions are parallel |
41//! | `relative` | `1e-10` | Fraction of the larger operand, for scale-aware comparison |
42//!
43//! Three presets are provided. [`Tolerance::new`](tolerance::Tolerance::new)
44//! is the CAD default above. [`loose`](tolerance::Tolerance::loose)
45//! (`1e-4`/`1e-8`/`1e-6`) suits visualization and rough checks.
46//! [`tight`](tolerance::Tolerance::tight) (`1e-10`/`1e-15`/`1e-14`) suits
47//! high-precision work, at the cost of rejecting geometry that a looser
48//! setting would accept.
49//!
50//! ## Scale-aware by default
51//!
52//! [`approx_eq`](tolerance::Tolerance::approx_eq) is not a plain epsilon
53//! compare. It returns true when
54//!
55//! ```text
56//! |a - b| <= max(linear, relative * max(|a|, |b|))
57//! ```
58//!
59//! The relative term is what keeps the comparison meaningful at any
60//! magnitude. Two coordinates near `1e6` differ by more than `1e-7` purely
61//! from rounding, and an absolute-only test would call them distinct forever.
62//!
63//! That scaling is wrong for quantities that are not coordinates. A dot
64//! product, a determinant, or anything already normalized should use
65//! [`approx_eq_abs`](tolerance::Tolerance::approx_eq_abs), which compares
66//! against `linear` alone. Reaching for `approx_eq` on a near-zero dot product
67//! works, because the relative term vanishes, but on a large one it silently
68//! widens the threshold.
69//!
70//! ## When exactness is required
71//!
72//! Some decisions cannot be tolerance-based at all. Whether a point is left
73//! of a line, or above a plane, has to be consistent across every call or the
74//! algorithm built on it will contradict itself and produce a non-manifold
75//! result. The [`predicates`] module provides filtered exact orientation
76//! tests ([`orient2d`](predicates::orient2d),
77//! [`orient3d`](predicates::orient3d)) that compute in floating point, check
78//! whether the error bound admits the answer, and fall back to exact
79//! arithmetic only when it does not. They are fast in the common case and
80//! never wrong in the degenerate one.
81//!
82//! ## When tolerance bites
83//!
84//! Two situations account for most tolerance trouble:
85//!
86//! - **Geometry far from the origin.** Doubles carry roughly 15 significant
87//!   digits. Near a coordinate of `1e7` the gap between representable values
88//!   is about `1.9e-9`, so a `1e-7` linear tolerance sits only some 50 times
89//!   above the noise floor. Booleans on far-flung parts lose precision well
90//!   before they lose correctness. Translate the part near the origin,
91//!   operate, and translate back.
92//! - **Units much smaller than a millimetre.** The defaults assume millimetre
93//!   scale. Modelling in micrometres makes `1e-7` of your unit a distance
94//!   the kernel cannot resolve, and distinct points start merging. Model in
95//!   millimetres and scale at export.
96//!
97//! As a rule, keep coordinates roughly within `1e0` to `1e4` in your chosen
98//! units and the defaults take care of themselves.
99//!
100//! # Analytic first, NURBS as the general case
101//!
102//! Curves and surfaces are enums, not one universal representation.
103//! [`Circle3D`](curves::Circle3D) is a circle, not a rational B-spline that
104//! happens to be circular. Analytic types get closed-form intersections where
105//! a pair admits one (see [`analytic_intersection`]), which is both faster and
106//! exact. NURBS is what everything can convert into and what free-form
107//! geometry uses: the fallback, not the default.
108//!
109//! # Example
110//!
111//! ```
112//! use brepkit_math::curves::Circle3D;
113//! use brepkit_math::tolerance::Tolerance;
114//! use brepkit_math::vec::{Point3, Vec3};
115//!
116//! let center = Point3::new(0.0, 0.0, 0.0);
117//! let circle = Circle3D::new(center, Vec3::new(0.0, 0.0, 1.0), 2.0)?;
118//!
119//! let tol = Tolerance::new();
120//! let start = circle.evaluate(0.0);
121//! let quarter = circle.evaluate(std::f64::consts::FRAC_PI_2);
122//!
123//! // Every point sits one radius from the center, in the plane the normal
124//! // defines, and a quarter turn is a right angle.
125//! assert!(tol.approx_eq((quarter - center).length(), 2.0));
126//! assert!(tol.approx_eq_abs(quarter.z(), 0.0));
127//! assert!(tol.approx_eq_abs((start - center).dot(quarter - center), 0.0));
128//!
129//! // Which direction `evaluate(0.0)` points is set by the frame derived from
130//! // the normal. Use `Circle3D::new_with_ref` when the seam position matters.
131//! # Ok::<(), brepkit_math::MathError>(())
132//! ```
133//!
134//! # See also
135//!
136//! - [`brepkit_topology`](https://docs.rs/brepkit-topology): the B-Rep
137//!   structures these types give shape to.
138//! - [`brepkit_operations`](https://docs.rs/brepkit-operations): the modeling
139//!   operations most projects call instead of this crate directly.
140//! - [brepjs.dev](https://brepjs.dev/concepts/tolerance): the same tolerance
141//!   model from the TypeScript side, with guidance on when to heal.
142
143/// Errors from math operations.
144#[derive(Debug, thiserror::Error)]
145pub enum MathError {
146    /// Knot vector length does not match control points and degree.
147    #[error("invalid knot vector: expected {expected} knots, got {got}")]
148    InvalidKnotVector {
149        /// Expected number of knots.
150        expected: usize,
151        /// Actual number of knots.
152        got: usize,
153    },
154
155    /// Weights vector length does not match control points.
156    #[error("invalid weights: expected {expected} weights, got {got}")]
157    InvalidWeights {
158        /// Expected number of weights.
159        expected: usize,
160        /// Actual number of weights.
161        got: usize,
162    },
163
164    /// Control point grid dimensions are inconsistent.
165    #[error(
166        "invalid control point grid: expected {expected_rows}x{expected_cols}, got inconsistent dimensions"
167    )]
168    InvalidControlPointGrid {
169        /// Expected number of rows.
170        expected_rows: usize,
171        /// Expected number of columns.
172        expected_cols: usize,
173    },
174
175    /// Cannot normalize a zero-length vector.
176    #[error("cannot normalize zero vector")]
177    ZeroVector,
178
179    /// Matrix is singular and cannot be inverted.
180    #[error("singular matrix cannot be inverted")]
181    SingularMatrix,
182
183    /// Input collection is empty where at least one element is required.
184    #[error("empty input where at least one element is required")]
185    EmptyInput,
186
187    /// Parameter is outside the valid range.
188    #[error("parameter {value} out of range [{min}, {max}]")]
189    ParameterOutOfRange {
190        /// The out-of-range value.
191        value: f64,
192        /// Lower bound of the valid range.
193        min: f64,
194        /// Upper bound of the valid range.
195        max: f64,
196    },
197
198    /// Newton iteration did not converge within the allowed iterations.
199    #[error("Newton iteration did not converge after {iterations} iterations")]
200    ConvergenceFailure {
201        /// Number of iterations attempted.
202        iterations: usize,
203    },
204}
205
206pub mod aabb;
207pub mod analytic_intersection;
208pub mod bvh;
209pub mod cdt;
210pub mod chord;
211pub mod convex_hull;
212pub mod curves;
213pub mod curves2d;
214pub mod det_hash;
215pub mod filtered;
216pub mod frame;
217pub mod mat;
218pub mod nurbs;
219pub mod obb;
220pub mod plane;
221pub mod polygon2d;
222pub mod polygon_boolean;
223pub mod polygon_offset;
224pub mod predicates;
225pub mod quadrature;
226pub mod ray_triangle;
227pub mod surfaces;
228pub mod tolerance;
229pub mod traits;
230pub mod vec;
231
232#[cfg(feature = "simd")]
233pub mod simd;