parry3d/shape/
compound_pseudo_normals.rs1use crate::math::{Real, Vector};
2
3#[cfg(feature = "alloc")]
4use crate::query::details::NormalConstraints;
5#[cfg(feature = "alloc")]
6use alloc::vec::Vec;
7
8#[cfg(all(feature = "alloc", feature = "dim2"))]
10pub(crate) fn turns_clockwise(from: Vector, to: Vector) -> bool {
11 from.x * to.y - from.y * to.x < 0.0
12}
13
14#[cfg(all(feature = "alloc", feature = "dim2"))]
21#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
22#[derive(Clone, Copy, Debug)]
23pub struct CompoundEdgeCone {
24 pub face: Vector,
26 pub clockwise_limit: Vector,
28 pub counter_clockwise_limit: Vector,
30}
31
32#[cfg(all(feature = "alloc", feature = "dim2"))]
33impl CompoundEdgeCone {
34 fn contains(&self, dir: Vector) -> bool {
35 dir.dot(self.face) > 0.0
36 && !turns_clockwise(self.clockwise_limit, dir)
37 && !turns_clockwise(dir, self.counter_clockwise_limit)
38 }
39
40 fn clamped(&self, dir: Vector) -> Vector {
42 if self.contains(dir) {
43 dir
44 } else if dir.dot(self.clockwise_limit) >= dir.dot(self.counter_clockwise_limit) {
45 self.clockwise_limit
46 } else {
47 self.counter_clockwise_limit
48 }
49 }
50}
51
52#[cfg(all(feature = "alloc", feature = "dim2"))]
64#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
65#[derive(Clone, Debug, Default)]
66pub struct CompoundPseudoNormals {
67 pub boundary_edges: Vec<CompoundEdgeCone>,
69}
70
71#[cfg(all(feature = "alloc", feature = "dim2"))]
72impl NormalConstraints for CompoundPseudoNormals {
73 fn project_local_normal_mut(&self, dir: &mut Vector) -> bool {
78 let mut nearest: Option<(Vector, Real)> = None;
79
80 for edge in &self.boundary_edges {
81 let candidate = edge.clamped(*dir);
82 let alignment = dir.dot(candidate);
83
84 match nearest {
85 Some((_, best)) if alignment <= best => {}
86 _ => nearest = Some((candidate, alignment)),
87 }
88 }
89
90 match nearest {
91 Some((direction, alignment)) if alignment > 0.0 => {
92 *dir = direction;
93 true
94 }
95 _ => false,
96 }
97 }
98}
99
100#[cfg(all(feature = "alloc", feature = "dim3"))]
108#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
109#[derive(Clone, Debug)]
110pub struct CompoundFaceCone {
111 pub face: Vector,
113 pub edge_pseudo_normals: Vec<Vector>,
115}
116
117#[cfg(all(feature = "alloc", feature = "dim3"))]
123#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
124#[derive(Clone, Debug, Default)]
125pub struct CompoundPseudoNormals {
126 pub boundary_faces: Vec<CompoundFaceCone>,
128}
129
130#[cfg(all(feature = "alloc", feature = "dim3"))]
131impl NormalConstraints for CompoundPseudoNormals {
132 fn project_local_normal_mut(&self, dir: &mut Vector) -> bool {
139 let mut best: Option<(&CompoundFaceCone, Real)> = None;
140
141 for cone in &self.boundary_faces {
142 let alignment = dir.dot(cone.face);
143 match best {
144 Some((_, best_alignment)) if alignment <= best_alignment => {}
145 _ => best = Some((cone, alignment)),
146 }
147 }
148
149 let Some((cone, _)) = best else {
150 return false;
151 };
152
153 let closest_edge = cone
154 .edge_pseudo_normals
155 .iter()
156 .copied()
157 .max_by(|a, b| dir.dot(*a).total_cmp(&dir.dot(*b)))
158 .unwrap_or(cone.face);
159 crate::shape::pseudo_normals::project_into_cone(cone.face, closest_edge, dir)
160 }
161}
162
163#[cfg(test)]
164#[cfg(all(feature = "dim2", feature = "alloc"))]
165mod test {
166 use super::{CompoundEdgeCone, CompoundPseudoNormals, NormalConstraints};
167 use crate::math::Vector;
168 use alloc::vec;
169
170 fn cone(face: Vector, clockwise: Vector, counter_clockwise: Vector) -> CompoundEdgeCone {
171 CompoundEdgeCone {
172 face,
173 clockwise_limit: clockwise,
174 counter_clockwise_limit: counter_clockwise,
175 }
176 }
177
178 fn pinned(face: Vector) -> CompoundEdgeCone {
180 cone(face, face, face)
181 }
182
183 #[test]
184 fn buried_part_rejects_everything() {
185 let normals = CompoundPseudoNormals::default();
186
187 assert!(normals.project_local_normal(Vector::Y).is_none());
188 assert!(normals.project_local_normal(-Vector::Y).is_none());
189 }
190
191 #[test]
192 fn a_direction_already_on_the_outline_is_left_alone() {
193 let normals = CompoundPseudoNormals {
194 boundary_edges: vec![cone(
195 Vector::Y,
196 Vector::new(1.0, 1.0).normalize(),
197 Vector::new(-1.0, 1.0).normalize(),
198 )],
199 };
200
201 assert_eq!(normals.project_local_normal(Vector::Y), Some(Vector::Y));
202
203 let inside_the_cone = Vector::new(0.3, 1.0).normalize();
204 assert_eq!(
205 normals.project_local_normal(inside_the_cone),
206 Some(inside_the_cone)
207 );
208 }
209
210 #[test]
211 fn a_direction_past_the_corner_is_pulled_back_to_it() {
212 let corner = Vector::new(1.0, 1.0).normalize();
213 let normals = CompoundPseudoNormals {
214 boundary_edges: vec![cone(Vector::Y, corner, Vector::new(-1.0, 1.0).normalize())],
215 };
216
217 assert_eq!(normals.project_local_normal(Vector::X), Some(corner));
218 }
219
220 #[test]
221 fn picks_the_edge_the_normal_points_out_of() {
222 let normals = CompoundPseudoNormals {
223 boundary_edges: vec![pinned(Vector::Y), pinned(Vector::X)],
224 };
225
226 assert_eq!(normals.project_local_normal(Vector::Y), Some(Vector::Y));
227 assert_eq!(normals.project_local_normal(Vector::X), Some(Vector::X));
228 }
229
230 #[test]
234 fn a_direction_reaching_the_part_from_behind_is_dropped() {
235 let normals = CompoundPseudoNormals {
236 boundary_edges: vec![pinned(Vector::Y)],
237 };
238
239 assert!(normals.project_local_normal(-Vector::Y).is_none());
240 }
241
242 #[test]
243 fn clamps_a_wedge_tip_normal_onto_the_boundary_edge() {
244 let ramp = Vector::new(-0.6, 0.8).normalize();
245 let normals = CompoundPseudoNormals {
246 boundary_edges: vec![pinned(ramp)],
247 };
248
249 let clamped = normals.project_local_normal(-Vector::X).unwrap();
250
251 assert!((clamped - ramp).length() < 1.0e-5);
252 }
253}