Skip to main content

oxihuman_export/
inventory_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! 3D asset inventory export (name, type, stats).
6
7/// Type of 3D asset.
8#[allow(dead_code)]
9#[derive(Debug, Clone, PartialEq)]
10pub enum AssetType {
11    Mesh,
12    Texture,
13    Material,
14    Animation,
15    Skeleton,
16    Scene,
17    Other(String),
18}
19
20impl AssetType {
21    #[allow(dead_code)]
22    pub fn as_str(&self) -> &str {
23        match self {
24            AssetType::Mesh => "mesh",
25            AssetType::Texture => "texture",
26            AssetType::Material => "material",
27            AssetType::Animation => "animation",
28            AssetType::Skeleton => "skeleton",
29            AssetType::Scene => "scene",
30            AssetType::Other(s) => s.as_str(),
31        }
32    }
33}
34
35/// Stats for a 3D asset.
36#[allow(dead_code)]
37#[derive(Debug, Clone)]
38pub struct AssetStats {
39    pub vertex_count: u32,
40    pub face_count: u32,
41    pub file_size_bytes: u64,
42    pub lod_levels: u8,
43}
44
45impl AssetStats {
46    #[allow(dead_code)]
47    pub fn zero() -> Self {
48        Self {
49            vertex_count: 0,
50            face_count: 0,
51            file_size_bytes: 0,
52            lod_levels: 0,
53        }
54    }
55}
56
57/// A single asset inventory entry.
58#[allow(dead_code)]
59#[derive(Debug, Clone)]
60pub struct AssetEntry {
61    pub id: u32,
62    pub name: String,
63    pub asset_type: AssetType,
64    pub path: String,
65    pub stats: AssetStats,
66}
67
68/// A 3D asset inventory.
69#[allow(dead_code)]
70pub struct AssetInventory {
71    pub project: String,
72    pub entries: Vec<AssetEntry>,
73    next_id: u32,
74}
75
76impl AssetInventory {
77    #[allow(dead_code)]
78    pub fn new(project: &str) -> Self {
79        Self {
80            project: project.to_string(),
81            entries: Vec::new(),
82            next_id: 0,
83        }
84    }
85}
86
87/// Add an asset to the inventory.
88#[allow(dead_code)]
89pub fn add_asset(
90    inventory: &mut AssetInventory,
91    name: &str,
92    asset_type: AssetType,
93    path: &str,
94    stats: AssetStats,
95) -> u32 {
96    let id = inventory.next_id;
97    inventory.next_id += 1;
98    inventory.entries.push(AssetEntry {
99        id,
100        name: name.to_string(),
101        asset_type,
102        path: path.to_string(),
103        stats,
104    });
105    id
106}
107
108/// Export inventory to CSV string.
109#[allow(dead_code)]
110pub fn export_inventory_csv(inventory: &AssetInventory) -> String {
111    let mut out = String::from("id,name,type,path,vertices,faces,size_bytes,lod_levels\n");
112    for e in &inventory.entries {
113        out.push_str(&format!(
114            "{},{},{},{},{},{},{},{}\n",
115            e.id,
116            e.name,
117            e.asset_type.as_str(),
118            e.path,
119            e.stats.vertex_count,
120            e.stats.face_count,
121            e.stats.file_size_bytes,
122            e.stats.lod_levels
123        ));
124    }
125    out
126}
127
128/// Export inventory to JSON-like string.
129#[allow(dead_code)]
130pub fn export_inventory_json(inventory: &AssetInventory) -> String {
131    let mut out = format!("{{\"project\":\"{}\",\"assets\":[", inventory.project);
132    for (i, e) in inventory.entries.iter().enumerate() {
133        if i > 0 {
134            out.push(',');
135        }
136        out.push_str(&format!(
137            "{{\"id\":{},\"name\":\"{}\",\"type\":\"{}\",\"path\":\"{}\",\
138            \"vertices\":{},\"faces\":{},\"size_bytes\":{},\"lod_levels\":{}}}",
139            e.id,
140            e.name,
141            e.asset_type.as_str(),
142            e.path,
143            e.stats.vertex_count,
144            e.stats.face_count,
145            e.stats.file_size_bytes,
146            e.stats.lod_levels
147        ));
148    }
149    out.push_str("]}");
150    out
151}
152
153/// Total asset count.
154#[allow(dead_code)]
155pub fn asset_count(inventory: &AssetInventory) -> usize {
156    inventory.entries.len()
157}
158
159/// Count assets by type.
160#[allow(dead_code)]
161pub fn count_by_type(inventory: &AssetInventory, asset_type: &AssetType) -> usize {
162    inventory
163        .entries
164        .iter()
165        .filter(|e| &e.asset_type == asset_type)
166        .count()
167}
168
169/// Total file size across all assets.
170#[allow(dead_code)]
171pub fn total_file_size(inventory: &AssetInventory) -> u64 {
172    inventory
173        .entries
174        .iter()
175        .map(|e| e.stats.file_size_bytes)
176        .sum()
177}
178
179/// Total vertex count across all mesh assets.
180#[allow(dead_code)]
181pub fn total_vertex_count(inventory: &AssetInventory) -> u64 {
182    inventory
183        .entries
184        .iter()
185        .map(|e| e.stats.vertex_count as u64)
186        .sum()
187}
188
189/// Find asset by name.
190#[allow(dead_code)]
191pub fn find_asset_by_name<'a>(inventory: &'a AssetInventory, name: &str) -> Option<&'a AssetEntry> {
192    inventory.entries.iter().find(|e| e.name == name)
193}
194
195/// Find asset by ID.
196#[allow(dead_code)]
197pub fn find_asset_by_id(inventory: &AssetInventory, id: u32) -> Option<&AssetEntry> {
198    inventory.entries.iter().find(|e| e.id == id)
199}
200
201#[cfg(test)]
202mod tests {
203    use super::*;
204
205    fn sample_inventory() -> AssetInventory {
206        let mut inv = AssetInventory::new("oxihuman-project");
207        add_asset(
208            &mut inv,
209            "head_mesh",
210            AssetType::Mesh,
211            "assets/head.glb",
212            AssetStats {
213                vertex_count: 5000,
214                face_count: 9800,
215                file_size_bytes: 204800,
216                lod_levels: 3,
217            },
218        );
219        add_asset(
220            &mut inv,
221            "body_mesh",
222            AssetType::Mesh,
223            "assets/body.glb",
224            AssetStats {
225                vertex_count: 12000,
226                face_count: 23000,
227                file_size_bytes: 512000,
228                lod_levels: 4,
229            },
230        );
231        add_asset(
232            &mut inv,
233            "skin_texture",
234            AssetType::Texture,
235            "assets/skin.png",
236            AssetStats {
237                vertex_count: 0,
238                face_count: 0,
239                file_size_bytes: 1048576,
240                lod_levels: 0,
241            },
242        );
243        inv
244    }
245
246    #[test]
247    fn asset_count_correct() {
248        let inv = sample_inventory();
249        assert_eq!(asset_count(&inv), 3);
250    }
251
252    #[test]
253    fn count_by_type_mesh() {
254        let inv = sample_inventory();
255        assert_eq!(count_by_type(&inv, &AssetType::Mesh), 2);
256    }
257
258    #[test]
259    fn count_by_type_texture() {
260        let inv = sample_inventory();
261        assert_eq!(count_by_type(&inv, &AssetType::Texture), 1);
262    }
263
264    #[test]
265    fn total_file_size_correct() {
266        let inv = sample_inventory();
267        assert_eq!(total_file_size(&inv), 204800 + 512000 + 1048576);
268    }
269
270    #[test]
271    fn total_vertex_count_correct() {
272        let inv = sample_inventory();
273        assert_eq!(total_vertex_count(&inv), 17000);
274    }
275
276    #[test]
277    fn csv_header_present() {
278        let inv = sample_inventory();
279        let csv = export_inventory_csv(&inv);
280        assert!(csv.starts_with("id,name,type"));
281    }
282
283    #[test]
284    fn json_contains_project() {
285        let inv = sample_inventory();
286        let json = export_inventory_json(&inv);
287        assert!(json.contains("oxihuman-project"));
288    }
289
290    #[test]
291    fn find_asset_by_name_some() {
292        let inv = sample_inventory();
293        assert!(find_asset_by_name(&inv, "head_mesh").is_some());
294    }
295
296    #[test]
297    fn find_asset_by_name_none() {
298        let inv = sample_inventory();
299        assert!(find_asset_by_name(&inv, "nonexistent").is_none());
300    }
301
302    #[test]
303    fn find_asset_by_id_correct() {
304        let inv = sample_inventory();
305        let e = find_asset_by_id(&inv, 1);
306        assert!(e.is_some_and(|a| a.name == "body_mesh"));
307    }
308}