1use std::collections::HashSet;
16
17use crate::{CoedgeGeometry, EdgeId, Model, ShellId, VertexId};
18use geop_core_geometry::{
19 contains::{curve::curve_could_contain, surface::surface_could_contain},
20 intersection::{curve_curve_intersect, curve_surface_intersect},
21 nurb_curve::NurbCurve3D,
22};
23use geop_core_math::{
24 geop_error::{GeopError, GeopResult},
25 scalars::Scalar,
26 vector::{Vector3, Vector4},
27};
28
29use super::{
30 face::{PointClassification as FaceClassification, face_contains},
31 rng::Rng,
32};
33
34#[derive(Clone, Copy, Debug, PartialEq, Eq)]
36pub enum PointClassification {
37 OnVertex,
39 OnEdge,
41 OnFace,
43 Inside,
45 Outside,
47}
48
49const MAX_RAY_ATTEMPTS: usize = 64;
50
51fn line3<S: Scalar>(a: Vector3<S>, b: Vector3<S>) -> GeopResult<NurbCurve3D<S>> {
52 NurbCurve3D::try_new(
53 1,
54 vec![
55 Vector4::from_array([a[0], a[1], a[2], S::ONE]),
56 Vector4::from_array([b[0], b[1], b[2], S::ONE]),
57 ],
58 vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
59 )
60}
61
62pub(crate) fn shell_vertices_and_edges<S: Scalar>(
64 model: &Model<S>,
65 shell_id: ShellId,
66) -> (Vec<VertexId>, Vec<EdgeId>) {
67 let shell = &model.shells[&shell_id];
68 let mut vertex_ids = Vec::new();
69 let mut edge_ids = Vec::new();
70 let mut seen_v = HashSet::new();
71 let mut seen_e = HashSet::new();
72 for &face_id in &shell.faces {
73 for coedge_id in model.iterate_face_coedges(face_id) {
74 let coedge = &model.coedges[&coedge_id];
75 match coedge.geometry {
76 CoedgeGeometry::Edge(edge_id) => {
77 if seen_e.insert(edge_id) {
78 edge_ids.push(edge_id);
79 }
80 let edge = &model.edges[&edge_id];
81 if seen_v.insert(edge.start_vertex) {
82 vertex_ids.push(edge.start_vertex);
83 }
84 if seen_v.insert(edge.end_vertex) {
85 vertex_ids.push(edge.end_vertex);
86 }
87 }
88 CoedgeGeometry::Vertex(vertex_id) => {
89 if seen_v.insert(vertex_id) {
90 vertex_ids.push(vertex_id);
91 }
92 }
93 }
94 }
95 }
96 (vertex_ids, edge_ids)
97}
98
99pub(crate) fn ray_length_for<S: Scalar>(
104 model: &Model<S>,
105 vertex_ids: &[VertexId],
106 point: &Vector3<S>,
107) -> GeopResult<S> {
108 let mut max_dist_sq = S::ONE;
109 for &vertex_id in vertex_ids {
110 let d = model.vertices[&vertex_id].point.sub(point).norm_sq();
111 if d.could_be_greater(max_dist_sq) {
112 max_dist_sq = d;
113 }
114 }
115 Ok(max_dist_sq.sqrt()?.mul(S::from_f64(3.0)).add(S::ONE))
116}
117
118pub(crate) fn cast_ray<S: Scalar>(
133 model: &Model<S>,
134 shell_id: ShellId,
135 point: Vector3<S>,
136 direction: Vector3<S>,
137 vertex_ids: &[VertexId],
138 edge_ids: &[EdgeId],
139 ray_length: S,
140 max_nodes: usize,
141 epsilon: S,
142 seed: u64,
143) -> GeopResult<Result<PointClassification, String>> {
144 let shell = &model.shells[&shell_id];
145 let t_epsilon = epsilon.div(ray_length)?;
146 let far = point.add(&direction.prod_scalar(ray_length));
147 let ray = line3(point, far)?;
148
149 for &vertex_id in vertex_ids {
151 let vp = model.vertices[&vertex_id].point;
152 if curve_could_contain(&ray, &vp, max_nodes, epsilon)?.is_some() {
153 return Ok(Err(format!(
154 "it could pass through vertex {vertex_id} at {vp:?}"
155 )));
156 }
157 }
158
159 for &edge_id in edge_ids {
165 let full_curve = &model.edges[&edge_id].curve;
166 let hits = match curve_curve_intersect(&ray, full_curve, 1, max_nodes, epsilon) {
167 Ok(hits) => hits,
168 Err(e) => {
169 return Ok(Err(format!(
170 "its search against edge {edge_id} failed: {e}"
171 )));
172 }
173 };
174 if hits.is_coincident() {
175 return Ok(Err(format!("it runs along edge {edge_id}")));
176 }
177 for (t_hit, _mid) in hits.into_vec() {
178 if t_hit.definitely_greater(t_epsilon) {
179 return Ok(Err(format!("it could cross edge {edge_id} at t={t_hit:?}")));
180 }
181 }
182 }
183
184 let mut count = 0usize;
185 for &face_id in &shell.faces {
186 let surface = &model.faces[&face_id].surface;
187 let hits = match curve_surface_intersect(&ray, surface, max_nodes, max_nodes, epsilon) {
192 Ok(hits) => hits,
193 Err(e) => {
194 return Ok(Err(format!(
195 "its search against face {face_id} failed: {e}"
196 )));
197 }
198 };
199 if hits.is_coincident() {
200 return Ok(Err(format!("it lies in face {face_id}'s surface")));
201 }
202 for (t_hit, uv) in hits.into_vec() {
203 if !t_hit.definitely_greater(t_epsilon) {
204 continue;
205 }
206 let (u, v) = (uv[0].midpoint(), uv[1].midpoint());
210 let face_seed = seed ^ face_id.0;
211 match face_contains(model, face_id, u, v, max_nodes, epsilon, face_seed) {
212 Ok(FaceClassification::Inside) => count += 1,
213 Ok(FaceClassification::Outside) => {}
214 other => {
218 return Ok(Err(format!(
219 "its hit on face {face_id} at uv=({u:?}, {v:?}) classified as {other:?}"
220 )));
221 }
222 }
223 }
224 }
225 Ok(Ok(if count % 2 == 1 {
233 PointClassification::Inside
234 } else {
235 PointClassification::Outside
236 }))
237}
238
239pub fn shell_contains<S: Scalar>(
255 model: &Model<S>,
256 shell_id: ShellId,
257 point: Vector3<S>,
258 max_nodes: usize,
259 epsilon: S,
260 seed: u64,
261) -> GeopResult<PointClassification> {
262 let shell = &model.shells[&shell_id];
263 let (vertex_ids, edge_ids) = shell_vertices_and_edges(model, shell_id);
264
265 for &vertex_id in &vertex_ids {
269 if model.vertices[&vertex_id].point.could_be_equal(&point) {
270 return Ok(PointClassification::OnVertex);
271 }
272 }
273 for &edge_id in &edge_ids {
274 if curve_could_contain(&model.edges[&edge_id].curve, &point, max_nodes, epsilon)?.is_some()
275 {
276 return Ok(PointClassification::OnEdge);
277 }
278 }
279 for &face_id in &shell.faces {
280 let Some((u, v)) =
290 surface_could_contain(&model.faces[&face_id].surface, &point, max_nodes, epsilon)?
291 else {
292 continue;
293 };
294 if !matches!(
295 face_contains(model, face_id, u, v, max_nodes, epsilon, seed ^ face_id.0)?,
296 FaceClassification::Outside
297 ) {
298 return Ok(PointClassification::OnFace);
299 }
300 }
301
302 let ray_length = ray_length_for(model, &vertex_ids, &point)?;
303
304 let mut rng = Rng::new(seed);
305 let mut last_rejection = String::new();
306 for attempt in 0..MAX_RAY_ATTEMPTS {
307 let direction = rng.next_direction3::<S>();
308 let attempt_seed = seed ^ (attempt as u64).wrapping_mul(0x9E3779B97F4A7C15);
309 match cast_ray(
310 model,
311 shell_id,
312 point,
313 direction,
314 &vertex_ids,
315 &edge_ids,
316 ray_length,
317 max_nodes,
318 epsilon,
319 attempt_seed,
320 )? {
321 Ok(classification) => return Ok(classification),
322 Err(reason) => {
323 last_rejection = format!("the ray along {direction:?} was rejected: {reason}")
324 }
325 }
326 }
327 Err(GeopError::new(format!(
328 "shell_contains: could not find a ray direction clear of every vertex and edge after many \
329 attempts; the last one was rejected because {last_rejection}"
330 )))
331}
332
333#[cfg(test)]
334mod tests {
335 use super::{PointClassification, shell_contains};
336 use crate::{
337 Coedge, CoedgeGeometry, CoedgeId, Edge, EdgeId, Face, FaceId, Model, Sense, Shell, ShellId,
338 SolidId, Vertex, VertexId, boundary::BoundaryType,
339 };
340 use geop_core_geometry::{
341 nurb_curve::{NurbCurve, NurbCurve2D, NurbCurve3D},
342 nurb_surface::NurbSurface3D,
343 };
344 use geop_core_math::{
345 for_all_scalars,
346 scalars::Scalar,
347 vector::{Vector3, Vector4},
348 };
349
350 const MAX: usize = 200;
351 const EPS: f64 = 1e-3;
352 const SEED: u64 = 424_242;
353
354 type P3 = (f64, f64, f64);
355
356 fn quad_face<S: Scalar>(
361 model: &mut Model<S>,
362 shell: ShellId,
363 p00: P3,
364 p10: P3,
365 p11: P3,
366 p01: P3,
367 ) -> FaceId {
368 let p4 = |p: P3| {
369 Vector4::from_array([S::from_f64(p.0), S::from_f64(p.1), S::from_f64(p.2), S::ONE])
370 };
371 let surface = NurbSurface3D::try_new(
372 1,
373 1,
374 vec![p4(p00), p4(p01), p4(p10), p4(p11)],
375 vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
376 vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
377 )
378 .unwrap();
379 let face_id = model.insert_face(Face {
380 surface,
381 outer: BoundaryType::Vertex(crate::VertexId(0)),
382 holes: Vec::new(),
383 shell,
384 });
385
386 let corners = [p00, p10, p11, p01];
387 let verts: Vec<VertexId> = corners
388 .iter()
389 .map(|&(x, y, z)| {
390 model.insert_vertex(Vertex {
391 point: Vector3::from_array([S::from_f64(x), S::from_f64(y), S::from_f64(z)]),
392 })
393 })
394 .collect();
395 let edges: Vec<EdgeId> = (0..4)
396 .map(|i| {
397 model.insert_edge(Edge {
398 curve: NurbCurve3D::try_new(
399 1,
400 vec![p4(corners[i]), p4(corners[(i + 1) % 4])],
401 vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
402 )
403 .unwrap(),
404 start_vertex: verts[i],
405 end_vertex: verts[(i + 1) % 4],
406 })
407 })
408 .collect();
409 let uv = [(0.0, 0.0), (1.0, 0.0), (1.0, 1.0), (0.0, 1.0)];
410 let pc = |a: (f64, f64), b: (f64, f64)| -> NurbCurve2D<S> {
411 NurbCurve::try_new(
412 1,
413 vec![
414 Vector3::from_array([S::from_f64(a.0), S::from_f64(a.1), S::ONE]),
415 Vector3::from_array([S::from_f64(b.0), S::from_f64(b.1), S::ONE]),
416 ],
417 vec![S::ZERO, S::ZERO, S::ONE, S::ONE],
418 )
419 .unwrap()
420 };
421 let coedges: Vec<CoedgeId> = (0..4)
422 .map(|i| {
423 model.insert_coedge(Coedge {
424 geometry: CoedgeGeometry::Edge(edges[i]),
425 sense: Sense::Forward,
426 pcurve: pc(uv[i], uv[(i + 1) % 4]),
427 next: CoedgeId(0),
428 prev: CoedgeId(0),
429 face: face_id,
430 })
431 })
432 .collect();
433 for i in 0..4 {
434 model.coedges.get_mut(&coedges[i]).unwrap().next = coedges[(i + 1) % 4];
435 model.coedges.get_mut(&coedges[i]).unwrap().prev = coedges[(i + 3) % 4];
436 }
437 model.faces.get_mut(&face_id).unwrap().outer = BoundaryType::Loop(coedges[0]);
438
439 face_id
440 }
441
442 fn unit_cube<S: Scalar>(model: &mut Model<S>) -> ShellId {
446 let shell_id = model.insert_shell(Shell {
447 faces: vec![],
448 solid: SolidId(999),
449 });
450 let faces = vec![
451 quad_face(
452 model,
453 shell_id,
454 (0., 0., 1.),
455 (1., 0., 1.),
456 (1., 1., 1.),
457 (0., 1., 1.),
458 ), quad_face(
460 model,
461 shell_id,
462 (0., 0., 0.),
463 (0., 1., 0.),
464 (1., 1., 0.),
465 (1., 0., 0.),
466 ), quad_face(
468 model,
469 shell_id,
470 (1., 0., 0.),
471 (1., 1., 0.),
472 (1., 1., 1.),
473 (1., 0., 1.),
474 ), quad_face(
476 model,
477 shell_id,
478 (0., 0., 0.),
479 (0., 0., 1.),
480 (0., 1., 1.),
481 (0., 1., 0.),
482 ), quad_face(
484 model,
485 shell_id,
486 (0., 1., 0.),
487 (1., 1., 0.),
488 (1., 1., 1.),
489 (0., 1., 1.),
490 ), quad_face(
492 model,
493 shell_id,
494 (0., 0., 0.),
495 (1., 0., 0.),
496 (1., 0., 1.),
497 (0., 0., 1.),
498 ), ];
500 model.shells.get_mut(&shell_id).unwrap().faces = faces;
501 shell_id
502 }
503
504 fn check_center_is_inside<S: Scalar>() {
505 let mut model = Model::<S>::new();
506 let shell_id = unit_cube(&mut model);
507 let p = Vector3::from_array([S::from_f64(0.5); 3]);
508 assert_eq!(
509 shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
510 PointClassification::Inside
511 );
512 }
513 #[test]
514 fn center_is_inside() {
515 for_all_scalars!(check_center_is_inside);
516 }
517
518 fn check_far_point_is_outside<S: Scalar>() {
519 let mut model = Model::<S>::new();
520 let shell_id = unit_cube(&mut model);
521 let p = Vector3::from_array([S::from_f64(-5.0), S::from_f64(0.5), S::from_f64(0.5)]);
522 assert_eq!(
523 shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
524 PointClassification::Outside
525 );
526 }
527 #[test]
528 fn far_point_is_outside() {
529 for_all_scalars!(check_far_point_is_outside);
530 }
531
532 fn check_point_just_outside_face_is_outside<S: Scalar>() {
533 let mut model = Model::<S>::new();
534 let shell_id = unit_cube(&mut model);
535 let p = Vector3::from_array([S::from_f64(-0.1), S::from_f64(0.5), S::from_f64(0.5)]);
536 assert_eq!(
537 shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
538 PointClassification::Outside
539 );
540 }
541 #[test]
542 fn point_just_outside_face_is_outside() {
543 for_all_scalars!(check_point_just_outside_face_is_outside);
544 }
545
546 fn check_face_point_is_on_face<S: Scalar>() {
547 let mut model = Model::<S>::new();
548 let shell_id = unit_cube(&mut model);
549 let p = Vector3::from_array([S::ZERO, S::from_f64(0.5), S::from_f64(0.5)]);
550 assert_eq!(
551 shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
552 PointClassification::OnFace
553 );
554 }
555 #[test]
556 fn face_point_is_on_face() {
557 for_all_scalars!(check_face_point_is_on_face);
558 }
559
560 fn check_edge_point_is_on_edge<S: Scalar>() {
561 let mut model = Model::<S>::new();
562 let shell_id = unit_cube(&mut model);
563 let p = Vector3::from_array([S::ZERO, S::ZERO, S::from_f64(0.5)]);
564 assert_eq!(
565 shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
566 PointClassification::OnEdge
567 );
568 }
569 #[test]
570 fn edge_point_is_on_edge() {
571 for_all_scalars!(check_edge_point_is_on_edge);
572 }
573
574 fn check_vertex_point_is_on_vertex<S: Scalar>() {
575 let mut model = Model::<S>::new();
576 let shell_id = unit_cube(&mut model);
577 let p = Vector3::from_array([S::ZERO, S::ZERO, S::ZERO]);
578 assert_eq!(
579 shell_contains(&model, shell_id, p, MAX, S::from_f64(EPS), SEED).unwrap(),
580 PointClassification::OnVertex
581 );
582 }
583 #[test]
584 fn vertex_point_is_on_vertex() {
585 for_all_scalars!(check_vertex_point_is_on_vertex);
586 }
587}