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 hit_in_inner_wire_3d(
283 topo: &Topology,
284 face_id: FaceId,
285 hit: Point3,
286 normal: &Vec3,
287) -> Result<bool, OperationsError> {
288 for &iw in topo.face(face_id)?.inner_wires() {
289 let hole = brepkit_check::util::wire_polygon(topo, iw)?;
290 if hole.len() >= 3 && point_in_polygon_3d(&hit, &hole, normal) {
291 return Ok(true);
292 }
293 }
294 Ok(false)
295}
296
297fn hit_in_inner_wire_uv<F>(
299 topo: &Topology,
300 face_id: FaceId,
301 hit_u: f64,
302 hit_v: f64,
303 project: &F,
304 v_periodic: bool,
305) -> Result<bool, OperationsError>
306where
307 F: Fn(Point3) -> (f64, f64),
308{
309 for &iw in topo.face(face_id)?.inner_wires() {
310 let hole = brepkit_check::util::wire_polygon(topo, iw)?;
311 if hole.len() < 3 {
312 continue;
313 }
314 let uv_hole = build_uv_boundary(&hole, project, v_periodic);
315 if point_in_uv_boundary(hit_u, hit_v, &uv_hole, v_periodic) {
316 return Ok(true);
317 }
318 }
319 Ok(false)
320}
321
322fn ray_plane_crossings(
324 topo: &Topology,
325 face_id: FaceId,
326 origin: Point3,
327 direction: Vec3,
328 normal: Vec3,
329 d: f64,
330) -> Result<u32, OperationsError> {
331 let denom = normal.dot(direction);
332 if denom.abs() < NEAR_ZERO {
333 return Ok(0);
334 }
335
336 let t = (d - normal.dot(Vec3::new(origin.x(), origin.y(), origin.z()))) / denom;
337 if t <= RAY_T_MIN {
338 return Ok(0);
339 }
340
341 let hit = origin + direction * t;
342 let verts = brepkit_check::util::face_polygon(topo, face_id)?;
348 if verts.len() < 3 {
349 return Ok(0);
350 }
351
352 if point_in_polygon_3d(&hit, &verts, &normal)
353 && !hit_in_inner_wire_3d(topo, face_id, hit, &normal)?
354 {
355 Ok(1)
356 } else {
357 Ok(0)
358 }
359}
360
361fn nonplanar_sphere_arc_halfspaces(
372 topo: &Topology,
373 face_id: FaceId,
374 verts: &[Point3],
375) -> Option<Vec<(Point3, Vec3, f64)>> {
376 let face = topo.face(face_id).ok()?;
377 if !face.inner_wires().is_empty() {
378 return None;
379 }
380 let wire = topo.wire(face.outer_wire()).ok()?;
381 let mut planes: Vec<(Point3, Vec3)> = Vec::new();
382 for oe in wire.edges() {
383 let e = topo.edge(oe.edge()).ok()?;
384 let brepkit_topology::edge::EdgeCurve::Circle(c) = e.curve() else {
385 return None;
386 };
387 planes.push((c.center(), c.normal().normalize().ok()?));
388 }
389 if planes.len() < 2 {
390 return None;
391 }
392 let tol = Tolerance::new();
396 let (c0, n0) = planes[0];
397 let coplanar = planes
398 .iter()
399 .all(|&(c, n)| n.cross(n0).length() <= 1e-9 && (c - c0).dot(n0).abs() <= tol.linear);
400 if coplanar {
401 return None;
402 }
403 let mut cx = 0.0;
405 let mut cy = 0.0;
406 let mut cz = 0.0;
407 #[allow(clippy::cast_precision_loss)]
408 let inv = 1.0 / verts.len() as f64;
409 for v in verts {
410 cx += v.x() * inv;
411 cy += v.y() * inv;
412 cz += v.z() * inv;
413 }
414 let centroid = Point3::new(cx, cy, cz);
415 let FaceSurface::Sphere(sph) = face.surface() else {
416 return None;
417 };
418 let dir = (centroid - sph.center()).normalize().ok()?;
419 let p_ref = sph.center() + dir * sph.radius();
420 let mut out = Vec::with_capacity(planes.len());
421 for (c, n) in planes {
422 let side = (p_ref - c).dot(n);
423 if side.abs() <= tol.linear {
424 return None;
425 }
426 out.push((c, n, side.signum()));
427 }
428 Some(out)
429}
430
431fn count_3d_polygon_crossings(
432 topo: &Topology,
433 face_id: FaceId,
434 origin: Point3,
435 direction: Vec3,
436 roots: &[f64],
437) -> Result<u32, OperationsError> {
438 if roots.is_empty() {
439 return Ok(0);
440 }
441
442 let verts = face_polygon(topo, face_id)?;
443 if verts.len() < 3 {
444 return Ok(0);
445 }
446 if let Some(halfspaces) = nonplanar_sphere_arc_halfspaces(topo, face_id, &verts) {
454 let mut crossings = 0u32;
455 for &t in roots {
456 if t <= RAY_T_MIN {
457 continue;
458 }
459 let hit = origin + direction * t;
460 if halfspaces
461 .iter()
462 .all(|&(c, n, sign)| (hit - c).dot(n) * sign >= -HALF_SPACE_EPS)
463 {
464 crossings += 1;
465 }
466 }
467 return Ok(crossings);
468 }
469 let mut normal = polygon_normal(&verts);
470 let face = topo.face(face_id)?;
473 if face.is_reversed() {
474 normal = -normal;
475 }
476 let ref_pt = verts[0];
478
479 let mut crossings = 0u32;
480 for &t in roots {
481 if t <= RAY_T_MIN {
482 continue;
483 }
484 let hit = origin + direction * t;
485
486 let side = (hit - ref_pt).dot(normal);
489 if side < -HALF_SPACE_EPS {
490 continue;
491 }
492
493 if point_in_polygon_3d(&hit, &verts, &normal)
494 && !hit_in_inner_wire_3d(topo, face_id, hit, &normal)?
495 {
496 crossings += 1;
497 }
498 }
499
500 Ok(crossings)
501}
502
503fn count_analytic_crossings<F>(
512 topo: &Topology,
513 face_id: FaceId,
514 origin: Point3,
515 direction: Vec3,
516 roots: &[f64],
517 project: F,
518 v_periodic: bool,
519) -> Result<u32, OperationsError>
520where
521 F: Fn(Point3) -> (f64, f64),
522{
523 if roots.is_empty() {
524 return Ok(0);
525 }
526
527 let verts = brepkit_check::util::face_polygon(topo, face_id)?;
535
536 let is_full_surface = verts.len() < 3 || {
540 let ref_pt = verts[0];
541 verts
542 .iter()
543 .all(|v| (*v - ref_pt).length_squared() < COINCIDENT_SQ)
544 };
545 if is_full_surface {
546 return Ok(roots.iter().filter(|&&t| t > RAY_T_MIN).count() as u32);
547 }
548
549 let uv_boundary = build_uv_boundary(&verts, &project, v_periodic);
550
551 let mut crossings = 0u32;
552 for &t in roots {
553 if t <= RAY_T_MIN {
554 continue;
555 }
556 let hit = origin + direction * t;
557 let (hit_u, hit_v) = project(hit);
558
559 if point_in_uv_boundary(hit_u, hit_v, &uv_boundary, v_periodic)
560 && !hit_in_inner_wire_uv(topo, face_id, hit_u, hit_v, &project, v_periodic)?
561 {
562 crossings += 1;
563 }
564 }
565
566 Ok(crossings)
567}
568
569#[inline]
575fn unwrap_angle(prev: f64, next: f64) -> f64 {
576 let tau = std::f64::consts::TAU;
577 let diff = next - prev;
578 prev + diff - tau * ((diff + PI) / tau).floor()
579}
580
581fn build_uv_boundary<F>(verts: &[Point3], project: &F, v_periodic: bool) -> Vec<(f64, f64)>
587where
588 F: Fn(Point3) -> (f64, f64),
589{
590 let mut uv: Vec<(f64, f64)> = verts.iter().map(|&p| project(p)).collect();
591
592 for i in 1..uv.len() {
593 uv[i].0 = unwrap_angle(uv[i - 1].0, uv[i].0);
595
596 if v_periodic {
598 uv[i].1 = unwrap_angle(uv[i - 1].1, uv[i].1);
599 }
600 }
601
602 uv
603}
604
605fn point_in_uv_boundary(
610 hit_u: f64,
611 hit_v: f64,
612 uv_boundary: &[(f64, f64)],
613 v_periodic: bool,
614) -> bool {
615 let u_min = uv_boundary
617 .iter()
618 .map(|(u, _)| *u)
619 .fold(f64::INFINITY, f64::min);
620 let u_max = uv_boundary
621 .iter()
622 .map(|(u, _)| *u)
623 .fold(f64::NEG_INFINITY, f64::max);
624 let u_center = (u_min + u_max) * 0.5;
625
626 let hu = unwrap_angle(u_center, hit_u);
628
629 let hv = if v_periodic {
631 let v_min = uv_boundary
632 .iter()
633 .map(|(_, v)| *v)
634 .fold(f64::INFINITY, f64::min);
635 let v_max = uv_boundary
636 .iter()
637 .map(|(_, v)| *v)
638 .fold(f64::NEG_INFINITY, f64::max);
639 let v_center = (v_min + v_max) * 0.5;
640 unwrap_angle(v_center, hit_v)
641 } else {
642 hit_v
643 };
644
645 let poly: Vec<Point2> = uv_boundary
646 .iter()
647 .map(|(u, v)| Point2::new(*u, *v))
648 .collect();
649 let test = Point2::new(hu, hv);
650 point_in_polygon(test, &poly)
651}
652
653fn ray_cylinder_roots(
655 origin: Point3,
656 direction: Vec3,
657 cyl: &brepkit_math::surfaces::CylindricalSurface,
658) -> Vec<f64> {
659 let ov = origin - cyl.origin();
660 let axis = cyl.axis();
661
662 let ov_perp = ov - axis * ov.dot(axis);
664 let d_perp = direction - axis * direction.dot(axis);
665
666 let a = d_perp.dot(d_perp);
667 let b = 2.0 * ov_perp.dot(d_perp);
668 let c = ov_perp.dot(ov_perp) - cyl.radius() * cyl.radius();
669
670 solve_quadratic(a, b, c)
671}
672
673fn ray_cone_roots(
675 origin: Point3,
676 direction: Vec3,
677 cone: &brepkit_math::surfaces::ConicalSurface,
678) -> Vec<f64> {
679 let ov = origin - cone.apex();
680 let axis = cone.axis();
681 let cos_a = cone.half_angle().cos();
682 let cos2 = cos_a * cos_a;
683
684 let d_dot_a = direction.dot(axis);
685 let ov_dot_a = ov.dot(axis);
686
687 let sin2 = 1.0 - cos2;
693
694 let a = cos2 * d_dot_a * d_dot_a - sin2 * (direction.dot(direction) - d_dot_a * d_dot_a);
695 let half_b = cos2 * d_dot_a * ov_dot_a - sin2 * (direction.dot(ov) - d_dot_a * ov_dot_a);
696 let c = cos2 * ov_dot_a * ov_dot_a - sin2 * (ov.dot(ov) - ov_dot_a * ov_dot_a);
697
698 solve_quadratic(a, 2.0 * half_b, c)
699}
700
701fn ray_sphere_roots(
703 origin: Point3,
704 direction: Vec3,
705 sph: &brepkit_math::surfaces::SphericalSurface,
706) -> Vec<f64> {
707 let ov = origin - sph.center();
708
709 let a = direction.dot(direction);
710 let b = 2.0 * ov.dot(direction);
711 let c = ov.dot(ov) - sph.radius() * sph.radius();
712
713 solve_quadratic(a, b, c)
714}
715
716fn ray_torus_roots(
723 origin: Point3,
724 direction: Vec3,
725 tor: &brepkit_math::surfaces::ToroidalSurface,
726) -> Vec<f64> {
727 brepkit_math::analytic_intersection::intersect_line_torus(tor, origin, direction)
728}
729
730fn ray_crossings_nurbs(
735 topo: &Topology,
736 face_id: FaceId,
737 origin: Point3,
738 direction: Vec3,
739 surface: &brepkit_math::nurbs::surface::NurbsSurface,
740) -> Result<u32, OperationsError> {
741 use brepkit_math::nurbs::intersection::intersect_line_nurbs;
742
743 let hits = intersect_line_nurbs(surface, origin, direction, 20)?;
744 if hits.is_empty() {
745 return Ok(0);
746 }
747
748 let verts = face_polygon(topo, face_id)?;
749 if verts.len() < 3 {
750 return Ok(hits
752 .iter()
753 .filter(|h| {
754 let diff = h.point - origin;
755 let t = Vec3::new(diff.x(), diff.y(), diff.z()).dot(direction);
756 t > RAY_T_MIN
757 })
758 .count() as u32);
759 }
760
761 let project = |p: Point3| -> (f64, f64) { surface.project_point(p) };
762 let uv_boundary = build_uv_boundary(&verts, &project, false);
763
764 let mut crossings = 0u32;
765 for hit in &hits {
766 let diff = hit.point - origin;
768 let t = Vec3::new(diff.x(), diff.y(), diff.z()).dot(direction) / direction.dot(direction);
769 if t <= RAY_T_MIN {
770 continue;
771 }
772
773 let (hit_u, hit_v) = hit.param1;
775 if point_in_uv_boundary(hit_u, hit_v, &uv_boundary, false)
776 && !hit_in_inner_wire_uv(topo, face_id, hit_u, hit_v, &project, false)?
777 {
778 crossings += 1;
779 }
780 }
781
782 Ok(crossings)
783}
784
785#[allow(clippy::similar_names)]
793fn compute_winding_number(
794 topo: &Topology,
795 solid: SolidId,
796 point: Point3,
797 deflection: f64,
798 tolerance: f64,
799) -> Result<(f64, bool), OperationsError> {
800 let solid_data = topo.solid(solid)?;
801 let shell = topo.shell(solid_data.outer_shell())?;
802
803 if is_on_boundary(topo, shell.faces(), point, tolerance)? {
804 return Ok((0.0, true));
805 }
806
807 let direction = Vec3::new(1.0, 0.3, 0.1); let mut crossings = 0u32;
809 for &fid in shell.faces() {
810 crossings += count_face_ray_crossings(topo, fid, point, direction, deflection)?;
811 }
812
813 let winding = if crossings % 2 == 1 { 1.0 } else { 0.0 };
815 Ok((winding, false))
816}
817
818fn polygon_normal(verts: &[Point3]) -> Vec3 {
820 let mut nx = 0.0;
821 let mut ny = 0.0;
822 let mut nz = 0.0;
823 let n = verts.len();
824 for i in 0..n {
825 let j = (i + 1) % n;
826 let vi = verts[i];
827 let vj = verts[j];
828 nx += (vi.y() - vj.y()) * (vi.z() + vj.z());
829 ny += (vi.z() - vj.z()) * (vi.x() + vj.x());
830 nz += (vi.x() - vj.x()) * (vi.y() + vj.y());
831 }
832 let len = (nx * nx + ny * ny + nz * nz).sqrt();
833 if len < DEGENERATE_LEN {
834 Vec3::new(0.0, 0.0, 1.0)
835 } else {
836 Vec3::new(nx / len, ny / len, nz / len)
837 }
838}
839
840fn solve_quadratic(a: f64, b: f64, c: f64) -> Vec<f64> {
842 if a.abs() < NEAR_ZERO {
843 if b.abs() < NEAR_ZERO {
844 return Vec::new();
845 }
846 return vec![-c / b];
847 }
848
849 let disc = b * b - 4.0 * a * c;
850 if disc < -RAY_T_MIN {
851 return Vec::new();
852 }
853 if disc < RAY_T_MIN {
854 return vec![-b / (2.0 * a)];
855 }
856
857 let sqrt_disc = disc.sqrt();
858 let q = if b >= 0.0 {
859 -0.5 * (b + sqrt_disc)
860 } else {
861 -0.5 * (b - sqrt_disc)
862 };
863
864 let mut roots = Vec::with_capacity(2);
865 roots.push(q / a);
866 if q.abs() > NEAR_ZERO {
867 roots.push(c / q);
868 }
869 roots
870}
871
872#[cfg(test)]
873#[allow(clippy::unwrap_used, clippy::expect_used)]
874mod tests {
875 use super::*;
876 use crate::primitives::{make_box, make_cone, make_cylinder, make_sphere, make_torus};
877
878 #[test]
879 fn point_inside_box() {
880 let mut topo = Topology::new();
881 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
882
883 let result = classify_point(&topo, solid, Point3::new(1.0, 1.0, 1.0), 0.1, 1e-6).unwrap();
884 assert_eq!(result, PointClassification::Inside);
885 }
886
887 #[test]
888 fn point_outside_box() {
889 let mut topo = Topology::new();
890 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
891
892 let result = classify_point(&topo, solid, Point3::new(5.0, 5.0, 5.0), 0.1, 1e-6).unwrap();
893 assert_eq!(result, PointClassification::Outside);
894 }
895
896 #[test]
897 fn point_on_boundary_box() {
898 let mut topo = Topology::new();
899 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
900
901 let result = classify_point(&topo, solid, Point3::new(1.0, 1.0, 2.0), 0.1, 1e-3).unwrap();
902 assert_eq!(result, PointClassification::OnBoundary);
903 }
904
905 #[test]
906 fn point_outside_negative_direction() {
907 let mut topo = Topology::new();
908 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
909
910 let result =
911 classify_point(&topo, solid, Point3::new(-5.0, -5.0, -5.0), 0.1, 1e-6).unwrap();
912 assert_eq!(result, PointClassification::Outside);
913 }
914
915 #[test]
921 fn point_in_open_pocket_is_outside() {
922 let mut topo = Topology::new();
923 let plate = make_box(&mut topo, 100.0, 100.0, 10.0).unwrap();
924 let tool = make_box(&mut topo, 60.0, 60.0, 4.0).unwrap();
925 crate::transform::transform_solid(
926 &mut topo,
927 tool,
928 &brepkit_math::mat::Mat4::translation(20.0, 20.0, 6.0),
929 )
930 .unwrap();
931 let pocketed =
932 crate::boolean::boolean(&mut topo, crate::boolean::BooleanOp::Cut, plate, tool)
933 .unwrap();
934
935 let result =
936 classify_point(&topo, pocketed, Point3::new(50.0, 50.0, 8.0), 0.1, 1e-6).unwrap();
937 assert_eq!(result, PointClassification::Outside);
938 }
939
940 #[test]
941 fn point_near_corner() {
942 let mut topo = Topology::new();
943 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
944
945 let result = classify_point(&topo, solid, Point3::new(0.9, 0.9, 0.9), 0.1, 1e-6).unwrap();
946 assert_eq!(result, PointClassification::Inside);
947 }
948
949 #[test]
950 fn point_inside_cylinder() {
951 let mut topo = Topology::new();
952 let solid = make_cylinder(&mut topo, 2.0, 5.0).unwrap();
953
954 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
955 assert_eq!(result, PointClassification::Inside);
956 }
957
958 #[test]
959 fn point_outside_cylinder() {
960 let mut topo = Topology::new();
961 let solid = make_cylinder(&mut topo, 2.0, 5.0).unwrap();
962
963 let result = classify_point(&topo, solid, Point3::new(10.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
964 assert_eq!(result, PointClassification::Outside);
965 }
966
967 #[test]
968 fn point_inside_sphere() {
969 let mut topo = Topology::new();
970 let solid = make_sphere(&mut topo, 3.0, 32).unwrap();
971
972 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
973 assert_eq!(result, PointClassification::Inside);
974 }
975
976 #[test]
977 fn point_outside_sphere() {
978 let mut topo = Topology::new();
979 let solid = make_sphere(&mut topo, 3.0, 32).unwrap();
980
981 let result = classify_point(&topo, solid, Point3::new(5.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
982 assert_eq!(result, PointClassification::Outside);
983 }
984
985 #[test]
986 fn point_inside_cone() {
987 let mut topo = Topology::new();
988 let solid = make_cone(&mut topo, 2.0, 1.0, 5.0).unwrap();
989
990 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
992 assert_eq!(result, PointClassification::Inside);
993 }
994
995 #[test]
996 fn point_outside_cone() {
997 let mut topo = Topology::new();
998 let solid = make_cone(&mut topo, 2.0, 1.0, 5.0).unwrap();
999
1000 let result = classify_point(&topo, solid, Point3::new(10.0, 0.0, 2.5), 0.1, 1e-6).unwrap();
1001 assert_eq!(result, PointClassification::Outside);
1002 }
1003
1004 #[test]
1005 fn point_inside_torus() {
1006 let mut topo = Topology::new();
1007 let solid = make_torus(&mut topo, 3.0, 1.0, 32).unwrap();
1009
1010 let result = classify_point(&topo, solid, Point3::new(3.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
1012 assert_eq!(result, PointClassification::Inside);
1013 }
1014
1015 #[test]
1016 fn point_outside_torus() {
1017 let mut topo = Topology::new();
1018 let solid = make_torus(&mut topo, 3.0, 1.0, 32).unwrap();
1019
1020 let result = classify_point(&topo, solid, Point3::new(0.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
1022 assert_eq!(result, PointClassification::Outside);
1023 }
1024
1025 #[test]
1026 fn point_outside_torus_far() {
1027 let mut topo = Topology::new();
1028 let solid = make_torus(&mut topo, 3.0, 1.0, 32).unwrap();
1029
1030 let result = classify_point(&topo, solid, Point3::new(10.0, 0.0, 0.0), 0.1, 1e-6).unwrap();
1032 assert_eq!(result, PointClassification::Outside);
1033 }
1034
1035 fn make_partial_torus(
1038 topo: &mut Topology,
1039 big_r: f64,
1040 rho: f64,
1041 angle: f64,
1042 ) -> brepkit_topology::solid::SolidId {
1043 use brepkit_math::curves::Circle3D;
1044 use brepkit_topology::edge::{Edge, EdgeCurve};
1045 use brepkit_topology::face::Face;
1046 use brepkit_topology::vertex::Vertex;
1047 use brepkit_topology::wire::{OrientedEdge, Wire};
1048
1049 let circ =
1050 Circle3D::new(Point3::new(big_r, 0.0, 0.0), Vec3::new(0.0, 1.0, 0.0), rho).unwrap();
1051 let p0 = circ.evaluate(0.0);
1052 let v0 = topo.add_vertex(Vertex::new(p0, 1e-7));
1053 let eid = topo.add_edge(Edge::new(v0, v0, EdgeCurve::Circle(circ)));
1054 let wire = Wire::new(vec![OrientedEdge::new(eid, true)], true).unwrap();
1055 let wid = topo.add_wire(wire);
1056 let face = topo.add_face(Face::new(
1057 wid,
1058 vec![],
1059 FaceSurface::Plane {
1060 normal: Vec3::new(0.0, 1.0, 0.0),
1061 d: 0.0,
1062 },
1063 ));
1064 crate::revolve::revolve(
1065 topo,
1066 face,
1067 Point3::new(0.0, 0.0, 0.0),
1068 Vec3::new(0.0, 0.0, 1.0),
1069 angle,
1070 )
1071 .unwrap()
1072 }
1073
1074 #[test]
1081 fn partial_turn_torus_band_classification() {
1082 let (big_r, rho, angle) = (6.0_f64, 2.0_f64, 2.0 * PI / 3.0);
1083 let mut topo = Topology::new();
1084 let solid = make_partial_torus(&mut topo, big_r, rho, angle);
1085
1086 let mid = angle / 2.0;
1087 let inside = [
1088 Point3::new(big_r * mid.cos(), big_r * mid.sin(), 0.0),
1089 Point3::new(big_r * mid.cos(), big_r * mid.sin(), 1.0),
1090 Point3::new(big_r * mid.cos(), big_r * mid.sin(), -1.0),
1091 Point3::new(big_r * 0.05f64.cos(), big_r * 0.05f64.sin(), 0.0),
1092 Point3::new(
1093 big_r * (angle - 0.05).cos(),
1094 big_r * (angle - 0.05).sin(),
1095 0.0,
1096 ),
1097 Point3::new((big_r - 1.5) * mid.cos(), (big_r - 1.5) * mid.sin(), 0.0),
1098 Point3::new((big_r + 1.5) * mid.cos(), (big_r + 1.5) * mid.sin(), 0.0),
1099 ];
1100 for p in inside {
1101 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1102 assert_eq!(result, PointClassification::Inside, "probe {p:?}");
1103 }
1104
1105 let outside = [
1106 Point3::new(big_r * mid.cos(), big_r * mid.sin(), 2.5),
1107 Point3::new(0.0, 0.0, 0.0),
1108 Point3::new(-big_r, 0.0, 0.0),
1109 Point3::new(
1110 big_r * (angle + 0.1).cos(),
1111 big_r * (angle + 0.1).sin(),
1112 0.0,
1113 ),
1114 Point3::new(big_r * (-0.1f64).cos(), big_r * (-0.1f64).sin(), 0.0),
1115 ];
1116 for p in outside {
1117 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1118 assert_eq!(result, PointClassification::Outside, "probe {p:?}");
1119 }
1120 }
1121
1122 #[test]
1125 fn full_turn_torus_classification() {
1126 let (big_r, rho) = (6.0_f64, 2.0_f64);
1127 let mut topo = Topology::new();
1128 let solid = make_partial_torus(&mut topo, big_r, rho, 2.0 * PI);
1129
1130 for theta in [0.0_f64, 1.0, 2.5, 4.0, 5.5] {
1131 let p = Point3::new(big_r * theta.cos(), big_r * theta.sin(), 0.0);
1132 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1133 assert_eq!(result, PointClassification::Inside, "tube center {theta}");
1134 }
1135 for p in [
1136 Point3::new(0.0, 0.0, 0.0),
1137 Point3::new(big_r, 0.0, 2.5),
1138 Point3::new(2.0 * big_r, 0.0, 0.0),
1139 ] {
1140 let result = classify_point(&topo, solid, p, 0.05, 1e-6).unwrap();
1141 assert_eq!(result, PointClassification::Outside, "probe {p:?}");
1142 }
1143 }
1144
1145 #[test]
1146 fn winding_point_inside_box() {
1147 let mut topo = Topology::new();
1148 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1149
1150 let result =
1151 classify_point_winding(&topo, solid, Point3::new(1.0, 1.0, 1.0), 0.1, 1e-6).unwrap();
1152 assert_eq!(result, PointClassification::Inside);
1153 }
1154
1155 #[test]
1156 fn winding_point_outside_box() {
1157 let mut topo = Topology::new();
1158 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1159
1160 let result =
1161 classify_point_winding(&topo, solid, Point3::new(5.0, 5.0, 5.0), 0.1, 1e-6).unwrap();
1162 assert_eq!(result, PointClassification::Outside);
1163 }
1164
1165 #[test]
1166 fn robust_point_inside_box() {
1167 let mut topo = Topology::new();
1168 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1169
1170 let result =
1171 classify_point_robust(&topo, solid, Point3::new(1.0, 1.0, 1.0), 0.1, 1e-6).unwrap();
1172 assert_eq!(result, PointClassification::Inside);
1173 }
1174
1175 #[test]
1176 fn robust_point_outside_box() {
1177 let mut topo = Topology::new();
1178 let solid = make_box(&mut topo, 2.0, 2.0, 2.0).unwrap();
1179
1180 let result =
1181 classify_point_robust(&topo, solid, Point3::new(5.0, 5.0, 5.0), 0.1, 1e-6).unwrap();
1182 assert_eq!(result, PointClassification::Outside);
1183 }
1184
1185 #[test]
1186 fn quadratic_two_roots() {
1187 let mut roots = solve_quadratic(1.0, -5.0, 6.0);
1188 assert_eq!(roots.len(), 2);
1189 roots.sort_by(|a, b| a.partial_cmp(b).unwrap());
1190 let sorted = roots;
1191 assert!((sorted[0] - 2.0).abs() < 1e-10);
1192 assert!((sorted[1] - 3.0).abs() < 1e-10);
1193 }
1194
1195 #[test]
1196 fn quadratic_no_roots() {
1197 let roots = solve_quadratic(1.0, 0.0, 1.0);
1198 assert!(roots.is_empty());
1199 }
1200}