ogeom_algo/distance.rs
1//! Signed distances from many points to a mesh or a shape.
2//!
3//! [`to_mesh`] measures to a triangle mesh through its [`Bvh`], and
4//! [`to_shape`] to a B-rep's own surfaces, curves and points, never to a
5//! tessellation of them. Both answer every point independently, in
6//! parallel, in the points' order, and the same points give the same
7//! answers bit for bit on any thread count.
8//!
9//! # The sign
10//!
11//! A distance is positive outside and negative inside. On a mesh the side
12//! is read from the closest feature's angle-weighted pseudonormal: the
13//! triangle's normal where the closest point is inside a triangle, the sum
14//! of the two triangles' normals where it is on an edge, and the sum of the
15//! triangles' normals at a vertex weighted by their corner angles there.
16//! For a closed, consistently wound mesh the displacement from the closest
17//! point to the query lies in that feature's normal cone, and its dot with
18//! the pseudonormal has the sign of the side, so a point beside an edge or
19//! a corner is never read on the wrong side. Corners that share a position
20//! are one vertex, so a mesh written with its vertices repeated per face
21//! reads as the surface it draws.
22//!
23//! On a shape the side is the face's own: the oriented normal at the foot
24//! where the closest point is inside a face, the sum of the faces' normals
25//! where it is on an edge (the displacement then lies in the plane across
26//! the edge, between the two normals outside or their opposites inside),
27//! and the solid's own classification of the point where it is at a vertex.
28//! A shape with no closed boundary, or a point the classification puts on
29//! it, reads a vertex by the sum of its faces' normals.
30//!
31//! # Facing
32//!
33//! With [`DistanceOptions::facing_limit`], each point carries a normal of
34//! its own (a scan's) and only surface turned within the limit of it is
35//! measured to: on a mesh, the triangles whose normals are; on a shape,
36//! the face feet whose normals are, and the edges and vertices where one of
37//! their faces' normals is. The nearest of what is left is the answer, so
38//! a point scanned on one side of a thin wall reads that side however near
39//! the other is. The test is on cosines, `n·m ≥ cos(limit)` for unit
40//! normals, so a surface exactly at the limit is in when the rounding of
41//! the cosine allows. The sign is still the side of the feature the
42//! answer lies on, read as above.
43
44mod mesh;
45mod shape;
46
47use ogeom_core::{OgeomResult, ogeom_bail};
48use ogeom_math::{Point, Vector};
49use ogeom_topo::Shape;
50
51pub use mesh::to_mesh;
52pub(crate) use mesh::{Pseudonormals, to_mesh_with};
53pub use shape::to_shape;
54pub(crate) use shape::{Passed, Target};
55
56#[cfg(doc)]
57use ogeom_mesh::Bvh;
58
59/// How [`to_mesh`] and [`to_shape`] measure.
60#[derive(Debug, Clone, Copy, PartialEq)]
61pub struct DistanceOptions {
62 /// The farthest a closest point may be; a point with nothing within it
63 /// answers `None`. May be infinite.
64 pub max: f64,
65 /// The greatest angle, in radians, between a point's own normal and
66 /// the surface it is measured to, in `[0, π]`; `None` measures to all
67 /// of it.
68 pub facing_limit: Option<f64>,
69 /// Whether a point inside reads negative. Unsigned distances are never
70 /// negative.
71 pub signed: bool,
72}
73
74impl Default for DistanceOptions {
75 /// Everything, at any distance, signed.
76 fn default() -> Self {
77 Self {
78 max: f64::INFINITY,
79 facing_limit: None,
80 signed: true,
81 }
82 }
83}
84
85impl DistanceOptions {
86 /// The options checked: `max` not negative or undefined, and the facing
87 /// limit, where there is one, in `[0, π]`.
88 ///
89 /// # Errors
90 ///
91 /// [`OgeomError::Domain`](ogeom_core::OgeomError::Domain) for either out
92 /// of its range.
93 pub fn check(&self) -> OgeomResult<()> {
94 if self.max.is_nan() || self.max < 0.0 {
95 ogeom_bail!(
96 Domain,
97 "the farthest distance {} must not be negative",
98 self.max
99 );
100 }
101 if let Some(limit) = self.facing_limit
102 && !(0.0..=core::f64::consts::PI).contains(&limit)
103 {
104 ogeom_bail!(Domain, "the facing limit {limit} must be in [0, π]");
105 }
106 Ok(())
107 }
108
109 /// The options checked against the points and their normals: normals
110 /// where there is a facing limit, as many of them as points, and the
111 /// cosine of the limit where there is one.
112 fn checked(&self, points: usize, normals: Option<&[Vector]>) -> OgeomResult<Option<f64>> {
113 self.check()?;
114 if let Some(normals) = normals
115 && normals.len() != points
116 {
117 ogeom_bail!(Dimension, "{} normals for {points} points", normals.len());
118 }
119 let Some(limit) = self.facing_limit else {
120 return Ok(None);
121 };
122 if normals.is_none() {
123 ogeom_bail!(Domain, "a facing limit needs the points' normals");
124 }
125 Ok(Some(limit.cos()))
126 }
127}
128
129/// What a point is nearest to.
130#[derive(Debug, Clone, PartialEq)]
131pub struct Closest<F> {
132 /// The distance to [`Closest::point`]: negative inside where the
133 /// distance is signed.
134 pub distance: f64,
135 /// The closest point.
136 pub point: Point,
137 /// The face, edge or vertex it lies on.
138 pub feature: F,
139 /// The unit normal the sign is read from: the face's, or at an edge or
140 /// a vertex the pseudonormal there. Zero where the faces' normals
141 /// cancel.
142 pub normal: Vector,
143}
144
145/// A feature of a triangle mesh, by a triangle that has it.
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
147pub enum MeshFeature {
148 /// Inside the triangle.
149 Face(u32),
150 /// On the triangle's edge `k`, from its corner `k` to its corner
151 /// `k + 1` (mod 3).
152 Edge(u32, u8),
153 /// At the triangle's corner `k`.
154 Vertex(u32, u8),
155}
156
157/// A feature of a shape: the face, edge or vertex the closest point lies
158/// on, as the shape holds it.
159#[derive(Debug, Clone, PartialEq)]
160pub enum ShapeFeature {
161 /// Inside a face.
162 Face(Shape),
163 /// On an edge.
164 Edge(Shape),
165 /// At a vertex.
166 Vertex(Shape),
167}
168
169/// A point's own normal made unit, or `None` where it has no direction.
170fn unit(v: Vector) -> Option<Vector> {
171 let length = v.magnitude();
172 (length > 0.0 && length.is_finite()).then(|| v / length)
173}
174
175/// `v` made unit, or zero where it has no direction.
176fn unit_or_zero(v: Vector) -> Vector {
177 unit(v).unwrap_or(Vector::ZERO)
178}
179
180/// The distance signed by the side `normal` puts `p` on, seen from the
181/// closest point `at`: negative where the displacement runs against it.
182fn signed(distance: f64, p: Point, at: Point, normal: Vector, sign: bool) -> f64 {
183 if sign && (p - at).dot(normal) < 0.0 {
184 -distance
185 } else {
186 distance
187 }
188}