geop_core_sketch/
geometry.rs1use geop_core_math::{geop_error::GeopResult, scalars::Scalar};
21
22#[derive(Clone, Copy, Debug)]
24pub struct V<T> {
25 pub x: T,
26 pub y: T,
27}
28
29#[allow(clippy::should_implement_trait)]
32impl<T: Scalar> V<T> {
33 pub fn new(x: T, y: T) -> Self {
34 V { x, y }
35 }
36 pub fn cst(p: [f64; 2]) -> Self {
37 V::new(T::from_f64(p[0]), T::from_f64(p[1]))
38 }
39 pub fn add(self, o: Self) -> Self {
40 V::new(self.x.add(o.x), self.y.add(o.y))
41 }
42 pub fn sub(self, o: Self) -> Self {
43 V::new(self.x.sub(o.x), self.y.sub(o.y))
44 }
45 pub fn scale(self, s: T) -> Self {
46 V::new(self.x.mul(s), self.y.mul(s))
47 }
48 pub fn dot(self, o: Self) -> T {
49 self.x.mul(o.x).add(self.y.mul(o.y))
50 }
51 pub fn cross(self, o: Self) -> T {
52 self.x.mul(o.y).sub(self.y.mul(o.x))
53 }
54 pub fn norm(self) -> GeopResult<T> {
55 self.dot(self).sqrt()
56 }
57 pub fn perp(self) -> Self {
59 V::new(T::ZERO.sub(self.y), self.x)
60 }
61 pub fn unit(self) -> GeopResult<Self> {
62 Ok(self.scale(T::ONE.div(self.norm()?)?))
63 }
64 pub fn rotate(self, c: T, s: T) -> Self {
66 V::new(
67 self.x.mul(c).sub(self.y.mul(s)),
68 self.x.mul(s).add(self.y.mul(c)),
69 )
70 }
71 pub fn value(self) -> [f64; 2] {
72 [self.x.to_f64(), self.y.to_f64()]
73 }
74}
75
76#[derive(Clone, Copy, Debug)]
79pub struct Arc<T> {
80 pub s: V<T>,
81 pub e: V<T>,
82 pub half: T,
83}
84
85impl<T: Scalar> Arc<T> {
86 pub fn chord(&self) -> V<T> {
87 self.e.sub(self.s)
88 }
89 pub fn chord_length(&self) -> GeopResult<T> {
90 self.chord().norm()
91 }
92 pub fn chord_mid(&self) -> V<T> {
93 self.s.add(self.e).scale(T::from_f64(0.5))
94 }
95 pub fn left(&self) -> GeopResult<V<T>> {
98 Ok(self.chord().unit()?.perp())
99 }
100 pub fn curvature(&self) -> GeopResult<T> {
101 T::TWO.mul(self.half.sin()).div(self.chord_length()?)
102 }
103 pub fn radius(&self) -> GeopResult<T> {
105 self.chord_length()?.div(T::TWO.mul(self.half.sin().abs()))
106 }
107 pub fn center(&self) -> GeopResult<V<T>> {
111 let d = self
112 .chord_length()?
113 .mul(T::from_f64(0.5))
114 .mul(self.half.cos())
115 .div(self.half.sin())?;
116 Ok(self.chord_mid().add(self.left()?.scale(d)))
117 }
118 pub fn arc_mid(&self) -> GeopResult<V<T>> {
120 let tan_quarter = self.half.sin().div(T::ONE.add(self.half.cos()))?;
121 let sagitta = self.chord_length()?.mul(T::from_f64(0.5)).mul(tan_quarter);
122 Ok(self.chord_mid().sub(self.left()?.scale(sagitta)))
123 }
124 pub fn circle_residual(&self, p: V<T>) -> GeopResult<T> {
134 let q = p.sub(self.chord_mid());
135 let k = self.curvature()?;
136 let l = self.chord_length()?;
137 let g = k
138 .mul(q.dot(q))
139 .sub(T::TWO.mul(self.half.cos()).mul(self.left()?.dot(q)))
140 .sub(k.mul(l).mul(l).mul(T::from_f64(0.25)));
141 Ok(g.mul(T::from_f64(0.5)))
142 }
143 pub fn tangent_start(&self) -> GeopResult<V<T>> {
145 let c = self.chord().unit()?;
146 Ok(c.rotate(self.half.cos(), T::ZERO.sub(self.half.sin())))
147 }
148 pub fn tangent_end(&self) -> GeopResult<V<T>> {
150 let c = self.chord().unit()?;
151 Ok(c.rotate(self.half.cos(), self.half.sin()))
152 }
153 pub fn length(&self) -> GeopResult<T> {
155 let l = self.chord_length()?;
156 let h = self.half;
157 Ok(if h.to_f64().abs() < 1e-4 {
158 l.mul(
160 T::ONE
161 .add(h.mul(h).mul(T::from_f64(1.0 / 6.0)))
162 .add(h.mul(h).mul(h).mul(h).mul(T::from_f64(7.0 / 360.0))),
163 )
164 } else {
165 l.mul(h).div(h.sin())?
166 })
167 }
168}
169
170pub fn line_distance<T: Scalar>(a: V<T>, b: V<T>, p: V<T>) -> GeopResult<T> {
173 let d = b.sub(a);
174 d.cross(p.sub(a)).div(d.norm()?)
175}
176
177#[cfg(test)]
178mod tests {
179 use super::*;
180 use geop_core_math::scalars::scal_in_f64::ScalInF64;
181
182 fn quarter() -> Arc<ScalInF64> {
183 Arc {
184 s: V::new(ScalInF64::from_f64(1.0), ScalInF64::from_f64(0.0)),
185 e: V::new(ScalInF64::from_f64(0.0), ScalInF64::from_f64(1.0)),
186 half: ScalInF64::from_f64(std::f64::consts::FRAC_PI_4),
187 }
188 }
189
190 fn close(a: [f64; 2], b: [f64; 2]) -> bool {
191 (a[0] - b[0]).abs() < 1e-12 && (a[1] - b[1]).abs() < 1e-12
192 }
193
194 #[test]
195 fn quarter_circle_has_unit_radius_around_origin() {
196 let a = quarter();
197 assert!(close(a.center().unwrap().value(), [0.0, 0.0]));
198 assert!((a.radius().unwrap().to_f64() - 1.0).abs() < 1e-12);
199 assert!((a.curvature().unwrap().to_f64() - 1.0).abs() < 1e-12);
200 let s = std::f64::consts::FRAC_1_SQRT_2;
201 assert!(close(a.arc_mid().unwrap().value(), [s, s]));
202 assert!(close(a.tangent_start().unwrap().value(), [0.0, 1.0]));
203 assert!(close(a.tangent_end().unwrap().value(), [-1.0, 0.0]));
204 assert!((a.length().unwrap().to_f64() - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
205 assert!(
206 a.circle_residual(V::new(ScalInF64::from_f64(-s), ScalInF64::from_f64(-s)))
207 .unwrap()
208 .to_f64()
209 .abs()
210 < 1e-12
211 );
212 assert!(
214 (a.circle_residual(V::new(ScalInF64::from_f64(2.0), ScalInF64::from_f64(0.0)))
215 .unwrap()
216 .to_f64()
217 - 1.5)
218 .abs()
219 < 1e-12
220 );
221 }
222
223 #[test]
224 fn major_arc_center_is_right_of_chord() {
225 let a = Arc {
226 half: ScalInF64::from_f64(3.0 * std::f64::consts::FRAC_PI_4),
227 ..quarter()
228 };
229 assert!(close(a.center().unwrap().value(), [1.0, 1.0]));
230 assert!((a.length().unwrap().to_f64() - 3.0 * std::f64::consts::FRAC_PI_2).abs() < 1e-12);
231 }
232
233 #[test]
234 fn straight_arc_is_its_chord() {
235 let a = Arc {
236 half: ScalInF64::from_f64(0.0),
237 ..quarter()
238 };
239 assert!((a.length().unwrap().to_f64() - 2f64.sqrt()).abs() < 1e-12);
240 assert!(
242 a.circle_residual(V::new(ScalInF64::from_f64(0.5), ScalInF64::from_f64(0.5)))
243 .unwrap()
244 .to_f64()
245 .abs()
246 < 1e-12
247 );
248 assert!(close(a.arc_mid().unwrap().value(), [0.5, 0.5]));
249 }
250}