1use lib3mf_core::model::Color;
23use std::collections::HashMap;
24use std::io::{BufRead, BufReader, Read};
25use std::path::Path;
26
27const DEFAULT_GRAY: Color = Color {
29 r: 128,
30 g: 128,
31 b: 128,
32 a: 255,
33};
34
35#[derive(Debug, Clone)]
37pub struct MtlMaterial {
38 pub name: String,
40 pub display_color: Color,
42}
43
44pub fn parse_mtl<R: Read>(reader: R) -> HashMap<String, MtlMaterial> {
56 let buf = BufReader::new(reader);
57 let mut materials: HashMap<String, MtlMaterial> = HashMap::new();
58 let mut current_name: Option<String> = None;
59 let mut current_color: Option<Color> = None;
60
61 for line_result in buf.lines() {
62 let line = match line_result {
63 Ok(l) => l,
64 Err(_) => continue,
65 };
66 let trimmed = line.trim();
67 if trimmed.is_empty() || trimmed.starts_with('#') {
68 continue;
69 }
70
71 let parts: Vec<&str> = trimmed.split_whitespace().collect();
72 if parts.is_empty() {
73 continue;
74 }
75
76 match parts[0] {
77 "newmtl" => {
78 if let Some(name) = current_name.take() {
80 let color = current_color.take().unwrap_or(DEFAULT_GRAY);
81 materials.insert(
82 name.clone(),
83 MtlMaterial {
84 name,
85 display_color: color,
86 },
87 );
88 }
89 if parts.len() >= 2 {
91 current_name = Some(parts[1..].join(" "));
92 }
93 current_color = None;
94 }
95 "Kd" => {
96 if parts.len() >= 4
97 && let (Ok(r), Ok(g), Ok(b)) = (
98 parts[1].parse::<f32>(),
99 parts[2].parse::<f32>(),
100 parts[3].parse::<f32>(),
101 )
102 {
103 current_color = Some(Color::new(
104 (r.clamp(0.0, 1.0) * 255.0).round() as u8,
105 (g.clamp(0.0, 1.0) * 255.0).round() as u8,
106 (b.clamp(0.0, 1.0) * 255.0).round() as u8,
107 255,
108 ));
109 }
110 }
112 directive if directive.starts_with("map_") => {
113 let texture_path = if parts.len() >= 2 {
115 parts[1..].join(" ")
116 } else {
117 String::new()
118 };
119 let mat_name = current_name.as_deref().unwrap_or("unknown");
120 eprintln!(
121 "Warning: texture map '{}' skipped for material '{}' (texture import not supported)",
122 texture_path, mat_name
123 );
124 }
125 _ => {}
127 }
128 }
129
130 if let Some(name) = current_name.take() {
132 let color = current_color.take().unwrap_or(DEFAULT_GRAY);
133 materials.insert(
134 name.clone(),
135 MtlMaterial {
136 name,
137 display_color: color,
138 },
139 );
140 }
141
142 materials
143}
144
145pub fn parse_mtl_file(path: &Path) -> HashMap<String, MtlMaterial> {
151 let file = match std::fs::File::open(path) {
152 Ok(f) => f,
153 Err(e) => {
154 if e.kind() == std::io::ErrorKind::NotFound {
155 eprintln!("Warning: MTL file not found: {}", path.display());
156 } else {
157 eprintln!(
158 "Warning: failed to open MTL file '{}': {}",
159 path.display(),
160 e
161 );
162 }
163 return HashMap::new();
164 }
165 };
166 parse_mtl(file)
167}
168
169#[cfg(test)]
170mod tests {
171 use super::*;
172
173 #[test]
174 fn test_basic_kd_parsing() {
175 let mtl = b"newmtl Red\nKd 1.0 0.0 0.0\n";
176 let materials = parse_mtl(&mtl[..]);
177 assert_eq!(materials.len(), 1);
178 let red = &materials["Red"];
179 assert_eq!(red.name, "Red");
180 assert_eq!(red.display_color, Color::new(255, 0, 0, 255));
181 }
182
183 #[test]
184 fn test_missing_kd_defaults_to_gray() {
185 let mtl = b"newmtl NoColor\n";
186 let materials = parse_mtl(&mtl[..]);
187 assert_eq!(materials.len(), 1);
188 let mat = &materials["NoColor"];
189 assert_eq!(mat.display_color, Color::new(128, 128, 128, 255));
190 }
191
192 #[test]
193 fn test_kd_clamping() {
194 let mtl = b"newmtl Bright\nKd 1.5 -0.5 0.5\n";
195 let materials = parse_mtl(&mtl[..]);
196 let mat = &materials["Bright"];
197 assert_eq!(mat.display_color.r, 255); assert_eq!(mat.display_color.g, 0); assert_eq!(mat.display_color.b, 128); }
201
202 #[test]
203 fn test_map_kd_warning_falls_back() {
204 let mtl = b"newmtl Textured\nKd 0.0 1.0 0.0\nmap_Kd texture.png\n";
206 let materials = parse_mtl(&mtl[..]);
207 let mat = &materials["Textured"];
208 assert_eq!(mat.display_color, Color::new(0, 255, 0, 255));
210 }
211
212 #[test]
213 fn test_map_kd_without_kd_gets_gray() {
214 let mtl = b"newmtl TexturedOnly\nmap_Kd texture.png\n";
216 let materials = parse_mtl(&mtl[..]);
217 let mat = &materials["TexturedOnly"];
218 assert_eq!(mat.display_color, Color::new(128, 128, 128, 255));
219 }
220
221 #[test]
222 fn test_multiple_materials() {
223 let mtl = b"newmtl Red\nKd 1.0 0.0 0.0\nnewmtl Blue\nKd 0.0 0.0 1.0\nnewmtl Green\nKd 0.0 1.0 0.0\n";
224 let materials = parse_mtl(&mtl[..]);
225 assert_eq!(materials.len(), 3);
226 assert_eq!(materials["Red"].display_color, Color::new(255, 0, 0, 255));
227 assert_eq!(materials["Blue"].display_color, Color::new(0, 0, 255, 255));
228 assert_eq!(materials["Green"].display_color, Color::new(0, 255, 0, 255));
229 }
230
231 #[test]
232 fn test_bad_lines_are_skipped() {
233 let mtl = b"newmtl Good\nKd 1.0 0.0 0.0\ngarbage line here\nKd bad bad bad\n";
234 let materials = parse_mtl(&mtl[..]);
235 assert_eq!(materials.len(), 1);
236 assert_eq!(materials["Good"].display_color, Color::new(255, 0, 0, 255));
239 }
240
241 #[test]
242 fn test_empty_input() {
243 let mtl = b"";
244 let materials = parse_mtl(&mtl[..]);
245 assert!(materials.is_empty());
246 }
247
248 #[test]
249 fn test_comments_and_blank_lines() {
250 let mtl = b"# comment\n\nnewmtl Mat\n# another comment\nKd 0.5 0.5 0.5\n\n";
251 let materials = parse_mtl(&mtl[..]);
252 assert_eq!(materials.len(), 1);
253 assert_eq!(
254 materials["Mat"].display_color,
255 Color::new(128, 128, 128, 255)
256 );
257 }
258
259 #[test]
260 fn test_ignored_directives() {
261 let mtl =
262 b"newmtl Fancy\nKa 0.1 0.1 0.1\nKd 1.0 0.0 0.0\nKs 1.0 1.0 1.0\nNs 100.0\nNi 1.5\nillum 2\nd 0.5\nTr 0.5\nKe 0.0 0.0 0.0\n";
263 let materials = parse_mtl(&mtl[..]);
264 assert_eq!(materials.len(), 1);
265 assert_eq!(materials["Fancy"].display_color, Color::new(255, 0, 0, 255));
267 }
268
269 #[test]
270 fn test_material_name_with_spaces() {
271 let mtl = b"newmtl My Material Name\nKd 0.0 0.0 1.0\n";
272 let materials = parse_mtl(&mtl[..]);
273 assert_eq!(materials.len(), 1);
274 assert!(materials.contains_key("My Material Name"));
275 }
276
277 #[test]
278 fn test_parse_mtl_file_nonexistent() {
279 let materials = parse_mtl_file(Path::new("/tmp/nonexistent_test_mtl_file_12345.mtl"));
280 assert!(materials.is_empty());
281 }
282}