#[allow(dead_code)]
#[derive(Clone, Debug, PartialEq)]
pub enum MaterialProperty {
Float(f32),
Color([f32; 4]),
TexturePath(String),
}
#[allow(dead_code)]
#[derive(Clone, Debug)]
pub struct ExportMaterial {
pub name: String,
pub properties: Vec<(String, MaterialProperty)>,
}
#[allow(dead_code)]
pub struct MaterialExportConfig {
pub pretty_print: bool,
pub include_defaults: bool,
pub gltf_compatible: bool,
}
#[allow(dead_code)]
pub struct MaterialExportBundle {
pub materials: Vec<ExportMaterial>,
}
#[allow(dead_code)]
pub type PropertyLookup<'a> = Option<&'a MaterialProperty>;
#[allow(dead_code)]
pub type ValidationResult = Vec<String>;
#[allow(dead_code)]
pub fn default_material_export_config() -> MaterialExportConfig {
MaterialExportConfig {
pretty_print: true,
include_defaults: false,
gltf_compatible: true,
}
}
#[allow(dead_code)]
pub fn new_export_material(name: &str) -> ExportMaterial {
ExportMaterial {
name: name.to_string(),
properties: Vec::new(),
}
}
#[allow(dead_code)]
pub fn set_property_float(mat: &mut ExportMaterial, key: &str, value: f32) {
remove_property(mat, key);
mat.properties
.push((key.to_string(), MaterialProperty::Float(value)));
}
#[allow(dead_code)]
pub fn set_property_color(mat: &mut ExportMaterial, key: &str, rgba: [f32; 4]) {
remove_property(mat, key);
mat.properties
.push((key.to_string(), MaterialProperty::Color(rgba)));
}
#[allow(dead_code)]
pub fn set_property_texture_path(mat: &mut ExportMaterial, key: &str, path: &str) {
remove_property(mat, key);
mat.properties.push((
key.to_string(),
MaterialProperty::TexturePath(path.to_string()),
));
}
#[allow(dead_code)]
pub fn get_property<'a>(mat: &'a ExportMaterial, key: &str) -> PropertyLookup<'a> {
mat.properties
.iter()
.find(|(k, _)| k == key)
.map(|(_, v)| v)
}
#[allow(dead_code)]
pub fn material_to_json(mat: &ExportMaterial) -> String {
let mut out = String::from("{\n");
out.push_str(&format!(" \"name\": \"{}\",\n", mat.name));
out.push_str(" \"properties\": {\n");
for (i, (k, v)) in mat.properties.iter().enumerate() {
let comma = if i + 1 < mat.properties.len() {
","
} else {
""
};
match v {
MaterialProperty::Float(f) => {
out.push_str(&format!(" \"{k}\": {f:.6}{comma}\n"));
}
MaterialProperty::Color(c) => {
out.push_str(&format!(
" \"{k}\": [{:.4}, {:.4}, {:.4}, {:.4}]{comma}\n",
c[0], c[1], c[2], c[3]
));
}
MaterialProperty::TexturePath(p) => {
out.push_str(&format!(" \"{k}\": \"{p}\"{comma}\n"));
}
}
}
out.push_str(" }\n}");
out
}
#[allow(dead_code)]
pub fn material_to_gltf_json(mat: &ExportMaterial) -> String {
let mut out = String::from("{\n");
out.push_str(&format!(" \"name\": \"{}\",\n", mat.name));
out.push_str(" \"pbrMetallicRoughness\": {\n");
let base_color = mat
.properties
.iter()
.find(|(k, _)| k == "baseColor")
.and_then(|(_, v)| match v {
MaterialProperty::Color(c) => Some(*c),
_ => None,
})
.unwrap_or([1.0, 1.0, 1.0, 1.0]);
let metallic = mat
.properties
.iter()
.find(|(k, _)| k == "metallic")
.and_then(|(_, v)| match v {
MaterialProperty::Float(f) => Some(*f),
_ => None,
})
.unwrap_or(0.0);
let roughness = mat
.properties
.iter()
.find(|(k, _)| k == "roughness")
.and_then(|(_, v)| match v {
MaterialProperty::Float(f) => Some(*f),
_ => None,
})
.unwrap_or(0.5);
out.push_str(&format!(
" \"baseColorFactor\": [{:.4}, {:.4}, {:.4}, {:.4}],\n",
base_color[0], base_color[1], base_color[2], base_color[3]
));
out.push_str(&format!(" \"metallicFactor\": {metallic:.4},\n"));
out.push_str(&format!(" \"roughnessFactor\": {roughness:.4}\n"));
out.push_str(" }\n}");
out
}
#[allow(dead_code)]
pub fn material_count(bundle: &MaterialExportBundle) -> usize {
bundle.materials.len()
}
#[allow(dead_code)]
pub fn add_material_to_bundle(bundle: &mut MaterialExportBundle, mat: ExportMaterial) {
bundle.materials.push(mat);
}
#[allow(dead_code)]
pub fn material_property_count(mat: &ExportMaterial) -> usize {
mat.properties.len()
}
#[allow(dead_code)]
pub fn validate_material(mat: &ExportMaterial) -> ValidationResult {
let mut warnings = Vec::new();
if mat.name.is_empty() {
warnings.push("Material name is empty".to_string());
}
for (k, v) in &mat.properties {
if k.is_empty() {
warnings.push("Empty property key".to_string());
}
if let MaterialProperty::Float(f) = v {
if f.is_nan() || f.is_infinite() {
warnings.push(format!("Property '{k}' has non-finite value"));
}
}
if let MaterialProperty::Color(c) = v {
for (ci, &ch) in c.iter().enumerate() {
if ch.is_nan() || ch.is_infinite() {
warnings.push(format!("Property '{k}' color channel {ci} is non-finite"));
}
}
}
}
warnings
}
#[allow(dead_code)]
pub fn default_pbr_material(name: &str) -> ExportMaterial {
let mut mat = new_export_material(name);
set_property_color(&mut mat, "baseColor", [0.8, 0.8, 0.8, 1.0]);
set_property_float(&mut mat, "metallic", 0.0);
set_property_float(&mut mat, "roughness", 0.5);
set_property_float(&mut mat, "emissive", 0.0);
mat
}
fn remove_property(mat: &mut ExportMaterial, key: &str) {
mat.properties.retain(|(k, _)| k != key);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn default_config() {
let cfg = default_material_export_config();
assert!(cfg.pretty_print);
assert!(!cfg.include_defaults);
assert!(cfg.gltf_compatible);
}
#[test]
fn new_material_empty() {
let mat = new_export_material("Skin");
assert_eq!(mat.name, "Skin");
assert!(mat.properties.is_empty());
}
#[test]
fn set_get_float() {
let mut mat = new_export_material("M");
set_property_float(&mut mat, "roughness", 0.7);
match get_property(&mat, "roughness") {
Some(MaterialProperty::Float(f)) => assert!((*f - 0.7).abs() < 1e-6),
_ => panic!("expected float property"),
}
}
#[test]
fn set_get_color() {
let mut mat = new_export_material("M");
set_property_color(&mut mat, "baseColor", [1.0, 0.5, 0.0, 1.0]);
match get_property(&mat, "baseColor") {
Some(MaterialProperty::Color(c)) => {
assert!((c[0] - 1.0).abs() < 1e-6);
assert!((c[1] - 0.5).abs() < 1e-6);
}
_ => panic!("expected color property"),
}
}
#[test]
fn set_get_texture() {
let mut mat = new_export_material("M");
set_property_texture_path(&mut mat, "diffuseMap", "/tex/diffuse.png");
match get_property(&mat, "diffuseMap") {
Some(MaterialProperty::TexturePath(p)) => assert_eq!(p, "/tex/diffuse.png"),
_ => panic!("expected texture property"),
}
}
#[test]
fn get_missing_property() {
let mat = new_export_material("M");
assert!(get_property(&mat, "nonexistent").is_none());
}
#[test]
fn overwrite_property() {
let mut mat = new_export_material("M");
set_property_float(&mut mat, "roughness", 0.5);
set_property_float(&mut mat, "roughness", 0.9);
assert_eq!(material_property_count(&mat), 1);
match get_property(&mat, "roughness") {
Some(MaterialProperty::Float(f)) => assert!((*f - 0.9).abs() < 1e-6),
_ => panic!("expected float"),
}
}
#[test]
fn material_to_json_contains_name() {
let mat = new_export_material("Skin");
let json = material_to_json(&mat);
assert!(json.contains("\"name\": \"Skin\""));
}
#[test]
fn material_to_gltf_json_contains_pbr() {
let mat = default_pbr_material("Default");
let json = material_to_gltf_json(&mat);
assert!(json.contains("pbrMetallicRoughness"));
assert!(json.contains("baseColorFactor"));
assert!(json.contains("metallicFactor"));
}
#[test]
fn bundle_count() {
let mut bundle = MaterialExportBundle {
materials: Vec::new(),
};
assert_eq!(material_count(&bundle), 0);
add_material_to_bundle(&mut bundle, new_export_material("A"));
add_material_to_bundle(&mut bundle, new_export_material("B"));
assert_eq!(material_count(&bundle), 2);
}
#[test]
fn property_count() {
let mat = default_pbr_material("P");
assert_eq!(material_property_count(&mat), 4);
}
#[test]
fn validate_valid_material() {
let mat = default_pbr_material("Valid");
let warnings = validate_material(&mat);
assert!(warnings.is_empty());
}
#[test]
fn validate_empty_name() {
let mat = new_export_material("");
let warnings = validate_material(&mat);
assert!(warnings.iter().any(|w| w.contains("name is empty")));
}
#[test]
fn validate_nan_float() {
let mut mat = new_export_material("Bad");
set_property_float(&mut mat, "roughness", f32::NAN);
let warnings = validate_material(&mat);
assert!(warnings.iter().any(|w| w.contains("non-finite")));
}
#[test]
fn default_pbr_has_expected_properties() {
let mat = default_pbr_material("Std");
assert!(get_property(&mat, "baseColor").is_some());
assert!(get_property(&mat, "metallic").is_some());
assert!(get_property(&mat, "roughness").is_some());
assert!(get_property(&mat, "emissive").is_some());
}
#[test]
fn gltf_json_default_metallic_zero() {
let mut mat = new_export_material("M");
set_property_color(&mut mat, "baseColor", [1.0, 1.0, 1.0, 1.0]);
let json = material_to_gltf_json(&mat);
assert!(json.contains("\"metallicFactor\": 0.0000"));
}
}