1use crate::curve::interior_knot_count;
2use super::*;
3
4pub fn trim_sample_count(curve: &NurbsCurve) -> usize {
25 2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4)
26}
27
28pub fn trim_station(start: f64, end: f64, index: usize, count: usize) -> f64 {
36 start + (end - start) * index as f64 / count as f64
37}
38
39pub const SEAM_BAND_MERGED_SWITCH: &str = "BREP_SEAM_BAND_MERGED";
44
45pub const HORIZON_CONTAINMENT_SWITCH: &str = "BREP_HORIZON_CONTAINMENT";
48
49pub const HORIZON_SINGLE_LOOP_SWITCH: &str = "BREP_HORIZON_SINGLE_LOOP";
53
54pub const HORIZON_CROSS_FRAME_SWITCH: &str = "BREP_HORIZON_CROSS_FRAME";
59
60pub const COVERING_RIM_STRIP_SWITCH: &str = "BREP_COVERING_RIM_STRIP";
63
64pub const NO_SPHERE_CHART_TRIM_SWITCH: &str = "BREP_NO_SPHERE_CHART_TRIM";
69
70fn switched_off(name: &str) -> bool {
73 std::env::var(name).as_deref() == Ok("0")
74}
75
76pub fn horizon_cross_frame() -> bool {
81 !switched_off(HORIZON_CROSS_FRAME_SWITCH)
82}
83
84fn sphere_chart_trim_enabled() -> bool {
88 std::env::var("BREP_NO_SPHERE_CHARTS").is_err()
89 && std::env::var(NO_SPHERE_CHART_TRIM_SWITCH).is_err()
90}
91
92fn point_segment_distance(point: Vec2, start: Vec2, end: Vec2) -> f64 {
93 let segment = end.sub(start);
94 let length_squared = segment.dot(segment);
95 if length_squared <= 1e-30 {
96 return point.sub(start).length();
97 }
98 let parameter = (point.sub(start).dot(segment) / length_squared).clamp(0.0, 1.0);
99 point.sub(start.add(segment.scale(parameter))).length()
100}
101
102#[derive(Clone, Copy, Debug, PartialEq)]
103pub enum PolygonClass {
104 Inside,
105 Outside,
106 Boundary,
107}
108
109fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
110 for index in 0..polygon.len() {
111 if point_segment_distance(point, polygon[index], polygon[(index + 1) % polygon.len()])
112 <= tolerance
113 {
114 return PolygonClass::Boundary;
115 }
116 }
117 let mut inside = false;
118 for index in 0..polygon.len() {
119 let a = polygon[index];
120 let b = polygon[(index + 1) % polygon.len()];
121 if (a.y > point.y) != (b.y > point.y) {
122 let crossing = a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x);
123 if crossing > point.x {
124 inside = !inside;
125 }
126 }
127 }
128 if inside {
129 PolygonClass::Inside
130 } else {
131 PolygonClass::Outside
132 }
133}
134
135
136#[derive(Clone, Copy)]
141struct SegmentPrep {
142 coedge_index: usize,
143 parameter_start: f64,
144 parameter_end: f64,
145}
146
147fn lane_switches() -> f64 {
151 let mut bits = 0u32;
152 if switched_off(SEAM_BAND_MERGED_SWITCH) {
153 bits |= 1;
154 }
155 if switched_off(HORIZON_CONTAINMENT_SWITCH) {
156 bits |= 2;
157 }
158 if switched_off(HORIZON_SINGLE_LOOP_SWITCH) {
159 bits |= 4;
160 }
161 if switched_off(COVERING_RIM_STRIP_SWITCH) {
162 bits |= 8;
163 }
164 bits as f64
165}
166
167enum SeamBandPrep {
169 NotApplicable,
171 AlwaysInside,
173 Merged { polygon: Vec<Vec2>, u_span: f64, v_span: f64 },
175 Analyzed { polygon: Vec<Vec2> },
177 Rings { p_is_u: bool, q_lo: f64, q_hi: f64, complement: bool },
179 Declined,
181}
182
183struct SphereCapPrep {
185 rim: Vec<Vec2>,
186 pole_v: f64,
187 period: f64,
188}
189
190struct HorizonPrep {
192 loops: Vec<Vec<Vec2>>,
193 u_period: f64,
194 index: std::cell::OnceCell<Option<ChordIndex>>,
196}
197
198struct RimStripPrep {
200 lower: Vec<Vec2>,
201 upper: Vec<Vec2>,
202 period: f64,
203}
204
205struct GenericPrep {
208 loops: Vec<LoopSamples>,
209 segments: Vec<Vec<SegmentPrep>>,
210 chord_count: usize,
212 index: std::cell::OnceCell<Option<ChordIndex>>,
215}
216
217const CHORD_INDEX_MIN_CHORDS: usize = 64;
221
222const CHORD_INDEX_LEAF: usize = 6;
224
225const CHORD_INDEX_MAX_COORDINATE: f64 = 1e100;
230
231fn chord_index_enabled() -> bool {
235 static ENABLED: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
236 *ENABLED.get_or_init(|| !switched_off("BREP_CONTAINMENT_INDEX"))
237}
238
239#[derive(Clone, Copy)]
245struct ChordNode {
246 low: Vec2,
247 high: Vec2,
248 loop_index: u32,
249 first: u32,
250 last: u32,
251 children: Option<(u32, u32)>,
252}
253
254struct ChordIndex {
287 nodes: Vec<ChordNode>,
288 roots: Vec<u32>,
290 slack: f64,
292}
293
294trait ChordLoops {
297 fn loop_count(&self) -> usize;
298 fn polygon(&self, index: usize) -> &[Vec2];
299}
300
301impl ChordLoops for [LoopSamples] {
302 fn loop_count(&self) -> usize {
303 self.len()
304 }
305
306 fn polygon(&self, index: usize) -> &[Vec2] {
307 &self[index].polygon
308 }
309}
310
311impl ChordLoops for [Vec<Vec2>] {
312 fn loop_count(&self) -> usize {
313 self.len()
314 }
315
316 fn polygon(&self, index: usize) -> &[Vec2] {
317 &self[index]
318 }
319}
320
321impl ChordIndex {
322 fn build<L: ChordLoops + ?Sized>(loops: &L) -> Option<ChordIndex> {
326 let mut nodes: Vec<ChordNode> = Vec::new();
327 let mut roots = Vec::new();
328 let mut largest: f64 = 0.0;
329 for loop_index in 0..loops.loop_count() {
330 let polygon = loops.polygon(loop_index);
331 let len = polygon.len();
332 if len == 0 {
333 continue;
334 }
335 u32::try_from(len).ok()?;
336 for sample in polygon {
337 if !(sample.x.abs() <= CHORD_INDEX_MAX_COORDINATE && sample.y.abs() <= CHORD_INDEX_MAX_COORDINATE) {
338 return None;
339 }
340 largest = largest.max(sample.x.abs()).max(sample.y.abs());
341 }
342 let loop_index = u32::try_from(loop_index).ok()?;
343 let mut level: Vec<u32> = Vec::with_capacity(len / CHORD_INDEX_LEAF + 1);
344 for first in (0..len).step_by(CHORD_INDEX_LEAF) {
345 let last = (first + CHORD_INDEX_LEAF).min(len);
346 let (mut low, mut high) = (polygon[first], polygon[first]);
347 for index in first..last {
348 let end = polygon[(index + 1) % len];
349 low = Vec2 { x: low.x.min(end.x), y: low.y.min(end.y) };
350 high = Vec2 { x: high.x.max(end.x), y: high.y.max(end.y) };
351 let start = polygon[index];
352 low = Vec2 { x: low.x.min(start.x), y: low.y.min(start.y) };
353 high = Vec2 { x: high.x.max(start.x), y: high.y.max(start.y) };
354 }
355 level.push(u32::try_from(nodes.len()).ok()?);
356 nodes.push(ChordNode {
357 low,
358 high,
359 loop_index,
360 first: first as u32,
361 last: last as u32,
362 children: None,
363 });
364 }
365 while level.len() > 1 {
366 let mut next = Vec::with_capacity(level.len() / 2 + 1);
367 for pair in level.chunks(2) {
368 let &[left, right] = pair else {
369 next.push(pair[0]);
370 continue;
371 };
372 let (a, b) = (nodes[left as usize], nodes[right as usize]);
373 next.push(u32::try_from(nodes.len()).ok()?);
374 nodes.push(ChordNode {
375 low: Vec2 { x: a.low.x.min(b.low.x), y: a.low.y.min(b.low.y) },
376 high: Vec2 { x: a.high.x.max(b.high.x), y: a.high.y.max(b.high.y) },
377 loop_index,
378 first: a.first,
379 last: b.last,
380 children: Some((left, right)),
381 });
382 }
383 level = next;
384 }
385 roots.push(level[0]);
386 }
387 if roots.is_empty() {
388 return None;
389 }
390 Some(ChordIndex {
391 nodes,
392 roots,
393 slack: 1e-12 * (1.0 + largest),
394 })
395 }
396
397 fn slack_at(&self, point: Vec2) -> f64 {
400 self.slack + 1e-12 * point.x.abs().max(point.y.abs())
401 }
402
403 fn box_distance(node: &ChordNode, point: Vec2) -> f64 {
404 let dx = (node.low.x - point.x).max(point.x - node.high.x).max(0.0);
405 let dy = (node.low.y - point.y).max(point.y - node.high.y).max(0.0);
406 (dx * dx + dy * dy).sqrt()
407 }
408
409 fn nearest<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2) -> Option<(usize, usize, f64)> {
413 let slack = self.slack_at(point);
414 let mut best: Option<(f64, u32, u32)> = None;
415 let mut stack: Vec<(u32, f64)> = Vec::with_capacity(64);
416 for &root in &self.roots {
417 stack.push((root, Self::box_distance(&self.nodes[root as usize], point)));
418 }
419 while let Some((node_index, bound)) = stack.pop() {
420 if best.is_some_and(|(distance, _, _)| bound > distance + slack + 1e-12 * distance) {
421 continue;
422 }
423 let node = self.nodes[node_index as usize];
424 match node.children {
425 None => {
426 let polygon = loops.polygon(node.loop_index as usize);
427 for index in node.first..node.last {
428 let start = polygon[index as usize];
429 let end = polygon[(index as usize + 1) % polygon.len()];
430 let distance = point_segment_distance(point, start, end);
431 let better = best.is_none_or(|(least, loop_index, chord_index)| {
432 distance < least
433 || (distance == least && (node.loop_index, index) < (loop_index, chord_index))
434 });
435 if better {
436 best = Some((distance, node.loop_index, index));
437 }
438 }
439 }
440 Some((left, right)) => {
441 let left_bound = Self::box_distance(&self.nodes[left as usize], point);
442 let right_bound = Self::box_distance(&self.nodes[right as usize], point);
443 if left_bound <= right_bound {
445 stack.push((right, right_bound));
446 stack.push((left, left_bound));
447 } else {
448 stack.push((left, left_bound));
449 stack.push((right, right_bound));
450 }
451 }
452 }
453 }
454 best.map(|(distance, loop_index, chord_index)| {
455 (loop_index as usize, chord_index as usize, distance)
456 })
457 }
458
459 fn any_within<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2, tolerance: f64) -> bool {
462 let reach = tolerance + self.slack_at(point) + 1e-12 * tolerance.abs();
463 let mut stack: Vec<u32> = self.roots.clone();
464 while let Some(node_index) = stack.pop() {
465 let node = self.nodes[node_index as usize];
466 if Self::box_distance(&node, point) > reach {
467 continue;
468 }
469 match node.children {
470 None => {
471 let polygon = loops.polygon(node.loop_index as usize);
472 for index in node.first as usize..node.last as usize {
473 let (start, end) = (polygon[index], polygon[(index + 1) % polygon.len()]);
474 if point_segment_distance(point, start, end) <= tolerance {
475 return true;
476 }
477 }
478 }
479 Some((left, right)) => {
480 stack.push(right);
481 stack.push(left);
482 }
483 }
484 }
485 false
486 }
487
488 fn flip_crossings<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2, odd: &mut [bool]) {
491 let slack = self.slack_at(point);
492 let mut stack: Vec<u32> = self.roots.clone();
493 while let Some(node_index) = stack.pop() {
494 let node = self.nodes[node_index as usize];
495 if !(node.low.y <= point.y && node.high.y > point.y) || node.high.x + slack < point.x {
496 continue;
497 }
498 match node.children {
499 None => {
500 let polygon = loops.polygon(node.loop_index as usize);
501 for index in node.first as usize..node.last as usize {
502 let a = polygon[index];
503 let b = polygon[(index + 1) % polygon.len()];
504 if (a.y > point.y) != (b.y > point.y) {
505 let crossing = a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x);
506 if crossing > point.x {
507 let parity = &mut odd[node.loop_index as usize];
508 *parity = !*parity;
509 }
510 }
511 }
512 }
513 Some((left, right)) => {
514 stack.push(right);
515 stack.push(left);
516 }
517 }
518 }
519 }
520}
521
522#[derive(Clone, Copy, PartialEq, Eq)]
525enum ChordScan {
526 Auto,
529 Indexed,
531 Linear,
533}
534
535impl ChordScan {
536 fn indexes(self, chords: usize) -> bool {
538 match self {
539 ChordScan::Linear => false,
540 ChordScan::Indexed => true,
541 ChordScan::Auto => chord_index_enabled() && chords >= CHORD_INDEX_MIN_CHORDS,
542 }
543 }
544}
545
546fn in_index_range(point: Vec2) -> bool {
549 point.x.abs() <= CHORD_INDEX_MAX_COORDINATE && point.y.abs() <= CHORD_INDEX_MAX_COORDINATE
550}
551
552fn linear_scan(
555 generic: &GenericPrep,
556 point: Vec2,
557 tolerance: f64,
558) -> (usize, bool, Option<(usize, usize, f64)>) {
559 let mut crossings = 0;
560 let mut boundary = false;
561 let mut nearest: Option<(usize, usize, f64)> = None;
564 for (loop_index, (samples, chords)) in generic.loops.iter().zip(&generic.segments).enumerate() {
565 let polygon = &samples.polygon;
566 match point_in_polygon(point, polygon, tolerance) {
567 PolygonClass::Boundary => boundary = true,
568 PolygonClass::Inside => crossings += 1,
569 PolygonClass::Outside => {}
570 }
571 for index in 0..chords.len() {
572 let start = polygon[index];
573 let end = polygon[(index + 1) % polygon.len()];
574 let distance = point_segment_distance(point, start, end);
575 if nearest
576 .as_ref()
577 .is_none_or(|(_, _, nearest_distance)| distance < *nearest_distance)
578 {
579 nearest = Some((loop_index, index, distance));
580 }
581 }
582 }
583 (crossings, boundary, nearest)
584}
585
586fn generic_scan(
590 generic: &GenericPrep,
591 point: Vec2,
592 tolerance: f64,
593 scan: ChordScan,
594) -> (usize, bool, Option<(usize, usize, f64)>) {
595 if scan.indexes(generic.chord_count) && in_index_range(point) {
596 if let Some(index) = generic.index.get_or_init(|| ChordIndex::build(generic.loops.as_slice())) {
597 let loops = generic.loops.as_slice();
598 let nearest = index.nearest(loops, point);
599 let boundary = nearest.is_some_and(|(_, _, distance)| distance <= tolerance);
600 let crossings = if boundary {
601 0
602 } else {
603 let mut odd = vec![false; loops.len()];
604 index.flip_crossings(loops, point, &mut odd);
605 odd.into_iter().filter(|&parity| parity).count()
606 };
607 return (crossings, boundary, nearest);
608 }
609 }
610 linear_scan(generic, point, tolerance)
611}
612
613struct PreparedFace {
620 seam_band: std::cell::OnceCell<Result<SeamBandPrep, String>>,
621 sphere_cap: std::cell::OnceCell<Result<Option<SphereCapPrep>, String>>,
622 horizon: std::cell::OnceCell<Result<Option<HorizonPrep>, String>>,
623 rim_strip: std::cell::OnceCell<Result<Option<RimStripPrep>, String>>,
624 generic: std::cell::OnceCell<Result<GenericPrep, String>>,
625 sphere_region: std::cell::OnceCell<Result<crate::sphere_chart::SphericalRegion, String>>,
627}
628
629impl PreparedFace {
630 fn new() -> Self {
631 Self {
632 seam_band: std::cell::OnceCell::new(),
633 sphere_cap: std::cell::OnceCell::new(),
634 horizon: std::cell::OnceCell::new(),
635 rim_strip: std::cell::OnceCell::new(),
636 generic: std::cell::OnceCell::new(),
637 sphere_region: std::cell::OnceCell::new(),
638 }
639 }
640}
641
642fn lane<'a, T>(
643 cell: &'a std::cell::OnceCell<Result<T, String>>,
644 build: impl FnOnce() -> Result<T, String>,
645) -> Result<&'a T, String> {
646 match cell.get_or_init(build) {
647 Ok(value) => Ok(value),
648 Err(error) => Err(error.clone()),
649 }
650}
651
652struct PreparedFaceCache {
670 entries: Vec<(u64, Vec<f64>, std::rc::Rc<PreparedFace>)>,
671}
672
673const PREPARED_FACE_CACHE_ENTRIES: usize = 64;
674
675thread_local! {
676 static PREPARED_FACES: std::cell::RefCell<PreparedFaceCache> = const {
677 std::cell::RefCell::new(PreparedFaceCache {
678 entries: Vec::new(),
679 })
680 };
681}
682
683fn walk_face_signature<F: FnMut(f64) -> bool>(face: &FaceRecord, switches: f64, visit: &mut F) -> bool {
688 let surface = &face.surface;
689 let point = |visit: &mut F, p: &crate::Vec4| visit(p.x) && visit(p.y) && visit(p.z) && visit(p.w);
690 if !(visit(switches)
691 && visit(if face.same_sense { 1.0 } else { 0.0 })
692 && visit(surface.degree_u as f64)
693 && visit(surface.degree_v as f64)
694 && visit(surface.knots_u.len() as f64))
695 {
696 return false;
697 }
698 if !(surface.knots_u.iter().all(|&knot| visit(knot)) && visit(surface.knots_v.len() as f64)) {
699 return false;
700 }
701 if !(surface.knots_v.iter().all(|&knot| visit(knot)) && visit(surface.control_points.len() as f64)) {
702 return false;
703 }
704 for row in &surface.control_points {
705 if !(visit(row.len() as f64) && row.iter().all(|p| point(visit, p))) {
706 return false;
707 }
708 }
709 if !visit(face.loops.len() as f64) {
710 return false;
711 }
712 for loop_record in &face.loops {
713 if !visit(loop_record.coedges.len() as f64) {
714 return false;
715 }
716 for coedge in &loop_record.coedges {
717 let curve = &coedge.pcurve;
718 if !(visit(coedge.edge_id as f64)
719 && visit(if coedge.forward { 1.0 } else { 0.0 })
720 && visit(curve.degree as f64)
721 && visit(curve.knots.len() as f64))
722 {
723 return false;
724 }
725 if !(curve.knots.iter().all(|&knot| visit(knot)) && visit(curve.control_points.len() as f64)) {
726 return false;
727 }
728 if !curve.control_points.iter().all(|p| point(visit, p)) {
729 return false;
730 }
731 }
732 }
733 true
734}
735
736fn face_key_hash(face: &FaceRecord, switches: f64) -> u64 {
741 use std::hash::Hasher;
742 let mut hasher = rustc_hash::FxHasher::default();
743 let mut write = |value: f64| hasher.write_u64(value.to_bits());
744 let surface = &face.surface;
745 write(switches);
746 write(if face.same_sense { 1.0 } else { 0.0 });
747 write(surface.degree_u as f64);
748 write(surface.degree_v as f64);
749 write(surface.knots_u.len() as f64);
750 write(surface.knots_v.len() as f64);
751 write(surface.control_points.len() as f64);
752 let corners = [surface.control_points.first().and_then(|row| row.first()), surface.control_points.last().and_then(|row| row.last())];
753 for p in corners.into_iter().flatten() {
754 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
755 }
756 write(face.loops.len() as f64);
757 for loop_record in &face.loops {
758 write(loop_record.coedges.len() as f64);
759 for coedge in &loop_record.coedges {
760 let curve = &coedge.pcurve;
761 write(coedge.edge_id as f64);
762 write(if coedge.forward { 1.0 } else { 0.0 });
763 write(curve.degree as f64);
764 write(curve.knots.len() as f64);
765 write(curve.control_points.len() as f64);
766 for knot in [curve.knots.first(), curve.knots.last()].into_iter().flatten() {
767 write(*knot);
768 }
769 for p in [curve.control_points.first(), curve.control_points.last()].into_iter().flatten() {
770 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
771 }
772 }
773 }
774 hasher.finish()
775}
776
777fn cached_entry(
779 entries: &[(u64, Vec<f64>, std::rc::Rc<PreparedFace>)],
780 face: &FaceRecord,
781 switches: f64,
782 hash: u64,
783) -> Option<usize> {
784 entries.iter().position(|(entry_hash, signature, _)| {
785 if *entry_hash != hash {
786 return false;
787 }
788 let mut at = 0;
789 let walked = walk_face_signature(face, switches, &mut |value: f64| {
790 let same = signature
791 .get(at)
792 .is_some_and(|stored| stored.to_bits() == value.to_bits() && !value.is_nan());
793 at += 1;
794 same
795 });
796 walked && at == signature.len()
797 })
798}
799
800pub(crate) fn forget_prepared_faces() {
803 PREPARED_FACES.with(|cache| cache.borrow_mut().entries.clear());
804}
805
806fn cached_prepared_face(face: &FaceRecord) -> Option<std::rc::Rc<PreparedFace>> {
810 PREPARED_FACES.with(|cache| {
811 let PreparedFaceCache { entries } = &*cache.borrow();
812 let switches = lane_switches();
813 let hash = face_key_hash(face, switches);
814 cached_entry(entries, face, switches, hash).map(|index| entries[index].2.clone())
815 })
816}
817
818fn prepared_face(face: &FaceRecord) -> std::rc::Rc<PreparedFace> {
819 PREPARED_FACES.with(|cache| {
820 let PreparedFaceCache { entries } = &mut *cache.borrow_mut();
821 let switches = lane_switches();
822 let hash = face_key_hash(face, switches);
823 if let Some(index) = cached_entry(entries, face, switches, hash) {
824 if index != 0 {
825 let entry = entries.remove(index);
826 entries.insert(0, entry);
827 }
828 return entries[0].2.clone();
829 }
830 let mut signature = Vec::new();
831 walk_face_signature(face, switches, &mut |value: f64| {
832 signature.push(value);
833 true
834 });
835 let prepared = std::rc::Rc::new(PreparedFace::new());
836 entries.insert(0, (hash, signature, prepared.clone()));
837 entries.truncate(PREPARED_FACE_CACHE_ENTRIES);
838 prepared
839 })
840}
841
842fn prepare_seam_band(face: &FaceRecord) -> Result<SeamBandPrep, String> {
851 if face.surface.closed_directions()? != (true, true) {
852 return Ok(SeamBandPrep::NotApplicable);
853 }
854 let [u0, u1] = face.surface.domain_u()?;
855 let [v0, v1] = face.surface.domain_v()?;
856 if crate::topology::doubly_periodic_has_only_collapsed_loops(face)? {
857 return Ok(SeamBandPrep::AlwaysInside);
858 }
859 let u_span = (u1 - u0).abs().max(1e-30);
860 let v_span = (v1 - v0).abs().max(1e-30);
861 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
862 let mut retained: Vec<usize> = Vec::with_capacity(face.loops.len());
866 for (loop_index, loop_record) in face.loops.iter().enumerate() {
867 let mut points: Vec<[f64; 2]> = Vec::new();
868 for coedge in &loop_record.coedges {
869 let [d0, d1] = coedge.pcurve.domain()?;
870 let samples = 24;
871 for k in 0..=samples {
872 let t = trim_station(d0, d1, k, samples);
873 let p = coedge.pcurve.evaluate(t)?;
874 points.push([p.x, p.y]);
875 }
876 }
877 let (mut umin, mut umax, mut vmin, mut vmax) = (
878 f64::INFINITY,
879 f64::NEG_INFINITY,
880 f64::INFINITY,
881 f64::NEG_INFINITY,
882 );
883 for p in &points {
884 umin = umin.min(p[0]);
885 umax = umax.max(p[0]);
886 vmin = vmin.min(p[1]);
887 vmax = vmax.max(p[1]);
888 }
889 if (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span {
909 continue;
910 }
911 retained.push(loop_index);
912 loops_uv.push(points);
913 }
914 if loops_uv.len() == 1 && !switched_off(SEAM_BAND_MERGED_SWITCH) {
925 let coedges = &face.loops[retained[0]].coedges;
926 let offsets = crate::topology::loop_seam_offsets(coedges, true, true, u_span, v_span)?;
927 if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
928 let mut polygon: Vec<Vec2> = Vec::new();
929 for (coedge_index, coedge) in coedges.iter().enumerate() {
930 let [d0, d1] = coedge.pcurve.domain()?;
931 let samples = 24;
932 for k in 0..samples {
933 let t = trim_station(d0, d1, k, samples);
934 let p = coedge.pcurve.evaluate(t)?;
935 polygon.push(Vec2 {
936 x: p.x + offsets[coedge_index][0],
937 y: p.y + offsets[coedge_index][1],
938 });
939 }
940 }
941 return Ok(SeamBandPrep::Merged { polygon, u_span, v_span });
942 }
943 }
944 if loops_uv.len() != 2 {
945 return Ok(SeamBandPrep::Declined);
946 }
947 if let Some(band) = crate::topology::analyze_doubly_periodic_seam_band(
948 &loops_uv,
949 [u0, u1, v0, v1],
950 face.same_sense,
951 ) {
952 let polygon: Vec<Vec2> =
953 crate::topology::seam_band_uv_polygon(&loops_uv, [u0, u1, v0, v1], &band)
954 .into_iter()
955 .map(|p| Vec2 { x: p[0], y: p[1] })
956 .collect();
957 return Ok(SeamBandPrep::Analyzed { polygon });
958 }
959 for p_is_u in [true, false] {
966 let (period, q_extent) = if p_is_u {
967 (u1 - u0, v_span)
968 } else {
969 (v1 - v0, u_span)
970 };
971 if !(period > 0.0) {
972 continue;
973 }
974 let coord = |p: &[f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
975 let mut rings = Vec::with_capacity(2);
976 for points in &loops_uv {
977 let (mut pmin, mut pmax, mut qmin, mut qmax) = (
978 f64::INFINITY,
979 f64::NEG_INFINITY,
980 f64::INFINITY,
981 f64::NEG_INFINITY,
982 );
983 for p in points {
984 let (periodic, cross) = coord(p);
985 pmin = pmin.min(periodic);
986 pmax = pmax.max(periodic);
987 qmin = qmin.min(cross);
988 qmax = qmax.max(cross);
989 }
990 if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
991 rings.clear();
992 break;
993 }
994 let mut net = 0.0;
995 for pair in points.windows(2) {
996 let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
997 if delta > 0.5 * period {
998 delta -= period;
999 } else if delta < -0.5 * period {
1000 delta += period;
1001 }
1002 net += delta;
1003 }
1004 let direction = if net > 0.25 * period {
1005 1
1006 } else if net < -0.25 * period {
1007 -1
1008 } else {
1009 0
1010 };
1011 rings.push((0.5 * (qmin + qmax), direction));
1012 }
1013 if rings.len() != 2 {
1014 continue;
1015 }
1016 rings.sort_by(|a, b| a.0.total_cmp(&b.0));
1017 let [(q_lo, lower_direction), (q_hi, upper_direction)] = rings.as_slice() else {
1018 unreachable!()
1019 };
1020 let inconclusive =
1021 *lower_direction == 0 || *upper_direction == 0 || lower_direction == upper_direction;
1022 let between_is_ccw_uv = if p_is_u {
1023 *lower_direction > 0
1024 } else {
1025 *lower_direction < 0
1026 };
1027 let complement = !inconclusive && between_is_ccw_uv != face.same_sense;
1028 return Ok(SeamBandPrep::Rings {
1029 p_is_u,
1030 q_lo: *q_lo,
1031 q_hi: *q_hi,
1032 complement,
1033 });
1034 }
1035 Ok(SeamBandPrep::Declined)
1036}
1037
1038fn seam_band_point_in_face(
1039 prepared: &SeamBandPrep,
1040 point: Vec2,
1041 tolerance: f64,
1042) -> Option<PolygonClass> {
1043 match prepared {
1044 SeamBandPrep::NotApplicable | SeamBandPrep::Declined => None,
1045 SeamBandPrep::AlwaysInside => Some(PolygonClass::Inside),
1046 SeamBandPrep::Merged { polygon, u_span, v_span } => {
1047 let mut best = PolygonClass::Outside;
1048 'images: for du in [-1.0, 0.0, 1.0] {
1049 for dv in [-1.0, 0.0, 1.0] {
1050 let image = Vec2 {
1051 x: point.x + du * u_span,
1052 y: point.y + dv * v_span,
1053 };
1054 match point_in_polygon(image, polygon, tolerance) {
1055 PolygonClass::Boundary => {
1056 best = PolygonClass::Boundary;
1057 break 'images;
1058 }
1059 PolygonClass::Inside => best = PolygonClass::Inside,
1060 PolygonClass::Outside => {}
1061 }
1062 }
1063 }
1064 Some(best)
1065 }
1066 SeamBandPrep::Analyzed { polygon } => Some(point_in_polygon(point, polygon, tolerance)),
1067 SeamBandPrep::Rings { p_is_u, q_lo, q_hi, complement } => {
1068 let q = if *p_is_u { point.y } else { point.x };
1069 if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
1070 return Some(PolygonClass::Boundary);
1071 }
1072 let between = q > *q_lo && q < *q_hi;
1073 Some(if between != *complement {
1074 PolygonClass::Inside
1075 } else {
1076 PolygonClass::Outside
1077 })
1078 }
1079 }
1080}
1081
1082fn prepare_wrapped_horizon(face: &FaceRecord) -> Result<Option<HorizonPrep>, String> {
1098 if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
1099 return Ok(None);
1100 }
1101 if switched_off(HORIZON_CONTAINMENT_SWITCH) {
1102 return Ok(None);
1103 }
1104 if face.loops.len() <= 2 && switched_off(HORIZON_SINGLE_LOOP_SWITCH) {
1115 return Ok(None);
1116 }
1117 let [u0, u1] = face.surface.domain_u()?;
1118 let u_period = u1 - u0;
1119 if u_period <= 0.0 {
1120 return Ok(None);
1121 }
1122 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1123 let mut loops: Vec<Vec<Vec2>> = Vec::new();
1124 let mut straddling = false;
1125 for loop_record in &face.loops {
1126 let mut points: Vec<Vec2> = Vec::new();
1127 for coedge in &loop_record.coedges {
1128 let curve = &coedge.pcurve;
1129 let [start, end] = curve.domain()?;
1130 let sample_count = trim_sample_count(curve);
1131 for index in 0..sample_count {
1132 let parameter = trim_station(start, end, index, sample_count);
1133 let evaluated = curve.evaluate(parameter)?;
1134 points.push(Vec2 {
1135 x: evaluated.x,
1136 y: evaluated.y,
1137 });
1138 }
1139 }
1140 if points.len() < 3 {
1141 continue;
1142 }
1143 let mut unwrapped = Vec::with_capacity(points.len());
1147 let mut jumps = 0usize;
1148 let mut cursor = points[0];
1149 unwrapped.push(cursor);
1150 for pair in points.windows(2) {
1151 let mut du = pair[1].x - pair[0].x;
1152 let folded = du - u_period * (du / u_period).round();
1153 if (du - folded).abs() > 0.25 * u_period {
1154 jumps += 1;
1155 }
1156 du = folded;
1157 cursor = Vec2 {
1158 x: cursor.x + du,
1159 y: pair[1].y,
1160 };
1161 unwrapped.push(cursor);
1162 }
1163 let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
1164 if closure > 0.25 * u_period {
1165 if debug {
1166 eprintln!(
1167 "horizon: face {} loop winds the period (closure {closure:.3}) — bail",
1168 face.id
1169 );
1170 }
1171 return Ok(None); }
1173 if jumps > 0 {
1174 straddling = true;
1175 }
1176 loops.push(unwrapped);
1177 }
1178 if !straddling || loops.is_empty() {
1179 return Ok(None);
1180 }
1181 Ok(Some(HorizonPrep {
1182 loops,
1183 u_period,
1184 index: std::cell::OnceCell::new(),
1185 }))
1186}
1187
1188fn wrapped_horizon_point_in_face(
1189 face: &FaceRecord,
1190 prepared: &HorizonPrep,
1191 point: Vec2,
1192 tolerance: f64,
1193 scan: ChordScan,
1194) -> Option<PolygonClass> {
1195 let HorizonPrep { loops, u_period, index } = prepared;
1196 let u_period = *u_period;
1197 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1198 if !horizon_cross_frame() {
1213 let mut best: Option<PolygonClass> = None;
1214 for shift in [-u_period, 0.0, u_period] {
1215 let image = Vec2 {
1216 x: point.x + shift,
1217 y: point.y,
1218 };
1219 let mut crossings = 0usize;
1220 for polygon in loops {
1221 match point_in_polygon(image, polygon, tolerance) {
1222 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1223 PolygonClass::Inside => crossings += 1,
1224 PolygonClass::Outside => {}
1225 }
1226 }
1227 if crossings % 2 == 1 {
1228 best = Some(PolygonClass::Inside);
1229 } else if best.is_none() {
1230 best = Some(PolygonClass::Outside);
1231 }
1232 }
1233 if debug {
1234 eprintln!(
1235 "horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
1236 face.id,
1237 loops.len(),
1238 best
1239 );
1240 }
1241 return best;
1242 }
1243 let images = [-u_period, 0.0, u_period].map(|shift| Vec2 {
1244 x: point.x + shift,
1245 y: point.y,
1246 });
1247 let chords: usize = loops.iter().map(Vec::len).sum();
1248 let indexed = (scan.indexes(chords) && images.iter().all(|image| in_index_range(*image)))
1249 .then(|| index.get_or_init(|| ChordIndex::build(loops.as_slice())).as_ref())
1250 .flatten();
1251 let mut crossings = 0usize;
1252 if let Some(index) = indexed {
1253 let loops = loops.as_slice();
1257 if images.iter().any(|image| index.any_within(loops, *image, tolerance)) {
1258 return Some(PolygonClass::Boundary);
1259 }
1260 let mut odd = vec![false; loops.len()];
1261 for image in images {
1262 index.flip_crossings(loops, image, &mut odd);
1263 }
1264 crossings = odd.into_iter().filter(|&parity| parity).count();
1265 } else {
1266 for polygon in loops {
1267 let mut image_hits = 0usize;
1268 for image in images {
1269 match point_in_polygon(image, polygon, tolerance) {
1270 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1271 PolygonClass::Inside => image_hits += 1,
1272 PolygonClass::Outside => {}
1273 }
1274 }
1275 crossings += image_hits % 2;
1276 }
1277 }
1278 let class = if crossings % 2 == 1 {
1279 PolygonClass::Inside
1280 } else {
1281 PolygonClass::Outside
1282 };
1283 if debug {
1284 eprintln!(
1285 "horizon: face {} loops={} straddling -> {:?}",
1286 face.id,
1287 loops.len(),
1288 class
1289 );
1290 }
1291 Some(class)
1292}
1293
1294fn prepare_winding_sphere_cap(face: &FaceRecord) -> Result<Option<SphereCapPrep>, String> {
1299 if !matches!(
1300 face.surface.analytic(),
1301 Some(crate::AnalyticSurface::Sphere { .. })
1302 ) || face.surface.closed_directions()? != (true, false)
1303 || face.loops.len() != 2
1304 {
1305 return Ok(None);
1306 }
1307 let [u0, u1] = face.surface.domain_u()?;
1308 let [v0, v1] = face.surface.domain_v()?;
1309 let period = u1 - u0;
1310 let v_span = v1 - v0;
1311 if !(period > 0.0 && v_span > 0.0) {
1312 return Ok(None);
1313 }
1314 struct WindingLoop {
1315 points: Vec<Vec2>,
1316 winding: f64,
1317 vmin: f64,
1318 vmax: f64,
1319 }
1320 let mut loops = Vec::with_capacity(2);
1321 for loop_record in &face.loops {
1322 let mut points = Vec::new();
1323 for coedge in &loop_record.coedges {
1324 let [start, end] = coedge.pcurve.domain()?;
1325 let samples = trim_sample_count(&coedge.pcurve);
1326 for index in 0..samples {
1327 let parameter = trim_station(start, end, index, samples);
1328 let p = coedge.pcurve.evaluate(parameter)?;
1329 points.push(Vec2 { x: p.x, y: p.y });
1330 }
1331 }
1332 if points.len() < 2 {
1333 return Ok(None);
1334 }
1335 let first = points[0];
1336 let mut cursor = first;
1337 let mut unwrapped = vec![cursor];
1338 for next in points.iter().skip(1) {
1339 let du = next.x - cursor.x;
1340 let folded = du - period * (du / period).round();
1341 cursor = Vec2 {
1342 x: cursor.x + folded,
1343 y: next.y,
1344 };
1345 unwrapped.push(cursor);
1346 }
1347 let last_raw = *points.last().unwrap();
1348 let closing_du = first.x - last_raw.x;
1349 let closing_folded = closing_du - period * (closing_du / period).round();
1350 let winding = cursor.x + closing_folded - first.x;
1351 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1352 for p in &unwrapped {
1353 vmin = vmin.min(p.y);
1354 vmax = vmax.max(p.y);
1355 }
1356 loops.push(WindingLoop {
1357 points: unwrapped,
1358 winding,
1359 vmin,
1360 vmax,
1361 });
1362 }
1363 if loops
1364 .iter()
1365 .any(|loop_data| (loop_data.winding.abs() - period).abs() > 0.05 * period)
1366 || loops[0].winding * loops[1].winding >= 0.0
1367 {
1368 return Ok(None);
1369 }
1370 let flat = |loop_data: &WindingLoop| loop_data.vmax - loop_data.vmin <= 1e-6 * v_span;
1371 let (pole, rim) = match (flat(&loops[0]), flat(&loops[1])) {
1372 (true, false) => (&loops[0], &loops[1]),
1373 (false, true) => (&loops[1], &loops[0]),
1374 _ => return Ok(None),
1375 };
1376 let pole_v = 0.5 * (pole.vmin + pole.vmax);
1377 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
1378 return Ok(None);
1379 }
1380 Ok(Some(SphereCapPrep {
1381 rim: rim.points.clone(),
1382 pole_v,
1383 period,
1384 }))
1385}
1386
1387fn winding_sphere_cap_point_in_face(
1388 prepared: &SphereCapPrep,
1389 point: Vec2,
1390 tolerance: f64,
1391) -> Option<PolygonClass> {
1392 let SphereCapPrep { rim, pole_v, period } = prepared;
1393 let (pole_v, period) = (*pole_v, *period);
1394 let window_start = rim[0].x.min(rim[rim.len() - 1].x);
1395 let query_u = window_start + (point.x - window_start).rem_euclid(period);
1396 let mut crossings = Vec::new();
1397 for pair in rim.windows(2) {
1398 let (a, b) = (pair[0], pair[1]);
1399 if (a.x > query_u) != (b.x > query_u) {
1400 crossings.push(a.y + (query_u - a.x) / (b.x - a.x) * (b.y - a.y));
1401 }
1402 for image in [-period, 0.0, period] {
1403 if point_segment_distance(
1404 Vec2 {
1405 x: point.x + image,
1406 y: point.y,
1407 },
1408 a,
1409 b,
1410 ) <= tolerance
1411 {
1412 return Some(PolygonClass::Boundary);
1413 }
1414 }
1415 }
1416 let rim_v = crossings
1417 .into_iter()
1418 .min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))?;
1419 let inside = if pole_v < rim_v {
1420 point.y <= rim_v + tolerance
1421 } else {
1422 point.y >= rim_v - tolerance
1423 };
1424 Some(if inside {
1425 PolygonClass::Inside
1426 } else {
1427 PolygonClass::Outside
1428 })
1429}
1430
1431fn prepare_covering_rim_strip(face: &FaceRecord) -> Result<Option<RimStripPrep>, String> {
1463 if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
1464 return Ok(None);
1465 }
1466 if switched_off(COVERING_RIM_STRIP_SWITCH) {
1467 return Ok(None);
1468 }
1469 let [u0, u1] = face.surface.domain_u()?;
1470 let [v0, v1] = face.surface.domain_v()?;
1471 let period = u1 - u0;
1472 if !(period > 0.0) {
1473 return Ok(None);
1474 }
1475 let v_span = (v1 - v0).abs().max(1e-30);
1476 struct Rim {
1477 points: Vec<Vec2>,
1478 vmin: f64,
1479 vmax: f64,
1480 ascending: bool,
1481 }
1482 let mut rims: Vec<Rim> = Vec::with_capacity(2);
1483 let mut out_of_domain = false;
1484 for loop_record in &face.loops {
1485 let mut points: Vec<Vec2> = Vec::new();
1486 for coedge in &loop_record.coedges {
1487 let curve = &coedge.pcurve;
1488 let [start, end] = curve.domain()?;
1489 let sample_count = trim_sample_count(curve);
1490 for index in 0..=sample_count {
1491 let parameter = trim_station(start, end, index, sample_count);
1492 let evaluated = curve.evaluate(parameter)?;
1493 points.push(Vec2 {
1494 x: evaluated.x,
1495 y: evaluated.y,
1496 });
1497 }
1498 }
1499 if points.len() < 3 {
1500 return Ok(None);
1501 }
1502 for p in &points {
1505 if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
1506 out_of_domain = true;
1507 }
1508 }
1509 let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
1513 let mut cursor = points[0];
1514 unwrapped.push(cursor);
1515 for pair in points.windows(2) {
1516 let du = pair[1].x - pair[0].x;
1517 let folded = du - period * (du / period).round();
1518 cursor = Vec2 {
1519 x: cursor.x + folded,
1520 y: pair[1].y,
1521 };
1522 unwrapped.push(cursor);
1523 }
1524 let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
1525 if (net.abs() - period).abs() > 2e-2 * period {
1526 return Ok(None); }
1528 if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
1529 return Ok(None); }
1531 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1532 for p in &unwrapped {
1533 vmin = vmin.min(p.y);
1534 vmax = vmax.max(p.y);
1535 }
1536 rims.push(Rim {
1537 points: unwrapped,
1538 vmin,
1539 vmax,
1540 ascending: net > 0.0,
1541 });
1542 }
1543 if !out_of_domain {
1544 return Ok(None);
1545 }
1546 if rims[0].ascending == rims[1].ascending {
1547 return Ok(None); }
1549 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
1550 (&rims[0], &rims[1])
1551 } else if rims[1].vmax <= rims[0].vmin {
1552 (&rims[1], &rims[0])
1553 } else {
1554 return Ok(None); };
1556 if upper.vmin - lower.vmax <= 1e-6 * v_span {
1557 return Ok(None);
1558 }
1559 if (lower.ascending) != face.same_sense {
1564 return Ok(None);
1565 }
1566 Ok(Some(RimStripPrep {
1567 lower: lower.points.clone(),
1568 upper: upper.points.clone(),
1569 period,
1570 }))
1571}
1572
1573fn covering_rim_strip_point_in_face(
1574 prepared: &RimStripPrep,
1575 point: Vec2,
1576 tolerance: f64,
1577) -> Option<PolygonClass> {
1578 let RimStripPrep { lower, upper, period } = prepared;
1579 let period = *period;
1580 for rim in [lower, upper] {
1582 for shift in [-period, 0.0, period] {
1583 let image = Vec2 {
1584 x: point.x + shift,
1585 y: point.y,
1586 };
1587 for pair in rim.windows(2) {
1588 if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
1589 return Some(PolygonClass::Boundary);
1590 }
1591 }
1592 }
1593 }
1594 let mut crossings = 0usize;
1597 for rim in [lower, upper] {
1598 let window_base = rim[0].x.min(rim[rim.len() - 1].x);
1599 let x = window_base + (point.x - window_base).rem_euclid(period);
1600 for pair in rim.windows(2) {
1601 let (a, b) = (pair[0], pair[1]);
1602 if (a.x > x) != (b.x > x) {
1603 let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
1604 if v_cross > point.y {
1605 crossings += 1;
1606 }
1607 }
1608 }
1609 }
1610 Some(if crossings % 2 == 1 {
1611 PolygonClass::Inside
1612 } else {
1613 PolygonClass::Outside
1614 })
1615}
1616
1617fn prepare_generic(face: &FaceRecord) -> Result<GenericPrep, String> {
1621 let mut loops: Vec<LoopSamples> = Vec::with_capacity(face.loops.len());
1622 let mut segments: Vec<Vec<SegmentPrep>> = Vec::with_capacity(face.loops.len());
1623 let mut chord_count = 0;
1624 for loop_record in &face.loops {
1625 let mut polygon = Vec::new();
1626 let mut chords = Vec::new();
1627 let mut coedges = Vec::with_capacity(loop_record.coedges.len());
1628 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
1629 let first = polygon.len();
1630 let curve = &coedge.pcurve;
1631 let [start, end] = curve.domain()?;
1632 let sample_count = trim_sample_count(curve);
1633 for index in 0..sample_count {
1634 let parameter = trim_station(start, end, index, sample_count);
1635 let evaluated = curve.evaluate(parameter)?;
1636 polygon.push(Vec2 {
1637 x: evaluated.x,
1638 y: evaluated.y,
1639 });
1640 let parameter_end = if index + 1 < sample_count {
1641 trim_station(start, end, index + 1, sample_count)
1642 } else {
1643 end
1644 };
1645 chords.push(SegmentPrep {
1646 coedge_index,
1647 parameter_start: parameter,
1648 parameter_end,
1649 });
1650 }
1651 coedges.push((coedge.edge_id, first, polygon.len() - first));
1652 }
1653 chord_count += chords.len();
1654 loops.push(LoopSamples { polygon, coedges });
1655 segments.push(chords);
1656 }
1657 Ok(GenericPrep {
1658 loops,
1659 segments,
1660 chord_count,
1661 index: std::cell::OnceCell::new(),
1662 })
1663}
1664
1665struct LoopSamples {
1670 polygon: Vec<Vec2>,
1671 coedges: Vec<(u64, usize, usize)>,
1672}
1673
1674fn sphere_chart_point_in_face(
1699 face: &FaceRecord,
1700 point: Vec2,
1701 prepared: &PreparedFace,
1702 sampled: &[LoopSamples],
1703) -> Result<Option<PolygonClass>, String> {
1704 let Some(atlas) = sphere_chart_atlas(face) else {
1705 return Ok(None);
1706 };
1707 let region = lane(&prepared.sphere_region, || build_sphere_region(face, &atlas, sampled))?;
1708 if region.is_whole_sphere() {
1709 return Ok(Some(PolygonClass::Inside));
1710 }
1711 if !region.is_decidable() {
1712 return Ok(None);
1715 }
1716 let probe = face.surface.evaluate(point.x, point.y)?;
1717 Ok(Some(if region.contains(atlas.centre, probe) {
1718 PolygonClass::Inside
1719 } else {
1720 PolygonClass::Outside
1721 }))
1722}
1723
1724fn build_sphere_region(
1742 face: &FaceRecord,
1743 atlas: &crate::sphere_chart::SphereAtlas,
1744 sampled: &[LoopSamples],
1745) -> Result<crate::sphere_chart::SphericalRegion, String> {
1746 let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
1750 for coedge in face.loops.iter().flat_map(|record| record.coedges.iter()) {
1751 *uses.entry(coedge.edge_id).or_insert(0) += 1;
1752 }
1753 let outward_face_normal =
1778 face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
1779 let mut points: Vec<Vec3> = Vec::new();
1780 let mut spans: Vec<(usize, usize)> = Vec::new();
1781 for samples in sampled {
1782 let count = samples.polygon.len();
1783 if count < 2 {
1784 continue;
1785 }
1786 for &(edge_id, first, span) in &samples.coedges {
1787 if span == 0 || uses.get(&edge_id).copied().unwrap_or(0) >= 2 {
1788 continue;
1789 }
1790 let start = points.len();
1794 for offset in 0..=span {
1795 let uv = samples.polygon[(first + offset) % count];
1796 points.push(face.surface.evaluate(uv.x, uv.y)?);
1797 }
1798 spans.push((start, points.len()));
1799 }
1800 }
1801 crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
1806 let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
1807 for (first, last) in spans {
1808 for index in first..last.saturating_sub(1) {
1809 boundary.push((points[index], points[index + 1]));
1810 }
1811 }
1812 Ok(crate::sphere_chart::SphericalRegion::from_segments(
1813 atlas.centre,
1814 &boundary,
1815 outward_face_normal,
1816 ))
1817}
1818
1819
1820fn sphere_chart_atlas(face: &FaceRecord) -> Option<crate::sphere_chart::SphereAtlas> {
1823 if !sphere_chart_trim_enabled() {
1824 return None;
1825 }
1826 crate::sphere_chart::SphereAtlas::of_surface(&face.surface)
1827}
1828
1829fn containment_trace_enabled() -> bool {
1833 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1834 *ON.get_or_init(|| std::env::var("BREP_DEBUG_CONTAINMENT").is_ok())
1835}
1836
1837fn trace_lane(face: &FaceRecord, point: Vec2, lane: &str, class: PolygonClass) {
1842 if containment_trace_enabled() {
1843 eprintln!(
1844 "containment face={} uv=({:.8},{:.8}) lane={lane} -> {class:?}",
1845 face.id, point.x, point.y
1846 );
1847 }
1848}
1849
1850
1851enum Lane<'a> {
1853 SeamBand(&'a SeamBandPrep),
1854 SphereCap(&'a SphereCapPrep),
1855 Horizon(&'a HorizonPrep),
1856 RimStrip(&'a RimStripPrep),
1857 Generic,
1858}
1859
1860fn lane_from<'a>(
1868 face: &FaceRecord,
1869 prepared: &'a PreparedFace,
1870 from: usize,
1871) -> Result<(usize, Lane<'a>), String> {
1872 if from == 0 {
1873 let band = lane(&prepared.seam_band, || prepare_seam_band(face))?;
1874 if !matches!(band, SeamBandPrep::NotApplicable | SeamBandPrep::Declined) {
1875 return Ok((0, Lane::SeamBand(band)));
1876 }
1877 }
1878 if from <= 1 {
1879 if let Some(cap) = lane(&prepared.sphere_cap, || prepare_winding_sphere_cap(face))? {
1880 return Ok((1, Lane::SphereCap(cap)));
1881 }
1882 }
1883 if from <= 2 {
1884 if let Some(horizon) = lane(&prepared.horizon, || prepare_wrapped_horizon(face))? {
1885 return Ok((2, Lane::Horizon(horizon)));
1886 }
1887 }
1888 if from <= 3 {
1889 if let Some(strip) = lane(&prepared.rim_strip, || prepare_covering_rim_strip(face))? {
1890 return Ok((3, Lane::RimStrip(strip)));
1891 }
1892 }
1893 Ok((4, Lane::Generic))
1894}
1895
1896pub fn containment_lane(face: &FaceRecord) -> Result<ContainmentLane, String> {
1938 let prepared = cached_prepared_face(face).unwrap_or_else(|| std::rc::Rc::new(PreparedFace::new()));
1939 Ok(match lane_from(face, &prepared, 0)?.1 {
1940 Lane::SeamBand(_) => ContainmentLane::SeamBand,
1941 Lane::SphereCap(_) => ContainmentLane::SphereCap,
1942 Lane::Horizon(horizon) => ContainmentLane::WrappedHorizon {
1943 loops: horizon.loops.clone(),
1944 u_period: horizon.u_period,
1945 cross_frame: horizon_cross_frame(),
1946 },
1947 Lane::RimStrip(_) => ContainmentLane::CoveringRimStrip,
1948 Lane::Generic => ContainmentLane::Generic {
1949 sphere_chart: sphere_chart_atlas(face).is_some(),
1950 },
1951 })
1952}
1953
1954#[derive(Clone, Debug)]
1956pub enum ContainmentLane {
1957 SeamBand,
1959 SphereCap,
1963 WrappedHorizon {
1970 loops: Vec<Vec<Vec2>>,
1973 u_period: f64,
1974 cross_frame: bool,
1975 },
1976 CoveringRimStrip,
1978 Generic {
1984 sphere_chart: bool,
1987 },
1988}
1989
1990pub fn parameter_point_in_face(
1991 face: &FaceRecord,
1992 point: Vec2,
1993 tolerance: f64,
1994) -> Result<PolygonClass, String> {
1995 point_in_face_scanned(face, point, tolerance, ChordScan::Auto)
1996}
1997
1998#[doc(hidden)]
2003pub fn parameter_point_in_face_scan(
2004 face: &FaceRecord,
2005 point: Vec2,
2006 tolerance: f64,
2007 indexed: bool,
2008) -> Result<PolygonClass, String> {
2009 let scan = if indexed {
2010 ChordScan::Indexed
2011 } else {
2012 ChordScan::Linear
2013 };
2014 point_in_face_scanned(face, point, tolerance, scan)
2015}
2016
2017fn point_in_face_scanned(
2018 face: &FaceRecord,
2019 point: Vec2,
2020 tolerance: f64,
2021 scan: ChordScan,
2022) -> Result<PolygonClass, String> {
2023 let prepared = prepared_face(face);
2024 let mut from = 0;
2025 loop {
2026 let (position, lane) = lane_from(face, &prepared, from)?;
2027 let answer = match lane {
2028 Lane::SeamBand(band) => {
2029 seam_band_point_in_face(band, point, tolerance).map(|class| ("seam_band", class))
2030 }
2031 Lane::SphereCap(cap) => winding_sphere_cap_point_in_face(cap, point, tolerance)
2032 .map(|class| ("winding_sphere_cap", class)),
2033 Lane::Horizon(horizon) => wrapped_horizon_point_in_face(face, horizon, point, tolerance, scan)
2034 .map(|class| ("wrapped_horizon", class)),
2035 Lane::RimStrip(strip) => covering_rim_strip_point_in_face(strip, point, tolerance)
2036 .map(|class| ("covering_rim_strip", class)),
2037 Lane::Generic => break,
2038 };
2039 if let Some((name, class)) = answer {
2040 trace_lane(face, point, name, class);
2041 return Ok(class);
2042 }
2043 from = position + 1;
2044 }
2045 let generic = lane(&prepared.generic, || prepare_generic(face))?;
2046 let (crossings, boundary, nearest) = generic_scan(generic, point, tolerance, scan);
2047 if boundary {
2048 trace_lane(face, point, "polygon_boundary", PolygonClass::Boundary);
2049 return Ok(PolygonClass::Boundary);
2050 }
2051 let parity = if crossings % 2 == 1 {
2052 PolygonClass::Inside
2053 } else {
2054 PolygonClass::Outside
2055 };
2056 let chord = nearest.map(|(loop_index, index, distance)| {
2057 let polygon = &generic.loops[loop_index].polygon;
2058 let prep = generic.segments[loop_index][index];
2059 (
2060 polygon[index],
2061 polygon[(index + 1) % polygon.len()],
2062 &face.loops[loop_index].coedges[prep.coedge_index].pcurve,
2063 prep,
2064 distance,
2065 )
2066 });
2067 if containment_trace_enabled() {
2068 let (distance, length) = match chord.as_ref() {
2069 Some((start, end, _, _, distance)) => (*distance, end.sub(*start).length()),
2070 None => (f64::NAN, f64::NAN),
2071 };
2072 eprintln!(
2073 "containment face={} uv=({:.8},{:.8}) crossings={crossings} parity={parity:?} \
2074 nearest distance={distance:.3e} segment length={length:.3e}",
2075 face.id, point.x, point.y
2076 );
2077 }
2078 let parity_decides = match chord.as_ref() {
2087 None => true,
2088 Some((start, end, _, _, distance)) => {
2089 let length = end.sub(*start).length();
2090 *distance > length || length <= 0.0
2091 }
2092 };
2093 if parity_decides {
2094 if let Some(class) = sphere_chart_point_in_face(face, point, &prepared, &generic.loops)? {
2095 trace_lane(face, point, "sphere_chart", class);
2096 return Ok(class);
2097 }
2098 }
2099 let Some((segment_start, segment_end, curve, prep, distance)) = chord else {
2100 trace_lane(face, point, "parity (no segment)", parity);
2101 return Ok(parity);
2102 };
2103 let segment_length = segment_end.sub(segment_start).length();
2104 if distance > segment_length || segment_length <= 0.0 {
2105 trace_lane(face, point, "parity (far from every segment)", parity);
2106 return Ok(parity);
2107 }
2108 let chord_parameter = prep.parameter_start
2109 + (prep.parameter_end - prep.parameter_start)
2110 * (point.sub(segment_start).dot(segment_end.sub(segment_start))
2111 / (segment_length * segment_length))
2112 .clamp(0.0, 1.0);
2113 let [domain_start, domain_end] = curve.domain()?;
2114 let mut parameter = chord_parameter;
2115 for _ in 0..12 {
2116 let derivatives = curve.derivatives(parameter, 2)?;
2117 let on_curve = Vec2 {
2118 x: derivatives[0].x,
2119 y: derivatives[0].y,
2120 };
2121 let tangent = Vec2 {
2122 x: derivatives[1].x,
2123 y: derivatives[1].y,
2124 };
2125 let second = Vec2 {
2126 x: derivatives[2].x,
2127 y: derivatives[2].y,
2128 };
2129 let residual = on_curve.sub(point);
2130 let denominator = tangent.dot(tangent) + residual.dot(second);
2131 if denominator.abs() < 1e-30 {
2132 break;
2133 }
2134 let step = -residual.dot(tangent) / denominator;
2135 parameter = (parameter + step).clamp(domain_start, domain_end);
2136 if step.abs() < 1e-14 * (domain_end - domain_start + 1.0) {
2137 break;
2138 }
2139 }
2140 let margin = 1e-9 * (domain_end - domain_start);
2141 if parameter > domain_start + margin && parameter < domain_end - margin {
2142 let derivatives = curve.derivatives(parameter, 1)?;
2143 let on_curve = Vec2 {
2144 x: derivatives[0].x,
2145 y: derivatives[0].y,
2146 };
2147 let tangent = Vec2 {
2148 x: derivatives[1].x,
2149 y: derivatives[1].y,
2150 };
2151 let offset = point.sub(on_curve);
2152 if offset.length() <= tolerance {
2153 trace_lane(face, point, "nearest curve (on it)", PolygonClass::Boundary);
2154 return Ok(PolygonClass::Boundary);
2155 }
2156 let cross = tangent.x * offset.y - tangent.y * offset.x;
2157 if cross.abs() > 1e-30 {
2158 let class = if (cross > 0.0) == face.same_sense {
2159 PolygonClass::Inside
2160 } else {
2161 PolygonClass::Outside
2162 };
2163 trace_lane(face, point, "nearest curve (side)", class);
2164 return Ok(class);
2165 }
2166 }
2167 trace_lane(face, point, "parity (nearest curve declined)", parity);
2168 Ok(parity)
2169}