200 lines
8.3 KiB
Rust
200 lines
8.3 KiB
Rust
//! What surface a face lies on (plane, cylinder, cone, sphere, torus), for construction
|
|
//! geometry: axes through round faces, centres of spheres and tori.
|
|
|
|
use solidcraft_geom::Vec3;
|
|
|
|
use truck_modeling as mt;
|
|
|
|
use crate::Body;
|
|
use crate::body::from_p3;
|
|
use crate::offset::{Surf, surf_of};
|
|
|
|
/// A face's analytic surface. Axes are unit vectors.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub enum FaceSurface {
|
|
/// Outward unit normal and a point on the plane.
|
|
Plane {
|
|
normal: Vec3,
|
|
point: Vec3,
|
|
},
|
|
Cylinder {
|
|
origin: Vec3,
|
|
axis: Vec3,
|
|
radius: f64,
|
|
},
|
|
/// Apex, axis pointing into the cone, half-angle (radians).
|
|
Cone {
|
|
apex: Vec3,
|
|
axis: Vec3,
|
|
half_angle: f64,
|
|
},
|
|
Sphere {
|
|
center: Vec3,
|
|
radius: f64,
|
|
},
|
|
/// Centre of the ring, its axis, the ring (major) and tube (minor) radii.
|
|
Torus {
|
|
center: Vec3,
|
|
axis: Vec3,
|
|
major: f64,
|
|
minor: f64,
|
|
},
|
|
}
|
|
|
|
/// The surface of face `face` (in face order), if it is one of the analytic kinds.
|
|
pub fn face_surface(b: &Body, face: usize) -> Option<FaceSurface> {
|
|
surface_of(b.solid.face_iter().nth(face)?, b.size())
|
|
}
|
|
|
|
/// The analytic surface of face `f` of a body whose box diagonal is `size`.
|
|
pub(crate) fn surface_of(f: &mt::Face, size: f64) -> Option<FaceSurface> {
|
|
let tol = (size * 1e-7).max(1e-9) * 100.0;
|
|
Some(match surf_of(f, tol)? {
|
|
Surf::Plane { n, d } => FaceSurface::Plane { normal: n, point: n * d },
|
|
Surf::Cylinder { o, a, r, .. } => FaceSurface::Cylinder { origin: o, axis: a, radius: r },
|
|
Surf::Cone { v, a, half, .. } => FaceSurface::Cone { apex: v, axis: a, half_angle: half },
|
|
Surf::Sphere { c, r, .. } => FaceSurface::Sphere { center: c, radius: r },
|
|
Surf::Torus { o, a, big, h, r, .. } => FaceSurface::Torus { center: o + a * h, axis: a, major: big, minor: r },
|
|
})
|
|
}
|
|
|
|
/// The circle (or arc of one) an edge's curve lies on, from its exact curve.
|
|
#[derive(Clone, Copy, Debug, PartialEq)]
|
|
pub struct EdgeCircle {
|
|
pub center: Vec3,
|
|
/// Unit normal of the circle's plane, oriented so the edge runs counter-clockwise about it.
|
|
pub axis: Vec3,
|
|
pub radius: f64,
|
|
/// The edge's ends (the same point for a closed circle).
|
|
pub start: Vec3,
|
|
pub end: Vec3,
|
|
/// The angle the edge spans (radians, 2π for a whole circle).
|
|
pub angle: f64,
|
|
}
|
|
|
|
/// The circle edge `edge` (in the order of [`Body::edges`] and the tessellated mesh's edges)
|
|
/// lies on, if its curve is a circle or an arc of one. `None` for other curves, mesh bodies
|
|
/// and an index out of range.
|
|
pub fn edge_circle(b: &Body, edge: usize) -> Option<EdgeCircle> {
|
|
use mt::{BoundedCurve, ParametricCurve};
|
|
if b.mesh.is_some() {
|
|
return None;
|
|
}
|
|
let e = Body::unique_edges(&b.solid).into_iter().nth(edge)?;
|
|
let tol = (b.size() * 1e-7).max(1e-9) * 100.0;
|
|
crate::guard("edge circle", || {
|
|
let c = e.oriented_curve();
|
|
let (t0, t1) = c.range_tuple();
|
|
let at = |f: f64| from_p3(c.subs(t0 + (t1 - t0) * f));
|
|
let samples: Vec<Vec3> = (0..=24).map(|i| at(f64::from(i) / 24.0)).collect();
|
|
Ok(circle_through(&samples, tol))
|
|
})
|
|
.ok()
|
|
.flatten()
|
|
}
|
|
|
|
/// The circle through evenly spaced samples of a curve (first to last), if they all lie on one.
|
|
fn circle_through(s: &[Vec3], tol: f64) -> Option<EdgeCircle> {
|
|
let (start, end) = (*s.first()?, *s.last()?);
|
|
let closed = start.dist(end) <= tol;
|
|
// Three well-spread samples: a quarter, half and three quarters for a closed curve.
|
|
let n = s.len().checked_sub(1)?;
|
|
let (a, b, c) = if closed { (*s.get(n / 4)?, *s.get(n / 2)?, *s.get(3 * n / 4)?) } else { (start, *s.get(n / 2)?, end) };
|
|
let (u, v) = (b - a, c - a);
|
|
let w = u.cross(v);
|
|
if !(w.len2() > u.len2() * v.len2() * 1e-16) {
|
|
return None;
|
|
}
|
|
let center = a + (v * u.len2() - u * v.len2()).cross(w) * (1.0 / (2.0 * w.len2()));
|
|
let axis = w.normalized()?;
|
|
let radius = center.dist(a);
|
|
if !(radius.is_finite() && radius > tol) || s.iter().any(|p| (p.dist(center) - radius).abs() > tol || (*p - center).dot(axis).abs() > tol) {
|
|
return None;
|
|
}
|
|
let x = (start - center).normalized()?;
|
|
let y = axis.cross(x);
|
|
let turn = |p: Vec3| {
|
|
let d = p - center;
|
|
let t = d.dot(y).atan2(d.dot(x));
|
|
if t < 0.0 { t + std::f64::consts::TAU } else { t }
|
|
};
|
|
// Accumulate the turn sample to sample so the span is right whichever way the curve runs.
|
|
let mut angle = 0.0;
|
|
let mut last = 0.0;
|
|
for p in s.iter().skip(1) {
|
|
let t = turn(*p);
|
|
let mut d = t - last;
|
|
if d < -std::f64::consts::PI {
|
|
d += std::f64::consts::TAU;
|
|
} else if d > std::f64::consts::PI {
|
|
d -= std::f64::consts::TAU;
|
|
}
|
|
angle += d;
|
|
last = t;
|
|
}
|
|
let (axis, angle) = if angle < 0.0 { (axis * -1.0, -angle) } else { (axis, angle) };
|
|
let angle = if closed { std::f64::consts::TAU } else { angle.min(std::f64::consts::TAU) };
|
|
Some(EdgeCircle { center, axis, radius, start, end, angle })
|
|
}
|
|
|
|
impl FaceSurface {
|
|
/// The point of the whole surface (not only of its face) nearest `p`. `None` where every
|
|
/// point of a ring is as near (on a cylinder's or cone's axis, at a sphere's or torus's
|
|
/// centre), and for a point that is not finite.
|
|
pub fn nearest(&self, p: Vec3) -> Option<Vec3> {
|
|
if !p.is_finite() {
|
|
return None;
|
|
}
|
|
match *self {
|
|
FaceSurface::Plane { normal, point } => Some(p - normal * (p - point).dot(normal)),
|
|
FaceSurface::Cylinder { origin, axis, radius } => {
|
|
let w = p - origin;
|
|
let along = axis * w.dot(axis);
|
|
Some(origin + along + (w - along).normalized()? * radius)
|
|
}
|
|
FaceSurface::Cone { apex, axis, half_angle } => {
|
|
let w = p - apex;
|
|
let out = (w - axis * w.dot(axis)).normalized()?;
|
|
// The cone's line through the apex that p is nearest, in the plane of the axis and p.
|
|
let line = axis * half_angle.cos() + out * half_angle.sin();
|
|
Some(apex + line * w.dot(line).max(0.0))
|
|
}
|
|
FaceSurface::Sphere { center, radius } => Some(center + (p - center).normalized()? * radius),
|
|
FaceSurface::Torus { center, axis, major, minor } => {
|
|
let w = p - center;
|
|
let tube = center + (w - axis * w.dot(axis)).normalized()? * major;
|
|
Some(tube + (p - tube).normalized()? * minor)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
fn near(a: Option<Vec3>, b: Vec3) -> bool {
|
|
a.is_some_and(|a| a.dist(b) < 1e-12)
|
|
}
|
|
|
|
/// Each surface's nearest point, worked out by hand.
|
|
#[test]
|
|
fn the_nearest_point_of_each_surface() {
|
|
let plane = FaceSurface::Plane { normal: Vec3::Z, point: Vec3::new(0.0, 0.0, 4.0) };
|
|
assert!(near(plane.nearest(Vec3::new(3.0, 5.0, 9.0)), Vec3::new(3.0, 5.0, 4.0)));
|
|
// A cylinder of radius 5 about the line x = 1, y = 2: (7, 10, 3) is 10 from it along (3, 4)/5.
|
|
let cylinder = FaceSurface::Cylinder { origin: Vec3::new(1.0, 2.0, 0.0), axis: Vec3::Z, radius: 5.0 };
|
|
assert!(near(cylinder.nearest(Vec3::new(7.0, 10.0, 3.0)), Vec3::new(4.0, 6.0, 3.0)));
|
|
assert!(cylinder.nearest(Vec3::new(1.0, 2.0, 3.0)).is_none(), "on the axis every direction is as near");
|
|
let sphere = FaceSurface::Sphere { center: Vec3::new(1.0, 2.0, 3.0), radius: 5.0 };
|
|
assert!(near(sphere.nearest(Vec3::new(1.0, 2.0, 13.0)), Vec3::new(1.0, 2.0, 8.0)));
|
|
// A torus of ring radius 10 and tube radius 2 about Z: (14, 0, 0) is 2 beyond the tube's outside.
|
|
let torus = FaceSurface::Torus { center: Vec3::ZERO, axis: Vec3::Z, major: 10.0, minor: 2.0 };
|
|
assert!(near(torus.nearest(Vec3::new(14.0, 0.0, 0.0)), Vec3::new(12.0, 0.0, 0.0)));
|
|
// A cone of half-angle 45 degrees: (2, 0, 0) is √2 along its line (1, 0, 1)/√2.
|
|
let cone = FaceSurface::Cone { apex: Vec3::ZERO, axis: Vec3::Z, half_angle: std::f64::consts::FRAC_PI_4 };
|
|
assert!(cone.nearest(Vec3::new(2.0, 0.0, 0.0)).is_some_and(|q| q.dist(Vec3::new(1.0, 0.0, 1.0)) < 1e-12));
|
|
assert!(plane.nearest(Vec3::new(f64::NAN, 0.0, 0.0)).is_none());
|
|
}
|
|
}
|