geop_core_sketch/
geometry.rs1use geop_core_math::{geop_error::GeopResult, scalars::Number, vector::Vector2};
21
22#[derive(Clone, Copy, Debug)]
25pub struct Arc<T> {
26 pub s: Vector2<T>,
27 pub e: Vector2<T>,
28 pub half: T,
29}
30
31impl<T: Number> Arc<T> {
32 pub fn chord(&self) -> Vector2<T> {
33 self.e.sub(&self.s)
34 }
35 pub fn chord_length(&self) -> GeopResult<T> {
36 self.chord().try_norm()
37 }
38 pub fn chord_mid(&self) -> Vector2<T> {
39 self.s.add(&self.e).prod_scalar(half::<T>())
40 }
41 pub fn left(&self) -> GeopResult<Vector2<T>> {
44 Ok(self.chord().normalize()?.perp())
45 }
46 pub fn curvature(&self) -> GeopResult<T> {
47 T::TWO.mul(self.half.sin()).div(self.chord_length()?)
48 }
49 pub fn radius(&self) -> GeopResult<T> {
51 self.chord_length()?.div(T::TWO.mul(self.half.sin().abs()))
52 }
53 pub fn center(&self) -> GeopResult<Vector2<T>> {
57 let d = self
58 .chord_length()?
59 .mul(half::<T>())
60 .mul(self.half.cos())
61 .div(self.half.sin())?;
62 Ok(self.chord_mid().add(&self.left()?.prod_scalar(d)))
63 }
64 pub fn arc_mid(&self) -> GeopResult<Vector2<T>> {
66 let tan_quarter = self.half.sin().div(T::ONE.add(self.half.cos()))?;
67 let sagitta = self.chord_length()?.mul(half::<T>()).mul(tan_quarter);
68 Ok(self.chord_mid().sub(&self.left()?.prod_scalar(sagitta)))
69 }
70 pub fn circle_residual(&self, p: Vector2<T>) -> GeopResult<T> {
80 let q = p.sub(&self.chord_mid());
81 let k = self.curvature()?;
82 let l = self.chord_length()?;
83 let g = k
84 .mul(q.norm_sq())
85 .sub(T::TWO.mul(self.half.cos()).mul(self.left()?.prod_dot(&q)))
86 .sub(k.mul(l).mul(l).mul(half::<T>().mul(half::<T>())));
87 Ok(g.mul(half::<T>()))
88 }
89 pub fn tangent_start(&self) -> GeopResult<Vector2<T>> {
91 let c = self.chord().normalize()?;
92 Ok(c.rotate(self.half.cos(), self.half.sin().neg()))
93 }
94 pub fn tangent_end(&self) -> GeopResult<Vector2<T>> {
96 let c = self.chord().normalize()?;
97 Ok(c.rotate(self.half.cos(), self.half.sin()))
98 }
99 pub fn length(&self) -> GeopResult<T> {
109 let l = self.chord_length()?;
110 let h = self.half;
111 let sin = h.sin();
112 if sin.definitely_not_equal(T::ZERO) || !h.abs().definitely_less(T::ONE) {
113 return l.mul(h).div(sin);
114 }
115 let ratio = |num, den| T::from_ratio(num, den).expect("a positive denominator");
116 let h2 = h.mul(h);
117 let h4 = h2.mul(h2);
118 let rest = T::ZERO.union(ratio(1, 300)).mul(h4).mul(h2);
119 Ok(l.mul(
120 T::ONE
121 .add(h2.mul(ratio(1, 6)))
122 .add(h4.mul(ratio(7, 360)))
123 .add(rest),
124 ))
125 }
126}
127
128fn half<T: Number>() -> T {
130 T::ONE.div(T::TWO).expect("2 is not zero")
131}
132
133pub fn line_distance<T: Number>(a: Vector2<T>, b: Vector2<T>, p: Vector2<T>) -> GeopResult<T> {
136 let d = b.sub(&a);
137 d.prod_cross(&p.sub(&a)).div(d.try_norm()?)
138}
139
140#[cfg(test)]
141mod tests {
142 use super::*;
143 use geop_core_math::scalars::Scalar;
144
145 fn vec2(x: Scalar, y: Scalar) -> Vector2 {
146 Vector2::from_array([x, y])
147 }
148
149 fn quarter() -> Arc<Scalar> {
150 Arc {
151 s: vec2(Scalar::from_f64(1.0), Scalar::from_f64(0.0)),
152 e: vec2(Scalar::from_f64(0.0), Scalar::from_f64(1.0)),
153 half: Scalar::from_f64(std::f64::consts::FRAC_PI_4),
154 }
155 }
156
157 fn close(a: Vector2, b: [f64; 2]) -> bool {
158 (a[0].to_f64() - b[0]).abs() < 1e-12 && (a[1].to_f64() - b[1]).abs() < 1e-12
159 }
160
161 #[test]
162 fn quarter_circle_has_unit_radius_around_origin() {
163 let a = quarter();
164 assert!(close(a.center().unwrap(), [0.0, 0.0]));
165 assert!((a.radius().unwrap().to_f64() - 1.0).abs() < 1e-12);
166 assert!((a.curvature().unwrap().to_f64() - 1.0).abs() < 1e-12);
167 let s = std::f64::consts::FRAC_1_SQRT_2;
168 assert!(close(a.arc_mid().unwrap(), [s, s]));
169 assert!(close(a.tangent_start().unwrap(), [0.0, 1.0]));
170 assert!(close(a.tangent_end().unwrap(), [-1.0, 0.0]));
171 assert!((a.length().unwrap().to_f64() - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
172 assert!(
173 a.circle_residual(vec2(Scalar::from_f64(-s), Scalar::from_f64(-s)))
174 .unwrap()
175 .to_f64()
176 .abs()
177 < 1e-12
178 );
179 assert!(
181 (a.circle_residual(vec2(Scalar::from_f64(2.0), Scalar::from_f64(0.0)))
182 .unwrap()
183 .to_f64()
184 - 1.5)
185 .abs()
186 < 1e-12
187 );
188 }
189
190 #[test]
191 fn major_arc_center_is_right_of_chord() {
192 let a = Arc {
193 half: Scalar::from_f64(3.0 * std::f64::consts::FRAC_PI_4),
194 ..quarter()
195 };
196 assert!(close(a.center().unwrap(), [1.0, 1.0]));
197 assert!((a.length().unwrap().to_f64() - 3.0 * std::f64::consts::FRAC_PI_2).abs() < 1e-12);
198 }
199
200 #[test]
201 fn straight_arc_is_its_chord() {
202 let a = Arc {
203 half: Scalar::from_f64(0.0),
204 ..quarter()
205 };
206 assert!((a.length().unwrap().to_f64() - 2f64.sqrt()).abs() < 1e-12);
207 assert!(
209 a.circle_residual(vec2(Scalar::from_f64(0.5), Scalar::from_f64(0.5)))
210 .unwrap()
211 .to_f64()
212 .abs()
213 < 1e-12
214 );
215 assert!(close(a.arc_mid().unwrap(), [0.5, 0.5]));
216 }
217}