parry3d/shape/
triangle_pseudo_normals.rs1use crate::math::Vector;
2
3#[cfg(feature = "alloc")]
4use crate::{math::Vector3, query::details::NormalConstraints};
5
6#[derive(Clone, Debug)]
19pub struct TrianglePseudoNormals {
20 pub face: Vector,
22 pub edges: [Vector; 3],
28}
29
30#[cfg(feature = "alloc")]
31impl NormalConstraints for TrianglePseudoNormals {
32 fn project_local_normal_mut(&self, dir: &mut Vector) -> bool {
35 let dot_face = dir.dot(self.face);
36
37 let dots = Vector3::new(
39 dir.dot(self.edges[0]),
40 dir.dot(self.edges[1]),
41 dir.dot(self.edges[2]),
42 );
43 let closest_dot = dots.max_position();
44 let closest_edge = self.edges[closest_dot];
45
46 if closest_edge == self.face {
52 *dir = self.face;
54 return dot_face >= 0.0;
55 }
56
57 let dot_edge_face = self.face.dot(closest_edge);
60 let dot_dir_face = self.face.dot(*dir);
61 let dot_corrected_dir_face = 2.0 * dot_edge_face * dot_edge_face - 1.0; if dot_dir_face >= dot_corrected_dir_face {
64 return true;
66 }
67
68 let edge_on_normal = self.face * dot_edge_face;
70 let edge_orthogonal_to_normal = closest_edge - edge_on_normal;
71
72 let dir_on_normal = self.face * dot_dir_face;
73 let dir_orthogonal_to_normal = *dir - dir_on_normal;
74 let Some(unit_dir_orthogonal_to_normal) = dir_orthogonal_to_normal.try_normalize() else {
75 return dot_face >= 0.0;
76 };
77
78 let adjusted_pseudo_normal =
81 edge_on_normal + unit_dir_orthogonal_to_normal * edge_orthogonal_to_normal.length();
82 let (adjusted_pseudo_normal, length) = adjusted_pseudo_normal.normalize_and_length();
83 if length <= 1.0e-6 {
84 return dot_face >= 0.0;
85 };
86
87 *dir = adjusted_pseudo_normal * (2.0 * self.face.dot(adjusted_pseudo_normal)) - self.face;
90 dot_face >= 0.0
91 }
92}
93
94#[cfg(test)]
95#[cfg(all(feature = "dim3", feature = "alloc"))]
96mod test {
97 use super::NormalConstraints;
98 use crate::math::{Real, Vector};
99 use crate::shape::TrianglePseudoNormals;
100
101 fn bisector(v1: Vector, v2: Vector) -> Vector {
102 (v1 + v2).normalize()
103 }
104
105 fn bisector_y(v: Vector) -> Vector {
106 bisector(v, Vector::Y)
107 }
108
109 #[test]
110 fn trivial_pseudo_normals_projection() {
111 let pn = TrianglePseudoNormals {
112 face: Vector::Y,
113 edges: [Vector::Y; 3],
114 };
115
116 assert_eq!(
117 pn.project_local_normal(Vector::new(1.0, 1.0, 1.0)),
118 Some(Vector::Y)
119 );
120 assert!(pn.project_local_normal(-Vector::Y).is_none());
121 }
122
123 #[test]
124 fn edge_pseudo_normals_projection_strictly_positive() {
125 let bisector = |v1: Vector, v2: Vector| (v1 + v2).normalize();
126 let bisector_y = |v: Vector| bisector(v, Vector::Y);
127
128 let cones_ref_dir = [
130 -Vector::Z,
131 -Vector::X,
132 Vector::new(1.0, 0.0, 1.0).normalize(),
133 ];
134 let cones_ends = cones_ref_dir.map(bisector_y);
135 let cones_axes = cones_ends.map(bisector_y);
136
137 let pn = TrianglePseudoNormals {
138 face: Vector::Y,
139 edges: cones_axes.map(|v| v.normalize()),
140 };
141
142 for i in 0..3 {
143 assert!(pn
144 .project_local_normal(cones_ends[i])
145 .unwrap()
146 .abs_diff_eq(cones_ends[i], 1.0e-5));
147 assert_eq!(pn.project_local_normal(cones_axes[i]), Some(cones_axes[i]));
148
149 let subdivs = 100;
151
152 for k in 1..100 {
153 let v = Vector::Y
154 .lerp(cones_ends[i], k as Real / (subdivs as Real))
155 .normalize();
156 assert_eq!(pn.project_local_normal(v).unwrap(), v);
157 }
158
159 for k in 1..subdivs {
161 let v = cones_ref_dir[i]
162 .lerp(cones_ends[i], k as Real / (subdivs as Real))
163 .normalize();
164 assert!(pn
165 .project_local_normal(v)
166 .unwrap()
167 .abs_diff_eq(cones_ends[i], 1.0e-5));
168 }
169
170 for k in 1..subdivs {
172 let v = cones_ref_dir[i]
173 .lerp(-Vector::Y, k as Real / (subdivs as Real))
174 .normalize();
175 assert!(pn.project_local_normal(v).is_none(),);
176 }
177 }
178 }
179
180 #[test]
181 fn edge_pseudo_normals_projection_negative() {
182 let cones_ref_dir = [
184 -Vector::Z,
185 -Vector::X,
186 Vector::new(1.0, 0.0, 1.0).normalize(),
187 ];
188 let cones_ends = cones_ref_dir.map(|v| bisector(v, -Vector::Y));
189 let cones_axes = [
190 bisector(bisector_y(cones_ref_dir[0]), cones_ref_dir[0]),
191 bisector(bisector_y(cones_ref_dir[1]), cones_ref_dir[1]),
192 bisector(bisector_y(cones_ref_dir[2]), cones_ref_dir[2]),
193 ];
194
195 let pn = TrianglePseudoNormals {
196 face: Vector::Y,
197 edges: cones_axes.map(|v| v.normalize()),
198 };
199
200 for i in 0..3 {
201 assert_eq!(pn.project_local_normal(cones_axes[i]), Some(cones_axes[i]));
202
203 let subdivs = 100;
205
206 for k in 1..subdivs {
207 let v = Vector::Y
208 .lerp(cones_ends[i], k as Real / (subdivs as Real))
209 .normalize();
210 assert_eq!(pn.project_local_normal(v).unwrap(), v);
211 }
212
213 for k in 1..subdivs {
216 let v = (-Vector::Y)
217 .lerp(cones_ends[i], k as Real / (subdivs as Real))
218 .normalize();
219 assert!(pn.project_local_normal(v).is_none());
220 }
221 }
222 }
223}