Skip to main content

oxihuman_export/
collision_compound_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Export compound collision shapes (collections of primitive shapes).
6
7/// Primitive collision shape type.
8#[allow(dead_code)]
9#[derive(Debug, Clone)]
10pub enum CollisionPrimitive {
11    Sphere {
12        center: [f32; 3],
13        radius: f32,
14    },
15    Box {
16        center: [f32; 3],
17        half_extents: [f32; 3],
18    },
19    Capsule {
20        a: [f32; 3],
21        b: [f32; 3],
22        radius: f32,
23    },
24}
25
26/// A compound collision shape composed of primitives.
27#[allow(dead_code)]
28#[derive(Debug, Clone, Default)]
29pub struct CompoundCollision {
30    pub name: String,
31    pub primitives: Vec<CollisionPrimitive>,
32}
33
34/// Create a new compound collision shape.
35#[allow(dead_code)]
36pub fn new_compound(name: &str) -> CompoundCollision {
37    CompoundCollision {
38        name: name.to_string(),
39        primitives: vec![],
40    }
41}
42
43/// Add a sphere primitive.
44#[allow(dead_code)]
45pub fn add_sphere(compound: &mut CompoundCollision, center: [f32; 3], radius: f32) {
46    compound
47        .primitives
48        .push(CollisionPrimitive::Sphere { center, radius });
49}
50
51/// Add a box primitive.
52#[allow(dead_code)]
53pub fn add_box(compound: &mut CompoundCollision, center: [f32; 3], half_extents: [f32; 3]) {
54    compound.primitives.push(CollisionPrimitive::Box {
55        center,
56        half_extents,
57    });
58}
59
60/// Add a capsule primitive.
61#[allow(dead_code)]
62pub fn add_capsule(compound: &mut CompoundCollision, a: [f32; 3], b: [f32; 3], radius: f32) {
63    compound
64        .primitives
65        .push(CollisionPrimitive::Capsule { a, b, radius });
66}
67
68/// Count of primitives.
69#[allow(dead_code)]
70pub fn primitive_count(compound: &CompoundCollision) -> usize {
71    compound.primitives.len()
72}
73
74/// Approximate total volume of the compound shape.
75#[allow(dead_code)]
76pub fn compound_volume(compound: &CompoundCollision) -> f32 {
77    use std::f32::consts::PI;
78    compound
79        .primitives
80        .iter()
81        .map(|p| match p {
82            CollisionPrimitive::Sphere { radius, .. } => {
83                (4.0 / 3.0) * PI * radius * radius * radius
84            }
85            CollisionPrimitive::Box { half_extents, .. } => {
86                8.0 * half_extents[0] * half_extents[1] * half_extents[2]
87            }
88            CollisionPrimitive::Capsule { a, b, radius } => {
89                let len = {
90                    let d = [b[0] - a[0], b[1] - a[1], b[2] - a[2]];
91                    (d[0] * d[0] + d[1] * d[1] + d[2] * d[2]).sqrt()
92                };
93                PI * radius * radius * len + (4.0 / 3.0) * PI * radius * radius * radius
94            }
95        })
96        .sum()
97}
98
99/// Compute the AABB of all sphere primitives' centers.
100#[allow(dead_code)]
101pub fn aabb_of_centers(compound: &CompoundCollision) -> Option<([f32; 3], [f32; 3])> {
102    let centers: Vec<[f32; 3]> = compound
103        .primitives
104        .iter()
105        .map(|p| match p {
106            CollisionPrimitive::Sphere { center, .. } => *center,
107            CollisionPrimitive::Box { center, .. } => *center,
108            CollisionPrimitive::Capsule { a, b, .. } => [
109                (a[0] + b[0]) * 0.5,
110                (a[1] + b[1]) * 0.5,
111                (a[2] + b[2]) * 0.5,
112            ],
113        })
114        .collect();
115    if centers.is_empty() {
116        return None;
117    }
118    let mn = centers
119        .iter()
120        .cloned()
121        .reduce(|a, b| [a[0].min(b[0]), a[1].min(b[1]), a[2].min(b[2])])?;
122    let mx = centers
123        .iter()
124        .cloned()
125        .reduce(|a, b| [a[0].max(b[0]), a[1].max(b[1]), a[2].max(b[2])])?;
126    Some((mn, mx))
127}
128
129/// Serialise compound to flat f32 buffer (type tag + data per primitive).
130#[allow(dead_code)]
131pub fn serialise_compound(compound: &CompoundCollision) -> Vec<f32> {
132    let mut buf = Vec::new();
133    for p in &compound.primitives {
134        match p {
135            CollisionPrimitive::Sphere { center, radius } => {
136                buf.extend_from_slice(&[0.0, center[0], center[1], center[2], *radius]);
137            }
138            CollisionPrimitive::Box {
139                center,
140                half_extents,
141            } => {
142                buf.extend_from_slice(&[
143                    1.0,
144                    center[0],
145                    center[1],
146                    center[2],
147                    half_extents[0],
148                    half_extents[1],
149                    half_extents[2],
150                ]);
151            }
152            CollisionPrimitive::Capsule { a, b, radius } => {
153                buf.extend_from_slice(&[2.0, a[0], a[1], a[2], b[0], b[1], b[2], *radius]);
154            }
155        }
156    }
157    buf
158}
159
160#[cfg(test)]
161mod tests {
162    use super::*;
163
164    #[test]
165    fn test_new_compound() {
166        let c = new_compound("body");
167        assert_eq!(primitive_count(&c), 0);
168    }
169
170    #[test]
171    fn test_add_sphere() {
172        let mut c = new_compound("c");
173        add_sphere(&mut c, [0.0; 3], 1.0);
174        assert_eq!(primitive_count(&c), 1);
175    }
176
177    #[test]
178    fn test_add_box() {
179        let mut c = new_compound("c");
180        add_box(&mut c, [0.0; 3], [1.0; 3]);
181        assert_eq!(primitive_count(&c), 1);
182    }
183
184    #[test]
185    fn test_add_capsule() {
186        let mut c = new_compound("c");
187        add_capsule(&mut c, [0.0; 3], [0.0, 1.0, 0.0], 0.1);
188        assert_eq!(primitive_count(&c), 1);
189    }
190
191    #[test]
192    fn test_compound_volume_positive() {
193        let mut c = new_compound("c");
194        add_sphere(&mut c, [0.0; 3], 1.0);
195        assert!(compound_volume(&c) > 0.0);
196    }
197
198    #[test]
199    fn test_aabb_of_centers_none_for_empty() {
200        let c = new_compound("c");
201        assert!(aabb_of_centers(&c).is_none());
202    }
203
204    #[test]
205    fn test_aabb_of_centers_some() {
206        let mut c = new_compound("c");
207        add_sphere(&mut c, [-1.0, 0.0, 0.0], 0.5);
208        add_sphere(&mut c, [1.0, 0.0, 0.0], 0.5);
209        let (mn, mx) = aabb_of_centers(&c).expect("should succeed");
210        assert!((mn[0] - (-1.0)).abs() < 1e-6);
211        assert!((mx[0] - 1.0).abs() < 1e-6);
212    }
213
214    #[test]
215    fn test_serialise_sphere() {
216        let mut c = new_compound("c");
217        add_sphere(&mut c, [0.0; 3], 1.0);
218        let buf = serialise_compound(&c);
219        assert_eq!(buf.len(), 5);
220        assert!((buf[0] - 0.0).abs() < 1e-6);
221    }
222
223    #[test]
224    fn test_serialise_empty() {
225        let c = new_compound("c");
226        assert!(serialise_compound(&c).is_empty());
227    }
228
229    #[test]
230    fn test_name_stored() {
231        let c = new_compound("spine");
232        assert_eq!(c.name, "spine".to_string());
233    }
234}