use bone_types::{ Aabb3, Angle, AngleTolerance, ChordHeightTolerance, Length, Parameter, Plane3, Point3, Tolerance, UnitVec3, Vec3, }; use core::f64::consts::{FRAC_PI_2, TAU}; use uom::si::angle::radian; use uom::si::length::millimeter; use crate::KernelError; use crate::angles; use crate::angular_span::AngularSpan; use crate::circular3; use crate::mesh::{MeshVertex, TriMesh}; use crate::surface3::Surface3; const MIN_SEGMENTS: u32 = 3; #[derive(Copy, Clone, Debug, PartialEq)] pub struct ConeSurface { plane: Plane3, base_radius: Length, half_angle: Angle, start_angle: Angle, sweep_angle: Angle, height: Length, } impl ConeSurface { pub fn new( plane: Plane3, base_radius: Length, half_angle: Angle, revolution: AngularSpan, height: Length, tolerance: Tolerance, ) -> Result { let start_angle = revolution.start(); let sweep_angle = revolution.sweep(); let base = base_radius.get::(); let h = height.get::(); let a = half_angle.get::(); if !base.is_finite() || !h.is_finite() || !a.is_finite() { return Err(KernelError::DegenerateCone); } if base < 0.0 || h < tolerance.value() || a.abs() >= FRAC_PI_2 { return Err(KernelError::DegenerateCone); } let top = base + h * a.tan(); if top < 0.0 || base.max(top) < tolerance.value() { return Err(KernelError::DegenerateCone); } let sweep = sweep_angle.get::().abs(); let angle_eps = AngleTolerance::from_arc_length(tolerance, Length::new::(base.max(top))) .radians(); if !sweep.is_finite() || sweep < angle_eps || sweep > TAU + angle_eps { return Err(KernelError::DegenerateCone); } Ok(Self { plane, base_radius, half_angle, start_angle, sweep_angle, height, }) } #[must_use] pub const fn plane(self) -> Plane3 { self.plane } #[must_use] pub fn center(self) -> Point3 { self.plane.origin() } #[must_use] pub fn axis(self) -> UnitVec3 { self.plane.normal() } #[must_use] pub const fn base_radius(self) -> Length { self.base_radius } #[must_use] pub const fn half_angle(self) -> Angle { self.half_angle } #[must_use] pub const fn start_angle(self) -> Angle { self.start_angle } #[must_use] pub const fn sweep_angle(self) -> Angle { self.sweep_angle } #[must_use] pub const fn height(self) -> Length { self.height } #[must_use] pub fn base_radius_mm(self) -> f64 { self.base_radius.get::() } #[must_use] pub fn height_mm(self) -> f64 { self.height.get::() } #[must_use] pub fn half_angle_rad(self) -> f64 { self.half_angle.get::() } #[must_use] pub fn start_rad(self) -> f64 { self.start_angle.get::() } #[must_use] pub fn sweep_rad(self) -> f64 { self.sweep_angle.get::() } #[must_use] pub fn top_radius_mm(self) -> f64 { self.base_radius_mm() + self.height_mm() * self.half_angle_rad().tan() } #[must_use] fn radius_at(self, v: f64) -> f64 { self.base_radius_mm() + v * self.height_mm() * self.half_angle_rad().tan() } #[must_use] fn point_at(self, u: f64, v: f64) -> Point3 { let theta = Angle::new::(self.start_rad() + self.sweep_rad() * u); let ring = circular3::point_at( self.plane, Length::new::(self.radius_at(v)), theta, ); ring + self.plane.normal().into_vec(self.height * v) } #[must_use] fn radial_components(self, u: f64) -> (f64, f64, f64) { let theta = self.start_rad() + self.sweep_rad() * u; let (xx, xy, xz) = self.plane.x_axis().components(); let (yx, yy, yz) = self.plane.y_axis().components(); let (c, s) = (theta.cos(), theta.sin()); (c * xx + s * yx, c * xy + s * yy, c * xz + s * yz) } #[must_use] pub(crate) fn orientation(self) -> f64 { if self.sweep_rad() < 0.0 { -1.0 } else { 1.0 } } #[must_use] pub(crate) fn min_radius_of_curvature(self) -> Length { let smallest = self.base_radius_mm().min(self.top_radius_mm()); Length::new::(smallest / self.half_angle_rad().cos()) } #[must_use] fn normal_at(self, u: f64) -> UnitVec3 { let (rx, ry, rz) = self.radial_components(u); let (ax, ay, az) = self.plane.normal().components(); let (ca, sa) = (self.half_angle_rad().cos(), self.half_angle_rad().sin()); let normal = UnitVec3::new_unchecked(ca * rx - sa * ax, ca * ry - sa * ay, ca * rz - sa * az); if self.orientation() < 0.0 { normal.reversed() } else { normal } } } impl Surface3 for ConeSurface { fn evaluate(&self, u: Parameter, v: Parameter) -> Point3 { self.point_at(u.value(), v.value()) } fn partials(&self, u: Parameter, v: Parameter) -> (Vec3, Vec3) { let theta = Angle::new::(self.start_rad() + self.sweep_rad() * u.value()); let tangent = circular3::tangent_vec( self.plane, Length::new::(self.radius_at(v.value())), theta, ) * self.sweep_rad(); let (rx, ry, rz) = self.radial_components(u.value()); let slope = self.height_mm() * self.half_angle_rad().tan(); let axial = self.plane.normal().into_vec(self.height) + Vec3::from_mm(rx, ry, rz) * slope; (tangent, axial) } fn normal(&self, u: Parameter, _v: Parameter) -> UnitVec3 { self.normal_at(u.value()) } fn bounding_box(&self) -> Aabb3 { let base = circular3::bounding_box( self.plane, self.base_radius, self.start_angle, self.sweep_angle, ); let top_origin = self.plane.origin() + self.plane.normal().into_vec(self.height); let top_plane = Plane3::new_unchecked(top_origin, self.plane.x_axis(), self.plane.y_axis()); let top = circular3::bounding_box( top_plane, Length::new::(self.top_radius_mm()), self.start_angle, self.sweep_angle, ); base.union(top) } fn contains_point(&self, p: Point3, tolerance: Tolerance) -> bool { let (px, py, pn) = circular3::local_coords(self.plane, p); if pn < -tolerance.value() || pn > self.height_mm() + tolerance.value() { return false; } let radial = (px * px + py * py).sqrt(); let expected = self.base_radius_mm() + pn * self.half_angle_rad().tan(); let distance = (radial - expected) * self.half_angle_rad().cos(); if distance.abs() > tolerance.value() { return false; } if expected < tolerance.value() { return true; } let angle_eps = AngleTolerance::from_arc_length(tolerance, Length::new::(expected)) .radians(); angles::contains(py.atan2(px), self.start_rad(), self.sweep_rad(), angle_eps) } fn tessellate(&self, chord: ChordHeightTolerance, angle: AngleTolerance) -> TriMesh { let radius = Length::new::(self.base_radius_mm().max(self.top_radius_mm())); let n = circular3::sample_count(radius, self.sweep_angle, chord, MIN_SEGMENTS).max( circular3::angular_sample_count(self.sweep_angle, angle, MIN_SEGMENTS), ); let inv_n = 1.0 / f64::from(n); TriMesh::from_grid(n + 1, 2, |i, j| { let u = f64::from(i) * inv_n; MeshVertex::new(self.point_at(u, f64::from(j)), self.normal_at(u)) }) } } impl core::fmt::Display for ConeSurface { fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { write!( f, "cone_surface{{ c={}, base_r={} mm, half_angle={} rad, start={} rad, sweep={} rad, h={} mm, axis={} }}", self.center(), self.base_radius_mm(), self.half_angle_rad(), self.start_rad(), self.sweep_rad(), self.height_mm(), self.axis(), ) } }