1use crate::{
14 aabb::aabb_could_contain,
15 fat_line::{
16 Stalled, carried_width, clip_tensor, extent, greville_abscissae, restriction, stalled,
17 },
18 knot_insertion::pinned_clamped_end,
19 nurb_curve::NurbCurve,
20 nurb_surface::NurbSurface,
21};
22use geop_core_math::{
23 geop_error::{GeopError, GeopResult},
24 scalars::Scalar,
25 solvers::subdivide::{Order, Searched, Split, subdivide},
26 vector::Vector3,
27};
28
29use super::curve::curve_could_contain;
30
31pub(crate) fn surface_extents(patch: &NurbSurface<4>) -> [f64; 2] {
35 let (nu, nv) = (patch.num_u, patch.num_v);
36 let cp = &patch.control_points;
37 [
38 extent((0..nv).map(|j| (0..nu).map(move |i| cp[i * nv + j]))),
39 extent(cp.chunks(nv).map(|row| row.iter().copied())),
40 ]
41}
42
43fn clip(patch: &NurbSurface<4>, point: &Vector3) -> GeopResult<Option<(Scalar, Scalar)>> {
49 let (u0, u1) = patch.domain_u();
50 let (v0, v1) = patch.domain_v();
51 let mut hats = [u0.union(u1), v0.union(v1)];
52 let (nu, nv) = (patch.num_u, patch.num_v);
53 let greville = [
54 greville_abscissae(&patch.knot_vector_u, patch.degree_u, nu)?,
55 greville_abscissae(&patch.knot_vector_v, patch.degree_v, nv)?,
56 ];
57 let mut d = Vec::with_capacity(nu * nv);
58 for k in 0..3 {
59 d.clear();
62 d.extend(
63 patch
64 .control_points
65 .iter()
66 .map(|q| q[k].sub(point[k].mul(q[3]))),
67 );
68 if !clip_tensor(&d, &[nu, nv], &greville, &mut hats) {
69 return Ok(None);
70 }
71 }
72 Ok(Some((hats[0], hats[1])))
73}
74
75pub fn boundary_curve(patch: &NurbSurface<4>, u_fixed: bool, first: bool) -> Option<NurbCurve<4>> {
80 let (lo, hi) = if u_fixed {
81 patch.domain_u()
82 } else {
83 patch.domain_v()
84 };
85 patch.iso_curve(u_fixed, if first { lo } else { hi }).ok()
86}
87
88pub(crate) fn collapsed_boundary(
93 patch: &NurbSurface<4>,
94 u_hat: Scalar,
95 v_hat: Scalar,
96) -> Option<(NurbCurve<4>, bool)> {
97 let (nu, nv) = (patch.num_u, patch.num_v);
98 if let Some(first) = pinned_clamped_end(u_hat, &patch.knot_vector_u, nu, patch.degree_u) {
99 return boundary_curve(patch, true, first).map(|c| (c, true));
100 }
101 if let Some(first) = pinned_clamped_end(v_hat, &patch.knot_vector_v, nv, patch.degree_v) {
102 return boundary_curve(patch, false, first).map(|c| (c, false));
103 }
104 None
105}
106
107pub fn surface_could_contain(
134 surface: &NurbSurface<4>,
135 point: &Vector3,
136 max_nodes: usize,
137 min_subdivision_size: Scalar,
138) -> GeopResult<Option<(Scalar, Scalar)>> {
139 let mut solution: Option<(Scalar, Scalar)> = None;
140 let mut report = |(u, v): (Scalar, Scalar)| {
141 solution = Some(match solution {
142 Some((su, sv)) => (su.union(u), sv.union(v)),
143 None => (u, v),
144 });
145 };
146
147 let searched = subdivide(
148 [surface.clone()],
149 Order::BreadthFirst,
150 max_nodes,
151 |patch, budget| -> GeopResult<_> {
152 if !aabb_could_contain(&patch.aabb, point) {
153 return Ok(Split::Done);
154 }
155 let Some((u_hat, v_hat)) = clip(&patch, point)? else {
156 return Ok(Split::Done);
157 };
158
159 if let Some((boundary, along_v)) = collapsed_boundary(&patch, u_hat, v_hat) {
160 if let Some(t) =
161 curve_could_contain(&boundary, point, budget, min_subdivision_size)?
162 {
163 let (fixed, free) = if along_v {
164 (u_hat, v_hat)
165 } else {
166 (v_hat, u_hat)
167 };
168 if t.could_be_equal(free) {
169 let free = free.intersect(t);
170 report(if along_v {
171 (fixed, free)
172 } else {
173 (free, fixed)
174 });
175 }
176 }
177 return Ok(Split::Done);
178 }
179
180 let ranges = [patch.domain_u(), patch.domain_v()];
181 let sizes = surface_extents(&patch);
182 let point_width = (0..3)
184 .map(|k| point[k].width().to_f64())
185 .fold(0.0, f64::max);
186 let carried = carried_width(&patch.control_points).max(point_width);
187 if let Some(b) = restriction(&[u_hat, v_hat], &ranges, &sizes, carried)?
188 && let Ok(restricted) = patch.sub_surface(b[0], b[1])
189 {
190 return Ok(Split::Restrict(restricted));
191 }
192
193 let order = match stalled(&ranges, &sizes, carried, min_subdivision_size) {
194 Stalled::Converged => {
195 report((u_hat, v_hat));
196 return Ok(Split::Done);
197 }
198 Stalled::Bisect(order) => order,
199 };
200 let halves = order.iter().find_map(|&dir| {
201 if dir == 0 {
202 patch.split_u_mid()
203 } else {
204 patch.split_v_mid()
205 }
206 .ok()
207 });
208 if let Some((left, right)) = halves {
209 return Ok(Split::Halves(left, right));
210 }
211 report((u_hat, v_hat));
213 Ok(Split::Done)
214 },
215 )?;
216 if let Searched::Exhausted { pending } = searched {
217 return Err(GeopError::new(format!(
218 "surface_could_contain (clipping): exhausted max_nodes={max_nodes} with {pending} \
219 patches pending; the result would be incomplete"
220 )));
221 }
222
223 Ok(solution)
224}
225
226#[cfg(test)]
227mod tests {
228 use super::surface_could_contain;
229 use crate::nurb_surface::NurbSurface;
230 use crate::test_fixtures::{knots, sphere_octant, weighted};
231 use geop_core_math::{scalars::Scalar, vector::Vector3};
232
233 const MAX: usize = 2000;
234 const MIN_SUBDIVISION_SIZE: f64 = 1e-4;
235
236 fn v3(x: f64, y: f64, z: f64) -> Vector3 {
237 Vector3::from_array([
238 Scalar::from_f64(x),
239 Scalar::from_f64(y),
240 Scalar::from_f64(z),
241 ])
242 }
243
244 fn lifted() -> NurbSurface<4> {
246 NurbSurface::try_new(
247 1,
248 1,
249 vec![
250 weighted(0., 0., 0., 1.),
251 weighted(0., 1., 0., 1.),
252 weighted(1., 0., 0., 1.),
253 weighted(1., 1., 1., 1.),
254 ],
255 knots(&[0., 0., 1., 1.]),
256 knots(&[0., 0., 1., 1.]),
257 )
258 .unwrap()
259 }
260
261 fn quarter_cylinder() -> NurbSurface<4> {
263 let w = std::f64::consts::FRAC_1_SQRT_2;
264 let circle = [(1., 0., 1.), (1., 1., w), (0., 1., 1.)];
265 let cps = circle
266 .iter()
267 .flat_map(|&(x, y, wi)| [weighted(x, y, 0., wi), weighted(x, y, 1., wi)])
268 .collect();
269 NurbSurface::try_new(
270 2,
271 1,
272 cps,
273 knots(&[0., 0., 0., 1., 1., 1.]),
274 knots(&[0., 0., 1., 1.]),
275 )
276 .unwrap()
277 }
278
279 fn wavy() -> NurbSurface<4> {
281 let cps = (0..5)
282 .flat_map(|i| {
283 (0..5).map(move |j| {
284 let z = ((i * 3 + j * 5) % 7) as f64 / 7.0 - 0.5;
285 weighted(i as f64, j as f64, z, 1.0)
286 })
287 })
288 .collect();
289 let k = knots(&[0., 0., 0., 0., 0.4, 1., 1., 1., 1.]);
290 NurbSurface::try_new(3, 3, cps, k.clone(), k).unwrap()
291 }
292
293 fn assert_contains_at(s: &NurbSurface<4>, u: f64, v: f64) {
295 let (u, v) = (Scalar::from_f64(u), Scalar::from_f64(v));
296 let p = s.evaluate(u, v).unwrap();
297 let (ru, rv) = surface_could_contain(s, &p, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
298 .unwrap()
299 .unwrap_or_else(|| panic!("point at ({u:?}, {v:?}) not found"));
300 assert!(
301 ru.could_be_equal(u) && rv.could_be_equal(v),
302 "({ru:?}, {rv:?}) misses ({u:?}, {v:?})"
303 );
304 }
305
306 #[test]
307 fn contains_grid() {
308 for s in [lifted(), quarter_cylinder(), sphere_octant(), wavy()] {
309 for u in [0., 0.3, 0.4, 0.75, 1.] {
310 for v in [0., 0.2, 0.4, 0.5, 1.] {
311 assert_contains_at(&s, u, v);
312 }
313 }
314 }
315 }
316
317 #[test]
319 fn pole_keeps_every_u() {
320 let (u, v) = surface_could_contain(
321 &sphere_octant(),
322 &v3(0., 0., 1.),
323 MAX,
324 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
325 )
326 .unwrap()
327 .unwrap();
328 assert!(
329 u.could_be_equal(Scalar::ZERO) && u.could_be_equal(Scalar::ONE),
330 "{u:?}"
331 );
332 assert!(v.could_be_equal(Scalar::ONE), "{v:?}");
333 }
334
335 #[test]
336 fn misses_off_surface_points() {
337 let miss = |s: &NurbSurface<4>, p: Vector3| {
338 surface_could_contain(s, &p, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
339 .unwrap()
340 .is_none()
341 };
342 assert!(miss(&lifted(), v3(0.5, 0.5, 5.)));
343 assert!(miss(&lifted(), v3(0.5, 0.5, 0.3)));
344 assert!(miss(&quarter_cylinder(), v3(0.8, 0.5, 0.5)));
346 assert!(miss(&sphere_octant(), v3(0.5, 0.5, 0.5)));
347 assert!(miss(&wavy(), v3(2., 2., 3.)));
348 }
349
350 #[test]
354 fn misses_points_closer_than_min_subdivision_size() {
355 let r = (1. + 1e-10) / 3f64.sqrt();
356 let found = surface_could_contain(
357 &sphere_octant(),
358 &v3(r, r, r),
359 MAX,
360 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
361 )
362 .unwrap();
363 assert!(found.is_none(), "{found:?}");
364 }
365
366 #[test]
369 fn zero_budget_is_an_error() {
370 let s = lifted();
371 assert!(
372 surface_could_contain(
373 &s,
374 &v3(0.5, 0.5, 5.),
375 0,
376 Scalar::from_f64(MIN_SUBDIVISION_SIZE)
377 )
378 .is_err()
379 );
380 }
381
382 #[test]
384 fn result_is_tight() {
385 let s = wavy();
386 let p = s
387 .evaluate(Scalar::from_f64(0.3), Scalar::from_f64(0.7))
388 .unwrap();
389 let (u, v) = surface_could_contain(&s, &p, MAX, Scalar::from_f64(MIN_SUBDIVISION_SIZE))
390 .unwrap()
391 .unwrap();
392 assert!(
393 u.width()
394 .definitely_less(Scalar::from_f64(MIN_SUBDIVISION_SIZE)),
395 "{u:?}"
396 );
397 assert!(
398 v.width()
399 .definitely_less(Scalar::from_f64(MIN_SUBDIVISION_SIZE)),
400 "{v:?}"
401 );
402 }
403}