1use alloc::vec::Vec;
2use core::fmt;
3#[cfg(not(feature = "std"))]
4use crate::F32Ext;
5
6use glam::{DMat3, DVec3, Vec3};
7use wide::{CmpLe, f32x8};
8
9use inherent::inherent;
10
11use crate::ConvexPolytope;
12use crate::Stretchable;
13use crate::capsule::Capsule;
14use crate::cuboid::Cuboid;
15use crate::line::{Line, LineSegment, Ray};
16use crate::plane::{ConvexPolygon, Plane};
17use crate::point::Point;
18use crate::sphere::Sphere;
19use crate::wreck_assert;
20use crate::{Bounded, Collides, Scalable, Transformable};
21
22#[derive(Debug, Clone, Copy, PartialEq)]
23#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
24pub struct Cylinder {
25 pub p1: Vec3,
26 pub dir: Vec3,
27 pub radius: f32,
28 pub(crate) rdv: f32,
29 pub(crate) z_aligned: bool,
30}
31
32impl Cylinder {
33 pub const fn new(p1: Vec3, p2: Vec3, radius: f32) -> Self {
34 wreck_assert!(radius >= 0.0, "Cylinder radius must be non-negative");
35 let dir = Vec3::new(p2.x - p1.x, p2.y - p1.y, p2.z - p1.z);
36 let len_sq = crate::dot(dir, dir);
37 let rdv = if len_sq > f32::EPSILON {
38 1.0 / len_sq
39 } else {
40 0.0
41 };
42 let z_aligned = dir.x == 0.0 && dir.y == 0.0;
43 Self {
44 p1,
45 dir,
46 radius,
47 rdv,
48 z_aligned,
49 }
50 }
51
52 pub fn from_center_orientation(
55 center: DVec3,
56 orientation: DMat3,
57 radius: f64,
58 length: f64,
59 ) -> Self {
60 let half_axis = orientation * DVec3::new(0.0, length * 0.5, 0.0);
61 let p1 = (center - half_axis).as_vec3();
62 let p2 = (center + half_axis).as_vec3();
63 Self::new(p1, p2, radius as f32)
64 }
65
66 pub fn to_center_orientation_length(&self) -> (DVec3, DMat3, f64) {
68 let p1 = self.p1.as_dvec3();
69 let p2 = self.p2().as_dvec3();
70 let center = (p1 + p2) * 0.5;
71 let dir = p2 - p1;
72 let length = dir.length();
73 let orientation = if length > f64::EPSILON {
74 let y_axis = dir / length;
75 let arbitrary = if y_axis.dot(DVec3::X).abs() < 0.9 {
76 DVec3::X
77 } else {
78 DVec3::Z
79 };
80 let x_axis = y_axis.cross(arbitrary).normalize();
81 let z_axis = x_axis.cross(y_axis).normalize();
82 DMat3::from_cols(x_axis, y_axis, z_axis)
83 } else {
84 DMat3::IDENTITY
85 };
86 (center, orientation, length)
87 }
88
89 #[inline]
91 pub fn length(&self) -> f32 {
92 self.dir.length()
93 }
94
95 #[inline]
96 pub fn p2(&self) -> Vec3 {
97 self.p1 + self.dir
98 }
99
100 #[inline]
101 pub fn bounding_sphere(&self) -> (Vec3, f32) {
102 let center = self.p1 + 0.5 * self.dir;
103 let half_len = if self.z_aligned {
104 self.dir.z.abs() * 0.5
105 } else {
106 self.dir.length() * 0.5
107 };
108 (center, half_len + self.radius)
109 }
110
111 #[inline]
113 pub fn point_dist_sq(&self, p: Vec3) -> f32 {
114 let w = p - self.p1;
115 let t = w.dot(self.dir) * self.rdv;
116 let along = self.dir * t;
117 let perp = w - along;
118 let r_sq = perp.dot(perp);
119 let cyl_r_sq = self.radius * self.radius;
120
121 let t_c = t.clamp(0.0, 1.0);
122 let t_excess = t - t_c;
123 let d_axial_sq = t_excess * t_excess * self.dir.dot(self.dir);
124
125 if r_sq <= cyl_r_sq {
126 d_axial_sq
127 } else {
128 let r = r_sq.sqrt();
129 let d_radial = r - self.radius;
130 d_radial * d_radial + d_axial_sq
131 }
132 }
133
134 #[inline]
136 pub fn contains_point(&self, p: Vec3) -> bool {
137 let w = p - self.p1;
138 let t = w.dot(self.dir) * self.rdv;
139 if t < 0.0 || t > 1.0 {
140 return false;
141 }
142 let along = self.dir * t;
143 let perp = w - along;
144 perp.dot(perp) <= self.radius * self.radius
145 }
146}
147
148#[inherent]
151impl Bounded for Cylinder {
152 pub fn broadphase(&self) -> Sphere {
153 let (center, radius) = self.bounding_sphere();
154 Sphere::new(center, radius)
155 }
156
157 pub fn obb(&self) -> Cuboid {
158 let center = self.p1 + 0.5 * self.dir;
159 let dir_len = self.dir.length();
160 if dir_len < f32::EPSILON {
161 return Cuboid::new(
162 center,
163 [Vec3::X, Vec3::Y, Vec3::Z],
164 [self.radius, self.radius, self.radius],
165 );
166 }
167 let ax0 = self.dir / dir_len;
168 let ref_vec = if ax0.x.abs() < 0.9 { Vec3::X } else { Vec3::Y };
169 let ax1 = ax0.cross(ref_vec).normalize();
170 let ax2 = ax0.cross(ax1);
171 Cuboid::new(
172 center,
173 [ax0, ax1, ax2],
174 [dir_len * 0.5, self.radius, self.radius],
175 )
176 }
177
178 pub fn aabb(&self) -> Cuboid {
179 let p2 = self.p1 + self.dir;
180 let min = self.p1.min(p2) - Vec3::splat(self.radius);
181 let max = self.p1.max(p2) + Vec3::splat(self.radius);
182 Cuboid::from_aabb(min, max)
183 }
184}
185
186#[inherent]
187impl Scalable for Cylinder {
188 pub fn scale(&mut self, factor: f32) {
189 self.dir *= factor;
190 self.radius *= factor;
191 let len_sq = self.dir.dot(self.dir);
192 self.rdv = if len_sq > f32::EPSILON {
193 1.0 / len_sq
194 } else {
195 0.0
196 };
197 }
198}
199
200#[inherent]
201impl Transformable for Cylinder {
202 pub fn translate(&mut self, offset: glam::Vec3A) {
203 self.p1 = Vec3::from(glam::Vec3A::from(self.p1) + offset);
204 }
205
206 pub fn rotate_mat(&mut self, mat: glam::Mat3A) {
207 self.p1 = Vec3::from(mat * glam::Vec3A::from(self.p1));
208 self.dir = Vec3::from(mat * glam::Vec3A::from(self.dir));
209 self.z_aligned = self.dir.x == 0.0 && self.dir.y == 0.0;
210 }
211
212 pub fn rotate_quat(&mut self, quat: glam::Quat) {
213 self.p1 = quat * self.p1;
214 self.dir = quat * self.dir;
215 self.z_aligned = self.dir.x == 0.0 && self.dir.y == 0.0;
216 }
217
218 pub fn transform(&mut self, mat: glam::Affine3A) {
219 self.p1 = Vec3::from(mat.transform_point3a(glam::Vec3A::from(self.p1)));
220 self.dir = Vec3::from(mat.matrix3 * glam::Vec3A::from(self.dir));
221 let len_sq = self.dir.dot(self.dir);
222 self.rdv = if len_sq > f32::EPSILON {
223 1.0 / len_sq
224 } else {
225 0.0
226 };
227 self.z_aligned = self.dir.x == 0.0 && self.dir.y == 0.0;
228 }
229}
230
231#[inline]
234fn cylinder_sphere_collides(cyl: &Cylinder, sphere: &Sphere) -> bool {
235 let w = sphere.center - cyl.p1;
236 let t = w.dot(cyl.dir) * cyl.rdv;
237 let along = cyl.dir * t;
238 let perp = w - along;
239 let r_sq = perp.dot(perp);
240
241 if t >= 0.0 && t <= 1.0 {
242 let combined = cyl.radius + sphere.radius;
243 r_sq <= combined * combined
244 } else {
245 let t_c = if t < 0.0 { 0.0 } else { 1.0 };
246 let t_excess = t - t_c;
247 let dir_sq = cyl.dir.dot(cyl.dir);
248 let d_axial_sq = t_excess * t_excess * dir_sq;
249 let sr_sq = sphere.radius * sphere.radius;
250 let cyl_r_sq = cyl.radius * cyl.radius;
251
252 if r_sq <= cyl_r_sq {
253 d_axial_sq <= sr_sq
254 } else {
255 let l = r_sq + cyl_r_sq + d_axial_sq - sr_sq;
260 l <= 0.0 || l * l <= 4.0 * cyl_r_sq * r_sq
261 }
262 }
263}
264
265impl Collides<Sphere> for Cylinder {
266 #[inline]
267 fn test<const BROADPHASE: bool>(&self, other: &Sphere) -> bool {
268 cylinder_sphere_collides(self, other)
269 }
270}
271
272impl Collides<Cylinder> for Sphere {
273 #[inline]
274 fn test<const BROADPHASE: bool>(&self, other: &Cylinder) -> bool {
275 cylinder_sphere_collides(other, self)
276 }
277}
278
279impl Collides<Point> for Cylinder {
282 #[inline]
283 fn test<const BROADPHASE: bool>(&self, point: &Point) -> bool {
284 self.contains_point(point.0)
285 }
286}
287
288impl Collides<Cylinder> for Point {
289 #[inline]
290 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
291 cyl.contains_point(self.0)
292 }
293}
294
295#[inline]
299fn cylinder_capsule_collides<const BROADPHASE: bool>(cyl: &Cylinder, capsule: &Capsule) -> bool {
300 if BROADPHASE {
301 let (bc, br) = cyl.bounding_sphere();
302 let (cc, cr) = capsule.bounding_sphere();
303 let d = bc - cc;
304 let max_r = br + cr;
305 if d.dot(d) > max_r * max_r {
306 return false;
307 }
308 }
309
310 let cr_sq = capsule.radius * capsule.radius;
311
312 let r = capsule.p1 - cyl.p1;
315 let a = capsule.dir.dot(capsule.dir);
316 let e = cyl.dir.dot(cyl.dir);
317 let f = cyl.dir.dot(r);
318 let eps = f32::EPSILON;
319
320 let s_closest = if a > eps {
321 let c_val = capsule.dir.dot(r);
322 if e > eps {
323 let b = capsule.dir.dot(cyl.dir);
324 let denom = a * e - b * b;
325 if denom.abs() > eps {
326 ((b * f - c_val * e) / denom).clamp(0.0, 1.0)
327 } else {
328 0.5
329 }
330 } else {
331 (-c_val / a).clamp(0.0, 1.0)
332 }
333 } else {
334 0.0
335 };
336
337 let cap_dot_cyl = capsule.dir.dot(cyl.dir);
343 let r_dot_cyl = r.dot(cyl.dir);
344 let (s_t0, s_t1) = if cap_dot_cyl.abs() > eps {
345 let inv = 1.0 / cap_dot_cyl;
346 (
347 (-r_dot_cyl * inv).clamp(0.0, 1.0),
348 ((e - r_dot_cyl) * inv).clamp(0.0, 1.0),
349 )
350 } else {
351 (0.0, 1.0)
352 };
353
354 let samples = f32x8::new([0.0, 1.0, s_closest, s_t0, s_t1, 0.25, 0.5, 0.75]);
355
356 let px = f32x8::splat(capsule.p1.x) + f32x8::splat(capsule.dir.x) * samples;
359 let py = f32x8::splat(capsule.p1.y) + f32x8::splat(capsule.dir.y) * samples;
360 let pz = f32x8::splat(capsule.p1.z) + f32x8::splat(capsule.dir.z) * samples;
361
362 let wx = px - f32x8::splat(cyl.p1.x);
364 let wy = py - f32x8::splat(cyl.p1.y);
365 let wz = pz - f32x8::splat(cyl.p1.z);
366
367 let cdx = f32x8::splat(cyl.dir.x);
368 let cdy = f32x8::splat(cyl.dir.y);
369 let cdz = f32x8::splat(cyl.dir.z);
370 let crdv = f32x8::splat(cyl.rdv);
371
372 let t = (wx * cdx + wy * cdy + wz * cdz) * crdv;
374 let zero = f32x8::splat(0.0);
375 let one = f32x8::splat(1.0);
376 let t_c = t.max(zero).min(one);
377
378 let perpx = wx - cdx * t;
380 let perpy = wy - cdy * t;
381 let perpz = wz - cdz * t;
382 let r_sq = perpx * perpx + perpy * perpy + perpz * perpz;
383
384 let t_excess = t - t_c;
386 let dir_sq = f32x8::splat(e);
387 let d_axial_sq = t_excess * t_excess * dir_sq;
388
389 let cyl_r = f32x8::splat(cyl.radius);
390 let cyl_r_sq = cyl_r * cyl_r;
391 let cap_r_sq = f32x8::splat(cr_sq);
392
393 let in_barrel = t.simd_le(one) & t.simd_le(one.max(t)) & zero.simd_le(t);
395 let combined = cyl_r + f32x8::splat(capsule.radius);
396 let barrel_hit = in_barrel & r_sq.simd_le(combined * combined);
397
398 let inside_r = r_sq.simd_le(cyl_r_sq);
400 let endcap_inside = inside_r & d_axial_sq.simd_le(cap_r_sq);
401
402 let l = r_sq + cyl_r_sq + d_axial_sq - cap_r_sq;
408 let four_r_sq = f32x8::splat(4.0) * cyl_r_sq;
409 let endcap_outside = l.simd_le(zero) | (l * l).simd_le(four_r_sq * r_sq);
410
411 let hit = barrel_hit | endcap_inside | endcap_outside;
412 hit.any()
413}
414
415impl Collides<Capsule> for Cylinder {
416 #[inline]
417 fn test<const BROADPHASE: bool>(&self, capsule: &Capsule) -> bool {
418 cylinder_capsule_collides::<BROADPHASE>(self, capsule)
419 }
420}
421
422impl Collides<Cylinder> for Capsule {
423 #[inline]
424 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
425 cylinder_capsule_collides::<BROADPHASE>(cyl, self)
426 }
427}
428
429#[inline]
432fn cylinder_cuboid_collides<const BROADPHASE: bool>(cyl: &Cylinder, cuboid: &Cuboid) -> bool {
433 if BROADPHASE {
434 let (bc, br) = cyl.bounding_sphere();
435 let d = bc - cuboid.center;
436 let max_r = br + cuboid.bounding_sphere_radius();
437 if d.dot(d) > max_r * max_r {
438 return false;
439 }
440 }
441
442 let p0_world = cyl.p1 - cuboid.center;
445 let (p0, dir) = if cuboid.axis_aligned {
446 (
447 [p0_world.x, p0_world.y, p0_world.z],
448 [cyl.dir.x, cyl.dir.y, cyl.dir.z],
449 )
450 } else {
451 (
452 [
453 p0_world.dot(cuboid.axes[0]),
454 p0_world.dot(cuboid.axes[1]),
455 p0_world.dot(cuboid.axes[2]),
456 ],
457 [
458 cyl.dir.dot(cuboid.axes[0]),
459 cyl.dir.dot(cuboid.axes[1]),
460 cyl.dir.dot(cuboid.axes[2]),
461 ],
462 )
463 };
464 let he = cuboid.half_extents;
465 let rs_sq = cyl.radius * cyl.radius;
466
467 let inv_dir = [
468 if dir[0].abs() > f32::EPSILON {
469 1.0 / dir[0]
470 } else {
471 f32::MAX
472 },
473 if dir[1].abs() > f32::EPSILON {
474 1.0 / dir[1]
475 } else {
476 f32::MAX
477 },
478 if dir[2].abs() > f32::EPSILON {
479 1.0 / dir[2]
480 } else {
481 f32::MAX
482 },
483 ];
484
485 let ts = f32x8::new([
486 0.0,
487 1.0,
488 ((-he[0] - p0[0]) * inv_dir[0]).clamp(0.0, 1.0),
489 ((he[0] - p0[0]) * inv_dir[0]).clamp(0.0, 1.0),
490 ((-he[1] - p0[1]) * inv_dir[1]).clamp(0.0, 1.0),
491 ((he[1] - p0[1]) * inv_dir[1]).clamp(0.0, 1.0),
492 ((-he[2] - p0[2]) * inv_dir[2]).clamp(0.0, 1.0),
493 ((he[2] - p0[2]) * inv_dir[2]).clamp(0.0, 1.0),
494 ]);
495
496 let zero = f32x8::splat(0.0);
497 let mut dist_sq = zero;
498 for i in 0..3 {
499 let pos = f32x8::splat(p0[i]) + f32x8::splat(dir[i]) * ts;
500 let abs_pos = pos.max(-pos);
501 let excess = (abs_pos - f32x8::splat(he[i])).max(zero);
502 dist_sq = dist_sq + excess * excess;
503 }
504
505 if dist_sq.simd_le(f32x8::splat(rs_sq)).any() {
506 return true;
507 }
508
509 let mut corners = [[0.0f32; 3]; 8];
511 let mut idx = 0;
512 for &sx in &[-1.0f32, 1.0] {
513 for &sy in &[-1.0f32, 1.0] {
514 for &sz in &[-1.0f32, 1.0] {
515 let v = cuboid.center
516 + cuboid.axes[0] * (he[0] * sx)
517 + cuboid.axes[1] * (he[1] * sy)
518 + cuboid.axes[2] * (he[2] * sz);
519 corners[idx] = [v.x, v.y, v.z];
520 idx += 1;
521 }
522 }
523 }
524
525 let cx = f32x8::new(corners.map(|c| c[0]));
527 let cy = f32x8::new(corners.map(|c| c[1]));
528 let cz = f32x8::new(corners.map(|c| c[2]));
529
530 let wx = cx - f32x8::splat(cyl.p1.x);
531 let wy = cy - f32x8::splat(cyl.p1.y);
532 let wz = cz - f32x8::splat(cyl.p1.z);
533
534 let cdx = f32x8::splat(cyl.dir.x);
535 let cdy = f32x8::splat(cyl.dir.y);
536 let cdz = f32x8::splat(cyl.dir.z);
537 let crdv = f32x8::splat(cyl.rdv);
538
539 let t = (wx * cdx + wy * cdy + wz * cdz) * crdv;
540 let one = f32x8::splat(1.0);
541 let in_slab = zero.simd_le(t) & t.simd_le(one);
542
543 let perpx = wx - cdx * t;
544 let perpy = wy - cdy * t;
545 let perpz = wz - cdz * t;
546 let r_sq = perpx * perpx + perpy * perpy + perpz * perpz;
547
548 let corner_in_barrel = in_slab & r_sq.simd_le(f32x8::splat(rs_sq));
549 if corner_in_barrel.any() {
550 return true;
551 }
552
553 false
554}
555
556impl Collides<Cuboid> for Cylinder {
557 #[inline]
558 fn test<const BROADPHASE: bool>(&self, cuboid: &Cuboid) -> bool {
559 cylinder_cuboid_collides::<BROADPHASE>(self, cuboid)
560 }
561}
562
563impl Collides<Cylinder> for Cuboid {
564 #[inline]
565 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
566 cylinder_cuboid_collides::<BROADPHASE>(cyl, self)
567 }
568}
569
570impl Collides<Cylinder> for Cylinder {
573 fn test<const BROADPHASE: bool>(&self, other: &Cylinder) -> bool {
574 if BROADPHASE {
575 let (c1, r1) = self.bounding_sphere();
576 let (c2, r2) = other.bounding_sphere();
577 let d = c1 - c2;
578 let max_r = r1 + r2;
579 if d.dot(d) > max_r * max_r {
580 return false;
581 }
582 }
583
584 let dist_sq =
586 crate::capsule::segment_segment_dist_sq(self.p1, self.dir, other.p1, other.dir);
587 let combined = self.radius + other.radius;
588 if dist_sq <= combined * combined {
589 return true;
593 }
594
595 for cyl_a in [self, other] {
597 let cyl_b = if core::ptr::eq(cyl_a, self) {
598 other
599 } else {
600 self
601 };
602 for &t in &[0.0f32, 0.25, 0.5, 0.75, 1.0] {
603 let p = cyl_a.p1 + cyl_a.dir * t;
604 if cyl_b.point_dist_sq(p) <= cyl_a.radius * cyl_a.radius {
605 return true;
606 }
607 }
608 }
609
610 false
611 }
612}
613
614#[inline]
617fn plane_cylinder_collides(plane: &Plane, cyl: &Cylinder) -> bool {
618 let proj1 = plane.normal.dot(cyl.p1);
619 let proj2 = plane.normal.dot(cyl.p1 + cyl.dir);
620 let min_proj = proj1.min(proj2);
621
622 let dir_sq = cyl.dir.dot(cyl.dir);
623 let n_dot_dir = plane.normal.dot(cyl.dir);
624 let n_perp_sq = if dir_sq > f32::EPSILON {
625 (1.0 - n_dot_dir * n_dot_dir / dir_sq).max(0.0)
626 } else {
627 1.0
628 };
629 let disc_extent = cyl.radius * n_perp_sq.sqrt();
630
631 min_proj - disc_extent <= plane.d
632}
633
634impl Collides<Plane> for Cylinder {
635 #[inline]
636 fn test<const BROADPHASE: bool>(&self, plane: &Plane) -> bool {
637 plane_cylinder_collides(plane, self)
638 }
639}
640
641impl Collides<Cylinder> for Plane {
642 #[inline]
643 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
644 plane_cylinder_collides(self, cyl)
645 }
646}
647
648pub(crate) fn line_cylinder_collides(
652 origin: Vec3,
653 line_dir: Vec3,
654 cyl: &Cylinder,
655 t_min: f32,
656 t_max: f32,
657) -> bool {
658 let dir_sq = cyl.dir.dot(cyl.dir);
659 if dir_sq < f32::EPSILON {
660 let sphere = Sphere::new(cyl.p1, cyl.radius);
662 return crate::line::line_sphere_collides(
663 origin,
664 line_dir,
665 if line_dir.dot(line_dir) > f32::EPSILON {
666 1.0 / line_dir.dot(line_dir)
667 } else {
668 0.0
669 },
670 &sphere,
671 t_min,
672 t_max,
673 );
674 }
675
676 let cyl_len = dir_sq.sqrt();
677 let cyl_axis = cyl.dir / cyl_len;
678
679 let w = origin - cyl.p1;
680 let d_par = line_dir.dot(cyl_axis);
681 let w_par = w.dot(cyl_axis);
682
683 let d_perp = line_dir - cyl_axis * d_par;
684 let w_perp = w - cyl_axis * w_par;
685
686 let a = d_perp.dot(d_perp);
687 let b = 2.0 * w_perp.dot(d_perp);
688 let c = w_perp.dot(w_perp) - cyl.radius * cyl.radius;
689
690 if a > f32::EPSILON {
692 let disc = b * b - 4.0 * a * c;
693 if disc >= 0.0 {
694 let sqrt_disc = disc.sqrt();
695 let inv_2a = 0.5 / a;
696 let t_cyl_enter = (-b - sqrt_disc) * inv_2a;
697 let t_cyl_exit = (-b + sqrt_disc) * inv_2a;
698
699 let (t_slab_enter, t_slab_exit) = if d_par.abs() > f32::EPSILON {
701 let t0 = -w_par / d_par;
702 let t1 = (cyl_len - w_par) / d_par;
703 if t0 < t1 { (t0, t1) } else { (t1, t0) }
704 } else if w_par >= 0.0 && w_par <= cyl_len {
705 (f32::NEG_INFINITY, f32::INFINITY)
706 } else {
707 (1.0, -1.0) };
709
710 let t_enter = t_cyl_enter.max(t_slab_enter).max(t_min);
711 let t_exit = t_cyl_exit.min(t_slab_exit).min(t_max);
712 if t_enter <= t_exit {
713 return true;
714 }
715 }
716 } else {
717 if c <= 0.0 {
719 let (t_slab_enter, t_slab_exit) = if d_par.abs() > f32::EPSILON {
721 let t0 = -w_par / d_par;
722 let t1 = (cyl_len - w_par) / d_par;
723 if t0 < t1 { (t0, t1) } else { (t1, t0) }
724 } else if w_par >= 0.0 && w_par <= cyl_len {
725 (f32::NEG_INFINITY, f32::INFINITY)
726 } else {
727 (1.0, -1.0)
728 };
729 if t_slab_enter.max(t_min) <= t_slab_exit.min(t_max) {
730 return true;
731 }
732 }
733 }
734
735 let r_sq = cyl.radius * cyl.radius;
737 if d_par.abs() > f32::EPSILON {
738 let t0 = -w_par / d_par;
740 if t0 >= t_min && t0 <= t_max {
741 let hit = origin + line_dir * t0;
742 let diff = hit - cyl.p1;
743 let perp = diff - cyl_axis * diff.dot(cyl_axis);
744 if perp.dot(perp) <= r_sq {
745 return true;
746 }
747 }
748 let t1 = (cyl_len - w_par) / d_par;
750 if t1 >= t_min && t1 <= t_max {
751 let hit = origin + line_dir * t1;
752 let p2 = cyl.p1 + cyl.dir;
753 let diff = hit - p2;
754 let perp = diff - cyl_axis * diff.dot(cyl_axis);
755 if perp.dot(perp) <= r_sq {
756 return true;
757 }
758 }
759 }
760
761 false
762}
763
764impl Collides<Line> for Cylinder {
765 #[inline]
766 fn test<const BROADPHASE: bool>(&self, line: &Line) -> bool {
767 line_cylinder_collides(
768 line.origin,
769 line.dir,
770 self,
771 f32::NEG_INFINITY,
772 f32::INFINITY,
773 )
774 }
775}
776
777impl Collides<Cylinder> for Line {
778 #[inline]
779 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
780 line_cylinder_collides(self.origin, self.dir, cyl, f32::NEG_INFINITY, f32::INFINITY)
781 }
782}
783
784impl Collides<Ray> for Cylinder {
785 #[inline]
786 fn test<const BROADPHASE: bool>(&self, ray: &Ray) -> bool {
787 line_cylinder_collides(ray.origin, ray.dir, self, 0.0, f32::INFINITY)
788 }
789}
790
791impl Collides<Cylinder> for Ray {
792 #[inline]
793 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
794 line_cylinder_collides(self.origin, self.dir, cyl, 0.0, f32::INFINITY)
795 }
796}
797
798impl Collides<LineSegment> for Cylinder {
799 #[inline]
800 fn test<const BROADPHASE: bool>(&self, seg: &LineSegment) -> bool {
801 line_cylinder_collides(seg.p1, seg.dir, self, 0.0, 1.0)
802 }
803}
804
805impl Collides<Cylinder> for LineSegment {
806 #[inline]
807 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
808 line_cylinder_collides(self.p1, self.dir, cyl, 0.0, 1.0)
809 }
810}
811
812impl Collides<ConvexPolygon> for Cylinder {
815 fn test<const BROADPHASE: bool>(&self, polygon: &ConvexPolygon) -> bool {
816 if BROADPHASE {
817 let (bc, br) = self.bounding_sphere();
818 let bp = polygon.broadphase();
819 let d = bc - bp.center;
820 let max_r = br + bp.radius;
821 if d.dot(d) > max_r * max_r {
822 return false;
823 }
824 }
825
826 for &v in &polygon.vertices_3d {
828 if self.contains_point(v) {
829 return true;
830 }
831 }
832
833 let dist_sq = polygon.parametric_line_dist_sq(self.p1, self.dir, 0.0, 1.0);
835 if dist_sq <= self.radius * self.radius {
836 return true;
837 }
838
839 false
840 }
841}
842
843impl Collides<Cylinder> for ConvexPolygon {
844 #[inline]
845 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
846 cyl.test::<BROADPHASE>(self)
847 }
848}
849
850impl Collides<ConvexPolytope> for Cylinder {
853 fn test<const BROADPHASE: bool>(&self, polytope: &ConvexPolytope) -> bool {
854 if BROADPHASE {
855 let (bc, br) = self.bounding_sphere();
856 let bp = polytope.broadphase();
857 let d = bc - bp.center;
858 let max_r = br + bp.radius;
859 if d.dot(d) > max_r * max_r {
860 return false;
861 }
862 }
863
864 if crate::line::line_polytope_collides(
866 self.p1,
867 self.dir,
868 &polytope.planes,
869 &polytope.obb,
870 0.0,
871 1.0,
872 ) {
873 return true;
874 }
875
876 for &v in &polytope.vertices {
878 if self.contains_point(v) {
879 return true;
880 }
881 }
882
883 let dir_sq = self.dir.dot(self.dir);
885 if dir_sq > f32::EPSILON {
886 let cyl_len = dir_sq.sqrt();
887 let cyl_axis = self.dir / cyl_len;
888 for &(normal, d) in &polytope.planes {
889 let proj1 = normal.dot(self.p1);
890 let proj2 = normal.dot(self.p1 + self.dir);
891 let min_proj = proj1.min(proj2);
892
893 let n_dot_axis = normal.dot(cyl_axis);
894 let n_perp_sq = (1.0 - n_dot_axis * n_dot_axis).max(0.0);
895 let disc_extent = self.radius * n_perp_sq.sqrt();
896
897 if min_proj - disc_extent > d {
898 return false;
899 }
900 }
901 }
902
903 true
904 }
905}
906
907impl Collides<Cylinder> for ConvexPolytope {
908 #[inline]
909 fn test<const BROADPHASE: bool>(&self, cyl: &Cylinder) -> bool {
910 cyl.test::<BROADPHASE>(self)
911 }
912}
913
914#[derive(Debug, Clone)]
917#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
918pub enum CylinderStretch {
919 Aligned(Cylinder),
920 Unaligned([Capsule; 4], ConvexPolytope),
921}
922
923impl Stretchable for Cylinder {
924 type Output = CylinderStretch;
925
926 fn stretch(&self, translation: Vec3) -> Self::Output {
927 let p2 = self.p2();
928 let cross = self.dir.cross(translation);
929 if cross.length_squared() < 1e-10 {
930 let proj = translation.dot(self.dir);
931 let (new_p1, new_p2) = if proj >= 0.0 {
932 (self.p1, p2 + translation)
933 } else {
934 (self.p1 + translation, p2)
935 };
936 return CylinderStretch::Aligned(Cylinder::new(new_p1, new_p2, self.radius));
937 }
938
939 let p1t = self.p1 + translation;
940 let p2t = p2 + translation;
941
942 let edges = [
943 Capsule::new(self.p1, p2, self.radius),
944 Capsule::new(p1t, p2t, self.radius),
945 Capsule::new(self.p1, p1t, self.radius),
946 Capsule::new(p2, p2t, self.radius),
947 ];
948
949 let n = cross.normalize();
950 let rn = self.radius * n;
951 let corners = [self.p1, p2, p1t, p2t];
952 let vertices: Vec<Vec3> = corners.iter().flat_map(|&c| [c + rn, c - rn]).collect();
953
954 let s1 = n.cross(self.dir).normalize();
955 let s2 = n.cross(translation).normalize();
956 let normals = [n, -n, s1, -s1, s2, -s2];
957 let planes: Vec<(Vec3, f32)> = normals
958 .iter()
959 .map(|&norm| {
960 let d = crate::convex_polytope::max_projection(&vertices, norm);
961 (norm, d)
962 })
963 .collect();
964
965 let dir_n = self.dir.normalize_or_zero();
966 let obb_axes = [dir_n, s1, n];
967 let mut obb_he = [0.0f32; 3];
968 let obb_center = self.p1 + self.dir * 0.5 + translation * 0.5;
969 for i in 0..3 {
970 let max_p = crate::convex_polytope::max_projection(&vertices, obb_axes[i]);
971 let min_p = crate::convex_polytope::min_projection(&vertices, obb_axes[i]);
972 obb_he[i] = (max_p - min_p) * 0.5;
973 }
974 let obb = Cuboid::new(obb_center, obb_axes, obb_he);
975
976 CylinderStretch::Unaligned(edges, ConvexPolytope::with_obb(planes, vertices, obb))
977 }
978}
979
980impl fmt::Display for Cylinder {
981 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
982 let p1 = self.p1;
983 let p2 = self.p2();
984 write!(
985 f,
986 "Cylinder(p1: [{}, {}, {}], p2: [{}, {}, {}], radius: {})",
987 p1.x, p1.y, p1.z, p2.x, p2.y, p2.z, self.radius
988 )
989 }
990}