Skip to main content

parry3d/shape/
segment_pseudo_normals.rs

1use crate::math::Vector;
2
3#[cfg(feature = "alloc")]
4use crate::query::details::NormalConstraints;
5
6/// The pseudo-normals of a polyline segment, approximating the outward normal cones of its
7/// features.
8///
9/// This is the 2D segment analog of [`TrianglePseudoNormals`](crate::shape::TrianglePseudoNormals):
10/// `face` is the segment's outward normal and `edges` are the outward pseudo-normals at its two
11/// endpoints. An oriented polyline uses them to clamp contact normals to one side, so it acts as a
12/// one-sided surface.
13#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
14#[derive(Clone, Debug)]
15pub struct SegmentPseudoNormals {
16    /// The segment's outward normal.
17    pub face: Vector,
18    /// The outward pseudo-normals at the segment's two endpoints.
19    pub edges: [Vector; 2],
20}
21
22#[cfg(feature = "alloc")]
23impl NormalConstraints for SegmentPseudoNormals {
24    /// Projects `dir` so it lies within the outward cone defined by `self`.
25    fn project_local_normal_mut(&self, dir: &mut Vector) -> bool {
26        // Find the closest pseudo-normal.
27        let closest_edge = if dir.dot(self.edges[0]) >= dir.dot(self.edges[1]) {
28            self.edges[0]
29        } else {
30            self.edges[1]
31        };
32        crate::shape::pseudo_normals::project_into_cone(self.face, closest_edge, dir)
33    }
34}
35
36#[cfg(test)]
37#[cfg(all(feature = "dim2", feature = "alloc"))]
38mod test {
39    use super::{NormalConstraints, SegmentPseudoNormals};
40    use crate::math::Vector;
41
42    fn bisector(v1: Vector, v2: Vector) -> Vector {
43        (v1 + v2).normalize()
44    }
45
46    #[test]
47    fn degenerate_cone_collapses_to_face() {
48        let pn = SegmentPseudoNormals {
49            face: Vector::Y,
50            edges: [Vector::Y, Vector::Y],
51        };
52
53        assert_eq!(
54            pn.project_local_normal(Vector::new(1.0, 1.0)),
55            Some(Vector::Y)
56        );
57        assert!(pn.project_local_normal(-Vector::Y).is_none());
58    }
59
60    #[test]
61    fn clamps_into_the_outward_cone() {
62        // A cone centered on +Y, bounded at +-45 degrees (endpoint pseudo-normals at +-22.5 degrees).
63        let ends = [
64            bisector(Vector::Y, Vector::X),
65            bisector(Vector::Y, -Vector::X),
66        ];
67        let edges = [bisector(Vector::Y, ends[0]), bisector(Vector::Y, ends[1])];
68        let pn = SegmentPseudoNormals {
69            face: Vector::Y,
70            edges,
71        };
72
73        // Inside the cone: returned unchanged.
74        assert_eq!(pn.project_local_normal(Vector::Y), Some(Vector::Y));
75        for edge in edges {
76            assert_eq!(pn.project_local_normal(edge), Some(edge));
77        }
78        let inside = Vector::new(0.2, 1.0).normalize();
79        assert!(pn
80            .project_local_normal(inside)
81            .unwrap()
82            .abs_diff_eq(inside, 1.0e-5));
83
84        // Outside the cone but still outward: pulled onto the boundary, not left crossing the surface.
85        let sideways = Vector::new(1.0, 0.2).normalize();
86        let clamped = pn.project_local_normal(sideways).unwrap();
87        assert!(clamped.dot(Vector::Y) > sideways.dot(Vector::Y));
88        assert!(clamped.abs_diff_eq(ends[0], 1.0e-5));
89
90        // Into the solid (-face half-space): rejected.
91        assert!(pn.project_local_normal(-Vector::Y).is_none());
92        assert!(pn
93            .project_local_normal(Vector::new(0.3, -1.0).normalize())
94            .is_none());
95    }
96}