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 sphere_region_reduced: std::cell::OnceCell<Result<Option<(crate::sphere_chart::SphericalRegion, f64)>, String>>,
647 loop_samples: std::cell::RefCell<Vec<Option<Result<std::rc::Rc<Vec<Vec2>>, String>>>>,
652}
653
654impl PreparedFace {
655 fn new() -> Self {
656 Self {
657 seam_band: std::cell::OnceCell::new(),
658 sphere_cap: std::cell::OnceCell::new(),
659 horizon: std::cell::OnceCell::new(),
660 rim_strip: std::cell::OnceCell::new(),
661 generic: std::cell::OnceCell::new(),
662 sphere_region: std::cell::OnceCell::new(),
663 sphere_region_reduced: std::cell::OnceCell::new(),
664 loop_samples: std::cell::RefCell::new(Vec::new()),
665 }
666 }
667}
668
669fn loop_samples(prepared: &PreparedFace, face: &FaceRecord, loop_index: usize) -> Result<std::rc::Rc<Vec<Vec2>>, String> {
679 if let Some(Some(sampled)) = prepared.loop_samples.borrow().get(loop_index) {
680 return sampled.clone();
681 }
682 let sampled = (|| {
683 let mut points: Vec<Vec2> = Vec::new();
684 for coedge in &face.loops[loop_index].coedges {
685 let curve = &coedge.pcurve;
686 let [start, end] = curve.domain()?;
687 let sample_count = trim_sample_count(curve);
688 for index in 0..sample_count {
689 let parameter = trim_station(start, end, index, sample_count);
690 let evaluated = curve.evaluate(parameter)?;
691 points.push(Vec2 {
692 x: evaluated.x,
693 y: evaluated.y,
694 });
695 }
696 }
697 Ok(std::rc::Rc::new(points))
698 })();
699 let mut cells = prepared.loop_samples.borrow_mut();
700 if cells.len() < face.loops.len() {
701 cells.resize(face.loops.len(), None);
702 }
703 cells[loop_index] = Some(sampled.clone());
704 sampled
705}
706
707#[derive(Default, Clone, Copy)]
717struct ContainmentProfile {
718 enabled: bool,
719 cache_misses: u64,
720 seam_band_preps: u64,
721 seam_band_ms: f64,
722 sphere_cap_preps: u64,
723 sphere_cap_ms: f64,
724 horizon_preps: u64,
725 horizon_ms: f64,
726 rim_strip_preps: u64,
727 rim_strip_ms: f64,
728 generic_preps: u64,
729 generic_ms: f64,
730 generic_chords: u64,
731 index_builds: u64,
732 index_ms: f64,
733 region_builds: u64,
734 region_evaluate_ms: f64,
735 region_canonical_ms: f64,
736 region_segments_ms: f64,
737}
738
739thread_local! {
740 static CONTAINMENT_PROFILE: std::cell::Cell<ContainmentProfile> =
741 const { std::cell::Cell::new(ContainmentProfile {
742 enabled: false, cache_misses: 0, seam_band_preps: 0, seam_band_ms: 0.0,
743 sphere_cap_preps: 0, sphere_cap_ms: 0.0, horizon_preps: 0, horizon_ms: 0.0,
744 rim_strip_preps: 0, rim_strip_ms: 0.0, generic_preps: 0, generic_ms: 0.0,
745 generic_chords: 0, index_builds: 0, index_ms: 0.0, region_builds: 0,
746 region_evaluate_ms: 0.0, region_canonical_ms: 0.0, region_segments_ms: 0.0,
747 }) };
748}
749
750impl crate::profile::Counters for ContainmentProfile {
751 fn armed(&self) -> bool {
752 self.enabled
753 }
754}
755
756fn containment_profile(update: impl FnOnce(&mut ContainmentProfile)) {
757 crate::profile::update(&CONTAINMENT_PROFILE, update)
758}
759
760fn containment_profile_started() -> Option<web_time::Instant> {
761 crate::profile::started(&CONTAINMENT_PROFILE)
762}
763
764use crate::profile::elapsed_ms as containment_elapsed_ms;
765
766pub(crate) fn containment_profile_begin() {
768 let enabled = crate::profile::profile_enabled();
769 CONTAINMENT_PROFILE.with(|cell| {
770 cell.set(ContainmentProfile {
771 enabled,
772 ..ContainmentProfile::default()
773 })
774 });
775}
776
777pub(crate) fn containment_profile_report(label: &str) {
780 let p = CONTAINMENT_PROFILE.with(|cell| cell.get());
781 if !p.enabled {
782 return;
783 }
784 profile_line!(
785 "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}",
786 p.cache_misses, p.seam_band_preps, p.seam_band_ms, p.sphere_cap_preps, p.sphere_cap_ms,
787 p.horizon_preps, p.horizon_ms, p.rim_strip_preps, p.rim_strip_ms, p.generic_preps,
788 p.generic_ms, p.generic_chords, p.index_builds, p.index_ms, p.region_builds,
789 p.region_evaluate_ms, p.region_canonical_ms, p.region_segments_ms,
790 );
791 CONTAINMENT_PROFILE.with(|cell| cell.set(ContainmentProfile::default()));
792}
793
794fn timed_lane<'a, T>(
797 cell: &'a std::cell::OnceCell<Result<T, String>>,
798 build: impl FnOnce() -> Result<T, String>,
799 record: fn(&mut ContainmentProfile, f64),
800) -> Result<&'a T, String> {
801 lane(cell, || {
802 let started = containment_profile_started();
803 let built = build();
804 let ms = containment_elapsed_ms(started);
805 containment_profile(|p| record(p, ms));
806 built
807 })
808}
809
810fn lane<'a, T>(
811 cell: &'a std::cell::OnceCell<Result<T, String>>,
812 build: impl FnOnce() -> Result<T, String>,
813) -> Result<&'a T, String> {
814 match cell.get_or_init(build) {
815 Ok(value) => Ok(value),
816 Err(error) => Err(error.clone()),
817 }
818}
819
820struct PreparedFaceCache {
838 entries: Vec<(u64, Vec<f64>, std::rc::Rc<PreparedFace>)>,
839}
840
841const PREPARED_FACE_CACHE_ENTRIES: usize = 64;
842
843thread_local! {
844 static PREPARED_FACES: std::cell::RefCell<PreparedFaceCache> = const {
845 std::cell::RefCell::new(PreparedFaceCache {
846 entries: Vec::new(),
847 })
848 };
849}
850
851fn coedge_slots(face: &FaceRecord) -> Vec<usize> {
865 let ids: Vec<u64> = face.loops.iter().flat_map(|record| record.coedges.iter().map(|coedge| coedge.edge_id)).collect();
866 ids.iter().enumerate().map(|(at, id)| ids[..at].iter().position(|earlier| earlier == id).unwrap_or(at)).collect()
867}
868
869fn walk_face_signature<F: FnMut(f64) -> bool>(face: &FaceRecord, switches: f64, visit: &mut F) -> bool {
870 let slots = coedge_slots(face);
871 let mut flat = 0usize;
872 let surface = &face.surface;
873 let point = |visit: &mut F, p: &crate::Vec4| visit(p.x) && visit(p.y) && visit(p.z) && visit(p.w);
874 if !(visit(switches)
875 && visit(if face.same_sense { 1.0 } else { 0.0 })
876 && visit(surface.degree_u as f64)
877 && visit(surface.degree_v as f64)
878 && visit(surface.knots_u.len() as f64))
879 {
880 return false;
881 }
882 if !(surface.knots_u.iter().all(|&knot| visit(knot)) && visit(surface.knots_v.len() as f64)) {
883 return false;
884 }
885 if !(surface.knots_v.iter().all(|&knot| visit(knot)) && visit(surface.control_points.len() as f64)) {
886 return false;
887 }
888 for row in &surface.control_points {
889 if !(visit(row.len() as f64) && row.iter().all(|p| point(visit, p))) {
890 return false;
891 }
892 }
893 if !visit(face.loops.len() as f64) {
894 return false;
895 }
896 for loop_record in &face.loops {
897 if !visit(loop_record.coedges.len() as f64) {
898 return false;
899 }
900 for coedge in &loop_record.coedges {
901 let curve = &coedge.pcurve;
902 let slot = slots[flat];
903 flat += 1;
904 if !(visit(slot as f64)
905 && visit(if coedge.forward { 1.0 } else { 0.0 })
906 && visit(curve.degree as f64)
907 && visit(curve.knots.len() as f64))
908 {
909 return false;
910 }
911 if !(curve.knots.iter().all(|&knot| visit(knot)) && visit(curve.control_points.len() as f64)) {
912 return false;
913 }
914 if !curve.control_points.iter().all(|p| point(visit, p)) {
915 return false;
916 }
917 }
918 }
919 true
920}
921
922fn face_key_hash(face: &FaceRecord, switches: f64) -> u64 {
927 use std::hash::Hasher;
928 let slots = coedge_slots(face);
929 let mut flat = 0usize;
930 let mut hasher = rustc_hash::FxHasher::default();
931 let mut write = |value: f64| hasher.write_u64(value.to_bits());
932 let surface = &face.surface;
933 write(switches);
934 write(if face.same_sense { 1.0 } else { 0.0 });
935 write(surface.degree_u as f64);
936 write(surface.degree_v as f64);
937 write(surface.knots_u.len() as f64);
938 write(surface.knots_v.len() as f64);
939 write(surface.control_points.len() as f64);
940 let corners = [surface.control_points.first().and_then(|row| row.first()), surface.control_points.last().and_then(|row| row.last())];
941 for p in corners.into_iter().flatten() {
942 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
943 }
944 write(face.loops.len() as f64);
945 for loop_record in &face.loops {
946 write(loop_record.coedges.len() as f64);
947 for coedge in &loop_record.coedges {
948 let curve = &coedge.pcurve;
949 write(slots[flat] as f64);
950 flat += 1;
951 write(if coedge.forward { 1.0 } else { 0.0 });
952 write(curve.degree as f64);
953 write(curve.knots.len() as f64);
954 write(curve.control_points.len() as f64);
955 for knot in [curve.knots.first(), curve.knots.last()].into_iter().flatten() {
956 write(*knot);
957 }
958 for p in [curve.control_points.first(), curve.control_points.last()].into_iter().flatten() {
959 [p.x, p.y, p.z, p.w].into_iter().for_each(&mut write);
960 }
961 }
962 }
963 hasher.finish()
964}
965
966fn cached_entry(
968 entries: &[(u64, Vec<f64>, std::rc::Rc<PreparedFace>)],
969 face: &FaceRecord,
970 switches: f64,
971 hash: u64,
972) -> Option<usize> {
973 entries.iter().position(|(entry_hash, signature, _)| {
974 if *entry_hash != hash {
975 return false;
976 }
977 let mut at = 0;
978 let walked = walk_face_signature(face, switches, &mut |value: f64| {
979 let same = signature
980 .get(at)
981 .is_some_and(|stored| stored.to_bits() == value.to_bits() && !value.is_nan());
982 at += 1;
983 same
984 });
985 walked && at == signature.len()
986 })
987}
988
989pub(crate) fn forget_prepared_faces() {
992 PREPARED_FACES.with(|cache| cache.borrow_mut().entries.clear());
993}
994
995
996fn cached_prepared_face(face: &FaceRecord) -> Option<std::rc::Rc<PreparedFace>> {
1000 PREPARED_FACES.with(|cache| {
1001 let PreparedFaceCache { entries } = &*cache.borrow();
1002 let switches = lane_switches();
1003 let hash = face_key_hash(face, switches);
1004 cached_entry(entries, face, switches, hash).map(|index| entries[index].2.clone())
1005 })
1006}
1007
1008fn prepared_face(face: &FaceRecord) -> std::rc::Rc<PreparedFace> {
1009 PREPARED_FACES.with(|cache| {
1010 let PreparedFaceCache { entries } = &mut *cache.borrow_mut();
1011 let switches = lane_switches();
1012 let hash = face_key_hash(face, switches);
1013 if let Some(index) = cached_entry(entries, face, switches, hash) {
1014 if index != 0 {
1015 let entry = entries.remove(index);
1016 entries.insert(0, entry);
1017 }
1018 return entries[0].2.clone();
1019 }
1020 let mut signature = Vec::new();
1021 walk_face_signature(face, switches, &mut |value: f64| {
1022 signature.push(value);
1023 true
1024 });
1025 let prepared = std::rc::Rc::new(PreparedFace::new());
1026 entries.insert(0, (hash, signature, prepared.clone()));
1027 entries.truncate(PREPARED_FACE_CACHE_ENTRIES);
1028 containment_profile(|p| p.cache_misses += 1);
1029 prepared
1030 })
1031}
1032
1033fn prepare_seam_band(face: &FaceRecord) -> Result<SeamBandPrep, String> {
1042 if face.surface.closed_directions()? != (true, true) {
1043 return Ok(SeamBandPrep::NotApplicable);
1044 }
1045 let [u0, u1] = face.surface.domain_u()?;
1046 let [v0, v1] = face.surface.domain_v()?;
1047 if crate::topology::doubly_periodic_has_only_collapsed_loops(face)? {
1048 return Ok(SeamBandPrep::AlwaysInside);
1049 }
1050 let u_span = (u1 - u0).abs().max(1e-30);
1051 let v_span = (v1 - v0).abs().max(1e-30);
1052 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
1053 let mut retained: Vec<usize> = Vec::with_capacity(face.loops.len());
1057 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1058 let mut points: Vec<[f64; 2]> = Vec::new();
1059 for coedge in &loop_record.coedges {
1060 let [d0, d1] = coedge.pcurve.domain()?;
1061 let samples = 24;
1062 for k in 0..=samples {
1063 let t = trim_station(d0, d1, k, samples);
1064 let p = coedge.pcurve.evaluate(t)?;
1065 points.push([p.x, p.y]);
1066 }
1067 }
1068 let (mut umin, mut umax, mut vmin, mut vmax) = (
1069 f64::INFINITY,
1070 f64::NEG_INFINITY,
1071 f64::INFINITY,
1072 f64::NEG_INFINITY,
1073 );
1074 for p in &points {
1075 umin = umin.min(p[0]);
1076 umax = umax.max(p[0]);
1077 vmin = vmin.min(p[1]);
1078 vmax = vmax.max(p[1]);
1079 }
1080 if (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span {
1100 continue;
1101 }
1102 retained.push(loop_index);
1103 loops_uv.push(points);
1104 }
1105 if loops_uv.len() == 1 && switch!(SEAM_BAND_MERGED_SWITCH, true) {
1116 let coedges = &face.loops[retained[0]].coedges;
1117 let offsets = crate::topology::loop_seam_offsets(coedges, true, true, u_span, v_span)?;
1118 if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
1119 let mut polygon: Vec<Vec2> = Vec::new();
1120 for (coedge_index, coedge) in coedges.iter().enumerate() {
1121 let [d0, d1] = coedge.pcurve.domain()?;
1122 let samples = 24;
1123 for k in 0..samples {
1124 let t = trim_station(d0, d1, k, samples);
1125 let p = coedge.pcurve.evaluate(t)?;
1126 polygon.push(Vec2 {
1127 x: p.x + offsets[coedge_index][0],
1128 y: p.y + offsets[coedge_index][1],
1129 });
1130 }
1131 }
1132 return Ok(SeamBandPrep::Merged { polygon, u_span, v_span });
1133 }
1134 }
1135 if loops_uv.len() != 2 {
1136 return Ok(SeamBandPrep::Declined);
1137 }
1138 if let Some(band) = crate::topology::analyze_doubly_periodic_seam_band(
1139 &loops_uv,
1140 [u0, u1, v0, v1],
1141 face.same_sense,
1142 ) {
1143 let polygon: Vec<Vec2> =
1144 crate::topology::seam_band_uv_polygon(&loops_uv, [u0, u1, v0, v1], &band)
1145 .into_iter()
1146 .map(|p| Vec2 { x: p[0], y: p[1] })
1147 .collect();
1148 return Ok(SeamBandPrep::Analyzed { polygon });
1149 }
1150 for p_is_u in [true, false] {
1157 let (period, q_extent) = if p_is_u {
1158 (u1 - u0, v_span)
1159 } else {
1160 (v1 - v0, u_span)
1161 };
1162 if !(period > 0.0) {
1163 continue;
1164 }
1165 let coord = |p: &[f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
1166 let mut rings = Vec::with_capacity(2);
1167 for points in &loops_uv {
1168 let (mut pmin, mut pmax, mut qmin, mut qmax) = (
1169 f64::INFINITY,
1170 f64::NEG_INFINITY,
1171 f64::INFINITY,
1172 f64::NEG_INFINITY,
1173 );
1174 for p in points {
1175 let (periodic, cross) = coord(p);
1176 pmin = pmin.min(periodic);
1177 pmax = pmax.max(periodic);
1178 qmin = qmin.min(cross);
1179 qmax = qmax.max(cross);
1180 }
1181 if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
1182 rings.clear();
1183 break;
1184 }
1185 let mut net = 0.0;
1186 for pair in points.windows(2) {
1187 let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
1188 if delta > 0.5 * period {
1189 delta -= period;
1190 } else if delta < -0.5 * period {
1191 delta += period;
1192 }
1193 net += delta;
1194 }
1195 let direction = if net > 0.25 * period {
1196 1
1197 } else if net < -0.25 * period {
1198 -1
1199 } else {
1200 0
1201 };
1202 rings.push((0.5 * (qmin + qmax), direction));
1203 }
1204 if rings.len() != 2 {
1205 continue;
1206 }
1207 rings.sort_by(|a, b| a.0.total_cmp(&b.0));
1208 let [(q_lo, lower_direction), (q_hi, upper_direction)] = rings.as_slice() else {
1209 unreachable!()
1210 };
1211 let inconclusive =
1212 *lower_direction == 0 || *upper_direction == 0 || lower_direction == upper_direction;
1213 let between_is_ccw_uv = if p_is_u {
1214 *lower_direction > 0
1215 } else {
1216 *lower_direction < 0
1217 };
1218 let complement = !inconclusive && between_is_ccw_uv != face.same_sense;
1219 return Ok(SeamBandPrep::Rings {
1220 p_is_u,
1221 q_lo: *q_lo,
1222 q_hi: *q_hi,
1223 complement,
1224 });
1225 }
1226 Ok(SeamBandPrep::Declined)
1227}
1228
1229fn seam_band_point_in_face(
1230 prepared: &SeamBandPrep,
1231 point: Vec2,
1232 tolerance: f64,
1233) -> Option<PolygonClass> {
1234 match prepared {
1235 SeamBandPrep::NotApplicable | SeamBandPrep::Declined => None,
1236 SeamBandPrep::AlwaysInside => Some(PolygonClass::Inside),
1237 SeamBandPrep::Merged { polygon, u_span, v_span } => {
1238 let mut best = PolygonClass::Outside;
1239 'images: for du in [-1.0, 0.0, 1.0] {
1240 for dv in [-1.0, 0.0, 1.0] {
1241 let image = Vec2 {
1242 x: point.x + du * u_span,
1243 y: point.y + dv * v_span,
1244 };
1245 match point_in_polygon(image, polygon, tolerance) {
1246 PolygonClass::Boundary => {
1247 best = PolygonClass::Boundary;
1248 break 'images;
1249 }
1250 PolygonClass::Inside => best = PolygonClass::Inside,
1251 PolygonClass::Outside => {}
1252 }
1253 }
1254 }
1255 Some(best)
1256 }
1257 SeamBandPrep::Analyzed { polygon } => Some(point_in_polygon(point, polygon, tolerance)),
1258 SeamBandPrep::Rings { p_is_u, q_lo, q_hi, complement } => {
1259 let q = if *p_is_u { point.y } else { point.x };
1260 if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
1261 return Some(PolygonClass::Boundary);
1262 }
1263 let between = q > *q_lo && q < *q_hi;
1264 Some(if between != *complement {
1265 PolygonClass::Inside
1266 } else {
1267 PolygonClass::Outside
1268 })
1269 }
1270 }
1271}
1272
1273fn prepare_wrapped_horizon(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<HorizonPrep>, String> {
1289 if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
1290 return Ok(None);
1291 }
1292 if !switch!(HORIZON_CONTAINMENT_SWITCH, true) {
1293 return Ok(None);
1294 }
1295 if face.loops.len() <= 2 && !switch!(HORIZON_SINGLE_LOOP_SWITCH, true) {
1306 return Ok(None);
1307 }
1308 let [u0, u1] = face.surface.domain_u()?;
1309 let u_period = u1 - u0;
1310 if u_period <= 0.0 {
1311 return Ok(None);
1312 }
1313 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1314 let mut loops: Vec<Vec<Vec2>> = Vec::new();
1315 let mut straddling = false;
1316 for loop_index in 0..face.loops.len() {
1317 let points = loop_samples(prepared, face, loop_index)?;
1318 if points.len() < 3 {
1319 continue;
1320 }
1321 let mut unwrapped = Vec::with_capacity(points.len());
1325 let mut jumps = 0usize;
1326 let mut cursor = points[0];
1327 unwrapped.push(cursor);
1328 for pair in points.windows(2) {
1329 let mut du = pair[1].x - pair[0].x;
1330 let folded = du - u_period * (du / u_period).round();
1331 if (du - folded).abs() > 0.25 * u_period {
1332 jumps += 1;
1333 }
1334 du = folded;
1335 cursor = Vec2 {
1336 x: cursor.x + du,
1337 y: pair[1].y,
1338 };
1339 unwrapped.push(cursor);
1340 }
1341 let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
1342 if closure > 0.25 * u_period {
1343 if debug {
1344 eprintln!(
1345 "horizon: face {} loop winds the period (closure {closure:.3}) — bail",
1346 face.id
1347 );
1348 }
1349 return Ok(None); }
1351 if jumps > 0 {
1352 straddling = true;
1353 }
1354 loops.push(unwrapped);
1355 }
1356 if !straddling || loops.is_empty() {
1357 return Ok(None);
1358 }
1359 Ok(Some(HorizonPrep {
1360 loops,
1361 u_period,
1362 index: std::cell::OnceCell::new(),
1363 }))
1364}
1365
1366fn wrapped_horizon_point_in_face(
1367 face: &FaceRecord,
1368 prepared: &HorizonPrep,
1369 point: Vec2,
1370 tolerance: f64,
1371 scan: ChordScan,
1372) -> Option<PolygonClass> {
1373 let HorizonPrep { loops, u_period, index } = prepared;
1374 let u_period = *u_period;
1375 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
1376 if !horizon_cross_frame() {
1391 let mut best: Option<PolygonClass> = None;
1392 for shift in [-u_period, 0.0, u_period] {
1393 let image = Vec2 {
1394 x: point.x + shift,
1395 y: point.y,
1396 };
1397 let mut crossings = 0usize;
1398 for polygon in loops {
1399 match point_in_polygon(image, polygon, tolerance) {
1400 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1401 PolygonClass::Inside => crossings += 1,
1402 PolygonClass::Outside => {}
1403 }
1404 }
1405 if crossings % 2 == 1 {
1406 best = Some(PolygonClass::Inside);
1407 } else if best.is_none() {
1408 best = Some(PolygonClass::Outside);
1409 }
1410 }
1411 if debug {
1412 eprintln!(
1413 "horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
1414 face.id,
1415 loops.len(),
1416 best
1417 );
1418 }
1419 return best;
1420 }
1421 let images = [-u_period, 0.0, u_period].map(|shift| Vec2 {
1422 x: point.x + shift,
1423 y: point.y,
1424 });
1425 let chords: usize = loops.iter().map(Vec::len).sum();
1426 let indexed = (scan.indexes(chords) && images.iter().all(|image| in_index_range(*image)))
1427 .then(|| index.get_or_init(|| ChordIndex::build(loops.as_slice())).as_ref())
1428 .flatten();
1429 let mut crossings = 0usize;
1430 if let Some(index) = indexed {
1431 let loops = loops.as_slice();
1435 if images.iter().any(|image| index.any_within(loops, *image, tolerance)) {
1436 return Some(PolygonClass::Boundary);
1437 }
1438 let mut odd = vec![false; loops.len()];
1439 for image in images {
1440 index.flip_crossings(loops, image, &mut odd);
1441 }
1442 crossings = odd.into_iter().filter(|&parity| parity).count();
1443 } else {
1444 for polygon in loops {
1445 let mut image_hits = 0usize;
1446 for image in images {
1447 match point_in_polygon(image, polygon, tolerance) {
1448 PolygonClass::Boundary => return Some(PolygonClass::Boundary),
1449 PolygonClass::Inside => image_hits += 1,
1450 PolygonClass::Outside => {}
1451 }
1452 }
1453 crossings += image_hits % 2;
1454 }
1455 }
1456 let class = if crossings % 2 == 1 {
1457 PolygonClass::Inside
1458 } else {
1459 PolygonClass::Outside
1460 };
1461 if debug {
1462 eprintln!(
1463 "horizon: face {} loops={} straddling -> {:?}",
1464 face.id,
1465 loops.len(),
1466 class
1467 );
1468 }
1469 Some(class)
1470}
1471
1472fn prepare_winding_sphere_cap(face: &FaceRecord, prepared: &PreparedFace) -> Result<Option<SphereCapPrep>, String> {
1477 if !matches!(
1478 face.surface.analytic(),
1479 Some(crate::AnalyticSurface::Sphere { .. })
1480 ) || face.surface.closed_directions()? != (true, false)
1481 || face.loops.len() != 2
1482 {
1483 return Ok(None);
1484 }
1485 let [u0, u1] = face.surface.domain_u()?;
1486 let [v0, v1] = face.surface.domain_v()?;
1487 let period = u1 - u0;
1488 let v_span = v1 - v0;
1489 if !(period > 0.0 && v_span > 0.0) {
1490 return Ok(None);
1491 }
1492 struct WindingLoop {
1493 points: Vec<Vec2>,
1494 winding: f64,
1495 vmin: f64,
1496 vmax: f64,
1497 }
1498 let mut loops = Vec::with_capacity(2);
1499 for loop_index in 0..face.loops.len() {
1500 let points = loop_samples(prepared, face, loop_index)?;
1501 if points.len() < 2 {
1502 return Ok(None);
1503 }
1504 let first = points[0];
1505 let mut cursor = first;
1506 let mut unwrapped = vec![cursor];
1507 for next in points.iter().skip(1) {
1508 let du = next.x - cursor.x;
1509 let folded = du - period * (du / period).round();
1510 cursor = Vec2 {
1511 x: cursor.x + folded,
1512 y: next.y,
1513 };
1514 unwrapped.push(cursor);
1515 }
1516 let last_raw = *points.last().unwrap();
1517 let closing_du = first.x - last_raw.x;
1518 let closing_folded = closing_du - period * (closing_du / period).round();
1519 let winding = cursor.x + closing_folded - first.x;
1520 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1521 for p in &unwrapped {
1522 vmin = vmin.min(p.y);
1523 vmax = vmax.max(p.y);
1524 }
1525 loops.push(WindingLoop {
1526 points: unwrapped,
1527 winding,
1528 vmin,
1529 vmax,
1530 });
1531 }
1532 if loops
1533 .iter()
1534 .any(|loop_data| (loop_data.winding.abs() - period).abs() > 0.05 * period)
1535 || loops[0].winding * loops[1].winding >= 0.0
1536 {
1537 return Ok(None);
1538 }
1539 let flat = |loop_data: &WindingLoop| loop_data.vmax - loop_data.vmin <= 1e-6 * v_span;
1540 let (pole, rim) = match (flat(&loops[0]), flat(&loops[1])) {
1541 (true, false) => (&loops[0], &loops[1]),
1542 (false, true) => (&loops[1], &loops[0]),
1543 _ => return Ok(None),
1544 };
1545 let pole_v = 0.5 * (pole.vmin + pole.vmax);
1546 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
1547 return Ok(None);
1548 }
1549 Ok(Some(SphereCapPrep {
1550 rim: rim.points.clone(),
1551 pole_v,
1552 period,
1553 }))
1554}
1555
1556fn winding_sphere_cap_point_in_face(
1557 prepared: &SphereCapPrep,
1558 point: Vec2,
1559 tolerance: f64,
1560) -> Option<PolygonClass> {
1561 let SphereCapPrep { rim, pole_v, period } = prepared;
1562 let (pole_v, period) = (*pole_v, *period);
1563 let window_start = rim[0].x.min(rim[rim.len() - 1].x);
1564 let query_u = window_start + (point.x - window_start).rem_euclid(period);
1565 let mut crossings = Vec::new();
1566 for pair in rim.windows(2) {
1567 let (a, b) = (pair[0], pair[1]);
1568 if (a.x > query_u) != (b.x > query_u) {
1569 crossings.push(a.y + (query_u - a.x) / (b.x - a.x) * (b.y - a.y));
1570 }
1571 for image in [-period, 0.0, period] {
1572 if point_segment_distance(
1573 Vec2 {
1574 x: point.x + image,
1575 y: point.y,
1576 },
1577 a,
1578 b,
1579 ) <= tolerance
1580 {
1581 return Some(PolygonClass::Boundary);
1582 }
1583 }
1584 }
1585 let rim_v = crossings
1586 .into_iter()
1587 .min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))?;
1588 let inside = if pole_v < rim_v {
1589 point.y <= rim_v + tolerance
1590 } else {
1591 point.y >= rim_v - tolerance
1592 };
1593 Some(if inside {
1594 PolygonClass::Inside
1595 } else {
1596 PolygonClass::Outside
1597 })
1598}
1599
1600fn prepare_covering_rim_strip(face: &FaceRecord) -> Result<Option<RimStripPrep>, String> {
1632 if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
1633 return Ok(None);
1634 }
1635 if !switch!(COVERING_RIM_STRIP_SWITCH, true) {
1636 return Ok(None);
1637 }
1638 let [u0, u1] = face.surface.domain_u()?;
1639 let [v0, v1] = face.surface.domain_v()?;
1640 let period = u1 - u0;
1641 if !(period > 0.0) {
1642 return Ok(None);
1643 }
1644 let v_span = (v1 - v0).abs().max(1e-30);
1645 struct Rim {
1646 points: Vec<Vec2>,
1647 vmin: f64,
1648 vmax: f64,
1649 ascending: bool,
1650 }
1651 let mut rims: Vec<Rim> = Vec::with_capacity(2);
1652 let mut out_of_domain = false;
1653 for loop_record in &face.loops {
1654 let mut points: Vec<Vec2> = Vec::new();
1655 for coedge in &loop_record.coedges {
1656 let curve = &coedge.pcurve;
1657 let [start, end] = curve.domain()?;
1658 let sample_count = trim_sample_count(curve);
1659 for index in 0..=sample_count {
1660 let parameter = trim_station(start, end, index, sample_count);
1661 let evaluated = curve.evaluate(parameter)?;
1662 points.push(Vec2 {
1663 x: evaluated.x,
1664 y: evaluated.y,
1665 });
1666 }
1667 }
1668 if points.len() < 3 {
1669 return Ok(None);
1670 }
1671 for p in &points {
1674 if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
1675 out_of_domain = true;
1676 }
1677 }
1678 let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
1682 let mut cursor = points[0];
1683 unwrapped.push(cursor);
1684 for pair in points.windows(2) {
1685 let du = pair[1].x - pair[0].x;
1686 let folded = du - period * (du / period).round();
1687 cursor = Vec2 {
1688 x: cursor.x + folded,
1689 y: pair[1].y,
1690 };
1691 unwrapped.push(cursor);
1692 }
1693 let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
1694 if (net.abs() - period).abs() > 2e-2 * period {
1695 return Ok(None); }
1697 if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
1698 return Ok(None); }
1700 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
1701 for p in &unwrapped {
1702 vmin = vmin.min(p.y);
1703 vmax = vmax.max(p.y);
1704 }
1705 rims.push(Rim {
1706 points: unwrapped,
1707 vmin,
1708 vmax,
1709 ascending: net > 0.0,
1710 });
1711 }
1712 if !out_of_domain {
1713 return Ok(None);
1714 }
1715 if rims[0].ascending == rims[1].ascending {
1716 return Ok(None); }
1718 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
1719 (&rims[0], &rims[1])
1720 } else if rims[1].vmax <= rims[0].vmin {
1721 (&rims[1], &rims[0])
1722 } else {
1723 return Ok(None); };
1725 if upper.vmin - lower.vmax <= 1e-6 * v_span {
1726 return Ok(None);
1727 }
1728 if (lower.ascending) != face.same_sense {
1733 return Ok(None);
1734 }
1735 Ok(Some(RimStripPrep {
1736 lower: lower.points.clone(),
1737 upper: upper.points.clone(),
1738 period,
1739 }))
1740}
1741
1742fn covering_rim_strip_point_in_face(
1743 prepared: &RimStripPrep,
1744 point: Vec2,
1745 tolerance: f64,
1746) -> Option<PolygonClass> {
1747 let RimStripPrep { lower, upper, period } = prepared;
1748 let period = *period;
1749 for rim in [lower, upper] {
1751 for shift in [-period, 0.0, period] {
1752 let image = Vec2 {
1753 x: point.x + shift,
1754 y: point.y,
1755 };
1756 for pair in rim.windows(2) {
1757 if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
1758 return Some(PolygonClass::Boundary);
1759 }
1760 }
1761 }
1762 }
1763 let mut crossings = 0usize;
1766 for rim in [lower, upper] {
1767 let window_base = rim[0].x.min(rim[rim.len() - 1].x);
1768 let x = window_base + (point.x - window_base).rem_euclid(period);
1769 for pair in rim.windows(2) {
1770 let (a, b) = (pair[0], pair[1]);
1771 if (a.x > x) != (b.x > x) {
1772 let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
1773 if v_cross > point.y {
1774 crossings += 1;
1775 }
1776 }
1777 }
1778 }
1779 Some(if crossings % 2 == 1 {
1780 PolygonClass::Inside
1781 } else {
1782 PolygonClass::Outside
1783 })
1784}
1785
1786fn prepare_generic(face: &FaceRecord, prepared: &PreparedFace) -> Result<GenericPrep, String> {
1790 let slots = coedge_slots(face);
1791 let mut flat = 0usize;
1792 let mut loops: Vec<LoopSamples> = Vec::with_capacity(face.loops.len());
1793 let mut segments: Vec<Vec<SegmentPrep>> = Vec::with_capacity(face.loops.len());
1794 let mut chord_count = 0;
1795 for (loop_index, loop_record) in face.loops.iter().enumerate() {
1796 let polygon = loop_samples(prepared, face, loop_index)?;
1797 let mut chords = Vec::with_capacity(polygon.len());
1798 let mut coedges = Vec::with_capacity(loop_record.coedges.len());
1799 let mut first = 0;
1800 for (coedge_index, coedge) in loop_record.coedges.iter().enumerate() {
1801 let curve = &coedge.pcurve;
1802 let [start, end] = curve.domain()?;
1803 let sample_count = trim_sample_count(curve);
1804 for index in 0..sample_count {
1805 let parameter = trim_station(start, end, index, sample_count);
1806 let parameter_end = if index + 1 < sample_count {
1807 trim_station(start, end, index + 1, sample_count)
1808 } else {
1809 end
1810 };
1811 chords.push(SegmentPrep {
1812 coedge_index,
1813 parameter_start: parameter,
1814 parameter_end,
1815 });
1816 }
1817 coedges.push((slots[flat] as u64, first, sample_count));
1818 flat += 1;
1819 first += sample_count;
1820 }
1821 debug_assert_eq!(first, polygon.len());
1822 chord_count += chords.len();
1823 loops.push(LoopSamples { polygon, coedges });
1824 segments.push(chords);
1825 }
1826 containment_profile(|p| p.generic_chords += chord_count as u64);
1827 Ok(GenericPrep {
1828 loops,
1829 segments,
1830 chord_count,
1831 index: std::cell::OnceCell::new(),
1832 })
1833}
1834
1835struct LoopSamples {
1840 polygon: std::rc::Rc<Vec<Vec2>>,
1841 coedges: Vec<(u64, usize, usize)>,
1842}
1843
1844fn sphere_chart_point_in_face(
1869 face: &FaceRecord,
1870 point: Vec2,
1871 prepared: &PreparedFace,
1872 sampled: &[LoopSamples],
1873) -> Result<Option<PolygonClass>, String> {
1874 let Some(atlas) = sphere_chart_atlas(face) else {
1875 return Ok(None);
1876 };
1877 if sphere_region_reduced_enabled() {
1878 let reduced = lane(&prepared.sphere_region_reduced, || build_sphere_region_reduced(face, &atlas, sampled))?;
1879 if let Some((region, margin)) = reduced {
1880 if !region.is_whole_sphere() && region.is_decidable() {
1881 let probe = face.surface.evaluate(point.x, point.y)?;
1882 if region.clear_of_boundary(atlas.centre, probe, *margin) {
1883 return Ok(Some(if region.contains(atlas.centre, probe) {
1884 PolygonClass::Inside
1885 } else {
1886 PolygonClass::Outside
1887 }));
1888 }
1889 }
1890 }
1891 }
1892 let region = lane(&prepared.sphere_region, || build_sphere_region(face, &atlas, sampled))?;
1893 if region.is_whole_sphere() {
1894 return Ok(Some(PolygonClass::Inside));
1895 }
1896 if !region.is_decidable() {
1897 return Ok(None);
1900 }
1901 let probe = face.surface.evaluate(point.x, point.y)?;
1902 Ok(Some(if region.contains(atlas.centre, probe) {
1903 PolygonClass::Inside
1904 } else {
1905 PolygonClass::Outside
1906 }))
1907}
1908
1909fn sphere_region_reduced_enabled() -> bool {
1912 crate::switch!("BREP_SPHERE_REGION_REDUCED", true)
1913}
1914
1915const REDUCED_REGION_SAMPLES: usize = 4096;
1917
1918fn build_sphere_region_reduced(
1936 face: &FaceRecord,
1937 atlas: &crate::sphere_chart::SphereAtlas,
1938 sampled: &[LoopSamples],
1939) -> Result<Option<(crate::sphere_chart::SphericalRegion, f64)>, String> {
1940 let total: usize = sampled.iter().map(|samples| samples.polygon.len()).sum();
1941 if total < REDUCED_REGION_SAMPLES {
1942 return Ok(None);
1943 }
1944 let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
1945 for slot in coedge_slots(face) {
1946 *uses.entry(slot as u64).or_insert(0) += 1;
1947 }
1948 let outward_face_normal = face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
1949 let memo = sphere_reduction_memo_enabled();
1950 let carrier = memo.then(|| {
1951 let mut lane = crate::mass_properties::TwoLane::new();
1952 crate::mass_properties::hash_surface(&mut lane, &face.surface);
1953 lane.finish()
1954 });
1955 let mut points: Vec<Vec3> = Vec::new();
1956 let mut spans: Vec<(usize, usize)> = Vec::new();
1957 let mut deviation = 0.0f64;
1958 for samples in sampled {
1959 let count = samples.polygon.len();
1960 if count < 2 {
1961 continue;
1962 }
1963 for &(edge_id, first, span) in &samples.coedges {
1964 if span == 0 || uses.get(&edge_id).copied().unwrap_or(0) >= 2 {
1965 continue;
1966 }
1967 let key = carrier.map(|carrier| {
1968 let mut lane = crate::mass_properties::TwoLane::new();
1969 lane.u64(carrier[0]);
1970 lane.u64(carrier[1]);
1971 lane.usize(span);
1972 for offset in 0..=span {
1973 let uv = samples.polygon[(first + offset) % count];
1974 lane.f64(uv.x);
1975 lane.f64(uv.y);
1976 }
1977 lane.finish()
1978 });
1979 if let Some(served) = key.and_then(|key| SPHERE_REDUCTIONS.with(|memo| memo.borrow().get(&key).cloned())) {
1980 let Some((kept, kept_deviation)) = served else {
1981 return Ok(None);
1982 };
1983 deviation = deviation.max(kept_deviation);
1984 let start = points.len();
1985 points.extend(kept.iter().copied());
1986 spans.push((start, points.len()));
1987 continue;
1988 }
1989 let reduced = reduce_sphere_coedge(face, atlas, &samples.polygon, first, span)?;
1990 if let Some(key) = key {
1991 SPHERE_REDUCTIONS.with(|memo| {
1992 let mut memo = memo.borrow_mut();
1993 if memo.len() >= SPHERE_REDUCTION_ENTRIES {
1994 memo.clear();
1995 }
1996 memo.insert(key, reduced.clone());
1997 });
1998 }
1999 let Some((kept, kept_deviation)) = reduced else {
2000 return Ok(None);
2001 };
2002 deviation = deviation.max(kept_deviation);
2003 let start = points.len();
2004 points.extend(kept.iter().copied());
2005 spans.push((start, points.len()));
2006 }
2007 }
2008 if 2 * points.len() > total {
2009 return Ok(None);
2010 }
2011 crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
2012 let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
2013 for (first, last) in spans {
2014 for index in first..last.saturating_sub(1) {
2015 boundary.push((points[index], points[index + 1]));
2016 }
2017 }
2018 let region = crate::sphere_chart::SphericalRegion::from_segments(atlas.centre, &boundary, outward_face_normal);
2019 let rounding = 4e-9 / atlas.radius.max(f64::MIN_POSITIVE);
2020 Ok(Some((region, 2.0 * deviation + rounding)))
2021}
2022
2023fn reduce_sphere_coedge(
2029 face: &FaceRecord,
2030 atlas: &crate::sphere_chart::SphereAtlas,
2031 polygon: &[Vec2],
2032 first: usize,
2033 span: usize,
2034) -> Result<Option<(std::rc::Rc<Vec<Vec3>>, f64)>, String> {
2035 use crate::sphere_chart::{arc_distance, arc_normal};
2036 let count = polygon.len();
2037 let mut along: Vec<Vec3> = Vec::with_capacity(span + 1);
2038 for offset in 0..=span {
2039 let uv = polygon[(first + offset) % count];
2040 along.push(face.surface.evaluate(uv.x, uv.y)?);
2041 }
2042 let Ok(units) = along.iter().map(|p| p.sub(atlas.centre).normalized()).collect::<Result<Vec<_>, _>>() else {
2043 return Ok(None);
2044 };
2045 let last = along.len() - 1;
2046 let mut chords: Vec<f64> = units.windows(2).map(|pair| pair[0].sub(pair[1]).length()).collect();
2047 chords.sort_by(f64::total_cmp);
2048 let target = 0.1 * chords[chords.len() / 2];
2049 let mut kept: Vec<usize> = (0..=last).collect();
2050 let mut kept_deviation = 0.0f64;
2051 for stride in [32usize, 16, 8, 4, 2] {
2052 if last < 2 * stride {
2053 continue;
2054 }
2055 let indices: Vec<usize> = (0..=last).filter(|&i| i.min(last - i) % stride == 0).collect();
2056 let mut worst = 0.0f64;
2057 for pair in indices.windows(2) {
2058 let (a, b) = (units[pair[0]], units[pair[1]]);
2059 let normal = arc_normal(a, b);
2060 for j in pair[0] + 1..pair[1] {
2061 worst = worst.max(arc_distance(a, b, normal, units[j]));
2062 }
2063 }
2064 if worst <= target {
2065 kept = indices;
2066 kept_deviation = worst;
2067 break;
2068 }
2069 }
2070 Ok(Some((std::rc::Rc::new(kept.iter().map(|&i| along[i]).collect()), kept_deviation)))
2071}
2072
2073fn sphere_reduction_memo_enabled() -> bool {
2080 let enabled = crate::switch!("BREP_SPHERE_REDUCTION_MEMO", true);
2081 enabled
2082}
2083
2084
2085const SPHERE_REDUCTION_ENTRIES: usize = 4096;
2087
2088thread_local! {
2089 static SPHERE_REDUCTIONS: std::cell::RefCell<std::collections::HashMap<[u64; 2], Option<(std::rc::Rc<Vec<Vec3>>, f64)>>> =
2093 std::cell::RefCell::new(std::collections::HashMap::new());
2094}
2095
2096fn build_sphere_region(
2114 face: &FaceRecord,
2115 atlas: &crate::sphere_chart::SphereAtlas,
2116 sampled: &[LoopSamples],
2117) -> Result<crate::sphere_chart::SphericalRegion, String> {
2118 let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
2122 for slot in coedge_slots(face) {
2123 *uses.entry(slot as u64).or_insert(0) += 1;
2124 }
2125 let outward_face_normal =
2150 face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
2151 let evaluate_started = containment_profile_started();
2152 let mut points: Vec<Vec3> = Vec::new();
2153 let mut spans: Vec<(usize, usize)> = Vec::new();
2154 for samples in sampled {
2155 let count = samples.polygon.len();
2156 if count < 2 {
2157 continue;
2158 }
2159 for &(edge_id, first, span) in &samples.coedges {
2160 if span == 0 || uses.get(&edge_id).copied().unwrap_or(0) >= 2 {
2161 continue;
2162 }
2163 let start = points.len();
2167 for offset in 0..=span {
2168 let uv = samples.polygon[(first + offset) % count];
2169 points.push(face.surface.evaluate(uv.x, uv.y)?);
2170 }
2171 spans.push((start, points.len()));
2172 }
2173 }
2174 let evaluate_ms = containment_elapsed_ms(evaluate_started);
2179 let canonical_started = containment_profile_started();
2180 crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
2181 let canonical_ms = containment_elapsed_ms(canonical_started);
2182 let segments_started = containment_profile_started();
2183 let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
2184 for (first, last) in spans {
2185 for index in first..last.saturating_sub(1) {
2186 boundary.push((points[index], points[index + 1]));
2187 }
2188 }
2189 let region = crate::sphere_chart::SphericalRegion::from_segments(
2190 atlas.centre,
2191 &boundary,
2192 outward_face_normal,
2193 );
2194 let segments_ms = containment_elapsed_ms(segments_started);
2195 containment_profile(|p| {
2196 p.region_builds += 1;
2197 p.region_evaluate_ms += evaluate_ms;
2198 p.region_canonical_ms += canonical_ms;
2199 p.region_segments_ms += segments_ms;
2200 });
2201 Ok(region)
2202}
2203
2204
2205fn sphere_chart_atlas(face: &FaceRecord) -> Option<crate::sphere_chart::SphereAtlas> {
2208 if !sphere_chart_trim_enabled() {
2209 return None;
2210 }
2211 crate::sphere_chart::SphereAtlas::of_surface(&face.surface)
2212}
2213
2214fn containment_trace_enabled() -> bool {
2218 debug_flag!("BREP_DEBUG_CONTAINMENT")
2219}
2220
2221fn trace_lane(face: &FaceRecord, point: Vec2, lane: &str, class: PolygonClass) {
2226 if containment_trace_enabled() {
2227 eprintln!(
2228 "containment face={} uv=({:.8},{:.8}) lane={lane} -> {class:?}",
2229 face.id, point.x, point.y
2230 );
2231 }
2232}
2233
2234
2235enum Lane<'a> {
2237 SeamBand(&'a SeamBandPrep),
2238 SphereCap(&'a SphereCapPrep),
2239 Horizon(&'a HorizonPrep),
2240 RimStrip(&'a RimStripPrep),
2241 Generic,
2242}
2243
2244fn lane_from<'a>(
2252 face: &FaceRecord,
2253 prepared: &'a PreparedFace,
2254 from: usize,
2255) -> Result<(usize, Lane<'a>), String> {
2256 if from == 0 {
2257 let band = timed_lane(&prepared.seam_band, || prepare_seam_band(face), |p, ms| {
2258 p.seam_band_preps += 1;
2259 p.seam_band_ms += ms;
2260 })?;
2261 if !matches!(band, SeamBandPrep::NotApplicable | SeamBandPrep::Declined) {
2262 return Ok((0, Lane::SeamBand(band)));
2263 }
2264 }
2265 if from <= 1 {
2266 if let Some(cap) = timed_lane(&prepared.sphere_cap, || prepare_winding_sphere_cap(face, prepared), |p, ms| {
2267 p.sphere_cap_preps += 1;
2268 p.sphere_cap_ms += ms;
2269 })? {
2270 return Ok((1, Lane::SphereCap(cap)));
2271 }
2272 }
2273 if from <= 2 {
2274 if let Some(horizon) = timed_lane(&prepared.horizon, || prepare_wrapped_horizon(face, prepared), |p, ms| {
2275 p.horizon_preps += 1;
2276 p.horizon_ms += ms;
2277 })? {
2278 return Ok((2, Lane::Horizon(horizon)));
2279 }
2280 }
2281 if from <= 3 {
2282 if let Some(strip) = timed_lane(&prepared.rim_strip, || prepare_covering_rim_strip(face), |p, ms| {
2283 p.rim_strip_preps += 1;
2284 p.rim_strip_ms += ms;
2285 })? {
2286 return Ok((3, Lane::RimStrip(strip)));
2287 }
2288 }
2289 Ok((4, Lane::Generic))
2290}
2291
2292pub fn containment_lane(face: &FaceRecord) -> Result<ContainmentLane, String> {
2334 let prepared = cached_prepared_face(face).unwrap_or_else(|| std::rc::Rc::new(PreparedFace::new()));
2335 Ok(match lane_from(face, &prepared, 0)?.1 {
2336 Lane::SeamBand(_) => ContainmentLane::SeamBand,
2337 Lane::SphereCap(_) => ContainmentLane::SphereCap,
2338 Lane::Horizon(horizon) => ContainmentLane::WrappedHorizon {
2339 loops: horizon.loops.clone(),
2340 u_period: horizon.u_period,
2341 cross_frame: horizon_cross_frame(),
2342 },
2343 Lane::RimStrip(_) => ContainmentLane::CoveringRimStrip,
2344 Lane::Generic => ContainmentLane::Generic {
2345 sphere_chart: sphere_chart_atlas(face).is_some(),
2346 },
2347 })
2348}
2349
2350#[derive(Clone, Debug)]
2352pub enum ContainmentLane {
2353 SeamBand,
2355 SphereCap,
2359 WrappedHorizon {
2366 loops: Vec<Vec<Vec2>>,
2369 u_period: f64,
2370 cross_frame: bool,
2371 },
2372 CoveringRimStrip,
2374 Generic {
2380 sphere_chart: bool,
2383 },
2384}
2385
2386pub fn parameter_point_in_face(
2387 face: &FaceRecord,
2388 point: Vec2,
2389 tolerance: f64,
2390) -> Result<PolygonClass, String> {
2391 point_in_face_scanned(face, point, tolerance, ChordScan::Auto)
2392}
2393
2394#[doc(hidden)]
2399pub fn parameter_point_in_face_scan(
2400 face: &FaceRecord,
2401 point: Vec2,
2402 tolerance: f64,
2403 indexed: bool,
2404) -> Result<PolygonClass, String> {
2405 let scan = if indexed {
2406 ChordScan::Indexed
2407 } else {
2408 ChordScan::Linear
2409 };
2410 point_in_face_scanned(face, point, tolerance, scan)
2411}
2412
2413fn point_in_face_scanned(
2414 face: &FaceRecord,
2415 point: Vec2,
2416 tolerance: f64,
2417 scan: ChordScan,
2418) -> Result<PolygonClass, String> {
2419 let _wall = crate::wall_profile::Scope::enter(crate::wall_profile::CLASSIFY);
2420 let prepared = prepared_face(face);
2421 let mut from = 0;
2422 loop {
2423 let (position, lane) = lane_from(face, &prepared, from)?;
2424 let answer = match lane {
2425 Lane::SeamBand(band) => {
2426 seam_band_point_in_face(band, point, tolerance).map(|class| ("seam_band", class))
2427 }
2428 Lane::SphereCap(cap) => winding_sphere_cap_point_in_face(cap, point, tolerance)
2429 .map(|class| ("winding_sphere_cap", class)),
2430 Lane::Horizon(horizon) => wrapped_horizon_point_in_face(face, horizon, point, tolerance, scan)
2431 .map(|class| ("wrapped_horizon", class)),
2432 Lane::RimStrip(strip) => covering_rim_strip_point_in_face(strip, point, tolerance)
2433 .map(|class| ("covering_rim_strip", class)),
2434 Lane::Generic => break,
2435 };
2436 if let Some((name, class)) = answer {
2437 trace_lane(face, point, name, class);
2438 return Ok(class);
2439 }
2440 from = position + 1;
2441 }
2442 let generic = timed_lane(&prepared.generic, || prepare_generic(face, &prepared), |p, ms| {
2443 p.generic_preps += 1;
2444 p.generic_ms += ms;
2445 })?;
2446 let (crossings, boundary, nearest) = generic_scan(generic, point, tolerance, scan);
2447 if boundary {
2448 trace_lane(face, point, "polygon_boundary", PolygonClass::Boundary);
2449 return Ok(PolygonClass::Boundary);
2450 }
2451 let parity = if crossings % 2 == 1 {
2452 PolygonClass::Inside
2453 } else {
2454 PolygonClass::Outside
2455 };
2456 let chord = nearest.map(|(loop_index, index, distance)| {
2457 let polygon = &generic.loops[loop_index].polygon;
2458 let prep = generic.segments[loop_index][index];
2459 (
2460 polygon[index],
2461 polygon[(index + 1) % polygon.len()],
2462 &face.loops[loop_index].coedges[prep.coedge_index].pcurve,
2463 prep,
2464 distance,
2465 )
2466 });
2467 if containment_trace_enabled() {
2468 let (distance, length) = match chord.as_ref() {
2469 Some((start, end, _, _, distance)) => (*distance, end.sub(*start).length()),
2470 None => (f64::NAN, f64::NAN),
2471 };
2472 eprintln!(
2473 "containment face={} uv=({:.8},{:.8}) crossings={crossings} parity={parity:?} \
2474 nearest distance={distance:.3e} segment length={length:.3e}",
2475 face.id, point.x, point.y
2476 );
2477 }
2478 let parity_decides = match chord.as_ref() {
2487 None => true,
2488 Some((start, end, _, _, distance)) => {
2489 let length = end.sub(*start).length();
2490 *distance > length || length <= 0.0
2491 }
2492 };
2493 if parity_decides {
2494 if let Some(class) = sphere_chart_point_in_face(face, point, &prepared, &generic.loops)? {
2495 trace_lane(face, point, "sphere_chart", class);
2496 return Ok(class);
2497 }
2498 }
2499 let Some((segment_start, segment_end, curve, prep, distance)) = chord else {
2500 trace_lane(face, point, "parity (no segment)", parity);
2501 return Ok(parity);
2502 };
2503 let segment_length = segment_end.sub(segment_start).length();
2504 if distance > segment_length || segment_length <= 0.0 {
2505 trace_lane(face, point, "parity (far from every segment)", parity);
2506 return Ok(parity);
2507 }
2508 let chord_parameter = prep.parameter_start
2509 + (prep.parameter_end - prep.parameter_start)
2510 * (point.sub(segment_start).dot(segment_end.sub(segment_start))
2511 / (segment_length * segment_length))
2512 .clamp(0.0, 1.0);
2513 let [domain_start, domain_end] = curve.domain()?;
2514 let small = crate::curve::small_curve_derivatives_enabled();
2515 let read = |parameter: f64, count: usize| -> Result<[Vec3; 3], String> {
2516 if small {
2517 return curve.derivatives_small(parameter, count);
2518 }
2519 let heap = curve.derivatives(parameter, count)?;
2520 let mut grid = [Vec3::default(); 3];
2521 grid[..heap.len()].copy_from_slice(&heap);
2522 Ok(grid)
2523 };
2524 use crate::projection::{curve_foot_newton, CurveFootPolicy, FootDomain, FootMode};
2525 let (parameter, _) = curve_foot_newton(curve, Vec3::new(point.x, point.y, 0.0),
2526 chord_parameter, [domain_start, domain_end], CurveFootPolicy {
2527 domain: FootDomain::Clamp,
2528 iterations: 12,
2529 slope_guard: 1e-30,
2530 step_limit: f64::INFINITY,
2531 parameter_tolerance: 1e-14 * (domain_end - domain_start + 1.0),
2532 mode: FootMode::Planar,
2533 })?;
2534 let margin = 1e-9 * (domain_end - domain_start);
2535 if parameter > domain_start + margin && parameter < domain_end - margin {
2536 let derivatives = read(parameter, 1)?;
2537 let on_curve = Vec2 {
2538 x: derivatives[0].x,
2539 y: derivatives[0].y,
2540 };
2541 let tangent = Vec2 {
2542 x: derivatives[1].x,
2543 y: derivatives[1].y,
2544 };
2545 let offset = point.sub(on_curve);
2546 if offset.length() <= tolerance {
2547 trace_lane(face, point, "nearest curve (on it)", PolygonClass::Boundary);
2548 return Ok(PolygonClass::Boundary);
2549 }
2550 let cross = tangent.x * offset.y - tangent.y * offset.x;
2551 if cross.abs() > 1e-30 {
2552 let class = if (cross > 0.0) == face.same_sense {
2553 PolygonClass::Inside
2554 } else {
2555 PolygonClass::Outside
2556 };
2557 trace_lane(face, point, "nearest curve (side)", class);
2558 return Ok(class);
2559 }
2560 }
2561 trace_lane(face, point, "parity (nearest curve declined)", parity);
2562 Ok(parity)
2563}
2564
2565
2566