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