Skip to main content

parry3d/query/point/
point_composite_shape.rs

1#![allow(unused_parens)] // Needed by the macro.
2
3use crate::math::{Real, Vector};
4use crate::partitioning::BvhNode;
5use crate::query::{PointProjection, PointQuery, PointQueryWithLocation};
6use crate::shape::{
7    CompositeShapeRef, FeatureId, SegmentPointLocation, SubShapeId, TriMesh, TrianglePointLocation,
8    TypedCompositeShape,
9};
10
11use crate::shape::{Compound, Polyline};
12
13/// The feature of a triangle a point projected onto, as `Triangle`'s own point query reports it.
14fn triangle_point_location_feature(location: TrianglePointLocation) -> FeatureId {
15    match location {
16        TrianglePointLocation::OnVertex(i) => FeatureId::Vertex(i),
17        #[cfg(feature = "dim3")]
18        TrianglePointLocation::OnEdge(i, _) => FeatureId::Edge(i),
19        #[cfg(feature = "dim2")]
20        TrianglePointLocation::OnEdge(i, _) => FeatureId::Face(i),
21        TrianglePointLocation::OnFace(i, _) => FeatureId::Face(i),
22        TrianglePointLocation::OnSolid => FeatureId::Face(0),
23    }
24}
25
26impl<S: TypedCompositeShape> CompositeShapeRef<'_, S> {
27    /// Project a point on this composite shape.
28    ///
29    /// Returns the index of the sub-shape of `self` that answered, the projection as that
30    /// sub-shape reported it, and some shape-specific information about the projected point.
31    #[inline]
32    pub fn project_local_point_and_get_location(
33        &self,
34        point: Vector,
35        max_dist: Real,
36        solid: bool,
37    ) -> Option<(
38        SubShapeId,
39        PointProjection,
40        <S::PartShape as PointQueryWithLocation>::Location,
41    )>
42    where
43        S::PartShape: PointQueryWithLocation,
44    {
45        self.0
46            .bvh()
47            .find_best(
48                max_dist,
49                |node: &BvhNode, _best_so_far| node.aabb().distance_to_local_point(point, true),
50                |primitive, _best_so_far| {
51                    let proj = self.0.map_typed_part_at(primitive, |pose, shape, _| {
52                        if let Some(pose) = pose {
53                            shape.project_point_and_get_location(pose, point, solid)
54                        } else {
55                            shape.project_local_point_and_get_location(point, solid)
56                        }
57                    })?;
58                    let cost = (proj.0.point - point).length();
59                    Some((cost, proj))
60                },
61            )
62            .map(|(best_id, (_, (proj, location)))| (best_id, proj, location))
63    }
64
65    /// Project a point on this composite shape.
66    ///
67    /// Returns the index of the sub-shape of `self` that answered and the projection as that
68    /// sub-shape reported it. If `solid` is `false` then the point will be projected to the
69    /// closest boundary of `self` even if it is contained by one of its sub-shapes.
70    pub fn project_local_point(
71        &self,
72        point: Vector,
73        max_dist: Real,
74        solid: bool,
75    ) -> Option<(SubShapeId, PointProjection)> {
76        let (best_id, (_, proj)) = self.0.bvh().find_best(
77            max_dist,
78            |node: &BvhNode, _best_so_far| node.aabb().distance_to_local_point(point, true),
79            |primitive, _best_so_far| {
80                let proj = self.0.map_typed_part_at(primitive, |pose, shape, _| {
81                    if let Some(pose) = pose {
82                        shape.project_point(pose, point, solid)
83                    } else {
84                        shape.project_local_point(point, solid)
85                    }
86                })?;
87                let dist = (proj.point - point).length();
88                Some((dist, proj))
89            },
90        )?;
91        Some((best_id, proj))
92    }
93
94    /// Project a point on this composite shape.
95    ///
96    /// Returns the index of the sub-shape of `self` that answered, the projection as that
97    /// sub-shape reported it, and the feature of that sub-shape the projection landed on.
98    #[inline]
99    pub fn project_local_point_and_get_feature(
100        &self,
101        point: Vector,
102        max_dist: Real,
103    ) -> Option<(SubShapeId, PointProjection, FeatureId)> {
104        let (best_id, (_, (proj, feature_id))) = self.0.bvh().find_best(
105            max_dist,
106            |node: &BvhNode, _best_so_far| node.aabb().distance_to_local_point(point, true),
107            |primitive, _best_so_far| {
108                let proj = self.0.map_typed_part_at(primitive, |pose, shape, _| {
109                    if let Some(pose) = pose {
110                        shape.project_point_and_get_feature(pose, point)
111                    } else {
112                        shape.project_local_point_and_get_feature(point)
113                    }
114                })?;
115                let cost = (proj.0.point - point).length();
116                Some((cost, proj))
117            },
118        )?;
119        Some((best_id, proj, feature_id))
120    }
121
122    // TODO: implement distance_to_point too?
123
124    /// Returns the index of any sub-shape of `self` that contains the given point.
125    #[inline]
126    pub fn contains_local_point(&self, point: Vector) -> Option<SubShapeId> {
127        self.0
128            .bvh()
129            .leaves(|node: &BvhNode| node.aabb().contains_local_point(point))
130            .find(|leaf_id| {
131                self.0
132                    .map_typed_part_at(*leaf_id, |pose, shape, _| {
133                        if let Some(pose) = pose {
134                            shape.contains_point(pose, point)
135                        } else {
136                            shape.contains_local_point(point)
137                        }
138                    })
139                    .unwrap_or(false)
140            })
141    }
142}
143
144impl PointQuery for Polyline {
145    #[inline]
146    fn project_local_point(&self, point: Vector, solid: bool) -> PointProjection {
147        self.project_local_point_and_get_location(point, solid).0
148    }
149
150    #[inline]
151    #[allow(unused_mut)] // Because we need mut in 2D but not in 3D.
152    fn project_local_point_and_get_feature(&self, point: Vector) -> (PointProjection, FeatureId) {
153        // Every comparison involving a NaN is false, so the traversal finds no candidate
154        // at all when `point` (or `self`) isn’t finite. Report `point` itself rather than
155        // an arbitrary projection onto whichever part we happened to pick.
156        let Some((segment_id, mut proj, feature)) =
157            CompositeShapeRef(self).project_local_point_and_get_feature(point, Real::MAX)
158        else {
159            return (PointProjection::new(false, point), FeatureId::Unknown);
160        };
161        proj.subshape = segment_id;
162
163        // A point behind the outward pseudo-normal is inside.
164        #[cfg(feature = "dim2")]
165        if let Some(constraints) = self.segment_normal_constraints(segment_id) {
166            let pseudo_normal = match feature {
167                FeatureId::Vertex(i) => constraints.edges[i as usize],
168                _ => constraints.face,
169            };
170            proj.is_inside = (point - proj.point).dot(pseudo_normal) <= 0.0;
171        }
172
173        // The feature is the segment's own; `proj.subshape` says which segment it belongs to.
174        (proj, feature)
175    }
176
177    // TODO: implement distance_to_point too?
178
179    #[inline]
180    fn contains_local_point(&self, point: Vector) -> bool {
181        // An oriented polyline has a solid interior; reuse the projection's inside test.
182        #[cfg(feature = "dim2")]
183        if self.flags().contains(crate::shape::PolylineFlags::ORIENTED) {
184            return self
185                .project_local_point_and_get_location(point, true)
186                .0
187                .is_inside;
188        }
189
190        CompositeShapeRef(self)
191            .contains_local_point(point)
192            .is_some()
193    }
194}
195
196impl PointQuery for TriMesh {
197    #[inline]
198    fn project_local_point(&self, point: Vector, solid: bool) -> PointProjection {
199        self.project_local_point_with_max_dist(point, solid, Real::MAX)
200            // Shouldn’t happen (trimesh must not be empty). But return something
201            // instead of crashing with `unwrap`.
202            .unwrap_or(PointProjection::new(false, point))
203    }
204
205    #[inline]
206    fn project_local_point_and_get_feature(&self, point: Vector) -> (PointProjection, FeatureId) {
207        #[cfg(feature = "dim3")]
208        if self.pseudo_normals().is_some() {
209            // If we can, in 3D, take the pseudo-normals into account. The location carries the
210            // triangle's own feature; `proj.subshape` says which triangle it belongs to.
211            let (proj, (_, location)) = self.project_local_point_and_get_location(point, false);
212            return (proj, triangle_point_location_feature(location));
213        }
214
215        let solid = cfg!(feature = "dim2");
216        // No candidate: `point` (or `self`) isn’t finite. See
217        // `Polyline::project_local_point_and_get_feature`.
218        let Some((triangle_id, proj, location)) =
219            CompositeShapeRef(self).project_local_point_and_get_location(point, Real::MAX, solid)
220        else {
221            return (PointProjection::new(false, point), FeatureId::Unknown);
222        };
223        // The feature is the triangle's own; `proj.subshape` says which triangle it belongs to.
224        (
225            proj.with_subshape(triangle_id),
226            triangle_point_location_feature(location),
227        )
228    }
229
230    // TODO: implement distance_to_point too?
231
232    #[inline]
233    fn contains_local_point(&self, point: Vector) -> bool {
234        #[cfg(feature = "dim3")]
235        if self.pseudo_normals.is_some() {
236            // If we can, in 3D, take the pseudo-normals into account.
237            return self
238                .project_local_point_and_get_location(point, true)
239                .0
240                .is_inside;
241        }
242
243        CompositeShapeRef(self)
244            .contains_local_point(point)
245            .is_some()
246    }
247
248    /// Projects a point on `self` transformed by `m`, unless the projection lies further than the given max distance.
249    fn project_local_point_with_max_dist(
250        &self,
251        pt: Vector,
252        solid: bool,
253        max_dist: Real,
254    ) -> Option<PointProjection> {
255        self.project_local_point_and_get_location_with_max_dist(pt, solid, max_dist)
256            .map(|proj| proj.0)
257    }
258}
259
260impl PointQuery for Compound {
261    #[inline]
262    fn project_local_point(&self, point: Vector, solid: bool) -> PointProjection {
263        CompositeShapeRef(self)
264            .project_local_point(point, Real::MAX, solid)
265            .map(|(part_id, proj)| proj.with_subshape(part_id))
266            // No candidate: `point` (or `self`) isn’t finite. See
267            // `Polyline::project_local_point_and_get_feature`.
268            .unwrap_or(PointProjection::new(false, point))
269    }
270
271    #[inline]
272    fn project_local_point_and_get_feature(&self, point: Vector) -> (PointProjection, FeatureId) {
273        // The feature is the part's own; `proj.subshape` says which part it belongs to.
274        CompositeShapeRef(self)
275            .project_local_point_and_get_feature(point, Real::MAX)
276            .map(|(part_id, proj, feature)| (proj.with_subshape(part_id), feature))
277            // No candidate: `point` (or `self`) isn’t finite. See
278            // `Polyline::project_local_point_and_get_feature`.
279            .unwrap_or((PointProjection::new(false, point), FeatureId::Unknown))
280    }
281
282    #[inline]
283    fn contains_local_point(&self, point: Vector) -> bool {
284        CompositeShapeRef(self)
285            .contains_local_point(point)
286            .is_some()
287    }
288}
289
290impl PointQueryWithLocation for Polyline {
291    type Location = (u32, SegmentPointLocation);
292
293    #[inline]
294    fn project_local_point_and_get_location(
295        &self,
296        point: Vector,
297        solid: bool,
298    ) -> (PointProjection, Self::Location) {
299        self.project_local_point_and_get_location_with_max_dist(point, solid, Real::MAX)
300            // No candidate: `point` (or `self`) isn’t finite. See
301            // `Polyline::project_local_point_and_get_feature`.
302            .unwrap_or((
303                PointProjection::new(false, point),
304                (0, SegmentPointLocation::OnVertex(0)),
305            ))
306    }
307
308    /// Projects a point on `self`, with a maximum projection distance.
309    fn project_local_point_and_get_location_with_max_dist(
310        &self,
311        point: Vector,
312        solid: bool,
313        max_dist: Real,
314    ) -> Option<(PointProjection, Self::Location)> {
315        #[allow(unused_mut)] // Because we need mut in 2D but not in 3D.
316        if let Some((seg_id, mut proj, loc)) =
317            CompositeShapeRef(self).project_local_point_and_get_location(point, max_dist, solid)
318        {
319            proj.subshape = seg_id;
320
321            // A point behind the outward pseudo-normal is inside.
322            #[cfg(feature = "dim2")]
323            if let Some(constraints) = self.segment_normal_constraints(seg_id) {
324                let pseudo_normal = match loc {
325                    SegmentPointLocation::OnVertex(i) => constraints.edges[i as usize],
326                    SegmentPointLocation::OnEdge(_) => constraints.face,
327                };
328                proj.is_inside = (point - proj.point).dot(pseudo_normal) <= 0.0;
329
330                if proj.is_inside && solid {
331                    proj.point = point;
332                }
333            }
334
335            Some((proj, (seg_id, loc)))
336        } else {
337            None
338        }
339    }
340}
341
342impl PointQueryWithLocation for TriMesh {
343    type Location = (u32, TrianglePointLocation);
344
345    #[inline]
346    #[allow(unused_mut)] // Because we need mut in 3D but not in 2D.
347    fn project_local_point_and_get_location(
348        &self,
349        point: Vector,
350        solid: bool,
351    ) -> (PointProjection, Self::Location) {
352        self.project_local_point_and_get_location_with_max_dist(point, solid, Real::MAX)
353            // No candidate: `point` (or `self`) isn’t finite. See
354            // `Polyline::project_local_point_and_get_feature`.
355            .unwrap_or((
356                PointProjection::new(false, point),
357                (0, TrianglePointLocation::OnVertex(0)),
358            ))
359    }
360
361    /// Projects a point on `self`, with a maximum projection distance.
362    fn project_local_point_and_get_location_with_max_dist(
363        &self,
364        point: Vector,
365        solid: bool,
366        max_dist: Real,
367    ) -> Option<(PointProjection, Self::Location)> {
368        #[allow(unused_mut)] // mut is needed in 3D.
369        if let Some((part_id, mut proj, location)) =
370            CompositeShapeRef(self).project_local_point_and_get_location(point, max_dist, solid)
371        {
372            proj.subshape = part_id;
373
374            #[cfg(feature = "dim3")]
375            if let Some(pseudo_normals) = self.pseudo_normals_if_oriented() {
376                let pseudo_normal = match location {
377                    TrianglePointLocation::OnFace(..) | TrianglePointLocation::OnSolid => {
378                        Some(self.triangle(part_id).scaled_normal())
379                    }
380                    TrianglePointLocation::OnEdge(i, _) => pseudo_normals
381                        .edges_pseudo_normal
382                        .get(part_id as usize)
383                        .map(|pn| pn[i as usize]),
384                    TrianglePointLocation::OnVertex(i) => {
385                        let idx = self.indices()[part_id as usize];
386                        pseudo_normals
387                            .vertices_pseudo_normal
388                            .get(idx[i as usize] as usize)
389                            .copied()
390                    }
391                };
392
393                if let Some(pseudo_normal) = pseudo_normal {
394                    let dpt = point - proj.point;
395                    proj.is_inside = dpt.dot(pseudo_normal) <= 0.0;
396                }
397            }
398
399            Some((proj, (part_id, location)))
400        } else {
401            None
402        }
403    }
404}