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 && std::env::var("BREP_HORIZON_SINGLE_LOOP").as_deref() == Ok("0") {
314 return Ok(None);
315 }
316 let [u0, u1] = face.surface.domain_u()?;
317 let u_period = u1 - u0;
318 if u_period <= 0.0 {
319 return Ok(None);
320 }
321 let debug = std::env::var("BREP_DEBUG_HORIZON").is_ok();
322 let mut loops: Vec<Vec<Vec2>> = Vec::new();
323 let mut straddling = false;
324 for loop_record in &face.loops {
325 let mut points: Vec<Vec2> = Vec::new();
326 for coedge in &loop_record.coedges {
327 let curve = &coedge.pcurve;
328 let [start, end] = curve.domain()?;
329 let sample_count =
330 2usize.max((interior_knot_count(curve) + 1) * (curve.degree + 1) * 4);
331 for index in 0..sample_count {
332 let parameter = start + (end - start) * index as f64 / sample_count as f64;
333 let evaluated = curve.evaluate(parameter)?;
334 points.push(Vec2 {
335 x: evaluated.x,
336 y: evaluated.y,
337 });
338 }
339 }
340 if points.len() < 3 {
341 continue;
342 }
343 let mut unwrapped = Vec::with_capacity(points.len());
347 let mut jumps = 0usize;
348 let mut cursor = points[0];
349 unwrapped.push(cursor);
350 for pair in points.windows(2) {
351 let mut du = pair[1].x - pair[0].x;
352 let folded = du - u_period * (du / u_period).round();
353 if (du - folded).abs() > 0.25 * u_period {
354 jumps += 1;
355 }
356 du = folded;
357 cursor = Vec2 {
358 x: cursor.x + du,
359 y: pair[1].y,
360 };
361 unwrapped.push(cursor);
362 }
363 let closure = (unwrapped[0].x - unwrapped[unwrapped.len() - 1].x).abs();
364 if closure > 0.25 * u_period {
365 if debug {
366 eprintln!(
367 "horizon: face {} loop winds the period (closure {closure:.3}) — bail",
368 face.id
369 );
370 }
371 return Ok(None); }
373 if jumps > 0 {
374 straddling = true;
375 }
376 loops.push(unwrapped);
377 }
378 if !straddling || loops.is_empty() {
379 return Ok(None);
380 }
381 if std::env::var("BREP_HORIZON_CROSS_FRAME").as_deref() == Ok("0") {
396 let mut best: Option<PolygonClass> = None;
397 for shift in [-u_period, 0.0, u_period] {
398 let image = Vec2 {
399 x: point.x + shift,
400 y: point.y,
401 };
402 let mut crossings = 0usize;
403 for polygon in &loops {
404 match point_in_polygon(image, polygon, tolerance) {
405 PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
406 PolygonClass::Inside => crossings += 1,
407 PolygonClass::Outside => {}
408 }
409 }
410 if crossings % 2 == 1 {
411 best = Some(PolygonClass::Inside);
412 } else if best.is_none() {
413 best = Some(PolygonClass::Outside);
414 }
415 }
416 if debug {
417 eprintln!(
418 "horizon: face {} loops={} straddling (legacy per-image) -> {:?}",
419 face.id,
420 loops.len(),
421 best
422 );
423 }
424 return Ok(best);
425 }
426 let mut crossings = 0usize;
427 for polygon in &loops {
428 let mut image_hits = 0usize;
429 for shift in [-u_period, 0.0, u_period] {
430 let image = Vec2 {
431 x: point.x + shift,
432 y: point.y,
433 };
434 match point_in_polygon(image, polygon, tolerance) {
435 PolygonClass::Boundary => return Ok(Some(PolygonClass::Boundary)),
436 PolygonClass::Inside => image_hits += 1,
437 PolygonClass::Outside => {}
438 }
439 }
440 crossings += image_hits % 2;
441 }
442 let class = if crossings % 2 == 1 {
443 PolygonClass::Inside
444 } else {
445 PolygonClass::Outside
446 };
447 if debug {
448 eprintln!(
449 "horizon: face {} loops={} straddling -> {:?}",
450 face.id,
451 loops.len(),
452 class
453 );
454 }
455 Ok(Some(class))
456}
457
458fn winding_sphere_cap_point_in_face(
463 face: &FaceRecord,
464 point: Vec2,
465 tolerance: f64,
466) -> Result<Option<PolygonClass>, String> {
467 if !matches!(
468 face.surface.analytic(),
469 Some(crate::AnalyticSurface::Sphere { .. })
470 ) || face.surface.closed_directions()? != (true, false)
471 || face.loops.len() != 2
472 {
473 return Ok(None);
474 }
475 let [u0, u1] = face.surface.domain_u()?;
476 let [v0, v1] = face.surface.domain_v()?;
477 let period = u1 - u0;
478 let v_span = v1 - v0;
479 if !(period > 0.0 && v_span > 0.0) {
480 return Ok(None);
481 }
482 struct WindingLoop {
483 points: Vec<Vec2>,
484 winding: f64,
485 vmin: f64,
486 vmax: f64,
487 }
488 let mut loops = Vec::with_capacity(2);
489 for loop_record in &face.loops {
490 let mut points = Vec::new();
491 for coedge in &loop_record.coedges {
492 let [start, end] = coedge.pcurve.domain()?;
493 let samples = 2usize
494 .max((interior_knot_count(&coedge.pcurve) + 1) * (coedge.pcurve.degree + 1) * 4);
495 for index in 0..samples {
496 let parameter = start + (end - start) * index as f64 / samples as f64;
497 let p = coedge.pcurve.evaluate(parameter)?;
498 points.push(Vec2 { x: p.x, y: p.y });
499 }
500 }
501 if points.len() < 2 {
502 return Ok(None);
503 }
504 let first = points[0];
505 let mut cursor = first;
506 let mut unwrapped = vec![cursor];
507 for next in points.iter().skip(1) {
508 let du = next.x - cursor.x;
509 let folded = du - period * (du / period).round();
510 cursor = Vec2 {
511 x: cursor.x + folded,
512 y: next.y,
513 };
514 unwrapped.push(cursor);
515 }
516 let last_raw = *points.last().unwrap();
517 let closing_du = first.x - last_raw.x;
518 let closing_folded = closing_du - period * (closing_du / period).round();
519 let winding = cursor.x + closing_folded - first.x;
520 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
521 for p in &unwrapped {
522 vmin = vmin.min(p.y);
523 vmax = vmax.max(p.y);
524 }
525 loops.push(WindingLoop {
526 points: unwrapped,
527 winding,
528 vmin,
529 vmax,
530 });
531 }
532 if loops
533 .iter()
534 .any(|loop_data| (loop_data.winding.abs() - period).abs() > 0.05 * period)
535 || loops[0].winding * loops[1].winding >= 0.0
536 {
537 return Ok(None);
538 }
539 let flat = |loop_data: &WindingLoop| loop_data.vmax - loop_data.vmin <= 1e-6 * v_span;
540 let (pole, rim) = match (flat(&loops[0]), flat(&loops[1])) {
541 (true, false) => (&loops[0], &loops[1]),
542 (false, true) => (&loops[1], &loops[0]),
543 _ => return Ok(None),
544 };
545 let pole_v = 0.5 * (pole.vmin + pole.vmax);
546 if (pole_v - v0).abs() > 1e-6 * v_span && (pole_v - v1).abs() > 1e-6 * v_span {
547 return Ok(None);
548 }
549
550 let window_start = rim.points[0].x.min(rim.points[rim.points.len() - 1].x);
551 let query_u = window_start + (point.x - window_start).rem_euclid(period);
552 let mut crossings = Vec::new();
553 for pair in rim.points.windows(2) {
554 let (a, b) = (pair[0], pair[1]);
555 if (a.x > query_u) != (b.x > query_u) {
556 crossings.push(a.y + (query_u - a.x) / (b.x - a.x) * (b.y - a.y));
557 }
558 for image in [-period, 0.0, period] {
559 if point_segment_distance(
560 Vec2 {
561 x: point.x + image,
562 y: point.y,
563 },
564 a,
565 b,
566 ) <= tolerance
567 {
568 return Ok(Some(PolygonClass::Boundary));
569 }
570 }
571 }
572 let Some(rim_v) = crossings
573 .into_iter()
574 .min_by(|a, b| (a - point.y).abs().total_cmp(&(b - point.y).abs()))
575 else {
576 return Ok(None);
577 };
578 let inside = if pole_v < rim_v {
579 point.y <= rim_v + tolerance
580 } else {
581 point.y >= rim_v - tolerance
582 };
583 Ok(Some(if inside {
584 PolygonClass::Inside
585 } else {
586 PolygonClass::Outside
587 }))
588}
589
590fn covering_rim_strip_point_in_face(
622 face: &FaceRecord,
623 point: Vec2,
624 tolerance: f64,
625) -> Result<Option<PolygonClass>, String> {
626 if face.surface.closed_directions()? != (true, false) || face.loops.len() != 2 {
627 return Ok(None);
628 }
629 if std::env::var("BREP_COVERING_RIM_STRIP").as_deref() == Ok("0") {
630 return Ok(None);
631 }
632 let [u0, u1] = face.surface.domain_u()?;
633 let [v0, v1] = face.surface.domain_v()?;
634 let period = u1 - u0;
635 if !(period > 0.0) {
636 return Ok(None);
637 }
638 let v_span = (v1 - v0).abs().max(1e-30);
639 struct Rim {
640 points: Vec<Vec2>,
641 vmin: f64,
642 vmax: f64,
643 ascending: bool,
644 }
645 let mut rims: Vec<Rim> = Vec::with_capacity(2);
646 let mut out_of_domain = false;
647 for loop_record in &face.loops {
648 let mut points: Vec<Vec2> = Vec::new();
649 for coedge in &loop_record.coedges {
650 let curve = &coedge.pcurve;
651 let [start, end] = curve.domain()?;
652 let sample_count =
653 2usize.max((interior_knot_count(curve) + 1) * (curve.degree + 1) * 4);
654 for index in 0..=sample_count {
655 let parameter = start + (end - start) * index as f64 / sample_count as f64;
656 let evaluated = curve.evaluate(parameter)?;
657 points.push(Vec2 {
658 x: evaluated.x,
659 y: evaluated.y,
660 });
661 }
662 }
663 if points.len() < 3 {
664 return Ok(None);
665 }
666 for p in &points {
669 if p.x < u0 - 1e-3 * period || p.x > u1 + 1e-3 * period {
670 out_of_domain = true;
671 }
672 }
673 let mut unwrapped: Vec<Vec2> = Vec::with_capacity(points.len());
677 let mut cursor = points[0];
678 unwrapped.push(cursor);
679 for pair in points.windows(2) {
680 let du = pair[1].x - pair[0].x;
681 let folded = du - period * (du / period).round();
682 cursor = Vec2 {
683 x: cursor.x + folded,
684 y: pair[1].y,
685 };
686 unwrapped.push(cursor);
687 }
688 let net = unwrapped[unwrapped.len() - 1].x - unwrapped[0].x;
689 if (net.abs() - period).abs() > 2e-2 * period {
690 return Ok(None); }
692 if (unwrapped[unwrapped.len() - 1].y - unwrapped[0].y).abs() > 1e-3 * v_span {
693 return Ok(None); }
695 let (mut vmin, mut vmax) = (f64::INFINITY, f64::NEG_INFINITY);
696 for p in &unwrapped {
697 vmin = vmin.min(p.y);
698 vmax = vmax.max(p.y);
699 }
700 rims.push(Rim {
701 points: unwrapped,
702 vmin,
703 vmax,
704 ascending: net > 0.0,
705 });
706 }
707 if !out_of_domain {
708 return Ok(None);
709 }
710 if rims[0].ascending == rims[1].ascending {
711 return Ok(None); }
713 let (lower, upper) = if rims[0].vmax <= rims[1].vmin {
714 (&rims[0], &rims[1])
715 } else if rims[1].vmax <= rims[0].vmin {
716 (&rims[1], &rims[0])
717 } else {
718 return Ok(None); };
720 if upper.vmin - lower.vmax <= 1e-6 * v_span {
721 return Ok(None);
722 }
723 if (lower.ascending) != face.same_sense {
728 return Ok(None);
729 }
730 for rim in [lower, upper] {
732 for shift in [-period, 0.0, period] {
733 let image = Vec2 {
734 x: point.x + shift,
735 y: point.y,
736 };
737 for pair in rim.points.windows(2) {
738 if point_segment_distance(image, pair[0], pair[1]) <= tolerance {
739 return Ok(Some(PolygonClass::Boundary));
740 }
741 }
742 }
743 }
744 let mut crossings = 0usize;
747 for rim in [lower, upper] {
748 let window_base = rim.points[0].x.min(rim.points[rim.points.len() - 1].x);
749 let x = window_base + (point.x - window_base).rem_euclid(period);
750 for pair in rim.points.windows(2) {
751 let (a, b) = (pair[0], pair[1]);
752 if (a.x > x) != (b.x > x) {
753 let v_cross = a.y + (x - a.x) / (b.x - a.x) * (b.y - a.y);
754 if v_cross > point.y {
755 crossings += 1;
756 }
757 }
758 }
759 }
760 Ok(Some(if crossings % 2 == 1 {
761 PolygonClass::Inside
762 } else {
763 PolygonClass::Outside
764 }))
765}
766
767pub fn parameter_point_in_face(
768 face: &FaceRecord,
769 point: Vec2,
770 tolerance: f64,
771) -> Result<PolygonClass, String> {
772 if let Some(class) = seam_band_point_in_face(face, point, tolerance)? {
773 return Ok(class);
774 }
775 if let Some(class) = winding_sphere_cap_point_in_face(face, point, tolerance)? {
776 return Ok(class);
777 }
778 if let Some(class) = wrapped_horizon_point_in_face(face, point, tolerance)? {
779 return Ok(class);
780 }
781 if let Some(class) = covering_rim_strip_point_in_face(face, point, tolerance)? {
782 return Ok(class);
783 }
784 let mut crossings = 0;
785 let mut boundary = false;
786 let mut nearest: Option<(SegmentReference<'_>, f64)> = None;
787 for loop_record in &face.loops {
788 let mut polygon = Vec::new();
789 let mut segments = Vec::new();
790 for coedge in &loop_record.coedges {
791 let curve = &coedge.pcurve;
792 let [start, end] = curve.domain()?;
793 let sample_count =
794 2usize.max((interior_knot_count(curve) + 1) * (curve.degree + 1) * 4);
795 for index in 0..sample_count {
796 let parameter = start + (end - start) * index as f64 / sample_count as f64;
797 let evaluated = curve.evaluate(parameter)?;
798 polygon.push(Vec2 {
799 x: evaluated.x,
800 y: evaluated.y,
801 });
802 let parameter_end = if index + 1 < sample_count {
803 start + (end - start) * (index + 1) as f64 / sample_count as f64
804 } else {
805 end
806 };
807 segments.push(SegmentReference {
808 start: Vec2 {
809 x: evaluated.x,
810 y: evaluated.y,
811 },
812 end: Vec2 { x: 0.0, y: 0.0 },
813 curve,
814 parameter_start: parameter,
815 parameter_end,
816 });
817 }
818 }
819 for index in 0..polygon.len() {
820 segments[index].end = polygon[(index + 1) % polygon.len()];
821 }
822 match point_in_polygon(point, &polygon, tolerance) {
823 PolygonClass::Boundary => boundary = true,
824 PolygonClass::Inside => crossings += 1,
825 PolygonClass::Outside => {}
826 }
827 for segment in segments {
828 let distance = point_segment_distance(point, segment.start, segment.end);
829 if nearest
830 .as_ref()
831 .is_none_or(|(_, nearest_distance)| distance < *nearest_distance)
832 {
833 nearest = Some((segment, distance));
834 }
835 }
836 }
837 if boundary {
838 return Ok(PolygonClass::Boundary);
839 }
840 let parity = if crossings % 2 == 1 {
841 PolygonClass::Inside
842 } else {
843 PolygonClass::Outside
844 };
845 let Some((segment, distance)) = nearest else {
846 return Ok(parity);
847 };
848 let segment_length = segment.end.sub(segment.start).length();
849 if distance > segment_length || segment_length <= 0.0 {
850 return Ok(parity);
851 }
852 let chord_parameter = segment.parameter_start
853 + (segment.parameter_end - segment.parameter_start)
854 * (point.sub(segment.start).dot(segment.end.sub(segment.start))
855 / (segment_length * segment_length))
856 .clamp(0.0, 1.0);
857 let [domain_start, domain_end] = segment.curve.domain()?;
858 let mut parameter = chord_parameter;
859 for _ in 0..12 {
860 let derivatives = segment.curve.derivatives(parameter, 2)?;
861 let on_curve = Vec2 {
862 x: derivatives[0].x,
863 y: derivatives[0].y,
864 };
865 let tangent = Vec2 {
866 x: derivatives[1].x,
867 y: derivatives[1].y,
868 };
869 let second = Vec2 {
870 x: derivatives[2].x,
871 y: derivatives[2].y,
872 };
873 let residual = on_curve.sub(point);
874 let denominator = tangent.dot(tangent) + residual.dot(second);
875 if denominator.abs() < 1e-30 {
876 break;
877 }
878 let step = -residual.dot(tangent) / denominator;
879 parameter = (parameter + step).clamp(domain_start, domain_end);
880 if step.abs() < 1e-14 * (domain_end - domain_start + 1.0) {
881 break;
882 }
883 }
884 let margin = 1e-9 * (domain_end - domain_start);
885 if parameter > domain_start + margin && parameter < domain_end - margin {
886 let derivatives = segment.curve.derivatives(parameter, 1)?;
887 let on_curve = Vec2 {
888 x: derivatives[0].x,
889 y: derivatives[0].y,
890 };
891 let tangent = Vec2 {
892 x: derivatives[1].x,
893 y: derivatives[1].y,
894 };
895 let offset = point.sub(on_curve);
896 if offset.length() <= tolerance {
897 return Ok(PolygonClass::Boundary);
898 }
899 let cross = tangent.x * offset.y - tangent.y * offset.x;
900 if cross.abs() > 1e-30 {
901 return Ok(if (cross > 0.0) == face.same_sense {
902 PolygonClass::Inside
903 } else {
904 PolygonClass::Outside
905 });
906 }
907 }
908 Ok(parity)
909}