Skip to main content

oxihuman_export/
geo_instance_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! Geometry instance placement export (point-scatter instancing).
6
7#[allow(dead_code)]
8#[derive(Debug, Clone)]
9pub struct GeoInstanceEntry {
10    pub mesh_name: String,
11    pub position: [f32; 3],
12    pub rotation: [f32; 4],
13    pub scale: [f32; 3],
14}
15
16#[allow(dead_code)]
17#[derive(Debug, Clone)]
18pub struct GeoInstanceSetExport {
19    pub instances: Vec<GeoInstanceEntry>,
20}
21
22#[allow(dead_code)]
23pub fn new_geo_instance_set() -> GeoInstanceSetExport {
24    GeoInstanceSetExport {
25        instances: Vec::new(),
26    }
27}
28
29#[allow(dead_code)]
30pub fn add_instance_entry(exp: &mut GeoInstanceSetExport, mesh: &str, pos: [f32; 3]) {
31    exp.instances.push(GeoInstanceEntry {
32        mesh_name: mesh.to_string(),
33        position: pos,
34        rotation: [0.0, 0.0, 0.0, 1.0],
35        scale: [1.0, 1.0, 1.0],
36    });
37}
38
39#[allow(dead_code)]
40pub fn instance_count(exp: &GeoInstanceSetExport) -> usize {
41    exp.instances.len()
42}
43
44#[allow(dead_code)]
45pub fn instances_of_mesh<'a>(
46    exp: &'a GeoInstanceSetExport,
47    mesh: &str,
48) -> Vec<&'a GeoInstanceEntry> {
49    exp.instances
50        .iter()
51        .filter(|i| i.mesh_name == mesh)
52        .collect()
53}
54
55#[allow(dead_code)]
56pub fn unique_mesh_names(exp: &GeoInstanceSetExport) -> Vec<String> {
57    let mut names: Vec<String> = exp.instances.iter().map(|i| i.mesh_name.clone()).collect();
58    names.sort_unstable();
59    names.dedup();
60    names
61}
62
63#[allow(dead_code)]
64pub fn instance_bounds(exp: &GeoInstanceSetExport) -> ([f32; 3], [f32; 3]) {
65    if exp.instances.is_empty() {
66        return ([0.0; 3], [0.0; 3]);
67    }
68    let mut mn = [f32::MAX; 3];
69    let mut mx = [f32::MIN; 3];
70    for inst in &exp.instances {
71        for j in 0..3 {
72            mn[j] = mn[j].min(inst.position[j]);
73            mx[j] = mx[j].max(inst.position[j]);
74        }
75    }
76    (mn, mx)
77}
78
79#[allow(dead_code)]
80pub fn geo_instance_to_json(exp: &GeoInstanceSetExport) -> String {
81    format!(
82        "{{\"instance_count\":{},\"mesh_types\":{}}}",
83        instance_count(exp),
84        unique_mesh_names(exp).len()
85    )
86}
87
88#[allow(dead_code)]
89pub fn validate_instances(exp: &GeoInstanceSetExport) -> bool {
90    exp.instances.iter().all(|i| !i.mesh_name.is_empty())
91}
92
93#[allow(dead_code)]
94pub fn clear_instances(exp: &mut GeoInstanceSetExport) {
95    exp.instances.clear();
96}
97
98#[cfg(test)]
99mod tests {
100    use super::*;
101
102    #[test]
103    fn test_empty() {
104        let exp = new_geo_instance_set();
105        assert_eq!(instance_count(&exp), 0);
106    }
107
108    #[test]
109    fn test_add_instance() {
110        let mut exp = new_geo_instance_set();
111        add_instance_entry(&mut exp, "tree", [1.0, 0.0, 0.0]);
112        assert_eq!(instance_count(&exp), 1);
113    }
114
115    #[test]
116    fn test_instances_of_mesh() {
117        let mut exp = new_geo_instance_set();
118        add_instance_entry(&mut exp, "rock", [0.0; 3]);
119        add_instance_entry(&mut exp, "tree", [1.0, 0.0, 0.0]);
120        add_instance_entry(&mut exp, "rock", [2.0, 0.0, 0.0]);
121        assert_eq!(instances_of_mesh(&exp, "rock").len(), 2);
122    }
123
124    #[test]
125    fn test_unique_mesh_names() {
126        let mut exp = new_geo_instance_set();
127        add_instance_entry(&mut exp, "tree", [0.0; 3]);
128        add_instance_entry(&mut exp, "rock", [0.0; 3]);
129        add_instance_entry(&mut exp, "tree", [1.0, 0.0, 0.0]);
130        assert_eq!(unique_mesh_names(&exp).len(), 2);
131    }
132
133    #[test]
134    fn test_bounds() {
135        let mut exp = new_geo_instance_set();
136        add_instance_entry(&mut exp, "x", [-1.0, 0.0, 0.0]);
137        add_instance_entry(&mut exp, "x", [2.0, 0.0, 0.0]);
138        let (mn, mx) = instance_bounds(&exp);
139        assert!((mn[0] - -1.0).abs() < 1e-5);
140        assert!((mx[0] - 2.0).abs() < 1e-5);
141    }
142
143    #[test]
144    fn test_json_output() {
145        let exp = new_geo_instance_set();
146        let j = geo_instance_to_json(&exp);
147        assert!(j.contains("instance_count"));
148    }
149
150    #[test]
151    fn test_validate_ok() {
152        let mut exp = new_geo_instance_set();
153        add_instance_entry(&mut exp, "mesh", [0.0; 3]);
154        assert!(validate_instances(&exp));
155    }
156
157    #[test]
158    fn test_clear() {
159        let mut exp = new_geo_instance_set();
160        add_instance_entry(&mut exp, "mesh", [0.0; 3]);
161        clear_instances(&mut exp);
162        assert_eq!(instance_count(&exp), 0);
163    }
164
165    #[test]
166    fn test_default_scale_one() {
167        let mut exp = new_geo_instance_set();
168        add_instance_entry(&mut exp, "mesh", [0.0; 3]);
169        assert_eq!(exp.instances[0].scale, [1.0, 1.0, 1.0]);
170    }
171
172    #[test]
173    fn test_default_rotation_identity() {
174        let mut exp = new_geo_instance_set();
175        add_instance_entry(&mut exp, "mesh", [0.0; 3]);
176        assert!((exp.instances[0].rotation[3] - 1.0).abs() < 1e-6);
177    }
178}