Skip to main content

wreck/
cylinder.rs

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    /// Create a `Cylinder` from center, orientation (DMat3), radius, and length.
53    /// The cylinder axis is along the orientation's Y column.
54    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    /// Extract center, orientation (DMat3), and length from this cylinder.
67    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    /// Returns the length of the cylinder's central axis.
90    #[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    /// Squared distance from a point to the nearest point on the cylinder surface/interior.
112    #[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    /// Returns true if the point lies inside or on the cylinder surface.
135    #[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// ─── Trait impls ─────────────────────────────────────────────────────────────
149
150#[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// ─── Cylinder-Sphere ─────────────────────────────────────────────────────────
232
233#[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            // sqrt-free: (r - R)^2 + d_axial_sq <= sr^2
256            // ⇔ r_sq + R^2 + d_axial_sq - sr^2 <= 2*r*R
257            // Let L = r_sq + R^2 + d_axial_sq - sr^2
258            // True when L <= 0 OR L^2 <= 4*R^2*r_sq
259            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
279// ─── Cylinder-Point ──────────────────────────────────────────────────────────
280
281impl 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// ─── Cylinder-Capsule ────────────────────────────────────────────────────────
296
297/// Evaluate cylinder distance at 8 sample points along the capsule axis.
298#[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    // Compute key s-values on the capsule axis
313    // s_closest: closest approach between the two axis segments
314    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    // s where cylinder axis parameter t = 0 and t = 1
338    // t(s) = ((capsule.p1 + s*capsule.dir - cyl.p1) · cyl.dir) * cyl.rdv
339    // t(s) = (r·cyl.dir + s * capsule.dir·cyl.dir) * cyl.rdv
340    // t = 0 → s = -r·cyl.dir / (capsule.dir·cyl.dir)
341    // t = 1 → s = (|cyl.dir|² - r·cyl.dir) / (capsule.dir·cyl.dir)
342    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    // Evaluate point_dist_sq at each sample on capsule axis
357    // p(s) = capsule.p1 + s * capsule.dir
358    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    // w = p - cyl.p1
363    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    // t = w · cyl.dir * rdv
373    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    // perp = w - t * cyl.dir (using unclamped t for perpendicular distance)
379    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    // d_axial_sq = (t - t_c)^2 * |cyl.dir|^2
385    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    // Barrel check: t in [0,1] and r_sq <= (cyl.radius + capsule.radius)^2
394    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    // End cap, radially inside: d_axial_sq <= capsule.radius^2
399    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    // End cap, radially outside: sqrt-free algebraic check
403    // (r - R)^2 + d_axial_sq <= cap_r_sq
404    // r_sq + R^2 + d_axial_sq - cap_r_sq <= 2*r*R
405    // Let L = r_sq + R^2 + d_axial_sq - cap_r_sq
406    // L <= 0 OR L^2 <= 4*R^2*r_sq
407    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// ─── Cylinder-Cuboid ─────────────────────────────────────────────────────────
430
431#[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    // Test 1: Sample cylinder axis at 8 t-values against cuboid (barrel test)
443    // Same approach as capsule-cuboid but the hit means barrel collision.
444    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    // Test 2: Check 8 cuboid corners against cylinder
510    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    // SIMD check: all 8 corners against cylinder
526    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
570// ─── Cylinder-Cylinder ───────────────────────────────────────────────────────
571
572impl 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        // Check axis-axis closest approach (barrel-barrel)
585        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            // Need to verify closest approach is in barrel region of both cylinders
590            // The segment-segment distance already clamps to [0,1] on both,
591            // so if the distance is within combined radii, the barrels overlap.
592            return true;
593        }
594
595        // Check end caps: sample 5 points on each cylinder against the other
596        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// ─── Cylinder-Plane ──────────────────────────────────────────────────────────
615
616#[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
648// ─── Cylinder-Line/Ray/Segment ───────────────────────────────────────────────
649
650/// Line-cylinder intersection using quadratic for barrel + disc checks for end caps.
651pub(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        // Degenerate cylinder (disc at p1): treat as sphere
661        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    // Check barrel intersection
691    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            // Clip to slab
700            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) // empty
708            };
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        // Line parallel to axis: check if inside infinite cylinder
718        if c <= 0.0 {
719            // Inside cylinder radius — check slab overlap
720            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    // Check end cap discs
736    let r_sq = cyl.radius * cyl.radius;
737    if d_par.abs() > f32::EPSILON {
738        // Disc at p1 (axial pos = 0)
739        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        // Disc at p2 (axial pos = cyl_len)
749        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
812// ─── Cylinder-ConvexPolygon ──────────────────────────────────────────────────
813
814impl 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        // Check polygon vertices against cylinder
827        for &v in &polygon.vertices_3d {
828            if self.contains_point(v) {
829                return true;
830            }
831        }
832
833        // Check cylinder axis vs polygon (capsule-like distance)
834        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
850// ─── Cylinder-ConvexPolytope ─────────────────────────────────────────────────
851
852impl 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        // Check if cylinder axis passes through (OBB-expanded) polytope
865        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        // Check polytope vertices against cylinder
877        for &v in &polytope.vertices {
878            if self.contains_point(v) {
879                return true;
880            }
881        }
882
883        // SAT: cylinder support function against polytope planes
884        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// ─── Stretchable ─────────────────────────────────────────────────────────────
915
916#[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}