1use super::*;
2
3fn point_segment_distance(point: Vec2, start: Vec2, end: Vec2) -> f64 {
4 let segment = end.sub(start);
5 let length_squared = segment.dot(segment);
6 if length_squared <= 1e-30 {
7 return point.sub(start).length();
8 }
9 let parameter = (point.sub(start).dot(segment) / length_squared).clamp(0.0, 1.0);
10 point.sub(start.add(segment.scale(parameter))).length()
11}
12
13#[derive(Clone, Copy, Debug, PartialEq)]
14pub enum PolygonClass {
15 Inside,
16 Outside,
17 Boundary,
18}
19
20fn point_in_polygon(point: Vec2, polygon: &[Vec2], tolerance: f64) -> PolygonClass {
21 for index in 0..polygon.len() {
22 if point_segment_distance(point, polygon[index], polygon[(index + 1) % polygon.len()])
23 <= tolerance
24 {
25 return PolygonClass::Boundary;
26 }
27 }
28 let mut inside = false;
29 for index in 0..polygon.len() {
30 let a = polygon[index];
31 let b = polygon[(index + 1) % polygon.len()];
32 if (a.y > point.y) != (b.y > point.y) {
33 let crossing = a.x + (point.y - a.y) / (b.y - a.y) * (b.x - a.x);
34 if crossing > point.x {
35 inside = !inside;
36 }
37 }
38 }
39 if inside {
40 PolygonClass::Inside
41 } else {
42 PolygonClass::Outside
43 }
44}
45
46struct SegmentReference<'a> {
47 start: Vec2,
48 end: Vec2,
49 curve: &'a NurbsCurve,
50 parameter_start: f64,
51 parameter_end: f64,
52}
53
54fn interior_knot_count(curve: &NurbsCurve) -> usize {
55 let start = curve.knots[curve.degree];
56 let end = curve.knots[curve.knots.len() - 1 - curve.degree];
57 let mut previous = None;
58 let mut count = 0;
59 for &knot in &curve.knots {
60 if knot <= start + 1e-12 || knot >= end - 1e-12 {
61 continue;
62 }
63 if previous.is_none_or(|value: f64| (value - knot).abs() > 1e-12) {
64 previous = Some(knot);
65 count += 1;
66 }
67 }
68 count
69}
70
71fn seam_band_point_in_face(
80 face: &FaceRecord,
81 point: Vec2,
82 tolerance: f64,
83) -> Result<Option<PolygonClass>, String> {
84 if face.surface.closed_directions()? != (true, true) {
85 return Ok(None);
86 }
87 let [u0, u1] = face.surface.domain_u()?;
88 let [v0, v1] = face.surface.domain_v()?;
89 if crate::topology::doubly_periodic_has_only_collapsed_loops(face)? {
90 return Ok(Some(PolygonClass::Inside));
91 }
92 let u_span = (u1 - u0).abs().max(1e-30);
93 let v_span = (v1 - v0).abs().max(1e-30);
94 let mut loops_uv: Vec<Vec<[f64; 2]>> = Vec::with_capacity(face.loops.len());
95 for loop_record in &face.loops {
96 let mut points: Vec<[f64; 2]> = Vec::new();
97 for coedge in &loop_record.coedges {
98 let [d0, d1] = coedge.pcurve.domain()?;
99 let samples = 24;
100 for k in 0..=samples {
101 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
102 let p = coedge.pcurve.evaluate(t)?;
103 points.push([p.x, p.y]);
104 }
105 }
106 let (mut umin, mut umax, mut vmin, mut vmax) = (
107 f64::INFINITY,
108 f64::NEG_INFINITY,
109 f64::INFINITY,
110 f64::NEG_INFINITY,
111 );
112 for p in &points {
113 umin = umin.min(p[0]);
114 umax = umax.max(p[0]);
115 vmin = vmin.min(p[1]);
116 vmax = vmax.max(p[1]);
117 }
118 if face.loops.len() > 2 && (umax - umin) <= 1e-3 * u_span && (vmax - vmin) <= 1e-3 * v_span
124 {
125 continue;
126 }
127 loops_uv.push(points);
128 }
129 if face.loops.len() == 1
140 && loops_uv.len() == 1
141 && std::env::var("BREP_SEAM_BAND_MERGED").as_deref() != Ok("0")
142 {
143 let coedges = &face.loops[0].coedges;
144 let offsets = crate::topology::loop_seam_offsets(coedges, true, true, u_span, v_span)?;
145 if offsets.iter().any(|o| o[0] != 0.0 || o[1] != 0.0) {
146 let mut polygon: Vec<Vec2> = Vec::new();
147 for (coedge_index, coedge) in coedges.iter().enumerate() {
148 let [d0, d1] = coedge.pcurve.domain()?;
149 let samples = 24;
150 for k in 0..samples {
151 let t = d0 + (d1 - d0) * k as f64 / samples as f64;
152 let p = coedge.pcurve.evaluate(t)?;
153 polygon.push(Vec2 {
154 x: p.x + offsets[coedge_index][0],
155 y: p.y + offsets[coedge_index][1],
156 });
157 }
158 }
159 let mut best = PolygonClass::Outside;
160 'images: for du in [-1.0, 0.0, 1.0] {
161 for dv in [-1.0, 0.0, 1.0] {
162 let image = Vec2 {
163 x: point.x + du * u_span,
164 y: point.y + dv * v_span,
165 };
166 match point_in_polygon(image, &polygon, tolerance) {
167 PolygonClass::Boundary => {
168 best = PolygonClass::Boundary;
169 break 'images;
170 }
171 PolygonClass::Inside => best = PolygonClass::Inside,
172 PolygonClass::Outside => {}
173 }
174 }
175 }
176 return Ok(Some(best));
177 }
178 }
179 if loops_uv.len() != 2 {
180 return Ok(None);
181 }
182 if let Some(band) = crate::topology::analyze_doubly_periodic_seam_band(
183 &loops_uv,
184 [u0, u1, v0, v1],
185 face.same_sense,
186 ) {
187 let polygon: Vec<Vec2> =
188 crate::topology::seam_band_uv_polygon(&loops_uv, [u0, u1, v0, v1], &band)
189 .into_iter()
190 .map(|p| Vec2 { x: p[0], y: p[1] })
191 .collect();
192 return Ok(Some(point_in_polygon(point, &polygon, tolerance)));
193 }
194 for p_is_u in [true, false] {
201 let (period, q_extent) = if p_is_u {
202 (u1 - u0, v_span)
203 } else {
204 (v1 - v0, u_span)
205 };
206 if !(period > 0.0) {
207 continue;
208 }
209 let coord = |p: &[f64; 2]| if p_is_u { (p[0], p[1]) } else { (p[1], p[0]) };
210 let mut rings = Vec::with_capacity(2);
211 for points in &loops_uv {
212 let (mut pmin, mut pmax, mut qmin, mut qmax) = (
213 f64::INFINITY,
214 f64::NEG_INFINITY,
215 f64::INFINITY,
216 f64::NEG_INFINITY,
217 );
218 for p in points {
219 let (periodic, cross) = coord(p);
220 pmin = pmin.min(periodic);
221 pmax = pmax.max(periodic);
222 qmin = qmin.min(cross);
223 qmax = qmax.max(cross);
224 }
225 if (pmax - pmin) < 0.6 * period || (qmax - qmin) > 0.05 * q_extent {
226 rings.clear();
227 break;
228 }
229 let mut net = 0.0;
230 for pair in points.windows(2) {
231 let mut delta = coord(&pair[1]).0 - coord(&pair[0]).0;
232 if delta > 0.5 * period {
233 delta -= period;
234 } else if delta < -0.5 * period {
235 delta += period;
236 }
237 net += delta;
238 }
239 let direction = if net > 0.25 * period {
240 1
241 } else if net < -0.25 * period {
242 -1
243 } else {
244 0
245 };
246 rings.push((0.5 * (qmin + qmax), direction));
247 }
248 if rings.len() != 2 {
249 continue;
250 }
251 rings.sort_by(|a, b| a.0.total_cmp(&b.0));
252 let [(q_lo, lower_direction), (q_hi, upper_direction)] = rings.as_slice() else {
253 unreachable!()
254 };
255 let inconclusive =
256 *lower_direction == 0 || *upper_direction == 0 || lower_direction == upper_direction;
257 let between_is_ccw_uv = if p_is_u {
258 *lower_direction > 0
259 } else {
260 *lower_direction < 0
261 };
262 let complement = !inconclusive && between_is_ccw_uv != face.same_sense;
263 let q = if p_is_u { point.y } else { point.x };
264 if (q - q_lo).abs() <= tolerance || (q - q_hi).abs() <= tolerance {
265 return Ok(Some(PolygonClass::Boundary));
266 }
267 let between = q > *q_lo && q < *q_hi;
268 return Ok(Some(if between != complement {
269 PolygonClass::Inside
270 } else {
271 PolygonClass::Outside
272 }));
273 }
274 Ok(None)
275}
276
277fn wrapped_horizon_point_in_face(
293 face: &FaceRecord,
294 point: Vec2,
295 tolerance: f64,
296) -> Result<Option<PolygonClass>, String> {
297 if face.surface.closed_directions()? != (true, false) || face.loops.is_empty() {
298 return Ok(None);
299 }
300 if std::env::var("BREP_HORIZON_CONTAINMENT").as_deref() == Ok("0") {
301 return Ok(None);
302 }
303 if face.loops.len() <= 2
314 && std::env::var("BREP_HORIZON_SINGLE_LOOP").as_deref() == Ok("0")
315 {
316 return Ok(None);
317 }
318 let [u0, u1] = face.surface.domain_u()?;
319 let u_period = u1 - u0;
320 if u_period <= 0.0 {
321 return Ok(None);
322 }
323 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
324 let mut loops: Vec<Vec<Vec2>> = Vec::new();
325 let mut straddling = false;
326 for loop_record in &face.loops {
327 let mut points: Vec<Vec2> = Vec::new();
328 for coedge in &loop_record.coedges {
329 let curve = &coedge.pcurve;
330 let [start, end] = curve.domain()?;
331 let sample_count =
332 2usize.max((interior_knot_count(curve) + 1) * (curve.degree + 1) * 4);
333 for index in 0..sample_count {
334 let parameter = start + (end - start) * index as f64 / sample_count as f64;
335 let evaluated = curve.evaluate(parameter)?;
336 points.push(Vec2 {
337 x: evaluated.x,
338 y: evaluated.y,
339 });
340 }
341 }
342 if points.len() < 3 {
343 continue;
344 }
345 let mut unwrapped = Vec::with_capacity(points.len());
349 let mut jumps = 0usize;
350 let mut cursor = points[0];
351 unwrapped.push(cursor);
352 for pair in points.windows(2) {
353 let mut du = pair[1].x - pair[0].x;
354 let folded = du - u_period * (du / u_period).round();
355 if (du - folded).abs() > 0.25 * u_period {
356 jumps += 1;
357 }
358 du = folded;
359 cursor = Vec2 {
360 x: cursor.x + du,
361 y: pair[1].y,
362 };
363 unwrapped.push(cursor);
364 }
365 let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
366 if closure > 0.25 * u_period {
367 if debug {
368 eprintln!(
369 "horizon: face {} loop winds the period (closure {closure:.3}) — bail",
370 face.id
371 );
372 }
373 return Ok(None); }
375 if jumps > 0 {
376 straddling = true;
377 }
378 loops.push(unwrapped);
379 }
380 if !straddling || loops.is_empty() {
381 return Ok(None);
382 }
383 if std::env::var("BREP_HORIZON_CROSS_FRAME").as_deref() == Ok("0") {
398 let mut best: Option<PolygonClass> = None;
399 for shift in [-u_period, 0.0, u_period] {
400 let image = Vec2 {
401 x: point.x + shift,
402 y: point.y,
403 };
404 let mut crossings = 0usize;
405 for polygon in &loops {
406 match point_in_polygon(image, polygon, tolerance) {
407 PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
408 PolygonClass::Inside => crossings += 1,
409 PolygonClass::Outside => {}
410 }
411 }
412 if crossings % 2 == 1 {
413 best = Some(PolygonClass::Inside);
414 } else if best.is_none() {
415 best = Some(PolygonClass::Outside);
416 }
417 }
418 if debug {
419 eprintln!(
420 "horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
421 face.id,
422 loops.len(),
423 best
424 );
425 }
426 return Ok(best);
427 }
428 let mut crossings = 0usize;
429 for polygon in &loops {
430 let mut image_hits = 0usize;
431 for shift in [-u_period, 0.0, u_period] {
432 let image = Vec2 {
433 x: point.x + shift,
434 y: point.y,
435 };
436 match point_in_polygon(image, polygon, tolerance) {
437 PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
438 PolygonClass::Inside => image_hits += 1,
439 PolygonClass::Outside => {}
440 }
441 }
442 crossings += image_hits % 2;
443 }
444 let class = if crossings % 2 == 1 {
445 PolygonClass::Inside
446 } else {
447 PolygonClass::Outside
448 };
449 if debug {
450 eprintln!(
451 "horizon: face {} loops={} straddling -> {:?}",
452 face.id,
453 loops.len(),
454 class
455 );
456 }
457 Ok(Some(class))
458}
459
460
461fn covering_rim_strip_point_in_face(
493 face: &FaceRecord,
494 point: Vec2,
495 tolerance: f64,
496) -> Result<Option<PolygonClass>, String> {
497 if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
498 return Ok(None);
499 }
500 if std::env::var("BREP_COVERING_RIM_STRIP").as_deref() == Ok("0") {
501 return Ok(None);
502 }
503 let [u0, u1] = face.surface.domain_u()?;
504 let [v0, v1] = face.surface.domain_v()?;
505 let period = u1 - u0;
506 if !(period > 0.0) {
507 return Ok(None);
508 }
509 let v_span = (v1 - v0).abs().max(1e-30);
510 struct Rim {
511 points: Vec<Vec2>,
512 vmin: f64,
513 vmax: f64,
514 ascending: bool,
515 }
516 let mut rims: Vec<Rim> = Vec::with_capacity(2);
517 let mut out_of_domain = false;
518 for loop_record in &face.loops {
519 let mut points: Vec<Vec2> = Vec::new();
520 for coedge in &loop_record.coedges {
521 let curve = &coedge.pcurve;
522 let [start, end] = curve.domain()?;
523 let sample_count =
524 2usize.max((interior_knot_count(curve) + 1) * (curve.degree + 1) * 4);
525 for index in 0..=sample_count {
526 let parameter = start + (end - start) * index as f64 / sample_count as f64;
527 let evaluated = curve.evaluate(parameter)?;
528 points.push(Vec2 {
529 x: evaluated.x,
530 y: evaluated.y,
531 });
532 }
533 }
534 if points.len() < 3 {
535 return Ok(None);
536 }
537 for p in &points {
540 if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
541 out_of_domain = true;
542 }
543 }
544 let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
548 let mut cursor = points[0];
549 unwrapped.push(cursor);
550 for pair in points.windows(2) {
551 let du = pair[1].x - pair[0].x;
552 let folded = du - period * (du / period).round();
553 cursor = Vec2 {
554 x: cursor.x + folded,
555 y: pair[1].y,
556 };
557 unwrapped.push(cursor);
558 }
559 let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
560 if (net.abs() - period).abs() > 2e-2 * period {
561 return Ok(None); }
563 if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
564 return Ok(None); }
566 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
567 for p in &unwrapped {
568 vmin = vmin.min(p.y);
569 vmax = vmax.max(p.y);
570 }
571 rims.push(Rim {
572 points: unwrapped,
573 vmin,
574 vmax,
575 ascending: net > 0.0,
576 });
577 }
578 if !out_of_domain {
579 return Ok(None);
580 }
581 if rims[0].ascending == rims[1].ascending {
582 return Ok(None); }
584 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
585 (&rims[0], &rims[1])
586 } else if rims[1].vmax <= rims[0].vmin {
587 (&rims[1], &rims[0])
588 } else {
589 return Ok(None); };
591 if upper.vmin - lower.vmax <= 1e-6 * v_span {
592 return Ok(None);
593 }
594 if (lower.ascending) != face.same_sense {
599 return Ok(None);
600 }
601 for rim in [lower, upper] {
603 for shift in [-period, 0.0, period] {
604 let image = Vec2 {
605 x: point.x + shift,
606 y: point.y,
607 };
608 for pair in rim.points.windows(2) {
609 if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
610 return Ok(Some(PolygonClass::Boundary));
611 }
612 }
613 }
614 }
615 let mut crossings = 0usize;
618 for rim in [lower, upper] {
619 let window_base = rim.points[0].x.min(rim.points[rim.points.len() - 1].x);
620 let x = window_base + (point.x - window_base).rem_euclid(period);
621 for pair in rim.points.windows(2) {
622 let (a, b) = (pair[0], pair[1]);
623 if (a.x > x) != (b.x > x) {
624 let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
625 if v_cross > point.y {
626 crossings += 1;
627 }
628 }
629 }
630 }
631 Ok(Some(if crossings % 2 == 1 {
632 PolygonClass::Inside
633 } else {
634 PolygonClass::Outside
635 }))
636}
637
638pub fn parameter_point_in_face(
639 face: &FaceRecord,
640 point: Vec2,
641 tolerance: f64,
642) -> Result<PolygonClass, String> {
643 if let Some(class) = seam_band_point_in_face(face, point, tolerance)? {
644 return Ok(class);
645 }
646 if let Some(class) = wrapped_horizon_point_in_face(face, point, tolerance)? {
647 return Ok(class);
648 }
649 if let Some(class) = covering_rim_strip_point_in_face(face, point, tolerance)? {
650 return Ok(class);
651 }
652 let mut crossings = 0;
653 let mut boundary = false;
654 let mut nearest: Option<(SegmentReference<'_>, f64)> = None;
655 for loop_record in &face.loops {
656 let mut polygon = Vec::new();
657 let mut segments = Vec::new();
658 for coedge in &loop_record.coedges {
659 let curve = &coedge.pcurve;
660 let [start, end] = curve.domain()?;
661 let sample_count =
662 2usize.max((interior_knot_count(curve) + 1) * (curve.degree + 1) * 4);
663 for index in 0..sample_count {
664 let parameter = start + (end - start) * index as f64 / sample_count as f64;
665 let evaluated = curve.evaluate(parameter)?;
666 polygon.push(Vec2 {
667 x: evaluated.x,
668 y: evaluated.y,
669 });
670 let parameter_end = if index + 1 < sample_count {
671 start + (end - start) * (index + 1) as f64 / sample_count as f64
672 } else {
673 end
674 };
675 segments.push(SegmentReference {
676 start: Vec2 {
677 x: evaluated.x,
678 y: evaluated.y,
679 },
680 end: Vec2 { x: 0.0, y: 0.0 },
681 curve,
682 parameter_start: parameter,
683 parameter_end,
684 });
685 }
686 }
687 for index in 0..polygon.len() {
688 segments[index].end = polygon[(index + 1) % polygon.len()];
689 }
690 match point_in_polygon(point, &polygon, tolerance) {
691 PolygonClass::Boundary => boundary = true,
692 PolygonClass::Inside => crossings += 1,
693 PolygonClass::Outside => {}
694 }
695 for segment in segments {
696 let distance = point_segment_distance(point, segment.start, segment.end);
697 if nearest
698 .as_ref()
699 .is_none_or(|(_, nearest_distance)| distance < *nearest_distance)
700 {
701 nearest = Some((segment, distance));
702 }
703 }
704 }
705 if boundary {
706 return Ok(PolygonClass::Boundary);
707 }
708 let parity = if crossings % 2 == 1 {
709 PolygonClass::Inside
710 } else {
711 PolygonClass::Outside
712 };
713 let Some((segment, distance)) = nearest else {
714 return Ok(parity);
715 };
716 let segment_length = segment.end.sub(segment.start).length();
717 if distance > segment_length || segment_length <= 0.0 {
718 return Ok(parity);
719 }
720 let chord_parameter = segment.parameter_start
721 + (segment.parameter_end - segment.parameter_start)
722 * (point.sub(segment.start).dot(segment.end.sub(segment.start))
723 / (segment_length * segment_length))
724 .clamp(0.0, 1.0);
725 let [domain_start, domain_end] = segment.curve.domain()?;
726 let mut parameter = chord_parameter;
727 for _ in 0..12 {
728 let derivatives = segment.curve.derivatives(parameter, 2)?;
729 let on_curve = Vec2 {
730 x: derivatives[0].x,
731 y: derivatives[0].y,
732 };
733 let tangent = Vec2 {
734 x: derivatives[1].x,
735 y: derivatives[1].y,
736 };
737 let second = Vec2 {
738 x: derivatives[2].x,
739 y: derivatives[2].y,
740 };
741 let residual = on_curve.sub(point);
742 let denominator = tangent.dot(tangent) + residual.dot(second);
743 if denominator.abs() < 1e-30 {
744 break;
745 }
746 let step = -residual.dot(tangent) / denominator;
747 parameter = (parameter + step).clamp(domain_start, domain_end);
748 if step.abs() < 1e-14 * (domain_end - domain_start + 1.0) {
749 break;
750 }
751 }
752 let margin = 1e-9 * (domain_end - domain_start);
753 if parameter > domain_start + margin && parameter < domain_end - margin {
754 let derivatives = segment.curve.derivatives(parameter, 1)?;
755 let on_curve = Vec2 {
756 x: derivatives[0].x,
757 y: derivatives[0].y,
758 };
759 let tangent = Vec2 {
760 x: derivatives[1].x,
761 y: derivatives[1].y,
762 };
763 let offset = point.sub(on_curve);
764 if offset.length() <= tolerance {
765 return Ok(PolygonClass::Boundary);
766 }
767 let cross = tangent.x * offset.y - tangent.y * offset.x;
768 if cross.abs() > 1e-30 {
769 return Ok(if (cross > 0.0) == face.same_sense {
770 PolygonClass::Inside
771 } else {
772 PolygonClass::Outside
773 });
774 }
775 }
776 Ok(parity)
777}