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
147pub(crate) fn 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(|| {
597 let started = containment_profile_started();
598 let built = ChordIndex::build(generic.loops.as_slice());
599 let ms = containment_elapsed_ms(started);
600 containment_profile(|p| {
601 p.index_builds += 1;
602 p.index_ms += ms;
603 });
604 built
605 }) {
606 let loops = generic.loops.as_slice();
607 let nearest = index.nearest(loops, point);
608 let boundary = nearest.is_some_and(|(_, _, distance)| distance <= tolerance);
609 let crossings = if boundary {
610 0
611 } else {
612 let mut odd = vec![false; loops.len()];
613 index.flip_crossings(loops, point, &mut odd);
614 odd.into_iter().filter(|&parity| parity).count()
615 };
616 return (crossings, boundary, nearest);
617 }
618 }
619 linear_scan(generic, point, tolerance)
620}
621
622struct PreparedFace {
629 seam_band: std::cell::OnceCell<Result<SeamBandPrep, String>>,
630 sphere_cap: std::cell::OnceCell<Result<Option<SphereCapPrep>, String>>,
631 horizon: std::cell::OnceCell<Result<Option<HorizonPrep>, String>>,
632 rim_strip: std::cell::OnceCell<Result<Option<RimStripPrep>, String>>,
633 generic: std::cell::OnceCell<Result<GenericPrep, String>>,
634 sphere_region: std::cell::OnceCell<Result<crate::sphere_chart::SphericalRegion, String>>,
636 loop_samples: std::cell::RefCell<Vec<Option<Result<std::rc::Rc<Vec<Vec2>>, String>>>>,
641}
642
643impl PreparedFace {
644 fn new() -> Self {
645 Self {
646 seam_band: std::cell::OnceCell::new(),
647 sphere_cap: std::cell::OnceCell::new(),
648 horizon: std::cell::OnceCell::new(),
649 rim_strip: std::cell::OnceCell::new(),
650 generic: std::cell::OnceCell::new(),
651 sphere_region: std::cell::OnceCell::new(),
652 loop_samples: std::cell::RefCell::new(Vec::new()),
653 }
654 }
655}
656
657fn loop_samples(prepared: &PreparedFace, face: &FaceRecord, loop_index: usize) -> Result<std::rc::Rc<Vec<Vec2>>, String> {
667 if let Some(Some(sampled)) = prepared.loop_samples.borrow().get(loop_index) {
668 return sampled.clone();
669 }
670 let sampled = (|| {
671 let mut points: Vec<Vec2> = Vec::new();
672 for coedge in &face.loops[loop_index].coedges {
673 let curve = &coedge.pcurve;
674 let [start, end] = curve.domain()?;
675 let sample_count = trim_sample_count(curve);
676 for index in 0..sample_count {
677 let parameter = trim_station(start, end, index, sample_count);
678 let evaluated = curve.evaluate(parameter)?;
679 points.push(Vec2 {
680 x: evaluated.x,
681 y: evaluated.y,
682 });
683 }
684 }
685 Ok(std::rc::Rc::new(points))
686 })();
687 let mut cells = prepared.loop_samples.borrow_mut();
688 if cells.len() < face.loops.len() {
689 cells.resize(face.loops.len(), None);
690 }
691 cells[loop_index] = Some(sampled.clone());
692 sampled
693}
694
695#[derive(Default, Clone, Copy)]
705struct ContainmentProfile {
706 enabled: bool,
707 cache_misses: u64,
708 seam_band_preps: u64,
709 seam_band_ms: f64,
710 sphere_cap_preps: u64,
711 sphere_cap_ms: f64,
712 horizon_preps: u64,
713 horizon_ms: f64,
714 rim_strip_preps: u64,
715 rim_strip_ms: f64,
716 generic_preps: u64,
717 generic_ms: f64,
718 generic_chords: u64,
719 index_builds: u64,
720 index_ms: f64,
721 region_builds: u64,
722 region_evaluate_ms: f64,
723 region_canonical_ms: f64,
724 region_segments_ms: f64,
725}
726
727thread_local! {
728 static CONTAINMENT_PROFILE: std::cell::Cell<ContainmentProfile> =
729 const { std::cell::Cell::new(ContainmentProfile {
730 enabled: false, cache_misses: 0, seam_band_preps: 0, seam_band_ms: 0.0,
731 sphere_cap_preps: 0, sphere_cap_ms: 0.0, horizon_preps: 0, horizon_ms: 0.0,
732 rim_strip_preps: 0, rim_strip_ms: 0.0, generic_preps: 0, generic_ms: 0.0,
733 generic_chords: 0, index_builds: 0, index_ms: 0.0, region_builds: 0,
734 region_evaluate_ms: 0.0, region_canonical_ms: 0.0, region_segments_ms: 0.0,
735 }) };
736}
737
738fn containment_profile(update: impl FnOnce(&mut ContainmentProfile)) {
739 CONTAINMENT_PROFILE.with(|cell| {
740 let mut profile = cell.get();
741 if profile.enabled {
742 update(&mut profile);
743 cell.set(profile);
744 }
745 });
746}
747
748fn containment_profile_started() -> Option<web_time::Instant> {
749 CONTAINMENT_PROFILE
750 .with(|cell| cell.get().enabled)
751 .then(web_time::Instant::now)
752}
753
754fn containment_elapsed_ms(started: Option<web_time::Instant>) -> f64 {
755 started.map_or(0.0, |s| s.elapsed().as_secs_f64() * 1_000.0)
756}
757
758pub(crate) fn containment_profile_begin() {
760 let enabled = std::env::var("BREP_PROFILE").is_ok();
761 CONTAINMENT_PROFILE.with(|cell| {
762 cell.set(ContainmentProfile {
763 enabled,
764 ..ContainmentProfile::default()
765 })
766 });
767}
768
769pub(crate) fn containment_profile_report(label: &str) {
772 let p = CONTAINMENT_PROFILE.with(|cell| cell.get());
773 if !p.enabled {
774 return;
775 }
776 eprintln!(
777 "containment.profile {label} cache_misses={} seam_band: preps={} ms={:.2} sphere_cap: preps={} ms={:.2} horizon: preps={} ms={:.2} rim_strip: preps={} ms={:.2} generic: preps={} ms={:.2} chords={} index: builds={} ms={:.2} region: builds={} evaluate_ms={:.2} canonical_ms={:.2} segments_ms={:.2}",
778 p.cache_misses, p.seam_band_preps, p.seam_band_ms, p.sphere_cap_preps, p.sphere_cap_ms,
779 p.horizon_preps, p.horizon_ms, p.rim_strip_preps, p.rim_strip_ms, p.generic_preps,
780 p.generic_ms, p.generic_chords, p.index_builds, p.index_ms, p.region_builds,
781 p.region_evaluate_ms, p.region_canonical_ms, p.region_segments_ms,
782 );
783 CONTAINMENT_PROFILE.with(|cell| cell.set(ContainmentProfile::default()));
784}
785
786fn timed_lane<'a, T>(
789 cell: &'a std::cell::OnceCell<Result<T, String>>,
790 build: impl FnOnce() -> Result<T, String>,
791 record: fn(&mut ContainmentProfile, f64),
792) -> Result<&'a T, String> {
793 lane(cell, || {
794 let started = containment_profile_started();
795 let built = build();
796 let ms = containment_elapsed_ms(started);
797 containment_profile(|p| record(p, ms));
798 built
799 })
800}
801
802fn lane<'a, T>(
803 cell: &'a std::cell::OnceCell<Result<T, String>>,
804 build: impl FnOnce() -> Result<T, String>,
805) -> Result<&'a T, String> {
806 match cell.get_or_init(build) {
807 Ok(value) => Ok(value),
808 Err(error) => Err(error.clone()),
809 }
810}
811
812struct PreparedFaceCache {
830 entries: Vec<(u64, Vec<f64>, std::rc::Rc<PreparedFace>)>,
831}
832
833const PREPARED_FACE_CACHE_ENTRIES: usize = 64;
834
835thread_local! {
836 static PREPARED_FACES: std::cell::RefCell<PreparedFaceCache> = const {
837 std::cell::RefCell::new(PreparedFaceCache {
838 entries: Vec::new(),
839 })
840 };
841}
842
843fn walk_face_signature<F: FnMut(f64) -> bool>(face: &FaceRecord, switches: f64, visit: &mut F) -> bool {
848 let surface = &face.surface;
849 let point = |visit: &mut F, p: &crate::Vec4| visit(p.x) && visit(p.y) && visit(p.z) && visit(p.w);
850 if !(visit(switches)
851 && visit(if face.same_sense { 1.0 } else { 0.0 })
852 && visit(surface.degree_u as f64)
853 && visit(surface.degree_v as f64)
854 && visit(surface.knots_u.len() as f64))
855 {
856 return false;
857 }
858 if !(surface.knots_u.iter().all(|&knot| visit(knot)) && visit(surface.knots_v.len() as f64)) {
859 return false;
860 }
861 if !(surface.knots_v.iter().all(|&knot| visit(knot)) && visit(surface.control_points.len() as f64)) {
862 return false;
863 }
864 for row in &surface.control_points {
865 if !(visit(row.len() as f64) && row.iter().all(|p| point(visit, p))) {
866 return false;
867 }
868 }
869 if !visit(face.loops.len() as f64) {
870 return false;
871 }
872 for loop_record in &face.loops {
873 if !visit(loop_record.coedges.len() as f64) {
874 return false;
875 }
876 for coedge in &loop_record.coedges {
877 let curve = &coedge.pcurve;
878 if !(visit(coedge.edge_id as f64)
879 && visit(if coedge.forward { 1.0 } else { 0.0 })
880 && visit(curve.degree as f64)
881 && visit(curve.knots.len() as f64))
882 {
883 return false;
884 }
885 if !(curve.knots.iter().all(|&knot| visit(knot)) && visit(curve.control_points.len() as f64)) {
886 return false;
887 }
888 if !curve.control_points.iter().all(|p| point(visit, p)) {
889 return false;
890 }
891 }
892 }
893 true
894}
895
896fn face_key_hash(face: &FaceRecord, switches: f64) -> u64 {
901 use std::hash::Hasher;
902 let mut hasher = rustc_hash::FxHasher::default();
903 let mut write = |value: f64| hasher.write_u64(value.to_bits());
904 let surface = &face.surface;
905 write(switches);
906 write(if face.same_sense { 1.0 } else { 0.0 });
907 write(surface.degree_u as f64);
908 write(surface.degree_v as f64);
909 write(surface.knots_u.len() as f64);
910 write(surface.knots_v.len() as f64);
911 write(surface.control_points.len() as f64);
912 let corners = [surface.control_points.first().and_then(|row| row.first()), surface.control_points.last().and_then(|row| row.last())];
913 for p in corners.into_iter().flatten() {
914 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
915 }
916 write(face.loops.len() as f64);
917 for loop_record in &face.loops {
918 write(loop_record.coedges.len() as f64);
919 for coedge in &loop_record.coedges {
920 let curve = &coedge.pcurve;
921 write(coedge.edge_id as f64);
922 write(if coedge.forward { 1.0 } else { 0.0 });
923 write(curve.degree as f64);
924 write(curve.knots.len() as f64);
925 write(curve.control_points.len() as f64);
926 for knot in [curve.knots.first(), curve.knots.last()].into_iter().flatten() {
927 write(*knot);
928 }
929 for p in [curve.control_points.first(), curve.control_points.last()].into_iter().flatten() {
930 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
931 }
932 }
933 }
934 hasher.finish()
935}
936
937fn cached_entry(
939 entries: &[(u64, Vec<f64>, std::rc::Rc<PreparedFace>)],
940 face: &FaceRecord,
941 switches: f64,
942 hash: u64,
943) -> Option<usize> {
944 entries.iter().position(|(entry_hash, signature, _)| {
945 if *entry_hash != hash {
946 return false;
947 }
948 let mut at = 0;
949 let walked = walk_face_signature(face, switches, &mut |value: f64| {
950 let same = signature
951 .get(at)
952 .is_some_and(|stored| stored.to_bits() == value.to_bits() && !value.is_nan());
953 at += 1;
954 same
955 });
956 walked && at == signature.len()
957 })
958}
959
960pub(crate) fn forget_prepared_faces() {
963 PREPARED_FACES.with(|cache| cache.borrow_mut().entries.clear());
964}
965
966fn cached_prepared_face(face: &FaceRecord) -> Option<std::rc::Rc<PreparedFace>> {
970 PREPARED_FACES.with(|cache| {
971 let PreparedFaceCache { entries } = &*cache.borrow();
972 let switches = lane_switches();
973 let hash = face_key_hash(face, switches);
974 cached_entry(entries, face, switches, hash).map(|index| entries[index].2.clone())
975 })
976}
977
978fn prepared_face(face: &FaceRecord) -> std::rc::Rc<PreparedFace> {
979 PREPARED_FACES.with(|cache| {
980 let PreparedFaceCache { entries } = &mut *cache.borrow_mut();
981 let switches = lane_switches();
982 let hash = face_key_hash(face, switches);
983 if let Some(index) = cached_entry(entries, face, switches, hash) {
984 if index != 0 {
985 let entry = entries.remove(index);
986 entries.insert(0, entry);
987 }
988 return entries[0].2.clone();
989 }
990 let mut signature = Vec::new();
991 walk_face_signature(face, switches, &mut |value: f64| {
992 signature.push(value);
993 true
994 });
995 let prepared = std::rc::Rc::new(PreparedFace::new());
996 entries.insert(0, (hash, signature, prepared.clone()));
997 entries.truncate(PREPARED_FACE_CACHE_ENTRIES);
998 containment_profile(|p| p.cache_misses += 1);
999 prepared
1000 })
1001}
1002
1003fn prepare_seam_band(face: &FaceRecord) -> Result<SeamBandPrep, String> {
1012 if face.surface.closed_directions()? != (true, true) {
1013 return Ok(SeamBandPrep::NotApplicable);
1014 }
1015 let [u0, u1] = face.surface.domain_u()?;
1016 let [v0, v1] = face.surface.domain_v()?;
1017 if crate::topology::doubly_periodic_has_only_collapsed_loops(face)? {
1018 return Ok(SeamBandPrep::AlwaysInside);
1019 }
1020 let u_span = (u1 - u0).abs().max(1e-30);
1021 let v_span = (v1 - v0).abs().max(1e-30);
1022 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
1023 let mut retained: Vec<usize> = Vec::with_capacity(face.loops.len());
1027 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1028 let mut points: Vec<[f64; 2]> = Vec::new();
1029 for coedge in &loop_record.coedges {
1030 let [d0, d1] = coedge.pcurve.domain()?;
1031 let samples = 24;
1032 for k in 0..=samples {
1033 let t = trim_station(d0, d1, k, samples);
1034 let p = coedge.pcurve.evaluate(t)?;
1035 points.push([p.x, p.y]);
1036 }
1037 }
1038 let (mut umin, mut umax, mut vmin, mut vmax) = (
1039 f64::INFINITY,
1040 f64::NEG_INFINITY,
1041 f64::INFINITY,
1042 f64::NEG_INFINITY,
1043 );
1044 for p in &points {
1045 umin = umin.min(p[0]);
1046 umax = umax.max(p[0]);
1047 vmin = vmin.min(p[1]);
1048 vmax = vmax.max(p[1]);
1049 }
1050 if (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span {
1070 continue;
1071 }
1072 retained.push(loop_index);
1073 loops_uv.push(points);
1074 }
1075 if loops_uv.len() == 1 && !switched_off(SEAM_BAND_MERGED_SWITCH) {
1086 let coedges = &face.loops[retained[0]].coedges;
1087 let offsets = crate::topology::loop_seam_offsets(coedges, true, true, u_span, v_span)?;
1088 if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
1089 let mut polygon: Vec<Vec2> = Vec::new();
1090 for (coedge_index, coedge) in coedges.iter().enumerate() {
1091 let [d0, d1] = coedge.pcurve.domain()?;
1092 let samples = 24;
1093 for k in 0..samples {
1094 let t = trim_station(d0, d1, k, samples);
1095 let p = coedge.pcurve.evaluate(t)?;
1096 polygon.push(Vec2 {
1097 x: p.x + offsets[coedge_index][0],
1098 y: p.y + offsets[coedge_index][1],
1099 });
1100 }
1101 }
1102 return Ok(SeamBandPrep::Merged { polygon, u_span, v_span });
1103 }
1104 }
1105 if loops_uv.len() != 2 {
1106 return Ok(SeamBandPrep::Declined);
1107 }
1108 if let Some(band) = crate::topology::analyze_doubly_periodic_seam_band(
1109 &loops_uv,
1110 [u0, u1, v0, v1],
1111 face.same_sense,
1112 ) {
1113 let polygon: Vec<Vec2> =
1114 crate::topology::seam_band_uv_polygon(&loops_uv, [u0, u1, v0, v1], &band)
1115 .into_iter()
1116 .map(|p| Vec2 { x: p[0], y: p[1] })
1117 .collect();
1118 return Ok(SeamBandPrep::Analyzed { polygon });
1119 }
1120 for p_is_u in [true, false] {
1127 let (period, q_extent) = if p_is_u {
1128 (u1 - u0, v_span)
1129 } else {
1130 (v1 - v0, u_span)
1131 };
1132 if !(period > 0.0) {
1133 continue;
1134 }
1135 let coord = |p: &[f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
1136 let mut rings = Vec::with_capacity(2);
1137 for points in &loops_uv {
1138 let (mut pmin, mut pmax, mut qmin, mut qmax) = (
1139 f64::INFINITY,
1140 f64::NEG_INFINITY,
1141 f64::INFINITY,
1142 f64::NEG_INFINITY,
1143 );
1144 for p in points {
1145 let (periodic, cross) = coord(p);
1146 pmin = pmin.min(periodic);
1147 pmax = pmax.max(periodic);
1148 qmin = qmin.min(cross);
1149 qmax = qmax.max(cross);
1150 }
1151 if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
1152 rings.clear();
1153 break;
1154 }
1155 let mut net = 0.0;
1156 for pair in points.windows(2) {
1157 let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
1158 if delta > 0.5 * period {
1159 delta -= period;
1160 } else if delta < -0.5 * period {
1161 delta += period;
1162 }
1163 net += delta;
1164 }
1165 let direction = if net > 0.25 * period {
1166 1
1167 } else if net < -0.25 * period {
1168 -1
1169 } else {
1170 0
1171 };
1172 rings.push((0.5 * (qmin + qmax), direction));
1173 }
1174 if rings.len() != 2 {
1175 continue;
1176 }
1177 rings.sort_by(|a, b| a.0.total_cmp(&b.0));
1178 let [(q_lo, lower_direction), (q_hi, upper_direction)] = rings.as_slice() else {
1179 unreachable!()
1180 };
1181 let inconclusive =
1182 *lower_direction == 0 || *upper_direction == 0 || lower_direction == upper_direction;
1183 let between_is_ccw_uv = if p_is_u {
1184 *lower_direction > 0
1185 } else {
1186 *lower_direction < 0
1187 };
1188 let complement = !inconclusive && between_is_ccw_uv != face.same_sense;
1189 return Ok(SeamBandPrep::Rings {
1190 p_is_u,
1191 q_lo: *q_lo,
1192 q_hi: *q_hi,
1193 complement,
1194 });
1195 }
1196 Ok(SeamBandPrep::Declined)
1197}
1198
1199fn seam_band_point_in_face(
1200 prepared: &SeamBandPrep,
1201 point: Vec2,
1202 tolerance: f64,
1203) -> Option<PolygonClass> {
1204 match prepared {
1205 SeamBandPrep::NotApplicable | SeamBandPrep::Declined => None,
1206 SeamBandPrep::AlwaysInside => Some(PolygonClass::Inside),
1207 SeamBandPrep::Merged { polygon, u_span, v_span } => {
1208 let mut best = PolygonClass::Outside;
1209 'images: for du in [-1.0, 0.0, 1.0] {
1210 for dv in [-1.0, 0.0, 1.0] {
1211 let image = Vec2 {
1212 x: point.x + du * u_span,
1213 y: point.y + dv * v_span,
1214 };
1215 match point_in_polygon(image, polygon, tolerance) {
1216 PolygonClass::Boundary => {
1217 best = PolygonClass::Boundary;
1218 break 'images;
1219 }
1220 PolygonClass::Inside => best = PolygonClass::Inside,
1221 PolygonClass::Outside => {}
1222 }
1223 }
1224 }
1225 Some(best)
1226 }
1227 SeamBandPrep::Analyzed { polygon } => Some(point_in_polygon(point, polygon, tolerance)),
1228 SeamBandPrep::Rings { p_is_u, q_lo, q_hi, complement } => {
1229 let q = if *p_is_u { point.y } else { point.x };
1230 if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
1231 return Some(PolygonClass::Boundary);
1232 }
1233 let between = q > *q_lo && q < *q_hi;
1234 Some(if between != *complement {
1235 PolygonClass::Inside
1236 } else {
1237 PolygonClass::Outside
1238 })
1239 }
1240 }
1241}
1242
1243fn prepare_wrapped_horizon(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<HorizonPrep>, String> {
1259 if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
1260 return Ok(None);
1261 }
1262 if switched_off(HORIZON_CONTAINMENT_SWITCH) {
1263 return Ok(None);
1264 }
1265 if face.loops.len() <= 2 && switched_off(HORIZON_SINGLE_LOOP_SWITCH) {
1276 return Ok(None);
1277 }
1278 let [u0, u1] = face.surface.domain_u()?;
1279 let u_period = u1 - u0;
1280 if u_period <= 0.0 {
1281 return Ok(None);
1282 }
1283 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1284 let mut loops: Vec<Vec<Vec2>> = Vec::new();
1285 let mut straddling = false;
1286 for loop_index in 0..face.loops.len() {
1287 let points = loop_samples(prepared, face, loop_index)?;
1288 if points.len() < 3 {
1289 continue;
1290 }
1291 let mut unwrapped = Vec::with_capacity(points.len());
1295 let mut jumps = 0usize;
1296 let mut cursor = points[0];
1297 unwrapped.push(cursor);
1298 for pair in points.windows(2) {
1299 let mut du = pair[1].x - pair[0].x;
1300 let folded = du - u_period * (du / u_period).round();
1301 if (du - folded).abs() > 0.25 * u_period {
1302 jumps += 1;
1303 }
1304 du = folded;
1305 cursor = Vec2 {
1306 x: cursor.x + du,
1307 y: pair[1].y,
1308 };
1309 unwrapped.push(cursor);
1310 }
1311 let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
1312 if closure > 0.25 * u_period {
1313 if debug {
1314 eprintln!(
1315 "horizon: face {} loop winds the period (closure {closure:.3}) — bail",
1316 face.id
1317 );
1318 }
1319 return Ok(None); }
1321 if jumps > 0 {
1322 straddling = true;
1323 }
1324 loops.push(unwrapped);
1325 }
1326 if !straddling || loops.is_empty() {
1327 return Ok(None);
1328 }
1329 Ok(Some(HorizonPrep {
1330 loops,
1331 u_period,
1332 index: std::cell::OnceCell::new(),
1333 }))
1334}
1335
1336fn wrapped_horizon_point_in_face(
1337 face: &FaceRecord,
1338 prepared: &HorizonPrep,
1339 point: Vec2,
1340 tolerance: f64,
1341 scan: ChordScan,
1342) -> Option<PolygonClass> {
1343 let HorizonPrep { loops, u_period, index } = prepared;
1344 let u_period = *u_period;
1345 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1346 if !horizon_cross_frame() {
1361 let mut best: Option<PolygonClass> = None;
1362 for shift in [-u_period, 0.0, u_period] {
1363 let image = Vec2 {
1364 x: point.x + shift,
1365 y: point.y,
1366 };
1367 let mut crossings = 0usize;
1368 for polygon in loops {
1369 match point_in_polygon(image, polygon, tolerance) {
1370 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1371 PolygonClass::Inside => crossings += 1,
1372 PolygonClass::Outside => {}
1373 }
1374 }
1375 if crossings % 2 == 1 {
1376 best = Some(PolygonClass::Inside);
1377 } else if best.is_none() {
1378 best = Some(PolygonClass::Outside);
1379 }
1380 }
1381 if debug {
1382 eprintln!(
1383 "horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
1384 face.id,
1385 loops.len(),
1386 best
1387 );
1388 }
1389 return best;
1390 }
1391 let images = [-u_period, 0.0, u_period].map(|shift| Vec2 {
1392 x: point.x + shift,
1393 y: point.y,
1394 });
1395 let chords: usize = loops.iter().map(Vec::len).sum();
1396 let indexed = (scan.indexes(chords) && images.iter().all(|image| in_index_range(*image)))
1397 .then(|| index.get_or_init(|| ChordIndex::build(loops.as_slice())).as_ref())
1398 .flatten();
1399 let mut crossings = 0usize;
1400 if let Some(index) = indexed {
1401 let loops = loops.as_slice();
1405 if images.iter().any(|image| index.any_within(loops, *image, tolerance)) {
1406 return Some(PolygonClass::Boundary);
1407 }
1408 let mut odd = vec![false; loops.len()];
1409 for image in images {
1410 index.flip_crossings(loops, image, &mut odd);
1411 }
1412 crossings = odd.into_iter().filter(|&parity| parity).count();
1413 } else {
1414 for polygon in loops {
1415 let mut image_hits = 0usize;
1416 for image in images {
1417 match point_in_polygon(image, polygon, tolerance) {
1418 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1419 PolygonClass::Inside => image_hits += 1,
1420 PolygonClass::Outside => {}
1421 }
1422 }
1423 crossings += image_hits % 2;
1424 }
1425 }
1426 let class = if crossings % 2 == 1 {
1427 PolygonClass::Inside
1428 } else {
1429 PolygonClass::Outside
1430 };
1431 if debug {
1432 eprintln!(
1433 "horizon: face {} loops={} straddling -> {:?}",
1434 face.id,
1435 loops.len(),
1436 class
1437 );
1438 }
1439 Some(class)
1440}
1441
1442fn prepare_winding_sphere_cap(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<SphereCapPrep>, String> {
1447 if !matches!(
1448 face.surface.analytic(),
1449 Some(crate::AnalyticSurface::Sphere { .. })
1450 ) || face.surface.closed_directions()? != (true, false)
1451 || face.loops.len() != 2
1452 {
1453 return Ok(None);
1454 }
1455 let [u0, u1] = face.surface.domain_u()?;
1456 let [v0, v1] = face.surface.domain_v()?;
1457 let period = u1 - u0;
1458 let v_span = v1 - v0;
1459 if !(period > 0.0 && v_span > 0.0) {
1460 return Ok(None);
1461 }
1462 struct WindingLoop {
1463 points: Vec<Vec2>,
1464 winding: f64,
1465 vmin: f64,
1466 vmax: f64,
1467 }
1468 let mut loops = Vec::with_capacity(2);
1469 for loop_index in 0..face.loops.len() {
1470 let points = loop_samples(prepared, face, loop_index)?;
1471 if points.len() < 2 {
1472 return Ok(None);
1473 }
1474 let first = points[0];
1475 let mut cursor = first;
1476 let mut unwrapped = vec![cursor];
1477 for next in points.iter().skip(1) {
1478 let du = next.x - cursor.x;
1479 let folded = du - period * (du / period).round();
1480 cursor = Vec2 {
1481 x: cursor.x + folded,
1482 y: next.y,
1483 };
1484 unwrapped.push(cursor);
1485 }
1486 let last_raw = *points.last().unwrap();
1487 let closing_du = first.x - last_raw.x;
1488 let closing_folded = closing_du - period * (closing_du / period).round();
1489 let winding = cursor.x + closing_folded - first.x;
1490 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1491 for p in &unwrapped {
1492 vmin = vmin.min(p.y);
1493 vmax = vmax.max(p.y);
1494 }
1495 loops.push(WindingLoop {
1496 points: unwrapped,
1497 winding,
1498 vmin,
1499 vmax,
1500 });
1501 }
1502 if loops
1503 .iter()
1504 .any(|loop_data| (loop_data.winding.abs() - period).abs() > 0.05 * period)
1505 || loops[0].winding * loops[1].winding >= 0.0
1506 {
1507 return Ok(None);
1508 }
1509 let flat = |loop_data: &WindingLoop| loop_data.vmax - loop_data.vmin <= 1e-6 * v_span;
1510 let (pole, rim) = match (flat(&loops[0]), flat(&loops[1])) {
1511 (true, false) => (&loops[0], &loops[1]),
1512 (false, true) => (&loops[1], &loops[0]),
1513 _ => return Ok(None),
1514 };
1515 let pole_v = 0.5 * (pole.vmin + pole.vmax);
1516 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
1517 return Ok(None);
1518 }
1519 Ok(Some(SphereCapPrep {
1520 rim: rim.points.clone(),
1521 pole_v,
1522 period,
1523 }))
1524}
1525
1526fn winding_sphere_cap_point_in_face(
1527 prepared: &SphereCapPrep,
1528 point: Vec2,
1529 tolerance: f64,
1530) -> Option<PolygonClass> {
1531 let SphereCapPrep { rim, pole_v, period } = prepared;
1532 let (pole_v, period) = (*pole_v, *period);
1533 let window_start = rim[0].x.min(rim[rim.len() - 1].x);
1534 let query_u = window_start + (point.x - window_start).rem_euclid(period);
1535 let mut crossings = Vec::new();
1536 for pair in rim.windows(2) {
1537 let (a, b) = (pair[0], pair[1]);
1538 if (a.x > query_u) != (b.x > query_u) {
1539 crossings.push(a.y + (query_u - a.x) / (b.x - a.x) * (b.y - a.y));
1540 }
1541 for image in [-period, 0.0, period] {
1542 if point_segment_distance(
1543 Vec2 {
1544 x: point.x + image,
1545 y: point.y,
1546 },
1547 a,
1548 b,
1549 ) <= tolerance
1550 {
1551 return Some(PolygonClass::Boundary);
1552 }
1553 }
1554 }
1555 let rim_v = crossings
1556 .into_iter()
1557 .min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))?;
1558 let inside = if pole_v < rim_v {
1559 point.y <= rim_v + tolerance
1560 } else {
1561 point.y >= rim_v - tolerance
1562 };
1563 Some(if inside {
1564 PolygonClass::Inside
1565 } else {
1566 PolygonClass::Outside
1567 })
1568}
1569
1570fn prepare_covering_rim_strip(face: &FaceRecord) -> Result<Option<RimStripPrep>, String> {
1602 if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
1603 return Ok(None);
1604 }
1605 if switched_off(COVERING_RIM_STRIP_SWITCH) {
1606 return Ok(None);
1607 }
1608 let [u0, u1] = face.surface.domain_u()?;
1609 let [v0, v1] = face.surface.domain_v()?;
1610 let period = u1 - u0;
1611 if !(period > 0.0) {
1612 return Ok(None);
1613 }
1614 let v_span = (v1 - v0).abs().max(1e-30);
1615 struct Rim {
1616 points: Vec<Vec2>,
1617 vmin: f64,
1618 vmax: f64,
1619 ascending: bool,
1620 }
1621 let mut rims: Vec<Rim> = Vec::with_capacity(2);
1622 let mut out_of_domain = false;
1623 for loop_record in &face.loops {
1624 let mut points: Vec<Vec2> = Vec::new();
1625 for coedge in &loop_record.coedges {
1626 let curve = &coedge.pcurve;
1627 let [start, end] = curve.domain()?;
1628 let sample_count = trim_sample_count(curve);
1629 for index in 0..=sample_count {
1630 let parameter = trim_station(start, end, index, sample_count);
1631 let evaluated = curve.evaluate(parameter)?;
1632 points.push(Vec2 {
1633 x: evaluated.x,
1634 y: evaluated.y,
1635 });
1636 }
1637 }
1638 if points.len() < 3 {
1639 return Ok(None);
1640 }
1641 for p in &points {
1644 if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
1645 out_of_domain = true;
1646 }
1647 }
1648 let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
1652 let mut cursor = points[0];
1653 unwrapped.push(cursor);
1654 for pair in points.windows(2) {
1655 let du = pair[1].x - pair[0].x;
1656 let folded = du - period * (du / period).round();
1657 cursor = Vec2 {
1658 x: cursor.x + folded,
1659 y: pair[1].y,
1660 };
1661 unwrapped.push(cursor);
1662 }
1663 let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
1664 if (net.abs() - period).abs() > 2e-2 * period {
1665 return Ok(None); }
1667 if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
1668 return Ok(None); }
1670 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1671 for p in &unwrapped {
1672 vmin = vmin.min(p.y);
1673 vmax = vmax.max(p.y);
1674 }
1675 rims.push(Rim {
1676 points: unwrapped,
1677 vmin,
1678 vmax,
1679 ascending: net > 0.0,
1680 });
1681 }
1682 if !out_of_domain {
1683 return Ok(None);
1684 }
1685 if rims[0].ascending == rims[1].ascending {
1686 return Ok(None); }
1688 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
1689 (&rims[0], &rims[1])
1690 } else if rims[1].vmax <= rims[0].vmin {
1691 (&rims[1], &rims[0])
1692 } else {
1693 return Ok(None); };
1695 if upper.vmin - lower.vmax <= 1e-6 * v_span {
1696 return Ok(None);
1697 }
1698 if (lower.ascending) != face.same_sense {
1703 return Ok(None);
1704 }
1705 Ok(Some(RimStripPrep {
1706 lower: lower.points.clone(),
1707 upper: upper.points.clone(),
1708 period,
1709 }))
1710}
1711
1712fn covering_rim_strip_point_in_face(
1713 prepared: &RimStripPrep,
1714 point: Vec2,
1715 tolerance: f64,
1716) -> Option<PolygonClass> {
1717 let RimStripPrep { lower, upper, period } = prepared;
1718 let period = *period;
1719 for rim in [lower, upper] {
1721 for shift in [-period, 0.0, period] {
1722 let image = Vec2 {
1723 x: point.x + shift,
1724 y: point.y,
1725 };
1726 for pair in rim.windows(2) {
1727 if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
1728 return Some(PolygonClass::Boundary);
1729 }
1730 }
1731 }
1732 }
1733 let mut crossings = 0usize;
1736 for rim in [lower, upper] {
1737 let window_base = rim[0].x.min(rim[rim.len() - 1].x);
1738 let x = window_base + (point.x - window_base).rem_euclid(period);
1739 for pair in rim.windows(2) {
1740 let (a, b) = (pair[0], pair[1]);
1741 if (a.x > x) != (b.x > x) {
1742 let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
1743 if v_cross > point.y {
1744 crossings += 1;
1745 }
1746 }
1747 }
1748 }
1749 Some(if crossings % 2 == 1 {
1750 PolygonClass::Inside
1751 } else {
1752 PolygonClass::Outside
1753 })
1754}
1755
1756fn prepare_generic(face: &FaceRecord, prepared: &PreparedFace) -> Result<GenericPrep, String> {
1760 let mut loops: Vec<LoopSamples> = Vec::with_capacity(face.loops.len());
1761 let mut segments: Vec<Vec<SegmentPrep>> = Vec::with_capacity(face.loops.len());
1762 let mut chord_count = 0;
1763 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1764 let polygon = loop_samples(prepared, face, loop_index)?;
1765 let mut chords = Vec::with_capacity(polygon.len());
1766 let mut coedges = Vec::with_capacity(loop_record.coedges.len());
1767 let mut first = 0;
1768 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
1769 let curve = &coedge.pcurve;
1770 let [start, end] = curve.domain()?;
1771 let sample_count = trim_sample_count(curve);
1772 for index in 0..sample_count {
1773 let parameter = trim_station(start, end, index, sample_count);
1774 let parameter_end = if index + 1 < sample_count {
1775 trim_station(start, end, index + 1, sample_count)
1776 } else {
1777 end
1778 };
1779 chords.push(SegmentPrep {
1780 coedge_index,
1781 parameter_start: parameter,
1782 parameter_end,
1783 });
1784 }
1785 coedges.push((coedge.edge_id, first, sample_count));
1786 first += sample_count;
1787 }
1788 debug_assert_eq!(first, polygon.len());
1789 chord_count += chords.len();
1790 loops.push(LoopSamples { polygon, coedges });
1791 segments.push(chords);
1792 }
1793 containment_profile(|p| p.generic_chords += chord_count as u64);
1794 Ok(GenericPrep {
1795 loops,
1796 segments,
1797 chord_count,
1798 index: std::cell::OnceCell::new(),
1799 })
1800}
1801
1802struct LoopSamples {
1807 polygon: std::rc::Rc<Vec<Vec2>>,
1808 coedges: Vec<(u64, usize, usize)>,
1809}
1810
1811fn sphere_chart_point_in_face(
1836 face: &FaceRecord,
1837 point: Vec2,
1838 prepared: &PreparedFace,
1839 sampled: &[LoopSamples],
1840) -> Result<Option<PolygonClass>, String> {
1841 let Some(atlas) = sphere_chart_atlas(face) else {
1842 return Ok(None);
1843 };
1844 let region = lane(&prepared.sphere_region, || build_sphere_region(face, &atlas, sampled))?;
1845 if region.is_whole_sphere() {
1846 return Ok(Some(PolygonClass::Inside));
1847 }
1848 if !region.is_decidable() {
1849 return Ok(None);
1852 }
1853 let probe = face.surface.evaluate(point.x, point.y)?;
1854 Ok(Some(if region.contains(atlas.centre, probe) {
1855 PolygonClass::Inside
1856 } else {
1857 PolygonClass::Outside
1858 }))
1859}
1860
1861fn build_sphere_region(
1879 face: &FaceRecord,
1880 atlas: &crate::sphere_chart::SphereAtlas,
1881 sampled: &[LoopSamples],
1882) -> Result<crate::sphere_chart::SphericalRegion, String> {
1883 let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
1887 for coedge in face.loops.iter().flat_map(|record| record.coedges.iter()) {
1888 *uses.entry(coedge.edge_id).or_insert(0) += 1;
1889 }
1890 let outward_face_normal =
1915 face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
1916 let evaluate_started = containment_profile_started();
1917 let mut points: Vec<Vec3> = Vec::new();
1918 let mut spans: Vec<(usize, usize)> = Vec::new();
1919 for samples in sampled {
1920 let count = samples.polygon.len();
1921 if count < 2 {
1922 continue;
1923 }
1924 for &(edge_id, first, span) in &samples.coedges {
1925 if span == 0 || uses.get(&edge_id).copied().unwrap_or(0) >= 2 {
1926 continue;
1927 }
1928 let start = points.len();
1932 for offset in 0..=span {
1933 let uv = samples.polygon[(first + offset) % count];
1934 points.push(face.surface.evaluate(uv.x, uv.y)?);
1935 }
1936 spans.push((start, points.len()));
1937 }
1938 }
1939 let evaluate_ms = containment_elapsed_ms(evaluate_started);
1944 let canonical_started = containment_profile_started();
1945 crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
1946 let canonical_ms = containment_elapsed_ms(canonical_started);
1947 let segments_started = containment_profile_started();
1948 let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
1949 for (first, last) in spans {
1950 for index in first..last.saturating_sub(1) {
1951 boundary.push((points[index], points[index + 1]));
1952 }
1953 }
1954 let region = crate::sphere_chart::SphericalRegion::from_segments(
1955 atlas.centre,
1956 &boundary,
1957 outward_face_normal,
1958 );
1959 let segments_ms = containment_elapsed_ms(segments_started);
1960 containment_profile(|p| {
1961 p.region_builds += 1;
1962 p.region_evaluate_ms += evaluate_ms;
1963 p.region_canonical_ms += canonical_ms;
1964 p.region_segments_ms += segments_ms;
1965 });
1966 Ok(region)
1967}
1968
1969
1970fn sphere_chart_atlas(face: &FaceRecord) -> Option<crate::sphere_chart::SphereAtlas> {
1973 if !sphere_chart_trim_enabled() {
1974 return None;
1975 }
1976 crate::sphere_chart::SphereAtlas::of_surface(&face.surface)
1977}
1978
1979fn containment_trace_enabled() -> bool {
1983 static ON: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
1984 *ON.get_or_init(|| std::env::var("BREP_DEBUG_CONTAINMENT").is_ok())
1985}
1986
1987fn trace_lane(face: &FaceRecord, point: Vec2, lane: &str, class: PolygonClass) {
1992 if containment_trace_enabled() {
1993 eprintln!(
1994 "containment face={} uv=({:.8},{:.8}) lane={lane} -> {class:?}",
1995 face.id, point.x, point.y
1996 );
1997 }
1998}
1999
2000
2001enum Lane<'a> {
2003 SeamBand(&'a SeamBandPrep),
2004 SphereCap(&'a SphereCapPrep),
2005 Horizon(&'a HorizonPrep),
2006 RimStrip(&'a RimStripPrep),
2007 Generic,
2008}
2009
2010fn lane_from<'a>(
2018 face: &FaceRecord,
2019 prepared: &'a PreparedFace,
2020 from: usize,
2021) -> Result<(usize, Lane<'a>), String> {
2022 if from == 0 {
2023 let band = timed_lane(&prepared.seam_band, || prepare_seam_band(face), |p, ms| {
2024 p.seam_band_preps += 1;
2025 p.seam_band_ms += ms;
2026 })?;
2027 if !matches!(band, SeamBandPrep::NotApplicable | SeamBandPrep::Declined) {
2028 return Ok((0, Lane::SeamBand(band)));
2029 }
2030 }
2031 if from <= 1 {
2032 if let Some(cap) = timed_lane(&prepared.sphere_cap, || prepare_winding_sphere_cap(face, prepared), |p, ms| {
2033 p.sphere_cap_preps += 1;
2034 p.sphere_cap_ms += ms;
2035 })? {
2036 return Ok((1, Lane::SphereCap(cap)));
2037 }
2038 }
2039 if from <= 2 {
2040 if let Some(horizon) = timed_lane(&prepared.horizon, || prepare_wrapped_horizon(face, prepared), |p, ms| {
2041 p.horizon_preps += 1;
2042 p.horizon_ms += ms;
2043 })? {
2044 return Ok((2, Lane::Horizon(horizon)));
2045 }
2046 }
2047 if from <= 3 {
2048 if let Some(strip) = timed_lane(&prepared.rim_strip, || prepare_covering_rim_strip(face), |p, ms| {
2049 p.rim_strip_preps += 1;
2050 p.rim_strip_ms += ms;
2051 })? {
2052 return Ok((3, Lane::RimStrip(strip)));
2053 }
2054 }
2055 Ok((4, Lane::Generic))
2056}
2057
2058pub fn containment_lane(face: &FaceRecord) -> Result<ContainmentLane, String> {
2100 let prepared = cached_prepared_face(face).unwrap_or_else(|| std::rc::Rc::new(PreparedFace::new()));
2101 Ok(match lane_from(face, &prepared, 0)?.1 {
2102 Lane::SeamBand(_) => ContainmentLane::SeamBand,
2103 Lane::SphereCap(_) => ContainmentLane::SphereCap,
2104 Lane::Horizon(horizon) => ContainmentLane::WrappedHorizon {
2105 loops: horizon.loops.clone(),
2106 u_period: horizon.u_period,
2107 cross_frame: horizon_cross_frame(),
2108 },
2109 Lane::RimStrip(_) => ContainmentLane::CoveringRimStrip,
2110 Lane::Generic => ContainmentLane::Generic {
2111 sphere_chart: sphere_chart_atlas(face).is_some(),
2112 },
2113 })
2114}
2115
2116#[derive(Clone, Debug)]
2118pub enum ContainmentLane {
2119 SeamBand,
2121 SphereCap,
2125 WrappedHorizon {
2132 loops: Vec<Vec<Vec2>>,
2135 u_period: f64,
2136 cross_frame: bool,
2137 },
2138 CoveringRimStrip,
2140 Generic {
2146 sphere_chart: bool,
2149 },
2150}
2151
2152pub fn parameter_point_in_face(
2153 face: &FaceRecord,
2154 point: Vec2,
2155 tolerance: f64,
2156) -> Result<PolygonClass, String> {
2157 point_in_face_scanned(face, point, tolerance, ChordScan::Auto)
2158}
2159
2160#[doc(hidden)]
2165pub fn parameter_point_in_face_scan(
2166 face: &FaceRecord,
2167 point: Vec2,
2168 tolerance: f64,
2169 indexed: bool,
2170) -> Result<PolygonClass, String> {
2171 let scan = if indexed {
2172 ChordScan::Indexed
2173 } else {
2174 ChordScan::Linear
2175 };
2176 point_in_face_scanned(face, point, tolerance, scan)
2177}
2178
2179fn point_in_face_scanned(
2180 face: &FaceRecord,
2181 point: Vec2,
2182 tolerance: f64,
2183 scan: ChordScan,
2184) -> Result<PolygonClass, String> {
2185 let prepared = prepared_face(face);
2186 let mut from = 0;
2187 loop {
2188 let (position, lane) = lane_from(face, &prepared, from)?;
2189 let answer = match lane {
2190 Lane::SeamBand(band) => {
2191 seam_band_point_in_face(band, point, tolerance).map(|class| ("seam_band", class))
2192 }
2193 Lane::SphereCap(cap) => winding_sphere_cap_point_in_face(cap, point, tolerance)
2194 .map(|class| ("winding_sphere_cap", class)),
2195 Lane::Horizon(horizon) => wrapped_horizon_point_in_face(face, horizon, point, tolerance, scan)
2196 .map(|class| ("wrapped_horizon", class)),
2197 Lane::RimStrip(strip) => covering_rim_strip_point_in_face(strip, point, tolerance)
2198 .map(|class| ("covering_rim_strip", class)),
2199 Lane::Generic => break,
2200 };
2201 if let Some((name, class)) = answer {
2202 trace_lane(face, point, name, class);
2203 return Ok(class);
2204 }
2205 from = position + 1;
2206 }
2207 let generic = timed_lane(&prepared.generic, || prepare_generic(face, &prepared), |p, ms| {
2208 p.generic_preps += 1;
2209 p.generic_ms += ms;
2210 })?;
2211 let (crossings, boundary, nearest) = generic_scan(generic, point, tolerance, scan);
2212 if boundary {
2213 trace_lane(face, point, "polygon_boundary", PolygonClass::Boundary);
2214 return Ok(PolygonClass::Boundary);
2215 }
2216 let parity = if crossings % 2 == 1 {
2217 PolygonClass::Inside
2218 } else {
2219 PolygonClass::Outside
2220 };
2221 let chord = nearest.map(|(loop_index, index, distance)| {
2222 let polygon = &generic.loops[loop_index].polygon;
2223 let prep = generic.segments[loop_index][index];
2224 (
2225 polygon[index],
2226 polygon[(index + 1) % polygon.len()],
2227 &face.loops[loop_index].coedges[prep.coedge_index].pcurve,
2228 prep,
2229 distance,
2230 )
2231 });
2232 if containment_trace_enabled() {
2233 let (distance, length) = match chord.as_ref() {
2234 Some((start, end, _, _, distance)) => (*distance, end.sub(*start).length()),
2235 None => (f64::NAN, f64::NAN),
2236 };
2237 eprintln!(
2238 "containment face={} uv=({:.8},{:.8}) crossings={crossings} parity={parity:?} \
2239 nearest distance={distance:.3e} segment length={length:.3e}",
2240 face.id, point.x, point.y
2241 );
2242 }
2243 let parity_decides = match chord.as_ref() {
2252 None => true,
2253 Some((start, end, _, _, distance)) => {
2254 let length = end.sub(*start).length();
2255 *distance > length || length <= 0.0
2256 }
2257 };
2258 if parity_decides {
2259 if let Some(class) = sphere_chart_point_in_face(face, point, &prepared, &generic.loops)? {
2260 trace_lane(face, point, "sphere_chart", class);
2261 return Ok(class);
2262 }
2263 }
2264 let Some((segment_start, segment_end, curve, prep, distance)) = chord else {
2265 trace_lane(face, point, "parity (no segment)", parity);
2266 return Ok(parity);
2267 };
2268 let segment_length = segment_end.sub(segment_start).length();
2269 if distance > segment_length || segment_length <= 0.0 {
2270 trace_lane(face, point, "parity (far from every segment)", parity);
2271 return Ok(parity);
2272 }
2273 let chord_parameter = prep.parameter_start
2274 + (prep.parameter_end - prep.parameter_start)
2275 * (point.sub(segment_start).dot(segment_end.sub(segment_start))
2276 / (segment_length * segment_length))
2277 .clamp(0.0, 1.0);
2278 let [domain_start, domain_end] = curve.domain()?;
2279 let mut parameter = chord_parameter;
2280 for _ in 0..12 {
2281 let derivatives = curve.derivatives(parameter, 2)?;
2282 let on_curve = Vec2 {
2283 x: derivatives[0].x,
2284 y: derivatives[0].y,
2285 };
2286 let tangent = Vec2 {
2287 x: derivatives[1].x,
2288 y: derivatives[1].y,
2289 };
2290 let second = Vec2 {
2291 x: derivatives[2].x,
2292 y: derivatives[2].y,
2293 };
2294 let residual = on_curve.sub(point);
2295 let denominator = tangent.dot(tangent) + residual.dot(second);
2296 if denominator.abs() < 1e-30 {
2297 break;
2298 }
2299 let step = -residual.dot(tangent) / denominator;
2300 parameter = (parameter + step).clamp(domain_start, domain_end);
2301 if step.abs() < 1e-14 * (domain_end - domain_start + 1.0) {
2302 break;
2303 }
2304 }
2305 let margin = 1e-9 * (domain_end - domain_start);
2306 if parameter > domain_start + margin && parameter < domain_end - margin {
2307 let derivatives = curve.derivatives(parameter, 1)?;
2308 let on_curve = Vec2 {
2309 x: derivatives[0].x,
2310 y: derivatives[0].y,
2311 };
2312 let tangent = Vec2 {
2313 x: derivatives[1].x,
2314 y: derivatives[1].y,
2315 };
2316 let offset = point.sub(on_curve);
2317 if offset.length() <= tolerance {
2318 trace_lane(face, point, "nearest curve (on it)", PolygonClass::Boundary);
2319 return Ok(PolygonClass::Boundary);
2320 }
2321 let cross = tangent.x * offset.y - tangent.y * offset.x;
2322 if cross.abs() > 1e-30 {
2323 let class = if (cross > 0.0) == face.same_sense {
2324 PolygonClass::Inside
2325 } else {
2326 PolygonClass::Outside
2327 };
2328 trace_lane(face, point, "nearest curve (side)", class);
2329 return Ok(class);
2330 }
2331 }
2332 trace_lane(face, point, "parity (nearest curve declined)", parity);
2333 Ok(parity)
2334}