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
70pub fn horizon_cross_frame() -> bool {
83 switch!(HORIZON_CROSS_FRAME_SWITCH, true)
84}
85
86fn sphere_chart_trim_enabled() -> bool {
90 env_read!("BREP_NO_SPHERE_CHARTS").is_none()
91 && std::env::var(NO_SPHERE_CHART_TRIM_SWITCH).is_err()
92}
93
94fn point_segment_distance(point: Vec2, start: Vec2, end: Vec2) -> f64 {
95 point.distance_to_segment(start, end, 1e-30)
96}
97
98pub use crate::polygon::PolygonClass;
99
100fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
101 crate::polygon::classify(point, polygon, tolerance, 1e-30)
102}
103
104fn polygon_parity_inside(point: Vec2, polygon: &[Vec2]) -> bool {
107 crate::polygon::parity_inside([point.x, point.y], polygon.len(), |i| [polygon[i].x, polygon[i].y])
108}
109
110
111#[derive(Clone, Copy)]
116struct SegmentPrep {
117 coedge_index: usize,
118 parameter_start: f64,
119 parameter_end: f64,
120}
121
122pub(crate) fn lane_switches() -> f64 {
126 let mut bits = 0u32;
127 if !switch!(SEAM_BAND_MERGED_SWITCH, true) {
128 bits |= 1;
129 }
130 if !switch!(HORIZON_CONTAINMENT_SWITCH, true) {
131 bits |= 2;
132 }
133 if !switch!(HORIZON_SINGLE_LOOP_SWITCH, true) {
134 bits |= 4;
135 }
136 if !switch!(COVERING_RIM_STRIP_SWITCH, true) {
137 bits |= 8;
138 }
139 bits as f64
140}
141
142enum SeamBandPrep {
144 NotApplicable,
146 AlwaysInside,
148 Merged { polygon: Vec<Vec2>, u_span: f64, v_span: f64 },
150 Analyzed { polygon: Vec<Vec2> },
152 Rings { p_is_u: bool, q_lo: f64, q_hi: f64, complement: bool },
154 Declined,
156}
157
158struct SphereCapPrep {
160 rim: Vec<Vec2>,
161 pole_v: f64,
162 period: f64,
163}
164
165struct HorizonPrep {
167 loops: Vec<Vec<Vec2>>,
168 u_period: f64,
169 index: std::cell::OnceCell<Option<ChordIndex>>,
171}
172
173struct RimStripPrep {
175 lower: Vec<Vec2>,
176 upper: Vec<Vec2>,
177 period: f64,
178}
179
180struct GenericPrep {
183 loops: Vec<LoopSamples>,
184 segments: Vec<Vec<SegmentPrep>>,
185 chord_count: usize,
187 index: std::cell::OnceCell<Option<ChordIndex>>,
190}
191
192const CHORD_INDEX_MIN_CHORDS: usize = 64;
196
197const CHORD_INDEX_LEAF: usize = 6;
199
200const CHORD_INDEX_MAX_COORDINATE: f64 = 1e100;
205
206fn chord_index_enabled() -> bool {
210 switch!("BREP_CONTAINMENT_INDEX", true)
211}
212
213#[derive(Clone, Copy)]
219struct ChordNode {
220 low: Vec2,
221 high: Vec2,
222 loop_index: u32,
223 first: u32,
224 last: u32,
225 children: Option<(u32, u32)>,
226}
227
228struct ChordIndex {
261 nodes: Vec<ChordNode>,
262 roots: Vec<u32>,
264 slack: f64,
266}
267
268trait ChordLoops {
271 fn loop_count(&self) -> usize;
272 fn polygon(&self, index: usize) -> &[Vec2];
273}
274
275impl ChordLoops for [LoopSamples] {
276 fn loop_count(&self) -> usize {
277 self.len()
278 }
279
280 fn polygon(&self, index: usize) -> &[Vec2] {
281 &self[index].polygon
282 }
283}
284
285impl ChordLoops for [Vec<Vec2>] {
286 fn loop_count(&self) -> usize {
287 self.len()
288 }
289
290 fn polygon(&self, index: usize) -> &[Vec2] {
291 &self[index]
292 }
293}
294
295impl ChordIndex {
296 fn build<L: ChordLoops + ?Sized>(loops: &L) -> Option<ChordIndex> {
300 let mut nodes: Vec<ChordNode> = Vec::new();
301 let mut roots = Vec::new();
302 let mut largest: f64 = 0.0;
303 for loop_index in 0..loops.loop_count() {
304 let polygon = loops.polygon(loop_index);
305 let len = polygon.len();
306 if len == 0 {
307 continue;
308 }
309 u32::try_from(len).ok()?;
310 for sample in polygon {
311 if !(sample.x.abs() <= CHORD_INDEX_MAX_COORDINATE && sample.y.abs() <= CHORD_INDEX_MAX_COORDINATE) {
312 return None;
313 }
314 largest = largest.max(sample.x.abs()).max(sample.y.abs());
315 }
316 let loop_index = u32::try_from(loop_index).ok()?;
317 let mut level: Vec<u32> = Vec::with_capacity(len / CHORD_INDEX_LEAF + 1);
318 for first in (0..len).step_by(CHORD_INDEX_LEAF) {
319 let last = (first + CHORD_INDEX_LEAF).min(len);
320 let (mut low, mut high) = (polygon[first], polygon[first]);
321 for index in first..last {
322 let end = polygon[(index + 1) % len];
323 low = Vec2 { x: low.x.min(end.x), y: low.y.min(end.y) };
324 high = Vec2 { x: high.x.max(end.x), y: high.y.max(end.y) };
325 let start = polygon[index];
326 low = Vec2 { x: low.x.min(start.x), y: low.y.min(start.y) };
327 high = Vec2 { x: high.x.max(start.x), y: high.y.max(start.y) };
328 }
329 level.push(u32::try_from(nodes.len()).ok()?);
330 nodes.push(ChordNode {
331 low,
332 high,
333 loop_index,
334 first: first as u32,
335 last: last as u32,
336 children: None,
337 });
338 }
339 while level.len() > 1 {
340 let mut next = Vec::with_capacity(level.len() / 2 + 1);
341 for pair in level.chunks(2) {
342 let &[left, right] = pair else {
343 next.push(pair[0]);
344 continue;
345 };
346 let (a, b) = (nodes[left as usize], nodes[right as usize]);
347 next.push(u32::try_from(nodes.len()).ok()?);
348 nodes.push(ChordNode {
349 low: Vec2 { x: a.low.x.min(b.low.x), y: a.low.y.min(b.low.y) },
350 high: Vec2 { x: a.high.x.max(b.high.x), y: a.high.y.max(b.high.y) },
351 loop_index,
352 first: a.first,
353 last: b.last,
354 children: Some((left, right)),
355 });
356 }
357 level = next;
358 }
359 roots.push(level[0]);
360 }
361 if roots.is_empty() {
362 return None;
363 }
364 Some(ChordIndex {
365 nodes,
366 roots,
367 slack: 1e-12 * (1.0 + largest),
368 })
369 }
370
371 fn slack_at(&self, point: Vec2) -> f64 {
374 self.slack + 1e-12 * point.x.abs().max(point.y.abs())
375 }
376
377 fn box_distance(node: &ChordNode, point: Vec2) -> f64 {
378 let dx = (node.low.x - point.x).max(point.x - node.high.x).max(0.0);
379 let dy = (node.low.y - point.y).max(point.y - node.high.y).max(0.0);
380 (dx * dx + dy * dy).sqrt()
381 }
382
383 fn nearest<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2) -> Option<(usize, usize, f64)> {
387 let slack = self.slack_at(point);
388 let mut best: Option<(f64, u32, u32)> = None;
389 let mut stack: Vec<(u32, f64)> = Vec::with_capacity(64);
390 for &root in &self.roots {
391 stack.push((root, Self::box_distance(&self.nodes[root as usize], point)));
392 }
393 while let Some((node_index, bound)) = stack.pop() {
394 if best.is_some_and(|(distance, _, _)| bound > distance + slack + 1e-12 * distance) {
395 continue;
396 }
397 let node = self.nodes[node_index as usize];
398 match node.children {
399 None => {
400 let polygon = loops.polygon(node.loop_index as usize);
401 for index in node.first..node.last {
402 let start = polygon[index as usize];
403 let end = polygon[(index as usize + 1) % polygon.len()];
404 let distance = point_segment_distance(point, start, end);
405 let better = best.is_none_or(|(least, loop_index, chord_index)| {
406 distance < least
407 || (distance == least && (node.loop_index, index) < (loop_index, chord_index))
408 });
409 if better {
410 best = Some((distance, node.loop_index, index));
411 }
412 }
413 }
414 Some((left, right)) => {
415 let left_bound = Self::box_distance(&self.nodes[left as usize], point);
416 let right_bound = Self::box_distance(&self.nodes[right as usize], point);
417 if left_bound <= right_bound {
419 stack.push((right, right_bound));
420 stack.push((left, left_bound));
421 } else {
422 stack.push((left, left_bound));
423 stack.push((right, right_bound));
424 }
425 }
426 }
427 }
428 best.map(|(distance, loop_index, chord_index)| {
429 (loop_index as usize, chord_index as usize, distance)
430 })
431 }
432
433 fn any_within<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2, tolerance: f64) -> bool {
436 let reach = tolerance + self.slack_at(point) + 1e-12 * tolerance.abs();
437 let mut stack: Vec<u32> = self.roots.clone();
438 while let Some(node_index) = stack.pop() {
439 let node = self.nodes[node_index as usize];
440 if Self::box_distance(&node, point) > reach {
441 continue;
442 }
443 match node.children {
444 None => {
445 let polygon = loops.polygon(node.loop_index as usize);
446 for index in node.first as usize..node.last as usize {
447 let (start, end) = (polygon[index], polygon[(index + 1) % polygon.len()]);
448 if point_segment_distance(point, start, end) <= tolerance {
449 return true;
450 }
451 }
452 }
453 Some((left, right)) => {
454 stack.push(right);
455 stack.push(left);
456 }
457 }
458 }
459 false
460 }
461
462 fn flip_crossings<L: ChordLoops + ?Sized>(&self, loops: &L, point: Vec2, odd: &mut [bool]) {
465 let slack = self.slack_at(point);
466 let mut stack: Vec<u32> = self.roots.clone();
467 while let Some(node_index) = stack.pop() {
468 let node = self.nodes[node_index as usize];
469 if !(node.low.y <= point.y && node.high.y > point.y) || node.high.x + slack < point.x {
470 continue;
471 }
472 match node.children {
473 None => {
474 let polygon = loops.polygon(node.loop_index as usize);
475 for index in node.first as usize..node.last as usize {
476 let a = polygon[index];
477 let b = polygon[(index + 1) % polygon.len()];
478 if crate::polygon::crosses_ray([point.x, point.y], [a.x, a.y], [b.x, b.y]) {
479 let parity = &mut odd[node.loop_index as usize];
480 *parity = !*parity;
481 }
482 }
483 }
484 Some((left, right)) => {
485 stack.push(right);
486 stack.push(left);
487 }
488 }
489 }
490 }
491}
492
493#[derive(Clone, Copy, PartialEq, Eq)]
496enum ChordScan {
497 Auto,
500 Indexed,
502 Linear,
504}
505
506impl ChordScan {
507 fn indexes(self, chords: usize) -> bool {
509 match self {
510 ChordScan::Linear => false,
511 ChordScan::Indexed => true,
512 ChordScan::Auto => chord_index_enabled() && chords >= CHORD_INDEX_MIN_CHORDS,
513 }
514 }
515}
516
517fn in_index_range(point: Vec2) -> bool {
520 point.x.abs() <= CHORD_INDEX_MAX_COORDINATE && point.y.abs() <= CHORD_INDEX_MAX_COORDINATE
521}
522
523fn linear_scan(
526 generic: &GenericPrep,
527 point: Vec2,
528 tolerance: f64,
529) -> (usize, bool, Option<(usize, usize, f64)>) {
530 let mut crossings = 0;
531 let mut boundary = false;
532 let mut nearest: Option<(usize, usize, f64)> = None;
535 let fused = fused_scan_enabled();
536 for (loop_index, (samples, chords)) in generic.loops.iter().zip(&generic.segments).enumerate() {
537 let polygon = &samples.polygon;
538 if fused {
539 let mut on_boundary = false;
542 for index in 0..polygon.len() {
543 let distance = point_segment_distance(point, polygon[index], polygon[(index + 1) % polygon.len()]);
544 if distance <= tolerance {
545 on_boundary = true;
546 }
547 if index < chords.len()
548 && nearest
549 .as_ref()
550 .is_none_or(|(_, _, nearest_distance)| distance < *nearest_distance)
551 {
552 nearest = Some((loop_index, index, distance));
553 }
554 }
555 if on_boundary {
556 boundary = true;
557 } else if polygon_parity_inside(point, polygon) {
558 crossings += 1;
559 }
560 continue;
561 }
562 match point_in_polygon(point, polygon, tolerance) {
563 PolygonClass::Boundary => boundary = true,
564 PolygonClass::Inside => crossings += 1,
565 PolygonClass::Outside => {}
566 }
567 for index in 0..chords.len() {
568 let start = polygon[index];
569 let end = polygon[(index + 1) % polygon.len()];
570 let distance = point_segment_distance(point, start, end);
571 if nearest
572 .as_ref()
573 .is_none_or(|(_, _, nearest_distance)| distance < *nearest_distance)
574 {
575 nearest = Some((loop_index, index, distance));
576 }
577 }
578 }
579 (crossings, boundary, nearest)
580}
581
582fn fused_scan_enabled() -> bool {
589 switch!("BREP_CONTAINMENT_FUSED_SCAN", true)
590}
591
592
593fn generic_scan(
597 generic: &GenericPrep,
598 point: Vec2,
599 tolerance: f64,
600 scan: ChordScan,
601) -> (usize, bool, Option<(usize, usize, f64)>) {
602 if scan.indexes(generic.chord_count) && in_index_range(point) {
603 if let Some(index) = generic.index.get_or_init(|| {
604 let started = containment_profile_started();
605 let built = ChordIndex::build(generic.loops.as_slice());
606 let ms = containment_elapsed_ms(started);
607 containment_profile(|p| {
608 p.index_builds += 1;
609 p.index_ms += ms;
610 });
611 built
612 }) {
613 let loops = generic.loops.as_slice();
614 let nearest = index.nearest(loops, point);
615 let boundary = nearest.is_some_and(|(_, _, distance)| distance <= tolerance);
616 let crossings = if boundary {
617 0
618 } else {
619 let mut odd = vec![false; loops.len()];
620 index.flip_crossings(loops, point, &mut odd);
621 odd.into_iter().filter(|&parity| parity).count()
622 };
623 return (crossings, boundary, nearest);
624 }
625 }
626 linear_scan(generic, point, tolerance)
627}
628
629struct PreparedFace {
636 seam_band: std::cell::OnceCell<Result<SeamBandPrep, String>>,
637 sphere_cap: std::cell::OnceCell<Result<Option<SphereCapPrep>, String>>,
638 horizon: std::cell::OnceCell<Result<Option<HorizonPrep>, String>>,
639 rim_strip: std::cell::OnceCell<Result<Option<RimStripPrep>, String>>,
640 generic: std::cell::OnceCell<Result<GenericPrep, String>>,
641 sphere_region: std::cell::OnceCell<Result<crate::sphere_chart::SphericalRegion, String>>,
643 loop_samples: std::cell::RefCell<Vec<Option<Result<std::rc::Rc<Vec<Vec2>>, String>>>>,
648}
649
650impl PreparedFace {
651 fn new() -> Self {
652 Self {
653 seam_band: std::cell::OnceCell::new(),
654 sphere_cap: std::cell::OnceCell::new(),
655 horizon: std::cell::OnceCell::new(),
656 rim_strip: std::cell::OnceCell::new(),
657 generic: std::cell::OnceCell::new(),
658 sphere_region: std::cell::OnceCell::new(),
659 loop_samples: std::cell::RefCell::new(Vec::new()),
660 }
661 }
662}
663
664fn loop_samples(prepared: &PreparedFace, face: &FaceRecord, loop_index: usize) -> Result<std::rc::Rc<Vec<Vec2>>, String> {
674 if let Some(Some(sampled)) = prepared.loop_samples.borrow().get(loop_index) {
675 return sampled.clone();
676 }
677 let sampled = (|| {
678 let mut points: Vec<Vec2> = Vec::new();
679 for coedge in &face.loops[loop_index].coedges {
680 let curve = &coedge.pcurve;
681 let [start, end] = curve.domain()?;
682 let sample_count = trim_sample_count(curve);
683 for index in 0..sample_count {
684 let parameter = trim_station(start, end, index, sample_count);
685 let evaluated = curve.evaluate(parameter)?;
686 points.push(Vec2 {
687 x: evaluated.x,
688 y: evaluated.y,
689 });
690 }
691 }
692 Ok(std::rc::Rc::new(points))
693 })();
694 let mut cells = prepared.loop_samples.borrow_mut();
695 if cells.len() < face.loops.len() {
696 cells.resize(face.loops.len(), None);
697 }
698 cells[loop_index] = Some(sampled.clone());
699 sampled
700}
701
702#[derive(Default, Clone, Copy)]
712struct ContainmentProfile {
713 enabled: bool,
714 cache_misses: u64,
715 seam_band_preps: u64,
716 seam_band_ms: f64,
717 sphere_cap_preps: u64,
718 sphere_cap_ms: f64,
719 horizon_preps: u64,
720 horizon_ms: f64,
721 rim_strip_preps: u64,
722 rim_strip_ms: f64,
723 generic_preps: u64,
724 generic_ms: f64,
725 generic_chords: u64,
726 index_builds: u64,
727 index_ms: f64,
728 region_builds: u64,
729 region_evaluate_ms: f64,
730 region_canonical_ms: f64,
731 region_segments_ms: f64,
732}
733
734thread_local! {
735 static CONTAINMENT_PROFILE: std::cell::Cell<ContainmentProfile> =
736 const { std::cell::Cell::new(ContainmentProfile {
737 enabled: false, cache_misses: 0, seam_band_preps: 0, seam_band_ms: 0.0,
738 sphere_cap_preps: 0, sphere_cap_ms: 0.0, horizon_preps: 0, horizon_ms: 0.0,
739 rim_strip_preps: 0, rim_strip_ms: 0.0, generic_preps: 0, generic_ms: 0.0,
740 generic_chords: 0, index_builds: 0, index_ms: 0.0, region_builds: 0,
741 region_evaluate_ms: 0.0, region_canonical_ms: 0.0, region_segments_ms: 0.0,
742 }) };
743}
744
745impl crate::profile::Counters for ContainmentProfile {
746 fn armed(&self) -> bool {
747 self.enabled
748 }
749}
750
751fn containment_profile(update: impl FnOnce(&mut ContainmentProfile)) {
752 crate::profile::update(&CONTAINMENT_PROFILE, update)
753}
754
755fn containment_profile_started() -> Option<web_time::Instant> {
756 crate::profile::started(&CONTAINMENT_PROFILE)
757}
758
759use crate::profile::elapsed_ms as containment_elapsed_ms;
760
761pub(crate) fn containment_profile_begin() {
763 let enabled = crate::profile::profile_enabled();
764 CONTAINMENT_PROFILE.with(|cell| {
765 cell.set(ContainmentProfile {
766 enabled,
767 ..ContainmentProfile::default()
768 })
769 });
770}
771
772pub(crate) fn containment_profile_report(label: &str) {
775 let p = CONTAINMENT_PROFILE.with(|cell| cell.get());
776 if !p.enabled {
777 return;
778 }
779 profile_line!(
780 "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}",
781 p.cache_misses, p.seam_band_preps, p.seam_band_ms, p.sphere_cap_preps, p.sphere_cap_ms,
782 p.horizon_preps, p.horizon_ms, p.rim_strip_preps, p.rim_strip_ms, p.generic_preps,
783 p.generic_ms, p.generic_chords, p.index_builds, p.index_ms, p.region_builds,
784 p.region_evaluate_ms, p.region_canonical_ms, p.region_segments_ms,
785 );
786 CONTAINMENT_PROFILE.with(|cell| cell.set(ContainmentProfile::default()));
787}
788
789fn timed_lane<'a, T>(
792 cell: &'a std::cell::OnceCell<Result<T, String>>,
793 build: impl FnOnce() -> Result<T, String>,
794 record: fn(&mut ContainmentProfile, f64),
795) -> Result<&'a T, String> {
796 lane(cell, || {
797 let started = containment_profile_started();
798 let built = build();
799 let ms = containment_elapsed_ms(started);
800 containment_profile(|p| record(p, ms));
801 built
802 })
803}
804
805fn lane<'a, T>(
806 cell: &'a std::cell::OnceCell<Result<T, String>>,
807 build: impl FnOnce() -> Result<T, String>,
808) -> Result<&'a T, String> {
809 match cell.get_or_init(build) {
810 Ok(value) => Ok(value),
811 Err(error) => Err(error.clone()),
812 }
813}
814
815struct PreparedFaceCache {
833 entries: Vec<(u64, Vec<f64>, std::rc::Rc<PreparedFace>)>,
834}
835
836const PREPARED_FACE_CACHE_ENTRIES: usize = 64;
837
838thread_local! {
839 static PREPARED_FACES: std::cell::RefCell<PreparedFaceCache> = const {
840 std::cell::RefCell::new(PreparedFaceCache {
841 entries: Vec::new(),
842 })
843 };
844}
845
846fn coedge_slots(face: &FaceRecord) -> Vec<usize> {
860 let ids: Vec<u64> = face.loops.iter().flat_map(|record| record.coedges.iter().map(|coedge| coedge.edge_id)).collect();
861 ids.iter().enumerate().map(|(at, id)| ids[..at].iter().position(|earlier| earlier == id).unwrap_or(at)).collect()
862}
863
864fn walk_face_signature<F: FnMut(f64) -> bool>(face: &FaceRecord, switches: f64, visit: &mut F) -> bool {
865 let slots = coedge_slots(face);
866 let mut flat = 0usize;
867 let surface = &face.surface;
868 let point = |visit: &mut F, p: &crate::Vec4| visit(p.x) && visit(p.y) && visit(p.z) && visit(p.w);
869 if !(visit(switches)
870 && visit(if face.same_sense { 1.0 } else { 0.0 })
871 && visit(surface.degree_u as f64)
872 && visit(surface.degree_v as f64)
873 && visit(surface.knots_u.len() as f64))
874 {
875 return false;
876 }
877 if !(surface.knots_u.iter().all(|&knot| visit(knot)) && visit(surface.knots_v.len() as f64)) {
878 return false;
879 }
880 if !(surface.knots_v.iter().all(|&knot| visit(knot)) && visit(surface.control_points.len() as f64)) {
881 return false;
882 }
883 for row in &surface.control_points {
884 if !(visit(row.len() as f64) && row.iter().all(|p| point(visit, p))) {
885 return false;
886 }
887 }
888 if !visit(face.loops.len() as f64) {
889 return false;
890 }
891 for loop_record in &face.loops {
892 if !visit(loop_record.coedges.len() as f64) {
893 return false;
894 }
895 for coedge in &loop_record.coedges {
896 let curve = &coedge.pcurve;
897 let slot = slots[flat];
898 flat += 1;
899 if !(visit(slot as f64)
900 && visit(if coedge.forward { 1.0 } else { 0.0 })
901 && visit(curve.degree as f64)
902 && visit(curve.knots.len() as f64))
903 {
904 return false;
905 }
906 if !(curve.knots.iter().all(|&knot| visit(knot)) && visit(curve.control_points.len() as f64)) {
907 return false;
908 }
909 if !curve.control_points.iter().all(|p| point(visit, p)) {
910 return false;
911 }
912 }
913 }
914 true
915}
916
917fn face_key_hash(face: &FaceRecord, switches: f64) -> u64 {
922 use std::hash::Hasher;
923 let slots = coedge_slots(face);
924 let mut flat = 0usize;
925 let mut hasher = rustc_hash::FxHasher::default();
926 let mut write = |value: f64| hasher.write_u64(value.to_bits());
927 let surface = &face.surface;
928 write(switches);
929 write(if face.same_sense { 1.0 } else { 0.0 });
930 write(surface.degree_u as f64);
931 write(surface.degree_v as f64);
932 write(surface.knots_u.len() as f64);
933 write(surface.knots_v.len() as f64);
934 write(surface.control_points.len() as f64);
935 let corners = [surface.control_points.first().and_then(|row| row.first()), surface.control_points.last().and_then(|row| row.last())];
936 for p in corners.into_iter().flatten() {
937 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
938 }
939 write(face.loops.len() as f64);
940 for loop_record in &face.loops {
941 write(loop_record.coedges.len() as f64);
942 for coedge in &loop_record.coedges {
943 let curve = &coedge.pcurve;
944 write(slots[flat] as f64);
945 flat += 1;
946 write(if coedge.forward { 1.0 } else { 0.0 });
947 write(curve.degree as f64);
948 write(curve.knots.len() as f64);
949 write(curve.control_points.len() as f64);
950 for knot in [curve.knots.first(), curve.knots.last()].into_iter().flatten() {
951 write(*knot);
952 }
953 for p in [curve.control_points.first(), curve.control_points.last()].into_iter().flatten() {
954 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
955 }
956 }
957 }
958 hasher.finish()
959}
960
961fn cached_entry(
963 entries: &[(u64, Vec<f64>, std::rc::Rc<PreparedFace>)],
964 face: &FaceRecord,
965 switches: f64,
966 hash: u64,
967) -> Option<usize> {
968 entries.iter().position(|(entry_hash, signature, _)| {
969 if *entry_hash != hash {
970 return false;
971 }
972 let mut at = 0;
973 let walked = walk_face_signature(face, switches, &mut |value: f64| {
974 let same = signature
975 .get(at)
976 .is_some_and(|stored| stored.to_bits() == value.to_bits() && !value.is_nan());
977 at += 1;
978 same
979 });
980 walked && at == signature.len()
981 })
982}
983
984pub(crate) fn forget_prepared_faces() {
987 PREPARED_FACES.with(|cache| cache.borrow_mut().entries.clear());
988}
989
990
991fn cached_prepared_face(face: &FaceRecord) -> Option<std::rc::Rc<PreparedFace>> {
995 PREPARED_FACES.with(|cache| {
996 let PreparedFaceCache { entries } = &*cache.borrow();
997 let switches = lane_switches();
998 let hash = face_key_hash(face, switches);
999 cached_entry(entries, face, switches, hash).map(|index| entries[index].2.clone())
1000 })
1001}
1002
1003fn prepared_face(face: &FaceRecord) -> std::rc::Rc<PreparedFace> {
1004 PREPARED_FACES.with(|cache| {
1005 let PreparedFaceCache { entries } = &mut *cache.borrow_mut();
1006 let switches = lane_switches();
1007 let hash = face_key_hash(face, switches);
1008 if let Some(index) = cached_entry(entries, face, switches, hash) {
1009 if index != 0 {
1010 let entry = entries.remove(index);
1011 entries.insert(0, entry);
1012 }
1013 return entries[0].2.clone();
1014 }
1015 let mut signature = Vec::new();
1016 walk_face_signature(face, switches, &mut |value: f64| {
1017 signature.push(value);
1018 true
1019 });
1020 let prepared = std::rc::Rc::new(PreparedFace::new());
1021 entries.insert(0, (hash, signature, prepared.clone()));
1022 entries.truncate(PREPARED_FACE_CACHE_ENTRIES);
1023 containment_profile(|p| p.cache_misses += 1);
1024 prepared
1025 })
1026}
1027
1028fn prepare_seam_band(face: &FaceRecord) -> Result<SeamBandPrep, String> {
1037 if face.surface.closed_directions()? != (true, true) {
1038 return Ok(SeamBandPrep::NotApplicable);
1039 }
1040 let [u0, u1] = face.surface.domain_u()?;
1041 let [v0, v1] = face.surface.domain_v()?;
1042 if crate::topology::doubly_periodic_has_only_collapsed_loops(face)? {
1043 return Ok(SeamBandPrep::AlwaysInside);
1044 }
1045 let u_span = (u1 - u0).abs().max(1e-30);
1046 let v_span = (v1 - v0).abs().max(1e-30);
1047 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
1048 let mut retained: Vec<usize> = Vec::with_capacity(face.loops.len());
1052 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1053 let mut points: Vec<[f64; 2]> = Vec::new();
1054 for coedge in &loop_record.coedges {
1055 let [d0, d1] = coedge.pcurve.domain()?;
1056 let samples = 24;
1057 for k in 0..=samples {
1058 let t = trim_station(d0, d1, k, samples);
1059 let p = coedge.pcurve.evaluate(t)?;
1060 points.push([p.x, p.y]);
1061 }
1062 }
1063 let (mut umin, mut umax, mut vmin, mut vmax) = (
1064 f64::INFINITY,
1065 f64::NEG_INFINITY,
1066 f64::INFINITY,
1067 f64::NEG_INFINITY,
1068 );
1069 for p in &points {
1070 umin = umin.min(p[0]);
1071 umax = umax.max(p[0]);
1072 vmin = vmin.min(p[1]);
1073 vmax = vmax.max(p[1]);
1074 }
1075 if (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span {
1095 continue;
1096 }
1097 retained.push(loop_index);
1098 loops_uv.push(points);
1099 }
1100 if loops_uv.len() == 1 && switch!(SEAM_BAND_MERGED_SWITCH, true) {
1111 let coedges = &face.loops[retained[0]].coedges;
1112 let offsets = crate::topology::loop_seam_offsets(coedges, true, true, u_span, v_span)?;
1113 if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
1114 let mut polygon: Vec<Vec2> = Vec::new();
1115 for (coedge_index, coedge) in coedges.iter().enumerate() {
1116 let [d0, d1] = coedge.pcurve.domain()?;
1117 let samples = 24;
1118 for k in 0..samples {
1119 let t = trim_station(d0, d1, k, samples);
1120 let p = coedge.pcurve.evaluate(t)?;
1121 polygon.push(Vec2 {
1122 x: p.x + offsets[coedge_index][0],
1123 y: p.y + offsets[coedge_index][1],
1124 });
1125 }
1126 }
1127 return Ok(SeamBandPrep::Merged { polygon, u_span, v_span });
1128 }
1129 }
1130 if loops_uv.len() != 2 {
1131 return Ok(SeamBandPrep::Declined);
1132 }
1133 if let Some(band) = crate::topology::analyze_doubly_periodic_seam_band(
1134 &loops_uv,
1135 [u0, u1, v0, v1],
1136 face.same_sense,
1137 ) {
1138 let polygon: Vec<Vec2> =
1139 crate::topology::seam_band_uv_polygon(&loops_uv, [u0, u1, v0, v1], &band)
1140 .into_iter()
1141 .map(|p| Vec2 { x: p[0], y: p[1] })
1142 .collect();
1143 return Ok(SeamBandPrep::Analyzed { polygon });
1144 }
1145 for p_is_u in [true, false] {
1152 let (period, q_extent) = if p_is_u {
1153 (u1 - u0, v_span)
1154 } else {
1155 (v1 - v0, u_span)
1156 };
1157 if !(period > 0.0) {
1158 continue;
1159 }
1160 let coord = |p: &[f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
1161 let mut rings = Vec::with_capacity(2);
1162 for points in &loops_uv {
1163 let (mut pmin, mut pmax, mut qmin, mut qmax) = (
1164 f64::INFINITY,
1165 f64::NEG_INFINITY,
1166 f64::INFINITY,
1167 f64::NEG_INFINITY,
1168 );
1169 for p in points {
1170 let (periodic, cross) = coord(p);
1171 pmin = pmin.min(periodic);
1172 pmax = pmax.max(periodic);
1173 qmin = qmin.min(cross);
1174 qmax = qmax.max(cross);
1175 }
1176 if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
1177 rings.clear();
1178 break;
1179 }
1180 let mut net = 0.0;
1181 for pair in points.windows(2) {
1182 let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
1183 if delta > 0.5 * period {
1184 delta -= period;
1185 } else if delta < -0.5 * period {
1186 delta += period;
1187 }
1188 net += delta;
1189 }
1190 let direction = if net > 0.25 * period {
1191 1
1192 } else if net < -0.25 * period {
1193 -1
1194 } else {
1195 0
1196 };
1197 rings.push((0.5 * (qmin + qmax), direction));
1198 }
1199 if rings.len() != 2 {
1200 continue;
1201 }
1202 rings.sort_by(|a, b| a.0.total_cmp(&b.0));
1203 let [(q_lo, lower_direction), (q_hi, upper_direction)] = rings.as_slice() else {
1204 unreachable!()
1205 };
1206 let inconclusive =
1207 *lower_direction == 0 || *upper_direction == 0 || lower_direction == upper_direction;
1208 let between_is_ccw_uv = if p_is_u {
1209 *lower_direction > 0
1210 } else {
1211 *lower_direction < 0
1212 };
1213 let complement = !inconclusive && between_is_ccw_uv != face.same_sense;
1214 return Ok(SeamBandPrep::Rings {
1215 p_is_u,
1216 q_lo: *q_lo,
1217 q_hi: *q_hi,
1218 complement,
1219 });
1220 }
1221 Ok(SeamBandPrep::Declined)
1222}
1223
1224fn seam_band_point_in_face(
1225 prepared: &SeamBandPrep,
1226 point: Vec2,
1227 tolerance: f64,
1228) -> Option<PolygonClass> {
1229 match prepared {
1230 SeamBandPrep::NotApplicable | SeamBandPrep::Declined => None,
1231 SeamBandPrep::AlwaysInside => Some(PolygonClass::Inside),
1232 SeamBandPrep::Merged { polygon, u_span, v_span } => {
1233 let mut best = PolygonClass::Outside;
1234 'images: for du in [-1.0, 0.0, 1.0] {
1235 for dv in [-1.0, 0.0, 1.0] {
1236 let image = Vec2 {
1237 x: point.x + du * u_span,
1238 y: point.y + dv * v_span,
1239 };
1240 match point_in_polygon(image, polygon, tolerance) {
1241 PolygonClass::Boundary => {
1242 best = PolygonClass::Boundary;
1243 break 'images;
1244 }
1245 PolygonClass::Inside => best = PolygonClass::Inside,
1246 PolygonClass::Outside => {}
1247 }
1248 }
1249 }
1250 Some(best)
1251 }
1252 SeamBandPrep::Analyzed { polygon } => Some(point_in_polygon(point, polygon, tolerance)),
1253 SeamBandPrep::Rings { p_is_u, q_lo, q_hi, complement } => {
1254 let q = if *p_is_u { point.y } else { point.x };
1255 if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
1256 return Some(PolygonClass::Boundary);
1257 }
1258 let between = q > *q_lo && q < *q_hi;
1259 Some(if between != *complement {
1260 PolygonClass::Inside
1261 } else {
1262 PolygonClass::Outside
1263 })
1264 }
1265 }
1266}
1267
1268fn prepare_wrapped_horizon(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<HorizonPrep>, String> {
1284 if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
1285 return Ok(None);
1286 }
1287 if !switch!(HORIZON_CONTAINMENT_SWITCH, true) {
1288 return Ok(None);
1289 }
1290 if face.loops.len() <= 2 && !switch!(HORIZON_SINGLE_LOOP_SWITCH, true) {
1301 return Ok(None);
1302 }
1303 let [u0, u1] = face.surface.domain_u()?;
1304 let u_period = u1 - u0;
1305 if u_period <= 0.0 {
1306 return Ok(None);
1307 }
1308 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1309 let mut loops: Vec<Vec<Vec2>> = Vec::new();
1310 let mut straddling = false;
1311 for loop_index in 0..face.loops.len() {
1312 let points = loop_samples(prepared, face, loop_index)?;
1313 if points.len() < 3 {
1314 continue;
1315 }
1316 let mut unwrapped = Vec::with_capacity(points.len());
1320 let mut jumps = 0usize;
1321 let mut cursor = points[0];
1322 unwrapped.push(cursor);
1323 for pair in points.windows(2) {
1324 let mut du = pair[1].x - pair[0].x;
1325 let folded = du - u_period * (du / u_period).round();
1326 if (du - folded).abs() > 0.25 * u_period {
1327 jumps += 1;
1328 }
1329 du = folded;
1330 cursor = Vec2 {
1331 x: cursor.x + du,
1332 y: pair[1].y,
1333 };
1334 unwrapped.push(cursor);
1335 }
1336 let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
1337 if closure > 0.25 * u_period {
1338 if debug {
1339 eprintln!(
1340 "horizon: face {} loop winds the period (closure {closure:.3}) — bail",
1341 face.id
1342 );
1343 }
1344 return Ok(None); }
1346 if jumps > 0 {
1347 straddling = true;
1348 }
1349 loops.push(unwrapped);
1350 }
1351 if !straddling || loops.is_empty() {
1352 return Ok(None);
1353 }
1354 Ok(Some(HorizonPrep {
1355 loops,
1356 u_period,
1357 index: std::cell::OnceCell::new(),
1358 }))
1359}
1360
1361fn wrapped_horizon_point_in_face(
1362 face: &FaceRecord,
1363 prepared: &HorizonPrep,
1364 point: Vec2,
1365 tolerance: f64,
1366 scan: ChordScan,
1367) -> Option<PolygonClass> {
1368 let HorizonPrep { loops, u_period, index } = prepared;
1369 let u_period = *u_period;
1370 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1371 if !horizon_cross_frame() {
1386 let mut best: Option<PolygonClass> = None;
1387 for shift in [-u_period, 0.0, u_period] {
1388 let image = Vec2 {
1389 x: point.x + shift,
1390 y: point.y,
1391 };
1392 let mut crossings = 0usize;
1393 for polygon in loops {
1394 match point_in_polygon(image, polygon, tolerance) {
1395 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1396 PolygonClass::Inside => crossings += 1,
1397 PolygonClass::Outside => {}
1398 }
1399 }
1400 if crossings % 2 == 1 {
1401 best = Some(PolygonClass::Inside);
1402 } else if best.is_none() {
1403 best = Some(PolygonClass::Outside);
1404 }
1405 }
1406 if debug {
1407 eprintln!(
1408 "horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
1409 face.id,
1410 loops.len(),
1411 best
1412 );
1413 }
1414 return best;
1415 }
1416 let images = [-u_period, 0.0, u_period].map(|shift| Vec2 {
1417 x: point.x + shift,
1418 y: point.y,
1419 });
1420 let chords: usize = loops.iter().map(Vec::len).sum();
1421 let indexed = (scan.indexes(chords) && images.iter().all(|image| in_index_range(*image)))
1422 .then(|| index.get_or_init(|| ChordIndex::build(loops.as_slice())).as_ref())
1423 .flatten();
1424 let mut crossings = 0usize;
1425 if let Some(index) = indexed {
1426 let loops = loops.as_slice();
1430 if images.iter().any(|image| index.any_within(loops, *image, tolerance)) {
1431 return Some(PolygonClass::Boundary);
1432 }
1433 let mut odd = vec![false; loops.len()];
1434 for image in images {
1435 index.flip_crossings(loops, image, &mut odd);
1436 }
1437 crossings = odd.into_iter().filter(|&parity| parity).count();
1438 } else {
1439 for polygon in loops {
1440 let mut image_hits = 0usize;
1441 for image in images {
1442 match point_in_polygon(image, polygon, tolerance) {
1443 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1444 PolygonClass::Inside => image_hits += 1,
1445 PolygonClass::Outside => {}
1446 }
1447 }
1448 crossings += image_hits % 2;
1449 }
1450 }
1451 let class = if crossings % 2 == 1 {
1452 PolygonClass::Inside
1453 } else {
1454 PolygonClass::Outside
1455 };
1456 if debug {
1457 eprintln!(
1458 "horizon: face {} loops={} straddling -> {:?}",
1459 face.id,
1460 loops.len(),
1461 class
1462 );
1463 }
1464 Some(class)
1465}
1466
1467fn prepare_winding_sphere_cap(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<SphereCapPrep>, String> {
1472 if !matches!(
1473 face.surface.analytic(),
1474 Some(crate::AnalyticSurface::Sphere { .. })
1475 ) || face.surface.closed_directions()? != (true, false)
1476 || face.loops.len() != 2
1477 {
1478 return Ok(None);
1479 }
1480 let [u0, u1] = face.surface.domain_u()?;
1481 let [v0, v1] = face.surface.domain_v()?;
1482 let period = u1 - u0;
1483 let v_span = v1 - v0;
1484 if !(period > 0.0 && v_span > 0.0) {
1485 return Ok(None);
1486 }
1487 struct WindingLoop {
1488 points: Vec<Vec2>,
1489 winding: f64,
1490 vmin: f64,
1491 vmax: f64,
1492 }
1493 let mut loops = Vec::with_capacity(2);
1494 for loop_index in 0..face.loops.len() {
1495 let points = loop_samples(prepared, face, loop_index)?;
1496 if points.len() < 2 {
1497 return Ok(None);
1498 }
1499 let first = points[0];
1500 let mut cursor = first;
1501 let mut unwrapped = vec![cursor];
1502 for next in points.iter().skip(1) {
1503 let du = next.x - cursor.x;
1504 let folded = du - period * (du / period).round();
1505 cursor = Vec2 {
1506 x: cursor.x + folded,
1507 y: next.y,
1508 };
1509 unwrapped.push(cursor);
1510 }
1511 let last_raw = *points.last().unwrap();
1512 let closing_du = first.x - last_raw.x;
1513 let closing_folded = closing_du - period * (closing_du / period).round();
1514 let winding = cursor.x + closing_folded - first.x;
1515 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1516 for p in &unwrapped {
1517 vmin = vmin.min(p.y);
1518 vmax = vmax.max(p.y);
1519 }
1520 loops.push(WindingLoop {
1521 points: unwrapped,
1522 winding,
1523 vmin,
1524 vmax,
1525 });
1526 }
1527 if loops
1528 .iter()
1529 .any(|loop_data| (loop_data.winding.abs() - period).abs() > 0.05 * period)
1530 || loops[0].winding * loops[1].winding >= 0.0
1531 {
1532 return Ok(None);
1533 }
1534 let flat = |loop_data: &WindingLoop| loop_data.vmax - loop_data.vmin <= 1e-6 * v_span;
1535 let (pole, rim) = match (flat(&loops[0]), flat(&loops[1])) {
1536 (true, false) => (&loops[0], &loops[1]),
1537 (false, true) => (&loops[1], &loops[0]),
1538 _ => return Ok(None),
1539 };
1540 let pole_v = 0.5 * (pole.vmin + pole.vmax);
1541 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
1542 return Ok(None);
1543 }
1544 Ok(Some(SphereCapPrep {
1545 rim: rim.points.clone(),
1546 pole_v,
1547 period,
1548 }))
1549}
1550
1551fn winding_sphere_cap_point_in_face(
1552 prepared: &SphereCapPrep,
1553 point: Vec2,
1554 tolerance: f64,
1555) -> Option<PolygonClass> {
1556 let SphereCapPrep { rim, pole_v, period } = prepared;
1557 let (pole_v, period) = (*pole_v, *period);
1558 let window_start = rim[0].x.min(rim[rim.len() - 1].x);
1559 let query_u = window_start + (point.x - window_start).rem_euclid(period);
1560 let mut crossings = Vec::new();
1561 for pair in rim.windows(2) {
1562 let (a, b) = (pair[0], pair[1]);
1563 if (a.x > query_u) != (b.x > query_u) {
1564 crossings.push(a.y + (query_u - a.x) / (b.x - a.x) * (b.y - a.y));
1565 }
1566 for image in [-period, 0.0, period] {
1567 if point_segment_distance(
1568 Vec2 {
1569 x: point.x + image,
1570 y: point.y,
1571 },
1572 a,
1573 b,
1574 ) <= tolerance
1575 {
1576 return Some(PolygonClass::Boundary);
1577 }
1578 }
1579 }
1580 let rim_v = crossings
1581 .into_iter()
1582 .min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))?;
1583 let inside = if pole_v < rim_v {
1584 point.y <= rim_v + tolerance
1585 } else {
1586 point.y >= rim_v - tolerance
1587 };
1588 Some(if inside {
1589 PolygonClass::Inside
1590 } else {
1591 PolygonClass::Outside
1592 })
1593}
1594
1595fn prepare_covering_rim_strip(face: &FaceRecord) -> Result<Option<RimStripPrep>, String> {
1627 if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
1628 return Ok(None);
1629 }
1630 if !switch!(COVERING_RIM_STRIP_SWITCH, true) {
1631 return Ok(None);
1632 }
1633 let [u0, u1] = face.surface.domain_u()?;
1634 let [v0, v1] = face.surface.domain_v()?;
1635 let period = u1 - u0;
1636 if !(period > 0.0) {
1637 return Ok(None);
1638 }
1639 let v_span = (v1 - v0).abs().max(1e-30);
1640 struct Rim {
1641 points: Vec<Vec2>,
1642 vmin: f64,
1643 vmax: f64,
1644 ascending: bool,
1645 }
1646 let mut rims: Vec<Rim> = Vec::with_capacity(2);
1647 let mut out_of_domain = false;
1648 for loop_record in &face.loops {
1649 let mut points: Vec<Vec2> = Vec::new();
1650 for coedge in &loop_record.coedges {
1651 let curve = &coedge.pcurve;
1652 let [start, end] = curve.domain()?;
1653 let sample_count = trim_sample_count(curve);
1654 for index in 0..=sample_count {
1655 let parameter = trim_station(start, end, index, sample_count);
1656 let evaluated = curve.evaluate(parameter)?;
1657 points.push(Vec2 {
1658 x: evaluated.x,
1659 y: evaluated.y,
1660 });
1661 }
1662 }
1663 if points.len() < 3 {
1664 return Ok(None);
1665 }
1666 for p in &points {
1669 if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
1670 out_of_domain = true;
1671 }
1672 }
1673 let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
1677 let mut cursor = points[0];
1678 unwrapped.push(cursor);
1679 for pair in points.windows(2) {
1680 let du = pair[1].x - pair[0].x;
1681 let folded = du - period * (du / period).round();
1682 cursor = Vec2 {
1683 x: cursor.x + folded,
1684 y: pair[1].y,
1685 };
1686 unwrapped.push(cursor);
1687 }
1688 let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
1689 if (net.abs() - period).abs() > 2e-2 * period {
1690 return Ok(None); }
1692 if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
1693 return Ok(None); }
1695 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1696 for p in &unwrapped {
1697 vmin = vmin.min(p.y);
1698 vmax = vmax.max(p.y);
1699 }
1700 rims.push(Rim {
1701 points: unwrapped,
1702 vmin,
1703 vmax,
1704 ascending: net > 0.0,
1705 });
1706 }
1707 if !out_of_domain {
1708 return Ok(None);
1709 }
1710 if rims[0].ascending == rims[1].ascending {
1711 return Ok(None); }
1713 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
1714 (&rims[0], &rims[1])
1715 } else if rims[1].vmax <= rims[0].vmin {
1716 (&rims[1], &rims[0])
1717 } else {
1718 return Ok(None); };
1720 if upper.vmin - lower.vmax <= 1e-6 * v_span {
1721 return Ok(None);
1722 }
1723 if (lower.ascending) != face.same_sense {
1728 return Ok(None);
1729 }
1730 Ok(Some(RimStripPrep {
1731 lower: lower.points.clone(),
1732 upper: upper.points.clone(),
1733 period,
1734 }))
1735}
1736
1737fn covering_rim_strip_point_in_face(
1738 prepared: &RimStripPrep,
1739 point: Vec2,
1740 tolerance: f64,
1741) -> Option<PolygonClass> {
1742 let RimStripPrep { lower, upper, period } = prepared;
1743 let period = *period;
1744 for rim in [lower, upper] {
1746 for shift in [-period, 0.0, period] {
1747 let image = Vec2 {
1748 x: point.x + shift,
1749 y: point.y,
1750 };
1751 for pair in rim.windows(2) {
1752 if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
1753 return Some(PolygonClass::Boundary);
1754 }
1755 }
1756 }
1757 }
1758 let mut crossings = 0usize;
1761 for rim in [lower, upper] {
1762 let window_base = rim[0].x.min(rim[rim.len() - 1].x);
1763 let x = window_base + (point.x - window_base).rem_euclid(period);
1764 for pair in rim.windows(2) {
1765 let (a, b) = (pair[0], pair[1]);
1766 if (a.x > x) != (b.x > x) {
1767 let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
1768 if v_cross > point.y {
1769 crossings += 1;
1770 }
1771 }
1772 }
1773 }
1774 Some(if crossings % 2 == 1 {
1775 PolygonClass::Inside
1776 } else {
1777 PolygonClass::Outside
1778 })
1779}
1780
1781fn prepare_generic(face: &FaceRecord, prepared: &PreparedFace) -> Result<GenericPrep, String> {
1785 let slots = coedge_slots(face);
1786 let mut flat = 0usize;
1787 let mut loops: Vec<LoopSamples> = Vec::with_capacity(face.loops.len());
1788 let mut segments: Vec<Vec<SegmentPrep>> = Vec::with_capacity(face.loops.len());
1789 let mut chord_count = 0;
1790 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1791 let polygon = loop_samples(prepared, face, loop_index)?;
1792 let mut chords = Vec::with_capacity(polygon.len());
1793 let mut coedges = Vec::with_capacity(loop_record.coedges.len());
1794 let mut first = 0;
1795 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
1796 let curve = &coedge.pcurve;
1797 let [start, end] = curve.domain()?;
1798 let sample_count = trim_sample_count(curve);
1799 for index in 0..sample_count {
1800 let parameter = trim_station(start, end, index, sample_count);
1801 let parameter_end = if index + 1 < sample_count {
1802 trim_station(start, end, index + 1, sample_count)
1803 } else {
1804 end
1805 };
1806 chords.push(SegmentPrep {
1807 coedge_index,
1808 parameter_start: parameter,
1809 parameter_end,
1810 });
1811 }
1812 coedges.push((slots[flat] as u64, first, sample_count));
1813 flat += 1;
1814 first += sample_count;
1815 }
1816 debug_assert_eq!(first, polygon.len());
1817 chord_count += chords.len();
1818 loops.push(LoopSamples { polygon, coedges });
1819 segments.push(chords);
1820 }
1821 containment_profile(|p| p.generic_chords += chord_count as u64);
1822 Ok(GenericPrep {
1823 loops,
1824 segments,
1825 chord_count,
1826 index: std::cell::OnceCell::new(),
1827 })
1828}
1829
1830struct LoopSamples {
1835 polygon: std::rc::Rc<Vec<Vec2>>,
1836 coedges: Vec<(u64, usize, usize)>,
1837}
1838
1839fn sphere_chart_point_in_face(
1864 face: &FaceRecord,
1865 point: Vec2,
1866 prepared: &PreparedFace,
1867 sampled: &[LoopSamples],
1868) -> Result<Option<PolygonClass>, String> {
1869 let Some(atlas) = sphere_chart_atlas(face) else {
1870 return Ok(None);
1871 };
1872 let region = lane(&prepared.sphere_region, || build_sphere_region(face, &atlas, sampled))?;
1873 if region.is_whole_sphere() {
1874 return Ok(Some(PolygonClass::Inside));
1875 }
1876 if !region.is_decidable() {
1877 return Ok(None);
1880 }
1881 let probe = face.surface.evaluate(point.x, point.y)?;
1882 Ok(Some(if region.contains(atlas.centre, probe) {
1883 PolygonClass::Inside
1884 } else {
1885 PolygonClass::Outside
1886 }))
1887}
1888
1889fn build_sphere_region(
1907 face: &FaceRecord,
1908 atlas: &crate::sphere_chart::SphereAtlas,
1909 sampled: &[LoopSamples],
1910) -> Result<crate::sphere_chart::SphericalRegion, String> {
1911 let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
1915 for slot in coedge_slots(face) {
1916 *uses.entry(slot as u64).or_insert(0) += 1;
1917 }
1918 let outward_face_normal =
1943 face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
1944 let evaluate_started = containment_profile_started();
1945 let mut points: Vec<Vec3> = Vec::new();
1946 let mut spans: Vec<(usize, usize)> = Vec::new();
1947 for samples in sampled {
1948 let count = samples.polygon.len();
1949 if count < 2 {
1950 continue;
1951 }
1952 for &(edge_id, first, span) in &samples.coedges {
1953 if span == 0 || uses.get(&edge_id).copied().unwrap_or(0) >= 2 {
1954 continue;
1955 }
1956 let start = points.len();
1960 for offset in 0..=span {
1961 let uv = samples.polygon[(first + offset) % count];
1962 points.push(face.surface.evaluate(uv.x, uv.y)?);
1963 }
1964 spans.push((start, points.len()));
1965 }
1966 }
1967 let evaluate_ms = containment_elapsed_ms(evaluate_started);
1972 let canonical_started = containment_profile_started();
1973 crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
1974 let canonical_ms = containment_elapsed_ms(canonical_started);
1975 let segments_started = containment_profile_started();
1976 let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
1977 for (first, last) in spans {
1978 for index in first..last.saturating_sub(1) {
1979 boundary.push((points[index], points[index + 1]));
1980 }
1981 }
1982 let region = crate::sphere_chart::SphericalRegion::from_segments(
1983 atlas.centre,
1984 &boundary,
1985 outward_face_normal,
1986 );
1987 let segments_ms = containment_elapsed_ms(segments_started);
1988 containment_profile(|p| {
1989 p.region_builds += 1;
1990 p.region_evaluate_ms += evaluate_ms;
1991 p.region_canonical_ms += canonical_ms;
1992 p.region_segments_ms += segments_ms;
1993 });
1994 Ok(region)
1995}
1996
1997
1998fn sphere_chart_atlas(face: &FaceRecord) -> Option<crate::sphere_chart::SphereAtlas> {
2001 if !sphere_chart_trim_enabled() {
2002 return None;
2003 }
2004 crate::sphere_chart::SphereAtlas::of_surface(&face.surface)
2005}
2006
2007fn containment_trace_enabled() -> bool {
2011 debug_flag!("BREP_DEBUG_CONTAINMENT")
2012}
2013
2014fn trace_lane(face: &FaceRecord, point: Vec2, lane: &str, class: PolygonClass) {
2019 if containment_trace_enabled() {
2020 eprintln!(
2021 "containment face={} uv=({:.8},{:.8}) lane={lane} -> {class:?}",
2022 face.id, point.x, point.y
2023 );
2024 }
2025}
2026
2027
2028enum Lane<'a> {
2030 SeamBand(&'a SeamBandPrep),
2031 SphereCap(&'a SphereCapPrep),
2032 Horizon(&'a HorizonPrep),
2033 RimStrip(&'a RimStripPrep),
2034 Generic,
2035}
2036
2037fn lane_from<'a>(
2045 face: &FaceRecord,
2046 prepared: &'a PreparedFace,
2047 from: usize,
2048) -> Result<(usize, Lane<'a>), String> {
2049 if from == 0 {
2050 let band = timed_lane(&prepared.seam_band, || prepare_seam_band(face), |p, ms| {
2051 p.seam_band_preps += 1;
2052 p.seam_band_ms += ms;
2053 })?;
2054 if !matches!(band, SeamBandPrep::NotApplicable | SeamBandPrep::Declined) {
2055 return Ok((0, Lane::SeamBand(band)));
2056 }
2057 }
2058 if from <= 1 {
2059 if let Some(cap) = timed_lane(&prepared.sphere_cap, || prepare_winding_sphere_cap(face, prepared), |p, ms| {
2060 p.sphere_cap_preps += 1;
2061 p.sphere_cap_ms += ms;
2062 })? {
2063 return Ok((1, Lane::SphereCap(cap)));
2064 }
2065 }
2066 if from <= 2 {
2067 if let Some(horizon) = timed_lane(&prepared.horizon, || prepare_wrapped_horizon(face, prepared), |p, ms| {
2068 p.horizon_preps += 1;
2069 p.horizon_ms += ms;
2070 })? {
2071 return Ok((2, Lane::Horizon(horizon)));
2072 }
2073 }
2074 if from <= 3 {
2075 if let Some(strip) = timed_lane(&prepared.rim_strip, || prepare_covering_rim_strip(face), |p, ms| {
2076 p.rim_strip_preps += 1;
2077 p.rim_strip_ms += ms;
2078 })? {
2079 return Ok((3, Lane::RimStrip(strip)));
2080 }
2081 }
2082 Ok((4, Lane::Generic))
2083}
2084
2085pub fn containment_lane(face: &FaceRecord) -> Result<ContainmentLane, String> {
2127 let prepared = cached_prepared_face(face).unwrap_or_else(|| std::rc::Rc::new(PreparedFace::new()));
2128 Ok(match lane_from(face, &prepared, 0)?.1 {
2129 Lane::SeamBand(_) => ContainmentLane::SeamBand,
2130 Lane::SphereCap(_) => ContainmentLane::SphereCap,
2131 Lane::Horizon(horizon) => ContainmentLane::WrappedHorizon {
2132 loops: horizon.loops.clone(),
2133 u_period: horizon.u_period,
2134 cross_frame: horizon_cross_frame(),
2135 },
2136 Lane::RimStrip(_) => ContainmentLane::CoveringRimStrip,
2137 Lane::Generic => ContainmentLane::Generic {
2138 sphere_chart: sphere_chart_atlas(face).is_some(),
2139 },
2140 })
2141}
2142
2143#[derive(Clone, Debug)]
2145pub enum ContainmentLane {
2146 SeamBand,
2148 SphereCap,
2152 WrappedHorizon {
2159 loops: Vec<Vec<Vec2>>,
2162 u_period: f64,
2163 cross_frame: bool,
2164 },
2165 CoveringRimStrip,
2167 Generic {
2173 sphere_chart: bool,
2176 },
2177}
2178
2179pub fn parameter_point_in_face(
2180 face: &FaceRecord,
2181 point: Vec2,
2182 tolerance: f64,
2183) -> Result<PolygonClass, String> {
2184 point_in_face_scanned(face, point, tolerance, ChordScan::Auto)
2185}
2186
2187#[doc(hidden)]
2192pub fn parameter_point_in_face_scan(
2193 face: &FaceRecord,
2194 point: Vec2,
2195 tolerance: f64,
2196 indexed: bool,
2197) -> Result<PolygonClass, String> {
2198 let scan = if indexed {
2199 ChordScan::Indexed
2200 } else {
2201 ChordScan::Linear
2202 };
2203 point_in_face_scanned(face, point, tolerance, scan)
2204}
2205
2206fn point_in_face_scanned(
2207 face: &FaceRecord,
2208 point: Vec2,
2209 tolerance: f64,
2210 scan: ChordScan,
2211) -> Result<PolygonClass, String> {
2212 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::CLASSIFY);
2213 let prepared = prepared_face(face);
2214 let mut from = 0;
2215 loop {
2216 let (position, lane) = lane_from(face, &prepared, from)?;
2217 let answer = match lane {
2218 Lane::SeamBand(band) => {
2219 seam_band_point_in_face(band, point, tolerance).map(|class| ("seam_band", class))
2220 }
2221 Lane::SphereCap(cap) => winding_sphere_cap_point_in_face(cap, point, tolerance)
2222 .map(|class| ("winding_sphere_cap", class)),
2223 Lane::Horizon(horizon) => wrapped_horizon_point_in_face(face, horizon, point, tolerance, scan)
2224 .map(|class| ("wrapped_horizon", class)),
2225 Lane::RimStrip(strip) => covering_rim_strip_point_in_face(strip, point, tolerance)
2226 .map(|class| ("covering_rim_strip", class)),
2227 Lane::Generic => break,
2228 };
2229 if let Some((name, class)) = answer {
2230 trace_lane(face, point, name, class);
2231 return Ok(class);
2232 }
2233 from = position + 1;
2234 }
2235 let generic = timed_lane(&prepared.generic, || prepare_generic(face, &prepared), |p, ms| {
2236 p.generic_preps += 1;
2237 p.generic_ms += ms;
2238 })?;
2239 let (crossings, boundary, nearest) = generic_scan(generic, point, tolerance, scan);
2240 if boundary {
2241 trace_lane(face, point, "polygon_boundary", PolygonClass::Boundary);
2242 return Ok(PolygonClass::Boundary);
2243 }
2244 let parity = if crossings % 2 == 1 {
2245 PolygonClass::Inside
2246 } else {
2247 PolygonClass::Outside
2248 };
2249 let chord = nearest.map(|(loop_index, index, distance)| {
2250 let polygon = &generic.loops[loop_index].polygon;
2251 let prep = generic.segments[loop_index][index];
2252 (
2253 polygon[index],
2254 polygon[(index + 1) % polygon.len()],
2255 &face.loops[loop_index].coedges[prep.coedge_index].pcurve,
2256 prep,
2257 distance,
2258 )
2259 });
2260 if containment_trace_enabled() {
2261 let (distance, length) = match chord.as_ref() {
2262 Some((start, end, _, _, distance)) => (*distance, end.sub(*start).length()),
2263 None => (f64::NAN, f64::NAN),
2264 };
2265 eprintln!(
2266 "containment face={} uv=({:.8},{:.8}) crossings={crossings} parity={parity:?} \
2267 nearest distance={distance:.3e} segment length={length:.3e}",
2268 face.id, point.x, point.y
2269 );
2270 }
2271 let parity_decides = match chord.as_ref() {
2280 None => true,
2281 Some((start, end, _, _, distance)) => {
2282 let length = end.sub(*start).length();
2283 *distance > length || length <= 0.0
2284 }
2285 };
2286 if parity_decides {
2287 if let Some(class) = sphere_chart_point_in_face(face, point, &prepared, &generic.loops)? {
2288 trace_lane(face, point, "sphere_chart", class);
2289 return Ok(class);
2290 }
2291 }
2292 let Some((segment_start, segment_end, curve, prep, distance)) = chord else {
2293 trace_lane(face, point, "parity (no segment)", parity);
2294 return Ok(parity);
2295 };
2296 let segment_length = segment_end.sub(segment_start).length();
2297 if distance > segment_length || segment_length <= 0.0 {
2298 trace_lane(face, point, "parity (far from every segment)", parity);
2299 return Ok(parity);
2300 }
2301 let chord_parameter = prep.parameter_start
2302 + (prep.parameter_end - prep.parameter_start)
2303 * (point.sub(segment_start).dot(segment_end.sub(segment_start))
2304 / (segment_length * segment_length))
2305 .clamp(0.0, 1.0);
2306 let [domain_start, domain_end] = curve.domain()?;
2307 let small = crate::curve::small_curve_derivatives_enabled();
2308 let read = |parameter: f64, count: usize| -> Result<[Vec3; 3], String> {
2309 if small {
2310 return curve.derivatives_small(parameter, count);
2311 }
2312 let heap = curve.derivatives(parameter, count)?;
2313 let mut grid = [Vec3::default(); 3];
2314 grid[..heap.len()].copy_from_slice(&heap);
2315 Ok(grid)
2316 };
2317 use crate::projection::{curve_foot_newton, CurveFootPolicy, FootDomain, FootMode};
2318 let (parameter, _) = curve_foot_newton(curve, Vec3::new(point.x, point.y, 0.0),
2319 chord_parameter, [domain_start, domain_end], CurveFootPolicy {
2320 domain: FootDomain::Clamp,
2321 iterations: 12,
2322 slope_guard: 1e-30,
2323 step_limit: f64::INFINITY,
2324 parameter_tolerance: 1e-14 * (domain_end - domain_start + 1.0),
2325 mode: FootMode::Planar,
2326 })?;
2327 let margin = 1e-9 * (domain_end - domain_start);
2328 if parameter > domain_start + margin && parameter < domain_end - margin {
2329 let derivatives = read(parameter, 1)?;
2330 let on_curve = Vec2 {
2331 x: derivatives[0].x,
2332 y: derivatives[0].y,
2333 };
2334 let tangent = Vec2 {
2335 x: derivatives[1].x,
2336 y: derivatives[1].y,
2337 };
2338 let offset = point.sub(on_curve);
2339 if offset.length() <= tolerance {
2340 trace_lane(face, point, "nearest curve (on it)", PolygonClass::Boundary);
2341 return Ok(PolygonClass::Boundary);
2342 }
2343 let cross = tangent.x * offset.y - tangent.y * offset.x;
2344 if cross.abs() > 1e-30 {
2345 let class = if (cross > 0.0) == face.same_sense {
2346 PolygonClass::Inside
2347 } else {
2348 PolygonClass::Outside
2349 };
2350 trace_lane(face, point, "nearest curve (side)", class);
2351 return Ok(class);
2352 }
2353 }
2354 trace_lane(face, point, "parity (nearest curve declined)", parity);
2355 Ok(parity)
2356}
2357
2358