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 pub fn surface_area(&self) -> Real {
31 let h = 2.0 * self.half_height;
32 2.0 * PI * self.radius * self.radius + 2.0 * PI * self.radius * h
33 }
34
35 pub fn volume_explicit(&self) -> Real {
37 PI * self.radius * self.radius * 2.0 * self.half_height
38 }
39
40 pub fn inertia_tensor_array(&self, mass: f64) -> [[f64; 3]; 3] {
43 let r2 = self.radius * self.radius;
44 let h = 2.0 * self.half_height;
45 let h2 = h * h;
46 let iy = 0.5 * mass * r2;
47 let ixz = mass * (3.0 * r2 + h2) / 12.0;
48 [[ixz, 0.0, 0.0], [0.0, iy, 0.0], [0.0, 0.0, ixz]]
49 }
50
51 pub fn ray_cast_array(
54 &self,
55 origin: [f64; 3],
56 direction: [f64; 3],
57 max_toi: f64,
58 ) -> Option<(f64, [f64; 3])> {
59 let o = Vec3::new(origin[0], origin[1], origin[2]);
60 let d = Vec3::new(direction[0], direction[1], direction[2]);
61 let hit = self.ray_cast(&o, &d, max_toi)?;
62 Some((hit.toi, [hit.normal.x, hit.normal.y, hit.normal.z]))
63 }
64
65 pub fn closest_point(&self, p: [f64; 3]) -> [f64; 3] {
67 let px = p[0];
68 let py = p[1];
69 let pz = p[2];
70
71 let cy = py.clamp(-self.half_height, self.half_height);
73
74 let xz_len = (px * px + pz * pz).sqrt();
76 let (cx, cz) = if xz_len <= self.radius {
77 (px, pz)
78 } else {
79 let s = self.radius / xz_len;
80 (px * s, pz * s)
81 };
82
83 let inside_xz = xz_len <= self.radius;
85 let inside_y = py.abs() <= self.half_height;
86
87 if inside_xz && inside_y {
88 let dist_to_top = self.half_height - py;
90 let dist_to_bot = py + self.half_height;
91 let dist_to_side = self.radius - xz_len;
92 let min_d = dist_to_top.min(dist_to_bot).min(dist_to_side);
93
94 if min_d == dist_to_side {
95 let s = self.radius / xz_len.max(1e-30);
96 [px * s, py, pz * s]
97 } else if dist_to_top <= dist_to_bot {
98 [px, self.half_height, pz]
99 } else {
100 [px, -self.half_height, pz]
101 }
102 } else {
103 [cx, cy, cz]
104 }
105 }
106
107 pub fn contains_point(&self, p: [f64; 3]) -> bool {
109 let xz2 = p[0] * p[0] + p[2] * p[2];
110 xz2 <= self.radius * self.radius && p[1].abs() <= self.half_height
111 }
112
113 pub fn support(&self, direction: [f64; 3]) -> [f64; 3] {
118 let xz_len = (direction[0] * direction[0] + direction[2] * direction[2]).sqrt();
119 let (sx, sz) = if xz_len > 1e-10 {
120 (
121 self.radius * direction[0] / xz_len,
122 self.radius * direction[2] / xz_len,
123 )
124 } else {
125 (self.radius, 0.0)
126 };
127 let sy = self.half_height.copysign(direction[1]);
128 [sx, sy, sz]
129 }
130
131 pub fn signed_distance(&self, p: [f64; 3]) -> f64 {
134 let xz_len = (p[0] * p[0] + p[2] * p[2]).sqrt();
135 let dist_side = xz_len - self.radius;
136 let dist_cap = p[1].abs() - self.half_height;
137
138 if dist_side <= 0.0 && dist_cap <= 0.0 {
139 dist_side.max(dist_cap)
141 } else if dist_side > 0.0 && dist_cap > 0.0 {
142 (dist_side * dist_side + dist_cap * dist_cap).sqrt()
144 } else {
145 dist_side.max(dist_cap)
147 }
148 }
149
150 pub fn closest_point_to_segment(&self, a: [f64; 3], b: [f64; 3]) -> [f64; 3] {
153 let n_samples = 16;
156 let mut best_dist_sq = f64::INFINITY;
157 let mut best_cp = [0.0; 3];
158
159 for i in 0..=n_samples {
160 let t = i as f64 / n_samples as f64;
161 let seg_pt = [
162 a[0] + t * (b[0] - a[0]),
163 a[1] + t * (b[1] - a[1]),
164 a[2] + t * (b[2] - a[2]),
165 ];
166 let cp = self.closest_point(seg_pt);
167 let dx = cp[0] - seg_pt[0];
168 let dy = cp[1] - seg_pt[1];
169 let dz = cp[2] - seg_pt[2];
170 let d2 = dx * dx + dy * dy + dz * dz;
171 if d2 < best_dist_sq {
172 best_dist_sq = d2;
173 best_cp = cp;
174 }
175 }
176 best_cp
177 }
178
179 pub fn ray_cast_top_cap(
182 &self,
183 origin: [f64; 3],
184 direction: [f64; 3],
185 max_toi: f64,
186 ) -> Option<(f64, [f64; 3])> {
187 if direction[1].abs() < 1e-12 {
188 return None;
189 }
190 let t = (self.half_height - origin[1]) / direction[1];
191 if t < 0.0 || t > max_toi {
192 return None;
193 }
194 let px = origin[0] + t * direction[0];
195 let pz = origin[2] + t * direction[2];
196 if px * px + pz * pz <= self.radius * self.radius {
197 Some((t, [0.0, 1.0, 0.0]))
198 } else {
199 None
200 }
201 }
202
203 pub fn ray_cast_bottom_cap(
206 &self,
207 origin: [f64; 3],
208 direction: [f64; 3],
209 max_toi: f64,
210 ) -> Option<(f64, [f64; 3])> {
211 if direction[1].abs() < 1e-12 {
212 return None;
213 }
214 let t = (-self.half_height - origin[1]) / direction[1];
215 if t < 0.0 || t > max_toi {
216 return None;
217 }
218 let px = origin[0] + t * direction[0];
219 let pz = origin[2] + t * direction[2];
220 if px * px + pz * pz <= self.radius * self.radius {
221 Some((t, [0.0, -1.0, 0.0]))
222 } else {
223 None
224 }
225 }
226
227 pub fn ray_cast_lateral(
230 &self,
231 origin: [f64; 3],
232 direction: [f64; 3],
233 max_toi: f64,
234 ) -> Option<(f64, [f64; 3])> {
235 let a = direction[0] * direction[0] + direction[2] * direction[2];
236 if a < 1e-12 {
237 return None;
238 }
239 let b = 2.0 * (origin[0] * direction[0] + origin[2] * direction[2]);
240 let c = origin[0] * origin[0] + origin[2] * origin[2] - self.radius * self.radius;
241 let disc = b * b - 4.0 * a * c;
242 if disc < 0.0 {
243 return None;
244 }
245 let sqrt_disc = disc.sqrt();
246 for sign in [-1.0, 1.0] {
247 let t = (-b + sign * sqrt_disc) / (2.0 * a);
248 if t >= 0.0 && t <= max_toi {
249 let py = origin[1] + t * direction[1];
250 if py.abs() <= self.half_height {
251 let px = origin[0] + t * direction[0];
252 let pz = origin[2] + t * direction[2];
253 let xz_len = (px * px + pz * pz).sqrt();
254 let nx = if xz_len > 1e-12 { px / xz_len } else { 1.0 };
255 let nz = if xz_len > 1e-12 { pz / xz_len } else { 0.0 };
256 return Some((t, [nx, 0.0, nz]));
257 }
258 }
259 }
260 None
261 }
262
263 pub fn lateral_surface_area(&self) -> f64 {
265 2.0 * PI * self.radius * 2.0 * self.half_height
266 }
267
268 pub fn cap_area(&self) -> f64 {
270 PI * self.radius * self.radius
271 }
272
273 pub fn project_on_axis(&self, p: [f64; 3]) -> f64 {
276 p[1].clamp(-self.half_height, self.half_height)
277 }
278
279 pub fn distance_to_axis(&self, p: [f64; 3]) -> f64 {
281 let clamped_y = p[1].clamp(-self.half_height, self.half_height);
282 let dy = p[1] - clamped_y;
283 let xz2 = p[0] * p[0] + p[2] * p[2];
284 (xz2 + dy * dy).sqrt()
285 }
286
287 pub fn intersects_plane(&self, plane_normal: [f64; 3], plane_d: f64) -> bool {
294 let xz_proj =
298 (plane_normal[0] * plane_normal[0] + plane_normal[2] * plane_normal[2]).sqrt();
299 let y_contrib_top = plane_normal[1] * self.half_height;
300 let y_contrib_bot = -plane_normal[1] * self.half_height;
301 let r_contrib = self.radius * xz_proj;
302
303 let max_sd = y_contrib_top.max(y_contrib_bot) + r_contrib;
304 let min_sd = y_contrib_top.min(y_contrib_bot) - r_contrib;
305
306 min_sd <= plane_d && plane_d <= max_sd
307 }
308
309 pub fn sdf(&self, p: [f64; 3]) -> f64 {
313 self.signed_distance(p)
314 }
315
316 pub fn intersects_sphere(&self, sphere_center: [f64; 3], sphere_radius: f64) -> bool {
320 let cp = self.closest_point(sphere_center);
321 let dx = sphere_center[0] - cp[0];
322 let dy = sphere_center[1] - cp[1];
323 let dz = sphere_center[2] - cp[2];
324 dx * dx + dy * dy + dz * dz <= sphere_radius * sphere_radius
325 }
326
327 pub fn intersects_infinite_cylinder(&self, infinite_radius: f64, center_xz: [f64; 2]) -> bool {
333 let dist = (center_xz[0] * center_xz[0] + center_xz[1] * center_xz[1]).sqrt();
335 dist < self.radius + infinite_radius
337 }
338
339 pub fn random_surface_points(&self, n: usize, seed: u64) -> Vec<[f64; 3]> {
342 let mut points = Vec::with_capacity(n);
343 let lat = self.lateral_surface_area();
344 let cap = self.cap_area();
345 let total = lat + 2.0 * cap;
346 let p_lat = lat / total;
347 let p_top = cap / total;
348
349 let mut state = seed;
350 let next = |s: &mut u64| -> f64 {
351 *s ^= *s << 13;
352 *s ^= *s >> 7;
353 *s ^= *s << 17;
354 (*s as f64) / (u64::MAX as f64)
355 };
356
357 while points.len() < n {
358 let u = next(&mut state);
359 let v = next(&mut state);
360 let w = next(&mut state);
361
362 if u < p_lat {
363 let theta = v * 2.0 * PI;
364 let y = (w * 2.0 - 1.0) * self.half_height;
365 points.push([self.radius * theta.cos(), y, self.radius * theta.sin()]);
366 } else if u < p_lat + p_top {
367 let r = self.radius * v.sqrt();
369 let theta = w * 2.0 * PI;
370 points.push([r * theta.cos(), self.half_height, r * theta.sin()]);
371 } else {
372 let r = self.radius * v.sqrt();
374 let theta = w * 2.0 * PI;
375 points.push([r * theta.cos(), -self.half_height, r * theta.sin()]);
376 }
377 }
378 points
379 }
380
381 pub fn support_array(&self, direction: [f64; 3]) -> [f64; 3] {
383 self.support(direction)
384 }
385
386 pub fn full_height(&self) -> f64 {
388 2.0 * self.half_height
389 }
390
391 pub fn radius_of_gyration_y(&self) -> f64 {
395 self.radius / 2.0_f64.sqrt()
396 }
397
398 pub fn radius_of_gyration_x(&self, _mass: f64) -> f64 {
402 let r2 = self.radius * self.radius;
403 let h = 2.0 * self.half_height;
404 let h2 = h * h;
405 ((3.0 * r2 + h2) / 12.0).sqrt()
406 }
407
408 pub fn volume_swept(&self, delta: [f64; 3]) -> f64 {
415 let dist = (delta[0] * delta[0] + delta[1] * delta[1] + delta[2] * delta[2]).sqrt();
416 self.volume() + self.cap_area() * dist
417 }
418
419 pub fn cap_uv(&self, p: [f64; 3]) -> [f64; 2] {
424 let u = 0.5 + p[0] / (2.0 * self.radius);
425 let v = 0.5 + p[2] / (2.0 * self.radius);
426 [u.clamp(0.0, 1.0), v.clamp(0.0, 1.0)]
427 }
428
429 pub fn lateral_uv(&self, p: [f64; 3]) -> [f64; 2] {
434 let theta = p[2].atan2(p[0]); let u = ((theta / (2.0 * PI)) + 1.0) % 1.0;
436 let v = (p[1] + self.half_height) / (2.0 * self.half_height);
437 [u, v.clamp(0.0, 1.0)]
438 }
439
440 pub fn contains_point_hollow(&self, p: [f64; 3], inner_r: f64) -> bool {
445 let xz2 = p[0] * p[0] + p[2] * p[2];
446 let r_inner = inner_r.max(0.0).min(self.radius);
447 xz2 <= self.radius * self.radius
448 && xz2 >= r_inner * r_inner
449 && p[1].abs() <= self.half_height
450 }
451
452 pub fn volume_hollow(&self, inner_radius: f64) -> f64 {
456 let r = inner_radius.max(0.0).min(self.radius);
457 PI * (self.radius * self.radius - r * r) * 2.0 * self.half_height
458 }
459
460 pub fn frustum_slant_height(&self, r_top: f64, r_bottom: f64) -> f64 {
465 let h = 2.0 * self.half_height;
466 let dr = (r_top - r_bottom).abs();
467 (h * h + dr * dr).sqrt()
468 }
469
470 pub fn frustum_lateral_area(&self, r_top: f64, r_bottom: f64) -> f64 {
474 let slant = self.frustum_slant_height(r_top, r_bottom);
475 PI * (r_top + r_bottom) * slant
476 }
477
478 pub fn frustum_volume(&self, r_top: f64, r_bottom: f64) -> f64 {
482 let h = 2.0 * self.half_height;
483 PI * h * (r_top * r_top + r_top * r_bottom + r_bottom * r_bottom) / 3.0
484 }
485
486 pub fn oblique_cap_area(&self, tilt_angle: f64) -> f64 {
491 let cos_a = tilt_angle.cos().abs().max(1e-12);
492 PI * self.radius * self.radius / cos_a
493 }
494
495 pub fn ruled_surface_point(&self, u: f64, t: f64) -> [f64; 3] {
503 let y = -self.half_height + t * 2.0 * self.half_height;
504 [self.radius * u.cos(), y, self.radius * u.sin()]
505 }
506
507 pub fn stacked_volume(&self, other: &Cylinder) -> f64 {
513 self.volume() + other.volume()
514 }
515
516 pub fn stacked_half_height(&self, other: &Cylinder) -> f64 {
521 self.half_height + other.half_height
522 }
523
524 pub fn stacked_bounding_radius(&self, other: &Cylinder) -> f64 {
528 self.radius.max(other.radius)
529 }
530
531 pub fn lateral_normal_at(&self, p: [f64; 3]) -> [f64; 3] {
535 let len = (p[0] * p[0] + p[2] * p[2]).sqrt();
536 if len < 1e-12 {
537 return [1.0, 0.0, 0.0];
538 }
539 [p[0] / len, 0.0, p[2] / len]
540 }
541
542 pub fn sdf_gradient(&self, p: [f64; 3]) -> [f64; 3] {
547 let eps = 1e-5;
548 let sdf0 = self.sdf(p);
549 let gx = (self.sdf([p[0] + eps, p[1], p[2]]) - sdf0) / eps;
550 let gy = (self.sdf([p[0], p[1] + eps, p[2]]) - sdf0) / eps;
551 let gz = (self.sdf([p[0], p[1], p[2] + eps]) - sdf0) / eps;
552 let len = (gx * gx + gy * gy + gz * gz).sqrt().max(1e-14);
553 [gx / len, gy / len, gz / len]
554 }
555
556 pub fn lateral_ray_intersection_count(&self, origin: [f64; 3], direction: [f64; 3]) -> usize {
561 let a = direction[0] * direction[0] + direction[2] * direction[2];
562 if a < 1e-14 {
563 return 0;
564 }
565 let b = 2.0 * (origin[0] * direction[0] + origin[2] * direction[2]);
566 let c = origin[0] * origin[0] + origin[2] * origin[2] - self.radius * self.radius;
567 let disc = b * b - 4.0 * a * c;
568 if disc < 0.0 {
569 return 0;
570 }
571 if disc < 1e-14 {
572 return 1;
573 }
574 2
575 }
576
577 pub fn top_rim_points(&self, n: usize) -> Vec<[f64; 3]> {
581 let n = n.max(2);
582 (0..n)
583 .map(|i| {
584 let t = 2.0 * PI * i as f64 / n as f64;
585 [
586 self.radius * t.cos(),
587 self.half_height,
588 self.radius * t.sin(),
589 ]
590 })
591 .collect()
592 }
593}
594
595impl Shape for Cylinder {
596 fn bounding_box(&self) -> Aabb {
597 Aabb::new(
598 Vec3::new(-self.radius, -self.half_height, -self.radius),
599 Vec3::new(self.radius, self.half_height, self.radius),
600 )
601 }
602
603 fn support_point(&self, direction: &Vec3) -> Vec3 {
604 let xz_len = (direction.x * direction.x + direction.z * direction.z).sqrt();
605 let (sx, sz) = if xz_len > 1e-10 {
606 (
607 self.radius * direction.x / xz_len,
608 self.radius * direction.z / xz_len,
609 )
610 } else {
611 (self.radius, 0.0)
612 };
613 let sy = self.half_height.copysign(direction.y);
614 Vec3::new(sx, sy, sz)
615 }
616
617 fn volume(&self) -> Real {
618 PI * self.radius * self.radius * 2.0 * self.half_height
619 }
620
621 fn center_of_mass(&self) -> Vec3 {
622 Vec3::zeros()
623 }
624
625 fn inertia_tensor(&self, mass: Real) -> Mat3 {
626 let r2 = self.radius * self.radius;
627 let h2 = (2.0 * self.half_height).powi(2);
628 let iy = 0.5 * mass * r2;
629 let ixz = mass * (3.0 * r2 + h2) / 12.0;
630 Mat3::new(ixz, 0.0, 0.0, 0.0, iy, 0.0, 0.0, 0.0, ixz)
631 }
632
633 fn ray_cast(&self, ray_origin: &Vec3, ray_direction: &Vec3, max_toi: Real) -> Option<RayHit> {
634 let mut best: Option<RayHit> = None;
635
636 let a = ray_direction.x.powi(2) + ray_direction.z.powi(2);
638 let b = 2.0 * (ray_origin.x * ray_direction.x + ray_origin.z * ray_direction.z);
639 let c = ray_origin.x.powi(2) + ray_origin.z.powi(2) - self.radius.powi(2);
640
641 if a > 1e-12 {
642 let disc = b * b - 4.0 * a * c;
643 if disc >= 0.0 {
644 let sqrt_disc = disc.sqrt();
645 for sign in [-1.0, 1.0] {
646 let t = (-b + sign * sqrt_disc) / (2.0 * a);
647 if t >= 0.0 && t <= max_toi {
648 let p = ray_origin + ray_direction * t;
649 if p.y.abs() <= self.half_height {
650 let normal = Vec3::new(p.x, 0.0, p.z).normalize();
651 if best.as_ref().is_none_or(|prev| t < prev.toi) {
652 best = Some(RayHit {
653 point: p,
654 normal,
655 toi: t,
656 });
657 }
658 }
659 }
660 }
661 }
662 }
663
664 if ray_direction.y.abs() > 1e-12 {
666 for &cap_y in &[self.half_height, -self.half_height] {
667 let t = (cap_y - ray_origin.y) / ray_direction.y;
668 if t >= 0.0 && t <= max_toi {
669 let p = ray_origin + ray_direction * t;
670 if p.x.powi(2) + p.z.powi(2) <= self.radius.powi(2) {
671 let normal = Vec3::new(0.0, cap_y.signum(), 0.0);
672 if best.as_ref().is_none_or(|prev| t < prev.toi) {
673 best = Some(RayHit {
674 point: p,
675 normal,
676 toi: t,
677 });
678 }
679 }
680 }
681 }
682 }
683
684 best
685 }
686}
687
688#[cfg(test)]
689mod tests {
690 use super::*;
691
692 #[test]
693 fn test_cylinder_volume() {
694 let c = Cylinder::new(1.0, 1.0);
695 let expected = PI * 2.0;
696 assert!((c.volume() - expected).abs() < 1e-10);
697 }
698
699 #[test]
700 fn test_cylinder_surface_area() {
701 let c = Cylinder::new(1.0, 1.0);
703 let expected = 6.0 * PI;
704 assert!((c.surface_area() - expected).abs() < 1e-10);
705 }
706
707 #[test]
708 fn test_cylinder_inertia_symmetry() {
709 let c = Cylinder::new(1.5, 2.0);
710 let it = c.inertia_tensor(3.0);
711 let diff = (it[(0, 0)] - it[(2, 2)]).abs();
713 assert!(
714 diff < 1e-10,
715 "I_xx != I_zz: {} vs {}",
716 it[(0, 0)],
717 it[(2, 2)]
718 );
719 assert!(it[(0, 1)].abs() < 1e-10);
721 assert!(it[(1, 0)].abs() < 1e-10);
722 assert!(it[(0, 2)].abs() < 1e-10);
723 }
724
725 #[test]
726 fn test_cylinder_inertia_array() {
727 let c = Cylinder::new(1.0, 0.5);
728 let it = c.inertia_tensor_array(2.0);
729 assert!((it[1][1] - 1.0).abs() < 1e-10);
731 let h = 1.0_f64; let ixx = 2.0 * (3.0 + h * h) / 12.0;
734 assert!((it[0][0] - ixx).abs() < 1e-10);
735 }
736
737 #[test]
738 fn test_cylinder_raycast() {
739 let c = Cylinder::new(1.0, 2.0);
740 let origin = Vec3::new(-5.0, 0.0, 0.0);
741 let dir = Vec3::new(1.0, 0.0, 0.0);
742 let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
743 assert!((hit.toi - 4.0).abs() < 1e-10);
744 }
745
746 #[test]
747 fn test_cylinder_raycast_array() {
748 let c = Cylinder::new(1.0, 2.0);
749 let (t, n) = c
750 .ray_cast_array([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0)
751 .unwrap();
752 assert!((t - 4.0).abs() < 1e-10);
753 assert!((n[0] + 1.0).abs() < 1e-10); }
755
756 #[test]
757 fn test_cylinder_raycast_cap() {
758 let c = Cylinder::new(1.0, 2.0);
759 let origin = Vec3::new(0.0, 10.0, 0.0);
760 let dir = Vec3::new(0.0, -1.0, 0.0);
761 let hit = c.ray_cast(&origin, &dir, 100.0).unwrap();
762 assert!((hit.toi - 8.0).abs() < 1e-10);
763 assert!((hit.normal.y - 1.0).abs() < 1e-10);
764 }
765
766 #[test]
767 fn test_cylinder_contains_point() {
768 let c = Cylinder::new(1.0, 2.0);
769 assert!(c.contains_point([0.0, 0.0, 0.0]));
770 assert!(c.contains_point([0.9, 1.9, 0.0]));
771 assert!(!c.contains_point([0.0, 2.1, 0.0]));
772 assert!(!c.contains_point([1.1, 0.0, 0.0]));
773 }
774
775 #[test]
776 fn test_cylinder_closest_point_outside_side() {
777 let c = Cylinder::new(1.0, 2.0);
778 let cp = c.closest_point([3.0, 0.0, 0.0]);
779 assert!((cp[0] - 1.0).abs() < 1e-10);
780 assert!((cp[1]).abs() < 1e-10);
781 assert!((cp[2]).abs() < 1e-10);
782 }
783
784 #[test]
785 fn test_cylinder_closest_point_above_cap() {
786 let c = Cylinder::new(1.0, 2.0);
787 let cp = c.closest_point([0.0, 5.0, 0.0]);
788 assert!((cp[1] - 2.0).abs() < 1e-10);
789 }
790
791 #[test]
794 fn test_cylinder_support_array() {
795 let c = Cylinder::new(2.0, 3.0);
796 let sp = c.support([1.0, 0.0, 0.0]);
797 assert!((sp[0] - 2.0).abs() < 1e-10);
798 assert!((sp[1] - 3.0).abs() < 1e-10); }
800
801 #[test]
802 fn test_cylinder_support_negative_y() {
803 let c = Cylinder::new(1.0, 2.0);
804 let sp = c.support([0.0, -1.0, 0.0]);
805 assert!((sp[1] + 2.0).abs() < 1e-10);
806 }
807
808 #[test]
809 fn test_cylinder_support_diagonal() {
810 let c = Cylinder::new(1.0, 2.0);
811 let sp = c.support([1.0, 1.0, 0.0]);
812 assert!((sp[0] - 1.0).abs() < 1e-10); assert!((sp[1] - 2.0).abs() < 1e-10); }
815
816 #[test]
817 fn test_cylinder_signed_distance_inside() {
818 let c = Cylinder::new(2.0, 3.0);
819 let d = c.signed_distance([0.0, 0.0, 0.0]);
820 assert!(d < 0.0); assert!((d + 2.0).abs() < 1e-10);
823 }
824
825 #[test]
826 fn test_cylinder_signed_distance_outside_side() {
827 let c = Cylinder::new(1.0, 2.0);
828 let d = c.signed_distance([3.0, 0.0, 0.0]);
829 assert!((d - 2.0).abs() < 1e-10); }
831
832 #[test]
833 fn test_cylinder_signed_distance_outside_cap() {
834 let c = Cylinder::new(1.0, 2.0);
835 let d = c.signed_distance([0.0, 4.0, 0.0]);
836 assert!((d - 2.0).abs() < 1e-10); }
838
839 #[test]
840 fn test_cylinder_signed_distance_outside_corner() {
841 let c = Cylinder::new(1.0, 1.0);
842 let d = c.signed_distance([2.0, 2.0, 0.0]);
844 assert!((d - 2.0_f64.sqrt()).abs() < 1e-10);
846 }
847
848 #[test]
849 fn test_cylinder_ray_cast_top_cap() {
850 let c = Cylinder::new(1.0, 2.0);
851 let result = c.ray_cast_top_cap([0.0, 5.0, 0.0], [0.0, -1.0, 0.0], 100.0);
852 assert!(result.is_some());
853 let (t, n) = result.unwrap();
854 assert!((t - 3.0).abs() < 1e-10);
855 assert!((n[1] - 1.0).abs() < 1e-10);
856 }
857
858 #[test]
859 fn test_cylinder_ray_cast_top_cap_miss() {
860 let c = Cylinder::new(1.0, 2.0);
861 let result = c.ray_cast_top_cap([3.0, 5.0, 0.0], [0.0, -1.0, 0.0], 100.0);
863 assert!(result.is_none());
864 }
865
866 #[test]
867 fn test_cylinder_ray_cast_bottom_cap() {
868 let c = Cylinder::new(1.0, 2.0);
869 let result = c.ray_cast_bottom_cap([0.0, -5.0, 0.0], [0.0, 1.0, 0.0], 100.0);
870 assert!(result.is_some());
871 let (t, n) = result.unwrap();
872 assert!((t - 3.0).abs() < 1e-10);
873 assert!((n[1] + 1.0).abs() < 1e-10);
874 }
875
876 #[test]
877 fn test_cylinder_ray_cast_lateral() {
878 let c = Cylinder::new(1.0, 2.0);
879 let result = c.ray_cast_lateral([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0], 100.0);
880 assert!(result.is_some());
881 let (t, n) = result.unwrap();
882 assert!((t - 4.0).abs() < 1e-10);
883 assert!((n[0] + 1.0).abs() < 1e-10);
884 }
885
886 #[test]
887 fn test_cylinder_ray_cast_lateral_miss_height() {
888 let c = Cylinder::new(1.0, 1.0);
889 let result = c.ray_cast_lateral([-5.0, 5.0, 0.0], [1.0, 0.0, 0.0], 100.0);
891 assert!(result.is_none());
892 }
893
894 #[test]
895 fn test_cylinder_lateral_surface_area() {
896 let c = Cylinder::new(1.0, 1.0);
897 assert!((c.lateral_surface_area() - 4.0 * PI).abs() < 1e-10);
899 }
900
901 #[test]
902 fn test_cylinder_cap_area() {
903 let c = Cylinder::new(2.0, 1.0);
904 assert!((c.cap_area() - 4.0 * PI).abs() < 1e-10);
905 }
906
907 #[test]
908 fn test_cylinder_surface_area_decomposition() {
909 let c = Cylinder::new(1.5, 2.5);
910 let total = c.surface_area();
911 let decomposed = c.lateral_surface_area() + 2.0 * c.cap_area();
912 assert!((total - decomposed).abs() < 1e-10);
913 }
914
915 #[test]
916 fn test_cylinder_project_on_axis() {
917 let c = Cylinder::new(1.0, 2.0);
918 assert!((c.project_on_axis([0.0, 5.0, 0.0]) - 2.0).abs() < 1e-10);
919 assert!((c.project_on_axis([0.0, -5.0, 0.0]) + 2.0).abs() < 1e-10);
920 assert!((c.project_on_axis([3.0, 1.0, 0.0]) - 1.0).abs() < 1e-10);
921 }
922
923 #[test]
924 fn test_cylinder_distance_to_axis() {
925 let c = Cylinder::new(1.0, 2.0);
926 assert!(c.distance_to_axis([0.0, 0.0, 0.0]).abs() < 1e-10);
928 assert!((c.distance_to_axis([3.0, 0.0, 0.0]) - 3.0).abs() < 1e-10);
930 assert!((c.distance_to_axis([0.0, 5.0, 0.0]) - 3.0).abs() < 1e-10);
932 }
933
934 #[test]
935 fn test_cylinder_closest_point_to_segment() {
936 let c = Cylinder::new(1.0, 2.0);
937 let cp = c.closest_point_to_segment([5.0, 0.0, 0.0], [5.0, 1.0, 0.0]);
939 assert!((cp[0] - 1.0).abs() < 1e-10);
940 }
941
942 #[test]
943 fn test_cylinder_volume_explicit_matches() {
944 let c = Cylinder::new(1.5, 2.5);
945 assert!((c.volume_explicit() - c.volume()).abs() < 1e-10);
946 }
947
948 #[test]
951 fn test_cylinder_intersects_plane_through_center() {
952 let c = Cylinder::new(1.0, 2.0);
953 assert!(c.intersects_plane([0.0, 1.0, 0.0], 0.0));
955 }
956
957 #[test]
958 fn test_cylinder_intersects_plane_above() {
959 let c = Cylinder::new(1.0, 2.0);
960 assert!(!c.intersects_plane([0.0, 1.0, 0.0], 10.0));
962 }
963
964 #[test]
965 fn test_cylinder_intersects_plane_lateral() {
966 let c = Cylinder::new(2.0, 1.0);
967 assert!(c.intersects_plane([1.0, 0.0, 0.0], 1.0));
969 assert!(!c.intersects_plane([1.0, 0.0, 0.0], 3.0));
971 }
972
973 #[test]
974 fn test_cylinder_sdf_matches_signed_distance() {
975 let c = Cylinder::new(1.0, 2.0);
976 let p = [3.0, 0.0, 0.0];
977 assert!((c.sdf(p) - c.signed_distance(p)).abs() < 1e-12);
978 }
979
980 #[test]
981 fn test_cylinder_intersects_sphere_yes() {
982 let c = Cylinder::new(1.0, 2.0);
983 assert!(c.intersects_sphere([0.0, 0.0, 0.0], 0.5));
984 assert!(c.intersects_sphere([1.5, 0.0, 0.0], 0.6)); }
986
987 #[test]
988 fn test_cylinder_intersects_sphere_no() {
989 let c = Cylinder::new(1.0, 2.0);
990 assert!(!c.intersects_sphere([5.0, 0.0, 0.0], 0.5));
991 }
992
993 #[test]
994 fn test_cylinder_intersects_infinite_cylinder_overlap() {
995 let c = Cylinder::new(1.0, 2.0);
996 assert!(c.intersects_infinite_cylinder(1.0, [0.0, 0.0]));
998 }
999
1000 #[test]
1001 fn test_cylinder_intersects_infinite_cylinder_no_overlap() {
1002 let c = Cylinder::new(1.0, 2.0);
1003 assert!(!c.intersects_infinite_cylinder(0.5, [5.0, 0.0]));
1005 }
1006
1007 #[test]
1008 fn test_cylinder_random_surface_points_count() {
1009 let c = Cylinder::new(1.0, 2.0);
1010 let pts = c.random_surface_points(50, 12345);
1011 assert_eq!(pts.len(), 50);
1012 }
1013
1014 #[test]
1015 fn test_cylinder_random_surface_points_on_surface() {
1016 let c = Cylinder::new(1.0, 2.0);
1017 let pts = c.random_surface_points(80, 99);
1018 for p in &pts {
1019 let sdf = c.sdf(*p);
1020 assert!(sdf.abs() < 0.01, "point {:?} has sdf={sdf}", p);
1021 }
1022 }
1023
1024 #[test]
1025 fn test_cylinder_support_array_matches_support() {
1026 let c = Cylinder::new(2.0, 3.0);
1027 let d = [1.0, 1.0, 0.0];
1028 let a = c.support_array(d);
1029 let b = c.support(d);
1030 assert_eq!(a, b);
1031 }
1032
1033 #[test]
1034 fn test_cylinder_full_height() {
1035 let c = Cylinder::new(1.0, 3.0);
1036 assert!((c.full_height() - 6.0).abs() < 1e-12);
1037 }
1038
1039 #[test]
1040 fn test_cylinder_radius_of_gyration_y() {
1041 let c = Cylinder::new(2.0, 1.0);
1042 assert!((c.radius_of_gyration_y() - 2.0_f64.sqrt()).abs() < 1e-12);
1044 }
1045
1046 #[test]
1047 fn test_cylinder_radius_of_gyration_x() {
1048 let c = Cylinder::new(1.0, 1.0); let expected = (7.0_f64 / 12.0).sqrt();
1051 assert!((c.radius_of_gyration_x(1.0) - expected).abs() < 1e-12);
1052 }
1053
1054 #[test]
1057 fn test_cylinder_volume_swept_zero_delta() {
1058 let c = Cylinder::new(1.0, 2.0);
1059 let swept = c.volume_swept([0.0, 0.0, 0.0]);
1061 assert!((swept - c.volume()).abs() < 1e-10);
1062 }
1063
1064 #[test]
1065 fn test_cylinder_volume_swept_positive_delta() {
1066 let c = Cylinder::new(1.0, 1.0);
1067 let swept = c.volume_swept([1.0, 0.0, 0.0]);
1068 assert!(swept > c.volume());
1070 assert!((swept - c.volume() - PI).abs() < 1e-10);
1071 }
1072
1073 #[test]
1074 fn test_cylinder_cap_uv_center() {
1075 let c = Cylinder::new(2.0, 1.0);
1076 let uv = c.cap_uv([0.0, 2.0, 0.0]);
1077 assert!((uv[0] - 0.5).abs() < 1e-10);
1078 assert!((uv[1] - 0.5).abs() < 1e-10);
1079 }
1080
1081 #[test]
1082 fn test_cylinder_cap_uv_edge() {
1083 let c = Cylinder::new(2.0, 1.0);
1084 let uv = c.cap_uv([2.0, 1.0, 0.0]);
1086 assert!((uv[0] - 1.0).abs() < 1e-10);
1087 assert!((uv[1] - 0.5).abs() < 1e-10);
1088 }
1089
1090 #[test]
1091 fn test_cylinder_lateral_uv_bottom_left() {
1092 let c = Cylinder::new(1.0, 1.0);
1093 let uv = c.lateral_uv([1.0, -1.0, 0.0]);
1095 assert!(uv[1].abs() < 1e-10, "v={}", uv[1]);
1096 }
1097
1098 #[test]
1099 fn test_cylinder_lateral_uv_top() {
1100 let c = Cylinder::new(1.0, 1.0);
1101 let uv = c.lateral_uv([1.0, 1.0, 0.0]);
1102 assert!((uv[1] - 1.0).abs() < 1e-10, "v={}", uv[1]);
1103 }
1104
1105 #[test]
1106 fn test_cylinder_hollow_inside_ring() {
1107 let c = Cylinder::new(2.0, 1.0);
1108 assert!(c.contains_point_hollow([1.5, 0.0, 0.0], 1.0));
1110 }
1111
1112 #[test]
1113 fn test_cylinder_hollow_outside_outer() {
1114 let c = Cylinder::new(2.0, 1.0);
1115 assert!(!c.contains_point_hollow([3.0, 0.0, 0.0], 1.0));
1116 }
1117
1118 #[test]
1119 fn test_cylinder_hollow_inside_inner_hole() {
1120 let c = Cylinder::new(2.0, 1.0);
1121 assert!(!c.contains_point_hollow([0.5, 0.0, 0.0], 1.0));
1123 }
1124
1125 #[test]
1126 fn test_cylinder_volume_hollow_full_solid() {
1127 let c = Cylinder::new(1.0, 1.0);
1129 assert!((c.volume_hollow(0.0) - c.volume()).abs() < 1e-10);
1130 }
1131
1132 #[test]
1133 fn test_cylinder_volume_hollow_positive() {
1134 let c = Cylinder::new(2.0, 1.0);
1135 let v = c.volume_hollow(1.0);
1136 assert!(v > 0.0);
1137 assert!((v - 6.0 * PI).abs() < 1e-10);
1139 }
1140
1141 #[test]
1142 fn test_cylinder_frustum_slant_height_equal_radii() {
1143 let c = Cylinder::new(1.0, 1.0);
1144 let sl = c.frustum_slant_height(1.0, 1.0);
1146 assert!((sl - 2.0).abs() < 1e-10);
1147 }
1148
1149 #[test]
1150 fn test_cylinder_frustum_slant_height_cone() {
1151 let c = Cylinder::new(1.0, 1.0);
1152 let sl = c.frustum_slant_height(0.0, 1.0);
1154 assert!((sl - 5.0_f64.sqrt()).abs() < 1e-10);
1155 }
1156
1157 #[test]
1158 fn test_cylinder_frustum_lateral_area() {
1159 let c = Cylinder::new(1.0, 1.0); let area = c.frustum_lateral_area(1.0, 1.0);
1162 assert!((area - 4.0 * PI).abs() < 1e-10);
1163 }
1164
1165 #[test]
1166 fn test_cylinder_frustum_volume_cylinder() {
1167 let c = Cylinder::new(1.0, 1.0);
1168 let fv = c.frustum_volume(1.0, 1.0);
1170 assert!((fv - c.volume()).abs() < 1e-10);
1171 }
1172
1173 #[test]
1174 fn test_cylinder_frustum_volume_cone() {
1175 let c = Cylinder::new(1.0, 1.0);
1176 let fv = c.frustum_volume(0.0, 1.0);
1178 let expected = PI * 1.0 * 2.0 / 3.0;
1179 assert!((fv - expected).abs() < 1e-10);
1180 }
1181
1182 #[test]
1183 fn test_cylinder_oblique_cap_area_zero_tilt() {
1184 let c = Cylinder::new(1.0, 1.0);
1185 assert!((c.oblique_cap_area(0.0) - PI).abs() < 1e-10);
1187 }
1188
1189 #[test]
1190 fn test_cylinder_oblique_cap_area_larger_at_tilt() {
1191 let c = Cylinder::new(1.0, 1.0);
1192 assert!(c.oblique_cap_area(0.5) > c.oblique_cap_area(0.0));
1193 }
1194
1195 #[test]
1196 fn test_cylinder_ruled_surface_point_on_surface() {
1197 let c = Cylinder::new(1.0, 2.0);
1198 for i in 0..8 {
1200 let u = 2.0 * PI * i as f64 / 8.0;
1201 for j in 0..=4 {
1202 let t = j as f64 / 4.0;
1203 let p = c.ruled_surface_point(u, t);
1204 let sdf = c.sdf(p);
1205 assert!(sdf.abs() < 1e-9, "ruled sdf={sdf}");
1206 }
1207 }
1208 }
1209
1210 #[test]
1211 fn test_cylinder_stacked_volume() {
1212 let c1 = Cylinder::new(1.0, 1.0);
1213 let c2 = Cylinder::new(1.0, 2.0);
1214 let combined = c1.stacked_volume(&c2);
1215 assert!((combined - c1.volume() - c2.volume()).abs() < 1e-10);
1216 }
1217
1218 #[test]
1219 fn test_cylinder_stacked_half_height() {
1220 let c1 = Cylinder::new(1.0, 1.0);
1221 let c2 = Cylinder::new(1.0, 2.0);
1222 assert!((c1.stacked_half_height(&c2) - 3.0).abs() < 1e-12);
1223 }
1224
1225 #[test]
1226 fn test_cylinder_stacked_bounding_radius() {
1227 let c1 = Cylinder::new(1.0, 1.0);
1228 let c2 = Cylinder::new(3.0, 0.5);
1229 assert!((c1.stacked_bounding_radius(&c2) - 3.0).abs() < 1e-12);
1230 }
1231
1232 #[test]
1233 fn test_cylinder_lateral_normal_at_x_axis() {
1234 let c = Cylinder::new(1.0, 2.0);
1235 let n = c.lateral_normal_at([1.0, 0.5, 0.0]);
1236 assert!((n[0] - 1.0).abs() < 1e-10);
1237 assert!(n[1].abs() < 1e-12);
1238 assert!(n[2].abs() < 1e-12);
1239 }
1240
1241 #[test]
1242 fn test_cylinder_lateral_normal_unit() {
1243 let c = Cylinder::new(1.0, 2.0);
1244 let n = c.lateral_normal_at([0.6, 1.0, 0.8]);
1245 let len = (n[0] * n[0] + n[1] * n[1] + n[2] * n[2]).sqrt();
1246 assert!((len - 1.0).abs() < 1e-9);
1247 }
1248
1249 #[test]
1250 fn test_cylinder_sdf_gradient_outside_points_outward() {
1251 let c = Cylinder::new(1.0, 2.0);
1252 let g = c.sdf_gradient([3.0, 0.0, 0.0]);
1253 assert!(g[0] > 0.5, "gx={}", g[0]);
1255 }
1256
1257 #[test]
1258 fn test_cylinder_lateral_ray_intersection_count_two() {
1259 let c = Cylinder::new(1.0, 2.0);
1260 let count = c.lateral_ray_intersection_count([-5.0, 0.0, 0.0], [1.0, 0.0, 0.0]);
1261 assert_eq!(count, 2);
1262 }
1263
1264 #[test]
1265 fn test_cylinder_lateral_ray_intersection_count_miss() {
1266 let c = Cylinder::new(1.0, 2.0);
1267 let count = c.lateral_ray_intersection_count([0.0, 5.0, 0.0], [0.0, 1.0, 0.0]);
1268 assert_eq!(count, 0);
1269 }
1270
1271 #[test]
1272 fn test_cylinder_top_rim_points_count() {
1273 let c = Cylinder::new(1.0, 2.0);
1274 let pts = c.top_rim_points(16);
1275 assert_eq!(pts.len(), 16);
1276 }
1277
1278 #[test]
1279 fn test_cylinder_top_rim_points_at_top() {
1280 let c = Cylinder::new(1.0, 2.0);
1281 let pts = c.top_rim_points(8);
1282 for p in &pts {
1283 assert!((p[1] - c.half_height).abs() < 1e-12);
1284 let xz = (p[0] * p[0] + p[2] * p[2]).sqrt();
1285 assert!((xz - c.radius).abs() < 1e-9);
1286 }
1287 }
1288}