1use crate::curve::interior_knot_count;
2use super::*;
3
4fn point_segment_distance(point: Vec2, start: Vec2, end: Vec2) -> f64 {
5 let segment = end.sub(start);
6 let length_squared = segment.dot(segment);
7 if length_squared <= 1e-30 {
8 return point.sub(start).length();
9 }
10 let parameter = (point.sub(start).dot(segment) / length_squared).clamp(0.0, 1.0);
11 point.sub(start.add(segment.scale(parameter))).length()
12}
13
14#[derive(Clone, Copy, Debug, PartialEq)]
15pub enum PolygonClass {
16 Inside,
17 Outside,
18 Boundary,
19}
20
21fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
22 for index in 0..polygon.len() {
23 if point_segment_distance(point, polygon[index], polygon[(index + 1) % polygon.len()])
24 <= tolerance
25 {
26 return PolygonClass::Boundary;
27 }
28 }
29 let mut inside = false;
30 for index in 0..polygon.len() {
31 let a = polygon[index];
32 let b = polygon[(index + 1) % polygon.len()];
33 if (a.y > point.y) != (b.y > point.y) {
34 let crossing = a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x);
35 if crossing > point.x {
36 inside = !inside;
37 }
38 }
39 }
40 if inside {
41 PolygonClass::Inside
42 } else {
43 PolygonClass::Outside
44 }
45}
46
47struct SegmentReference<'a> {
48 start: Vec2,
49 end: Vec2,
50 curve: &'a NurbsCurve,
51 parameter_start: f64,
52 parameter_end: f64,
53}
54
55
56fn seam_band_point_in_face(
65 face: &FaceRecord,
66 point: Vec2,
67 tolerance: f64,
68) -> Result<Option<PolygonClass>, String> {
69 if face.surface.closed_directions()? != (true, true) {
70 return Ok(None);
71 }
72 let [u0, u1] = face.surface.domain_u()?;
73 let [v0, v1] = face.surface.domain_v()?;
74 if crate::topology::doubly_periodic_has_only_collapsed_loops(face)? {
75 return Ok(Some(PolygonClass::Inside));
76 }
77 let u_span = (u1 - u0).abs().max(1e-30);
78 let v_span = (v1 - v0).abs().max(1e-30);
79 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
80 for loop_record in &face.loops {
81 let mut points: Vec<[f64; 2]> = Vec::new();
82 for coedge in &loop_record.coedges {
83 let [d0, d1] = coedge.pcurve.domain()?;
84 let samples = 24;
85 for k in 0..=samples {
86 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
87 let p = coedge.pcurve.evaluate(t)?;
88 points.push([p.x, p.y]);
89 }
90 }
91 let (mut umin, mut umax, mut vmin, mut vmax) = (
92 f64::INFINITY,
93 f64::NEG_INFINITY,
94 f64::INFINITY,
95 f64::NEG_INFINITY,
96 );
97 for p in &points {
98 umin = umin.min(p[0]);
99 umax = umax.max(p[0]);
100 vmin = vmin.min(p[1]);
101 vmax = vmax.max(p[1]);
102 }
103 if face.loops.len() > 2 && (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span
109 {
110 continue;
111 }
112 loops_uv.push(points);
113 }
114 if face.loops.len() == 1
125 && loops_uv.len() == 1
126 && std::env::var("BREP_SEAM_BAND_MERGED").as_deref() != Ok("0")
127 {
128 let coedges = &face.loops[0].coedges;
129 let offsets = crate::topology::loop_seam_offsets(coedges, true, true, u_span, v_span)?;
130 if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
131 let mut polygon: Vec<Vec2> = Vec::new();
132 for (coedge_index, coedge) in coedges.iter().enumerate() {
133 let [d0, d1] = coedge.pcurve.domain()?;
134 let samples = 24;
135 for k in 0..samples {
136 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
137 let p = coedge.pcurve.evaluate(t)?;
138 polygon.push(Vec2 {
139 x: p.x + offsets[coedge_index][0],
140 y: p.y + offsets[coedge_index][1],
141 });
142 }
143 }
144 let mut best = PolygonClass::Outside;
145 'images: for du in [-1.0, 0.0, 1.0] {
146 for dv in [-1.0, 0.0, 1.0] {
147 let image = Vec2 {
148 x: point.x + du * u_span,
149 y: point.y + dv * v_span,
150 };
151 match point_in_polygon(image, &polygon, tolerance) {
152 PolygonClass::Boundary => {
153 best = PolygonClass::Boundary;
154 break 'images;
155 }
156 PolygonClass::Inside => best = PolygonClass::Inside,
157 PolygonClass::Outside => {}
158 }
159 }
160 }
161 return Ok(Some(best));
162 }
163 }
164 if loops_uv.len() != 2 {
165 return Ok(None);
166 }
167 if let Some(band) = crate::topology::analyze_doubly_periodic_seam_band(
168 &loops_uv,
169 [u0, u1, v0, v1],
170 face.same_sense,
171 ) {
172 let polygon: Vec<Vec2> =
173 crate::topology::seam_band_uv_polygon(&loops_uv, [u0, u1, v0, v1], &band)
174 .into_iter()
175 .map(|p| Vec2 { x: p[0], y: p[1] })
176 .collect();
177 return Ok(Some(point_in_polygon(point, &polygon, tolerance)));
178 }
179 for p_is_u in [true, false] {
186 let (period, q_extent) = if p_is_u {
187 (u1 - u0, v_span)
188 } else {
189 (v1 - v0, u_span)
190 };
191 if !(period > 0.0) {
192 continue;
193 }
194 let coord = |p: &[f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
195 let mut rings = Vec::with_capacity(2);
196 for points in &loops_uv {
197 let (mut pmin, mut pmax, mut qmin, mut qmax) = (
198 f64::INFINITY,
199 f64::NEG_INFINITY,
200 f64::INFINITY,
201 f64::NEG_INFINITY,
202 );
203 for p in points {
204 let (periodic, cross) = coord(p);
205 pmin = pmin.min(periodic);
206 pmax = pmax.max(periodic);
207 qmin = qmin.min(cross);
208 qmax = qmax.max(cross);
209 }
210 if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
211 rings.clear();
212 break;
213 }
214 let mut net = 0.0;
215 for pair in points.windows(2) {
216 let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
217 if delta > 0.5 * period {
218 delta -= period;
219 } else if delta < -0.5 * period {
220 delta += period;
221 }
222 net += delta;
223 }
224 let direction = if net > 0.25 * period {
225 1
226 } else if net < -0.25 * period {
227 -1
228 } else {
229 0
230 };
231 rings.push((0.5 * (qmin + qmax), direction));
232 }
233 if rings.len() != 2 {
234 continue;
235 }
236 rings.sort_by(|a, b| a.0.total_cmp(&b.0));
237 let [(q_lo, lower_direction), (q_hi, upper_direction)] = rings.as_slice() else {
238 unreachable!()
239 };
240 let inconclusive =
241 *lower_direction == 0 || *upper_direction == 0 || lower_direction == upper_direction;
242 let between_is_ccw_uv = if p_is_u {
243 *lower_direction > 0
244 } else {
245 *lower_direction < 0
246 };
247 let complement = !inconclusive && between_is_ccw_uv != face.same_sense;
248 let q = if p_is_u { point.y } else { point.x };
249 if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
250 return Ok(Some(PolygonClass::Boundary));
251 }
252 let between = q > *q_lo && q < *q_hi;
253 return Ok(Some(if between != complement {
254 PolygonClass::Inside
255 } else {
256 PolygonClass::Outside
257 }));
258 }
259 Ok(None)
260}
261
262fn wrapped_horizon_point_in_face(
278 face: &FaceRecord,
279 point: Vec2,
280 tolerance: f64,
281) -> Result<Option<PolygonClass>, String> {
282 if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
283 return Ok(None);
284 }
285 if std::env::var("BREP_HORIZON_CONTAINMENT").as_deref() == Ok("0") {
286 return Ok(None);
287 }
288 if face.loops.len() <= 2 && std::env::var("BREP_HORIZON_SINGLE_LOOP").as_deref() == Ok("0") {
299 return Ok(None);
300 }
301 let [u0, u1] = face.surface.domain_u()?;
302 let u_period = u1 - u0;
303 if u_period <= 0.0 {
304 return Ok(None);
305 }
306 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
307 let mut loops: Vec<Vec<Vec2>> = Vec::new();
308 let mut straddling = false;
309 for loop_record in &face.loops {
310 let mut points: Vec<Vec2> = Vec::new();
311 for coedge in &loop_record.coedges {
312 let curve = &coedge.pcurve;
313 let [start, end] = curve.domain()?;
314 let sample_count =
315 2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4);
316 for index in 0..sample_count {
317 let parameter = start + (end - start) * index as f64 / sample_count as f64;
318 let evaluated = curve.evaluate(parameter)?;
319 points.push(Vec2 {
320 x: evaluated.x,
321 y: evaluated.y,
322 });
323 }
324 }
325 if points.len() < 3 {
326 continue;
327 }
328 let mut unwrapped = Vec::with_capacity(points.len());
332 let mut jumps = 0usize;
333 let mut cursor = points[0];
334 unwrapped.push(cursor);
335 for pair in points.windows(2) {
336 let mut du = pair[1].x - pair[0].x;
337 let folded = du - u_period * (du / u_period).round();
338 if (du - folded).abs() > 0.25 * u_period {
339 jumps += 1;
340 }
341 du = folded;
342 cursor = Vec2 {
343 x: cursor.x + du,
344 y: pair[1].y,
345 };
346 unwrapped.push(cursor);
347 }
348 let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
349 if closure > 0.25 * u_period {
350 if debug {
351 eprintln!(
352 "horizon: face {} loop winds the period (closure {closure:.3}) — bail",
353 face.id
354 );
355 }
356 return Ok(None); }
358 if jumps > 0 {
359 straddling = true;
360 }
361 loops.push(unwrapped);
362 }
363 if !straddling || loops.is_empty() {
364 return Ok(None);
365 }
366 if std::env::var("BREP_HORIZON_CROSS_FRAME").as_deref() == Ok("0") {
381 let mut best: Option<PolygonClass> = None;
382 for shift in [-u_period, 0.0, u_period] {
383 let image = Vec2 {
384 x: point.x + shift,
385 y: point.y,
386 };
387 let mut crossings = 0usize;
388 for polygon in &loops {
389 match point_in_polygon(image, polygon, tolerance) {
390 PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
391 PolygonClass::Inside => crossings += 1,
392 PolygonClass::Outside => {}
393 }
394 }
395 if crossings % 2 == 1 {
396 best = Some(PolygonClass::Inside);
397 } else if best.is_none() {
398 best = Some(PolygonClass::Outside);
399 }
400 }
401 if debug {
402 eprintln!(
403 "horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
404 face.id,
405 loops.len(),
406 best
407 );
408 }
409 return Ok(best);
410 }
411 let mut crossings = 0usize;
412 for polygon in &loops {
413 let mut image_hits = 0usize;
414 for shift in [-u_period, 0.0, u_period] {
415 let image = Vec2 {
416 x: point.x + shift,
417 y: point.y,
418 };
419 match point_in_polygon(image, polygon, tolerance) {
420 PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
421 PolygonClass::Inside => image_hits += 1,
422 PolygonClass::Outside => {}
423 }
424 }
425 crossings += image_hits % 2;
426 }
427 let class = if crossings % 2 == 1 {
428 PolygonClass::Inside
429 } else {
430 PolygonClass::Outside
431 };
432 if debug {
433 eprintln!(
434 "horizon: face {} loops={} straddling -> {:?}",
435 face.id,
436 loops.len(),
437 class
438 );
439 }
440 Ok(Some(class))
441}
442
443fn winding_sphere_cap_point_in_face(
448 face: &FaceRecord,
449 point: Vec2,
450 tolerance: f64,
451) -> Result<Option<PolygonClass>, String> {
452 if !matches!(
453 face.surface.analytic(),
454 Some(crate::AnalyticSurface::Sphere { .. })
455 ) || face.surface.closed_directions()? != (true, false)
456 || face.loops.len() != 2
457 {
458 return Ok(None);
459 }
460 let [u0, u1] = face.surface.domain_u()?;
461 let [v0, v1] = face.surface.domain_v()?;
462 let period = u1 - u0;
463 let v_span = v1 - v0;
464 if !(period > 0.0 && v_span > 0.0) {
465 return Ok(None);
466 }
467 struct WindingLoop {
468 points: Vec<Vec2>,
469 winding: f64,
470 vmin: f64,
471 vmax: f64,
472 }
473 let mut loops = Vec::with_capacity(2);
474 for loop_record in &face.loops {
475 let mut points = Vec::new();
476 for coedge in &loop_record.coedges {
477 let [start, end] = coedge.pcurve.domain()?;
478 let samples = 2usize
479 .max((interior_knot_count(&coedge.pcurve.knots, coedge.pcurve.degree) + 1) * (coedge.pcurve.degree + 1) * 4);
480 for index in 0..samples {
481 let parameter = start + (end - start) * index as f64 / samples as f64;
482 let p = coedge.pcurve.evaluate(parameter)?;
483 points.push(Vec2 { x: p.x, y: p.y });
484 }
485 }
486 if points.len() < 2 {
487 return Ok(None);
488 }
489 let first = points[0];
490 let mut cursor = first;
491 let mut unwrapped = vec![cursor];
492 for next in points.iter().skip(1) {
493 let du = next.x - cursor.x;
494 let folded = du - period * (du / period).round();
495 cursor = Vec2 {
496 x: cursor.x + folded,
497 y: next.y,
498 };
499 unwrapped.push(cursor);
500 }
501 let last_raw = *points.last().unwrap();
502 let closing_du = first.x - last_raw.x;
503 let closing_folded = closing_du - period * (closing_du / period).round();
504 let winding = cursor.x + closing_folded - first.x;
505 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
506 for p in &unwrapped {
507 vmin = vmin.min(p.y);
508 vmax = vmax.max(p.y);
509 }
510 loops.push(WindingLoop {
511 points: unwrapped,
512 winding,
513 vmin,
514 vmax,
515 });
516 }
517 if loops
518 .iter()
519 .any(|loop_data| (loop_data.winding.abs() - period).abs() > 0.05 * period)
520 || loops[0].winding * loops[1].winding >= 0.0
521 {
522 return Ok(None);
523 }
524 let flat = |loop_data: &WindingLoop| loop_data.vmax - loop_data.vmin <= 1e-6 * v_span;
525 let (pole, rim) = match (flat(&loops[0]), flat(&loops[1])) {
526 (true, false) => (&loops[0], &loops[1]),
527 (false, true) => (&loops[1], &loops[0]),
528 _ => return Ok(None),
529 };
530 let pole_v = 0.5 * (pole.vmin + pole.vmax);
531 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
532 return Ok(None);
533 }
534
535 let window_start = rim.points[0].x.min(rim.points[rim.points.len() - 1].x);
536 let query_u = window_start + (point.x - window_start).rem_euclid(period);
537 let mut crossings = Vec::new();
538 for pair in rim.points.windows(2) {
539 let (a, b) = (pair[0], pair[1]);
540 if (a.x > query_u) != (b.x > query_u) {
541 crossings.push(a.y + (query_u - a.x) / (b.x - a.x) * (b.y - a.y));
542 }
543 for image in [-period, 0.0, period] {
544 if point_segment_distance(
545 Vec2 {
546 x: point.x + image,
547 y: point.y,
548 },
549 a,
550 b,
551 ) <= tolerance
552 {
553 return Ok(Some(PolygonClass::Boundary));
554 }
555 }
556 }
557 let Some(rim_v) = crossings
558 .into_iter()
559 .min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))
560 else {
561 return Ok(None);
562 };
563 let inside = if pole_v < rim_v {
564 point.y <= rim_v + tolerance
565 } else {
566 point.y >= rim_v - tolerance
567 };
568 Ok(Some(if inside {
569 PolygonClass::Inside
570 } else {
571 PolygonClass::Outside
572 }))
573}
574
575fn covering_rim_strip_point_in_face(
607 face: &FaceRecord,
608 point: Vec2,
609 tolerance: f64,
610) -> Result<Option<PolygonClass>, String> {
611 if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
612 return Ok(None);
613 }
614 if std::env::var("BREP_COVERING_RIM_STRIP").as_deref() == Ok("0") {
615 return Ok(None);
616 }
617 let [u0, u1] = face.surface.domain_u()?;
618 let [v0, v1] = face.surface.domain_v()?;
619 let period = u1 - u0;
620 if !(period > 0.0) {
621 return Ok(None);
622 }
623 let v_span = (v1 - v0).abs().max(1e-30);
624 struct Rim {
625 points: Vec<Vec2>,
626 vmin: f64,
627 vmax: f64,
628 ascending: bool,
629 }
630 let mut rims: Vec<Rim> = Vec::with_capacity(2);
631 let mut out_of_domain = false;
632 for loop_record in &face.loops {
633 let mut points: Vec<Vec2> = Vec::new();
634 for coedge in &loop_record.coedges {
635 let curve = &coedge.pcurve;
636 let [start, end] = curve.domain()?;
637 let sample_count =
638 2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4);
639 for index in 0..=sample_count {
640 let parameter = start + (end - start) * index as f64 / sample_count as f64;
641 let evaluated = curve.evaluate(parameter)?;
642 points.push(Vec2 {
643 x: evaluated.x,
644 y: evaluated.y,
645 });
646 }
647 }
648 if points.len() < 3 {
649 return Ok(None);
650 }
651 for p in &points {
654 if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
655 out_of_domain = true;
656 }
657 }
658 let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
662 let mut cursor = points[0];
663 unwrapped.push(cursor);
664 for pair in points.windows(2) {
665 let du = pair[1].x - pair[0].x;
666 let folded = du - period * (du / period).round();
667 cursor = Vec2 {
668 x: cursor.x + folded,
669 y: pair[1].y,
670 };
671 unwrapped.push(cursor);
672 }
673 let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
674 if (net.abs() - period).abs() > 2e-2 * period {
675 return Ok(None); }
677 if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
678 return Ok(None); }
680 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
681 for p in &unwrapped {
682 vmin = vmin.min(p.y);
683 vmax = vmax.max(p.y);
684 }
685 rims.push(Rim {
686 points: unwrapped,
687 vmin,
688 vmax,
689 ascending: net > 0.0,
690 });
691 }
692 if !out_of_domain {
693 return Ok(None);
694 }
695 if rims[0].ascending == rims[1].ascending {
696 return Ok(None); }
698 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
699 (&rims[0], &rims[1])
700 } else if rims[1].vmax <= rims[0].vmin {
701 (&rims[1], &rims[0])
702 } else {
703 return Ok(None); };
705 if upper.vmin - lower.vmax <= 1e-6 * v_span {
706 return Ok(None);
707 }
708 if (lower.ascending) != face.same_sense {
713 return Ok(None);
714 }
715 for rim in [lower, upper] {
717 for shift in [-period, 0.0, period] {
718 let image = Vec2 {
719 x: point.x + shift,
720 y: point.y,
721 };
722 for pair in rim.points.windows(2) {
723 if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
724 return Ok(Some(PolygonClass::Boundary));
725 }
726 }
727 }
728 }
729 let mut crossings = 0usize;
732 for rim in [lower, upper] {
733 let window_base = rim.points[0].x.min(rim.points[rim.points.len() - 1].x);
734 let x = window_base + (point.x - window_base).rem_euclid(period);
735 for pair in rim.points.windows(2) {
736 let (a, b) = (pair[0], pair[1]);
737 if (a.x > x) != (b.x > x) {
738 let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
739 if v_cross > point.y {
740 crossings += 1;
741 }
742 }
743 }
744 }
745 Ok(Some(if crossings % 2 == 1 {
746 PolygonClass::Inside
747 } else {
748 PolygonClass::Outside
749 }))
750}
751
752struct SphereRegionCache {
769 scratch: Vec<f64>,
770 entries: Vec<(Vec<f64>, crate::sphere_chart::SphericalRegion)>,
771}
772
773const SPHERE_REGION_CACHE_ENTRIES: usize = 4;
774
775thread_local! {
776 static SPHERE_REGIONS: std::cell::RefCell<SphereRegionCache> = const {
777 std::cell::RefCell::new(SphereRegionCache {
778 scratch: Vec::new(),
779 entries: Vec::new(),
780 })
781 };
782}
783
784struct LoopSamples {
789 polygon: Vec<Vec2>,
790 coedges: Vec<(u64, usize, usize)>,
791}
792
793fn sphere_chart_point_in_face(
818 face: &FaceRecord,
819 point: Vec2,
820 sampled: &[LoopSamples],
821) -> Result<Option<PolygonClass>, String> {
822 if std::env::var("BREP_NO_SPHERE_CHARTS").is_ok()
823 || std::env::var("BREP_NO_SPHERE_CHART_TRIM").is_ok()
824 {
825 return Ok(None);
826 }
827 let Some(atlas) = crate::sphere_chart::SphereAtlas::of_surface(&face.surface) else {
828 return Ok(None);
829 };
830 let mut uses: std::collections::HashMap<u64, usize> = std::collections::HashMap::new();
834 for coedge in face.loops.iter().flat_map(|record| record.coedges.iter()) {
835 *uses.entry(coedge.edge_id).or_insert(0) += 1;
836 }
837 let outward_face_normal =
862 face.same_sense == atlas.parameterization_is_outward(&face.surface)?;
863 SPHERE_REGIONS.with(|cache| {
864 let SphereRegionCache { scratch, entries } = &mut *cache.borrow_mut();
865 scratch.clear();
868 scratch.extend_from_slice(&[
869 atlas.centre.x,
870 atlas.centre.y,
871 atlas.centre.z,
872 atlas.radius,
873 if outward_face_normal { 1.0 } else { 0.0 },
874 ]);
875 for axis in atlas.basis {
876 scratch.extend_from_slice(&[axis.x, axis.y, axis.z]);
877 }
878 let header = scratch.len();
882 for samples in sampled.iter() {
883 let count = samples.polygon.len();
884 if count < 2 {
885 continue;
886 }
887 for &(edge_id, first, span) in &samples.coedges {
888 if span == 0 || uses.get(&edge_id).copied().unwrap_or(0) >= 2 {
889 continue;
890 }
891 scratch.push(span as f64);
892 for offset in 0..=span {
896 let uv = samples.polygon[(first + offset) % count];
897 scratch.push(uv.x);
898 scratch.push(uv.y);
899 }
900 }
901 }
902 if let Some(index) = entries.iter().position(|(signature, _)| signature == scratch) {
903 if index != 0 {
904 entries.swap(0, index);
905 }
906 } else {
907 let mut points: Vec<Vec3> = Vec::new();
908 let mut spans: Vec<(usize, usize)> = Vec::new();
909 let mut cursor = header;
910 while cursor < scratch.len() {
911 let span = scratch[cursor] as usize;
912 cursor += 1;
913 let start = points.len();
914 for index in 0..=span {
915 let uv = (scratch[cursor + 2 * index], scratch[cursor + 2 * index + 1]);
916 points.push(face.surface.evaluate(uv.0, uv.1)?);
917 }
918 cursor += 2 * (span + 1);
919 spans.push((start, points.len()));
920 }
921 crate::sphere_chart::canonicalize_points(&mut points, 1e-9);
926 let mut boundary: Vec<(Vec3, Vec3)> = Vec::new();
927 for (first, last) in spans {
928 for index in first..last.saturating_sub(1) {
929 boundary.push((points[index], points[index + 1]));
930 }
931 }
932 let region = crate::sphere_chart::SphericalRegion::from_segments(
933 atlas.centre,
934 &boundary,
935 outward_face_normal,
936 );
937 entries.insert(0, (scratch.clone(), region));
938 entries.truncate(SPHERE_REGION_CACHE_ENTRIES);
939 }
940 let region = &entries[0].1;
941 if region.is_whole_sphere() {
942 return Ok(Some(PolygonClass::Inside));
943 }
944 if !region.is_decidable() {
945 return Ok(None);
948 }
949 let probe = face.surface.evaluate(point.x, point.y)?;
950 Ok(Some(if region.contains(atlas.centre, probe) {
951 PolygonClass::Inside
952 } else {
953 PolygonClass::Outside
954 }))
955 })
956}
957
958pub fn parameter_point_in_face(
959 face: &FaceRecord,
960 point: Vec2,
961 tolerance: f64,
962) -> Result<PolygonClass, String> {
963 if let Some(class) = seam_band_point_in_face(face, point, tolerance)? {
964 return Ok(class);
965 }
966 if let Some(class) = winding_sphere_cap_point_in_face(face, point, tolerance)? {
967 return Ok(class);
968 }
969 if let Some(class) = wrapped_horizon_point_in_face(face, point, tolerance)? {
970 return Ok(class);
971 }
972 if let Some(class) = covering_rim_strip_point_in_face(face, point, tolerance)? {
973 return Ok(class);
974 }
975 let mut crossings = 0;
976 let mut boundary = false;
977 let mut nearest: Option<(SegmentReference<'_>, f64)> = None;
978 let mut sampled: Vec<LoopSamples> = Vec::with_capacity(face.loops.len());
981 for loop_record in &face.loops {
982 let mut polygon = Vec::new();
983 let mut segments = Vec::new();
984 let mut coedges = Vec::with_capacity(loop_record.coedges.len());
985 for coedge in &loop_record.coedges {
986 let first = polygon.len();
987 let curve = &coedge.pcurve;
988 let [start, end] = curve.domain()?;
989 let sample_count =
990 2usize.max((interior_knot_count(&curve.knots, curve.degree) + 1) * (curve.degree + 1) * 4);
991 for index in 0..sample_count {
992 let parameter = start + (end - start) * index as f64 / sample_count as f64;
993 let evaluated = curve.evaluate(parameter)?;
994 polygon.push(Vec2 {
995 x: evaluated.x,
996 y: evaluated.y,
997 });
998 let parameter_end = if index + 1 < sample_count {
999 start + (end - start) * (index + 1) as f64 / sample_count as f64
1000 } else {
1001 end
1002 };
1003 segments.push(SegmentReference {
1004 start: Vec2 {
1005 x: evaluated.x,
1006 y: evaluated.y,
1007 },
1008 end: Vec2 { x: 0.0, y: 0.0 },
1009 curve,
1010 parameter_start: parameter,
1011 parameter_end,
1012 });
1013 }
1014 coedges.push((coedge.edge_id, first, polygon.len() - first));
1015 }
1016 for index in 0..polygon.len() {
1017 segments[index].end = polygon[(index + 1) % polygon.len()];
1018 }
1019 match point_in_polygon(point, &polygon, tolerance) {
1020 PolygonClass::Boundary => boundary = true,
1021 PolygonClass::Inside => crossings += 1,
1022 PolygonClass::Outside => {}
1023 }
1024 for segment in segments {
1025 let distance = point_segment_distance(point, segment.start, segment.end);
1026 if nearest
1027 .as_ref()
1028 .is_none_or(|(_, nearest_distance)| distance < *nearest_distance)
1029 {
1030 nearest = Some((segment, distance));
1031 }
1032 }
1033 sampled.push(LoopSamples { polygon, coedges });
1034 }
1035 if boundary {
1036 return Ok(PolygonClass::Boundary);
1037 }
1038 let parity = if crossings % 2 == 1 {
1039 PolygonClass::Inside
1040 } else {
1041 PolygonClass::Outside
1042 };
1043 let parity_decides = match nearest.as_ref() {
1052 None => true,
1053 Some((segment, distance)) => {
1054 let length = segment.end.sub(segment.start).length();
1055 *distance > length || length <= 0.0
1056 }
1057 };
1058 if parity_decides {
1059 if let Some(class) = sphere_chart_point_in_face(face, point, &sampled)? {
1060 return Ok(class);
1061 }
1062 }
1063 let Some((segment, distance)) = nearest else {
1064 return Ok(parity);
1065 };
1066 let segment_length = segment.end.sub(segment.start).length();
1067 if distance > segment_length || segment_length <= 0.0 {
1068 return Ok(parity);
1069 }
1070 let chord_parameter = segment.parameter_start
1071 + (segment.parameter_end - segment.parameter_start)
1072 * (point.sub(segment.start).dot(segment.end.sub(segment.start))
1073 / (segment_length * segment_length))
1074 .clamp(0.0, 1.0);
1075 let [domain_start, domain_end] = segment.curve.domain()?;
1076 let mut parameter = chord_parameter;
1077 for _ in 0..12 {
1078 let derivatives = segment.curve.derivatives(parameter, 2)?;
1079 let on_curve = Vec2 {
1080 x: derivatives[0].x,
1081 y: derivatives[0].y,
1082 };
1083 let tangent = Vec2 {
1084 x: derivatives[1].x,
1085 y: derivatives[1].y,
1086 };
1087 let second = Vec2 {
1088 x: derivatives[2].x,
1089 y: derivatives[2].y,
1090 };
1091 let residual = on_curve.sub(point);
1092 let denominator = tangent.dot(tangent) + residual.dot(second);
1093 if denominator.abs() < 1e-30 {
1094 break;
1095 }
1096 let step = -residual.dot(tangent) / denominator;
1097 parameter = (parameter + step).clamp(domain_start, domain_end);
1098 if step.abs() < 1e-14 * (domain_end - domain_start + 1.0) {
1099 break;
1100 }
1101 }
1102 let margin = 1e-9 * (domain_end - domain_start);
1103 if parameter > domain_start + margin && parameter < domain_end - margin {
1104 let derivatives = segment.curve.derivatives(parameter, 1)?;
1105 let on_curve = Vec2 {
1106 x: derivatives[0].x,
1107 y: derivatives[0].y,
1108 };
1109 let tangent = Vec2 {
1110 x: derivatives[1].x,
1111 y: derivatives[1].y,
1112 };
1113 let offset = point.sub(on_curve);
1114 if offset.length() <= tolerance {
1115 return Ok(PolygonClass::Boundary);
1116 }
1117 let cross = tangent.x * offset.y - tangent.y * offset.x;
1118 if cross.abs() > 1e-30 {
1119 return Ok(if (cross > 0.0) == face.same_sense {
1120 PolygonClass::Inside
1121 } else {
1122 PolygonClass::Outside
1123 });
1124 }
1125 }
1126 Ok(parity)
1127}