Skip to main content

oxihuman_export/
ac3d_export.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3#![allow(dead_code)]
4
5//! AC3D 3D model format export.
6
7/// An AC3D surface.
8#[derive(Clone, Debug)]
9pub struct Ac3dSurface {
10    pub flags: u32,
11    pub vertices: Vec<(u32, f32, f32)>, // (index, u, v)
12}
13
14/// An AC3D material.
15#[derive(Clone, Debug)]
16pub struct Ac3dMaterial {
17    pub name: String,
18    pub rgb: [f32; 3],
19    pub amb: [f32; 3],
20    pub emis: [f32; 3],
21    pub spec: [f32; 3],
22    pub shi: u32,
23    pub trans: f32,
24}
25
26impl Default for Ac3dMaterial {
27    fn default() -> Self {
28        Self {
29            name: "default".to_string(),
30            rgb: [0.8, 0.8, 0.8],
31            amb: [0.2, 0.2, 0.2],
32            emis: [0.0, 0.0, 0.0],
33            spec: [0.5, 0.5, 0.5],
34            shi: 10,
35            trans: 0.0,
36        }
37    }
38}
39
40/// An AC3D object.
41#[derive(Clone, Debug)]
42pub struct Ac3dObject {
43    pub name: String,
44    pub positions: Vec<[f32; 3]>,
45    pub surfaces: Vec<Ac3dSurface>,
46    pub mat_index: u32,
47}
48
49/// An AC3D document.
50#[derive(Clone, Debug, Default)]
51pub struct Ac3dExport {
52    pub materials: Vec<Ac3dMaterial>,
53    pub objects: Vec<Ac3dObject>,
54}
55
56/// Create a new AC3D export document.
57pub fn new_ac3d_export() -> Ac3dExport {
58    Ac3dExport::default()
59}
60
61/// Add a material and return its index.
62pub fn ac3d_add_material(doc: &mut Ac3dExport, mat: Ac3dMaterial) -> u32 {
63    let idx = doc.materials.len() as u32;
64    doc.materials.push(mat);
65    idx
66}
67
68/// Add an object from mesh geometry.
69pub fn ac3d_add_mesh(
70    doc: &mut Ac3dExport,
71    name: &str,
72    positions: Vec<[f32; 3]>,
73    indices: &[u32],
74    mat_index: u32,
75) {
76    let surfaces: Vec<Ac3dSurface> = indices
77        .chunks(3)
78        .map(|t| Ac3dSurface {
79            flags: 0,
80            vertices: t.iter().map(|&i| (i, 0.0, 0.0)).collect(),
81        })
82        .collect();
83    doc.objects.push(Ac3dObject {
84        name: name.to_string(),
85        positions,
86        surfaces,
87        mat_index,
88    });
89}
90
91/// Return the material count.
92pub fn ac3d_material_count(doc: &Ac3dExport) -> usize {
93    doc.materials.len()
94}
95
96/// Return the object count.
97pub fn ac3d_object_count(doc: &Ac3dExport) -> usize {
98    doc.objects.len()
99}
100
101/// Render the AC3D file.
102pub fn render_ac3d(doc: &Ac3dExport) -> String {
103    let mut out = String::from("AC3Db\n");
104    for mat in &doc.materials {
105        out.push_str(&format!(
106            "MATERIAL \"{}\" rgb {:.4} {:.4} {:.4}  amb {:.4} {:.4} {:.4}  emis {:.4} {:.4} {:.4}  spec {:.4} {:.4} {:.4}  shi {}  trans {:.4}\n",
107            mat.name,
108            mat.rgb[0], mat.rgb[1], mat.rgb[2],
109            mat.amb[0], mat.amb[1], mat.amb[2],
110            mat.emis[0], mat.emis[1], mat.emis[2],
111            mat.spec[0], mat.spec[1], mat.spec[2],
112            mat.shi, mat.trans
113        ));
114    }
115    out.push_str("OBJECT world\nkids ");
116    out.push_str(&format!("{}\n", doc.objects.len()));
117    for obj in &doc.objects {
118        out.push_str("OBJECT poly\n");
119        out.push_str(&format!("name \"{}\"\n", obj.name));
120        out.push_str(&format!("numvert {}\n", obj.positions.len()));
121        for p in &obj.positions {
122            out.push_str(&format!("{:.6} {:.6} {:.6}\n", p[0], p[1], p[2]));
123        }
124        out.push_str(&format!("numsurf {}\n", obj.surfaces.len()));
125        for surf in &obj.surfaces {
126            out.push_str(&format!("SURF 0x{:02X}\n", surf.flags));
127            out.push_str(&format!("mat {}\n", obj.mat_index));
128            out.push_str(&format!("refs {}\n", surf.vertices.len()));
129            for &(idx, u, v) in &surf.vertices {
130                out.push_str(&format!("{} {:.4} {:.4}\n", idx, u, v));
131            }
132        }
133        out.push_str("kids 0\n");
134    }
135    out
136}
137
138/// Estimate the file size.
139pub fn ac3d_size_estimate(doc: &Ac3dExport) -> usize {
140    render_ac3d(doc).len()
141}
142
143/// Validate the document.
144pub fn validate_ac3d(doc: &Ac3dExport) -> bool {
145    for obj in &doc.objects {
146        let n = obj.positions.len() as u32;
147        for surf in &obj.surfaces {
148            for &(idx, _, _) in &surf.vertices {
149                if idx >= n {
150                    return false;
151                }
152            }
153        }
154    }
155    true
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    fn simple_doc() -> Ac3dExport {
163        let mut d = new_ac3d_export();
164        let mat_idx = ac3d_add_material(&mut d, Ac3dMaterial::default());
165        ac3d_add_mesh(
166            &mut d,
167            "tri",
168            vec![[0.0, 0.0, 0.0], [1.0, 0.0, 0.0], [0.0, 1.0, 0.0]],
169            &[0, 1, 2],
170            mat_idx,
171        );
172        d
173    }
174
175    #[test]
176    fn material_count() {
177        let d = simple_doc();
178        assert_eq!(ac3d_material_count(&d), 1);
179    }
180
181    #[test]
182    fn object_count() {
183        let d = simple_doc();
184        assert_eq!(ac3d_object_count(&d), 1);
185    }
186
187    #[test]
188    fn render_starts_with_ac3d() {
189        let d = simple_doc();
190        assert!(render_ac3d(&d).starts_with("AC3Db\n"));
191    }
192
193    #[test]
194    fn render_contains_material() {
195        let d = simple_doc();
196        assert!(render_ac3d(&d).contains("MATERIAL"));
197    }
198
199    #[test]
200    fn render_contains_numvert() {
201        let d = simple_doc();
202        assert!(render_ac3d(&d).contains("numvert 3"));
203    }
204
205    #[test]
206    fn validate_valid() {
207        let d = simple_doc();
208        assert!(validate_ac3d(&d));
209    }
210
211    #[test]
212    fn size_estimate_positive() {
213        let d = simple_doc();
214        assert!(ac3d_size_estimate(&d) > 0);
215    }
216
217    #[test]
218    fn empty_doc_valid() {
219        let d = new_ac3d_export();
220        assert!(validate_ac3d(&d));
221    }
222}