1use crate::shape::{RayHit, Shape};
7use oxiphysics_core::Aabb;
8use oxiphysics_core::math::{Mat3, Real, Vec3};
9use std::f64::consts::PI;
10
11#[derive(Debug, Clone)]
13pub struct Cylinder {
14 pub radius: Real,
16 pub half_height: Real,
18}
19
20impl Cylinder {
21 pub fn new(radius: Real, half_height: Real) -> Self {
23 Self {
24 radius,
25 half_height,
26 }
27 }
28
29 #[allow(dead_code)]
31 pub fn surface_area(&self) -> Real {
32 let h = 2.0 * self.half_height;
33 2.0 * PI * self.radius * self.radius + 2.0 * PI * self.radius * h
34 }
35
36 #[allow(dead_code)]
38 pub fn volume_explicit(&self) -> Real {
39 PI * self.radius * self.radius * 2.0 * self.half_height
40 }
41
42 #[allow(dead_code)]
45 pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
46 let r2 = self.radius * self.radius;
47 let h = 2.0 * self.half_height;
48 let h2 = h * h;
49 let iy = 0.5 * mass * r2;
50 let ixz = mass * (3.0 * r2 + h2) / 12.0;
51 [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
52 }
53
54 #[allow(dead_code)]
57 pub fn ray_cast_array(
58 &self,
59 origin: [f64; 3],
60 direction: [f64; 3],
61 max_toi: f64,
62 ) -> Option<(f64, [f64; 3])> {
63 let o = Vec3::new(origin[0], origin[1], origin[2]);
64 let d = Vec3::new(direction[0], direction[1], direction[2]);
65 let hit = self.ray_cast(&o, &d, max_toi)?;
66 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
67 }
68
69 #[allow(dead_code)]
71 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
72 let px = p[0];
73 let py = p[1];
74 let pz = p[2];
75
76 let cy = py.clamp(-self.half_height, self.half_height);
78
79 let xz_len = (px * px + pz * pz).sqrt();
81 let (cx, cz) = if xz_len <= self.radius {
82 (px, pz)
83 } else {
84 let s = self.radius / xz_len;
85 (px * s, pz * s)
86 };
87
88 let inside_xz = xz_len <= self.radius;
90 let inside_y = py.abs() <= self.half_height;
91
92 if inside_xz && inside_y {
93 let dist_to_top = self.half_height - py;
95 let dist_to_bot = py + self.half_height;
96 let dist_to_side = self.radius - xz_len;
97 let min_d = dist_to_top.min(dist_to_bot).min(dist_to_side);
98
99 if min_d == dist_to_side {
100 let s = self.radius / xz_len.max(1e-30);
101 [px * s, py, pz * s]
102 } else if dist_to_top <= dist_to_bot {
103 [px, self.half_height, pz]
104 } else {
105 [px, -self.half_height, pz]
106 }
107 } else {
108 [cx, cy, cz]
109 }
110 }
111
112 #[allow(dead_code)]
114 pub fn contains_point(&self, p: [f64; 3]) -> bool {
115 let xz2 = p[0] * p[0] + p[2] * p[2];
116 xz2 <= self.radius * self.radius && p[1].abs() <= self.half_height
117 }
118
119 #[allow(dead_code)]
124 pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
125 let xz_len = (direction[0] * direction[0] + direction[2] * direction[2]).sqrt();
126 let (sx, sz) = if xz_len > 1e-10 {
127 (
128 self.radius * direction[0] / xz_len,
129 self.radius * direction[2] / xz_len,
130 )
131 } else {
132 (self.radius, 0.0)
133 };
134 let sy = self.half_height.copysign(direction[1]);
135 [sx, sy, sz]
136 }
137
138 #[allow(dead_code)]
141 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
142 let xz_len = (p[0] * p[0] + p[2] * p[2]).sqrt();
143 let dist_side = xz_len - self.radius;
144 let dist_cap = p[1].abs() - self.half_height;
145
146 if dist_side <= 0.0 && dist_cap <= 0.0 {
147 dist_side.max(dist_cap)
149 } else if dist_side > 0.0 && dist_cap > 0.0 {
150 (dist_side * dist_side + dist_cap * dist_cap).sqrt()
152 } else {
153 dist_side.max(dist_cap)
155 }
156 }
157
158 #[allow(dead_code)]
161 pub fn closest_point_to_segment(&self, a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
162 let n_samples = 16;
165 let mut best_dist_sq = f64::INFINITY;
166 let mut best_cp = [0.0; 3];
167
168 for i in 0..=n_samples {
169 let t = i as f64 / n_samples as f64;
170 let seg_pt = [
171 a[0] + t * (b[0] - a[0]),
172 a[1] + t * (b[1] - a[1]),
173 a[2] + t * (b[2] - a[2]),
174 ];
175 let cp = self.closest_point(seg_pt);
176 let dx = cp[0] - seg_pt[0];
177 let dy = cp[1] - seg_pt[1];
178 let dz = cp[2] - seg_pt[2];
179 let d2 = dx * dx + dy * dy + dz * dz;
180 if d2 < best_dist_sq {
181 best_dist_sq = d2;
182 best_cp = cp;
183 }
184 }
185 best_cp
186 }
187
188 #[allow(dead_code)]
191 pub fn ray_cast_top_cap(
192 &self,
193 origin: [f64; 3],
194 direction: [f64; 3],
195 max_toi: f64,
196 ) -> Option<(f64, [f64; 3])> {
197 if direction[1].abs() < 1e-12 {
198 return None;
199 }
200 let t = (self.half_height - origin[1]) / direction[1];
201 if t < 0.0 || t > max_toi {
202 return None;
203 }
204 let px = origin[0] + t * direction[0];
205 let pz = origin[2] + t * direction[2];
206 if px * px + pz * pz <= self.radius * self.radius {
207 Some((t, [0.0, 1.0, 0.0]))
208 } else {
209 None
210 }
211 }
212
213 #[allow(dead_code)]
216 pub fn ray_cast_bottom_cap(
217 &self,
218 origin: [f64; 3],
219 direction: [f64; 3],
220 max_toi: f64,
221 ) -> Option<(f64, [f64; 3])> {
222 if direction[1].abs() < 1e-12 {
223 return None;
224 }
225 let t = (-self.half_height - origin[1]) / direction[1];
226 if t < 0.0 || t > max_toi {
227 return None;
228 }
229 let px = origin[0] + t * direction[0];
230 let pz = origin[2] + t * direction[2];
231 if px * px + pz * pz <= self.radius * self.radius {
232 Some((t, [0.0, -1.0, 0.0]))
233 } else {
234 None
235 }
236 }
237
238 #[allow(dead_code)]
241 pub fn ray_cast_lateral(
242 &self,
243 origin: [f64; 3],
244 direction: [f64; 3],
245 max_toi: f64,
246 ) -> Option<(f64, [f64; 3])> {
247 let a = direction[0] * direction[0] + direction[2] * direction[2];
248 if a < 1e-12 {
249 return None;
250 }
251 let b = 2.0 * (origin[0] * direction[0] + origin[2] * direction[2]);
252 let c = origin[0] * origin[0] + origin[2] * origin[2] - self.radius * self.radius;
253 let disc = b * b - 4.0 * a * c;
254 if disc < 0.0 {
255 return None;
256 }
257 let sqrt_disc = disc.sqrt();
258 for sign in [-1.0, 1.0] {
259 let t = (-b + sign * sqrt_disc) / (2.0 * a);
260 if t >= 0.0 && t <= max_toi {
261 let py = origin[1] + t * direction[1];
262 if py.abs() <= self.half_height {
263 let px = origin[0] + t * direction[0];
264 let pz = origin[2] + t * direction[2];
265 let xz_len = (px * px + pz * pz).sqrt();
266 let nx = if xz_len > 1e-12 { px / xz_len } else { 1.0 };
267 let nz = if xz_len > 1e-12 { pz / xz_len } else { 0.0 };
268 return Some((t, [nx, 0.0, nz]));
269 }
270 }
271 }
272 None
273 }
274
275 #[allow(dead_code)]
277 pub fn lateral_surface_area(&self) -> f64 {
278 2.0 * PI * self.radius * 2.0 * self.half_height
279 }
280
281 #[allow(dead_code)]
283 pub fn cap_area(&self) -> f64 {
284 PI * self.radius * self.radius
285 }
286
287 #[allow(dead_code)]
290 pub fn project_on_axis(&self, p: [f64; 3]) -> f64 {
291 p[1].clamp(-self.half_height, self.half_height)
292 }
293
294 #[allow(dead_code)]
296 pub fn distance_to_axis(&self, p: [f64; 3]) -> f64 {
297 let clamped_y = p[1].clamp(-self.half_height, self.half_height);
298 let dy = p[1] - clamped_y;
299 let xz2 = p[0] * p[0] + p[2] * p[2];
300 (xz2 + dy * dy).sqrt()
301 }
302
303 #[allow(dead_code)]
310 pub fn intersects_plane(&self, plane_normal: [f64; 3], plane_d: f64) -> bool {
311 let xz_proj =
315 (plane_normal[0] * plane_normal[0] + plane_normal[2] * plane_normal[2]).sqrt();
316 let y_contrib_top = plane_normal[1] * self.half_height;
317 let y_contrib_bot = -plane_normal[1] * self.half_height;
318 let r_contrib = self.radius * xz_proj;
319
320 let max_sd = y_contrib_top.max(y_contrib_bot) + r_contrib;
321 let min_sd = y_contrib_top.min(y_contrib_bot) - r_contrib;
322
323 min_sd <= plane_d && plane_d <= max_sd
324 }
325
326 #[allow(dead_code)]
330 pub fn sdf(&self, p: [f64; 3]) -> f64 {
331 self.signed_distance(p)
332 }
333
334 #[allow(dead_code)]
338 pub fn intersects_sphere(&self, sphere_center: [f64; 3], sphere_radius: f64) -> bool {
339 let cp = self.closest_point(sphere_center);
340 let dx = sphere_center[0] - cp[0];
341 let dy = sphere_center[1] - cp[1];
342 let dz = sphere_center[2] - cp[2];
343 dx * dx + dy * dy + dz * dz <= sphere_radius * sphere_radius
344 }
345
346 #[allow(dead_code)]
352 pub fn intersects_infinite_cylinder(&self, infinite_radius: f64, center_xz: [f64; 2]) -> bool {
353 let dist = (center_xz[0] * center_xz[0] + center_xz[1] * center_xz[1]).sqrt();
355 dist < self.radius + infinite_radius
357 }
358
359 #[allow(dead_code)]
362 pub fn random_surface_points(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
363 let mut points = Vec::with_capacity(n);
364 let lat = self.lateral_surface_area();
365 let cap = self.cap_area();
366 let total = lat + 2.0 * cap;
367 let p_lat = lat / total;
368 let p_top = cap / total;
369
370 let mut state = seed;
371 let next = |s: &mut u64| -> f64 {
372 *s ^= *s << 13;
373 *s ^= *s >> 7;
374 *s ^= *s << 17;
375 (*s as f64) / (u64::MAX as f64)
376 };
377
378 while points.len() < n {
379 let u = next(&mut state);
380 let v = next(&mut state);
381 let w = next(&mut state);
382
383 if u < p_lat {
384 let theta = v * 2.0 * PI;
385 let y = (w * 2.0 - 1.0) * self.half_height;
386 points.push([self.radius * theta.cos(), y, self.radius * theta.sin()]);
387 } else if u < p_lat + p_top {
388 let r = self.radius * v.sqrt();
390 let theta = w * 2.0 * PI;
391 points.push([r * theta.cos(), self.half_height, r * theta.sin()]);
392 } else {
393 let r = self.radius * v.sqrt();
395 let theta = w * 2.0 * PI;
396 points.push([r * theta.cos(), -self.half_height, r * theta.sin()]);
397 }
398 }
399 points
400 }
401
402 #[allow(dead_code)]
404 pub fn support_array(&self, direction: [f64; 3]) -> [f64; 3] {
405 self.support(direction)
406 }
407
408 #[allow(dead_code)]
410 pub fn full_height(&self) -> f64 {
411 2.0 * self.half_height
412 }
413
414 #[allow(dead_code)]
418 pub fn radius_of_gyration_y(&self) -> f64 {
419 self.radius / 2.0_f64.sqrt()
420 }
421
422 #[allow(dead_code)]
426 pub fn radius_of_gyration_x(&self, _mass: f64) -> f64 {
427 let r2 = self.radius * self.radius;
428 let h = 2.0 * self.half_height;
429 let h2 = h * h;
430 ((3.0 * r2 + h2) / 12.0).sqrt()
431 }
432
433 #[allow(dead_code)]
440 pub fn volume_swept(&self, delta: [f64; 3]) -> f64 {
441 let dist = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
442 self.volume() + self.cap_area() * dist
443 }
444
445 #[allow(dead_code)]
450 pub fn cap_uv(&self, p: [f64; 3]) -> [f64; 2] {
451 let u = 0.5 + p[0] / (2.0 * self.radius);
452 let v = 0.5 + p[2] / (2.0 * self.radius);
453 [u.clamp(0.0, 1.0), v.clamp(0.0, 1.0)]
454 }
455
456 #[allow(dead_code)]
461 pub fn lateral_uv(&self, p: [f64; 3]) -> [f64; 2] {
462 let theta = p[2].atan2(p[0]); let u = ((theta / (2.0 * PI)) + 1.0) % 1.0;
464 let v = (p[1] + self.half_height) / (2.0 * self.half_height);
465 [u, v.clamp(0.0, 1.0)]
466 }
467
468 #[allow(dead_code)]
473 pub fn contains_point_hollow(&self, p: [f64; 3], inner_r: f64) -> bool {
474 let xz2 = p[0] * p[0] + p[2] * p[2];
475 let r_inner = inner_r.max(0.0).min(self.radius);
476 xz2 <= self.radius * self.radius
477 && xz2 >= r_inner * r_inner
478 && p[1].abs() <= self.half_height
479 }
480
481 #[allow(dead_code)]
485 pub fn volume_hollow(&self, inner_radius: f64) -> f64 {
486 let r = inner_radius.max(0.0).min(self.radius);
487 PI * (self.radius * self.radius - r * r) * 2.0 * self.half_height
488 }
489
490 #[allow(dead_code)]
495 pub fn frustum_slant_height(&self, r_top: f64, r_bottom: f64) -> f64 {
496 let h = 2.0 * self.half_height;
497 let dr = (r_top - r_bottom).abs();
498 (h * h + dr * dr).sqrt()
499 }
500
501 #[allow(dead_code)]
505 pub fn frustum_lateral_area(&self, r_top: f64, r_bottom: f64) -> f64 {
506 let slant = self.frustum_slant_height(r_top, r_bottom);
507 PI * (r_top + r_bottom) * slant
508 }
509
510 #[allow(dead_code)]
514 pub fn frustum_volume(&self, r_top: f64, r_bottom: f64) -> f64 {
515 let h = 2.0 * self.half_height;
516 PI * h * (r_top * r_top + r_top * r_bottom + r_bottom * r_bottom) / 3.0
517 }
518
519 #[allow(dead_code)]
524 pub fn oblique_cap_area(&self, tilt_angle: f64) -> f64 {
525 let cos_a = tilt_angle.cos().abs().max(1e-12);
526 PI * self.radius * self.radius / cos_a
527 }
528
529 #[allow(dead_code)]
537 pub fn ruled_surface_point(&self, u: f64, t: f64) -> [f64; 3] {
538 let y = -self.half_height + t * 2.0 * self.half_height;
539 [self.radius * u.cos(), y, self.radius * u.sin()]
540 }
541
542 #[allow(dead_code)]
548 pub fn stacked_volume(&self, other: &Cylinder) -> f64 {
549 self.volume() + other.volume()
550 }
551
552 #[allow(dead_code)]
557 pub fn stacked_half_height(&self, other: &Cylinder) -> f64 {
558 self.half_height + other.half_height
559 }
560
561 #[allow(dead_code)]
565 pub fn stacked_bounding_radius(&self, other: &Cylinder) -> f64 {
566 self.radius.max(other.radius)
567 }
568
569 #[allow(dead_code)]
573 pub fn lateral_normal_at(&self, p: [f64; 3]) -> [f64; 3] {
574 let len = (p[0] * p[0] + p[2] * p[2]).sqrt();
575 if len < 1e-12 {
576 return [1.0, 0.0, 0.0];
577 }
578 [p[0] / len, 0.0, p[2] / len]
579 }
580
581 #[allow(dead_code)]
586 pub fn sdf_gradient(&self, p: [f64; 3]) -> [f64; 3] {
587 let eps = 1e-5;
588 let sdf0 = self.sdf(p);
589 let gx = (self.sdf([p[0] + eps, p[1], p[2]]) - sdf0) / eps;
590 let gy = (self.sdf([p[0], p[1] + eps, p[2]]) - sdf0) / eps;
591 let gz = (self.sdf([p[0], p[1], p[2] + eps]) - sdf0) / eps;
592 let len = (gx * gx + gy * gy + gz * gz).sqrt().max(1e-14);
593 [gx / len, gy / len, gz / len]
594 }
595
596 #[allow(dead_code)]
601 pub fn lateral_ray_intersection_count(&self, origin: [f64; 3], direction: [f64; 3]) -> usize {
602 let a = direction[0] * direction[0] + direction[2] * direction[2];
603 if a < 1e-14 {
604 return 0;
605 }
606 let b = 2.0 * (origin[0] * direction[0] + origin[2] * direction[2]);
607 let c = origin[0] * origin[0] + origin[2] * origin[2] - self.radius * self.radius;
608 let disc = b * b - 4.0 * a * c;
609 if disc < 0.0 {
610 return 0;
611 }
612 if disc < 1e-14 {
613 return 1;
614 }
615 2
616 }
617
618 #[allow(dead_code)]
622 pub fn top_rim_points(&self, n: usize) -> Vec<[f64; 3]> {
623 let n = n.max(2);
624 (0..n)
625 .map(|i| {
626 let t = 2.0 * PI * i as f64 / n as f64;
627 [
628 self.radius * t.cos(),
629 self.half_height,
630 self.radius * t.sin(),
631 ]
632 })
633 .collect()
634 }
635}
636
637impl Shape for Cylinder {
638 fn bounding_box(&self) -> Aabb {
639 Aabb::new(
640 Vec3::new(-self.radius, -self.half_height, -self.radius),
641 Vec3::new(self.radius, self.half_height, self.radius),
642 )
643 }
644
645 fn support_point(&self, direction: &Vec3) -> Vec3 {
646 let xz_len = (direction.x * direction.x + direction.z * direction.z).sqrt();
647 let (sx, sz) = if xz_len > 1e-10 {
648 (
649 self.radius * direction.x / xz_len,
650 self.radius * direction.z / xz_len,
651 )
652 } else {
653 (self.radius, 0.0)
654 };
655 let sy = self.half_height.copysign(direction.y);
656 Vec3::new(sx, sy, sz)
657 }
658
659 fn volume(&self) -> Real {
660 PI * self.radius * self.radius * 2.0 * self.half_height
661 }
662
663 fn center_of_mass(&self) -> Vec3 {
664 Vec3::zeros()
665 }
666
667 fn inertia_tensor(&self, mass: Real) -> Mat3 {
668 let r2 = self.radius * self.radius;
669 let h2 = (2.0 * self.half_height).powi(2);
670 let iy = 0.5 * mass * r2;
671 let ixz = mass * (3.0 * r2 + h2) / 12.0;
672 Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
673 }
674
675 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
676 let mut best: Option<RayHit> = None;
677
678 let a = ray_direction.x.powi(2) + ray_direction.z.powi(2);
680 let b = 2.0 * (ray_origin.x * ray_direction.x + ray_origin.z * ray_direction.z);
681 let c = ray_origin.x.powi(2) + ray_origin.z.powi(2) - self.radius.powi(2);
682
683 if a > 1e-12 {
684 let disc = b * b - 4.0 * a * c;
685 if disc >= 0.0 {
686 let sqrt_disc = disc.sqrt();
687 for sign in [-1.0, 1.0] {
688 let t = (-b + sign * sqrt_disc) / (2.0 * a);
689 if t >= 0.0 && t <= max_toi {
690 let p = ray_origin + ray_direction * t;
691 if p.y.abs() <= self.half_height {
692 let normal = Vec3::new(p.x, 0.0, p.z).normalize();
693 if best.as_ref().is_none_or(|prev| t < prev.toi) {
694 best = Some(RayHit {
695 point: p,
696 normal,
697 toi: t,
698 });
699 }
700 }
701 }
702 }
703 }
704 }
705
706 if ray_direction.y.abs() > 1e-12 {
708 for &cap_y in &[self.half_height, -self.half_height] {
709 let t = (cap_y - ray_origin.y) / ray_direction.y;
710 if t >= 0.0 && t <= max_toi {
711 let p = ray_origin + ray_direction * t;
712 if p.x.powi(2) + p.z.powi(2) <= self.radius.powi(2) {
713 let normal = Vec3::new(0.0, cap_y.signum(), 0.0);
714 if best.as_ref().is_none_or(|prev| t < prev.toi) {
715 best = Some(RayHit {
716 point: p,
717 normal,
718 toi: t,
719 });
720 }
721 }
722 }
723 }
724 }
725
726 best
727 }
728}
729
730#[cfg(test)]
731mod tests {
732 use super::*;
733
734 #[test]
735 fn test_cylinder_volume() {
736 let c = Cylinder::new(1.0, 1.0);
737 let expected = PI * 2.0;
738 assert!((c.volume() - expected).abs() < 1e-10);
739 }
740
741 #[test]
742 fn test_cylinder_surface_area() {
743 let c = Cylinder::new(1.0, 1.0);
745 let expected = 6.0 * PI;
746 assert!((c.surface_area() - expected).abs() < 1e-10);
747 }
748
749 #[test]
750 fn test_cylinder_inertia_symmetry() {
751 let c = Cylinder::new(1.5, 2.0);
752 let it = c.inertia_tensor(3.0);
753 let diff = (it[(0, 0)] - it[(2, 2)]).abs();
755 assert!(
756 diff < 1e-10,
757 "I_xx != I_zz: {} vs {}",
758 it[(0, 0)],
759 it[(2, 2)]
760 );
761 assert!(it[(0, 1)].abs() < 1e-10);
763 assert!(it[(1, 0)].abs() < 1e-10);
764 assert!(it[(0, 2)].abs() < 1e-10);
765 }
766
767 #[test]
768 fn test_cylinder_inertia_array() {
769 let c = Cylinder::new(1.0, 0.5);
770 let it = c.inertia_tensor_array(2.0);
771 assert!((it[1][1] - 1.0).abs() < 1e-10);
773 let h = 1.0_f64; let ixx = 2.0 * (3.0 + h * h) / 12.0;
776 assert!((it[0][0] - ixx).abs() < 1e-10);
777 }
778
779 #[test]
780 fn test_cylinder_raycast() {
781 let c = Cylinder::new(1.0, 2.0);
782 let origin = Vec3::new(-5.0, 0.0, 0.0);
783 let dir = Vec3::new(1.0, 0.0, 0.0);
784 let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
785 assert!((hit.toi - 4.0).abs() < 1e-10);
786 }
787
788 #[test]
789 fn test_cylinder_raycast_array() {
790 let c = Cylinder::new(1.0, 2.0);
791 let (t, n) = c
792 .ray_cast_array([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0)
793 .unwrap();
794 assert!((t - 4.0).abs() < 1e-10);
795 assert!((n[0] + 1.0).abs() < 1e-10); }
797
798 #[test]
799 fn test_cylinder_raycast_cap() {
800 let c = Cylinder::new(1.0, 2.0);
801 let origin = Vec3::new(0.0, 10.0, 0.0);
802 let dir = Vec3::new(0.0, -1.0, 0.0);
803 let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
804 assert!((hit.toi - 8.0).abs() < 1e-10);
805 assert!((hit.normal.y - 1.0).abs() < 1e-10);
806 }
807
808 #[test]
809 fn test_cylinder_contains_point() {
810 let c = Cylinder::new(1.0, 2.0);
811 assert!(c.contains_point([0.0, 0.0, 0.0]));
812 assert!(c.contains_point([0.9, 1.9, 0.0]));
813 assert!(!c.contains_point([0.0, 2.1, 0.0]));
814 assert!(!c.contains_point([1.1, 0.0, 0.0]));
815 }
816
817 #[test]
818 fn test_cylinder_closest_point_outside_side() {
819 let c = Cylinder::new(1.0, 2.0);
820 let cp = c.closest_point([3.0, 0.0, 0.0]);
821 assert!((cp[0] - 1.0).abs() < 1e-10);
822 assert!((cp[1]).abs() < 1e-10);
823 assert!((cp[2]).abs() < 1e-10);
824 }
825
826 #[test]
827 fn test_cylinder_closest_point_above_cap() {
828 let c = Cylinder::new(1.0, 2.0);
829 let cp = c.closest_point([0.0, 5.0, 0.0]);
830 assert!((cp[1] - 2.0).abs() < 1e-10);
831 }
832
833 #[test]
836 fn test_cylinder_support_array() {
837 let c = Cylinder::new(2.0, 3.0);
838 let sp = c.support([1.0, 0.0, 0.0]);
839 assert!((sp[0] - 2.0).abs() < 1e-10);
840 assert!((sp[1] - 3.0).abs() < 1e-10); }
842
843 #[test]
844 fn test_cylinder_support_negative_y() {
845 let c = Cylinder::new(1.0, 2.0);
846 let sp = c.support([0.0, -1.0, 0.0]);
847 assert!((sp[1] + 2.0).abs() < 1e-10);
848 }
849
850 #[test]
851 fn test_cylinder_support_diagonal() {
852 let c = Cylinder::new(1.0, 2.0);
853 let sp = c.support([1.0, 1.0, 0.0]);
854 assert!((sp[0] - 1.0).abs() < 1e-10); assert!((sp[1] - 2.0).abs() < 1e-10); }
857
858 #[test]
859 fn test_cylinder_signed_distance_inside() {
860 let c = Cylinder::new(2.0, 3.0);
861 let d = c.signed_distance([0.0, 0.0, 0.0]);
862 assert!(d < 0.0); assert!((d + 2.0).abs() < 1e-10);
865 }
866
867 #[test]
868 fn test_cylinder_signed_distance_outside_side() {
869 let c = Cylinder::new(1.0, 2.0);
870 let d = c.signed_distance([3.0, 0.0, 0.0]);
871 assert!((d - 2.0).abs() < 1e-10); }
873
874 #[test]
875 fn test_cylinder_signed_distance_outside_cap() {
876 let c = Cylinder::new(1.0, 2.0);
877 let d = c.signed_distance([0.0, 4.0, 0.0]);
878 assert!((d - 2.0).abs() < 1e-10); }
880
881 #[test]
882 fn test_cylinder_signed_distance_outside_corner() {
883 let c = Cylinder::new(1.0, 1.0);
884 let d = c.signed_distance([2.0, 2.0, 0.0]);
886 assert!((d - 2.0_f64.sqrt()).abs() < 1e-10);
888 }
889
890 #[test]
891 fn test_cylinder_ray_cast_top_cap() {
892 let c = Cylinder::new(1.0, 2.0);
893 let result = c.ray_cast_top_cap([0.0, 5.0, 0.0], [0.0, -1.0, 0.0], 100.0);
894 assert!(result.is_some());
895 let (t, n) = result.unwrap();
896 assert!((t - 3.0).abs() < 1e-10);
897 assert!((n[1] - 1.0).abs() < 1e-10);
898 }
899
900 #[test]
901 fn test_cylinder_ray_cast_top_cap_miss() {
902 let c = Cylinder::new(1.0, 2.0);
903 let result = c.ray_cast_top_cap([3.0, 5.0, 0.0], [0.0, -1.0, 0.0], 100.0);
905 assert!(result.is_none());
906 }
907
908 #[test]
909 fn test_cylinder_ray_cast_bottom_cap() {
910 let c = Cylinder::new(1.0, 2.0);
911 let result = c.ray_cast_bottom_cap([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
912 assert!(result.is_some());
913 let (t, n) = result.unwrap();
914 assert!((t - 3.0).abs() < 1e-10);
915 assert!((n[1] + 1.0).abs() < 1e-10);
916 }
917
918 #[test]
919 fn test_cylinder_ray_cast_lateral() {
920 let c = Cylinder::new(1.0, 2.0);
921 let result = c.ray_cast_lateral([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
922 assert!(result.is_some());
923 let (t, n) = result.unwrap();
924 assert!((t - 4.0).abs() < 1e-10);
925 assert!((n[0] + 1.0).abs() < 1e-10);
926 }
927
928 #[test]
929 fn test_cylinder_ray_cast_lateral_miss_height() {
930 let c = Cylinder::new(1.0, 1.0);
931 let result = c.ray_cast_lateral([-5.0, 5.0, 0.0], [1.0, 0.0, 0.0], 100.0);
933 assert!(result.is_none());
934 }
935
936 #[test]
937 fn test_cylinder_lateral_surface_area() {
938 let c = Cylinder::new(1.0, 1.0);
939 assert!((c.lateral_surface_area() - 4.0 * PI).abs() < 1e-10);
941 }
942
943 #[test]
944 fn test_cylinder_cap_area() {
945 let c = Cylinder::new(2.0, 1.0);
946 assert!((c.cap_area() - 4.0 * PI).abs() < 1e-10);
947 }
948
949 #[test]
950 fn test_cylinder_surface_area_decomposition() {
951 let c = Cylinder::new(1.5, 2.5);
952 let total = c.surface_area();
953 let decomposed = c.lateral_surface_area() + 2.0 * c.cap_area();
954 assert!((total - decomposed).abs() < 1e-10);
955 }
956
957 #[test]
958 fn test_cylinder_project_on_axis() {
959 let c = Cylinder::new(1.0, 2.0);
960 assert!((c.project_on_axis([0.0, 5.0, 0.0]) - 2.0).abs() < 1e-10);
961 assert!((c.project_on_axis([0.0, -5.0, 0.0]) + 2.0).abs() < 1e-10);
962 assert!((c.project_on_axis([3.0, 1.0, 0.0]) - 1.0).abs() < 1e-10);
963 }
964
965 #[test]
966 fn test_cylinder_distance_to_axis() {
967 let c = Cylinder::new(1.0, 2.0);
968 assert!(c.distance_to_axis([0.0, 0.0, 0.0]).abs() < 1e-10);
970 assert!((c.distance_to_axis([3.0, 0.0, 0.0]) - 3.0).abs() < 1e-10);
972 assert!((c.distance_to_axis([0.0, 5.0, 0.0]) - 3.0).abs() < 1e-10);
974 }
975
976 #[test]
977 fn test_cylinder_closest_point_to_segment() {
978 let c = Cylinder::new(1.0, 2.0);
979 let cp = c.closest_point_to_segment([5.0, 0.0, 0.0], [5.0, 1.0, 0.0]);
981 assert!((cp[0] - 1.0).abs() < 1e-10);
982 }
983
984 #[test]
985 fn test_cylinder_volume_explicit_matches() {
986 let c = Cylinder::new(1.5, 2.5);
987 assert!((c.volume_explicit() - c.volume()).abs() < 1e-10);
988 }
989
990 #[test]
993 fn test_cylinder_intersects_plane_through_center() {
994 let c = Cylinder::new(1.0, 2.0);
995 assert!(c.intersects_plane([0.0, 1.0, 0.0], 0.0));
997 }
998
999 #[test]
1000 fn test_cylinder_intersects_plane_above() {
1001 let c = Cylinder::new(1.0, 2.0);
1002 assert!(!c.intersects_plane([0.0, 1.0, 0.0], 10.0));
1004 }
1005
1006 #[test]
1007 fn test_cylinder_intersects_plane_lateral() {
1008 let c = Cylinder::new(2.0, 1.0);
1009 assert!(c.intersects_plane([1.0, 0.0, 0.0], 1.0));
1011 assert!(!c.intersects_plane([1.0, 0.0, 0.0], 3.0));
1013 }
1014
1015 #[test]
1016 fn test_cylinder_sdf_matches_signed_distance() {
1017 let c = Cylinder::new(1.0, 2.0);
1018 let p = [3.0, 0.0, 0.0];
1019 assert!((c.sdf(p) - c.signed_distance(p)).abs() < 1e-12);
1020 }
1021
1022 #[test]
1023 fn test_cylinder_intersects_sphere_yes() {
1024 let c = Cylinder::new(1.0, 2.0);
1025 assert!(c.intersects_sphere([0.0, 0.0, 0.0], 0.5));
1026 assert!(c.intersects_sphere([1.5, 0.0, 0.0], 0.6)); }
1028
1029 #[test]
1030 fn test_cylinder_intersects_sphere_no() {
1031 let c = Cylinder::new(1.0, 2.0);
1032 assert!(!c.intersects_sphere([5.0, 0.0, 0.0], 0.5));
1033 }
1034
1035 #[test]
1036 fn test_cylinder_intersects_infinite_cylinder_overlap() {
1037 let c = Cylinder::new(1.0, 2.0);
1038 assert!(c.intersects_infinite_cylinder(1.0, [0.0, 0.0]));
1040 }
1041
1042 #[test]
1043 fn test_cylinder_intersects_infinite_cylinder_no_overlap() {
1044 let c = Cylinder::new(1.0, 2.0);
1045 assert!(!c.intersects_infinite_cylinder(0.5, [5.0, 0.0]));
1047 }
1048
1049 #[test]
1050 fn test_cylinder_random_surface_points_count() {
1051 let c = Cylinder::new(1.0, 2.0);
1052 let pts = c.random_surface_points(50, 12345);
1053 assert_eq!(pts.len(), 50);
1054 }
1055
1056 #[test]
1057 fn test_cylinder_random_surface_points_on_surface() {
1058 let c = Cylinder::new(1.0, 2.0);
1059 let pts = c.random_surface_points(80, 99);
1060 for p in &pts {
1061 let sdf = c.sdf(*p);
1062 assert!(sdf.abs() < 0.01, "point {:?} has sdf={sdf}", p);
1063 }
1064 }
1065
1066 #[test]
1067 fn test_cylinder_support_array_matches_support() {
1068 let c = Cylinder::new(2.0, 3.0);
1069 let d = [1.0, 1.0, 0.0];
1070 let a = c.support_array(d);
1071 let b = c.support(d);
1072 assert_eq!(a, b);
1073 }
1074
1075 #[test]
1076 fn test_cylinder_full_height() {
1077 let c = Cylinder::new(1.0, 3.0);
1078 assert!((c.full_height() - 6.0).abs() < 1e-12);
1079 }
1080
1081 #[test]
1082 fn test_cylinder_radius_of_gyration_y() {
1083 let c = Cylinder::new(2.0, 1.0);
1084 assert!((c.radius_of_gyration_y() - 2.0_f64.sqrt()).abs() < 1e-12);
1086 }
1087
1088 #[test]
1089 fn test_cylinder_radius_of_gyration_x() {
1090 let c = Cylinder::new(1.0, 1.0); let expected = (7.0_f64 / 12.0).sqrt();
1093 assert!((c.radius_of_gyration_x(1.0) - expected).abs() < 1e-12);
1094 }
1095
1096 #[test]
1099 fn test_cylinder_volume_swept_zero_delta() {
1100 let c = Cylinder::new(1.0, 2.0);
1101 let swept = c.volume_swept([0.0, 0.0, 0.0]);
1103 assert!((swept - c.volume()).abs() < 1e-10);
1104 }
1105
1106 #[test]
1107 fn test_cylinder_volume_swept_positive_delta() {
1108 let c = Cylinder::new(1.0, 1.0);
1109 let swept = c.volume_swept([1.0, 0.0, 0.0]);
1110 assert!(swept > c.volume());
1112 assert!((swept - c.volume() - PI).abs() < 1e-10);
1113 }
1114
1115 #[test]
1116 fn test_cylinder_cap_uv_center() {
1117 let c = Cylinder::new(2.0, 1.0);
1118 let uv = c.cap_uv([0.0, 2.0, 0.0]);
1119 assert!((uv[0] - 0.5).abs() < 1e-10);
1120 assert!((uv[1] - 0.5).abs() < 1e-10);
1121 }
1122
1123 #[test]
1124 fn test_cylinder_cap_uv_edge() {
1125 let c = Cylinder::new(2.0, 1.0);
1126 let uv = c.cap_uv([2.0, 1.0, 0.0]);
1128 assert!((uv[0] - 1.0).abs() < 1e-10);
1129 assert!((uv[1] - 0.5).abs() < 1e-10);
1130 }
1131
1132 #[test]
1133 fn test_cylinder_lateral_uv_bottom_left() {
1134 let c = Cylinder::new(1.0, 1.0);
1135 let uv = c.lateral_uv([1.0, -1.0, 0.0]);
1137 assert!(uv[1].abs() < 1e-10, "v={}", uv[1]);
1138 }
1139
1140 #[test]
1141 fn test_cylinder_lateral_uv_top() {
1142 let c = Cylinder::new(1.0, 1.0);
1143 let uv = c.lateral_uv([1.0, 1.0, 0.0]);
1144 assert!((uv[1] - 1.0).abs() < 1e-10, "v={}", uv[1]);
1145 }
1146
1147 #[test]
1148 fn test_cylinder_hollow_inside_ring() {
1149 let c = Cylinder::new(2.0, 1.0);
1150 assert!(c.contains_point_hollow([1.5, 0.0, 0.0], 1.0));
1152 }
1153
1154 #[test]
1155 fn test_cylinder_hollow_outside_outer() {
1156 let c = Cylinder::new(2.0, 1.0);
1157 assert!(!c.contains_point_hollow([3.0, 0.0, 0.0], 1.0));
1158 }
1159
1160 #[test]
1161 fn test_cylinder_hollow_inside_inner_hole() {
1162 let c = Cylinder::new(2.0, 1.0);
1163 assert!(!c.contains_point_hollow([0.5, 0.0, 0.0], 1.0));
1165 }
1166
1167 #[test]
1168 fn test_cylinder_volume_hollow_full_solid() {
1169 let c = Cylinder::new(1.0, 1.0);
1171 assert!((c.volume_hollow(0.0) - c.volume()).abs() < 1e-10);
1172 }
1173
1174 #[test]
1175 fn test_cylinder_volume_hollow_positive() {
1176 let c = Cylinder::new(2.0, 1.0);
1177 let v = c.volume_hollow(1.0);
1178 assert!(v > 0.0);
1179 assert!((v - 6.0 * PI).abs() < 1e-10);
1181 }
1182
1183 #[test]
1184 fn test_cylinder_frustum_slant_height_equal_radii() {
1185 let c = Cylinder::new(1.0, 1.0);
1186 let sl = c.frustum_slant_height(1.0, 1.0);
1188 assert!((sl - 2.0).abs() < 1e-10);
1189 }
1190
1191 #[test]
1192 fn test_cylinder_frustum_slant_height_cone() {
1193 let c = Cylinder::new(1.0, 1.0);
1194 let sl = c.frustum_slant_height(0.0, 1.0);
1196 assert!((sl - 5.0_f64.sqrt()).abs() < 1e-10);
1197 }
1198
1199 #[test]
1200 fn test_cylinder_frustum_lateral_area() {
1201 let c = Cylinder::new(1.0, 1.0); let area = c.frustum_lateral_area(1.0, 1.0);
1204 assert!((area - 4.0 * PI).abs() < 1e-10);
1205 }
1206
1207 #[test]
1208 fn test_cylinder_frustum_volume_cylinder() {
1209 let c = Cylinder::new(1.0, 1.0);
1210 let fv = c.frustum_volume(1.0, 1.0);
1212 assert!((fv - c.volume()).abs() < 1e-10);
1213 }
1214
1215 #[test]
1216 fn test_cylinder_frustum_volume_cone() {
1217 let c = Cylinder::new(1.0, 1.0);
1218 let fv = c.frustum_volume(0.0, 1.0);
1220 let expected = PI * 1.0 * 2.0 / 3.0;
1221 assert!((fv - expected).abs() < 1e-10);
1222 }
1223
1224 #[test]
1225 fn test_cylinder_oblique_cap_area_zero_tilt() {
1226 let c = Cylinder::new(1.0, 1.0);
1227 assert!((c.oblique_cap_area(0.0) - PI).abs() < 1e-10);
1229 }
1230
1231 #[test]
1232 fn test_cylinder_oblique_cap_area_larger_at_tilt() {
1233 let c = Cylinder::new(1.0, 1.0);
1234 assert!(c.oblique_cap_area(0.5) > c.oblique_cap_area(0.0));
1235 }
1236
1237 #[test]
1238 fn test_cylinder_ruled_surface_point_on_surface() {
1239 let c = Cylinder::new(1.0, 2.0);
1240 for i in 0..8 {
1242 let u = 2.0 * PI * i as f64 / 8.0;
1243 for j in 0..=4 {
1244 let t = j as f64 / 4.0;
1245 let p = c.ruled_surface_point(u, t);
1246 let sdf = c.sdf(p);
1247 assert!(sdf.abs() < 1e-9, "ruled sdf={sdf}");
1248 }
1249 }
1250 }
1251
1252 #[test]
1253 fn test_cylinder_stacked_volume() {
1254 let c1 = Cylinder::new(1.0, 1.0);
1255 let c2 = Cylinder::new(1.0, 2.0);
1256 let combined = c1.stacked_volume(&c2);
1257 assert!((combined - c1.volume() - c2.volume()).abs() < 1e-10);
1258 }
1259
1260 #[test]
1261 fn test_cylinder_stacked_half_height() {
1262 let c1 = Cylinder::new(1.0, 1.0);
1263 let c2 = Cylinder::new(1.0, 2.0);
1264 assert!((c1.stacked_half_height(&c2) - 3.0).abs() < 1e-12);
1265 }
1266
1267 #[test]
1268 fn test_cylinder_stacked_bounding_radius() {
1269 let c1 = Cylinder::new(1.0, 1.0);
1270 let c2 = Cylinder::new(3.0, 0.5);
1271 assert!((c1.stacked_bounding_radius(&c2) - 3.0).abs() < 1e-12);
1272 }
1273
1274 #[test]
1275 fn test_cylinder_lateral_normal_at_x_axis() {
1276 let c = Cylinder::new(1.0, 2.0);
1277 let n = c.lateral_normal_at([1.0, 0.5, 0.0]);
1278 assert!((n[0] - 1.0).abs() < 1e-10);
1279 assert!(n[1].abs() < 1e-12);
1280 assert!(n[2].abs() < 1e-12);
1281 }
1282
1283 #[test]
1284 fn test_cylinder_lateral_normal_unit() {
1285 let c = Cylinder::new(1.0, 2.0);
1286 let n = c.lateral_normal_at([0.6, 1.0, 0.8]);
1287 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1288 assert!((len - 1.0).abs() < 1e-9);
1289 }
1290
1291 #[test]
1292 fn test_cylinder_sdf_gradient_outside_points_outward() {
1293 let c = Cylinder::new(1.0, 2.0);
1294 let g = c.sdf_gradient([3.0, 0.0, 0.0]);
1295 assert!(g[0] > 0.5, "gx={}", g[0]);
1297 }
1298
1299 #[test]
1300 fn test_cylinder_lateral_ray_intersection_count_two() {
1301 let c = Cylinder::new(1.0, 2.0);
1302 let count = c.lateral_ray_intersection_count([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1303 assert_eq!(count, 2);
1304 }
1305
1306 #[test]
1307 fn test_cylinder_lateral_ray_intersection_count_miss() {
1308 let c = Cylinder::new(1.0, 2.0);
1309 let count = c.lateral_ray_intersection_count([0.0, 5.0, 0.0], [0.0, 1.0, 0.0]);
1310 assert_eq!(count, 0);
1311 }
1312
1313 #[test]
1314 fn test_cylinder_top_rim_points_count() {
1315 let c = Cylinder::new(1.0, 2.0);
1316 let pts = c.top_rim_points(16);
1317 assert_eq!(pts.len(), 16);
1318 }
1319
1320 #[test]
1321 fn test_cylinder_top_rim_points_at_top() {
1322 let c = Cylinder::new(1.0, 2.0);
1323 let pts = c.top_rim_points(8);
1324 for p in &pts {
1325 assert!((p[1] - c.half_height).abs() < 1e-12);
1326 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1327 assert!((xz - c.radius).abs() < 1e-9);
1328 }
1329 }
1330}