//! Where boundary vertices may go: projection onto the B-rep faces and the feature curves
//! between them.
//!
//! The quality stage inserts and moves boundary vertices; each one stays on its B-rep face (or
//! feature curve) by projection. [`BoundaryGeometry`] answers the projection. Two sources:
//! - [`AnalyticSurface`]: the exact surface of a face when the geometry kernel knows it (plane,
//! cylinder, cone, sphere, torus), with closed-form closest points;
//! - [`Tessellation`]: the boundary surface's own triangles of that face, the fallback for
//! free-form faces (the projection is then accurate to the tessellation's chordal tolerance).
//!
//! A point on a feature curve between faces `A` and `B` is projected onto the curve's polyline
//! in the tessellation, then refined by alternating projections onto `A` and `B` when both are
//! analytic (von Neumann's alternating projections converge to the intersection for surfaces
//! meeting transversally); the result is kept only when it stays close to the polyline.
use crate::size::SizeField;
use crate::surface::BoundarySurface;
use super::measure::{self, P, add, cross, dot, norm, scale, sub};
/// A face's exact surface. Axes are unit vectors.
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum AnalyticSurface {
Plane {
normal: P,
point: P,
},
Cylinder {
origin: P,
axis: P,
radius: f64,
},
/// Apex, axis pointing into the cone, half-angle in radians.
Cone {
apex: P,
axis: P,
half_angle: f64,
},
Sphere {
center: P,
radius: f64,
},
Torus {
center: P,
axis: P,
major: f64,
minor: f64,
},
}
fn unit(v: P) -> Option
{
let l = norm(v);
(l > 0.0 && l.is_finite()).then(|| scale(v, 1.0 / l))
}
/// `(sin x, cos x)` for `|x| ≤ π` from their Taylor series (to 20 terms, accurate to rounding),
/// so the result is the same on every platform (the C library's `sin` and `cos` are not
/// correctly rounded and differ between platforms).
pub fn sin_cos(x: f64) -> (f64, f64) {
let x = x.clamp(-std::f64::consts::PI, std::f64::consts::PI);
let (mut s, mut c) = (0.0, 0.0);
let (mut ts, mut tc) = (x, 1.0);
for k in 0..20 {
s += ts;
c += tc;
let n = 2.0 * k as f64;
ts *= -x * x / ((n + 2.0) * (n + 3.0));
tc *= -x * x / ((n + 1.0) * (n + 2.0));
}
(s, c)
}
/// Any unit vector perpendicular to `a`.
fn perpendicular(a: P) -> P {
let t = if a[0].abs() < 0.9 { [1.0, 0.0, 0.0] } else { [0.0, 1.0, 0.0] };
unit(cross(a, t)).unwrap_or([0.0, 0.0, 1.0])
}
impl AnalyticSurface {
/// The closest point on the (untrimmed) surface, or `None` where it is not unique (on an
/// axis) or the surface's data is not finite.
pub fn project(&self, p: P) -> Option
{
let out = match *self {
AnalyticSurface::Plane { normal, point } => {
let n = unit(normal)?;
sub(p, scale(n, dot(sub(p, point), n)))
}
AnalyticSurface::Cylinder { origin, axis, radius } => {
let a = unit(axis)?;
let d = sub(p, origin);
let foot = add(origin, scale(a, dot(d, a)));
let r = unit(sub(p, foot))?;
add(foot, scale(r, radius))
}
AnalyticSurface::Sphere { center, radius } => add(center, scale(unit(sub(p, center))?, radius)),
AnalyticSurface::Cone { apex, axis, half_angle } => {
let a = unit(axis)?;
let d = sub(p, apex);
let along = dot(d, a);
let radial = sub(d, scale(a, along));
let r = unit(radial).unwrap_or_else(|| perpendicular(a));
// The generator through p's half-plane: direction g = cos·a + sin·r.
let (s, c) = sin_cos(half_angle);
let g = add(scale(a, c), scale(r, s));
let t = dot(d, g).max(0.0);
add(apex, scale(g, t))
}
AnalyticSurface::Torus { center, axis, major, minor } => {
let a = unit(axis)?;
let d = sub(p, center);
let radial = sub(d, scale(a, dot(d, a)));
let r = unit(radial)?;
let ring = add(center, scale(r, major));
add(ring, scale(unit(sub(p, ring))?, minor))
}
};
out.iter().all(|x| x.is_finite()).then_some(out)
}
/// The unit normal (up to sign) at a point on the surface, if defined there.
pub fn normal_at(&self, q: P) -> Option
{
let n = match *self {
AnalyticSurface::Plane { normal, .. } => normal,
AnalyticSurface::Cylinder { origin, axis, .. } => {
let a = unit(axis)?;
let d = sub(q, origin);
sub(d, scale(a, dot(d, a)))
}
AnalyticSurface::Sphere { center, .. } => sub(q, center),
AnalyticSurface::Cone { apex, axis, .. } => {
// Normal to both the generator through q and the circle through q.
let a = unit(axis)?;
let g = sub(q, apex);
let tangent = cross(a, g);
cross(g, tangent)
}
AnalyticSurface::Torus { center, axis, major, .. } => {
let a = unit(axis)?;
let d = sub(q, center);
let r = unit(sub(d, scale(a, dot(d, a))))?;
sub(q, add(center, scale(r, major)))
}
};
unit(n)
}
/// The smallest radius of curvature at `p` (a cone's depends on the distance to its axis).
pub fn radius_at(&self, p: P) -> f64 {
match *self {
AnalyticSurface::Cone { apex, axis, half_angle } => {
let Some(a) = unit(axis) else { return f64::INFINITY };
let d = sub(p, apex);
let rho = norm(sub(d, scale(a, dot(d, a))));
// Normal curvature of the circle of radius rho: cos(α)/rho.
rho / sin_cos(half_angle).1.abs().max(1e-12)
}
AnalyticSurface::Plane { .. } => f64::INFINITY,
AnalyticSurface::Cylinder { radius, .. } | AnalyticSurface::Sphere { radius, .. } => radius.abs(),
AnalyticSurface::Torus { minor, .. } => minor.abs(),
}
}
}
/// sin 10°: faces meeting at a smaller angle are treated as tangent along their curve.
const TRANSVERSAL_SIN: f64 = 0.173_648_177_666_930_35;
/// The projections the quality stage needs.
pub trait BoundaryGeometry {
/// The closest point to `p` on B-rep face `face`, near `p` (within a few element sizes).
fn project_face(&self, face: u32, p: P) -> Option
;
/// The closest point to `p` on the feature curve between faces `faces` (equal for a sharp
/// edge inside one face).
fn project_curve(&self, faces: [u32; 2], p: P) -> Option
;
}
/// A bounding-box tree over the boundary's triangles (or its feature segments) for
/// closest-point queries, filtered by tag.
#[derive(Debug, Clone)]
struct Tree {
nodes: Vec,
items: Vec,
}
#[derive(Debug, Clone, Copy)]
struct Node {
lo: P,
hi: P,
/// Children (left; right is left + 1) or a leaf over `items[start..end]`.
left: Option,
start: usize,
end: usize,
}
const LEAF: usize = 6;
impl Tree {
fn build(boxes: &[(P, P)]) -> Tree {
let mut items: Vec = (0..boxes.len() as u32).collect();
let empty = Node { lo: [0.0; 3], hi: [0.0; 3], left: None, start: 0, end: 0 };
let mut nodes = vec![empty];
let mut work = vec![(0usize, 0usize, boxes.len())];
while let Some((ni, start, end)) = work.pop() {
let Some(slice) = items.get_mut(start..end) else { continue };
let (mut lo, mut hi) = ([f64::INFINITY; 3], [f64::NEG_INFINITY; 3]);
for &i in slice.iter() {
if let Some((a, b)) = boxes.get(i as usize) {
for k in 0..3 {
lo[k] = lo[k].min(a[k]);
hi[k] = hi[k].max(b[k]);
}
}
}
let mut node = Node { lo, hi, left: None, start, end };
if end - start > LEAF {
let ext = [hi[0] - lo[0], hi[1] - lo[1], hi[2] - lo[2]];
let axis = if ext[0] >= ext[1] && ext[0] >= ext[2] {
0
} else if ext[1] >= ext[2] {
1
} else {
2
};
let key = |i: &u32| boxes.get(*i as usize).map_or(0.0, |(a, b)| a[axis] + b[axis]);
let mid = (end - start) / 2;
slice.select_nth_unstable_by(mid, |a, b| key(a).total_cmp(&key(b)).then(a.cmp(b)));
let left = nodes.len();
nodes.push(empty);
nodes.push(empty);
node.left = Some(left);
work.push((left, start, start + mid));
work.push((left + 1, start + mid, end));
}
if let Some(n) = nodes.get_mut(ni) {
*n = node;
}
}
Tree { nodes, items }
}
/// The item nearest to `p` among those `accept` takes, by `distance` (squared, with the
/// point and a fourth value carried along), searching boxes closer than the best so far.
fn nearest4(&self, p: P, accept: &dyn Fn(u32) -> bool, distance: &dyn Fn(u32) -> (f64, [f64; 4])) -> Option<[f64; 4]> {
let box_d2 = |n: &Node| -> f64 {
(0..3)
.map(|k| {
let d = (n.lo[k] - p[k]).max(0.0).max(p[k] - n.hi[k]);
d * d
})
.sum()
};
let mut best: Option<(f64, [f64; 4])> = None;
let mut stack = vec![0usize];
let mut steps = 0usize;
while let Some(ni) = stack.pop() {
steps += 1;
if steps > 4 * self.nodes.len() + 16 {
break;
}
let Some(n) = self.nodes.get(ni) else { continue };
if best.is_some_and(|(d, _)| box_d2(n) > d) {
continue;
}
match n.left {
Some(l) => {
let (a, b) = (self.nodes.get(l), self.nodes.get(l + 1));
let (da, db) = (a.map_or(f64::INFINITY, box_d2), b.map_or(f64::INFINITY, box_d2));
if da <= db {
stack.push(l + 1);
stack.push(l);
} else {
stack.push(l);
stack.push(l + 1);
}
}
None => {
for &i in self.items.get(n.start..n.end).unwrap_or_default() {
if !accept(i) {
continue;
}
let (d, q) = distance(i);
if best.is_none_or(|(b, _)| d < b) {
best = Some((d, q));
}
}
}
}
}
best.map(|(_, q)| q)
}
}
/// The closest point on triangle `abc` (Ericson, *Real-Time Collision Detection*, §5.1.5).
pub fn closest_on_triangle(p: P, a: P, b: P, c: P) -> P {
let (ab, ac, ap) = (sub(b, a), sub(c, a), sub(p, a));
let (d1, d2) = (dot(ab, ap), dot(ac, ap));
if d1 <= 0.0 && d2 <= 0.0 {
return a;
}
let bp = sub(p, b);
let (d3, d4) = (dot(ab, bp), dot(ac, bp));
if d3 >= 0.0 && d4 <= d3 {
return b;
}
let vc = d1 * d4 - d3 * d2;
if vc <= 0.0 && d1 >= 0.0 && d3 <= 0.0 {
let v = d1 / (d1 - d3);
return add(a, scale(ab, v));
}
let cp = sub(p, c);
let (d5, d6) = (dot(ab, cp), dot(ac, cp));
if d6 >= 0.0 && d5 <= d6 {
return c;
}
let vb = d5 * d2 - d1 * d6;
if vb <= 0.0 && d2 >= 0.0 && d6 <= 0.0 {
let w = d2 / (d2 - d6);
return add(a, scale(ac, w));
}
let va = d3 * d6 - d5 * d4;
if va <= 0.0 && (d4 - d3) >= 0.0 && (d5 - d6) >= 0.0 {
let w = (d4 - d3) / ((d4 - d3) + (d5 - d6));
return add(b, scale(sub(c, b), w));
}
let den = 1.0 / (va + vb + vc);
let (v, w) = (vb * den, vc * den);
let q = add(a, add(scale(ab, v), scale(ac, w)));
if q.iter().all(|x| x.is_finite()) { q } else { a }
}
/// The closest point on segment `ab`.
pub fn closest_on_segment(p: P, a: P, b: P) -> P {
let ab = sub(b, a);
let l2 = dot(ab, ab);
if !(l2 > 0.0) {
return a;
}
let t = (dot(sub(p, a), ab) / l2).clamp(0.0, 1.0);
measure::lerp(a, b, t)
}
/// The boundary surface as projection target, optionally with exact surfaces per face.
#[derive(Debug, Clone)]
pub struct Tessellation {
points: Vec
,
triangles: Vec<[u32; 3]>,
tri_face: Vec,
tri_tree: Tree,
/// Feature segments: vertices and the (sorted) face pair.
segments: Vec<([u32; 2], [u32; 2])>,
seg_tree: Tree,
analytic: Vec