1#[allow(dead_code)]
8#[derive(Clone, Debug, PartialEq)]
9pub enum MaterialProperty {
10 Float(f32),
11 Color([f32; 4]),
12 TexturePath(String),
13}
14
15#[allow(dead_code)]
17#[derive(Clone, Debug)]
18pub struct ExportMaterial {
19 pub name: String,
20 pub properties: Vec<(String, MaterialProperty)>,
21}
22
23#[allow(dead_code)]
25pub struct MaterialExportConfig {
26 pub pretty_print: bool,
27 pub include_defaults: bool,
28 pub gltf_compatible: bool,
29}
30
31#[allow(dead_code)]
33pub struct MaterialExportBundle {
34 pub materials: Vec<ExportMaterial>,
35}
36
37#[allow(dead_code)]
39pub type PropertyLookup<'a> = Option<&'a MaterialProperty>;
40
41#[allow(dead_code)]
43pub type ValidationResult = Vec<String>;
44
45#[allow(dead_code)]
49pub fn default_material_export_config() -> MaterialExportConfig {
50 MaterialExportConfig {
51 pretty_print: true,
52 include_defaults: false,
53 gltf_compatible: true,
54 }
55}
56
57#[allow(dead_code)]
59pub fn new_export_material(name: &str) -> ExportMaterial {
60 ExportMaterial {
61 name: name.to_string(),
62 properties: Vec::new(),
63 }
64}
65
66#[allow(dead_code)]
68pub fn set_property_float(mat: &mut ExportMaterial, key: &str, value: f32) {
69 remove_property(mat, key);
70 mat.properties
71 .push((key.to_string(), MaterialProperty::Float(value)));
72}
73
74#[allow(dead_code)]
76pub fn set_property_color(mat: &mut ExportMaterial, key: &str, rgba: [f32; 4]) {
77 remove_property(mat, key);
78 mat.properties
79 .push((key.to_string(), MaterialProperty::Color(rgba)));
80}
81
82#[allow(dead_code)]
84pub fn set_property_texture_path(mat: &mut ExportMaterial, key: &str, path: &str) {
85 remove_property(mat, key);
86 mat.properties.push((
87 key.to_string(),
88 MaterialProperty::TexturePath(path.to_string()),
89 ));
90}
91
92#[allow(dead_code)]
94pub fn get_property<'a>(mat: &'a ExportMaterial, key: &str) -> PropertyLookup<'a> {
95 mat.properties
96 .iter()
97 .find(|(k, _)| k == key)
98 .map(|(_, v)| v)
99}
100
101#[allow(dead_code)]
103pub fn material_to_json(mat: &ExportMaterial) -> String {
104 let mut out = String::from("{\n");
105 out.push_str(&format!(" \"name\": \"{}\",\n", mat.name));
106 out.push_str(" \"properties\": {\n");
107 for (i, (k, v)) in mat.properties.iter().enumerate() {
108 let comma = if i + 1 < mat.properties.len() {
109 ","
110 } else {
111 ""
112 };
113 match v {
114 MaterialProperty::Float(f) => {
115 out.push_str(&format!(" \"{k}\": {f:.6}{comma}\n"));
116 }
117 MaterialProperty::Color(c) => {
118 out.push_str(&format!(
119 " \"{k}\": [{:.4}, {:.4}, {:.4}, {:.4}]{comma}\n",
120 c[0], c[1], c[2], c[3]
121 ));
122 }
123 MaterialProperty::TexturePath(p) => {
124 out.push_str(&format!(" \"{k}\": \"{p}\"{comma}\n"));
125 }
126 }
127 }
128 out.push_str(" }\n}");
129 out
130}
131
132#[allow(dead_code)]
134pub fn material_to_gltf_json(mat: &ExportMaterial) -> String {
135 let mut out = String::from("{\n");
136 out.push_str(&format!(" \"name\": \"{}\",\n", mat.name));
137 out.push_str(" \"pbrMetallicRoughness\": {\n");
138
139 let base_color = mat
140 .properties
141 .iter()
142 .find(|(k, _)| k == "baseColor")
143 .and_then(|(_, v)| match v {
144 MaterialProperty::Color(c) => Some(*c),
145 _ => None,
146 })
147 .unwrap_or([1.0, 1.0, 1.0, 1.0]);
148
149 let metallic = mat
150 .properties
151 .iter()
152 .find(|(k, _)| k == "metallic")
153 .and_then(|(_, v)| match v {
154 MaterialProperty::Float(f) => Some(*f),
155 _ => None,
156 })
157 .unwrap_or(0.0);
158
159 let roughness = mat
160 .properties
161 .iter()
162 .find(|(k, _)| k == "roughness")
163 .and_then(|(_, v)| match v {
164 MaterialProperty::Float(f) => Some(*f),
165 _ => None,
166 })
167 .unwrap_or(0.5);
168
169 out.push_str(&format!(
170 " \"baseColorFactor\": [{:.4}, {:.4}, {:.4}, {:.4}],\n",
171 base_color[0], base_color[1], base_color[2], base_color[3]
172 ));
173 out.push_str(&format!(" \"metallicFactor\": {metallic:.4},\n"));
174 out.push_str(&format!(" \"roughnessFactor\": {roughness:.4}\n"));
175 out.push_str(" }\n}");
176 out
177}
178
179#[allow(dead_code)]
181pub fn material_count(bundle: &MaterialExportBundle) -> usize {
182 bundle.materials.len()
183}
184
185#[allow(dead_code)]
187pub fn add_material_to_bundle(bundle: &mut MaterialExportBundle, mat: ExportMaterial) {
188 bundle.materials.push(mat);
189}
190
191#[allow(dead_code)]
193pub fn material_property_count(mat: &ExportMaterial) -> usize {
194 mat.properties.len()
195}
196
197#[allow(dead_code)]
199pub fn validate_material(mat: &ExportMaterial) -> ValidationResult {
200 let mut warnings = Vec::new();
201 if mat.name.is_empty() {
202 warnings.push("Material name is empty".to_string());
203 }
204 for (k, v) in &mat.properties {
205 if k.is_empty() {
206 warnings.push("Empty property key".to_string());
207 }
208 if let MaterialProperty::Float(f) = v {
209 if f.is_nan() || f.is_infinite() {
210 warnings.push(format!("Property '{k}' has non-finite value"));
211 }
212 }
213 if let MaterialProperty::Color(c) = v {
214 for (ci, &ch) in c.iter().enumerate() {
215 if ch.is_nan() || ch.is_infinite() {
216 warnings.push(format!("Property '{k}' color channel {ci} is non-finite"));
217 }
218 }
219 }
220 }
221 warnings
222}
223
224#[allow(dead_code)]
226pub fn default_pbr_material(name: &str) -> ExportMaterial {
227 let mut mat = new_export_material(name);
228 set_property_color(&mut mat, "baseColor", [0.8, 0.8, 0.8, 1.0]);
229 set_property_float(&mut mat, "metallic", 0.0);
230 set_property_float(&mut mat, "roughness", 0.5);
231 set_property_float(&mut mat, "emissive", 0.0);
232 mat
233}
234
235fn remove_property(mat: &mut ExportMaterial, key: &str) {
238 mat.properties.retain(|(k, _)| k != key);
239}
240
241#[cfg(test)]
244mod tests {
245 use super::*;
246
247 #[test]
248 fn default_config() {
249 let cfg = default_material_export_config();
250 assert!(cfg.pretty_print);
251 assert!(!cfg.include_defaults);
252 assert!(cfg.gltf_compatible);
253 }
254
255 #[test]
256 fn new_material_empty() {
257 let mat = new_export_material("Skin");
258 assert_eq!(mat.name, "Skin");
259 assert!(mat.properties.is_empty());
260 }
261
262 #[test]
263 fn set_get_float() {
264 let mut mat = new_export_material("M");
265 set_property_float(&mut mat, "roughness", 0.7);
266 match get_property(&mat, "roughness") {
267 Some(MaterialProperty::Float(f)) => assert!((*f - 0.7).abs() < 1e-6),
268 _ => panic!("expected float property"),
269 }
270 }
271
272 #[test]
273 fn set_get_color() {
274 let mut mat = new_export_material("M");
275 set_property_color(&mut mat, "baseColor", [1.0, 0.5, 0.0, 1.0]);
276 match get_property(&mat, "baseColor") {
277 Some(MaterialProperty::Color(c)) => {
278 assert!((c[0] - 1.0).abs() < 1e-6);
279 assert!((c[1] - 0.5).abs() < 1e-6);
280 }
281 _ => panic!("expected color property"),
282 }
283 }
284
285 #[test]
286 fn set_get_texture() {
287 let mut mat = new_export_material("M");
288 set_property_texture_path(&mut mat, "diffuseMap", "/tex/diffuse.png");
289 match get_property(&mat, "diffuseMap") {
290 Some(MaterialProperty::TexturePath(p)) => assert_eq!(p, "/tex/diffuse.png"),
291 _ => panic!("expected texture property"),
292 }
293 }
294
295 #[test]
296 fn get_missing_property() {
297 let mat = new_export_material("M");
298 assert!(get_property(&mat, "nonexistent").is_none());
299 }
300
301 #[test]
302 fn overwrite_property() {
303 let mut mat = new_export_material("M");
304 set_property_float(&mut mat, "roughness", 0.5);
305 set_property_float(&mut mat, "roughness", 0.9);
306 assert_eq!(material_property_count(&mat), 1);
307 match get_property(&mat, "roughness") {
308 Some(MaterialProperty::Float(f)) => assert!((*f - 0.9).abs() < 1e-6),
309 _ => panic!("expected float"),
310 }
311 }
312
313 #[test]
314 fn material_to_json_contains_name() {
315 let mat = new_export_material("Skin");
316 let json = material_to_json(&mat);
317 assert!(json.contains("\"name\": \"Skin\""));
318 }
319
320 #[test]
321 fn material_to_gltf_json_contains_pbr() {
322 let mat = default_pbr_material("Default");
323 let json = material_to_gltf_json(&mat);
324 assert!(json.contains("pbrMetallicRoughness"));
325 assert!(json.contains("baseColorFactor"));
326 assert!(json.contains("metallicFactor"));
327 }
328
329 #[test]
330 fn bundle_count() {
331 let mut bundle = MaterialExportBundle {
332 materials: Vec::new(),
333 };
334 assert_eq!(material_count(&bundle), 0);
335 add_material_to_bundle(&mut bundle, new_export_material("A"));
336 add_material_to_bundle(&mut bundle, new_export_material("B"));
337 assert_eq!(material_count(&bundle), 2);
338 }
339
340 #[test]
341 fn property_count() {
342 let mat = default_pbr_material("P");
343 assert_eq!(material_property_count(&mat), 4);
344 }
345
346 #[test]
347 fn validate_valid_material() {
348 let mat = default_pbr_material("Valid");
349 let warnings = validate_material(&mat);
350 assert!(warnings.is_empty());
351 }
352
353 #[test]
354 fn validate_empty_name() {
355 let mat = new_export_material("");
356 let warnings = validate_material(&mat);
357 assert!(warnings.iter().any(|w| w.contains("name is empty")));
358 }
359
360 #[test]
361 fn validate_nan_float() {
362 let mut mat = new_export_material("Bad");
363 set_property_float(&mut mat, "roughness", f32::NAN);
364 let warnings = validate_material(&mat);
365 assert!(warnings.iter().any(|w| w.contains("non-finite")));
366 }
367
368 #[test]
369 fn default_pbr_has_expected_properties() {
370 let mat = default_pbr_material("Std");
371 assert!(get_property(&mat, "baseColor").is_some());
372 assert!(get_property(&mat, "metallic").is_some());
373 assert!(get_property(&mat, "roughness").is_some());
374 assert!(get_property(&mat, "emissive").is_some());
375 }
376
377 #[test]
378 fn gltf_json_default_metallic_zero() {
379 let mut mat = new_export_material("M");
380 set_property_color(&mut mat, "baseColor", [1.0, 1.0, 1.0, 1.0]);
381 let json = material_to_gltf_json(&mat);
382 assert!(json.contains("\"metallicFactor\": 0.0000"));
383 }
384}