Skip to main content

oxihuman_export/
collision_triangle_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Export a triangle-based collision mesh.
6
7/// A single collision triangle.
8#[allow(dead_code)]
9#[derive(Debug, Clone, Copy)]
10pub struct CollisionTriangle {
11    pub v0: [f32; 3],
12    pub v1: [f32; 3],
13    pub v2: [f32; 3],
14    pub material_id: u32,
15}
16
17/// A collision triangle mesh export.
18#[allow(dead_code)]
19#[derive(Debug, Clone)]
20pub struct CollisionTriangleExport {
21    pub triangles: Vec<CollisionTriangle>,
22}
23
24/// Create a new collision triangle export from positions and indices.
25#[allow(dead_code)]
26pub fn from_mesh(
27    positions: &[[f32; 3]],
28    indices: &[u32],
29    material_id: u32,
30) -> CollisionTriangleExport {
31    let tri_count = indices.len() / 3;
32    let mut triangles = Vec::with_capacity(tri_count);
33    for t in 0..tri_count {
34        let i0 = indices[t * 3] as usize;
35        let i1 = indices[t * 3 + 1] as usize;
36        let i2 = indices[t * 3 + 2] as usize;
37        if i0 < positions.len() && i1 < positions.len() && i2 < positions.len() {
38            triangles.push(CollisionTriangle {
39                v0: positions[i0],
40                v1: positions[i1],
41                v2: positions[i2],
42                material_id,
43            });
44        }
45    }
46    CollisionTriangleExport { triangles }
47}
48
49/// Count triangles.
50#[allow(dead_code)]
51pub fn collision_tri_count(export: &CollisionTriangleExport) -> usize {
52    export.triangles.len()
53}
54
55/// Compute the area of a single collision triangle.
56#[allow(dead_code)]
57pub fn triangle_area_ct(tri: &CollisionTriangle) -> f32 {
58    let e1 = [
59        tri.v1[0] - tri.v0[0],
60        tri.v1[1] - tri.v0[1],
61        tri.v1[2] - tri.v0[2],
62    ];
63    let e2 = [
64        tri.v2[0] - tri.v0[0],
65        tri.v2[1] - tri.v0[1],
66        tri.v2[2] - tri.v0[2],
67    ];
68    let cross = [
69        e1[1] * e2[2] - e1[2] * e2[1],
70        e1[2] * e2[0] - e1[0] * e2[2],
71        e1[0] * e2[1] - e1[1] * e2[0],
72    ];
73    (cross[0] * cross[0] + cross[1] * cross[1] + cross[2] * cross[2]).sqrt() * 0.5
74}
75
76/// Total surface area.
77#[allow(dead_code)]
78pub fn total_collision_area(export: &CollisionTriangleExport) -> f32 {
79    export.triangles.iter().map(triangle_area_ct).sum()
80}
81
82/// Count triangles by material id.
83#[allow(dead_code)]
84pub fn triangles_for_material(export: &CollisionTriangleExport, material_id: u32) -> usize {
85    export
86        .triangles
87        .iter()
88        .filter(|t| t.material_id == material_id)
89        .count()
90}
91
92/// Serialize to JSON.
93#[allow(dead_code)]
94pub fn collision_triangle_to_json(export: &CollisionTriangleExport) -> String {
95    format!(
96        "{{\"triangle_count\":{},\"total_area\":{:.4}}}",
97        export.triangles.len(),
98        total_collision_area(export)
99    )
100}
101
102#[cfg(test)]
103mod tests {
104    use super::*;
105
106    fn unit_tri_export() -> CollisionTriangleExport {
107        from_mesh(
108            &[[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
109            &[0, 1, 2],
110            0,
111        )
112    }
113
114    #[test]
115    fn test_from_mesh_count() {
116        assert_eq!(collision_tri_count(&unit_tri_export()), 1);
117    }
118
119    #[test]
120    fn test_triangle_area_positive() {
121        let t = CollisionTriangle {
122            v0: [0.0, 0.0, 0.0],
123            v1: [1.0, 0.0, 0.0],
124            v2: [0.0, 1.0, 0.0],
125            material_id: 0,
126        };
127        assert!(triangle_area_ct(&t) > 0.0);
128    }
129
130    #[test]
131    fn test_total_area_positive() {
132        let e = unit_tri_export();
133        assert!(total_collision_area(&e) > 0.0);
134    }
135
136    #[test]
137    fn test_triangles_for_material() {
138        let e = unit_tri_export();
139        assert_eq!(triangles_for_material(&e, 0), 1);
140    }
141
142    #[test]
143    fn test_triangles_for_wrong_material() {
144        let e = unit_tri_export();
145        assert_eq!(triangles_for_material(&e, 1), 0);
146    }
147
148    #[test]
149    fn test_empty_export() {
150        let e = from_mesh(&[], &[], 0);
151        assert_eq!(collision_tri_count(&e), 0);
152    }
153
154    #[test]
155    fn test_collision_triangle_to_json() {
156        let e = unit_tri_export();
157        let j = collision_triangle_to_json(&e);
158        assert!(j.contains("triangle_count"));
159    }
160
161    #[test]
162    fn test_oob_indices_skipped() {
163        let pos = vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0]];
164        let idx = vec![0u32, 1, 99];
165        let e = from_mesh(&pos, &idx, 0);
166        assert_eq!(collision_tri_count(&e), 0);
167    }
168
169    #[test]
170    fn test_area_unit_right_triangle() {
171        let t = CollisionTriangle {
172            v0: [0.0, 0.0, 0.0],
173            v1: [2.0, 0.0, 0.0],
174            v2: [0.0, 2.0, 0.0],
175            material_id: 0,
176        };
177        assert!((triangle_area_ct(&t) - 2.0).abs() < 1e-5);
178    }
179
180    #[test]
181    fn test_total_area_two_tris() {
182        let pos = vec![
183            [0.0, 0.0, 0.0],
184            [1.0, 0.0, 0.0],
185            [0.0, 1.0, 0.0],
186            [2.0, 0.0, 0.0],
187            [3.0, 0.0, 0.0],
188            [2.0, 1.0, 0.0],
189        ];
190        let idx = vec![0u32, 1, 2, 3, 4, 5];
191        let e = from_mesh(&pos, &idx, 0);
192        assert!(total_collision_area(&e) > 0.5);
193    }
194}