axiolid_collide/shape.rs
1// SPDX-License-Identifier: MPL-2.0
2
3//! The convex shape a query runs against.
4
5use axiolid_core::{Point3, Vec3};
6
7/// A convex shape as its vertices and face normals.
8///
9/// Storing normals alongside points is what keeps SAT honest: the candidate
10/// axes for two polyhedra are their face normals plus the cross products of
11/// their edge pairs. Deriving normals from a point cloud every call would
12/// repeat work the caller usually already has.
13///
14/// A shape with no normals still works -- a point cloud's hull is implied by
15/// the edge-pair axes -- but supplying them makes the test exact for
16/// polyhedra rather than conservative.
17#[derive(Debug, Clone, PartialEq)]
18pub struct ConvexShape {
19 pub(crate) points: Vec<Point3>,
20 pub(crate) normals: Vec<Vec3>,
21 pub(crate) edges: Vec<Vec3>,
22}
23
24impl ConvexShape {
25 /// A shape from its vertices alone.
26 ///
27 /// Face normals are not inferred: computing a hull here would make a
28 /// cheap constructor expensive and would silently discard the caller's
29 /// own topology. Use [`ConvexShape::with_faces`] when face normals are
30 /// available.
31 #[must_use]
32 pub fn from_points(points: &[Point3]) -> Self {
33 let edges = derive_edges(points);
34 Self {
35 points: points.to_vec(),
36 normals: Vec::new(),
37 edges,
38 }
39 }
40
41 /// A shape with known face normals and edge directions.
42 #[must_use]
43 pub fn with_faces(points: &[Point3], normals: &[Vec3]) -> Self {
44 let edges = derive_edges(points);
45 Self {
46 points: points.to_vec(),
47 normals: normals.to_vec(),
48 edges,
49 }
50 }
51
52 /// An axis-aligned box as a convex shape.
53 #[must_use]
54 pub fn from_aabb(min: Point3, max: Point3) -> Self {
55 let corners = [
56 Point3::new(min.x, min.y, min.z),
57 Point3::new(max.x, min.y, min.z),
58 Point3::new(max.x, max.y, min.z),
59 Point3::new(min.x, max.y, min.z),
60 Point3::new(min.x, min.y, max.z),
61 Point3::new(max.x, min.y, max.z),
62 Point3::new(max.x, max.y, max.z),
63 Point3::new(min.x, max.y, max.z),
64 ];
65 Self::with_faces(
66 &corners,
67 &[
68 Vec3::new(1.0, 0.0, 0.0),
69 Vec3::new(0.0, 1.0, 0.0),
70 Vec3::new(0.0, 0.0, 1.0),
71 ],
72 )
73 }
74
75 /// The shape's vertices.
76 #[must_use]
77 pub fn points(&self) -> &[Point3] {
78 &self.points
79 }
80
81 /// Whether the shape has no vertices.
82 #[must_use]
83 pub fn is_empty(&self) -> bool {
84 self.points.is_empty()
85 }
86}
87
88/// Edge directions between consecutive and diagonal vertex pairs.
89///
90/// Capped deliberately. The full O(n^2) edge set makes SAT O(n^4) against
91/// another shape, which is the wrong trade past a few dozen vertices; for the
92/// box and prism cases this crate targets, the cap is never reached.
93fn derive_edges(points: &[Point3]) -> Vec<Vec3> {
94 const MAX_POINTS_FOR_FULL_EDGES: usize = 16;
95 let mut edges = Vec::new();
96 if points.len() <= MAX_POINTS_FOR_FULL_EDGES {
97 for i in 0..points.len() {
98 for j in (i + 1)..points.len() {
99 let direction = points[j] - points[i];
100 if direction.length_squared() > 0.0 {
101 edges.push(direction);
102 }
103 }
104 }
105 } else {
106 for window in points.windows(2) {
107 let direction = window[1] - window[0];
108 if direction.length_squared() > 0.0 {
109 edges.push(direction);
110 }
111 }
112 }
113 edges
114}