geop_core_geometry/nurb_curve/
area.rs1use geop_core_math::{
25 geop_error::{GeopError, GeopResult},
26 quadrature::{MAX_POLYNOMIAL_DEGREE, Quadrature, integrate, integrate_polynomial},
27 scalars::Scalar,
28 vector::Vector2,
29};
30
31use super::NurbCurve2D;
32
33const RATIONAL: Quadrature = Quadrature {
35 relative_tolerance: 1e-10,
36 max_panels: 64,
37};
38
39impl NurbCurve2D {
40 pub fn area_term(&self, origin: &Vector2) -> GeopResult<Scalar> {
48 let local = self.translate(Vector2::from_array([origin[0].neg(), origin[1].neg()]));
49 let integrand = |t: Scalar| -> GeopResult<Vec<Scalar>> {
50 let [p, d] = local.cartesian_derivatives::<2>(t, 1)?[..] else {
51 unreachable!("a point and its first derivative");
52 };
53 Ok(vec![p[0].mul(d[1]).sub(p[1].mul(d[0]))])
54 };
55 let breaks = self.breakpoints();
56 let degree = (2 * self.degree).saturating_sub(1);
57 let integral = if self.is_polynomial() && degree <= MAX_POLYNOMIAL_DEGREE {
58 integrate_polynomial(integrand, &breaks, 1, degree)?
59 } else {
60 integrate(integrand, &breaks, 1, &RATIONAL)?
61 };
62 integral.value[0].div(Scalar::TWO)
63 }
64
65 fn is_polynomial(&self) -> bool {
68 let w = self.control_points[0][2];
69 let same = |a: Scalar| a.is_subset_of(w) && w.is_subset_of(a);
70 self.control_points.iter().all(|c| same(c[2]))
71 }
72}
73
74pub fn loop_signed_area<'a>(
81 pcurves: impl IntoIterator<Item = &'a NurbCurve2D>,
82) -> GeopResult<Scalar> {
83 let mut pcurves = pcurves.into_iter().peekable();
84 let first = pcurves
85 .peek()
86 .ok_or_else(|| GeopError::new("loop_signed_area: a loop of no curves"))?;
87 let origin = first.ends()?.0.sharpen();
88 pcurves.try_fold(Scalar::ZERO, |area, pcurve| {
89 Ok(area.add(pcurve.area_term(&origin)?))
90 })
91}
92
93#[cfg(test)]
94mod tests {
95 use super::*;
96 use geop_core_math::scalars::Scalar;
97
98 use crate::nurb_curve::sqrt2_over_2;
99
100 fn v2(u: f64, v: f64) -> Vector2 {
101 Vector2::from_array([Scalar::from_f64(u), Scalar::from_f64(v)])
102 }
103
104 #[test]
107 fn square_area_is_exact() {
108 let corners = [(1000., 500.), (1001., 500.), (1001., 501.), (1000., 501.)];
109 let ccw: Vec<NurbCurve2D> = (0..4)
110 .map(|k| {
111 let (a, b) = (corners[k], corners[(k + 1) % 4]);
112 NurbCurve2D::line(v2(a.0, a.1), v2(b.0, b.1)).unwrap()
113 })
114 .collect();
115 let area = loop_signed_area(&ccw).unwrap();
116 assert!(area.could_be_equal(Scalar::ONE), "{area:?}");
117 assert!(area.definitely_greater(Scalar::ZERO), "{area:?}");
118 let cw: Vec<NurbCurve2D> = ccw.iter().rev().map(|c| c.reverse()).collect();
119 let area = loop_signed_area(&cw).unwrap();
120 assert!(area.could_be_equal(Scalar::ONE.neg()), "{area:?}");
121 }
122
123 #[test]
126 fn disc_area() {
127 let r = 2.0;
128 let w = sqrt2_over_2();
129 let at = |k: usize| {
130 let (c, s) = [(1., 0.), (0., 1.), (-1., 0.), (0., -1.)][k % 4];
131 v2(3. + r * c, -1. + r * s)
132 };
133 let center = v2(3., -1.);
134 let quarters: Vec<NurbCurve2D> = (0..4)
135 .map(|k| {
136 let mid = at(k).add(&at(k + 1)).sub(¢er);
137 NurbCurve2D::arc(at(k), mid, at(k + 1), w).unwrap()
138 })
139 .collect();
140 let area = loop_signed_area(&quarters).unwrap();
141 let exact = std::f64::consts::PI * r * r;
142 assert!(
143 area.could_be_equal(Scalar::from_f64(exact)),
144 "{area:?}, expected {exact}"
145 );
146 }
147}