1#[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#[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 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#[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}