geop_core_geometry/nurb_surface/
sphere.rs1use geop_core_math::{
6 combinatorics::binomial,
7 geop_error::{GeopError, GeopResult},
8 scalars::Scalar,
9 vector::{Vector3, Vector4},
10};
11
12use super::NurbSurface3D;
13
14fn product(a: &[Scalar], b: &[Vector4]) -> GeopResult<Vec<Vector4>> {
20 let (m, n) = (a.len() - 1, b.len() - 1);
21 (0..=m + n)
22 .map(|k| {
23 let mut sum: Option<Vector4> = None;
24 let first = k.saturating_sub(n);
25 for (i, &ai) in a.iter().enumerate().take(k.min(m) + 1).skip(first) {
26 let j = k - i;
27 let factor =
28 Scalar::from_ratio(binomial(m, i) * binomial(n, j), binomial(m + n, k))?
29 .mul(ai);
30 let term = b[j].prod_scalar(factor);
31 sum = Some(match sum {
32 None => term,
33 Some(s) => s.add(&term),
34 });
35 }
36 Ok(sum.expect("at least one term"))
37 })
38 .collect()
39}
40
41fn elevate(a: &[Vector4], by: usize) -> GeopResult<Vec<Vector4>> {
44 let m = a.len() - 1;
45 (0..=m + by)
46 .map(|k| {
47 if k == 0 {
48 return Ok(a[0]);
49 }
50 if k == m + by {
51 return Ok(a[m]);
52 }
53 let mut sum: Option<Vector4> = None;
54 let first = k.saturating_sub(by);
55 for (i, ai) in a.iter().enumerate().take(k.min(m) + 1).skip(first) {
56 let factor =
57 Scalar::from_ratio(binomial(m, i) * binomial(by, k - i), binomial(m + by, k))?;
58 let term = ai.prod_scalar(factor);
59 sum = Some(match sum {
60 None => term,
61 Some(s) => s.add(&term),
62 });
63 }
64 Ok(sum.expect("at least one term"))
65 })
66 .collect()
67}
68
69impl NurbSurface3D {
70 pub fn spherical_triangle(
103 center: &Vector3,
104 radius: Scalar,
105 corners: [Vector3; 3],
106 ) -> GeopResult<Self> {
107 let origin = center.sharpen();
108 let center = ¢er.sub(&origin);
109 let corners = corners.map(|c| c.sub(&origin));
110 let r2 = radius.mul(radius);
111 let [p1, p2, p3] = &corners;
112 let (b1, b2, p) = (p1.sub(center), p2.sub(center), p3.sub(center));
113 let one_plus_cos = Scalar::ONE.add(b1.prod_dot(&b2).div(r2)?);
115 if !one_plus_cos.definitely_greater(Scalar::ZERO) {
116 return Err(GeopError::new(format!(
117 "NurbSurface::spherical_triangle: corners {p1:?} and {p2:?} could lie across the sphere from each other"
118 )));
119 }
120 let middle = center.add(&b1.add(&b2).prod_scalar(Scalar::ONE.div(one_plus_cos)?));
121 let w = one_plus_cos.div(Scalar::TWO)?.sqrt()?;
122 let base = [
123 p1.homogeneous(Scalar::ONE),
124 middle.homogeneous(w),
125 p2.homogeneous(Scalar::ONE),
126 ];
127 let mut gamma = Vec::with_capacity(3);
130 let mut inner = Vec::with_capacity(3);
131 for q in &base {
132 let x = Vector3::from_array([q[0], q[1], q[2]]);
133 let beta = q[3];
134 let delta = x.sub(¢er.prod_scalar(beta)).prod_dot(&p);
135 let g = beta.mul(r2).add(delta);
136 if !g.definitely_greater(Scalar::ZERO) {
137 return Err(GeopError::new(format!(
138 "NurbSurface::spherical_triangle: the base arc from {p1:?} to {p2:?} could reach across the sphere from {p3:?}"
139 )));
140 }
141 let m = center
142 .prod_scalar(delta)
143 .add(&x.add(&p.prod_scalar(beta)).prod_scalar(r2));
144 inner.push(Vector4::from_array([m[0], m[1], m[2], g]));
145 gamma.push(g);
146 }
147 let tau = |g: Scalar| Scalar::ONE.div(Scalar::TWO.mul(r2).mul(g).sqrt()?);
150 let tau = [tau(gamma[0])?, tau(gamma[2])?];
151 let tau_sq = [tau[0].mul(tau[0]), tau[0].mul(tau[1]), tau[1].mul(tau[1])];
152 let pole_weights = product(
153 &tau_sq,
154 &gamma
155 .iter()
156 .map(|&g| p3.homogeneous(Scalar::ONE).prod_scalar(g))
157 .collect::<Vec<_>>(),
158 )?;
159 let columns = [
160 elevate(&base, 2)?,
161 elevate(&product(&tau, &inner)?, 1)?,
162 pole_weights
163 .iter()
164 .map(|q| q.prod_scalar(Scalar::TWO.mul(r2)))
165 .collect(),
166 ];
167 let control_points = (0..5)
168 .flat_map(|i| columns.iter().map(move |c| c[i]))
169 .collect();
170 let knots = |degree: usize| {
171 let mut k = vec![Scalar::ZERO; degree + 1];
172 k.extend(vec![Scalar::ONE; degree + 1]);
173 k
174 };
175 Ok(NurbSurface3D::try_new(4, 2, control_points, knots(4), knots(2))?.translate(origin))
176 }
177}
178
179#[cfg(test)]
180mod tests {
181 use geop_core_math::{scalars::Scalar, vector::Vector3};
182
183 use super::super::NurbSurface3D;
184
185 fn v3(x: f64, y: f64, z: f64) -> Vector3 {
186 Vector3::from_array([
187 Scalar::from_f64(x),
188 Scalar::from_f64(y),
189 Scalar::from_f64(z),
190 ])
191 }
192
193 #[test]
198 fn far_from_the_origin_is_as_narrow() {
199 let radius = Scalar::from_f64(2.0);
200 for [x, y, z] in [[2.0, 2.0, 2.0], [1e5 + 2.0, -1e5 + 2.0, 2.0]] {
201 let center = v3(x, y, z);
202 let corners = [v3(x - 2.0, y, z), v3(x, y - 2.0, z), v3(x, y, z - 2.0)];
203 let s = NurbSurface3D::spherical_triangle(¢er, radius, corners).unwrap();
204 let widest = (s.control_points.iter())
205 .flat_map(|p| (0..4).map(move |c| p[c].width().to_f64()))
206 .fold(0.0, f64::max);
207 assert!(widest < 1e-9, "at {center:?}: {widest:e}");
208 let (u, v) = (Scalar::from_f64(0.4), Scalar::from_f64(0.3));
209 assert!(s.normal(u, v).is_ok(), "at {center:?}");
210 }
211 }
212
213 #[test]
217 fn lies_on_the_sphere() {
218 let center = v3(1.0, -2.0, 0.5);
219 let radius = Scalar::from_f64(0.7);
220 let on = |x: f64, y: f64, z: f64| {
221 let d = v3(x, y, z).normalize().unwrap();
222 center.add(&d.prod_scalar(radius))
223 };
224 for corners in [
225 [on(1.0, 0.0, 0.0), on(0.0, 1.0, 0.0), on(0.0, 0.0, 1.0)],
226 [on(1.0, 0.2, 0.1), on(-0.3, 1.0, 0.4), on(0.2, 0.5, 1.0)],
227 ] {
228 let s = NurbSurface3D::spherical_triangle(¢er, radius, corners).unwrap();
229 for i in 0..=6 {
230 for j in 0..=6 {
231 let (u, v) = (
232 Scalar::from_f64(i as f64 / 6.0),
233 Scalar::from_f64(j as f64 / 6.0),
234 );
235 let p = s.evaluate(u, v).unwrap();
236 let r = p.sub(¢er).norm();
237 assert!(r.could_be_equal(radius), "({i}, {j}): radius {r:?}");
238 }
239 }
240 let at = |u: f64, v: f64| {
241 s.evaluate(Scalar::from_f64(u), Scalar::from_f64(v))
242 .unwrap()
243 };
244 assert!(at(0.0, 0.0).could_be_equal(&corners[0]));
245 assert!(at(1.0, 0.0).could_be_equal(&corners[1]));
246 assert!(at(0.3, 1.0).could_be_equal(&corners[2]));
247 for (a, b, side) in [
250 (0, 1, (None, Some(0.0))),
251 (0, 2, (Some(0.0), None)),
252 (1, 2, (Some(1.0), None)),
253 ] {
254 let n = corners[a].sub(¢er).prod_cross(&corners[b].sub(¢er));
255 for k in 0..=4 {
256 let t = k as f64 / 4.0;
257 let p = match side {
258 (Some(u), None) => at(u, t),
259 (None, Some(v)) => at(t, v),
260 _ => unreachable!(),
261 };
262 assert!(p.sub(¢er).prod_dot(&n).could_be_equal(Scalar::ZERO));
263 }
264 }
265 let (u, v) = (Scalar::from_f64(0.4), Scalar::from_f64(0.3));
267 let p = s.evaluate(u, v).unwrap();
268 assert!(
269 s.normal(u, v)
270 .unwrap()
271 .prod_dot(&p.sub(¢er))
272 .definitely_greater(Scalar::ZERO)
273 );
274 }
275 }
276}