1use crate::point::Point;
11use crate::shape::Shape;
12use nalgebra::SVector;
13
14#[derive(Clone, Copy, Debug)]
23pub struct Capsule<const D: usize> {
24 pub half_height: f64,
26 pub radius: f64,
28 pub axis: usize,
30}
31
32impl<const D: usize> Capsule<D> {
33 pub fn new(half_height: f64, radius: f64, axis: usize) -> Self {
38 debug_assert!(axis < D, "axis {axis} out of range for D={D}");
39 Self {
40 half_height,
41 radius,
42 axis,
43 }
44 }
45
46 pub fn y_aligned(half_height: f64, radius: f64) -> Self {
48 assert!(D >= 2, "Y-axis requires D >= 2");
49 Self::new(half_height, radius, 1)
50 }
51
52 pub fn total_length(&self) -> f64 {
54 2.0 * self.half_height + 2.0 * self.radius
55 }
56}
57
58impl<const D: usize> Shape<D> for Capsule<D> {
59 fn support(&self, direction: &SVector<f64, D>) -> SVector<f64, D> {
64 let norm = direction.norm();
65 if norm < 1e-15 {
66 let mut result = SVector::zeros();
68 result[self.axis] = self.half_height;
69 return result;
70 }
71
72 let mut center = SVector::zeros();
74 center[self.axis] = if direction[self.axis] >= 0.0 {
75 self.half_height
76 } else {
77 -self.half_height
78 };
79
80 center + direction * (self.radius / norm)
82 }
83
84 fn bounding_sphere(&self) -> (Point<D>, f64) {
85 (Point::origin(), self.half_height + self.radius)
86 }
87
88 fn as_any(&self) -> &dyn std::any::Any {
89 self
90 }
91
92 fn clone_box(&self) -> Box<dyn Shape<D>> {
93 Box::new(*self)
94 }
95}
96
97#[cfg(test)]
98mod tests {
99 use super::*;
100
101 #[test]
102 fn support_along_axis() {
103 let cap = Capsule::<3>::y_aligned(2.0, 0.5);
104 let dir = SVector::from([0.0, 1.0, 0.0]);
105 let sp = cap.support(&dir);
106 assert!((sp[1] - 2.5).abs() < 1e-12, "support Y+ = {}", sp[1]);
108 }
109
110 #[test]
111 fn support_negative_axis() {
112 let cap = Capsule::<3>::y_aligned(2.0, 0.5);
113 let dir = SVector::from([0.0, -1.0, 0.0]);
114 let sp = cap.support(&dir);
115 assert!((sp[1] - (-2.5)).abs() < 1e-12, "support Y- = {}", sp[1]);
116 }
117
118 #[test]
119 fn support_perpendicular() {
120 let cap = Capsule::<3>::y_aligned(2.0, 0.5);
121 let dir = SVector::from([1.0, 0.0, 0.0]);
122 let sp = cap.support(&dir);
123 assert!((sp[0] - 0.5).abs() < 1e-12, "support X = {}", sp[0]);
125 }
126
127 #[test]
128 fn support_diagonal() {
129 let cap = Capsule::<3>::y_aligned(2.0, 0.5);
130 let dir = SVector::from([1.0, 1.0, 0.0]);
131 let sp = cap.support(&dir);
132 let expected_y = 2.0 + 0.5 / 2.0f64.sqrt();
134 let expected_x = 0.5 / 2.0f64.sqrt();
135 assert!((sp[1] - expected_y).abs() < 1e-10, "diag Y = {}", sp[1]);
136 assert!((sp[0] - expected_x).abs() < 1e-10, "diag X = {}", sp[0]);
137 }
138
139 #[test]
140 fn bounding_sphere_contains_capsule() {
141 let cap = Capsule::<3>::y_aligned(3.0, 1.0);
142 let (center, radius) = cap.bounding_sphere();
143 assert!((center.coord(0)).abs() < 1e-12);
144 assert!((radius - 4.0).abs() < 1e-12);
145 }
146
147 #[test]
148 fn capsule_x_aligned() {
149 let cap = Capsule::<3>::new(1.5, 0.3, 0);
150 let dir = SVector::from([1.0, 0.0, 0.0]);
151 let sp = cap.support(&dir);
152 assert!((sp[0] - 1.8).abs() < 1e-12, "X-capsule support = {}", sp[0]);
153 }
154
155 #[test]
156 fn capsule_4d() {
157 let cap = Capsule::<4>::new(2.0, 1.0, 3); let dir = SVector::from([0.0, 0.0, 0.0, 1.0]);
159 let sp = cap.support(&dir);
160 assert!((sp[3] - 3.0).abs() < 1e-12, "4D capsule W+ = {}", sp[3]);
161 }
162
163 #[test]
164 fn capsule_2d() {
165 let cap = Capsule::<2>::new(1.0, 0.5, 0); let dir = SVector::from([0.0, 1.0]);
167 let sp = cap.support(&dir);
168 assert!((sp[1] - 0.5).abs() < 1e-12, "2D capsule Y = {}", sp[1]);
170 }
171
172 #[test]
173 fn degenerate_direction() {
174 let cap = Capsule::<3>::y_aligned(2.0, 0.5);
175 let dir = SVector::from([0.0, 0.0, 0.0]);
176 let sp = cap.support(&dir);
177 assert!((sp[1] - 2.0).abs() < 1e-12);
179 }
180
181 #[test]
182 fn total_length() {
183 let cap = Capsule::<3>::y_aligned(2.0, 0.5);
184 assert!((cap.total_length() - 5.0).abs() < 1e-12);
185 }
186}