1use brepkit_math::predicates::point_in_polygon;
12use brepkit_math::tolerance::Tolerance;
13use brepkit_math::traits::ParametricSurface;
14use brepkit_math::vec::{Point2, Point3, Vec3};
15use brepkit_topology::Topology;
16use brepkit_topology::face::{FaceId, FaceSurface};
17use brepkit_topology::solid::SolidId;
18
19use std::f64::consts::PI;
20
21use crate::OperationsError;
22use crate::boolean::face_polygon;
23use crate::distance::{point_in_polygon_3d, point_to_face_distance};
24
25const NEAR_ZERO: f64 = 1e-15;
31
32const RAY_T_MIN: f64 = 1e-12;
34
35const HALF_SPACE_EPS: f64 = 1e-10;
37
38const DEGENERATE_LEN: f64 = 1e-30;
40
41const COINCIDENT_SQ: f64 = 1e-12;
43
44#[derive(Debug, Clone, Copy, PartialEq, Eq)]
46pub enum PointClassification {
47 Inside,
49 Outside,
51 OnBoundary,
53}
54
55pub fn classify_point(
68 topo: &Topology,
69 solid: SolidId,
70 point: Point3,
71 deflection: f64,
72 tolerance: f64,
73) -> Result<PointClassification, OperationsError> {
74 let solid_data = topo.solid(solid)?;
75 let shell = topo.shell(solid_data.outer_shell())?;
76
77 if is_on_boundary(topo, shell.faces(), point, tolerance)? {
78 return Ok(PointClassification::OnBoundary);
79 }
80
81 let ray_dirs = [
83 Vec3::new(
84 0.573_576_436_351_046,
85 0.740_535_693_464_567_5,
86 0.350_889_803_483_932_2,
87 ),
88 Vec3::new(
89 -0.350_889_803_483_932_2,
90 0.573_576_436_351_046,
91 0.740_535_693_464_567_5,
92 ),
93 ];
94
95 let mut inside_votes = 0u32;
96 for &dir in &ray_dirs {
97 let crossings = count_ray_crossings(topo, shell.faces(), point, dir, deflection)?;
98 if crossings % 2 == 1 {
99 inside_votes += 1;
100 }
101 }
102
103 if inside_votes >= 2 {
104 Ok(PointClassification::Inside)
105 } else {
106 Ok(PointClassification::Outside)
107 }
108}
109
110pub fn classify_point_winding(
126 topo: &Topology,
127 solid: SolidId,
128 point: Point3,
129 deflection: f64,
130 tolerance: f64,
131) -> Result<PointClassification, OperationsError> {
132 let (winding, on_boundary) = compute_winding_number(topo, solid, point, deflection, tolerance)?;
133 if on_boundary {
134 return Ok(PointClassification::OnBoundary);
135 }
136 if winding > 0.5 {
137 Ok(PointClassification::Inside)
138 } else {
139 Ok(PointClassification::Outside)
140 }
141}
142
143pub fn classify_point_robust(
152 topo: &Topology,
153 solid: SolidId,
154 point: Point3,
155 deflection: f64,
156 tolerance: f64,
157) -> Result<PointClassification, OperationsError> {
158 let (winding, on_boundary) = compute_winding_number(topo, solid, point, deflection, tolerance)?;
159 if on_boundary {
160 return Ok(PointClassification::OnBoundary);
161 }
162
163 if winding > 0.6 {
164 return Ok(PointClassification::Inside);
165 }
166 if winding < 0.4 {
167 return Ok(PointClassification::Outside);
168 }
169
170 classify_point(topo, solid, point, deflection, tolerance)
172}
173
174fn is_on_boundary(
178 topo: &Topology,
179 faces: &[FaceId],
180 point: Point3,
181 tolerance: f64,
182) -> Result<bool, OperationsError> {
183 let tol = Tolerance::new();
184 for &fid in faces {
185 if let Some((dist, _)) = point_to_face_distance(topo, point, fid, tol)?
186 && dist < tolerance
187 {
188 return Ok(true);
189 }
190 }
191 Ok(false)
192}
193
194fn count_ray_crossings(
196 topo: &Topology,
197 faces: &[FaceId],
198 origin: Point3,
199 direction: Vec3,
200 deflection: f64,
201) -> Result<u32, OperationsError> {
202 let mut crossings = 0u32;
203 for &fid in faces {
204 crossings += count_face_ray_crossings(topo, fid, origin, direction, deflection)?;
205 }
206 Ok(crossings)
207}
208
209#[allow(clippy::too_many_lines)]
211fn count_face_ray_crossings(
212 topo: &Topology,
213 face_id: FaceId,
214 origin: Point3,
215 direction: Vec3,
216 _deflection: f64,
217) -> Result<u32, OperationsError> {
218 let face = topo.face(face_id)?;
219 match face.surface() {
220 FaceSurface::Plane { normal, d } => {
221 ray_plane_crossings(topo, face_id, origin, direction, *normal, *d)
222 }
223 FaceSurface::Cylinder(cyl) => {
224 let cyl = cyl.clone();
225 let roots = ray_cylinder_roots(origin, direction, &cyl);
226 count_analytic_crossings(
227 topo,
228 face_id,
229 origin,
230 direction,
231 &roots,
232 |p| cyl.project_point(p),
233 false,
234 )
235 }
236 FaceSurface::Cone(cone) => {
237 let cone = cone.clone();
238 let roots = ray_cone_roots(origin, direction, &cone);
239 count_analytic_crossings(
240 topo,
241 face_id,
242 origin,
243 direction,
244 &roots,
245 |p| cone.project_point(p),
246 false,
247 )
248 }
249 FaceSurface::Sphere(sph) => {
250 let sph = sph.clone();
253 let roots = ray_sphere_roots(origin, direction, &sph);
254 count_3d_polygon_crossings(topo, face_id, origin, direction, &roots)
255 }
256 FaceSurface::Torus(tor) => {
257 let tor = tor.clone();
258 let roots = ray_torus_roots(origin, direction, &tor);
259 count_analytic_crossings(
260 topo,
261 face_id,
262 origin,
263 direction,
264 &roots,
265 |p| tor.project_point(p),
266 true,
267 )
268 }
269 FaceSurface::Nurbs(surface) => {
270 ray_crossings_nurbs(topo, face_id, origin, direction, surface)
271 }
272 }
273}
274
275fn ray_plane_crossings(
277 topo: &Topology,
278 face_id: FaceId,
279 origin: Point3,
280 direction: Vec3,
281 normal: Vec3,
282 d: f64,
283) -> Result<u32, OperationsError> {
284 let denom = normal.dot(direction);
285 if denom.abs() < NEAR_ZERO {
286 return Ok(0);
287 }
288
289 let t = (d - normal.dot(Vec3::new(origin.x(), origin.y(), origin.z()))) / denom;
290 if t <= RAY_T_MIN {
291 return Ok(0);
292 }
293
294 let hit = origin + direction * t;
295 let verts = brepkit_check::util::face_polygon(topo, face_id)?;
301 if verts.len() < 3 {
302 return Ok(0);
303 }
304
305 if point_in_polygon_3d(&hit, &verts, &normal) {
306 Ok(1)
307 } else {
308 Ok(0)
309 }
310}
311
312fn nonplanar_sphere_arc_halfspaces(
323 topo: &Topology,
324 face_id: FaceId,
325 verts: &[Point3],
326) -> Option<Vec<(Point3, Vec3, f64)>> {
327 let face = topo.face(face_id).ok()?;
328 if !face.inner_wires().is_empty() {
329 return None;
330 }
331 let wire = topo.wire(face.outer_wire()).ok()?;
332 let mut planes: Vec<(Point3, Vec3)> = Vec::new();
333 for oe in wire.edges() {
334 let e = topo.edge(oe.edge()).ok()?;
335 let brepkit_topology::edge::EdgeCurve::Circle(c) = e.curve() else {
336 return None;
337 };
338 planes.push((c.center(), c.normal().normalize().ok()?));
339 }
340 if planes.len() < 2 {
341 return None;
342 }
343 let tol = Tolerance::new();
347 let (c0, n0) = planes[0];
348 let coplanar = planes
349 .iter()
350 .all(|&(c, n)| n.cross(n0).length() <= 1e-9 && (c - c0).dot(n0).abs() <= tol.linear);
351 if coplanar {
352 return None;
353 }
354 let mut cx = 0.0;
356 let mut cy = 0.0;
357 let mut cz = 0.0;
358 #[allow(clippy::cast_precision_loss)]
359 let inv = 1.0 / verts.len() as f64;
360 for v in verts {
361 cx += v.x() * inv;
362 cy += v.y() * inv;
363 cz += v.z() * inv;
364 }
365 let centroid = Point3::new(cx, cy, cz);
366 let FaceSurface::Sphere(sph) = face.surface() else {
367 return None;
368 };
369 let dir = (centroid - sph.center()).normalize().ok()?;
370 let p_ref = sph.center() + dir * sph.radius();
371 let mut out = Vec::with_capacity(planes.len());
372 for (c, n) in planes {
373 let side = (p_ref - c).dot(n);
374 if side.abs() <= tol.linear {
375 return None;
376 }
377 out.push((c, n, side.signum()));
378 }
379 Some(out)
380}
381
382fn count_3d_polygon_crossings(
383 topo: &Topology,
384 face_id: FaceId,
385 origin: Point3,
386 direction: Vec3,
387 roots: &[f64],
388) -> Result<u32, OperationsError> {
389 if roots.is_empty() {
390 return Ok(0);
391 }
392
393 let verts = face_polygon(topo, face_id)?;
394 if verts.len() < 3 {
395 return Ok(0);
396 }
397 if let Some(halfspaces) = nonplanar_sphere_arc_halfspaces(topo, face_id, &verts) {
405 let mut crossings = 0u32;
406 for &t in roots {
407 if t <= RAY_T_MIN {
408 continue;
409 }
410 let hit = origin + direction * t;
411 if halfspaces
412 .iter()
413 .all(|&(c, n, sign)| (hit - c).dot(n) * sign >= -HALF_SPACE_EPS)
414 {
415 crossings += 1;
416 }
417 }
418 return Ok(crossings);
419 }
420 let mut normal = polygon_normal(&verts);
421 let face = topo.face(face_id)?;
424 if face.is_reversed() {
425 normal = -normal;
426 }
427 let ref_pt = verts[0];
429
430 let mut crossings = 0u32;
431 for &t in roots {
432 if t <= RAY_T_MIN {
433 continue;
434 }
435 let hit = origin + direction * t;
436
437 let side = (hit - ref_pt).dot(normal);
440 if side < -HALF_SPACE_EPS {
441 continue;
442 }
443
444 if point_in_polygon_3d(&hit, &verts, &normal) {
445 crossings += 1;
446 }
447 }
448
449 Ok(crossings)
450}
451
452fn count_analytic_crossings<F>(
461 topo: &Topology,
462 face_id: FaceId,
463 origin: Point3,
464 direction: Vec3,
465 roots: &[f64],
466 project: F,
467 v_periodic: bool,
468) -> Result<u32, OperationsError>
469where
470 F: Fn(Point3) -> (f64, f64),
471{
472 if roots.is_empty() {
473 return Ok(0);
474 }
475
476 let verts = brepkit_check::util::face_polygon(topo, face_id)?;
484
485 let is_full_surface = verts.len() < 3 || {
489 let ref_pt = verts[0];
490 verts
491 .iter()
492 .all(|v| (*v - ref_pt).length_squared() < COINCIDENT_SQ)
493 };
494 if is_full_surface {
495 return Ok(roots.iter().filter(|&&t| t > RAY_T_MIN).count() as u32);
496 }
497
498 let uv_boundary = build_uv_boundary(&verts, &project, v_periodic);
499
500 let mut crossings = 0u32;
501 for &t in roots {
502 if t <= RAY_T_MIN {
503 continue;
504 }
505 let hit = origin + direction * t;
506 let (hit_u, hit_v) = project(hit);
507
508 if point_in_uv_boundary(hit_u, hit_v, &uv_boundary, v_periodic) {
509 crossings += 1;
510 }
511 }
512
513 Ok(crossings)
514}
515
516#[inline]
522fn unwrap_angle(prev: f64, next: f64) -> f64 {
523 let tau = std::f64::consts::TAU;
524 let diff = next - prev;
525 prev + diff - tau * ((diff + PI) / tau).floor()
526}
527
528fn build_uv_boundary<F>(verts: &[Point3], project: &F, v_periodic: bool) -> Vec<(f64, f64)>
534where
535 F: Fn(Point3) -> (f64, f64),
536{
537 let mut uv: Vec<(f64, f64)> = verts.iter().map(|&p| project(p)).collect();
538
539 for i in 1..uv.len() {
540 uv[i].0 = unwrap_angle(uv[i - 1].0, uv[i].0);
542
543 if v_periodic {
545 uv[i].1 = unwrap_angle(uv[i - 1].1, uv[i].1);
546 }
547 }
548
549 uv
550}
551
552fn point_in_uv_boundary(
557 hit_u: f64,
558 hit_v: f64,
559 uv_boundary: &[(f64, f64)],
560 v_periodic: bool,
561) -> bool {
562 let u_min = uv_boundary
564 .iter()
565 .map(|(u, _)| *u)
566 .fold(f64::INFINITY, f64::min);
567 let u_max = uv_boundary
568 .iter()
569 .map(|(u, _)| *u)
570 .fold(f64::NEG_INFINITY, f64::max);
571 let u_center = (u_min + u_max) * 0.5;
572
573 let hu = unwrap_angle(u_center, hit_u);
575
576 let hv = if v_periodic {
578 let v_min = uv_boundary
579 .iter()
580 .map(|(_, v)| *v)
581 .fold(f64::INFINITY, f64::min);
582 let v_max = uv_boundary
583 .iter()
584 .map(|(_, v)| *v)
585 .fold(f64::NEG_INFINITY, f64::max);
586 let v_center = (v_min + v_max) * 0.5;
587 unwrap_angle(v_center, hit_v)
588 } else {
589 hit_v
590 };
591
592 let poly: Vec<Point2> = uv_boundary
593 .iter()
594 .map(|(u, v)| Point2::new(*u, *v))
595 .collect();
596 let test = Point2::new(hu, hv);
597 point_in_polygon(test, &poly)
598}
599
600fn ray_cylinder_roots(
602 origin: Point3,
603 direction: Vec3,
604 cyl: &brepkit_math::surfaces::CylindricalSurface,
605) -> Vec<f64> {
606 let ov = origin - cyl.origin();
607 let axis = cyl.axis();
608
609 let ov_perp = ov - axis * ov.dot(axis);
611 let d_perp = direction - axis * direction.dot(axis);
612
613 let a = d_perp.dot(d_perp);
614 let b = 2.0 * ov_perp.dot(d_perp);
615 let c = ov_perp.dot(ov_perp) - cyl.radius() * cyl.radius();
616
617 solve_quadratic(a, b, c)
618}
619
620fn ray_cone_roots(
622 origin: Point3,
623 direction: Vec3,
624 cone: &brepkit_math::surfaces::ConicalSurface,
625) -> Vec<f64> {
626 let ov = origin - cone.apex();
627 let axis = cone.axis();
628 let cos_a = cone.half_angle().cos();
629 let cos2 = cos_a * cos_a;
630
631 let d_dot_a = direction.dot(axis);
632 let ov_dot_a = ov.dot(axis);
633
634 let sin2 = 1.0 - cos2;
640
641 let a = cos2 * d_dot_a * d_dot_a - sin2 * (direction.dot(direction) - d_dot_a * d_dot_a);
642 let half_b = cos2 * d_dot_a * ov_dot_a - sin2 * (direction.dot(ov) - d_dot_a * ov_dot_a);
643 let c = cos2 * ov_dot_a * ov_dot_a - sin2 * (ov.dot(ov) - ov_dot_a * ov_dot_a);
644
645 solve_quadratic(a, 2.0 * half_b, c)
646}
647
648fn ray_sphere_roots(
650 origin: Point3,
651 direction: Vec3,
652 sph: &brepkit_math::surfaces::SphericalSurface,
653) -> Vec<f64> {
654 let ov = origin - sph.center();
655
656 let a = direction.dot(direction);
657 let b = 2.0 * ov.dot(direction);
658 let c = ov.dot(ov) - sph.radius() * sph.radius();
659
660 solve_quadratic(a, b, c)
661}
662
663fn ray_torus_roots(
670 origin: Point3,
671 direction: Vec3,
672 tor: &brepkit_math::surfaces::ToroidalSurface,
673) -> Vec<f64> {
674 brepkit_math::analytic_intersection::intersect_line_torus(tor, origin, direction)
675}
676
677fn ray_crossings_nurbs(
682 topo: &Topology,
683 face_id: FaceId,
684 origin: Point3,
685 direction: Vec3,
686 surface: &brepkit_math::nurbs::surface::NurbsSurface,
687) -> Result<u32, OperationsError> {
688 use brepkit_math::nurbs::intersection::intersect_line_nurbs;
689
690 let hits = intersect_line_nurbs(surface, origin, direction, 20)?;
691 if hits.is_empty() {
692 return Ok(0);
693 }
694
695 let verts = face_polygon(topo, face_id)?;
696 if verts.len() < 3 {
697 return Ok(hits
699 .iter()
700 .filter(|h| {
701 let diff = h.point - origin;
702 let t = Vec3::new(diff.x(), diff.y(), diff.z()).dot(direction);
703 t > RAY_T_MIN
704 })
705 .count() as u32);
706 }
707
708 let project = |p: Point3| -> (f64, f64) { surface.project_point(p) };
709 let uv_boundary = build_uv_boundary(&verts, &project, false);
710
711 let mut crossings = 0u32;
712 for hit in &hits {
713 let diff = hit.point - origin;
715 let t = Vec3::new(diff.x(), diff.y(), diff.z()).dot(direction) / direction.dot(direction);
716 if t <= RAY_T_MIN {
717 continue;
718 }
719
720 let (hit_u, hit_v) = hit.param1;
722 if point_in_uv_boundary(hit_u, hit_v, &uv_boundary, false) {
723 crossings += 1;
724 }
725 }
726
727 Ok(crossings)
728}
729
730#[allow(clippy::similar_names)]
738fn compute_winding_number(
739 topo: &Topology,
740 solid: SolidId,
741 point: Point3,
742 deflection: f64,
743 tolerance: f64,
744) -> Result<(f64, bool), OperationsError> {
745 let solid_data = topo.solid(solid)?;
746 let shell = topo.shell(solid_data.outer_shell())?;
747
748 if is_on_boundary(topo, shell.faces(), point, tolerance)? {
749 return Ok((0.0, true));
750 }
751
752 let direction = Vec3::new(1.0, 0.3, 0.1); let mut crossings = 0u32;
754 for &fid in shell.faces() {
755 crossings += count_face_ray_crossings(topo, fid, point, direction, deflection)?;
756 }
757
758 let winding = if crossings % 2 == 1 { 1.0 } else { 0.0 };
760 Ok((winding, false))
761}
762
763fn polygon_normal(verts: &[Point3]) -> Vec3 {
765 let mut nx = 0.0;
766 let mut ny = 0.0;
767 let mut nz = 0.0;
768 let n = verts.len();
769 for i in 0..n {
770 let j = (i + 1) % n;
771 let vi = verts[i];
772 let vj = verts[j];
773 nx += (vi.y() - vj.y()) * (vi.z() + vj.z());
774 ny += (vi.z() - vj.z()) * (vi.x() + vj.x());
775 nz += (vi.x() - vj.x()) * (vi.y() + vj.y());
776 }
777 let len = (nx * nx + ny * ny + nz * nz).sqrt();
778 if len < DEGENERATE_LEN {
779 Vec3::new(0.0, 0.0, 1.0)
780 } else {
781 Vec3::new(nx / len, ny / len, nz / len)
782 }
783}
784
785fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
787 if a.abs() < NEAR_ZERO {
788 if b.abs() < NEAR_ZERO {
789 return Vec::new();
790 }
791 return vec![-c / b];
792 }
793
794 let disc = b * b - 4.0 * a * c;
795 if disc < -RAY_T_MIN {
796 return Vec::new();
797 }
798 if disc < RAY_T_MIN {
799 return vec![-b / (2.0 * a)];
800 }
801
802 let sqrt_disc = disc.sqrt();
803 let q = if b >= 0.0 {
804 -0.5 * (b + sqrt_disc)
805 } else {
806 -0.5 * (b - sqrt_disc)
807 };
808
809 let mut roots = Vec::with_capacity(2);
810 roots.push(q / a);
811 if q.abs() > NEAR_ZERO {
812 roots.push(c / q);
813 }
814 roots
815}
816
817#[cfg(test)]
818#[allow(clippy::unwrap_used, clippy::expect_used)]
819mod tests {
820 use super::*;
821 use crate::primitives::{make_box, make_cone, make_cylinder, make_sphere, make_torus};
822
823 #[test]
824 fn point_inside_box() {
825 let mut topo = Topology::new();
826 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
827
828 let result = classify_point(&topo, solid, Point3::new(1.0, 1.0, 1.0), 0.1, 1e-6).unwrap();
829 assert_eq!(result, PointClassification::Inside);
830 }
831
832 #[test]
833 fn point_outside_box() {
834 let mut topo = Topology::new();
835 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
836
837 let result = classify_point(&topo, solid, Point3::new(5.0, 5.0, 5.0), 0.1, 1e-6).unwrap();
838 assert_eq!(result, PointClassification::Outside);
839 }
840
841 #[test]
842 fn point_on_boundary_box() {
843 let mut topo = Topology::new();
844 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
845
846 let result = classify_point(&topo, solid, Point3::new(1.0, 1.0, 2.0), 0.1, 1e-3).unwrap();
847 assert_eq!(result, PointClassification::OnBoundary);
848 }
849
850 #[test]
851 fn point_outside_negative_direction() {
852 let mut topo = Topology::new();
853 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
854
855 let result =
856 classify_point(&topo, solid, Point3::new(-5.0, -5.0, -5.0), 0.1, 1e-6).unwrap();
857 assert_eq!(result, PointClassification::Outside);
858 }
859
860 #[test]
861 fn point_near_corner() {
862 let mut topo = Topology::new();
863 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
864
865 let result = classify_point(&topo, solid, Point3::new(0.9, 0.9, 0.9), 0.1, 1e-6).unwrap();
866 assert_eq!(result, PointClassification::Inside);
867 }
868
869 #[test]
870 fn point_inside_cylinder() {
871 let mut topo = Topology::new();
872 let solid = make_cylinder(&mut topo, 2.0, 5.0).unwrap();
873
874 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
875 assert_eq!(result, PointClassification::Inside);
876 }
877
878 #[test]
879 fn point_outside_cylinder() {
880 let mut topo = Topology::new();
881 let solid = make_cylinder(&mut topo, 2.0, 5.0).unwrap();
882
883 let result = classify_point(&topo, solid, Point3::new(10.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
884 assert_eq!(result, PointClassification::Outside);
885 }
886
887 #[test]
888 fn point_inside_sphere() {
889 let mut topo = Topology::new();
890 let solid = make_sphere(&mut topo, 3.0, 32).unwrap();
891
892 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
893 assert_eq!(result, PointClassification::Inside);
894 }
895
896 #[test]
897 fn point_outside_sphere() {
898 let mut topo = Topology::new();
899 let solid = make_sphere(&mut topo, 3.0, 32).unwrap();
900
901 let result = classify_point(&topo, solid, Point3::new(5.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
902 assert_eq!(result, PointClassification::Outside);
903 }
904
905 #[test]
906 fn point_inside_cone() {
907 let mut topo = Topology::new();
908 let solid = make_cone(&mut topo, 2.0, 1.0, 5.0).unwrap();
909
910 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
912 assert_eq!(result, PointClassification::Inside);
913 }
914
915 #[test]
916 fn point_outside_cone() {
917 let mut topo = Topology::new();
918 let solid = make_cone(&mut topo, 2.0, 1.0, 5.0).unwrap();
919
920 let result = classify_point(&topo, solid, Point3::new(10.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
921 assert_eq!(result, PointClassification::Outside);
922 }
923
924 #[test]
925 fn point_inside_torus() {
926 let mut topo = Topology::new();
927 let solid = make_torus(&mut topo, 3.0, 1.0, 32).unwrap();
929
930 let result = classify_point(&topo, solid, Point3::new(3.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
932 assert_eq!(result, PointClassification::Inside);
933 }
934
935 #[test]
936 fn point_outside_torus() {
937 let mut topo = Topology::new();
938 let solid = make_torus(&mut topo, 3.0, 1.0, 32).unwrap();
939
940 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
942 assert_eq!(result, PointClassification::Outside);
943 }
944
945 #[test]
946 fn point_outside_torus_far() {
947 let mut topo = Topology::new();
948 let solid = make_torus(&mut topo, 3.0, 1.0, 32).unwrap();
949
950 let result = classify_point(&topo, solid, Point3::new(10.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
952 assert_eq!(result, PointClassification::Outside);
953 }
954
955 fn make_partial_torus(
958 topo: &mut Topology,
959 big_r: f64,
960 rho: f64,
961 angle: f64,
962 ) -> brepkit_topology::solid::SolidId {
963 use brepkit_math::curves::Circle3D;
964 use brepkit_topology::edge::{Edge, EdgeCurve};
965 use brepkit_topology::face::Face;
966 use brepkit_topology::vertex::Vertex;
967 use brepkit_topology::wire::{OrientedEdge, Wire};
968
969 let circ =
970 Circle3D::new(Point3::new(big_r, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), rho).unwrap();
971 let p0 = circ.evaluate(0.0);
972 let v0 = topo.add_vertex(Vertex::new(p0, 1e-7));
973 let eid = topo.add_edge(Edge::new(v0, v0, EdgeCurve::Circle(circ)));
974 let wire = Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap();
975 let wid = topo.add_wire(wire);
976 let face = topo.add_face(Face::new(
977 wid,
978 vec![],
979 FaceSurface::Plane {
980 normal: Vec3::new(0.0, 1.0, 0.0),
981 d: 0.0,
982 },
983 ));
984 crate::revolve::revolve(
985 topo,
986 face,
987 Point3::new(0.0, 0.0, 0.0),
988 Vec3::new(0.0, 0.0, 1.0),
989 angle,
990 )
991 .unwrap()
992 }
993
994 #[test]
1001 fn partial_turn_torus_band_classification() {
1002 let (big_r, rho, angle) = (6.0_f64, 2.0_f64, 2.0 * PI / 3.0);
1003 let mut topo = Topology::new();
1004 let solid = make_partial_torus(&mut topo, big_r, rho, angle);
1005
1006 let mid = angle / 2.0;
1007 let inside = [
1008 Point3::new(big_r * mid.cos(), big_r * mid.sin(), 0.0),
1009 Point3::new(big_r * mid.cos(), big_r * mid.sin(), 1.0),
1010 Point3::new(big_r * mid.cos(), big_r * mid.sin(), -1.0),
1011 Point3::new(big_r * 0.05f64.cos(), big_r * 0.05f64.sin(), 0.0),
1012 Point3::new(
1013 big_r * (angle - 0.05).cos(),
1014 big_r * (angle - 0.05).sin(),
1015 0.0,
1016 ),
1017 Point3::new((big_r - 1.5) * mid.cos(), (big_r - 1.5) * mid.sin(), 0.0),
1018 Point3::new((big_r + 1.5) * mid.cos(), (big_r + 1.5) * mid.sin(), 0.0),
1019 ];
1020 for p in inside {
1021 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1022 assert_eq!(result, PointClassification::Inside, "probe {p:?}");
1023 }
1024
1025 let outside = [
1026 Point3::new(big_r * mid.cos(), big_r * mid.sin(), 2.5),
1027 Point3::new(0.0, 0.0, 0.0),
1028 Point3::new(-big_r, 0.0, 0.0),
1029 Point3::new(
1030 big_r * (angle + 0.1).cos(),
1031 big_r * (angle + 0.1).sin(),
1032 0.0,
1033 ),
1034 Point3::new(big_r * (-0.1f64).cos(), big_r * (-0.1f64).sin(), 0.0),
1035 ];
1036 for p in outside {
1037 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1038 assert_eq!(result, PointClassification::Outside, "probe {p:?}");
1039 }
1040 }
1041
1042 #[test]
1045 fn full_turn_torus_classification() {
1046 let (big_r, rho) = (6.0_f64, 2.0_f64);
1047 let mut topo = Topology::new();
1048 let solid = make_partial_torus(&mut topo, big_r, rho, 2.0 * PI);
1049
1050 for theta in [0.0_f64, 1.0, 2.5, 4.0, 5.5] {
1051 let p = Point3::new(big_r * theta.cos(), big_r * theta.sin(), 0.0);
1052 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1053 assert_eq!(result, PointClassification::Inside, "tube center {theta}");
1054 }
1055 for p in [
1056 Point3::new(0.0, 0.0, 0.0),
1057 Point3::new(big_r, 0.0, 2.5),
1058 Point3::new(2.0 * big_r, 0.0, 0.0),
1059 ] {
1060 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1061 assert_eq!(result, PointClassification::Outside, "probe {p:?}");
1062 }
1063 }
1064
1065 #[test]
1066 fn winding_point_inside_box() {
1067 let mut topo = Topology::new();
1068 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1069
1070 let result =
1071 classify_point_winding(&topo, solid, Point3::new(1.0, 1.0, 1.0), 0.1, 1e-6).unwrap();
1072 assert_eq!(result, PointClassification::Inside);
1073 }
1074
1075 #[test]
1076 fn winding_point_outside_box() {
1077 let mut topo = Topology::new();
1078 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1079
1080 let result =
1081 classify_point_winding(&topo, solid, Point3::new(5.0, 5.0, 5.0), 0.1, 1e-6).unwrap();
1082 assert_eq!(result, PointClassification::Outside);
1083 }
1084
1085 #[test]
1086 fn robust_point_inside_box() {
1087 let mut topo = Topology::new();
1088 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1089
1090 let result =
1091 classify_point_robust(&topo, solid, Point3::new(1.0, 1.0, 1.0), 0.1, 1e-6).unwrap();
1092 assert_eq!(result, PointClassification::Inside);
1093 }
1094
1095 #[test]
1096 fn robust_point_outside_box() {
1097 let mut topo = Topology::new();
1098 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1099
1100 let result =
1101 classify_point_robust(&topo, solid, Point3::new(5.0, 5.0, 5.0), 0.1, 1e-6).unwrap();
1102 assert_eq!(result, PointClassification::Outside);
1103 }
1104
1105 #[test]
1106 fn quadratic_two_roots() {
1107 let mut roots = solve_quadratic(1.0, -5.0, 6.0);
1108 assert_eq!(roots.len(), 2);
1109 roots.sort_by(|a, b| a.partial_cmp(b).unwrap());
1110 let sorted = roots;
1111 assert!((sorted[0] - 2.0).abs() < 1e-10);
1112 assert!((sorted[1] - 3.0).abs() < 1e-10);
1113 }
1114
1115 #[test]
1116 fn quadratic_no_roots() {
1117 let roots = solve_quadratic(1.0, 0.0, 1.0);
1118 assert!(roots.is_empty());
1119 }
1120}