Another project
0

Configure Feed

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

feat(kernel): 3d line, arc, circle, polyline curves

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

author
Lewis
date (May 25, 2026, 7:38 PM +0300) commit f62a73ed parent b9914927 change-id oonroktl
+1120
+182
crates/bone-kernel/src/arc3.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::circle3::Circle3; 12 + use crate::circular3; 13 + use crate::closest::ClosestPoint3; 14 + use crate::curve3::{Curve3, Curve3Kind}; 15 + 16 + #[derive(Copy, Clone, Debug, PartialEq)] 17 + pub struct Arc3 { 18 + plane: Plane3, 19 + radius: Length, 20 + start_angle: Angle, 21 + sweep_angle: Angle, 22 + } 23 + 24 + impl Arc3 { 25 + pub fn new( 26 + plane: Plane3, 27 + radius: Length, 28 + start_angle: Angle, 29 + sweep_angle: Angle, 30 + tolerance: Tolerance, 31 + ) -> Result<Self, KernelError> { 32 + if radius.get::<millimeter>() < tolerance.value() { 33 + return Err(KernelError::DegenerateArc); 34 + } 35 + let sweep = sweep_angle.get::<radian>(); 36 + let angle_eps = AngleTolerance::from_arc_length(tolerance, radius).radians(); 37 + if sweep.abs() < angle_eps { 38 + return Err(KernelError::DegenerateArc); 39 + } 40 + if sweep.abs() > TAU + angle_eps { 41 + return Err(KernelError::DegenerateArc); 42 + } 43 + Ok(Self { 44 + plane, 45 + radius, 46 + start_angle, 47 + sweep_angle, 48 + }) 49 + } 50 + 51 + #[must_use] 52 + pub fn center(self) -> Point3 { 53 + self.plane.origin() 54 + } 55 + 56 + #[must_use] 57 + pub const fn plane(self) -> Plane3 { 58 + self.plane 59 + } 60 + 61 + #[must_use] 62 + pub fn normal(self) -> UnitVec3 { 63 + self.plane.normal() 64 + } 65 + 66 + #[must_use] 67 + pub const fn radius(self) -> Length { 68 + self.radius 69 + } 70 + 71 + #[must_use] 72 + pub const fn start_angle(self) -> Angle { 73 + self.start_angle 74 + } 75 + 76 + #[must_use] 77 + pub const fn sweep_angle(self) -> Angle { 78 + self.sweep_angle 79 + } 80 + 81 + #[must_use] 82 + pub fn radius_mm(self) -> f64 { 83 + self.radius.get::<millimeter>() 84 + } 85 + 86 + #[must_use] 87 + pub fn start_rad(self) -> f64 { 88 + self.start_angle.get::<radian>() 89 + } 90 + 91 + #[must_use] 92 + pub fn sweep_rad(self) -> f64 { 93 + self.sweep_angle.get::<radian>() 94 + } 95 + 96 + #[must_use] 97 + pub fn as_full_circle(self) -> Circle3 { 98 + Circle3::from_raw(self.plane, self.radius) 99 + } 100 + 101 + #[must_use] 102 + pub fn as_kind(self) -> Curve3Kind { 103 + Curve3Kind::Arc(self) 104 + } 105 + 106 + #[must_use] 107 + pub fn contains_point(self, p: Point3, tolerance: Tolerance) -> bool { 108 + let (px, py, pn) = circular3::local_coords(self.plane, p); 109 + let radial = (px * px + py * py).sqrt(); 110 + if (radial - self.radius_mm()).abs() > tolerance.value() || pn.abs() > tolerance.value() { 111 + return false; 112 + } 113 + let angle_eps = AngleTolerance::from_arc_length(tolerance, self.radius).radians(); 114 + angles::contains(py.atan2(px), self.start_rad(), self.sweep_rad(), angle_eps) 115 + } 116 + 117 + fn angle_at(self, t: Parameter) -> Angle { 118 + Angle::new::<radian>(self.start_rad() + self.sweep_rad() * t.value()) 119 + } 120 + } 121 + 122 + impl Curve3 for Arc3 { 123 + fn evaluate(&self, t: Parameter) -> Point3 { 124 + circular3::point_at(self.plane, self.radius, self.angle_at(t)) 125 + } 126 + 127 + fn derivative(&self, t: Parameter) -> Vec3 { 128 + circular3::tangent_vec(self.plane, self.radius, self.angle_at(t)) * self.sweep_rad() 129 + } 130 + 131 + fn bounding_box(&self) -> Aabb3 { 132 + circular3::bounding_box(self.plane, self.radius, self.start_angle, self.sweep_angle) 133 + } 134 + 135 + fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3 { 136 + let (px, py, _) = circular3::local_coords(self.plane, p); 137 + let radial = (px * px + py * py).sqrt(); 138 + let theta_unclamped = if radial < tolerance.value() { 139 + self.start_rad() 140 + } else { 141 + py.atan2(px) 142 + }; 143 + let theta = angles::clamp(theta_unclamped, self.start_rad(), self.sweep_rad(), 0.0); 144 + let sweep = self.sweep_rad(); 145 + let delta = if sweep >= 0.0 { 146 + (theta - self.start_rad()).rem_euclid(TAU) 147 + } else { 148 + (self.start_rad() - theta).rem_euclid(TAU) 149 + }; 150 + let t = (delta / sweep.abs()).clamp(0.0, 1.0); 151 + let point = circular3::point_at(self.plane, self.radius, Angle::new::<radian>(theta)); 152 + ClosestPoint3::new(Parameter::new(t), point, (p - point).norm()) 153 + } 154 + 155 + fn tessellate(&self, tolerance: ChordHeightTolerance) -> Vec<Point3> { 156 + let (start, sweep) = (self.start_rad(), self.sweep_rad()); 157 + let n = circular3::sample_count(self.radius, self.sweep_angle, tolerance, 1); 158 + (0..=n) 159 + .map(|i| { 160 + circular3::point_at( 161 + self.plane, 162 + self.radius, 163 + Angle::new::<radian>(start + sweep * f64::from(i) / f64::from(n)), 164 + ) 165 + }) 166 + .collect() 167 + } 168 + } 169 + 170 + impl core::fmt::Display for Arc3 { 171 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 172 + write!( 173 + f, 174 + "arc3{{ c={}, r={} mm, start={} rad, sweep={} rad, normal={} }}", 175 + self.center(), 176 + self.radius_mm(), 177 + self.start_rad(), 178 + self.sweep_rad(), 179 + self.normal(), 180 + ) 181 + } 182 + }
+129
crates/bone-kernel/src/circle3.rs
··· 1 + use bone_types::{ 2 + Aabb3, Angle, ChordHeightTolerance, Length, Parameter, Plane3, Point3, Tolerance, UnitVec3, 3 + 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::circular3; 11 + use crate::closest::ClosestPoint3; 12 + use crate::curve3::{Curve3, Curve3Kind}; 13 + 14 + #[derive(Copy, Clone, Debug, PartialEq)] 15 + pub struct Circle3 { 16 + plane: Plane3, 17 + radius: Length, 18 + } 19 + 20 + impl Circle3 { 21 + pub fn new(plane: Plane3, radius: Length, tolerance: Tolerance) -> Result<Self, KernelError> { 22 + if radius.get::<millimeter>() < tolerance.value() { 23 + return Err(KernelError::DegenerateCircle); 24 + } 25 + Ok(Self { plane, radius }) 26 + } 27 + 28 + #[must_use] 29 + pub(crate) const fn from_raw(plane: Plane3, radius: Length) -> Self { 30 + Self { plane, radius } 31 + } 32 + 33 + #[must_use] 34 + pub fn center(self) -> Point3 { 35 + self.plane.origin() 36 + } 37 + 38 + #[must_use] 39 + pub const fn plane(self) -> Plane3 { 40 + self.plane 41 + } 42 + 43 + #[must_use] 44 + pub fn normal(self) -> UnitVec3 { 45 + self.plane.normal() 46 + } 47 + 48 + #[must_use] 49 + pub const fn radius(self) -> Length { 50 + self.radius 51 + } 52 + 53 + #[must_use] 54 + pub fn radius_mm(self) -> f64 { 55 + self.radius.get::<millimeter>() 56 + } 57 + 58 + #[must_use] 59 + pub fn as_kind(self) -> Curve3Kind { 60 + Curve3Kind::Circle(self) 61 + } 62 + } 63 + 64 + impl Curve3 for Circle3 { 65 + fn evaluate(&self, t: Parameter) -> Point3 { 66 + circular3::point_at( 67 + self.plane, 68 + self.radius, 69 + Angle::new::<radian>(TAU * t.value()), 70 + ) 71 + } 72 + 73 + fn derivative(&self, t: Parameter) -> Vec3 { 74 + circular3::tangent_vec( 75 + self.plane, 76 + self.radius, 77 + Angle::new::<radian>(TAU * t.value()), 78 + ) * TAU 79 + } 80 + 81 + fn bounding_box(&self) -> Aabb3 { 82 + circular3::bounding_box( 83 + self.plane, 84 + self.radius, 85 + Angle::new::<radian>(0.0), 86 + Angle::new::<radian>(TAU), 87 + ) 88 + } 89 + 90 + fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3 { 91 + let (px, py, pn) = circular3::local_coords(self.plane, p); 92 + let radial = (px * px + py * py).sqrt(); 93 + let r = self.radius_mm(); 94 + let theta = if radial < tolerance.value() { 95 + 0.0 96 + } else { 97 + py.atan2(px) 98 + }; 99 + let point = circular3::point_at(self.plane, self.radius, Angle::new::<radian>(theta)); 100 + let t = (theta / TAU).rem_euclid(1.0); 101 + let distance = Length::new::<millimeter>(((radial - r).powi(2) + pn * pn).sqrt()); 102 + ClosestPoint3::new(Parameter::new(t), point, distance) 103 + } 104 + 105 + fn tessellate(&self, tolerance: ChordHeightTolerance) -> Vec<Point3> { 106 + let n = circular3::sample_count(self.radius, Angle::new::<radian>(TAU), tolerance, 3); 107 + (0..n) 108 + .map(|i| { 109 + circular3::point_at( 110 + self.plane, 111 + self.radius, 112 + Angle::new::<radian>(TAU * f64::from(i) / f64::from(n)), 113 + ) 114 + }) 115 + .collect() 116 + } 117 + } 118 + 119 + impl core::fmt::Display for Circle3 { 120 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 121 + write!( 122 + f, 123 + "circle3{{ c={}, r={} mm, normal={} }}", 124 + self.center(), 125 + self.radius_mm(), 126 + self.normal(), 127 + ) 128 + } 129 + }
+88
crates/bone-kernel/src/circular3.rs
··· 1 + use bone_types::{Aabb3, Angle, ChordHeightTolerance, Length, Plane3, Point3, Vec3}; 2 + use core::f64::consts::PI; 3 + use uom::si::angle::radian; 4 + use uom::si::length::millimeter; 5 + 6 + use crate::angles; 7 + 8 + const MAX_SEGMENTS: u32 = 4096; 9 + 10 + #[must_use] 11 + pub(crate) fn point_at(plane: Plane3, radius: Length, theta: Angle) -> Point3 { 12 + let radius_mm = radius.get::<millimeter>(); 13 + let theta = theta.get::<radian>(); 14 + let (cx, cy, cz) = plane.origin().coords_mm(); 15 + let (xx, xy, xz) = plane.x_axis().components(); 16 + let (yx, yy, yz) = plane.y_axis().components(); 17 + let (c, s) = (theta.cos(), theta.sin()); 18 + Point3::from_mm( 19 + cx + radius_mm * (c * xx + s * yx), 20 + cy + radius_mm * (c * xy + s * yy), 21 + cz + radius_mm * (c * xz + s * yz), 22 + ) 23 + } 24 + 25 + #[must_use] 26 + pub(crate) fn tangent_vec(plane: Plane3, radius: Length, theta: Angle) -> Vec3 { 27 + let radius_mm = radius.get::<millimeter>(); 28 + let theta = theta.get::<radian>(); 29 + let (xx, xy, xz) = plane.x_axis().components(); 30 + let (yx, yy, yz) = plane.y_axis().components(); 31 + let (c, s) = (theta.cos(), theta.sin()); 32 + Vec3::from_mm( 33 + radius_mm * (-s * xx + c * yx), 34 + radius_mm * (-s * xy + c * yy), 35 + radius_mm * (-s * xz + c * yz), 36 + ) 37 + } 38 + 39 + #[must_use] 40 + pub(crate) fn local_coords(plane: Plane3, p: Point3) -> (f64, f64, f64) { 41 + let (dx, dy, dz) = (p - plane.origin()).coords_mm(); 42 + let (xx, xy, xz) = plane.x_axis().components(); 43 + let (yx, yy, yz) = plane.y_axis().components(); 44 + let (nx, ny, nz) = plane.normal().components(); 45 + ( 46 + dx * xx + dy * xy + dz * xz, 47 + dx * yx + dy * yy + dz * yz, 48 + dx * nx + dy * ny + dz * nz, 49 + ) 50 + } 51 + 52 + #[must_use] 53 + pub(crate) fn bounding_box(plane: Plane3, radius: Length, start: Angle, sweep: Angle) -> Aabb3 { 54 + let start = start.get::<radian>(); 55 + let sweep = sweep.get::<radian>(); 56 + let start_pt = point_at(plane, radius, Angle::new::<radian>(start)); 57 + let end_pt = point_at(plane, radius, Angle::new::<radian>(start + sweep)); 58 + let base = Aabb3::from_corners(start_pt, end_pt); 59 + let (xx, xy, xz) = plane.x_axis().components(); 60 + let (yx, yy, yz) = plane.y_axis().components(); 61 + [(xx, yx), (xy, yy), (xz, yz)] 62 + .into_iter() 63 + .flat_map(|(xk, yk)| { 64 + let phi = yk.atan2(xk); 65 + [phi, phi + PI] 66 + }) 67 + .filter(|&theta| angles::contains(theta, start, sweep, 0.0)) 68 + .map(|theta| point_at(plane, radius, Angle::new::<radian>(theta))) 69 + .fold(base, |bb, p| bb.union(Aabb3::from_corners(p, p))) 70 + } 71 + 72 + #[must_use] 73 + pub(crate) fn sample_count( 74 + radius: Length, 75 + sweep: Angle, 76 + tolerance: ChordHeightTolerance, 77 + min_segments: u32, 78 + ) -> u32 { 79 + let radius_mm = radius.get::<millimeter>(); 80 + let sweep_abs = sweep.get::<radian>().abs(); 81 + let chord_tol_mm = tolerance.millimeters(); 82 + (min_segments..=MAX_SEGMENTS) 83 + .find(|&n| { 84 + let half_step = sweep_abs / (2.0 * f64::from(n)); 85 + radius_mm * (1.0 - half_step.cos()) <= chord_tol_mm 86 + }) 87 + .unwrap_or(MAX_SEGMENTS) 88 + }
+59
crates/bone-kernel/src/curve3.rs
··· 1 + use bone_types::{Aabb3, ChordHeightTolerance, Parameter, Point3, Tolerance, Vec3}; 2 + 3 + use crate::arc3::Arc3; 4 + use crate::circle3::Circle3; 5 + use crate::closest::ClosestPoint3; 6 + use crate::line3::Line3; 7 + 8 + pub trait Curve3 { 9 + fn evaluate(&self, t: Parameter) -> Point3; 10 + fn derivative(&self, t: Parameter) -> Vec3; 11 + fn bounding_box(&self) -> Aabb3; 12 + fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3; 13 + fn tessellate(&self, tolerance: ChordHeightTolerance) -> Vec<Point3>; 14 + } 15 + 16 + #[derive(Copy, Clone, Debug, PartialEq)] 17 + pub enum Curve3Kind { 18 + Line(Line3), 19 + Arc(Arc3), 20 + Circle(Circle3), 21 + } 22 + 23 + impl Curve3 for Curve3Kind { 24 + fn evaluate(&self, t: Parameter) -> Point3 { 25 + match self { 26 + Self::Line(c) => c.evaluate(t), 27 + Self::Arc(c) => c.evaluate(t), 28 + Self::Circle(c) => c.evaluate(t), 29 + } 30 + } 31 + fn derivative(&self, t: Parameter) -> Vec3 { 32 + match self { 33 + Self::Line(c) => c.derivative(t), 34 + Self::Arc(c) => c.derivative(t), 35 + Self::Circle(c) => c.derivative(t), 36 + } 37 + } 38 + fn bounding_box(&self) -> Aabb3 { 39 + match self { 40 + Self::Line(c) => c.bounding_box(), 41 + Self::Arc(c) => c.bounding_box(), 42 + Self::Circle(c) => c.bounding_box(), 43 + } 44 + } 45 + fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3 { 46 + match self { 47 + Self::Line(c) => c.closest_point(p, tolerance), 48 + Self::Arc(c) => c.closest_point(p, tolerance), 49 + Self::Circle(c) => c.closest_point(p, tolerance), 50 + } 51 + } 52 + fn tessellate(&self, tolerance: ChordHeightTolerance) -> Vec<Point3> { 53 + match self { 54 + Self::Line(c) => c.tessellate(tolerance), 55 + Self::Arc(c) => c.tessellate(tolerance), 56 + Self::Circle(c) => c.tessellate(tolerance), 57 + } 58 + } 59 + }
+78
crates/bone-kernel/src/line3.rs
··· 1 + use bone_types::{Aabb3, ChordHeightTolerance, Length, Parameter, Point3, Tolerance, Vec3}; 2 + use uom::si::length::millimeter; 3 + 4 + use crate::KernelError; 5 + use crate::closest::ClosestPoint3; 6 + use crate::curve3::{Curve3, Curve3Kind}; 7 + 8 + #[derive(Copy, Clone, Debug, PartialEq)] 9 + pub struct Line3 { 10 + start: Point3, 11 + end: Point3, 12 + } 13 + 14 + impl Line3 { 15 + pub fn new(start: Point3, end: Point3, tolerance: Tolerance) -> Result<Self, KernelError> { 16 + if (end - start).norm_mm() < tolerance.value() { 17 + return Err(KernelError::DegenerateLine); 18 + } 19 + Ok(Self { start, end }) 20 + } 21 + 22 + #[must_use] 23 + pub const fn start(self) -> Point3 { 24 + self.start 25 + } 26 + 27 + #[must_use] 28 + pub const fn end(self) -> Point3 { 29 + self.end 30 + } 31 + 32 + #[must_use] 33 + pub fn length_mm(self) -> f64 { 34 + (self.end - self.start).norm_mm() 35 + } 36 + 37 + #[must_use] 38 + pub fn as_kind(self) -> Curve3Kind { 39 + Curve3Kind::Line(self) 40 + } 41 + } 42 + 43 + impl Curve3 for Line3 { 44 + fn evaluate(&self, t: Parameter) -> Point3 { 45 + let (sx, sy, sz) = self.start.coords_mm(); 46 + let (ex, ey, ez) = self.end.coords_mm(); 47 + let u = t.value(); 48 + Point3::from_mm(sx + u * (ex - sx), sy + u * (ey - sy), sz + u * (ez - sz)) 49 + } 50 + 51 + fn derivative(&self, _t: Parameter) -> Vec3 { 52 + self.end - self.start 53 + } 54 + 55 + fn bounding_box(&self) -> Aabb3 { 56 + Aabb3::from_corners(self.start, self.end) 57 + } 58 + 59 + fn closest_point(&self, p: Point3, _tolerance: Tolerance) -> ClosestPoint3 { 60 + let d = self.end - self.start; 61 + let v = p - self.start; 62 + let len_sq = d.norm_squared_mm2(); 63 + let t = (v.dot_mm2(d) / len_sq).clamp(0.0, 1.0); 64 + let point = self.evaluate(Parameter::new(t)); 65 + let dist_mm = (p - point).norm_mm(); 66 + ClosestPoint3::new(Parameter::new(t), point, Length::new::<millimeter>(dist_mm)) 67 + } 68 + 69 + fn tessellate(&self, _tolerance: ChordHeightTolerance) -> Vec<Point3> { 70 + vec![self.start, self.end] 71 + } 72 + } 73 + 74 + impl core::fmt::Display for Line3 { 75 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 76 + write!(f, "line3{{ {} -> {} }}", self.start, self.end) 77 + } 78 + }
+121
crates/bone-kernel/src/polyline3.rs
··· 1 + use bone_types::{Aabb3, ChordHeightTolerance, Length, Parameter, Point3, Tolerance, Vec3}; 2 + use uom::si::length::millimeter; 3 + 4 + use crate::KernelError; 5 + use crate::closest::ClosestPoint3; 6 + use crate::curve3::Curve3; 7 + 8 + #[derive(Clone, Debug, PartialEq)] 9 + pub struct Polyline3 { 10 + vertices: Vec<Point3>, 11 + } 12 + 13 + impl Polyline3 { 14 + pub fn new(vertices: Vec<Point3>, tolerance: Tolerance) -> Result<Self, KernelError> { 15 + if vertices.len() < 2 { 16 + return Err(KernelError::DegeneratePolyline); 17 + } 18 + let has_zero_segment = vertices.windows(2).any(|w| match w { 19 + [a, b] => (*b - *a).norm_mm() < tolerance.value(), 20 + _ => false, 21 + }); 22 + if has_zero_segment { 23 + return Err(KernelError::DegeneratePolyline); 24 + } 25 + Ok(Self { vertices }) 26 + } 27 + 28 + #[must_use] 29 + pub fn vertices(&self) -> &[Point3] { 30 + &self.vertices 31 + } 32 + 33 + #[must_use] 34 + fn segments(&self) -> u32 { 35 + u32::try_from(self.vertices.len() - 1).unwrap_or(u32::MAX) 36 + } 37 + } 38 + 39 + impl Curve3 for Polyline3 { 40 + fn evaluate(&self, t: Parameter) -> Point3 { 41 + let segments = self.segments(); 42 + let scaled = t.value().clamp(0.0, 1.0) * f64::from(segments); 43 + self.vertices 44 + .windows(2) 45 + .enumerate() 46 + .find_map(|(index, pair)| { 47 + let &[from, to] = pair else { return None }; 48 + let lo = f64::from(u32::try_from(index).unwrap_or(u32::MAX)); 49 + let is_last = index + 1 == self.vertices.len() - 1; 50 + (scaled >= lo && (scaled < lo + 1.0 || is_last)) 51 + .then(|| from + (to - from) * (scaled - lo)) 52 + }) 53 + .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices")) 54 + } 55 + 56 + fn derivative(&self, t: Parameter) -> Vec3 { 57 + let segments = self.segments(); 58 + let scaled = t.value().clamp(0.0, 1.0) * f64::from(segments); 59 + self.vertices 60 + .windows(2) 61 + .enumerate() 62 + .find_map(|(index, pair)| { 63 + let &[from, to] = pair else { return None }; 64 + let lo = f64::from(u32::try_from(index).unwrap_or(u32::MAX)); 65 + let is_last = index + 1 == self.vertices.len() - 1; 66 + (scaled >= lo && (scaled < lo + 1.0 || is_last)) 67 + .then(|| (to - from) * f64::from(segments)) 68 + }) 69 + .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices")) 70 + } 71 + 72 + fn bounding_box(&self) -> Aabb3 { 73 + Aabb3::from_points(self.vertices.iter().copied()) 74 + .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices")) 75 + } 76 + 77 + fn closest_point(&self, query: Point3, _tolerance: Tolerance) -> ClosestPoint3 { 78 + let segments = self.segments(); 79 + self.vertices 80 + .windows(2) 81 + .enumerate() 82 + .filter_map(|(index, pair)| { 83 + let &[from, to] = pair else { return None }; 84 + let edge = to - from; 85 + let local = 86 + ((query - from).dot_mm2(edge) / edge.norm_squared_mm2()).clamp(0.0, 1.0); 87 + let point = from + edge * local; 88 + let distance = (query - point).norm_mm(); 89 + let param = (f64::from(u32::try_from(index).unwrap_or(u32::MAX)) + local) 90 + / f64::from(segments); 91 + Some(ClosestPoint3::new( 92 + Parameter::new(param), 93 + point, 94 + Length::new::<millimeter>(distance), 95 + )) 96 + }) 97 + .min_by(|left, right| { 98 + left.distance() 99 + .partial_cmp(&right.distance()) 100 + .unwrap_or(core::cmp::Ordering::Equal) 101 + }) 102 + .unwrap_or_else(|| unreachable!("Polyline3 holds at least two vertices")) 103 + } 104 + 105 + fn tessellate(&self, _tolerance: ChordHeightTolerance) -> Vec<Point3> { 106 + self.vertices.clone() 107 + } 108 + } 109 + 110 + impl core::fmt::Display for Polyline3 { 111 + fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result { 112 + write!(f, "polyline3{{ {} verts: ", self.vertices.len())?; 113 + self.vertices.iter().enumerate().try_for_each(|(i, v)| { 114 + if i > 0 { 115 + write!(f, " -> ")?; 116 + } 117 + write!(f, "{v}") 118 + })?; 119 + write!(f, " }}") 120 + } 121 + }
+347
crates/bone-kernel/tests/curves3.rs
··· 1 + use bone_kernel::{Arc3, Circle3, Curve3, Line3, Polyline3}; 2 + use bone_types::{ 3 + Angle, ChordHeightTolerance, Length, Parameter, Plane3, Point3, Tolerance, UnitVec3, 4 + }; 5 + use core::fmt::{Debug, Display}; 6 + use uom::si::angle::radian; 7 + use uom::si::length::millimeter; 8 + 9 + const TOL: Tolerance = Tolerance::new(1e-9); 10 + 11 + fn mm(value: f64) -> Length { 12 + Length::new::<millimeter>(value) 13 + } 14 + 15 + fn fmt_point(p: Point3) -> String { 16 + let (x, y, z) = p.coords_mm(); 17 + format!("({x:.6}, {y:.6}, {z:.6})") 18 + } 19 + 20 + fn xy_plane(origin: Point3) -> Plane3 { 21 + Plane3::new_unchecked(origin, UnitVec3::x_axis(), UnitVec3::y_axis()) 22 + } 23 + 24 + fn xz_plane(origin: Point3) -> Plane3 { 25 + Plane3::new_unchecked(origin, UnitVec3::x_axis(), UnitVec3::z_axis()) 26 + } 27 + 28 + fn fmt_curve3<C: Curve3 + Display + Debug>( 29 + name: &str, 30 + c: &C, 31 + tess: ChordHeightTolerance, 32 + ) -> String { 33 + let samples = [0.0, 0.25, 0.5, 0.75, 1.0].map(Parameter::new); 34 + let points = samples 35 + .iter() 36 + .map(|t| { 37 + format!( 38 + "t={:.2} -> p={} v={}", 39 + t.value(), 40 + fmt_point(c.evaluate(*t)), 41 + c.derivative(*t) 42 + ) 43 + }) 44 + .collect::<Vec<_>>() 45 + .join("\n "); 46 + let bbox = c.bounding_box(); 47 + let closest = c.closest_point(Point3::origin(), TOL); 48 + let tess_pts = c.tessellate(tess); 49 + let tess_str = tess_pts 50 + .iter() 51 + .map(|p| fmt_point(*p)) 52 + .collect::<Vec<_>>() 53 + .join("\n "); 54 + format!( 55 + "{name}_display = {c}\n\ 56 + {name}_debug = {c:?}\n\ 57 + {name}_samples:\n {points}\n\ 58 + {name}_bbox = {bbox}\n\ 59 + {name}_closest_origin = {closest}\n\ 60 + {name}_tessellate (n={}):\n {tess_str}", 61 + tess_pts.len() 62 + ) 63 + } 64 + 65 + #[test] 66 + fn line3_surface() { 67 + let Ok(line) = Line3::new( 68 + Point3::from_mm(1.0, 2.0, 3.0), 69 + Point3::from_mm(7.0, 6.0, 5.0), 70 + TOL, 71 + ) else { 72 + panic!("endpoints distinct"); 73 + }; 74 + insta::assert_snapshot!(fmt_curve3( 75 + "line3", 76 + &line, 77 + ChordHeightTolerance::from_mm(0.5) 78 + )); 79 + } 80 + 81 + #[test] 82 + fn circle3_xy_surface() { 83 + let Ok(circle) = Circle3::new(xy_plane(Point3::from_mm(2.0, -1.0, 0.0)), mm(3.0), TOL) else { 84 + panic!("radius positive"); 85 + }; 86 + insta::assert_snapshot!(fmt_curve3( 87 + "circle3_xy", 88 + &circle, 89 + ChordHeightTolerance::from_mm(0.5) 90 + )); 91 + } 92 + 93 + #[test] 94 + fn circle3_tilted_surface() { 95 + let Ok(circle) = Circle3::new(xz_plane(Point3::from_mm(0.0, 4.0, 0.0)), mm(5.0), TOL) else { 96 + panic!("radius positive"); 97 + }; 98 + insta::assert_snapshot!(fmt_curve3( 99 + "circle3_tilted", 100 + &circle, 101 + ChordHeightTolerance::from_mm(0.5) 102 + )); 103 + } 104 + 105 + #[test] 106 + fn arc3_quarter_surface() { 107 + let Ok(arc) = Arc3::new( 108 + xy_plane(Point3::origin()), 109 + mm(5.0), 110 + Angle::new::<radian>(0.0), 111 + Angle::new::<radian>(core::f64::consts::FRAC_PI_2), 112 + TOL, 113 + ) else { 114 + panic!("sweep nonzero"); 115 + }; 116 + insta::assert_snapshot!(fmt_curve3( 117 + "arc3_q", 118 + &arc, 119 + ChordHeightTolerance::from_mm(0.5) 120 + )); 121 + } 122 + 123 + #[test] 124 + fn arc3_tilted_half_surface() { 125 + let Ok(arc) = Arc3::new( 126 + xz_plane(Point3::from_mm(1.0, 0.0, 1.0)), 127 + mm(2.0), 128 + Angle::new::<radian>(core::f64::consts::PI), 129 + Angle::new::<radian>(-core::f64::consts::PI), 130 + TOL, 131 + ) else { 132 + panic!("sweep nonzero"); 133 + }; 134 + insta::assert_snapshot!(fmt_curve3( 135 + "arc3_h", 136 + &arc, 137 + ChordHeightTolerance::from_mm(0.5) 138 + )); 139 + } 140 + 141 + #[test] 142 + fn polyline3_surface() { 143 + let Ok(poly) = Polyline3::new( 144 + vec![ 145 + Point3::from_mm(0.0, 0.0, 0.0), 146 + Point3::from_mm(2.0, 0.0, 1.0), 147 + Point3::from_mm(2.0, 3.0, 1.0), 148 + Point3::from_mm(-1.0, 3.0, 4.0), 149 + ], 150 + TOL, 151 + ) else { 152 + panic!("at least two distinct vertices"); 153 + }; 154 + insta::assert_snapshot!(fmt_curve3( 155 + "polyline3", 156 + &poly, 157 + ChordHeightTolerance::from_mm(0.5) 158 + )); 159 + } 160 + 161 + #[test] 162 + fn line3_rejects_degenerate_endpoints() { 163 + assert!( 164 + Line3::new( 165 + Point3::from_mm(1.0, 1.0, 1.0), 166 + Point3::from_mm(1.0, 1.0, 1.0 + 1e-12), 167 + TOL, 168 + ) 169 + .is_err() 170 + ); 171 + } 172 + 173 + #[test] 174 + fn circle3_rejects_zero_radius() { 175 + assert!(Circle3::new(xy_plane(Point3::origin()), mm(0.0), TOL).is_err()); 176 + } 177 + 178 + #[test] 179 + fn arc3_rejects_zero_sweep() { 180 + assert!( 181 + Arc3::new( 182 + xy_plane(Point3::origin()), 183 + mm(1.0), 184 + Angle::new::<radian>(0.0), 185 + Angle::new::<radian>(0.0), 186 + TOL, 187 + ) 188 + .is_err() 189 + ); 190 + } 191 + 192 + #[test] 193 + fn polyline3_rejects_too_few_and_zero_segments() { 194 + assert!(Polyline3::new(vec![Point3::origin()], TOL).is_err()); 195 + assert!( 196 + Polyline3::new( 197 + vec![ 198 + Point3::from_mm(1.0, 1.0, 1.0), 199 + Point3::from_mm(1.0, 1.0, 1.0) 200 + ], 201 + TOL, 202 + ) 203 + .is_err() 204 + ); 205 + } 206 + 207 + #[test] 208 + fn circle3_xy_bbox_is_flat_in_normal() { 209 + let Ok(circle) = Circle3::new(xy_plane(Point3::from_mm(2.0, -1.0, 5.0)), mm(3.0), TOL) else { 210 + panic!("radius positive"); 211 + }; 212 + let bbox = circle.bounding_box(); 213 + let (lx, ly, lz) = bbox.min().coords_mm(); 214 + let (hx, hy, hz) = bbox.max().coords_mm(); 215 + assert!((lx + 1.0).abs() < 1e-9 && (hx - 5.0).abs() < 1e-9); 216 + assert!((ly + 4.0).abs() < 1e-9 && (hy - 2.0).abs() < 1e-9); 217 + assert!((lz - 5.0).abs() < 1e-9 && (hz - 5.0).abs() < 1e-9); 218 + } 219 + 220 + #[test] 221 + fn circle3_tilted_bbox_matches_projection() { 222 + let Ok(circle) = Circle3::new(xz_plane(Point3::from_mm(0.0, 4.0, 0.0)), mm(5.0), TOL) else { 223 + panic!("radius positive"); 224 + }; 225 + let bbox = circle.bounding_box(); 226 + let (lx, ly, lz) = bbox.min().coords_mm(); 227 + let (hx, hy, hz) = bbox.max().coords_mm(); 228 + assert!((lx + 5.0).abs() < 1e-9 && (hx - 5.0).abs() < 1e-9); 229 + assert!((ly - 4.0).abs() < 1e-9 && (hy - 4.0).abs() < 1e-9); 230 + assert!((lz + 5.0).abs() < 1e-9 && (hz - 5.0).abs() < 1e-9); 231 + } 232 + 233 + #[test] 234 + fn polyline3_bbox_spans_vertices() { 235 + let Ok(poly) = Polyline3::new( 236 + vec![ 237 + Point3::from_mm(0.0, 0.0, 0.0), 238 + Point3::from_mm(2.0, -1.0, 5.0), 239 + Point3::from_mm(-3.0, 4.0, 1.0), 240 + ], 241 + TOL, 242 + ) else { 243 + panic!("distinct vertices"); 244 + }; 245 + let bbox = poly.bounding_box(); 246 + let (lx, ly, lz) = bbox.min().coords_mm(); 247 + let (hx, hy, hz) = bbox.max().coords_mm(); 248 + assert!((lx + 3.0).abs() < 1e-9 && (ly + 1.0).abs() < 1e-9 && lz.abs() < 1e-9); 249 + assert!((hx - 2.0).abs() < 1e-9 && (hy - 4.0).abs() < 1e-9 && (hz - 5.0).abs() < 1e-9); 250 + } 251 + 252 + fn dist_mm(a: Point3, b: Point3) -> f64 { 253 + (a - b).norm_mm() 254 + } 255 + 256 + fn brute_min_distance<C: Curve3>(curve: &C, query: Point3) -> f64 { 257 + (0..=20_000) 258 + .map(|i| { 259 + dist_mm( 260 + curve.evaluate(Parameter::new(f64::from(i) / 20_000.0)), 261 + query, 262 + ) 263 + }) 264 + .fold(f64::INFINITY, f64::min) 265 + } 266 + 267 + fn assert_closest_contract<C: Curve3>(name: &str, curve: &C, query: Point3) { 268 + let cp = curve.closest_point(query, TOL); 269 + let reported = cp.distance().get::<millimeter>(); 270 + let to_returned = dist_mm(query, cp.point()); 271 + assert!( 272 + (reported - to_returned).abs() < 1e-9, 273 + "{name}: reported distance {reported} disagrees with distance to returned point {to_returned}" 274 + ); 275 + assert!( 276 + reported <= brute_min_distance(curve, query) + 1e-6, 277 + "{name}: reported distance {reported} undershoots the nearest sampled point" 278 + ); 279 + } 280 + 281 + #[test] 282 + fn closest_point_distance_matches_returned_point() { 283 + let Ok(line) = Line3::new( 284 + Point3::from_mm(0.0, 0.0, 0.0), 285 + Point3::from_mm(4.0, 0.0, 0.0), 286 + TOL, 287 + ) else { 288 + panic!("distinct endpoints"); 289 + }; 290 + assert_closest_contract("line/side", &line, Point3::from_mm(2.0, 3.0, 0.0)); 291 + assert_closest_contract("line/past_end", &line, Point3::from_mm(9.0, 1.0, -2.0)); 292 + assert_closest_contract("line/before_start", &line, Point3::from_mm(-5.0, 2.0, 0.0)); 293 + 294 + let Ok(quarter) = Arc3::new( 295 + xy_plane(Point3::origin()), 296 + mm(5.0), 297 + Angle::new::<radian>(0.0), 298 + Angle::new::<radian>(core::f64::consts::FRAC_PI_2), 299 + TOL, 300 + ) else { 301 + panic!("sweep nonzero"); 302 + }; 303 + assert_closest_contract("arc/inside", &quarter, Point3::from_mm(3.0, 3.0, 0.0)); 304 + assert_closest_contract( 305 + "arc/inside_offplane", 306 + &quarter, 307 + Point3::from_mm(3.0, 3.0, 4.0), 308 + ); 309 + assert_closest_contract( 310 + "arc/below_start", 311 + &quarter, 312 + Point3::from_mm(10.0, -10.0, 0.0), 313 + ); 314 + assert_closest_contract("arc/past_end", &quarter, Point3::from_mm(-10.0, 10.0, 0.0)); 315 + 316 + let Ok(tilted) = Arc3::new( 317 + xz_plane(Point3::from_mm(1.0, 0.0, 1.0)), 318 + mm(2.0), 319 + Angle::new::<radian>(core::f64::consts::PI), 320 + Angle::new::<radian>(-core::f64::consts::PI), 321 + TOL, 322 + ) else { 323 + panic!("sweep nonzero"); 324 + }; 325 + assert_closest_contract("tilted/origin", &tilted, Point3::origin()); 326 + assert_closest_contract("tilted/far", &tilted, Point3::from_mm(-5.0, 3.0, 5.0)); 327 + 328 + let Ok(circle) = Circle3::new(xy_plane(Point3::from_mm(2.0, -1.0, 0.0)), mm(3.0), TOL) else { 329 + panic!("radius positive"); 330 + }; 331 + assert_closest_contract("circle/in_plane", &circle, Point3::from_mm(8.0, -1.0, 0.0)); 332 + assert_closest_contract("circle/offplane", &circle, Point3::from_mm(2.0, -1.0, 6.0)); 333 + assert_closest_contract("circle/center", &circle, Point3::from_mm(2.0, -1.0, 0.0)); 334 + 335 + let Ok(poly) = Polyline3::new( 336 + vec![ 337 + Point3::from_mm(0.0, 0.0, 0.0), 338 + Point3::from_mm(2.0, 0.0, 1.0), 339 + Point3::from_mm(2.0, 3.0, 1.0), 340 + ], 341 + TOL, 342 + ) else { 343 + panic!("distinct vertices"); 344 + }; 345 + assert_closest_contract("poly/mid", &poly, Point3::from_mm(1.0, 2.0, 3.0)); 346 + assert_closest_contract("poly/past_end", &poly, Point3::from_mm(5.0, 5.0, 0.0)); 347 + }
+18
crates/bone-kernel/tests/snapshots/curves3__arc3_quarter_surface.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/curves3.rs 3 + expression: "fmt_curve3(\"arc3_q\", &arc, ChordHeightTolerance::from_mm(0.5))" 4 + --- 5 + arc3_q_display = arc3{ c=(0 mm, 0 mm, 0 mm), r=5 mm, start=0 rad, sweep=1.5707963267948966 rad, normal=[0, 0, 1] } 6 + arc3_q_debug = Arc3 { plane: Plane3 { origin: Point3(0 mm, 0 mm, 0 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) }, radius: 0.005 m^1, start_angle: 0.0, sweep_angle: 1.5707963267948966 } 7 + arc3_q_samples: 8 + t=0.00 -> p=(5.000000, 0.000000, 0.000000) v=<0 mm, 7.853981633974483 mm, 0 mm> 9 + t=0.25 -> p=(4.619398, 1.913417, 0.000000) v=<-3.0055886494217314 mm, 7.256132880348577 mm, 0 mm> 10 + t=0.50 -> p=(3.535534, 3.535534, 0.000000) v=<-5.553603672697957 mm, 5.553603672697958 mm, 0 mm> 11 + t=0.75 -> p=(1.913417, 4.619398, 0.000000) v=<-7.256132880348577 mm, 3.005588649421732 mm, 0 mm> 12 + t=1.00 -> p=(0.000000, 5.000000, 0.000000) v=<-7.853981633974483 mm, 0.0000000000000004809176734304475 mm, 0 mm> 13 + arc3_q_bbox = aabb[(0.0000000000000003061616997868383 mm, 0 mm, 0 mm)..(5 mm, 5 mm, 0 mm)] 14 + arc3_q_closest_origin = closest{ t=0, p=(5 mm, 0 mm, 0 mm), d=5 mm } 15 + arc3_q_tessellate (n=3): 16 + (5.000000, 0.000000, 0.000000) 17 + (3.535534, 3.535534, 0.000000) 18 + (0.000000, 5.000000, 0.000000)
+19
crates/bone-kernel/tests/snapshots/curves3__arc3_tilted_half_surface.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/curves3.rs 3 + expression: "fmt_curve3(\"arc3_h\", &arc, ChordHeightTolerance::from_mm(0.5))" 4 + --- 5 + arc3_h_display = arc3{ c=(1 mm, 0 mm, 1 mm), r=2 mm, start=3.141592653589793 rad, sweep=-3.141592653589793 rad, normal=[0, -1, 0] } 6 + arc3_h_debug = Arc3 { plane: Plane3 { origin: Point3(1 mm, 0 mm, 1 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 0.0, 1.0]]) }, radius: 0.002 m^1, start_angle: 3.141592653589793, sweep_angle: -3.141592653589793 } 7 + arc3_h_samples: 8 + t=0.00 -> p=(-1.000000, 0.000000, 1.000000) v=<0.0000000000000007694682774887159 mm, 0 mm, 6.283185307179586 mm> 9 + t=0.25 -> p=(-0.414214, 0.000000, 2.414214) v=<4.442882938158366 mm, 0 mm, 4.442882938158366 mm> 10 + t=0.50 -> p=(1.000000, 0.000000, 3.000000) v=<6.283185307179586 mm, -0 mm, -0.00000000000000038473413874435795 mm> 11 + t=0.75 -> p=(2.414214, 0.000000, 2.414214) v=<4.442882938158366 mm, -0 mm, -4.442882938158366 mm> 12 + t=1.00 -> p=(3.000000, 0.000000, 1.000000) v=<-0 mm, -0 mm, -6.283185307179586 mm> 13 + arc3_h_bbox = aabb[(-1 mm, 0 mm, 1 mm)..(3 mm, 0 mm, 3 mm)] 14 + arc3_h_closest_origin = closest{ t=0, p=(-1 mm, 0 mm, 1.0000000000000002 mm), d=1.4142135623730951 mm } 15 + arc3_h_tessellate (n=4): 16 + (-1.000000, 0.000000, 1.000000) 17 + (-0.000000, 0.000000, 2.732051) 18 + (2.000000, 0.000000, 2.732051) 19 + (3.000000, 0.000000, 1.000000)
+22
crates/bone-kernel/tests/snapshots/curves3__circle3_tilted_surface.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/curves3.rs 3 + expression: "fmt_curve3(\"circle3_tilted\", &circle, ChordHeightTolerance::from_mm(0.5))" 4 + --- 5 + circle3_tilted_display = circle3{ c=(0 mm, 4 mm, 0 mm), r=5 mm, normal=[0, -1, 0] } 6 + circle3_tilted_debug = Circle3 { plane: Plane3 { origin: Point3(0 mm, 4 mm, 0 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 0.0, 1.0]]) }, radius: 0.005 m^1 } 7 + circle3_tilted_samples: 8 + t=0.00 -> p=(5.000000, 4.000000, 0.000000) v=<0 mm, 0 mm, 31.41592653589793 mm> 9 + t=0.25 -> p=(0.000000, 4.000000, 5.000000) v=<-31.41592653589793 mm, 0 mm, 0.00000000000000192367069372179 mm> 10 + t=0.50 -> p=(-5.000000, 4.000000, 0.000000) v=<-0.00000000000000384734138744358 mm, -0 mm, -31.41592653589793 mm> 11 + t=0.75 -> p=(-0.000000, 4.000000, -5.000000) v=<31.41592653589793 mm, 0 mm, -0.000000000000005771012081165369 mm> 12 + t=1.00 -> p=(5.000000, 4.000000, -0.000000) v=<0.00000000000000769468277488716 mm, 0 mm, 31.41592653589793 mm> 13 + circle3_tilted_bbox = aabb[(-5 mm, 4 mm, -5 mm)..(5 mm, 4 mm, 5 mm)] 14 + circle3_tilted_closest_origin = closest{ t=0, p=(5 mm, 4 mm, 0 mm), d=6.403124237432849 mm } 15 + circle3_tilted_tessellate (n=7): 16 + (5.000000, 4.000000, 0.000000) 17 + (3.117449, 4.000000, 3.909157) 18 + (-1.112605, 4.000000, 4.874640) 19 + (-4.504844, 4.000000, 2.169419) 20 + (-4.504844, 4.000000, -2.169419) 21 + (-1.112605, 4.000000, -4.874640) 22 + (3.117449, 4.000000, -3.909157)
+21
crates/bone-kernel/tests/snapshots/curves3__circle3_xy_surface.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/curves3.rs 3 + expression: "fmt_curve3(\"circle3_xy\", &circle, ChordHeightTolerance::from_mm(0.5))" 4 + --- 5 + circle3_xy_display = circle3{ c=(2 mm, -1 mm, 0 mm), r=3 mm, normal=[0, 0, 1] } 6 + circle3_xy_debug = Circle3 { plane: Plane3 { origin: Point3(2 mm, -1 mm, 0 mm), x: UnitVec3([[1.0, 0.0, 0.0]]), y: UnitVec3([[0.0, 1.0, 0.0]]) }, radius: 0.003 m^1 } 7 + circle3_xy_samples: 8 + t=0.00 -> p=(5.000000, -1.000000, 0.000000) v=<0 mm, 18.84955592153876 mm, 0 mm> 9 + t=0.25 -> p=(2.000000, 2.000000, 0.000000) v=<-18.84955592153876 mm, 0.0000000000000011542024162330739 mm, 0 mm> 10 + t=0.50 -> p=(-1.000000, -1.000000, 0.000000) v=<-0.0000000000000023084048324661477 mm, -18.84955592153876 mm, -0 mm> 11 + t=0.75 -> p=(2.000000, -4.000000, 0.000000) v=<18.84955592153876 mm, -0.0000000000000034626072486992218 mm, 0 mm> 12 + t=1.00 -> p=(5.000000, -1.000000, 0.000000) v=<0.0000000000000046168096649322955 mm, 18.84955592153876 mm, 0 mm> 13 + circle3_xy_bbox = aabb[(-1 mm, -4 mm, 0 mm)..(5 mm, 2 mm, 0 mm)] 14 + circle3_xy_closest_origin = closest{ t=0.42620819117478337, p=(-0.6832815729997477 mm, 0.3416407864998743 mm, 0 mm), d=0.7639320225002102 mm } 15 + circle3_xy_tessellate (n=6): 16 + (5.000000, -1.000000, 0.000000) 17 + (3.500000, 1.598076, 0.000000) 18 + (0.500000, 1.598076, 0.000000) 19 + (-1.000000, -1.000000, 0.000000) 20 + (0.500000, -3.598076, 0.000000) 21 + (3.500000, -3.598076, 0.000000)
+17
crates/bone-kernel/tests/snapshots/curves3__line3_surface.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/curves3.rs 3 + expression: "fmt_curve3(\"line3\", &line, ChordHeightTolerance::from_mm(0.5))" 4 + --- 5 + line3_display = line3{ (1 mm, 2 mm, 3 mm) -> (7 mm, 6 mm, 5 mm) } 6 + line3_debug = Line3 { start: Point3(1 mm, 2 mm, 3 mm), end: Point3(7 mm, 6 mm, 5 mm) } 7 + line3_samples: 8 + t=0.00 -> p=(1.000000, 2.000000, 3.000000) v=<6 mm, 4 mm, 2 mm> 9 + t=0.25 -> p=(2.500000, 3.000000, 3.500000) v=<6 mm, 4 mm, 2 mm> 10 + t=0.50 -> p=(4.000000, 4.000000, 4.000000) v=<6 mm, 4 mm, 2 mm> 11 + t=0.75 -> p=(5.500000, 5.000000, 4.500000) v=<6 mm, 4 mm, 2 mm> 12 + t=1.00 -> p=(7.000000, 6.000000, 5.000000) v=<6 mm, 4 mm, 2 mm> 13 + line3_bbox = aabb[(1 mm, 2 mm, 3 mm)..(7 mm, 6 mm, 5 mm)] 14 + line3_closest_origin = closest{ t=0, p=(1 mm, 2 mm, 3 mm), d=3.7416573867739413 mm } 15 + line3_tessellate (n=2): 16 + (1.000000, 2.000000, 3.000000) 17 + (7.000000, 6.000000, 5.000000)
+19
crates/bone-kernel/tests/snapshots/curves3__polyline3_surface.snap
··· 1 + --- 2 + source: crates/bone-kernel/tests/curves3.rs 3 + expression: "fmt_curve3(\"polyline3\", &poly, ChordHeightTolerance::from_mm(0.5))" 4 + --- 5 + polyline3_display = polyline3{ 4 verts: (0 mm, 0 mm, 0 mm) -> (2 mm, 0 mm, 1 mm) -> (2 mm, 3 mm, 1 mm) -> (-1 mm, 3 mm, 4 mm) } 6 + polyline3_debug = Polyline3 { vertices: [Point3(0 mm, 0 mm, 0 mm), Point3(2 mm, 0 mm, 1 mm), Point3(2 mm, 3 mm, 1 mm), Point3(-1 mm, 3 mm, 4 mm)] } 7 + polyline3_samples: 8 + t=0.00 -> p=(0.000000, 0.000000, 0.000000) v=<6 mm, 0 mm, 3 mm> 9 + t=0.25 -> p=(1.500000, 0.000000, 0.750000) v=<6 mm, 0 mm, 3 mm> 10 + t=0.50 -> p=(2.000000, 1.500000, 1.000000) v=<0 mm, 9 mm, 0 mm> 11 + t=0.75 -> p=(1.250000, 3.000000, 1.750000) v=<-9 mm, 0 mm, 9 mm> 12 + t=1.00 -> p=(-1.000000, 3.000000, 4.000000) v=<-9 mm, 0 mm, 9 mm> 13 + polyline3_bbox = aabb[(-1 mm, 0 mm, 0 mm)..(2 mm, 3 mm, 4 mm)] 14 + polyline3_closest_origin = closest{ t=0, p=(0 mm, 0 mm, 0 mm), d=0 mm } 15 + polyline3_tessellate (n=4): 16 + (0.000000, 0.000000, 0.000000) 17 + (2.000000, 0.000000, 1.000000) 18 + (2.000000, 3.000000, 1.000000) 19 + (-1.000000, 3.000000, 4.000000)