Another project
0

Configure Feed

Select the types of activity you want to include in your feed.

feat(kernel): plane & cylinder surfaces w/ tessellation

Lewis: May this revision serve well! <lu5a@proton.me>

author
Lewis
date (May 26, 2026, 9:50 AM +0300) commit 57404634 parent 57cc588b change-id wzqrwuty
+434 -2
+23 -2
crates/bone-kernel/src/circular3.rs
··· 1 - use bone_types::{Aabb3, Angle, ChordHeightTolerance, Length, Plane3, Point3, Vec3}; 1 + use bone_types::{ 2 + Aabb3, Angle, AngleTolerance, ChordHeightTolerance, Length, Plane3, Point3, Vec3, 3 + }; 2 4 use core::f64::consts::PI; 3 5 use uom::si::angle::radian; 4 6 use uom::si::length::millimeter; ··· 76 78 tolerance: ChordHeightTolerance, 77 79 min_segments: u32, 78 80 ) -> u32 { 81 + let chord_tol_mm = tolerance.millimeters(); 82 + if chord_tol_mm <= 0.0 { 83 + return min_segments; 84 + } 79 85 let radius_mm = radius.get::<millimeter>(); 80 86 let sweep_abs = sweep.get::<radian>().abs(); 81 - let chord_tol_mm = tolerance.millimeters(); 82 87 (min_segments..=MAX_SEGMENTS) 83 88 .find(|&n| { 84 89 let half_step = sweep_abs / (2.0 * f64::from(n)); ··· 86 91 }) 87 92 .unwrap_or(MAX_SEGMENTS) 88 93 } 94 + 95 + #[must_use] 96 + pub(crate) fn angular_sample_count( 97 + sweep: Angle, 98 + tolerance: AngleTolerance, 99 + min_segments: u32, 100 + ) -> u32 { 101 + let sweep_abs = sweep.get::<radian>().abs(); 102 + let angle_tol = tolerance.radians(); 103 + if angle_tol <= 0.0 { 104 + return min_segments; 105 + } 106 + (min_segments..=MAX_SEGMENTS) 107 + .find(|&n| sweep_abs / f64::from(n) <= angle_tol) 108 + .unwrap_or(MAX_SEGMENTS) 109 + }
+199
crates/bone-kernel/src/cylinder_surface.rs
··· 1 + use bone_types::{ 2 + Aabb3, Angle, AngleTolerance, ChordHeightTolerance, Length, Parameter, Plane3, Point3, 3 + Tolerance, UnitVec3, Vec3, 4 + }; 5 + use core::f64::consts::TAU; 6 + use uom::si::angle::radian; 7 + use uom::si::length::millimeter; 8 + 9 + use crate::KernelError; 10 + use crate::angles; 11 + use crate::circular3; 12 + use crate::mesh::{MeshVertex, TriMesh}; 13 + use crate::surface3::Surface3; 14 + 15 + const MIN_SEGMENTS: u32 = 3; 16 + 17 + #[derive(Copy, Clone, Debug, PartialEq)] 18 + pub struct CylinderSurface { 19 + plane: Plane3, 20 + radius: Length, 21 + start_angle: Angle, 22 + sweep_angle: Angle, 23 + height: Length, 24 + } 25 + 26 + impl CylinderSurface { 27 + pub fn new( 28 + plane: Plane3, 29 + radius: Length, 30 + start_angle: Angle, 31 + sweep_angle: Angle, 32 + height: Length, 33 + tolerance: Tolerance, 34 + ) -> Result<Self, KernelError> { 35 + let r = radius.get::<millimeter>(); 36 + let h = height.get::<millimeter>(); 37 + if !r.is_finite() || !h.is_finite() || r < tolerance.value() || h < tolerance.value() { 38 + return Err(KernelError::DegenerateCylinder); 39 + } 40 + let sweep = sweep_angle.get::<radian>().abs(); 41 + let angle_eps = AngleTolerance::from_arc_length(tolerance, radius).radians(); 42 + if !sweep.is_finite() || sweep < angle_eps || sweep > TAU + angle_eps { 43 + return Err(KernelError::DegenerateCylinder); 44 + } 45 + Ok(Self { 46 + plane, 47 + radius, 48 + start_angle, 49 + sweep_angle, 50 + height, 51 + }) 52 + } 53 + 54 + #[must_use] 55 + pub const fn plane(self) -> Plane3 { 56 + self.plane 57 + } 58 + 59 + #[must_use] 60 + pub fn center(self) -> Point3 { 61 + self.plane.origin() 62 + } 63 + 64 + #[must_use] 65 + pub fn axis(self) -> UnitVec3 { 66 + self.plane.normal() 67 + } 68 + 69 + #[must_use] 70 + pub const fn radius(self) -> Length { 71 + self.radius 72 + } 73 + 74 + #[must_use] 75 + pub const fn start_angle(self) -> Angle { 76 + self.start_angle 77 + } 78 + 79 + #[must_use] 80 + pub const fn sweep_angle(self) -> Angle { 81 + self.sweep_angle 82 + } 83 + 84 + #[must_use] 85 + pub const fn height(self) -> Length { 86 + self.height 87 + } 88 + 89 + #[must_use] 90 + pub fn radius_mm(self) -> f64 { 91 + self.radius.get::<millimeter>() 92 + } 93 + 94 + #[must_use] 95 + pub fn height_mm(self) -> f64 { 96 + self.height.get::<millimeter>() 97 + } 98 + 99 + #[must_use] 100 + pub fn start_rad(self) -> f64 { 101 + self.start_angle.get::<radian>() 102 + } 103 + 104 + #[must_use] 105 + pub fn sweep_rad(self) -> f64 { 106 + self.sweep_angle.get::<radian>() 107 + } 108 + 109 + #[must_use] 110 + fn angle_at(self, u: f64) -> Angle { 111 + Angle::new::<radian>(self.start_rad() + self.sweep_rad() * u) 112 + } 113 + 114 + #[must_use] 115 + fn point_at(self, u: f64, v: f64) -> Point3 { 116 + let base = circular3::point_at(self.plane, self.radius, self.angle_at(u)); 117 + base + self.plane.normal().into_vec(self.height * v) 118 + } 119 + 120 + #[must_use] 121 + fn normal_at(self, u: f64) -> UnitVec3 { 122 + let theta = self.start_rad() + self.sweep_rad() * u; 123 + let (xx, xy, xz) = self.plane.x_axis().components(); 124 + let (yx, yy, yz) = self.plane.y_axis().components(); 125 + let (c, s) = (theta.cos(), theta.sin()); 126 + let radial = UnitVec3::new_unchecked(c * xx + s * yx, c * xy + s * yy, c * xz + s * yz); 127 + if self.sweep_rad() < 0.0 { 128 + radial.reversed() 129 + } else { 130 + radial 131 + } 132 + } 133 + } 134 + 135 + impl Surface3 for CylinderSurface { 136 + fn evaluate(&self, u: Parameter, v: Parameter) -> Point3 { 137 + self.point_at(u.value(), v.value()) 138 + } 139 + 140 + fn partials(&self, u: Parameter, _v: Parameter) -> (Vec3, Vec3) { 141 + let tangent = circular3::tangent_vec(self.plane, self.radius, self.angle_at(u.value())) 142 + * self.sweep_rad(); 143 + let axial = self.plane.normal().into_vec(self.height); 144 + (tangent, axial) 145 + } 146 + 147 + fn normal(&self, u: Parameter, _v: Parameter) -> UnitVec3 { 148 + self.normal_at(u.value()) 149 + } 150 + 151 + fn bounding_box(&self) -> Aabb3 { 152 + let base = 153 + circular3::bounding_box(self.plane, self.radius, self.start_angle, self.sweep_angle); 154 + let top_origin = self.plane.origin() + self.plane.normal().into_vec(self.height); 155 + let top_plane = Plane3::new_unchecked(top_origin, self.plane.x_axis(), self.plane.y_axis()); 156 + let top = 157 + circular3::bounding_box(top_plane, self.radius, self.start_angle, self.sweep_angle); 158 + base.union(top) 159 + } 160 + 161 + fn contains_point(&self, p: Point3, tolerance: Tolerance) -> bool { 162 + let (px, py, pn) = circular3::local_coords(self.plane, p); 163 + let radial = (px * px + py * py).sqrt(); 164 + if (radial - self.radius_mm()).abs() > tolerance.value() { 165 + return false; 166 + } 167 + if pn < -tolerance.value() || pn > self.height_mm() + tolerance.value() { 168 + return false; 169 + } 170 + let angle_eps = AngleTolerance::from_arc_length(tolerance, self.radius).radians(); 171 + angles::contains(py.atan2(px), self.start_rad(), self.sweep_rad(), angle_eps) 172 + } 173 + 174 + fn tessellate(&self, chord: ChordHeightTolerance, angle: AngleTolerance) -> TriMesh { 175 + let n = circular3::sample_count(self.radius, self.sweep_angle, chord, MIN_SEGMENTS).max( 176 + circular3::angular_sample_count(self.sweep_angle, angle, MIN_SEGMENTS), 177 + ); 178 + let inv_n = 1.0 / f64::from(n); 179 + TriMesh::from_grid(n + 1, 2, |i, j| { 180 + let u = f64::from(i) * inv_n; 181 + MeshVertex::new(self.point_at(u, f64::from(j)), self.normal_at(u)) 182 + }) 183 + } 184 + } 185 + 186 + impl core::fmt::Display for CylinderSurface { 187 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 188 + write!( 189 + f, 190 + "cylinder_surface{{ c={}, r={} mm, start={} rad, sweep={} rad, h={} mm, axis={} }}", 191 + self.center(), 192 + self.radius_mm(), 193 + self.start_rad(), 194 + self.sweep_rad(), 195 + self.height_mm(), 196 + self.axis(), 197 + ) 198 + } 199 + }
+12
crates/bone-kernel/src/lib.rs
··· 9 9 pub mod curvature; 10 10 pub mod curve2; 11 11 pub mod curve3; 12 + pub mod cylinder_surface; 12 13 pub mod intersect; 13 14 pub mod intersect3; 14 15 pub mod line2; 15 16 pub mod line3; 17 + pub mod mesh; 18 + pub mod plane_surface; 16 19 pub mod polyline3; 20 + pub mod surface3; 17 21 18 22 pub use aabb::Aabb2; 19 23 pub use arc2::{Arc2, arc_bounding_box}; ··· 24 28 pub use curvature::Curvature; 25 29 pub use curve2::{Curve2, Curve2Kind}; 26 30 pub use curve3::{Curve3, Curve3Kind}; 31 + pub use cylinder_surface::CylinderSurface; 27 32 pub use intersect::{IntersectionSet, IntersectionSet2, intersect_curves}; 28 33 pub use intersect3::{IntersectionSet3, intersect_curves_3}; 29 34 pub use line2::Line2; 30 35 pub use line3::Line3; 36 + pub use mesh::{MeshVertex, TriMesh}; 37 + pub use plane_surface::PlaneSurface; 31 38 pub use polyline3::Polyline3; 39 + pub use surface3::Surface3; 32 40 33 41 #[derive(Debug, thiserror::Error)] 34 42 pub enum KernelError { ··· 40 48 DegenerateCircle, 41 49 #[error("polyline needs at least two vertices and no zero-length segment")] 42 50 DegeneratePolyline, 51 + #[error("plane surface extent is within tolerance of zero")] 52 + DegeneratePlane, 53 + #[error("cylinder surface radius, height, or sweep is degenerate")] 54 + DegenerateCylinder, 43 55 } 44 56 45 57 pub type Result<T, E = KernelError> = core::result::Result<T, E>;
+67
crates/bone-kernel/src/mesh.rs
··· 1 + use bone_types::{Point3, UnitVec3}; 2 + 3 + #[derive(Copy, Clone, Debug, PartialEq)] 4 + pub struct MeshVertex { 5 + position: Point3, 6 + normal: UnitVec3, 7 + } 8 + 9 + impl MeshVertex { 10 + #[must_use] 11 + pub(crate) const fn new(position: Point3, normal: UnitVec3) -> Self { 12 + Self { position, normal } 13 + } 14 + 15 + #[must_use] 16 + pub const fn position(self) -> Point3 { 17 + self.position 18 + } 19 + 20 + #[must_use] 21 + pub const fn normal(self) -> UnitVec3 { 22 + self.normal 23 + } 24 + } 25 + 26 + #[derive(Clone, Debug, PartialEq)] 27 + pub struct TriMesh { 28 + vertices: Vec<MeshVertex>, 29 + triangles: Vec<[u32; 3]>, 30 + } 31 + 32 + impl TriMesh { 33 + #[must_use] 34 + pub fn vertices(&self) -> &[MeshVertex] { 35 + &self.vertices 36 + } 37 + 38 + #[must_use] 39 + pub fn triangles(&self) -> &[[u32; 3]] { 40 + &self.triangles 41 + } 42 + 43 + pub(crate) fn from_grid( 44 + columns: u32, 45 + rows: u32, 46 + vertex: impl Fn(u32, u32) -> MeshVertex, 47 + ) -> Self { 48 + let vertices = (0..rows) 49 + .flat_map(|j| (0..columns).map(move |i| (i, j))) 50 + .map(|(i, j)| vertex(i, j)) 51 + .collect(); 52 + let index = |i: u32, j: u32| j * columns + i; 53 + let triangles = (0..rows.saturating_sub(1)) 54 + .flat_map(move |j| (0..columns.saturating_sub(1)).map(move |i| (i, j))) 55 + .flat_map(|(i, j)| { 56 + [ 57 + [index(i, j), index(i + 1, j), index(i + 1, j + 1)], 58 + [index(i, j), index(i + 1, j + 1), index(i, j + 1)], 59 + ] 60 + }) 61 + .collect(); 62 + Self { 63 + vertices, 64 + triangles, 65 + } 66 + } 67 + }
+119
crates/bone-kernel/src/plane_surface.rs
··· 1 + use bone_types::{ 2 + Aabb3, AngleTolerance, ChordHeightTolerance, Length, Parameter, Plane3, Point3, Tolerance, 3 + UnitVec3, Vec3, 4 + }; 5 + use uom::si::length::millimeter; 6 + 7 + use crate::KernelError; 8 + use crate::circular3; 9 + use crate::mesh::{MeshVertex, TriMesh}; 10 + use crate::surface3::Surface3; 11 + 12 + #[derive(Copy, Clone, Debug, PartialEq)] 13 + pub struct PlaneSurface { 14 + plane: Plane3, 15 + u_extent: Length, 16 + v_extent: Length, 17 + } 18 + 19 + impl PlaneSurface { 20 + pub fn new( 21 + plane: Plane3, 22 + u_extent: Length, 23 + v_extent: Length, 24 + tolerance: Tolerance, 25 + ) -> Result<Self, KernelError> { 26 + let u = u_extent.get::<millimeter>(); 27 + let v = v_extent.get::<millimeter>(); 28 + if !u.is_finite() || !v.is_finite() || u < tolerance.value() || v < tolerance.value() { 29 + return Err(KernelError::DegeneratePlane); 30 + } 31 + Ok(Self { 32 + plane, 33 + u_extent, 34 + v_extent, 35 + }) 36 + } 37 + 38 + #[must_use] 39 + pub const fn plane(self) -> Plane3 { 40 + self.plane 41 + } 42 + 43 + #[must_use] 44 + pub const fn u_extent(self) -> Length { 45 + self.u_extent 46 + } 47 + 48 + #[must_use] 49 + pub const fn v_extent(self) -> Length { 50 + self.v_extent 51 + } 52 + 53 + #[must_use] 54 + fn point_at(self, u: f64, v: f64) -> Point3 { 55 + let (ox, oy, oz) = self.plane.origin().coords_mm(); 56 + let (xx, xy, xz) = self.plane.x_axis().components(); 57 + let (yx, yy, yz) = self.plane.y_axis().components(); 58 + let su = u * self.u_extent.get::<millimeter>(); 59 + let sv = v * self.v_extent.get::<millimeter>(); 60 + Point3::from_mm( 61 + ox + su * xx + sv * yx, 62 + oy + su * xy + sv * yy, 63 + oz + su * xz + sv * yz, 64 + ) 65 + } 66 + } 67 + 68 + impl Surface3 for PlaneSurface { 69 + fn evaluate(&self, u: Parameter, v: Parameter) -> Point3 { 70 + self.point_at(u.value(), v.value()) 71 + } 72 + 73 + fn partials(&self, _u: Parameter, _v: Parameter) -> (Vec3, Vec3) { 74 + ( 75 + self.plane.x_axis().into_vec(self.u_extent), 76 + self.plane.y_axis().into_vec(self.v_extent), 77 + ) 78 + } 79 + 80 + fn normal(&self, _u: Parameter, _v: Parameter) -> UnitVec3 { 81 + self.plane.normal() 82 + } 83 + 84 + fn bounding_box(&self) -> Aabb3 { 85 + Aabb3::from_corners(self.point_at(0.0, 0.0), self.point_at(1.0, 1.0)).union( 86 + Aabb3::from_corners(self.point_at(1.0, 0.0), self.point_at(0.0, 1.0)), 87 + ) 88 + } 89 + 90 + fn contains_point(&self, p: Point3, tolerance: Tolerance) -> bool { 91 + let (a, b, off_plane) = circular3::local_coords(self.plane, p); 92 + if off_plane.abs() > tolerance.value() { 93 + return false; 94 + } 95 + let tol = tolerance.value(); 96 + (-tol..=self.u_extent.get::<millimeter>() + tol).contains(&a) 97 + && (-tol..=self.v_extent.get::<millimeter>() + tol).contains(&b) 98 + } 99 + 100 + fn tessellate(&self, _chord: ChordHeightTolerance, _angle: AngleTolerance) -> TriMesh { 101 + let normal = self.plane.normal(); 102 + TriMesh::from_grid(2, 2, |i, j| { 103 + MeshVertex::new(self.point_at(f64::from(i), f64::from(j)), normal) 104 + }) 105 + } 106 + } 107 + 108 + impl core::fmt::Display for PlaneSurface { 109 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 110 + write!( 111 + f, 112 + "plane_surface{{ o={}, normal={}, u={} mm, v={} mm }}", 113 + self.plane.origin(), 114 + self.plane.normal(), 115 + self.u_extent.get::<millimeter>(), 116 + self.v_extent.get::<millimeter>(), 117 + ) 118 + } 119 + }
+14
crates/bone-kernel/src/surface3.rs
··· 1 + use bone_types::{ 2 + Aabb3, AngleTolerance, ChordHeightTolerance, Parameter, Point3, Tolerance, UnitVec3, Vec3, 3 + }; 4 + 5 + use crate::mesh::TriMesh; 6 + 7 + pub trait Surface3 { 8 + fn evaluate(&self, u: Parameter, v: Parameter) -> Point3; 9 + fn partials(&self, u: Parameter, v: Parameter) -> (Vec3, Vec3); 10 + fn normal(&self, u: Parameter, v: Parameter) -> UnitVec3; 11 + fn bounding_box(&self) -> Aabb3; 12 + fn contains_point(&self, p: Point3, tolerance: Tolerance) -> bool; 13 + fn tessellate(&self, chord: ChordHeightTolerance, angle: AngleTolerance) -> TriMesh; 14 + }