1use std::convert::Infallible;
41
42use geop_core_geometry::{
43 intersection::curve_curve_overlaps_and_crossings,
44 nurb_curve::{NurbCurve2D, NurbCurve3D},
45 nurb_surface::NurbSurface3D,
46};
47use geop_core_math::{
48 geop_error::{GeopError, GeopResult, WithContext},
49 plain,
50 scalars::Scalar,
51 solvers::{
52 bisect::sign_changes,
53 iterate::{End, Repeat, Step, iterate},
54 newton::{Arrive, newton_1d},
55 repeat::{self, repeat},
56 },
57 vector::{Vector2, Vector3},
58};
59use geop_core_topology::{
60 CoedgeId, FaceId, Model,
61 contains::face::{PointClassification, face_contains},
62};
63
64use crate::hidden_lines::{COARSE_SUBDIVISION, cut_points};
65use geop_core_geometry::search::{MAX_NODES, min_subdivision_size};
66
67const GRID_LINES: usize = 12;
69const GRID_SAMPLES: usize = 32;
71const GRID_OFFSET: f64 = 0.381_966_011_250_105;
74const BOX_DEPTH: usize = 1;
78const CORRECTOR_ITERATIONS: usize = 24;
80const BISECTIONS: usize = 60;
82const MAX_TRACE_STEPS: usize = 4000;
84const STEP_HALVINGS: usize = 6;
86const STEPS_PER_FACE: f64 = 24.0;
88const FACE_CONTAINS_SEED: u64 = 0x5114_u64;
90
91#[derive(Clone, Debug)]
93pub struct Silhouette {
94 pub face: FaceId,
95 pub curve: NurbCurve3D,
96 samples: Vec<Vector2>,
99}
100
101fn contour(
103 surface: &NurbSurface3D,
104 d: &Vector3,
105 u: Scalar,
106 v: Scalar,
107) -> GeopResult<(Scalar, Scalar, Scalar)> {
108 let (su, sv) = surface.derivatives(u, v)?;
109 let [suu, suv, svv] = surface.second_derivatives(u, v)?;
110 let g = su.prod_cross(&sv).prod_dot(d);
111 let gu = suu.prod_cross(&sv).add(&su.prod_cross(&suv)).prod_dot(d);
112 let gv = suv.prod_cross(&sv).add(&su.prod_cross(&svv)).prod_dot(d);
113 Ok((g, gu, gv))
114}
115
116fn sign(surface: &NurbSurface3D, d: &Vector3, u: Scalar, v: Scalar) -> Option<bool> {
119 surface.normal(u, v).ok()?.prod_dot(d).definite_sign()
120}
121
122fn correct(
135 surface: &NurbSurface3D,
136 d: &Vector3,
137 start: (Scalar, Scalar),
138) -> GeopResult<(Scalar, Scalar)> {
139 let (u_lo, u_hi) = surface.domain_u();
140 let (v_lo, v_hi) = surface.domain_v();
141 let seed = (start.0.clamped(u_lo, u_hi), start.1.clamped(v_lo, v_hi));
142 let on_domain =
143 |(u, v): (Scalar, Scalar)| (u.intersect(u_lo.union(u_hi)), v.intersect(v_lo.union(v_hi)));
144 let mut last = None;
145 let corrected = iterate(
146 seed,
147 seed,
148 CORRECTOR_ITERATIONS,
149 Repeat::Never,
150 |&(u, v)| {
151 let (u, v) = (u.clamped(u_lo, u_hi), v.clamped(v_lo, v_hi));
152 ((u.sharpen(), v.sharpen()), (u, v))
153 },
154 |&(u, v)| -> GeopResult<Step<(Scalar, Scalar)>> {
155 let (g, gu, gv) = contour(surface, d, u, v)?;
156 if g.could_be_equal(Scalar::ZERO) {
157 return Ok(Step::Done);
158 }
159 let scale = g.div(gu.mul(gu).add(gv.mul(gv)))?;
160 last = Some((u, v, g, gu, gv));
161 Ok(Step::Next((u.sub(gu.mul(scale)), v.sub(gv.mul(scale)))))
162 },
163 )?;
164 if corrected.end == End::Done {
165 let (u, v) = corrected.seed;
172 let (g, gu, gv) = contour(surface, d, u, v)?;
173 let Ok(scale) = g.div(gu.mul(gu).add(gv.mul(gv))) else {
174 return Ok((u, v));
175 };
176 return Ok(on_domain((u.sub(gu.mul(scale)), v.sub(gv.mul(scale)))));
177 }
178 let (u, v) = on_domain(corrected.answer);
183 if contour(surface, d, u, v)?.0.could_be_equal(Scalar::ZERO) {
184 return Ok((u, v));
185 }
186 Err(GeopError::new(format!(
187 "the point does not converge onto the silhouette: from {start:?}, the last \
188 (u, v, g, g_u, g_v) {last:?}, in the domain {:?} x {:?}",
189 surface.domain_u(),
190 surface.domain_v()
191 )))
192}
193
194fn correct_on_boundary(
197 surface: &NurbSurface3D,
198 d: &Vector3,
199 (u, v): (Scalar, Scalar),
200 u_fixed: bool,
201) -> Option<(Scalar, Scalar)> {
202 let (moving, (lo, hi)) = if u_fixed {
204 (v, surface.domain_v())
205 } else {
206 (u, surface.domain_u())
207 };
208 let at = |t: Scalar| if u_fixed { (u, t) } else { (t, v) };
209 let corrected = newton_1d(
210 moving,
211 (lo, hi),
212 CORRECTOR_ITERATIONS,
213 Arrive::AtZero,
214 |t| -> GeopResult<_> {
215 let (u, v) = at(t);
216 let (g, gu, gv) = contour(surface, d, u, v)?;
217 Ok((g, if u_fixed { gv } else { gu }))
218 },
219 );
220 let (t, arrived) = corrected.ok()?;
221 arrived.then(|| at(t))
222}
223
224struct GridLine {
227 u_fixed: bool,
229 value: Scalar,
230 params: Vec<Scalar>,
232 brackets: Vec<(usize, usize)>,
234 traced: Vec<bool>,
236 scale: f64,
240}
241
242impl GridLine {
243 fn point(&self, t: Scalar) -> (Scalar, Scalar) {
244 if self.u_fixed {
245 (self.value, t)
246 } else {
247 (t, self.value)
248 }
249 }
250
251 fn mark(&mut self, t: f64) {
256 let p = |k: usize| self.params[k].to_f64();
257 let line = (p(self.params.len() - 1) - p(0)) / 32.0;
258 let near = self
259 .brackets
260 .iter()
261 .enumerate()
262 .filter(|(_, (lo, hi))| {
263 let reach = p(*hi) - p(*lo) + line;
264 p(*lo) - reach <= t && t <= p(*hi) + reach
265 })
266 .min_by(|(_, a), (_, b)| {
267 let distance = |&(lo, hi): &(usize, usize)| (0.5 * (p(lo) + p(hi)) - t).abs();
268 distance(a).total_cmp(&distance(b))
269 })
270 .map(|(i, _)| i);
271 if let Some(i) = near {
272 self.traced[i] = true;
273 }
274 }
275}
276
277fn breaks(knots: &[Scalar], (lo, hi): (Scalar, Scalar)) -> Vec<Scalar> {
280 let mut out = vec![lo];
281 for &k in knots {
282 if k.definitely_greater(*out.last().expect("a first break")) && k.definitely_less(hi) {
283 out.push(k);
284 }
285 }
286 out.push(hi);
287 out
288}
289
290fn middle(a: Scalar, b: Scalar) -> Scalar {
292 Scalar::fraction(a, b, Scalar::from_f64(0.5)).sharpen()
293}
294
295fn grid(surface: &NurbSurface3D, d: &Vector3) -> Vec<GridLine> {
299 let mut fractions = vec![0.0];
300 fractions.extend((0..GRID_LINES).map(|k| (k as f64 + GRID_OFFSET) / GRID_LINES as f64));
301 fractions.push(1.0);
302 let at = |(lo, hi): (Scalar, Scalar), f: f64| match f {
303 0.0 => lo,
304 1.0 => hi,
305 _ => Scalar::fraction(lo, hi, Scalar::from_f64(f)),
306 };
307 let (du, dv) = (surface.domain_u(), surface.domain_v());
308 let us: Vec<Scalar> = fractions.iter().map(|&f| at(du, f)).collect();
309 let vs: Vec<Scalar> = fractions.iter().map(|&f| at(dv, f)).collect();
310 let mut lines = Vec::new();
311 for &u in &us {
312 lines.push(sampled_line(surface, d, true, u, dv));
313 }
314 for &v in &vs {
315 lines.push(sampled_line(surface, d, false, v, du));
316 }
317 for u in us.windows(2) {
318 for v in vs.windows(2) {
319 for (at, size) in loop_seats(surface, d, (u[0], u[1]), (v[0], v[1])) {
320 let mut line = sampled_line(surface, d, true, at, (v[0], v[1]));
321 line.scale = line.scale.min(size);
322 lines.push(line);
323 }
324 }
325 }
326 lines
327}
328
329fn loop_seats(
337 surface: &NurbSurface3D,
338 d: &Vector3,
339 cu: (Scalar, Scalar),
340 cv: (Scalar, Scalar),
341) -> Vec<(Scalar, f64)> {
342 let zero = Scalar::ZERO;
343 let us = breaks(&surface.knot_vector_u, cu);
344 let vs = breaks(&surface.knot_vector_v, cv);
345 let mut stack = Vec::new();
346 let mut vanishes = false;
347 for u in us.windows(2) {
348 for v in vs.windows(2) {
349 match contour(surface, d, u[0].union(u[1]), v[0].union(v[1])) {
350 Ok((g, _, _)) if !g.could_be_equal(zero) => {}
351 _ => vanishes = true,
352 }
353 stack.push(((u[0], u[1]), (v[0], v[1]), 0));
354 }
355 }
356 let mut seats: Vec<(Scalar, f64)> = Vec::new();
357 if !vanishes {
358 return seats;
359 }
360 while let Some(((u0, u1), (v0, v1), depth)) = stack.pop() {
361 let critical = match contour(surface, d, u0.union(u1), v0.union(v1)) {
362 Ok((_, gu, gv)) => gu.could_be_equal(zero) && gv.could_be_equal(zero),
363 Err(_) => true,
364 };
365 if !critical {
366 continue;
367 }
368 let (um, vm) = (middle(u0, u1), middle(v0, v1));
369 if depth >= BOX_DEPTH {
370 if !seats.iter().any(|s| s.0.could_be_equal(um)) {
371 let corner =
372 |u: Scalar, v: Scalar| surface.evaluate(u, v).map(|p| p.to_f64_array());
373 let size = match (corner(u0, v0), corner(u1, v1)) {
374 (Ok(a), Ok(b)) => plain::distance(a, b),
375 _ => f64::INFINITY,
376 };
377 seats.push((um, size));
378 }
379 continue;
380 }
381 for u in [(u0, um), (um, u1)] {
382 for v in [(v0, vm), (vm, v1)] {
383 stack.push((u, v, depth + 1));
384 }
385 }
386 }
387 seats
388}
389
390fn sampled_line(
393 surface: &NurbSurface3D,
394 d: &Vector3,
395 u_fixed: bool,
396 value: Scalar,
397 (lo, hi): (Scalar, Scalar),
398) -> GridLine {
399 let params: Vec<Scalar> = (0..=GRID_SAMPLES)
400 .map(|k| match k {
401 0 => lo,
402 k if k == GRID_SAMPLES => hi,
403 k => Scalar::fraction(lo, hi, Scalar::from_f64(k as f64 / GRID_SAMPLES as f64)),
404 })
405 .collect();
406 let mut line = GridLine {
407 u_fixed,
408 value,
409 params,
410 brackets: Vec::new(),
411 traced: Vec::new(),
412 scale: f64::INFINITY,
413 };
414 let at = |t: Scalar| {
415 let (u, v) = line.point(t);
416 surface.evaluate(u, v).map(|p| p.to_f64_array())
417 };
418 if let (Ok(a), Ok(b)) = (at(lo), at(hi)) {
419 line.scale = plain::distance(a, b);
420 }
421 let brackets: Vec<(usize, usize)> = sign_changes(0..line.params.len(), |k| {
422 let (u, v) = line.point(line.params[k]);
423 Ok::<_, Infallible>(sign(surface, d, u, v))
424 })
425 .flatten()
426 .map(|(j, k, _)| (j, k))
427 .collect();
428 line.traced = vec![false; brackets.len()];
429 line.brackets = brackets;
430 line
431}
432
433fn seed(
436 surface: &NurbSurface3D,
437 d: &Vector3,
438 line: &GridLine,
439 (lo, hi): (usize, usize),
440) -> Option<(Scalar, Scalar)> {
441 let sign_at = |t| -> GeopResult<_> {
442 let (u, v) = line.point(t);
443 Ok(sign(surface, d, u, v))
444 };
445 let bracket = sign_changes([line.params[lo], line.params[hi]], sign_at)
446 .bisected(BISECTIONS, false)
447 .next()?
448 .ok()?;
449 let (a, b) = match bracket.undecided {
451 Some(mid) => (mid, mid),
452 None => (bracket.lo, bracket.hi),
453 };
454 let t = a.add(b).div(Scalar::TWO).ok()?.sharpen();
455 correct(surface, d, line.point(t)).ok()
456}
457
458fn mark_crossings(
475 surface: &NurbSurface3D,
476 d: &Vector3,
477 lines: &mut [GridLine],
478 a: (Scalar, Scalar),
479 b: (Scalar, Scalar),
480) {
481 let (au, av, bu, bv) = (a.0.to_f64(), a.1.to_f64(), b.0.to_f64(), b.1.to_f64());
482 let chord = [bu - au, bv - av];
483 let length = chord[0].hypot(chord[1]);
484 let tangent = |(u, v): (Scalar, Scalar)| -> [f64; 2] {
487 let Ok((_, gu, gv)) = contour(surface, d, u, v) else {
488 return chord;
489 };
490 let t = [-gv.to_f64(), gu.to_f64()];
491 let norm = t[0].hypot(t[1]);
492 let along = t[0] * chord[0] + t[1] * chord[1];
493 if norm == 0.0 || along == 0.0 || !norm.is_finite() {
494 return chord;
495 }
496 let scale = length / norm * along.signum();
497 [t[0] * scale, t[1] * scale]
498 };
499 let (ta, tb) = (tangent(a), tangent(b));
500 let cubic = |k: usize| -> [f64; 4] {
503 let (x0, x1) = if k == 0 { (au, bu) } else { (av, bv) };
504 [
505 x0,
506 ta[k],
507 3.0 * (x1 - x0) - 2.0 * ta[k] - tb[k],
508 2.0 * (x0 - x1) + ta[k] + tb[k],
509 ]
510 };
511 let at = |c: &[f64; 4], s: f64| ((c[3] * s + c[2]) * s + c[1]) * s + c[0];
512 for line in lines.iter_mut() {
513 let (k, other) = if line.u_fixed { (0, 1) } else { (1, 0) };
514 for (end, p) in [(a, (au, av)), (b, (bu, bv))] {
515 let (x, y) = if line.u_fixed {
516 (end.0, p.1)
517 } else {
518 (end.1, p.0)
519 };
520 if x.could_be_equal(line.value) {
521 line.mark(y);
522 }
523 }
524 let mut here = cubic(k);
525 here[0] -= line.value.to_f64();
526 let across = cubic(other);
527 let (qa, qb, qc) = (3.0 * here[3], 2.0 * here[2], here[1]);
531 let mut ends = vec![0.0, 1.0];
532 if qa == 0.0 {
533 if qb != 0.0 {
534 ends.push(-qc / qb);
535 }
536 } else {
537 let disc = qb * qb - 4.0 * qa * qc;
538 if disc >= 0.0 {
539 let root = disc.sqrt();
540 ends.extend([(-qb - root) / (2.0 * qa), (-qb + root) / (2.0 * qa)]);
541 }
542 }
543 ends.retain(|s| (0.0..=1.0).contains(s));
544 ends.sort_by(f64::total_cmp);
545 let side = |s| {
547 let x = at(&here, s);
548 Ok::<_, GeopError>((x != 0.0 && !x.is_nan()).then_some(x > 0.0))
549 };
550 for bracket in sign_changes(ends, side)
551 .bisected(BISECTIONS, false)
552 .flatten()
553 {
554 let s = bracket.undecided.unwrap_or(0.5 * (bracket.lo + bracket.hi));
555 line.mark(at(&across, s));
556 }
557 }
558}
559
560enum Ending {
562 Closed,
564 Open,
566}
567
568#[allow(clippy::too_many_arguments)]
571fn march(
572 surface: &NurbSurface3D,
573 d: &Vector3,
574 start: (Scalar, Scalar),
575 forward: bool,
576 step: Scalar,
577 lines: &mut [GridLine],
578 points: &mut Vec<(Scalar, Scalar)>,
579) -> GeopResult<Ending> {
580 let (u_lo, u_hi) = surface.domain_u();
581 let (v_lo, v_hi) = surface.domain_v();
582 let start_point = surface.evaluate(start.0, start.1)?;
583 let mut current = start;
584 let mut travelled = Scalar::ZERO;
585 repeat(MAX_TRACE_STEPS, || -> GeopResult<_> {
586 let (u, v) = current;
587 let Ok((_, gu, gv)) = contour(surface, d, u, v) else {
588 return Ok(repeat::Step::Done(Ending::Open));
589 };
590 let (mut tu, mut tv) = (gv.neg(), gu);
591 if !forward {
592 (tu, tv) = (tu.neg(), tv.neg());
593 }
594 let (su, sv) = surface.derivatives(u, v)?;
595 let speed = su.prod_scalar(tu).add(&sv.prod_scalar(tv)).norm();
596 let from = surface.evaluate(u, v)?;
600 let mut tried = step;
601 let mut halvings = 0;
602 let next = loop {
603 let Ok(scale) = tried.div(speed) else {
604 return Ok(repeat::Step::Done(Ending::Open));
605 };
606 let (du, dv) = (tu.mul(scale), tv.mul(scale));
607 let (pu, pv) = (u.add(du).sharpen(), v.add(dv).sharpen());
608 let leaves_u = pu.definitely_less(u_lo) || pu.definitely_greater(u_hi);
609 let leaves_v = pv.definitely_less(v_lo) || pv.definitely_greater(v_hi);
610 if leaves_u || leaves_v {
611 let to = |x: Scalar, dx: Scalar, lo: Scalar, hi: Scalar| -> (f64, Scalar) {
615 let bound = if dx.to_f64() < 0.0 { lo } else { hi };
616 ((bound.sub(x).to_f64() / dx.to_f64()).clamp(0.0, 1.0), bound)
617 };
618 let (fu, bound_u) = to(u, du, u_lo, u_hi);
619 let (fv, bound_v) = to(v, dv, v_lo, v_hi);
620 let u_first = leaves_u && (!leaves_v || fu <= fv);
621 let landed = if u_first {
627 let f = Scalar::from_f64(fu);
628 (bound_u, v.add(dv.mul(f)).sharpen().clamped(v_lo, v_hi))
629 } else {
630 let f = Scalar::from_f64(fv);
631 (u.add(du.mul(f)).sharpen().clamped(u_lo, u_hi), bound_v)
632 };
633 if let Some(end) = correct_on_boundary(surface, d, landed, u_first) {
634 mark_crossings(surface, d, lines, current, end);
635 points.push(end);
636 }
637 return Ok(repeat::Step::Done(Ending::Open));
638 }
639 let landed = correct(surface, d, (pu, pv)).ok().filter(|next| {
640 surface
641 .evaluate(next.0, next.1)
642 .is_ok_and(|p| p.sub(&from).norm().definitely_less(tried.mul(Scalar::TWO)))
643 });
644 match landed {
645 Some(next) => break next,
646 None if halvings < STEP_HALVINGS => {
647 tried = tried.div(Scalar::TWO)?;
648 halvings += 1;
649 }
650 None => return Ok(repeat::Step::Done(Ending::Open)),
651 }
652 };
653 mark_crossings(surface, d, lines, current, next);
654 points.push(next);
655 travelled = travelled.add(tried);
656 let here = surface.evaluate(next.0, next.1)?;
657 if travelled.definitely_greater(step.mul(Scalar::TWO))
658 && here.sub(&start_point).norm().definitely_less(step)
659 {
660 mark_crossings(surface, d, lines, next, start);
661 points.push(start);
662 return Ok(repeat::Step::Done(Ending::Closed));
663 }
664 current = next;
665 Ok(repeat::Step::Again)
666 })?
667 .done_or(|steps| {
668 GeopError::new(format!(
669 "the silhouette trace from {start:?} did not end within {steps} steps of {step:?}"
670 ))
671 })
672}
673
674fn trace(
676 surface: &NurbSurface3D,
677 d: &Vector3,
678 seed: (Scalar, Scalar),
679 step: Scalar,
680 lines: &mut [GridLine],
681) -> GeopResult<Vec<(Scalar, Scalar)>> {
682 let mut forward = Vec::new();
683 let points = if let Ending::Closed = march(surface, d, seed, true, step, lines, &mut forward)? {
684 let mut points = vec![seed];
685 points.extend(forward);
686 points
687 } else {
688 let mut backward = Vec::new();
689 march(surface, d, seed, false, step, lines, &mut backward)?;
690 backward.reverse();
691 backward.push(seed);
692 backward.extend(forward);
693 backward
694 };
695 thin(surface, points, step)
696}
697
698fn thin(
703 surface: &NurbSurface3D,
704 points: Vec<(Scalar, Scalar)>,
705 step: Scalar,
706) -> GeopResult<Vec<(Scalar, Scalar)>> {
707 let close = step.div(Scalar::from_f64(8.0))?;
708 let at = |p: &(Scalar, Scalar)| surface.evaluate(p.0, p.1);
709 let count = points.len();
710 let mut kept: Vec<(Scalar, Scalar)> = Vec::with_capacity(count);
711 for (k, p) in points.into_iter().enumerate() {
712 if let Some(last) = kept.last()
713 && at(last)?.sub(&at(&p)?).norm().definitely_less(close)
714 {
715 if k + 1 == count && kept.len() > 1 {
716 kept.pop();
717 } else {
718 continue;
719 }
720 }
721 kept.push(p);
722 }
723 Ok(kept)
724}
725
726fn extent(surface: &NurbSurface3D) -> Scalar {
728 let b = surface.aabb();
729 Scalar::from_f64(plain::diagonal(
730 b.map(|x| x.lower().to_f64()),
731 b.map(|x| x.upper().to_f64()),
732 ))
733}
734
735fn swept_along(surface: &NurbSurface3D, d: &Vector3) -> bool {
750 let (nu, nv) = (surface.num_u, surface.num_v);
751 let point = |i: usize, j: usize| surface.control_points[i * nv + j];
752 let cartesian = |q: geop_core_math::vector::Vector4| -> Option<Vector3> {
753 let w = q[3];
754 Some(Vector3::from_array([
755 q[0].div(w).ok()?,
756 q[1].div(w).ok()?,
757 q[2].div(w).ok()?,
758 ]))
759 };
760 let along = |a: geop_core_math::vector::Vector4, b: geop_core_math::vector::Vector4| {
761 let (Some(pa), Some(pb)) = (cartesian(a), cartesian(b)) else {
762 return false;
763 };
764 a[3].could_be_equal(b[3])
765 && !(pb.sub(&pa).prod_cross(d).norm())
766 .definitely_greater(Scalar::from_f64(COARSE_SUBDIVISION))
767 };
768 let along_v = (0..nu).all(|i| (1..nv).all(|j| along(point(i, 0), point(i, j))));
769 let along_u = (0..nv).all(|j| (1..nu).all(|i| along(point(0, j), point(i, j))));
770 along_u || along_v
771}
772
773impl Silhouette {
774 pub fn exact_point(&self, model: &Model, d: &Vector3, near: &Vector3) -> GeopResult<Vector3> {
778 let surface = &model.get_face(self.face)?.surface;
779 let ctx = |e: GeopError| {
780 e.with_context(format!(
781 "Silhouette::exact_point(face={}, near={near:?})",
782 self.face
783 ))
784 };
785 let distance = |uv: &Vector2| -> f64 {
786 surface
787 .evaluate(uv[0], uv[1])
788 .map(|p| p.sub(near).norm_sq().to_f64())
789 .unwrap_or(f64::INFINITY)
790 };
791 let seed = self
792 .samples
793 .iter()
794 .min_by(|a, b| distance(a).total_cmp(&distance(b)))
795 .ok_or_else(|| GeopError::new("a silhouette without samples"))
796 .with_context(&ctx)?;
797 let (u, v) = surface
798 .project(*near, seed[0], seed[1], CORRECTOR_ITERATIONS)
799 .with_context(&ctx)?;
800 let (u, v) = correct(surface, d, (u.sharpen(), v.sharpen())).with_context(&ctx)?;
801 surface.evaluate(u, v).with_context(&ctx)
802 }
803}
804
805const EDGE_SAMPLES: usize = 32;
807
808pub fn edge_crossings(
818 model: &Model,
819 face_id: FaceId,
820 coedge_id: CoedgeId,
821 curve: &NurbCurve3D,
822 d: &Vector3,
823) -> GeopResult<Vec<Scalar>> {
824 let surface = &model.get_face(face_id)?.surface;
825 if surface.as_plane()?.is_some() || swept_along(surface, d) {
826 return Ok(Vec::new());
827 }
828 let coedge = model.get_coedge(coedge_id)?;
829 let (t0, t1) = curve.domain();
830 let (s0, s1) = coedge.pcurve.domain();
831 let edge_domain = model.get_edge(coedge.edge()?)?.curve.domain();
832 let foot = |t: Scalar| -> Option<(Scalar, Scalar)> {
835 let f = t
836 .sub(edge_domain.0)
837 .div(edge_domain.1.sub(edge_domain.0))
838 .ok()?;
839 let f = match coedge.sense {
840 geop_core_topology::Sense::Forward => f,
841 geop_core_topology::Sense::Reversed => Scalar::ONE.sub(f),
842 };
843 let seed = coedge.pcurve.evaluate(Scalar::fraction(s0, s1, f)).ok()?;
844 let point = curve.evaluate(t).ok()?;
845 surface
846 .project(point, seed[0], seed[1], CORRECTOR_ITERATIONS)
847 .ok()
848 };
849 let sign_at = |t: Scalar| -> Option<bool> {
850 let (u, v) = foot(t)?;
851 sign(surface, d, u, v)
852 };
853 let params = (0..=EDGE_SAMPLES).map(|k| match k {
854 0 => t0,
855 k if k == EDGE_SAMPLES => t1,
856 k => Scalar::fraction(t0, t1, Scalar::from_f64(k as f64 / EDGE_SAMPLES as f64)),
857 });
858 sign_changes(params, |t| -> GeopResult<_> { Ok(sign_at(t)) })
859 .bisected(BISECTIONS, false)
860 .map(|bracket| {
861 let bracket = bracket?;
862 Ok(bracket
863 .undecided
864 .unwrap_or_else(|| bracket.lo.union(bracket.hi)))
865 })
866 .collect()
867}
868
869pub fn face_silhouettes(
872 model: &Model,
873 face_id: FaceId,
874 d: &Vector3,
875) -> GeopResult<Vec<Silhouette>> {
876 let ctx = |e: GeopError| e.with_context(format!("face_silhouettes(face={face_id}, d={d:?})"));
877 let surface = &model.get_face(face_id).with_context(&ctx)?.surface;
878 if surface.as_plane().with_context(&ctx)?.is_some() || swept_along(surface, d) {
879 return Ok(Vec::new());
880 }
881 let step = extent(surface)
882 .div(Scalar::from_f64(STEPS_PER_FACE))
883 .with_context(&ctx)?;
884 let mut lines = grid(surface, d);
885 let mut traces = Vec::new();
886 for l in 0..lines.len() {
887 for b in 0..lines[l].brackets.len() {
888 if lines[l].traced[b] {
889 continue;
890 }
891 lines[l].traced[b] = true;
892 let Some(start) = seed(surface, d, &lines[l], lines[l].brackets[b]) else {
893 continue;
894 };
895 let half = Scalar::from_f64(lines[l].scale / 2.0);
896 let step = if half.definitely_less(step) {
897 half
898 } else {
899 step
900 };
901 let traced = trace(surface, d, start, step, &mut lines).map_err(|e| {
902 e.with_context(format!(
903 "tracing the silhouette of {surface:?} from {start:?} in steps of {step:?}"
904 ))
905 });
906 traces.push(traced.with_context(&ctx)?);
907 }
908 }
909 let mut silhouettes = Vec::new();
910 for points in traces {
911 let trimmed = trim(model, face_id, d, &points).map_err(|e| {
912 e.with_context(format!("trimming the silhouette traced through {points:?}"))
913 });
914 silhouettes.extend(trimmed.with_context(&ctx)?);
915 }
916 Ok(silhouettes)
917}
918
919fn trim(
922 model: &Model,
923 face_id: FaceId,
924 d: &Vector3,
925 points: &[(Scalar, Scalar)],
926) -> GeopResult<Vec<Silhouette>> {
927 let surface = &model.get_face(face_id)?.surface;
928 let mut uv: Vec<Vector2> = Vec::with_capacity(points.len());
929 for &(u, v) in points {
930 let p = Vector2::from_array([u, v]);
931 if uv.last().is_none_or(|q: &Vector2| !q.could_be_equal(&p)) {
932 uv.push(p);
933 }
934 }
935 if uv.len() < 2 {
936 return Ok(Vec::new());
937 }
938 let fit = NurbCurve2D::interpolate(&uv, (uv.len() - 1).min(3))
939 .map_err(|e| e.with_context(format!("fitting the traced silhouette through {uv:?}")))?;
940 let (t0, t1) = fit.domain();
941
942 let coarse = Scalar::from_f64(COARSE_SUBDIVISION);
957 let mut meetings = Vec::new();
958 for coedge_id in model.iterate_face_coedges(face_id) {
959 let pcurve = &model.get_coedge(coedge_id)?.pcurve;
960 let (overlaps, crossings) = curve_curve_overlaps_and_crossings(
961 &fit,
962 pcurve,
963 Scalar::ZERO,
964 MAX_NODES,
965 min_subdivision_size(),
966 )
967 .or_else(|error| {
968 curve_curve_overlaps_and_crossings(&fit, pcurve, coarse, MAX_NODES, coarse).map_err(
969 |e| {
970 e.with_context(format!(
971 "searched again to the sheet's resolution, after: {error}"
972 ))
973 },
974 )
975 })
976 .map_err(|e| {
977 e.with_context(format!(
978 "where the traced silhouette {fit:?} meets the pcurve {pcurve:?} of coedge \
979 {coedge_id}"
980 ))
981 })?;
982 meetings.extend(crossings.iter().map(|(s, _)| *s));
983 meetings.extend(overlaps.iter().flat_map(|o| [o.start.t, o.end.t]));
984 }
985 let mut cuts = vec![t0];
986 cuts.extend(cut_points(meetings, (t0, t1)));
987 cuts.push(t1);
988
989 let mut silhouettes = Vec::new();
990 for pair in cuts.windows(2) {
991 let (a, b) = (pair[0], pair[1]);
992 let mid = a.add(b).div(Scalar::TWO)?.sharpen();
993 let at = fit.evaluate(mid)?;
994 let inside = face_contains(
995 model,
996 face_id,
997 at[0],
998 at[1],
999 MAX_NODES,
1000 min_subdivision_size(),
1001 FACE_CONTAINS_SEED,
1002 )?;
1003 if inside != PointClassification::Inside {
1004 continue;
1005 }
1006 let share = b.sub(a).div(t1.sub(t0))?.to_f64();
1009 let count = ((share * uv.len() as f64).ceil() as usize).max(4);
1010 let mut samples = Vec::with_capacity(count + 1);
1011 for k in 0..=count {
1012 let t = match k {
1013 0 => a,
1014 k if k == count => b,
1015 k => Scalar::fraction(a, b, Scalar::from_f64(k as f64 / count as f64)),
1016 };
1017 let q = fit.evaluate(t)?;
1018 let (u, v) =
1019 correct(surface, d, (q[0].sharpen(), q[1].sharpen())).unwrap_or((q[0], q[1]));
1020 samples.push(Vector2::from_array([u, v]));
1021 }
1022 let mut stations: Vec<(Vector3, Vector2)> = Vec::with_capacity(samples.len());
1023 for s in &samples {
1024 let p = surface.evaluate(s[0], s[1])?;
1025 if stations
1026 .last()
1027 .is_none_or(|(q, _): &(Vector3, Vector2)| !q.could_be_equal(&p))
1028 {
1029 stations.push((p, *s));
1030 }
1031 }
1032 if stations.len() < 2 {
1033 continue;
1034 }
1035 let locate = |(_, from): &(Vector3, Vector2),
1042 (_, to): &(Vector3, Vector2),
1043 frac: Scalar|
1044 -> GeopResult<(Vector3, Vector2)> {
1045 let blend = |k: usize| Scalar::fraction(from[k], to[k], frac).sharpen();
1046 let (u, v) = correct(surface, d, (blend(0), blend(1)))?;
1047 Ok((surface.evaluate(u, v)?, Vector2::from_array([u, v])))
1048 };
1049 let ctx = |e: GeopError| {
1050 e.with_context(format!(
1051 "fitting the silhouette of face {face_id} through {stations:?}"
1052 ))
1053 };
1054 let curve = NurbCurve3D::fit_enclosing(&stations, locate, &mut None, &ctx)?;
1055 silhouettes.push(Silhouette {
1056 face: face_id,
1057 curve,
1058 samples,
1059 });
1060 }
1061 Ok(silhouettes)
1062}
1063
1064#[cfg(test)]
1065mod tests {
1066 use super::*;
1067 use geop_core_math::vector::Vector4;
1068 use geop_core_topology::build::BodySpec;
1069
1070 fn bumpy_sheet() -> (Model, FaceId) {
1077 let spans = 60;
1078 let n = spans + 3;
1079 let s = Scalar::from_f64;
1080 let mut knots = vec![Scalar::ZERO; 4];
1081 knots.extend((1..spans).map(|k| s(k as f64 / spans as f64)));
1082 knots.extend([Scalar::ONE; 4]);
1083 let mut control_points = Vec::with_capacity(n * n);
1084 for i in 0..n {
1085 for j in 0..n {
1086 let (x, y) = (i as f64 / (n - 1) as f64, j as f64 / (n - 1) as f64);
1087 let bump = if (i, j) == (30, 30) { 0.08 } else { 0.0 };
1088 control_points.push(Vector4::from_array([
1089 s(x),
1090 s(y),
1091 s(x / 20.0 + bump),
1092 s(1.0),
1093 ]));
1094 }
1095 }
1096 let surface = NurbSurface3D::try_new(3, 3, control_points, knots.clone(), knots).unwrap();
1097 let mut model = Model::new();
1098 let built = model
1099 .build_body(BodySpec::sheet_on(surface).unwrap())
1100 .unwrap();
1101 (model, built.faces[0])
1102 }
1103
1104 #[test]
1107 fn a_small_loop_between_grid_lines_is_found() {
1108 let (model, face) = bumpy_sheet();
1109 let d = Vector3::from_array([Scalar::ONE, Scalar::ZERO, Scalar::ZERO]);
1110 let pieces = face_silhouettes(&model, face, &d).unwrap();
1111 assert_eq!(pieces.len(), 1, "{pieces:?}");
1112 let curve = &pieces[0].curve;
1113 let (start, end) = (curve.start().unwrap(), curve.end().unwrap());
1114 assert!(start.could_be_equal(&end), "{start:?} {end:?}");
1115 }
1116}