Skip to main content

oxihuman_export/
material_library.rs

1// Copyright (C) 2026 COOLJAPAN OU (Team KitaSan)
2// SPDX-License-Identifier: Apache-2.0
3
4//! Material library serialization and management.
5
6// ── Types ─────────────────────────────────────────────────────────────────────
7
8#[allow(dead_code)]
9#[derive(Clone, PartialEq, Debug)]
10pub enum MatAlphaMode {
11    Opaque,
12    Mask,
13    Blend,
14}
15
16#[allow(dead_code)]
17#[derive(Clone)]
18pub struct PbrMaterialDef {
19    pub name: String,
20    pub base_color: [f32; 4],
21    pub metallic: f32,
22    pub roughness: f32,
23    pub emissive: [f32; 3],
24    pub alpha_mode: MatAlphaMode,
25    pub double_sided: bool,
26    pub texture_paths: Vec<String>,
27}
28
29#[allow(dead_code)]
30pub struct MatLibrary {
31    pub name: String,
32    pub materials: Vec<PbrMaterialDef>,
33    pub version: u32,
34}
35
36// ── Functions ─────────────────────────────────────────────────────────────────
37
38#[allow(dead_code)]
39pub fn new_material_library(name: &str) -> MatLibrary {
40    MatLibrary {
41        name: name.to_string(),
42        materials: Vec::new(),
43        version: 1,
44    }
45}
46
47#[allow(dead_code)]
48pub fn add_material(lib: &mut MatLibrary, mat: PbrMaterialDef) -> usize {
49    let idx = lib.materials.len();
50    lib.materials.push(mat);
51    idx
52}
53
54#[allow(dead_code)]
55pub fn get_material<'a>(lib: &'a MatLibrary, name: &str) -> Option<&'a PbrMaterialDef> {
56    lib.materials.iter().find(|m| m.name == name)
57}
58
59#[allow(dead_code)]
60pub fn default_pbr_material(name: &str) -> PbrMaterialDef {
61    PbrMaterialDef {
62        name: name.to_string(),
63        base_color: [0.5, 0.5, 0.5, 1.0],
64        metallic: 0.0,
65        roughness: 0.5,
66        emissive: [0.0, 0.0, 0.0],
67        alpha_mode: MatAlphaMode::Opaque,
68        double_sided: false,
69        texture_paths: Vec::new(),
70    }
71}
72
73#[allow(dead_code)]
74pub fn serialize_library_json(lib: &MatLibrary) -> String {
75    let mut out = String::new();
76    out.push_str(&format!(
77        "{{\"name\":\"{}\",\"version\":{},\"materials\":[",
78        lib.name, lib.version
79    ));
80    for (i, m) in lib.materials.iter().enumerate() {
81        if i > 0 {
82            out.push(',');
83        }
84        let bc = m.base_color;
85        let alpha_str = match m.alpha_mode {
86            MatAlphaMode::Opaque => "Opaque",
87            MatAlphaMode::Mask => "Mask",
88            MatAlphaMode::Blend => "Blend",
89        };
90        out.push_str(&format!(
91            "{{\"name\":\"{}\",\"base_color\":[{},{},{},{}],\"metallic\":{},\"roughness\":{},\"emissive\":[{},{},{}],\"alpha_mode\":\"{}\",\"double_sided\":{}}}",
92            m.name,
93            bc[0], bc[1], bc[2], bc[3],
94            m.metallic,
95            m.roughness,
96            m.emissive[0], m.emissive[1], m.emissive[2],
97            alpha_str,
98            m.double_sided
99        ));
100    }
101    out.push_str("]}");
102    out
103}
104
105#[allow(dead_code)]
106pub fn deserialize_library_json(_json: &str) -> Option<MatLibrary> {
107    // Stub — full JSON parsing without external deps is out of scope
108    None
109}
110
111#[allow(dead_code)]
112pub fn blend_materials(a: &PbrMaterialDef, b: &PbrMaterialDef, t: f32) -> PbrMaterialDef {
113    let lerp = |x: f32, y: f32| x + (y - x) * t;
114    PbrMaterialDef {
115        name: format!("{}_blend_{}", a.name, b.name),
116        base_color: [
117            lerp(a.base_color[0], b.base_color[0]),
118            lerp(a.base_color[1], b.base_color[1]),
119            lerp(a.base_color[2], b.base_color[2]),
120            lerp(a.base_color[3], b.base_color[3]),
121        ],
122        metallic: lerp(a.metallic, b.metallic),
123        roughness: lerp(a.roughness, b.roughness),
124        emissive: [
125            lerp(a.emissive[0], b.emissive[0]),
126            lerp(a.emissive[1], b.emissive[1]),
127            lerp(a.emissive[2], b.emissive[2]),
128        ],
129        alpha_mode: if t < 0.5 {
130            a.alpha_mode.clone()
131        } else {
132            b.alpha_mode.clone()
133        },
134        double_sided: if t < 0.5 {
135            a.double_sided
136        } else {
137            b.double_sided
138        },
139        texture_paths: Vec::new(),
140    }
141}
142
143#[allow(dead_code)]
144pub fn material_is_transparent(mat: &PbrMaterialDef) -> bool {
145    matches!(mat.alpha_mode, MatAlphaMode::Blend)
146}
147
148#[allow(dead_code)]
149pub fn count_textured(lib: &MatLibrary) -> usize {
150    lib.materials
151        .iter()
152        .filter(|m| !m.texture_paths.is_empty())
153        .count()
154}
155
156#[allow(dead_code)]
157pub fn remove_material(lib: &mut MatLibrary, name: &str) -> bool {
158    let before = lib.materials.len();
159    lib.materials.retain(|m| m.name != name);
160    lib.materials.len() < before
161}
162
163#[allow(dead_code)]
164pub fn list_names(lib: &MatLibrary) -> Vec<&str> {
165    lib.materials.iter().map(|m| m.name.as_str()).collect()
166}
167
168#[allow(dead_code)]
169pub fn material_roughness_category(mat: &PbrMaterialDef) -> &'static str {
170    if mat.roughness >= 0.9 {
171        "matte"
172    } else if mat.roughness >= 0.5 {
173        "satin"
174    } else if mat.roughness >= 0.1 {
175        "glossy"
176    } else {
177        "mirror"
178    }
179}
180
181#[allow(dead_code)]
182pub fn export_material_ids(lib: &MatLibrary) -> Vec<(usize, String)> {
183    lib.materials
184        .iter()
185        .enumerate()
186        .map(|(i, m)| (i, m.name.clone()))
187        .collect()
188}
189
190// ── Tests ─────────────────────────────────────────────────────────────────────
191
192#[cfg(test)]
193mod tests {
194    use super::*;
195
196    #[test]
197    fn test_new_library() {
198        let lib = new_material_library("TestLib");
199        assert_eq!(lib.name, "TestLib");
200        assert!(lib.materials.is_empty());
201        assert_eq!(lib.version, 1);
202    }
203
204    #[test]
205    fn test_add_and_get_material() {
206        let mut lib = new_material_library("Lib");
207        let mat = default_pbr_material("Gray");
208        let idx = add_material(&mut lib, mat);
209        assert_eq!(idx, 0);
210        let found = get_material(&lib, "Gray");
211        assert!(found.is_some());
212        assert_eq!(found.expect("should succeed").name, "Gray");
213    }
214
215    #[test]
216    fn test_get_missing_material() {
217        let lib = new_material_library("Lib");
218        assert!(get_material(&lib, "Missing").is_none());
219    }
220
221    #[test]
222    fn test_add_multiple_materials() {
223        let mut lib = new_material_library("Lib");
224        let a = default_pbr_material("A");
225        let b = default_pbr_material("B");
226        let ia = add_material(&mut lib, a);
227        let ib = add_material(&mut lib, b);
228        assert_eq!(ia, 0);
229        assert_eq!(ib, 1);
230        assert_eq!(lib.materials.len(), 2);
231    }
232
233    #[test]
234    fn test_serialize_nonempty() {
235        let mut lib = new_material_library("Lib");
236        let mat = default_pbr_material("Metal");
237        add_material(&mut lib, mat);
238        let json = serialize_library_json(&lib);
239        assert!(!json.is_empty());
240        assert!(json.contains("Metal"));
241        assert!(json.contains("Lib"));
242    }
243
244    #[test]
245    fn test_serialize_contains_fields() {
246        let mut lib = new_material_library("MyLib");
247        let mat = default_pbr_material("Base");
248        add_material(&mut lib, mat);
249        let json = serialize_library_json(&lib);
250        assert!(json.contains("base_color"));
251        assert!(json.contains("metallic"));
252        assert!(json.contains("roughness"));
253    }
254
255    #[test]
256    fn test_deserialize_stub_returns_none() {
257        let result = deserialize_library_json("{\"name\":\"Lib\"}");
258        assert!(result.is_none());
259    }
260
261    #[test]
262    fn test_blend_materials() {
263        let a = default_pbr_material("A");
264        let mut b = default_pbr_material("B");
265        b.metallic = 1.0;
266        b.roughness = 1.0;
267        let blended = blend_materials(&a, &b, 0.5);
268        assert!((blended.metallic - 0.5).abs() < 1e-5);
269        assert!((blended.roughness - 0.75).abs() < 1e-5);
270    }
271
272    #[test]
273    fn test_blend_at_zero() {
274        let a = default_pbr_material("A");
275        let b = default_pbr_material("B");
276        let blended = blend_materials(&a, &b, 0.0);
277        assert!((blended.metallic - a.metallic).abs() < 1e-5);
278    }
279
280    #[test]
281    fn test_material_is_transparent() {
282        let mut mat = default_pbr_material("Glass");
283        mat.alpha_mode = MatAlphaMode::Blend;
284        assert!(material_is_transparent(&mat));
285        let opaque = default_pbr_material("Opaque");
286        assert!(!material_is_transparent(&opaque));
287    }
288
289    #[test]
290    fn test_count_textured() {
291        let mut lib = new_material_library("Lib");
292        let mut mat = default_pbr_material("Textured");
293        mat.texture_paths.push("tex.png".to_string());
294        add_material(&mut lib, mat);
295        add_material(&mut lib, default_pbr_material("Plain"));
296        assert_eq!(count_textured(&lib), 1);
297    }
298
299    #[test]
300    fn test_remove_material() {
301        let mut lib = new_material_library("Lib");
302        add_material(&mut lib, default_pbr_material("ToRemove"));
303        add_material(&mut lib, default_pbr_material("Keep"));
304        let removed = remove_material(&mut lib, "ToRemove");
305        assert!(removed);
306        assert_eq!(lib.materials.len(), 1);
307        assert_eq!(lib.materials[0].name, "Keep");
308    }
309
310    #[test]
311    fn test_remove_nonexistent() {
312        let mut lib = new_material_library("Lib");
313        assert!(!remove_material(&mut lib, "Ghost"));
314    }
315
316    #[test]
317    fn test_roughness_category() {
318        let mut mat = default_pbr_material("M");
319        mat.roughness = 0.95;
320        assert_eq!(material_roughness_category(&mat), "matte");
321        mat.roughness = 0.6;
322        assert_eq!(material_roughness_category(&mat), "satin");
323        mat.roughness = 0.3;
324        assert_eq!(material_roughness_category(&mat), "glossy");
325        mat.roughness = 0.05;
326        assert_eq!(material_roughness_category(&mat), "mirror");
327    }
328
329    #[test]
330    fn test_export_material_ids() {
331        let mut lib = new_material_library("Lib");
332        add_material(&mut lib, default_pbr_material("A"));
333        add_material(&mut lib, default_pbr_material("B"));
334        let ids = export_material_ids(&lib);
335        assert_eq!(ids.len(), 2);
336        assert_eq!(ids[0], (0, "A".to_string()));
337        assert_eq!(ids[1], (1, "B".to_string()));
338    }
339
340    #[test]
341    fn test_list_names() {
342        let mut lib = new_material_library("Lib");
343        add_material(&mut lib, default_pbr_material("X"));
344        add_material(&mut lib, default_pbr_material("Y"));
345        let names = list_names(&lib);
346        assert_eq!(names, vec!["X", "Y"]);
347    }
348}