//! 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 { let f = b.solid.face_iter().nth(face)?; let tol = (b.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 { 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 = (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 { 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 { 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, 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()); } }