1use std::collections::HashSet;
16
17use crate::{Body, CoedgeGeometry, EdgeId, FaceId, Model, ShellId, SolidId, VertexId};
18use geop_core_geometry::{
19 contains::{curve::curve_could_contain, surface::surface_could_contain},
20 intersection::{
21 curve_curve_intersect, curve_surface_intersect, refine_crossing,
22 refine_curve_curve_crossing,
23 },
24 nurb_curve::NurbCurve3D,
25};
26use geop_core_math::{
27 geop_error::{GeopError, GeopResult},
28 scalars::Scalar,
29 solvers::probe::{Probe, attempts},
30 vector::{Vector2, Vector3},
31};
32
33use super::{
34 face::{PointClassification as FaceClassification, face_contains},
35 rng::Rng,
36};
37
38#[derive(Clone, Copy, Debug, PartialEq, Eq)]
40pub enum PointClassification {
41 OnVertex,
43 OnEdge,
45 OnFace,
47 Inside,
49 Outside,
51}
52
53const MAX_RAY_ATTEMPTS: usize = 64;
54
55pub(crate) struct ShellBoundary<'m> {
58 pub(crate) model: &'m Model,
59 pub(crate) shell_id: ShellId,
60 pub(crate) vertices: Vec<VertexId>,
61 pub(crate) edges: Vec<EdgeId>,
62}
63
64impl<'m> ShellBoundary<'m> {
65 pub(crate) fn of(model: &'m Model, shell_id: ShellId) -> Self {
68 let (vertices, edges) = shell_vertices_and_edges(model, shell_id);
69 Self {
70 model,
71 shell_id,
72 vertices,
73 edges,
74 }
75 }
76
77 pub(crate) fn ray_length(&self, point: &Vector3) -> GeopResult<Scalar> {
80 ray_length_for(self.model, &self.vertices, point)
81 }
82}
83
84fn shell_vertices_and_edges(model: &Model, shell_id: ShellId) -> (Vec<VertexId>, Vec<EdgeId>) {
86 let shell = &model.shells[&shell_id];
87 let mut vertex_ids = Vec::new();
88 let mut edge_ids = Vec::new();
89 let mut seen_v = HashSet::new();
90 let mut seen_e = HashSet::new();
91 for &face_id in &shell.faces {
92 for coedge_id in model.iterate_face_coedges(face_id) {
93 let coedge = &model.coedges[&coedge_id];
94 match coedge.geometry {
95 CoedgeGeometry::Edge(edge_id) => {
96 if seen_e.insert(edge_id) {
97 edge_ids.push(edge_id);
98 }
99 let edge = &model.edges[&edge_id];
100 if seen_v.insert(edge.start_vertex) {
101 vertex_ids.push(edge.start_vertex);
102 }
103 if seen_v.insert(edge.end_vertex) {
104 vertex_ids.push(edge.end_vertex);
105 }
106 }
107 CoedgeGeometry::Vertex(vertex_id) => {
108 if seen_v.insert(vertex_id) {
109 vertex_ids.push(vertex_id);
110 }
111 }
112 }
113 }
114 }
115 (vertex_ids, edge_ids)
116}
117
118fn ray_length_for(model: &Model, vertex_ids: &[VertexId], point: &Vector3) -> GeopResult<Scalar> {
123 let mut max_dist_sq = Scalar::ONE;
124 for &vertex_id in vertex_ids {
125 let d = model.vertices[&vertex_id].point.sub(point).norm_sq();
126 if d.could_be_greater(max_dist_sq) {
127 max_dist_sq = d;
128 }
129 }
130 Ok(max_dist_sq
131 .sqrt()?
132 .mul(Scalar::from_f64(3.0))
133 .add(Scalar::ONE))
134}
135
136#[allow(clippy::too_many_arguments)]
156pub(crate) fn cast_ray(
157 boundary: &ShellBoundary,
158 point: Vector3,
159 direction: Vector3,
160 ray_length: Scalar,
161 leaving: Option<FaceId>,
162 max_nodes: usize,
163 epsilon: Scalar,
164 seed: u64,
165) -> GeopResult<Result<Ray, String>> {
166 let ShellBoundary {
167 model,
168 shell_id,
169 vertices: vertex_ids,
170 edges: edge_ids,
171 } = boundary;
172 let (model, shell_id) = (*model, *shell_id);
173 let shell = &model.shells[&shell_id];
174 let far = point.add(&direction.prod_scalar(ray_length));
175 let ray = NurbCurve3D::line(point, far)?;
176
177 for &vertex_id in vertex_ids {
179 let vp = model.vertices[&vertex_id].point;
180 if curve_could_contain(&ray, &vp, max_nodes, epsilon)?.is_some() {
181 return Ok(Err(format!(
182 "it could pass through vertex {vertex_id} at {vp:?}"
183 )));
184 }
185 }
186
187 for &edge_id in edge_ids {
193 let full_curve = &model.edges[&edge_id].curve;
194 let hits = match curve_curve_intersect(&ray, full_curve, 1, max_nodes, epsilon) {
195 Ok(hits) => hits,
196 Err(e) => {
197 return Ok(Err(format!(
198 "its search against edge {edge_id} failed: {e}"
199 )));
200 }
201 };
202 if hits.is_coincident() {
203 return Ok(Err(format!("it runs along edge {edge_id}")));
204 }
205 for (t_hit, s_hit) in hits.into_vec() {
206 let (t_hit, _) = if t_hit.definitely_greater(Scalar::ZERO) {
213 (t_hit, s_hit)
214 } else {
215 refine_curve_curve_crossing(&ray, full_curve, t_hit, s_hit)
216 };
217 if t_hit.definitely_greater(Scalar::ZERO) {
218 return Ok(Err(format!("it could cross edge {edge_id} at t={t_hit:?}")));
219 }
220 }
221 }
222
223 let mut crossings = Vec::new();
224 for &face_id in &shell.faces {
225 let surface = &model.faces[&face_id].surface;
226 let search = |max_solutions: usize| {
235 curve_surface_intersect(&ray, surface, max_solutions, max_nodes, epsilon)
236 .map_err(|e| format!("its search against face {face_id} failed: {e}"))
237 };
238 let hits = match search(1) {
239 Ok(hits) if hits.is_coincident() => {
240 return Ok(Err(format!("it lies in face {face_id}'s surface")));
241 }
242 Ok(hits) if hits.len() == 1 => search(max_nodes),
243 other => other,
244 };
245 let hits = match hits {
246 Ok(hits) => hits,
247 Err(why) => return Ok(Err(why)),
248 };
249 for (t_hit, uv) in hits.into_vec() {
250 let (t_hit, uv) = if t_hit.definitely_greater(Scalar::ZERO) {
254 (t_hit, uv)
255 } else {
256 refine_crossing(&ray, surface, t_hit, uv)
257 };
258 let (u, v) = (uv[0].midpoint(), uv[1].midpoint());
262 let face_seed = seed ^ face_id.0;
263 if !t_hit.definitely_less(Scalar::ONE) {
269 return Ok(Err(format!(
270 "its hit on face {face_id} at t={t_hit:?} could be at its far end"
271 )));
272 }
273 match face_contains(model, face_id, u, v, max_nodes, epsilon, face_seed) {
274 Ok(FaceClassification::Inside) if t_hit.definitely_greater(Scalar::ZERO) => {
275 crossings.push((face_id, t_hit, uv))
276 }
277 Ok(FaceClassification::Inside) if leaving == Some(face_id) => {}
279 Ok(FaceClassification::Inside) => return Ok(Ok(Ray::OnFace)),
284 Ok(FaceClassification::Outside) => {}
286 other => {
290 return Ok(Err(format!(
291 "its hit on face {face_id} at uv=({u:?}, {v:?}) classified as {other:?}"
292 )));
293 }
294 }
295 }
296 }
297 Ok(Ok(Ray::Crossings(crossings)))
298}
299
300#[derive(Clone, Debug)]
302pub(crate) enum Ray {
303 OnFace,
305 Crossings(Vec<(FaceId, Scalar, Vector2)>),
308}
309
310impl Ray {
311 pub(crate) fn classification(&self) -> PointClassification {
322 match self {
323 Ray::OnFace => PointClassification::OnFace,
324 Ray::Crossings(crossings) if crossings.len() % 2 == 1 => PointClassification::Inside,
325 Ray::Crossings(_) => PointClassification::Outside,
326 }
327 }
328}
329
330pub fn shell_contains(
346 model: &Model,
347 shell_id: ShellId,
348 point: Vector3,
349 max_nodes: usize,
350 epsilon: Scalar,
351 seed: u64,
352) -> GeopResult<PointClassification> {
353 classify(model, shell_id, point, None, max_nodes, epsilon, seed)
354}
355
356#[allow(clippy::too_many_arguments)]
370pub fn shell_contains_from(
371 model: &Model,
372 shell_id: ShellId,
373 point: Vector3,
374 from: Vector3,
375 from_inside: bool,
376 max_nodes: usize,
377 epsilon: Scalar,
378 seed: u64,
379) -> GeopResult<PointClassification> {
380 classify(
381 model,
382 shell_id,
383 point,
384 Some((from, from_inside)),
385 max_nodes,
386 epsilon,
387 seed,
388 )
389}
390
391fn classify(
394 model: &Model,
395 shell_id: ShellId,
396 point: Vector3,
397 from: Option<(Vector3, bool)>,
398 max_nodes: usize,
399 epsilon: Scalar,
400 seed: u64,
401) -> GeopResult<PointClassification> {
402 let boundary = ShellBoundary::of(model, shell_id);
403 let (vertex_ids, edge_ids) = (&boundary.vertices, &boundary.edges);
404
405 for &vertex_id in vertex_ids {
409 if model.vertices[&vertex_id].point.could_be_equal(&point) {
410 return Ok(PointClassification::OnVertex);
411 }
412 }
413 for &edge_id in edge_ids {
414 if curve_could_contain(&model.edges[&edge_id].curve, &point, max_nodes, epsilon)?.is_some()
415 {
416 return Ok(PointClassification::OnEdge);
417 }
418 }
419 if shell_face_at(model, shell_id, &point, max_nodes, epsilon, seed)?.is_some() {
420 return Ok(PointClassification::OnFace);
421 }
422
423 let mut last_rejection = String::new();
424 if let Some((from, from_inside)) = from {
425 let inside = |other_side: bool| match other_side != from_inside {
428 true => PointClassification::Inside,
429 false => PointClassification::Outside,
430 };
431 if point.could_be_equal(&from) {
434 return Ok(inside(false));
435 }
436 let mut rng = Rng::new(seed);
444 let gap = from.sub(&point).norm().div(Scalar::TWO)?;
445 let via = point
446 .add(&from)
447 .prod_scalar(Scalar::ONE.div(Scalar::TWO)?)
448 .add(&rng.next_direction3().prod_scalar(gap))
449 .sharpen();
450 let leg = |start: Vector3| {
451 cast_ray(
452 &boundary,
453 start,
454 via.sub(&start),
455 Scalar::ONE,
456 None,
457 max_nodes,
458 epsilon,
459 seed,
460 )
461 };
462 match (
466 leg(point)?.map(|ray| ray.classification()),
467 leg(from)?.map(|ray| ray.classification()),
468 ) {
469 (
470 Ok(
471 on @ (PointClassification::OnFace
472 | PointClassification::OnEdge
473 | PointClassification::OnVertex),
474 ),
475 _,
476 ) => {
477 return Ok(on);
478 }
479 (
480 Ok(first),
481 Ok(second @ (PointClassification::Inside | PointClassification::Outside)),
482 ) => {
483 let odd = |c: PointClassification| c == PointClassification::Inside;
484 return Ok(inside(odd(first) != odd(second)));
485 }
486 (Err(reason), _) | (_, Err(reason)) => {
487 last_rejection = format!("the detour to {from:?} was rejected: {reason}");
488 }
489 (_, Ok(on)) => {
490 last_rejection =
491 format!("the detour to {from:?} found that point {on:?}, though classified");
492 }
493 }
494 }
495
496 let ray_length = boundary.ray_length(&point)?;
497
498 let mut rng = Rng::new(seed);
499 let found = attempts(MAX_RAY_ATTEMPTS, |&attempt| -> GeopResult<_> {
501 let direction = rng.next_direction3();
502 let attempt_seed = seed ^ (attempt as u64).wrapping_mul(0x9E3779B97F4A7C15);
503 match cast_ray(
504 &boundary,
505 point,
506 direction,
507 ray_length,
508 None,
509 max_nodes,
510 epsilon,
511 attempt_seed,
512 )? {
513 Ok(ray) => Ok(Probe::Found(ray.classification())),
514 Err(reason) => {
515 last_rejection = format!("the ray along {direction:?} was rejected: {reason}");
516 Ok(Probe::Next)
517 }
518 }
519 })?;
520 if let Some(classification) = found {
521 return Ok(classification);
522 }
523 Err(GeopError::new(format!(
524 "shell_contains: could not find a ray direction clear of every vertex and edge after many \
525 attempts; the last one was rejected because {last_rejection}"
526 )))
527}
528
529#[derive(Clone, Debug)]
532pub struct Beside {
533 pub inside: bool,
534 pub direction: Vector3,
535 pub crossings: Vec<(FaceId, Scalar, Vector2)>,
538}
539
540#[allow(clippy::too_many_arguments)]
556pub fn solid_contains_beside(
557 model: &Model,
558 solid_id: SolidId,
559 shell_id: ShellId,
560 face_id: FaceId,
561 point: Vector3,
562 normal: Vector3,
563 max_nodes: usize,
564 epsilon: Scalar,
565 seed: u64,
566) -> GeopResult<Beside> {
567 let boundaries: Vec<ShellBoundary> = model
568 .get_solid(solid_id)?
569 .shells
570 .iter()
571 .map(|&s| ShellBoundary::of(model, s))
572 .collect();
573 let mut rng = Rng::new(seed);
574 let mut last_rejection = String::new();
575 let found = attempts(MAX_RAY_ATTEMPTS, |&attempt| -> GeopResult<_> {
576 let drawn = rng.next_direction3();
577 let along = drawn.prod_dot(&normal);
578 let direction = if along.definitely_greater(Scalar::ZERO) {
581 drawn
582 } else if along.definitely_less(Scalar::ZERO) {
583 drawn.neg()
584 } else {
585 last_rejection = format!("the direction {drawn:?} could run along the face");
586 return Ok(Probe::Next);
587 };
588 let attempt_seed = seed ^ (attempt as u64).wrapping_mul(0x9E3779B97F4A7C15);
589 let mut crossings = Vec::new();
590 for boundary in &boundaries {
591 let leaving = (boundary.shell_id == shell_id).then_some(face_id);
592 match cast_ray(
593 boundary,
594 point,
595 direction,
596 boundary.ray_length(&point)?,
597 leaving,
598 max_nodes,
599 epsilon,
600 attempt_seed,
601 )? {
602 Ok(Ray::Crossings(crossed)) => crossings.extend(crossed),
603 Ok(Ray::OnFace) => {
604 last_rejection = format!(
605 "the ray along {direction:?} found the point on a face of shell {}",
606 boundary.shell_id
607 );
608 return Ok(Probe::Next);
609 }
610 Err(reason) => {
611 last_rejection = format!("the ray along {direction:?} was rejected: {reason}");
612 return Ok(Probe::Next);
613 }
614 }
615 }
616 Ok(Probe::Found(Beside {
619 inside: crossings.len() % 2 == 1,
620 direction,
621 crossings,
622 }))
623 })?;
624 found.ok_or_else(|| {
625 GeopError::new(format!(
626 "solid_contains_beside: no ray from {point:?} off face {face_id} of solid {solid_id} \
627 was clear of every vertex and edge after {MAX_RAY_ATTEMPTS} attempts; the last one \
628 was rejected because {last_rejection}"
629 ))
630 })
631}
632
633pub fn on_solid_boundary_besides(
640 model: &Model,
641 solid_id: SolidId,
642 face_id: FaceId,
643 point: &Vector3,
644 max_nodes: usize,
645 epsilon: Scalar,
646 seed: u64,
647) -> GeopResult<bool> {
648 for &shell_id in &model.get_solid(solid_id)?.shells {
649 let boundary = ShellBoundary::of(model, shell_id);
650 for &vertex_id in &boundary.vertices {
651 if model.get_vertex(vertex_id)?.point.could_be_equal(point) {
652 return Ok(true);
653 }
654 }
655 for &edge_id in &boundary.edges {
656 if curve_could_contain(&model.get_edge(edge_id)?.curve, point, max_nodes, epsilon)?
657 .is_some()
658 {
659 return Ok(true);
660 }
661 }
662 for &other in &model.get_shell(shell_id)?.faces {
663 if other == face_id {
664 continue;
665 }
666 let Some((u, v)) =
667 surface_could_contain(&model.get_face(other)?.surface, point, max_nodes, epsilon)?
668 else {
669 continue;
670 };
671 if !matches!(
672 face_contains(model, other, u, v, max_nodes, epsilon, seed ^ other.0)?,
673 FaceClassification::Outside
674 ) {
675 return Ok(true);
676 }
677 }
678 }
679 Ok(false)
680}
681
682pub fn shell_face_at(
691 model: &Model,
692 shell_id: ShellId,
693 point: &Vector3,
694 max_nodes: usize,
695 epsilon: Scalar,
696 seed: u64,
697) -> GeopResult<Option<(FaceId, Scalar, Scalar)>> {
698 for &face_id in &model.get_shell(shell_id)?.faces {
699 let Some((u, v)) =
700 surface_could_contain(&model.faces[&face_id].surface, point, max_nodes, epsilon)?
701 else {
702 continue;
703 };
704 if !matches!(
705 face_contains(model, face_id, u, v, max_nodes, epsilon, seed ^ face_id.0)?,
706 FaceClassification::Outside
707 ) {
708 return Ok(Some((face_id, u, v)));
709 }
710 }
711 Ok(None)
712}
713
714#[derive(Clone, Debug, Default)]
719pub struct Classified {
720 points: Vec<(ShellId, Vector3, bool)>,
721}
722
723impl Classified {
724 fn nearest(&self, shell: ShellId, point: &Vector3) -> Option<(Vector3, bool)> {
728 let distance = |p: &Vector3| p.sub(point).norm_sq().to_f64();
729 self.points
730 .iter()
731 .filter(|(s, _, _)| *s == shell)
732 .min_by(|a, b| distance(&a.1).total_cmp(&distance(&b.1)))
733 .map(|&(_, p, inside)| (p, inside))
734 }
735}
736
737pub fn solid_lumps(
746 model: &Model,
747 solid_id: SolidId,
748 max_nodes: usize,
749 epsilon: Scalar,
750 seed: u64,
751) -> GeopResult<Vec<(ShellId, Vec<ShellId>)>> {
752 let shells = model.get_solid(solid_id)?.shells.clone();
753 if shells.len() == 1 {
754 return Ok(vec![(shells[0], Vec::new())]);
755 }
756 let mut around: Vec<Vec<usize>> = vec![Vec::new(); shells.len()];
758 for (i, &shell) in shells.iter().enumerate() {
759 let vertices: Vec<VertexId> = model.iter_body_vertices(Body::Sheet(shell))?.collect();
760 for (j, &other) in shells.iter().enumerate() {
761 if i == j {
762 continue;
763 }
764 let mut decided = None;
765 for &vertex in &vertices {
766 let point = model.get_vertex(vertex)?.point;
767 match shell_contains(model, other, point, max_nodes, epsilon, seed)? {
768 PointClassification::Inside => decided = Some(true),
769 PointClassification::Outside => decided = Some(false),
770 _ => continue,
771 }
772 break;
773 }
774 match decided {
775 Some(true) => around[i].push(j),
776 Some(false) => {}
777 None => {
778 return Err(GeopError::new(format!(
779 "solid_lumps: every vertex of shell {shell} of solid {solid_id} lies on shell {other}: the two touch everywhere, and neither is inside the other"
780 )));
781 }
782 }
783 }
784 }
785 let depth: Vec<usize> = around.iter().map(Vec::len).collect();
786 let mut lumps: Vec<(ShellId, Vec<ShellId>)> = Vec::new();
787 let mut lump_of: Vec<Option<usize>> = vec![None; shells.len()];
788 for i in 0..shells.len() {
789 if depth[i].is_multiple_of(2) {
790 lump_of[i] = Some(lumps.len());
791 lumps.push((shells[i], Vec::new()));
792 }
793 }
794 for i in 0..shells.len() {
795 if !depth[i].is_multiple_of(2) {
796 let parent = around[i]
797 .iter()
798 .copied()
799 .find(|&j| depth[j] + 1 == depth[i])
800 .and_then(|j| lump_of[j])
801 .ok_or_else(|| {
802 GeopError::new(format!(
803 "solid_lumps: shell {} of solid {solid_id} is a void inside no lump",
804 shells[i]
805 ))
806 })?;
807 lumps[parent].1.push(shells[i]);
808 }
809 }
810 Ok(lumps)
811}
812
813pub fn solid_contains(
822 model: &Model,
823 solid_id: SolidId,
824 point: Vector3,
825 max_nodes: usize,
826 epsilon: Scalar,
827 seed: u64,
828 classified: &mut Classified,
829) -> GeopResult<PointClassification> {
830 let mut inside = false;
831 for &shell_id in &model.get_solid(solid_id)?.shells {
832 let from = classified.nearest(shell_id, &point);
833 let class = classify(model, shell_id, point, from, max_nodes, epsilon, seed)?;
834 match class {
835 PointClassification::Inside | PointClassification::Outside => {
836 let is_inside = class == PointClassification::Inside;
837 classified.points.push((shell_id, point, is_inside));
838 inside ^= is_inside;
839 }
840 on => return Ok(on),
841 }
842 }
843 Ok(if inside {
844 PointClassification::Inside
845 } else {
846 PointClassification::Outside
847 })
848}
849
850pub fn solid_face_at(
853 model: &Model,
854 solid_id: SolidId,
855 point: &Vector3,
856 max_nodes: usize,
857 epsilon: Scalar,
858 seed: u64,
859) -> GeopResult<Option<(FaceId, Scalar, Scalar)>> {
860 for &shell_id in &model.get_solid(solid_id)?.shells {
861 if let Some(at) = shell_face_at(model, shell_id, point, max_nodes, epsilon, seed)? {
862 return Ok(Some(at));
863 }
864 }
865 Ok(None)
866}
867
868#[cfg(test)]
869mod tests {
870 use super::{PointClassification, shell_contains, shell_contains_from};
871 use crate::{
872 Coedge, CoedgeGeometry, CoedgeId, Edge, EdgeId, Face, FaceId, Model, Sense, Shell, ShellId,
873 Vertex, VertexId, boundary::BoundaryType,
874 };
875 use geop_core_geometry::{
876 nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D},
877 nurb_surface::NurbSurface3D,
878 };
879 use geop_core_math::{
880 scalars::Scalar,
881 vector::{Vector3, Vector4},
882 };
883
884 const MAX: usize = 200;
885 const MIN_SUBDIVISION_SIZE: f64 = 1e-3;
886 const SEED: u64 = 424_242;
887
888 type P3 = (f64, f64, f64);
889
890 fn quad_face(model: &mut Model, shell: ShellId, p00: P3, p10: P3, p11: P3, p01: P3) -> FaceId {
895 let p4 = |p: P3| {
896 Vector4::from_array([
897 Scalar::from_f64(p.0),
898 Scalar::from_f64(p.1),
899 Scalar::from_f64(p.2),
900 Scalar::ONE,
901 ])
902 };
903 let surface = NurbSurface3D::try_new(
904 1,
905 1,
906 vec![p4(p00), p4(p01), p4(p10), p4(p11)],
907 vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
908 vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
909 )
910 .unwrap();
911 let face_id = model.insert_face(Face {
912 surface,
913 outer: BoundaryType::Vertex(crate::VertexId(0)),
914 holes: Vec::new(),
915 shell,
916 });
917
918 let corners = [p00, p10, p11, p01];
919 let verts: Vec<VertexId> = corners
920 .iter()
921 .map(|&(x, y, z)| {
922 model.insert_vertex(Vertex {
923 point: Vector3::from_array([
924 Scalar::from_f64(x),
925 Scalar::from_f64(y),
926 Scalar::from_f64(z),
927 ]),
928 })
929 })
930 .collect();
931 let edges: Vec<EdgeId> = (0..4)
932 .map(|i| {
933 model.insert_edge(Edge {
934 curve: NurbCurve3D::try_new(
935 1,
936 vec![p4(corners[i]), p4(corners[(i + 1) % 4])],
937 vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
938 )
939 .unwrap(),
940 start_vertex: verts[i],
941 end_vertex: verts[(i + 1) % 4],
942 })
943 })
944 .collect();
945 let uv = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
946 let pc = |a: (f64, f64), b: (f64, f64)| -> NurbCurve2D {
947 NurbCurve::try_new(
948 1,
949 vec![
950 Vector3::from_array([
951 Scalar::from_f64(a.0),
952 Scalar::from_f64(a.1),
953 Scalar::ONE,
954 ]),
955 Vector3::from_array([
956 Scalar::from_f64(b.0),
957 Scalar::from_f64(b.1),
958 Scalar::ONE,
959 ]),
960 ],
961 vec![Scalar::ZERO, Scalar::ZERO, Scalar::ONE, Scalar::ONE],
962 )
963 .unwrap()
964 };
965 let coedges: Vec<CoedgeId> = (0..4)
966 .map(|i| {
967 model.insert_coedge(Coedge {
968 geometry: CoedgeGeometry::Edge(edges[i]),
969 sense: Sense::Forward,
970 pcurve: pc(uv[i], uv[(i + 1) % 4]),
971 next: CoedgeId(0),
972 prev: CoedgeId(0),
973 face: face_id,
974 })
975 })
976 .collect();
977 for i in 0..4 {
978 model.coedges.get_mut(&coedges[i]).unwrap().next = coedges[(i + 1) % 4];
979 model.coedges.get_mut(&coedges[i]).unwrap().prev = coedges[(i + 3) % 4];
980 }
981 model.faces.get_mut(&face_id).unwrap().outer = BoundaryType::Loop(coedges[0]);
982
983 face_id
984 }
985
986 fn unit_cube(model: &mut Model) -> ShellId {
990 let shell_id = model.insert_shell(Shell {
991 faces: vec![],
992 solid: None,
993 });
994 let faces = vec![
995 quad_face(
996 model,
997 shell_id,
998 (0., 0., 1.),
999 (1., 0., 1.),
1000 (1., 1., 1.),
1001 (0., 1., 1.),
1002 ), quad_face(
1004 model,
1005 shell_id,
1006 (0., 0., 0.),
1007 (0., 1., 0.),
1008 (1., 1., 0.),
1009 (1., 0., 0.),
1010 ), quad_face(
1012 model,
1013 shell_id,
1014 (1., 0., 0.),
1015 (1., 1., 0.),
1016 (1., 1., 1.),
1017 (1., 0., 1.),
1018 ), quad_face(
1020 model,
1021 shell_id,
1022 (0., 0., 0.),
1023 (0., 0., 1.),
1024 (0., 1., 1.),
1025 (0., 1., 0.),
1026 ), quad_face(
1028 model,
1029 shell_id,
1030 (0., 1., 0.),
1031 (1., 1., 0.),
1032 (1., 1., 1.),
1033 (0., 1., 1.),
1034 ), quad_face(
1036 model,
1037 shell_id,
1038 (0., 0., 0.),
1039 (1., 0., 0.),
1040 (1., 0., 1.),
1041 (0., 0., 1.),
1042 ), ];
1044 model.shells.get_mut(&shell_id).unwrap().faces = faces;
1045 shell_id
1046 }
1047
1048 #[test]
1049 fn center_is_inside() {
1050 let mut model = Model::new();
1051 let shell_id = unit_cube(&mut model);
1052 let p = Vector3::from_array([Scalar::from_f64(0.5); 3]);
1053 assert_eq!(
1054 shell_contains(
1055 &model,
1056 shell_id,
1057 p,
1058 MAX,
1059 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
1060 SEED
1061 )
1062 .unwrap(),
1063 PointClassification::Inside
1064 );
1065 }
1066
1067 #[test]
1068 fn far_point_is_outside() {
1069 let mut model = Model::new();
1070 let shell_id = unit_cube(&mut model);
1071 let p = Vector3::from_array([
1072 Scalar::from_f64(-5.0),
1073 Scalar::from_f64(0.5),
1074 Scalar::from_f64(0.5),
1075 ]);
1076 assert_eq!(
1077 shell_contains(
1078 &model,
1079 shell_id,
1080 p,
1081 MAX,
1082 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
1083 SEED
1084 )
1085 .unwrap(),
1086 PointClassification::Outside
1087 );
1088 }
1089
1090 #[test]
1091 fn point_just_outside_face_is_outside() {
1092 let mut model = Model::new();
1093 let shell_id = unit_cube(&mut model);
1094 let p = Vector3::from_array([
1095 Scalar::from_f64(-0.1),
1096 Scalar::from_f64(0.5),
1097 Scalar::from_f64(0.5),
1098 ]);
1099 assert_eq!(
1100 shell_contains(
1101 &model,
1102 shell_id,
1103 p,
1104 MAX,
1105 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
1106 SEED
1107 )
1108 .unwrap(),
1109 PointClassification::Outside
1110 );
1111 }
1112
1113 #[test]
1114 fn face_point_is_on_face() {
1115 let mut model = Model::new();
1116 let shell_id = unit_cube(&mut model);
1117 let p = Vector3::from_array([Scalar::ZERO, Scalar::from_f64(0.5), Scalar::from_f64(0.5)]);
1118 assert_eq!(
1119 shell_contains(
1120 &model,
1121 shell_id,
1122 p,
1123 MAX,
1124 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
1125 SEED
1126 )
1127 .unwrap(),
1128 PointClassification::OnFace
1129 );
1130 }
1131
1132 #[test]
1133 fn edge_point_is_on_edge() {
1134 let mut model = Model::new();
1135 let shell_id = unit_cube(&mut model);
1136 let p = Vector3::from_array([Scalar::ZERO, Scalar::ZERO, Scalar::from_f64(0.5)]);
1137 assert_eq!(
1138 shell_contains(
1139 &model,
1140 shell_id,
1141 p,
1142 MAX,
1143 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
1144 SEED
1145 )
1146 .unwrap(),
1147 PointClassification::OnEdge
1148 );
1149 }
1150
1151 #[test]
1152 fn vertex_point_is_on_vertex() {
1153 let mut model = Model::new();
1154 let shell_id = unit_cube(&mut model);
1155 let p = Vector3::from_array([Scalar::ZERO, Scalar::ZERO, Scalar::ZERO]);
1156 assert_eq!(
1157 shell_contains(
1158 &model,
1159 shell_id,
1160 p,
1161 MAX,
1162 Scalar::from_f64(MIN_SUBDIVISION_SIZE),
1163 SEED
1164 )
1165 .unwrap(),
1166 PointClassification::OnVertex
1167 );
1168 }
1169
1170 #[test]
1176 fn classified_from_a_known_point() {
1177 let mut model = Model::new();
1178 let shell_id = unit_cube(&mut model);
1179 let p = |x: f64, y: f64, z: f64| Vector3::from_array([x, y, z].map(Scalar::from_f64));
1180 let points = [
1181 p(0.5, 0.5, 0.5),
1182 p(0.25, 0.5, 0.5),
1183 p(-0.5, 0.5, 0.5),
1184 p(1.5, 0.5, 0.5),
1185 p(-0.5, -0.5, 0.5),
1186 p(0.5, 0.5, 2.0),
1187 p(-0.5, 0.0, 0.0),
1188 p(0.0, -0.5, 0.0),
1189 p(0.0, 0.0, 0.0),
1190 p(0.0, 0.5, 0.5),
1191 ];
1192 let eps = Scalar::from_f64(MIN_SUBDIVISION_SIZE);
1193 for from in &points[..6] {
1194 let from_class = shell_contains(&model, shell_id, *from, MAX, eps, SEED).unwrap();
1195 let from_inside = from_class == PointClassification::Inside;
1196 for (k, point) in points.iter().enumerate() {
1197 let want = shell_contains(&model, shell_id, *point, MAX, eps, SEED).unwrap();
1198 let got = shell_contains_from(
1199 &model,
1200 shell_id,
1201 *point,
1202 *from,
1203 from_inside,
1204 MAX,
1205 eps,
1206 SEED ^ k as u64,
1207 )
1208 .unwrap();
1209 assert_eq!(got, want, "{point:?} from {from:?}");
1210 }
1211 }
1212 }
1213}