oxiphysics_collision/narrowphase/types.rs
1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5pub use super::specialized::*;
6
7use super::functions::{add3, cross3, dot3, len3, normalize3, scale3, shape_shape_contact, sub3};
8
9/// Result of a ray cast against a shape.
10#[derive(Debug, Clone)]
11pub struct RayCastResult {
12 /// Whether the ray hit the shape.
13 pub hit: bool,
14 /// Time of intersection (units of ray direction magnitude).
15 pub toi: f64,
16 /// Hit point in world space.
17 pub hit_point: [f64; 3],
18 /// Outward surface normal at the hit point.
19 pub normal: [f64; 3],
20}
21/// A unified contact result produced by the routing layer.
22#[derive(Debug, Clone)]
23pub struct NarrowPhaseContact {
24 /// Contact normal pointing from shape A toward shape B (unit length).
25 pub normal: [f64; 3],
26 /// Penetration depth (positive = overlapping).
27 pub depth: f64,
28 /// World-space witness point on shape A's surface.
29 pub point_a: [f64; 3],
30 /// World-space witness point on shape B's surface.
31 pub point_b: [f64; 3],
32}
33impl NarrowPhaseContact {
34 /// Return a copy with the normal flipped and witness points swapped.
35 pub fn flipped(&self) -> Self {
36 Self {
37 normal: scale3(self.normal, -1.0),
38 depth: self.depth,
39 point_a: self.point_b,
40 point_b: self.point_a,
41 }
42 }
43 /// Midpoint between the two witness points.
44 pub fn midpoint(&self) -> [f64; 3] {
45 scale3(add3(self.point_a, self.point_b), 0.5)
46 }
47}
48/// A compound shape: a collection of child shapes with local offsets.
49#[derive(Debug, Clone)]
50pub struct CompoundShape {
51 /// Child shapes in world space (pre-transformed by the caller).
52 pub children: Vec<ShapeKind>,
53}
54impl CompoundShape {
55 /// Create a compound shape from world-space children.
56 pub fn new(children: Vec<ShapeKind>) -> Self {
57 CompoundShape { children }
58 }
59 /// Compute the AABB enclosing all children.
60 pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
61 let mut mn = [f64::INFINITY; 3];
62 let mut mx = [f64::NEG_INFINITY; 3];
63 for child in &self.children {
64 let (cmin, cmax) = child.aabb();
65 for i in 0..3 {
66 mn[i] = mn[i].min(cmin[i]);
67 mx[i] = mx[i].max(cmax[i]);
68 }
69 }
70 (mn, mx)
71 }
72}
73/// Geometric feature that produced a contact (for incremental warm-starting).
74#[derive(Debug, Clone, PartialEq, Eq)]
75pub enum ContactFeature {
76 /// Contact between two faces.
77 FaceFace {
78 /// Index of face A.
79 face_a: u32,
80 /// Index of face B.
81 face_b: u32,
82 },
83 /// Contact between a face and an edge.
84 FaceEdge {
85 /// Index of the face.
86 face: u32,
87 /// Index of the edge.
88 edge: u32,
89 },
90 /// Contact between two edges.
91 EdgeEdge {
92 /// Index of edge A.
93 edge_a: u32,
94 /// Index of edge B.
95 edge_b: u32,
96 },
97 /// Contact between a vertex and a face.
98 VertexFace {
99 /// Index of the vertex.
100 vertex: u32,
101 /// Index of the face.
102 face: u32,
103 },
104 /// Unknown / unclassified feature (e.g. from GJK fallback).
105 Unknown,
106}
107/// Post-processing options applied to a [`NarrowPhaseContact`] after it is
108/// produced by the routing layer.
109#[derive(Debug, Clone)]
110pub struct ContactFilter {
111 /// Minimum depth to report a contact. Contacts shallower than this are
112 /// discarded (default: 0.0).
113 pub min_depth: f64,
114 /// Maximum depth that will be reported; deeper contacts are clamped.
115 /// Set to `f64::INFINITY` to disable clamping.
116 pub max_depth: f64,
117 /// If `true`, negate the contact normal before returning it.
118 pub flip_normal: bool,
119}
120impl ContactFilter {
121 /// Apply this filter to a contact result.
122 ///
123 /// Returns `None` if the contact should be discarded.
124 pub fn apply(&self, mut c: NarrowPhaseContact) -> Option<NarrowPhaseContact> {
125 if c.depth < self.min_depth {
126 return None;
127 }
128 if c.depth > self.max_depth {
129 c.depth = self.max_depth;
130 }
131 if self.flip_normal {
132 c.normal = scale3(c.normal, -1.0);
133 }
134 Some(c)
135 }
136}
137/// A narrowphase contact enriched with feature information.
138#[derive(Debug, Clone)]
139pub struct FeatureContact {
140 /// Underlying geometry contact.
141 pub contact: NarrowPhaseContact,
142 /// The geometric feature responsible for this contact.
143 pub feature: ContactFeature,
144}
145impl FeatureContact {
146 /// Wrap a plain contact with an `Unknown` feature tag.
147 pub fn from_plain(c: NarrowPhaseContact) -> Self {
148 FeatureContact {
149 contact: c,
150 feature: ContactFeature::Unknown,
151 }
152 }
153}
154/// Processes multiple broadphase pairs in a single call.
155///
156/// Pairs are described as indices into a shared slice of [`ShapeKind`] values.
157/// Results are returned in the same order as the input pairs; pairs that did
158/// not produce a contact get `None`.
159#[derive(Default)]
160pub struct BatchNarrowPhase {
161 /// Post-processing filter applied to every contact.
162 pub filter: ContactFilter,
163}
164impl BatchNarrowPhase {
165 /// Create a `BatchNarrowPhase` with default settings.
166 pub fn new() -> Self {
167 Self::default()
168 }
169 /// Run the narrow phase for all `pairs`.
170 ///
171 /// Returns one `Option<NarrowPhaseContact>` per input pair.
172 pub fn run(
173 &self,
174 shapes: &[ShapeKind],
175 pairs: &[(usize, usize)],
176 ) -> Vec<Option<NarrowPhaseContact>> {
177 pairs
178 .iter()
179 .map(|(i, j)| {
180 if *i >= shapes.len() || *j >= shapes.len() {
181 return None;
182 }
183 let contact = shape_shape_contact(&shapes[*i], &shapes[*j])?;
184 self.filter.apply(contact)
185 })
186 .collect()
187 }
188 /// Run and collect only pairs that produced a contact.
189 pub fn run_compact(
190 &self,
191 shapes: &[ShapeKind],
192 pairs: &[(usize, usize)],
193 ) -> Vec<((usize, usize), NarrowPhaseContact)> {
194 pairs
195 .iter()
196 .filter_map(|(i, j)| {
197 if *i >= shapes.len() || *j >= shapes.len() {
198 return None;
199 }
200 let contact = shape_shape_contact(&shapes[*i], &shapes[*j])?;
201 let filtered = self.filter.apply(contact)?;
202 Some(((*i, *j), filtered))
203 })
204 .collect()
205 }
206}
207/// Result of a point-in-shape query.
208#[derive(Debug, Clone)]
209pub struct PointQueryResult {
210 /// Whether the point is inside the shape.
211 pub is_inside: bool,
212 /// Closest point on the shape's surface to the query point.
213 pub closest_surface_point: [f64; 3],
214 /// Signed distance from the query point to the surface (negative = inside).
215 pub signed_distance: f64,
216 /// Outward surface normal at the closest point.
217 pub normal: [f64; 3],
218}
219/// A triangle mesh (concave shape) represented as a flat list of triangles.
220///
221/// Each triangle is three consecutive vertices: `[v0, v1, v2]`.
222#[derive(Debug, Clone)]
223pub struct TriangleMesh {
224 /// Flat list of triangle vertices; length must be a multiple of 3.
225 pub triangles: Vec<[f64; 3]>,
226}
227impl TriangleMesh {
228 /// Create a new triangle mesh.
229 pub fn new(triangles: Vec<[f64; 3]>) -> Self {
230 debug_assert!(
231 triangles.len().is_multiple_of(3),
232 "triangle count must be a multiple of 3"
233 );
234 TriangleMesh { triangles }
235 }
236 /// Number of triangles.
237 pub fn tri_count(&self) -> usize {
238 self.triangles.len() / 3
239 }
240 /// Get the three vertices of triangle `i`.
241 pub fn triangle(&self, i: usize) -> [[f64; 3]; 3] {
242 let base = i * 3;
243 [
244 self.triangles[base],
245 self.triangles[base + 1],
246 self.triangles[base + 2],
247 ]
248 }
249 /// Compute the face normal for triangle `i` (not normalized).
250 pub fn face_normal(&self, i: usize) -> [f64; 3] {
251 let [v0, v1, v2] = self.triangle(i);
252 let e0 = sub3(v1, v0);
253 let e1 = sub3(v2, v0);
254 cross3(e0, e1)
255 }
256}
257/// Plain-data descriptor for a convex shape, used by the routing layer.
258///
259/// All geometry is expressed in world space so no transform arithmetic is
260/// needed by the dispatcher.
261#[derive(Debug, Clone)]
262pub enum ShapeKind {
263 /// A sphere with centre and radius.
264 Sphere {
265 /// World-space centre.
266 center: [f64; 3],
267 /// Radius.
268 radius: f64,
269 },
270 /// An axis-aligned box with centre and half-extents.
271 Box {
272 /// World-space centre.
273 center: [f64; 3],
274 /// Half-extents along X, Y, Z.
275 half_extents: [f64; 3],
276 },
277 /// A capsule defined by two endpoint centres and a radius.
278 Capsule {
279 /// First endpoint centre.
280 p0: [f64; 3],
281 /// Second endpoint centre.
282 p1: [f64; 3],
283 /// Radius.
284 radius: f64,
285 },
286 /// An infinite plane through the origin with a given normal.
287 Plane {
288 /// Unit normal.
289 normal: [f64; 3],
290 /// Distance from origin along the normal (plane equation: n·x = d).
291 offset: f64,
292 },
293 /// A convex polyhedron described by its support vertices.
294 Convex {
295 /// Vertices in world space.
296 vertices: Vec<[f64; 3]>,
297 },
298}
299impl ShapeKind {
300 /// Returns an axis-aligned bounding box `(min, max)` for this shape.
301 pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
302 match self {
303 ShapeKind::Sphere { center, radius } => (
304 [center[0] - radius, center[1] - radius, center[2] - radius],
305 [center[0] + radius, center[1] + radius, center[2] + radius],
306 ),
307 ShapeKind::Box {
308 center,
309 half_extents,
310 } => (sub3(*center, *half_extents), add3(*center, *half_extents)),
311 ShapeKind::Capsule { p0, p1, radius } => {
312 let mn = [
313 p0[0].min(p1[0]) - radius,
314 p0[1].min(p1[1]) - radius,
315 p0[2].min(p1[2]) - radius,
316 ];
317 let mx = [
318 p0[0].max(p1[0]) + radius,
319 p0[1].max(p1[1]) + radius,
320 p0[2].max(p1[2]) + radius,
321 ];
322 (mn, mx)
323 }
324 ShapeKind::Plane { normal, offset } => {
325 let n = *normal;
326 let d = *offset;
327 let _ = (n, d);
328 ([-1e15; 3], [1e15; 3])
329 }
330 ShapeKind::Convex { vertices } => {
331 let mut mn = [f64::INFINITY; 3];
332 let mut mx = [f64::NEG_INFINITY; 3];
333 for v in vertices {
334 for i in 0..3 {
335 mn[i] = mn[i].min(v[i]);
336 mx[i] = mx[i].max(v[i]);
337 }
338 }
339 (mn, mx)
340 }
341 }
342 }
343 /// The largest sphere that bounds this shape (bounding sphere).
344 pub fn bounding_radius(&self) -> f64 {
345 match self {
346 ShapeKind::Sphere { radius, .. } => *radius,
347 ShapeKind::Box { half_extents, .. } => len3(*half_extents),
348 ShapeKind::Capsule { p0, p1, radius } => len3(sub3(*p1, *p0)) * 0.5 + radius,
349 ShapeKind::Plane { .. } => f64::INFINITY,
350 ShapeKind::Convex { vertices } => {
351 vertices.iter().map(|v| len3(*v)).fold(0.0_f64, f64::max)
352 }
353 }
354 }
355 /// Support function: point on the shape furthest in direction `dir`.
356 pub fn support(&self, dir: [f64; 3]) -> [f64; 3] {
357 match self {
358 ShapeKind::Sphere { center, radius } => {
359 let d = normalize3(dir);
360 add3(*center, scale3(d, *radius))
361 }
362 ShapeKind::Box {
363 center,
364 half_extents,
365 } => [
366 center[0] + half_extents[0] * dir[0].signum(),
367 center[1] + half_extents[1] * dir[1].signum(),
368 center[2] + half_extents[2] * dir[2].signum(),
369 ],
370 ShapeKind::Capsule { p0, p1, radius } => {
371 let d0 = dot3(*p0, dir);
372 let d1 = dot3(*p1, dir);
373 let base = if d0 > d1 { *p0 } else { *p1 };
374 add3(base, scale3(normalize3(dir), *radius))
375 }
376 ShapeKind::Plane { normal, offset } => scale3(*normal, *offset + 1e9),
377 ShapeKind::Convex { vertices } => vertices
378 .iter()
379 .max_by(|a, b| {
380 dot3(**a, dir)
381 .partial_cmp(&dot3(**b, dir))
382 .unwrap_or(std::cmp::Ordering::Equal)
383 })
384 .copied()
385 .unwrap_or([0.0; 3]),
386 }
387 }
388}
389/// Result of a segment cast against a shape.
390#[derive(Debug, Clone)]
391pub struct SegmentCastResult {
392 /// Whether the segment hit the shape.
393 pub hit: bool,
394 /// Parameter along the segment `[0, 1]` where the hit occurred.
395 pub t: f64,
396 /// Hit point in world space.
397 pub hit_point: [f64; 3],
398 /// Outward surface normal at the hit point.
399 pub normal: [f64; 3],
400}