Skip to main content

oxiphysics_geometry/swept/
types.rs

1//! Auto-generated module
2//!
3//! 🤖 Generated with [SplitRS](https://github.com/cool-japan/splitrs)
4
5use super::functions::*;
6/// A rotational sweep (lathe) — rotate a 2-D profile around the Y axis.
7///
8/// The profile is specified as `(r, y)` pairs where `r` is the radial distance
9/// from the Y axis.  The resulting solid of revolution is discretised into
10/// `segments` triangular strips.
11#[derive(Debug, Clone)]
12pub struct RotationalSweep {
13    /// Profile: `(radius, height)` pairs.
14    pub profile: Vec<[f64; 2]>,
15    /// Number of angular segments (resolution).
16    pub segments: usize,
17}
18impl RotationalSweep {
19    /// Create a new rotational sweep.
20    pub fn new(profile: Vec<[f64; 2]>, segments: usize) -> Self {
21        Self {
22            profile,
23            segments: segments.max(3),
24        }
25    }
26    /// Conservative AABB.
27    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
28        if self.profile.is_empty() {
29            return ([0.0; 3], [0.0; 3]);
30        }
31        let max_r = self
32            .profile
33            .iter()
34            .map(|p| p[0].abs())
35            .fold(0.0f64, f64::max);
36        let min_y = self
37            .profile
38            .iter()
39            .map(|p| p[1])
40            .fold(f64::INFINITY, f64::min);
41        let max_y = self
42            .profile
43            .iter()
44            .map(|p| p[1])
45            .fold(f64::NEG_INFINITY, f64::max);
46        ([-max_r, min_y, -max_r], [max_r, max_y, max_r])
47    }
48    /// Approximate volume via the disk/washer method (trapezoidal integration).
49    pub fn volume(&self) -> f64 {
50        if self.profile.len() < 2 {
51            return 0.0;
52        }
53        let mut vol = 0.0f64;
54        for i in 0..self.profile.len() - 1 {
55            let [r0, y0] = self.profile[i];
56            let [r1, y1] = self.profile[i + 1];
57            let dy = (y1 - y0).abs();
58            let avg_area = std::f64::consts::PI * (r0 * r0 + r0 * r1 + r1 * r1) / 3.0;
59            vol += avg_area * dy;
60        }
61        vol
62    }
63    /// Approximate surface area (lateral only, no caps).
64    pub fn lateral_surface_area(&self) -> f64 {
65        if self.profile.len() < 2 {
66            return 0.0;
67        }
68        let mut area = 0.0f64;
69        for i in 0..self.profile.len() - 1 {
70            let [r0, y0] = self.profile[i];
71            let [r1, y1] = self.profile[i + 1];
72            let dr = r1 - r0;
73            let dy = y1 - y0;
74            let slant = (dr * dr + dy * dy).sqrt();
75            area += std::f64::consts::PI * (r0 + r1) * slant;
76        }
77        area
78    }
79    /// Generate vertex positions for the mesh.
80    ///
81    /// Returns a `Vec` of `[f64; 3]` positions.
82    pub fn vertices(&self) -> Vec<[f64; 3]> {
83        let segs = self.segments;
84        let mut verts = Vec::with_capacity(self.profile.len() * segs);
85        for &[r, y] in &self.profile {
86            for s in 0..segs {
87                let angle = 2.0 * std::f64::consts::PI * s as f64 / segs as f64;
88                verts.push([r * angle.cos(), y, r * angle.sin()]);
89            }
90        }
91        verts
92    }
93}
94/// Result of a linear cast query.
95#[derive(Debug, Clone)]
96pub struct LinearCastResult {
97    /// Time of impact in `[0, 1]`.
98    pub toi: f64,
99    /// Approximate contact point at time of impact.
100    pub contact_point: [f64; 3],
101    /// Approximate contact normal (from B towards A).
102    pub normal: [f64; 3],
103}
104/// A sphere swept along a straight line segment — effectively a capsule.
105#[derive(Debug, Clone)]
106pub struct SweptSphere {
107    /// World-space centre at the start of the sweep.
108    pub center_start: [f64; 3],
109    /// World-space centre at the end of the sweep.
110    pub center_end: [f64; 3],
111    /// Radius of the sphere.
112    pub radius: f64,
113}
114impl SweptSphere {
115    /// Create a new `SweptSphere`.
116    pub fn new(center_start: [f64; 3], center_end: [f64; 3], radius: f64) -> Self {
117        Self {
118            center_start,
119            center_end,
120            radius,
121        }
122    }
123    /// Compute a tight axis-aligned bounding box enclosing the entire sweep.
124    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
125        let r = self.radius;
126        let min = [
127            self.center_start[0].min(self.center_end[0]) - r,
128            self.center_start[1].min(self.center_end[1]) - r,
129            self.center_start[2].min(self.center_end[2]) - r,
130        ];
131        let max = [
132            self.center_start[0].max(self.center_end[0]) + r,
133            self.center_start[1].max(self.center_end[1]) + r,
134            self.center_start[2].max(self.center_end[2]) + r,
135        ];
136        (min, max)
137    }
138    /// Length of the sweep path.
139    pub fn sweep_length(&self) -> f64 {
140        len3(sub3(self.center_end, self.center_start))
141    }
142    /// Centre position at parametric time `t` in `[0, 1]`.
143    pub fn center_at(&self, t: f64) -> [f64; 3] {
144        lerp3(self.center_start, self.center_end, t)
145    }
146    /// Sweep direction (unnormalized).
147    pub fn direction(&self) -> [f64; 3] {
148        sub3(self.center_end, self.center_start)
149    }
150    /// Surface area of the swept volume (capsule surface area).
151    pub fn surface_area(&self) -> f64 {
152        let l = self.sweep_length();
153        2.0 * std::f64::consts::PI * self.radius * l
154            + 4.0 * std::f64::consts::PI * self.radius * self.radius
155    }
156    /// Volume of the swept volume (capsule volume).
157    pub fn volume(&self) -> f64 {
158        let l = self.sweep_length();
159        let r = self.radius;
160        std::f64::consts::PI * r * r * l + (4.0 / 3.0) * std::f64::consts::PI * r * r * r
161    }
162    /// Ray vs swept sphere (capsule) intersection.
163    ///
164    /// Returns the smallest non-negative *t* such that the ray
165    /// `ray_origin + t * ray_dir` touches the capsule surface, or `None` on
166    /// miss.
167    ///
168    /// The capsule is the Minkowski sum of the line segment
169    /// `[center_start, center_end]` with a ball of `radius`.
170    pub fn ray_intersect(&self, ray_origin: [f64; 3], ray_dir: [f64; 3]) -> Option<f64> {
171        let pa = self.center_start;
172        let pb = self.center_end;
173        let r = self.radius;
174        let d = sub3(pb, pa);
175        let ro = sub3(ray_origin, pa);
176        let dd = dot3(d, d);
177        let rd = dot3(ray_dir, d);
178        let ro_d = dot3(ro, d);
179        let ro_ro = dot3(ro, ro);
180        let rd_rd = dot3(ray_dir, ray_dir);
181        let ro_rd = dot3(ro, ray_dir);
182        let a = rd_rd - rd * rd / dd;
183        let b = 2.0 * (ro_rd - ro_d * rd / dd);
184        let c = ro_ro - ro_d * ro_d / dd - r * r;
185        let mut t_min = f64::INFINITY;
186        let disc = b * b - 4.0 * a * c;
187        if disc >= 0.0 && a.abs() > 1e-14 {
188            let sq = disc.sqrt();
189            for &sign in &[-1.0_f64, 1.0_f64] {
190                let t = (-b + sign * sq) / (2.0 * a);
191                if t >= 0.0 {
192                    let proj = (ro_d + t * rd) / dd;
193                    if (0.0..=1.0).contains(&proj) && t < t_min {
194                        t_min = t;
195                    }
196                }
197            }
198        }
199        {
200            let qa = rd_rd;
201            let qb = 2.0 * ro_rd;
202            let qc = ro_ro - r * r;
203            let disc_a = qb * qb - 4.0 * qa * qc;
204            if disc_a >= 0.0 {
205                let sq = disc_a.sqrt();
206                for &sign in &[-1.0_f64, 1.0_f64] {
207                    let t = (-qb + sign * sq) / (2.0 * qa);
208                    if t >= 0.0 {
209                        let proj = (ro_d + t * rd) / dd;
210                        if proj <= 0.0 && t < t_min {
211                            t_min = t;
212                        }
213                    }
214                }
215            }
216        }
217        {
218            let ro_b = sub3(ray_origin, pb);
219            let ro_b_ro_b = dot3(ro_b, ro_b);
220            let ro_b_rd = dot3(ro_b, ray_dir);
221            let qa = rd_rd;
222            let qb = 2.0 * ro_b_rd;
223            let qc = ro_b_ro_b - r * r;
224            let disc_b = qb * qb - 4.0 * qa * qc;
225            if disc_b >= 0.0 {
226                let sq = disc_b.sqrt();
227                for &sign in &[-1.0_f64, 1.0_f64] {
228                    let t = (-qb + sign * sq) / (2.0 * qa);
229                    if t >= 0.0 {
230                        let proj = (ro_d + t * rd) / dd;
231                        if proj >= 1.0 && t < t_min {
232                            t_min = t;
233                        }
234                    }
235                }
236            }
237        }
238        if t_min.is_finite() { Some(t_min) } else { None }
239    }
240}
241/// An oriented bounding box swept linearly along a displacement vector.
242///
243/// The box is described by its centre, three orientation axes (rows of a 3×3
244/// rotation matrix), and half-extents along each local axis.  The swept volume
245/// is conservative: AABB of start + end OBB.
246#[derive(Debug, Clone)]
247pub struct SweptObb {
248    /// OBB centre at the start.
249    pub center_start: [f64; 3],
250    /// OBB orientation: three unit axis vectors.
251    pub axes: [[f64; 3]; 3],
252    /// Half-extents along each axis.
253    pub half_extents: [f64; 3],
254    /// Displacement vector (center_end = center_start + displacement).
255    pub displacement: [f64; 3],
256}
257impl SweptObb {
258    /// Create a new `SweptObb`.
259    pub fn new(
260        center_start: [f64; 3],
261        axes: [[f64; 3]; 3],
262        half_extents: [f64; 3],
263        displacement: [f64; 3],
264    ) -> Self {
265        Self {
266            center_start,
267            axes,
268            half_extents,
269            displacement,
270        }
271    }
272    /// Centre at the end of the sweep.
273    pub fn center_end(&self) -> [f64; 3] {
274        add3(self.center_start, self.displacement)
275    }
276    /// Conservative AABB enclosing the OBB at both start and end.
277    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
278        let mut world_min = [f64::INFINITY; 3];
279        let mut world_max = [f64::NEG_INFINITY; 3];
280        for &center in &[self.center_start, self.center_end()] {
281            for k in 0..3 {
282                let mut r = 0.0f64;
283                for j in 0..3 {
284                    r += self.half_extents[j] * self.axes[j][k].abs();
285                }
286                if center[k] - r < world_min[k] {
287                    world_min[k] = center[k] - r;
288                }
289                if center[k] + r > world_max[k] {
290                    world_max[k] = center[k] + r;
291                }
292            }
293        }
294        (world_min, world_max)
295    }
296    /// Volume of the OBB (static).
297    pub fn volume(&self) -> f64 {
298        8.0 * self.half_extents[0] * self.half_extents[1] * self.half_extents[2]
299    }
300    /// Support point of the static OBB in direction `dir`.
301    pub fn support(&self, dir: [f64; 3]) -> [f64; 3] {
302        let mut result = self.center_start;
303        for j in 0..3 {
304            let s = if dot3(self.axes[j], dir) >= 0.0 {
305                1.0
306            } else {
307                -1.0
308            };
309            result = add3(result, scale3(self.axes[j], s * self.half_extents[j]));
310        }
311        result
312    }
313}
314/// A capsule swept along a straight line defined by start and end positions.
315///
316/// The capsule itself is axis-aligned along the Y axis with given `radius`
317/// and `half_height`. The swept volume is the Minkowski sum of the capsule
318/// with the line segment.
319#[derive(Debug, Clone)]
320pub struct SweptCapsule {
321    /// World-space position of the capsule center at start.
322    pub position_start: [f64; 3],
323    /// World-space position of the capsule center at end.
324    pub position_end: [f64; 3],
325    /// Capsule radius.
326    pub radius: f64,
327    /// Capsule half-height (distance from centre to each hemisphere centre).
328    pub half_height: f64,
329}
330impl SweptCapsule {
331    /// Create a new `SweptCapsule`.
332    pub fn new(
333        position_start: [f64; 3],
334        position_end: [f64; 3],
335        radius: f64,
336        half_height: f64,
337    ) -> Self {
338        Self {
339            position_start,
340            position_end,
341            radius,
342            half_height,
343        }
344    }
345    /// Conservative AABB enclosing the swept capsule.
346    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
347        let r = self.radius;
348        let h = self.half_height;
349        let expand = [r, h + r, r];
350        let mut world_min = [f64::INFINITY; 3];
351        let mut world_max = [f64::NEG_INFINITY; 3];
352        for &pos in &[self.position_start, self.position_end] {
353            for k in 0..3 {
354                let lo = pos[k] - expand[k];
355                let hi = pos[k] + expand[k];
356                if lo < world_min[k] {
357                    world_min[k] = lo;
358                }
359                if hi > world_max[k] {
360                    world_max[k] = hi;
361                }
362            }
363        }
364        (world_min, world_max)
365    }
366    /// Position at parametric time `t`.
367    pub fn position_at(&self, t: f64) -> [f64; 3] {
368        lerp3(self.position_start, self.position_end, t)
369    }
370    /// Volume of the capsule (static, not swept).
371    pub fn capsule_volume(&self) -> f64 {
372        let r = self.radius;
373        let h = 2.0 * self.half_height;
374        std::f64::consts::PI * r * r * h + (4.0 / 3.0) * std::f64::consts::PI * r * r * r
375    }
376    /// Time of impact against a static sphere using conservative advancement.
377    ///
378    /// Returns `t` in `[0, 1]` or `None`.
379    pub fn toi_vs_sphere(&self, sphere_center: [f64; 3], sphere_radius: f64) -> Option<f64> {
380        let effective_radius = self.radius + self.half_height;
381        let combined = effective_radius + sphere_radius;
382        let vel = sub3(self.position_end, self.position_start);
383        let rel = sub3(self.position_start, sphere_center);
384        let a = dot3(vel, vel);
385        let b = 2.0 * dot3(rel, vel);
386        let c = dot3(rel, rel) - combined * combined;
387        if a < 1e-14 {
388            return if c <= 0.0 { Some(0.0) } else { None };
389        }
390        let disc = b * b - 4.0 * a * c;
391        if disc < 0.0 {
392            return None;
393        }
394        let sq = disc.sqrt();
395        let t1 = (-b - sq) / (2.0 * a);
396        let t2 = (-b + sq) / (2.0 * a);
397        let t = if t1 >= 0.0 { t1 } else { t2 };
398        if (0.0..=1.0).contains(&t) {
399            Some(t)
400        } else {
401            None
402        }
403    }
404}
405/// A box swept along a path defined by a start and end transform.
406///
407/// The conservative AABB encloses the box at both the start and end poses.
408#[derive(Debug, Clone)]
409pub struct SweptBox {
410    /// Homogeneous transform (row-major 4x4) at the start of the sweep.
411    pub transform_start: [[f64; 4]; 4],
412    /// Homogeneous transform (row-major 4x4) at the end of the sweep.
413    pub transform_end: [[f64; 4]; 4],
414    /// Half-extents of the box along its local axes.
415    pub half_extents: [f64; 3],
416}
417impl SweptBox {
418    /// Create a new `SweptBox`.
419    pub fn new(
420        transform_start: [[f64; 4]; 4],
421        transform_end: [[f64; 4]; 4],
422        half_extents: [f64; 3],
423    ) -> Self {
424        Self {
425            transform_start,
426            transform_end,
427            half_extents,
428        }
429    }
430    /// Conservative AABB enclosing the box at both the start and end poses.
431    ///
432    /// All 8 corners of the box are transformed into world space at each
433    /// endpoint and the result is the union of the two world-space AABBs.
434    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
435        let hx = self.half_extents[0];
436        let hy = self.half_extents[1];
437        let hz = self.half_extents[2];
438        let corners: [[f64; 3]; 8] = [
439            [-hx, -hy, -hz],
440            [hx, -hy, -hz],
441            [-hx, hy, -hz],
442            [hx, hy, -hz],
443            [-hx, -hy, hz],
444            [hx, -hy, hz],
445            [-hx, hy, hz],
446            [hx, hy, hz],
447        ];
448        let mut world_min = [f64::INFINITY; 3];
449        let mut world_max = [f64::NEG_INFINITY; 3];
450        for &m in &[self.transform_start, self.transform_end] {
451            for &lc in &corners {
452                let wc = transform_point(m, lc);
453                for k in 0..3 {
454                    if wc[k] < world_min[k] {
455                        world_min[k] = wc[k];
456                    }
457                    if wc[k] > world_max[k] {
458                        world_max[k] = wc[k];
459                    }
460                }
461            }
462        }
463        (world_min, world_max)
464    }
465    /// Conservative AABB including `n_samples` intermediate poses.
466    pub fn aabb_sampled(&self, n_samples: usize) -> ([f64; 3], [f64; 3]) {
467        let hx = self.half_extents[0];
468        let hy = self.half_extents[1];
469        let hz = self.half_extents[2];
470        let corners: [[f64; 3]; 8] = [
471            [-hx, -hy, -hz],
472            [hx, -hy, -hz],
473            [-hx, hy, -hz],
474            [hx, hy, -hz],
475            [-hx, -hy, hz],
476            [hx, -hy, hz],
477            [-hx, hy, hz],
478            [hx, hy, hz],
479        ];
480        let mut world_min = [f64::INFINITY; 3];
481        let mut world_max = [f64::NEG_INFINITY; 3];
482        let steps = n_samples.max(2);
483        for i in 0..steps {
484            let t = i as f64 / (steps - 1) as f64;
485            let m = lerp_matrix(self.transform_start, self.transform_end, t);
486            for &lc in &corners {
487                let wc = transform_point(m, lc);
488                for k in 0..3 {
489                    if wc[k] < world_min[k] {
490                        world_min[k] = wc[k];
491                    }
492                    if wc[k] > world_max[k] {
493                        world_max[k] = wc[k];
494                    }
495                }
496            }
497        }
498        (world_min, world_max)
499    }
500    /// Volume of the box (static).
501    pub fn box_volume(&self) -> f64 {
502        8.0 * self.half_extents[0] * self.half_extents[1] * self.half_extents[2]
503    }
504    /// Translation at the start pose.
505    pub fn start_translation(&self) -> [f64; 3] {
506        [
507            self.transform_start[0][3],
508            self.transform_start[1][3],
509            self.transform_start[2][3],
510        ]
511    }
512    /// Translation at the end pose.
513    pub fn end_translation(&self) -> [f64; 3] {
514        [
515            self.transform_end[0][3],
516            self.transform_end[1][3],
517            self.transform_end[2][3],
518        ]
519    }
520    /// Displacement vector from start to end translation.
521    pub fn displacement(&self) -> [f64; 3] {
522        sub3(self.end_translation(), self.start_translation())
523    }
524}
525/// A shape extruded linearly along a vector — a prism swept volume.
526///
527/// The base polygon is defined in the XY plane and the extrusion direction
528/// is given by `sweep_vec`.
529#[derive(Debug, Clone)]
530pub struct LinearExtrusion {
531    /// 2-D profile points (X, Y components).
532    pub profile: Vec<[f64; 2]>,
533    /// Extrusion vector in 3-D space.
534    pub sweep_vec: [f64; 3],
535}
536impl LinearExtrusion {
537    /// Create a new linear extrusion.
538    pub fn new(profile: Vec<[f64; 2]>, sweep_vec: [f64; 3]) -> Self {
539        Self { profile, sweep_vec }
540    }
541    /// Conservative AABB enclosing the extruded prism.
542    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
543        if self.profile.is_empty() {
544            return ([0.0; 3], [0.0; 3]);
545        }
546        let mut mn = [f64::INFINITY; 3];
547        let mut mx = [f64::NEG_INFINITY; 3];
548        for &[x, y] in &self.profile {
549            mn[0] = mn[0].min(x);
550            mx[0] = mx[0].max(x);
551            mn[1] = mn[1].min(y);
552            mx[1] = mx[1].max(y);
553            mn[2] = mn[2].min(0.0);
554            mx[2] = mx[2].max(0.0);
555            let sx = x + self.sweep_vec[0];
556            let sy = y + self.sweep_vec[1];
557            let sz = self.sweep_vec[2];
558            mn[0] = mn[0].min(sx);
559            mx[0] = mx[0].max(sx);
560            mn[1] = mn[1].min(sy);
561            mx[1] = mx[1].max(sy);
562            mn[2] = mn[2].min(sz);
563            mx[2] = mx[2].max(sz);
564        }
565        (mn, mx)
566    }
567    /// Approximate volume: profile area × extrusion length.
568    pub fn volume(&self) -> f64 {
569        let area = self.profile_area();
570        let len = len3(self.sweep_vec);
571        area * len
572    }
573    /// Area of the 2-D profile using the shoelace formula.
574    pub fn profile_area(&self) -> f64 {
575        let n = self.profile.len();
576        if n < 3 {
577            return 0.0;
578        }
579        let mut signed = 0.0f64;
580        for i in 0..n {
581            let [x0, y0] = self.profile[i];
582            let [x1, y1] = self.profile[(i + 1) % n];
583            signed += x0 * y1 - x1 * y0;
584        }
585        (signed * 0.5).abs()
586    }
587    /// Perimeter of the 2-D profile.
588    pub fn profile_perimeter(&self) -> f64 {
589        let n = self.profile.len();
590        if n < 2 {
591            return 0.0;
592        }
593        let mut perim = 0.0f64;
594        for i in 0..n {
595            let [x0, y0] = self.profile[i];
596            let [x1, y1] = self.profile[(i + 1) % n];
597            let dx = x1 - x0;
598            let dy = y1 - y0;
599            perim += (dx * dx + dy * dy).sqrt();
600        }
601        perim
602    }
603    /// Surface area of the extruded prism.
604    ///
605    /// = 2 * profile_area + perimeter * extrusion_length
606    pub fn surface_area(&self) -> f64 {
607        let area = self.profile_area();
608        let perim = self.profile_perimeter();
609        let len = len3(self.sweep_vec);
610        2.0 * area + perim * len
611    }
612}
613/// A shape defined by its start and end AABB — the swept union.
614///
615/// This represents the conservative volume swept by an AABB moving linearly
616/// from `start` to `end` (i.e., the union of the two AABBs and everything in
617/// between along each axis).
618#[derive(Debug, Clone)]
619pub struct SweptAabb {
620    /// AABB at the start: `(min, max)`.
621    pub start_min: [f64; 3],
622    /// AABB at the start: max corner.
623    pub start_max: [f64; 3],
624    /// AABB at the end: min corner.
625    pub end_min: [f64; 3],
626    /// AABB at the end: max corner.
627    pub end_max: [f64; 3],
628}
629impl SweptAabb {
630    /// Create a new `SweptAabb`.
631    pub fn new(
632        start_min: [f64; 3],
633        start_max: [f64; 3],
634        end_min: [f64; 3],
635        end_max: [f64; 3],
636    ) -> Self {
637        Self {
638            start_min,
639            start_max,
640            end_min,
641            end_max,
642        }
643    }
644    /// Conservative AABB covering the entire sweep.
645    pub fn aabb(&self) -> ([f64; 3], [f64; 3]) {
646        let mut mn = [f64::INFINITY; 3];
647        let mut mx = [f64::NEG_INFINITY; 3];
648        for k in 0..3 {
649            mn[k] = self.start_min[k].min(self.end_min[k]);
650            mx[k] = self.start_max[k].max(self.end_max[k]);
651        }
652        (mn, mx)
653    }
654    /// Test whether a static point is inside the swept volume AABB.
655    pub fn contains_point(&self, p: [f64; 3]) -> bool {
656        let (mn, mx) = self.aabb();
657        (0..3).all(|k| p[k] >= mn[k] && p[k] <= mx[k])
658    }
659    /// Displacement of the AABB centre from start to end.
660    pub fn displacement(&self) -> [f64; 3] {
661        let start_center = scale3(add3(self.start_min, self.start_max), 0.5);
662        let end_center = scale3(add3(self.end_min, self.end_max), 0.5);
663        sub3(end_center, start_center)
664    }
665}
666/// Result of a CCD time-of-impact query.
667#[derive(Debug, Clone)]
668pub struct CcdResult {
669    /// Normalised time of impact in `[0, 1]`.
670    pub toi: f64,
671    /// Approximate contact point.
672    pub contact_point: [f64; 3],
673}