Another project
1use bone_types::dimensioned_serde;
2use bone_types::{
3 Aabb3, Angle, AngleTolerance, ChordHeightTolerance, Length, Parameter, Plane3, Point3,
4 Tolerance, UnitVec3, Vec3,
5};
6use core::f64::consts::TAU;
7use serde::{Deserialize, Serialize};
8use uom::si::angle::radian;
9use uom::si::length::millimeter;
10
11use crate::KernelError;
12use crate::angles;
13use crate::circle3::Circle3;
14use crate::circular3;
15use crate::closest::ClosestPoint3;
16use crate::curve3::{Curve3, Curve3Kind};
17
18#[derive(Copy, Clone, Debug, PartialEq, Serialize, Deserialize)]
19#[serde(deny_unknown_fields)]
20pub struct Arc3 {
21 plane: Plane3,
22 #[serde(with = "dimensioned_serde::length_si")]
23 radius: Length,
24 #[serde(with = "dimensioned_serde::angle_si")]
25 start_angle: Angle,
26 #[serde(with = "dimensioned_serde::angle_si")]
27 sweep_angle: Angle,
28}
29
30impl Arc3 {
31 pub fn new(
32 plane: Plane3,
33 radius: Length,
34 start_angle: Angle,
35 sweep_angle: Angle,
36 tolerance: Tolerance,
37 ) -> Result<Self, KernelError> {
38 if radius.get::<millimeter>() < tolerance.value() {
39 return Err(KernelError::DegenerateArc);
40 }
41 let sweep = sweep_angle.get::<radian>();
42 let angle_eps = AngleTolerance::from_arc_length(tolerance, radius).radians();
43 if sweep.abs() < angle_eps {
44 return Err(KernelError::DegenerateArc);
45 }
46 if sweep.abs() > TAU + angle_eps {
47 return Err(KernelError::DegenerateArc);
48 }
49 Ok(Self {
50 plane,
51 radius,
52 start_angle,
53 sweep_angle,
54 })
55 }
56
57 #[must_use]
58 pub fn center(self) -> Point3 {
59 self.plane.origin()
60 }
61
62 #[must_use]
63 pub const fn plane(self) -> Plane3 {
64 self.plane
65 }
66
67 #[must_use]
68 pub fn normal(self) -> UnitVec3 {
69 self.plane.normal()
70 }
71
72 #[must_use]
73 pub const fn radius(self) -> Length {
74 self.radius
75 }
76
77 #[must_use]
78 pub const fn start_angle(self) -> Angle {
79 self.start_angle
80 }
81
82 #[must_use]
83 pub const fn sweep_angle(self) -> Angle {
84 self.sweep_angle
85 }
86
87 #[must_use]
88 pub fn radius_mm(self) -> f64 {
89 self.radius.get::<millimeter>()
90 }
91
92 #[must_use]
93 pub fn start_rad(self) -> f64 {
94 self.start_angle.get::<radian>()
95 }
96
97 #[must_use]
98 pub fn sweep_rad(self) -> f64 {
99 self.sweep_angle.get::<radian>()
100 }
101
102 #[must_use]
103 pub fn as_full_circle(self) -> Circle3 {
104 Circle3::from_raw(self.plane, self.radius)
105 }
106
107 #[must_use]
108 pub fn as_kind(self) -> Curve3Kind {
109 Curve3Kind::Arc(self)
110 }
111
112 #[must_use]
113 pub fn contains_point(self, p: Point3, tolerance: Tolerance) -> bool {
114 let (px, py, pn) = circular3::local_coords(self.plane, p);
115 let radial = (px * px + py * py).sqrt();
116 if (radial - self.radius_mm()).abs() > tolerance.value() || pn.abs() > tolerance.value() {
117 return false;
118 }
119 let angle_eps = AngleTolerance::from_arc_length(tolerance, self.radius).radians();
120 angles::contains(py.atan2(px), self.start_rad(), self.sweep_rad(), angle_eps)
121 }
122
123 fn angle_at(self, t: Parameter) -> Angle {
124 Angle::new::<radian>(self.start_rad() + self.sweep_rad() * t.value())
125 }
126}
127
128impl Curve3 for Arc3 {
129 fn evaluate(&self, t: Parameter) -> Point3 {
130 circular3::point_at(self.plane, self.radius, self.angle_at(t))
131 }
132
133 fn derivative(&self, t: Parameter) -> Vec3 {
134 circular3::tangent_vec(self.plane, self.radius, self.angle_at(t)) * self.sweep_rad()
135 }
136
137 fn bounding_box(&self) -> Aabb3 {
138 circular3::bounding_box(self.plane, self.radius, self.start_angle, self.sweep_angle)
139 }
140
141 fn closest_point(&self, p: Point3, tolerance: Tolerance) -> ClosestPoint3 {
142 let (px, py, _) = circular3::local_coords(self.plane, p);
143 let radial = (px * px + py * py).sqrt();
144 let theta_unclamped = if radial < tolerance.value() {
145 self.start_rad()
146 } else {
147 py.atan2(px)
148 };
149 let theta = angles::clamp(theta_unclamped, self.start_rad(), self.sweep_rad(), 0.0);
150 let sweep = self.sweep_rad();
151 let delta = if sweep >= 0.0 {
152 (theta - self.start_rad()).rem_euclid(TAU)
153 } else {
154 (self.start_rad() - theta).rem_euclid(TAU)
155 };
156 let t = (delta / sweep.abs()).clamp(0.0, 1.0);
157 let point = circular3::point_at(self.plane, self.radius, Angle::new::<radian>(theta));
158 ClosestPoint3::new(Parameter::new(t), point, (p - point).norm())
159 }
160
161 fn tessellate(&self, tolerance: ChordHeightTolerance) -> Vec<Point3> {
162 let (start, sweep) = (self.start_rad(), self.sweep_rad());
163 let n = circular3::sample_count(self.radius, self.sweep_angle, tolerance, 1);
164 (0..=n)
165 .map(|i| {
166 circular3::point_at(
167 self.plane,
168 self.radius,
169 Angle::new::<radian>(start + sweep * f64::from(i) / f64::from(n)),
170 )
171 })
172 .collect()
173 }
174}
175
176impl core::fmt::Display for Arc3 {
177 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
178 write!(
179 f,
180 "arc3{{ c={}, r={} mm, start={} rad, sweep={} rad, normal={} }}",
181 self.center(),
182 self.radius_mm(),
183 self.start_rad(),
184 self.sweep_rad(),
185 self.normal(),
186 )
187 }
188}