1use crate::error::{Lib3mfError, Result};
2use crate::model::{
3 BaseMaterialsGroup, ColorGroup, CompositeMaterials, Geometry, Model, MultiProperties, Object,
4 Texture2DGroup, Unit,
5};
6use crate::parser::boolean_parser::parse_boolean_shape;
7use crate::parser::build_parser::parse_build;
8use crate::parser::component_parser::parse_components;
9use crate::parser::displacement_parser::{parse_displacement_2d, parse_displacement_mesh};
10use crate::parser::material_parser::{
11 parse_base_materials, parse_color_group, parse_composite_materials, parse_multi_properties,
12 parse_texture_2d_group,
13};
14use crate::parser::mesh_parser::parse_mesh;
15use crate::parser::slice_parser::parse_slice_stack_content;
16use crate::parser::volumetric_parser::parse_volumetric_stack_content;
17use crate::parser::xml_parser::{XmlParser, get_attribute, get_attribute_f32, get_attribute_u32};
18use quick_xml::events::Event;
19use std::io::BufRead;
20
21pub fn parse_model<R: BufRead>(reader: R) -> Result<Model> {
22 let mut parser = XmlParser::new(reader);
23 let mut model = Model::default();
24 let mut seen_model_element = false;
25 let mut seen_build_element = false;
26 let mut model_ended = false;
27
28 loop {
29 match parser.read_next_event()? {
30 Event::Start(e) => match e.name().as_ref() {
31 b"model" => {
32 if seen_model_element {
33 return Err(Lib3mfError::Validation(
34 "Multiple <model> elements found. Only one <model> element is allowed per document".to_string(),
35 ));
36 }
37 if model_ended {
38 return Err(Lib3mfError::Validation(
39 "Multiple <model> elements found. Only one <model> element is allowed per document".to_string(),
40 ));
41 }
42 seen_model_element = true;
43
44 if get_attribute(&e, b"xml:space").is_some() {
47 return Err(Lib3mfError::Validation(
48 "The xml:space attribute is not allowed on the <model> element in 3MF files".to_string(),
49 ));
50 }
51
52 if let Some(unit_str) = get_attribute(&e, b"unit") {
53 model.unit = match unit_str.as_ref() {
54 "micron" => Unit::Micron,
55 "millimeter" => Unit::Millimeter,
56 "centimeter" => Unit::Centimeter,
57 "inch" => Unit::Inch,
58 "foot" => Unit::Foot,
59 "meter" => Unit::Meter,
60 _ => Unit::Millimeter, };
62 }
63 model.language = get_attribute(&e, b"xml:lang").map(|s| s.into_owned());
64 }
65 b"metadata" => {
66 let name = get_attribute(&e, b"name")
67 .ok_or(Lib3mfError::Validation("Metadata missing name".to_string()))?
68 .into_owned();
69 if model.metadata.contains_key(&name) {
70 return Err(Lib3mfError::Validation(format!(
71 "Duplicate metadata name '{}'. Each metadata name must be unique",
72 name
73 )));
74 }
75 let content = parser.read_text_content()?;
76 model.metadata.insert(name, content);
77 }
78 b"resources" => parse_resources(&mut parser, &mut model)?,
79 b"build" => {
80 seen_build_element = true;
81 model.build = parse_build(&mut parser)?;
82 }
83 _ => {}
84 },
85 Event::Empty(e) => {
86 if e.name().as_ref() == b"metadata" {
87 let name = get_attribute(&e, b"name")
88 .ok_or(Lib3mfError::Validation("Metadata missing name".to_string()))?;
89 if model.metadata.contains_key(name.as_ref()) {
90 return Err(Lib3mfError::Validation(format!(
91 "Duplicate metadata name '{}'. Each metadata name must be unique",
92 name
93 )));
94 }
95 model.metadata.insert(name.into_owned(), String::new());
96 }
97 }
98 Event::End(e) if e.name().as_ref() == b"model" => {
99 model_ended = true;
100 }
101 Event::Eof => break,
102 _ => {}
103 }
104 }
105
106 if !seen_build_element {
107 return Err(Lib3mfError::Validation(
108 "Missing required <build> element. Every 3MF model must contain a <build> element"
109 .to_string(),
110 ));
111 }
112
113 Ok(model)
114}
115
116fn parse_resources<R: BufRead>(parser: &mut XmlParser<R>, model: &mut Model) -> Result<()> {
117 loop {
118 match parser.read_next_event()? {
119 Event::Start(e) => {
120 let local_name = e.local_name();
121 match local_name.as_ref() {
122 b"object" => {
123 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
124 let name = get_attribute(&e, b"name").map(|s| s.into_owned());
125 let part_number = get_attribute(&e, b"partnumber").map(|s| s.into_owned());
126 let pid = get_attribute_u32(&e, b"pid")
127 .map(crate::model::ResourceId)
128 .ok();
129 let pindex = get_attribute_u32(&e, b"pindex").ok();
130 let uuid = crate::parser::xml_parser::get_attribute_uuid(&e)?;
131
132 let slice_stack_id = get_attribute_u32(&e, b"slicestackid")
134 .or_else(|_| get_attribute_u32(&e, b"s:slicestackid"))
135 .map(crate::model::ResourceId)
136 .ok();
137
138 let vol_stack_id = get_attribute_u32(&e, b"volumetricstackid")
140 .or_else(|_| get_attribute_u32(&e, b"v:volumetricstackid"))
141 .map(crate::model::ResourceId)
142 .ok();
143
144 let object_type = match get_attribute(&e, b"type") {
145 Some(type_str) => match type_str.as_ref() {
146 "model" => crate::model::ObjectType::Model,
147 "support" => crate::model::ObjectType::Support,
148 "solidsupport" => crate::model::ObjectType::SolidSupport,
149 "surface" => crate::model::ObjectType::Surface,
150 "other" => crate::model::ObjectType::Other,
151 unknown => {
152 return Err(Lib3mfError::Validation(format!(
153 "Invalid object type '{}'. Valid types are: model, support, solidsupport, surface, other",
154 unknown
155 )));
156 }
157 },
158 None => crate::model::ObjectType::Model,
159 };
160
161 let thumbnail = get_attribute(&e, b"thumbnail").map(|s| s.into_owned());
162
163 let geometry_content = parse_object_geometry(parser)?;
164
165 let geometry = if let Some(ssid) = slice_stack_id {
166 if geometry_content.has_content() {
167 eprintln!(
168 "Warning: Object {} has slicestackid but also contains geometry content; geometry will be ignored",
169 id.0
170 );
171 }
172 crate::model::Geometry::SliceStack(ssid)
173 } else if let Some(vsid) = vol_stack_id {
174 if geometry_content.has_content() {
175 eprintln!(
176 "Warning: Object {} has volumetricstackid but also contains geometry content; geometry will be ignored",
177 id.0
178 );
179 }
180 crate::model::Geometry::VolumetricStack(vsid)
181 } else {
182 geometry_content
183 };
184
185 let object = Object {
186 id,
187 object_type,
188 name,
189 part_number,
190 uuid,
191 pid,
192 pindex,
193 thumbnail,
194 geometry,
195 };
196 model.resources.add_object(object)?;
197 }
198 b"basematerials" => {
199 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
200 let group = parse_base_materials(parser, id)?;
201 model.resources.add_base_materials(group)?;
202 }
203 b"colorgroup" => {
204 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
205 let group = parse_color_group(parser, id)?;
206 model.resources.add_color_group(group)?;
207 }
208 b"texture2d" => {
209 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
210 let path = get_attribute(&e, b"path")
211 .ok_or(Lib3mfError::Validation(
212 "texture2d missing required 'path' attribute".to_string(),
213 ))?
214 .into_owned();
215 let contenttype = get_attribute(&e, b"contenttype")
216 .ok_or(Lib3mfError::Validation(
217 "texture2d missing required 'contenttype' attribute".to_string(),
218 ))?
219 .into_owned();
220
221 if contenttype.is_empty()
223 || (!contenttype.starts_with("image/png")
224 && !contenttype.starts_with("image/jpeg")
225 && !contenttype.starts_with("image/jpg"))
226 {
227 return Err(Lib3mfError::Validation(format!(
228 "Invalid contenttype '{}'. Must be 'image/png' or 'image/jpeg'",
229 contenttype
230 )));
231 }
232
233 let texture = crate::model::Texture2D {
234 id,
235 path,
236 contenttype,
237 };
238 model.resources.add_texture_2d(texture)?;
239 }
240 b"texture2dgroup" => {
241 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
242 let texid = crate::model::ResourceId(get_attribute_u32(&e, b"texid")?);
243 let group = parse_texture_2d_group(parser, id, texid)?;
244 model.resources.add_texture_2d_group(group)?;
245 }
246 b"compositematerials" => {
247 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
248 let matid = crate::model::ResourceId(get_attribute_u32(&e, b"matid")?);
249 let matindices_str = get_attribute(&e, b"matindices").ok_or_else(|| {
250 Lib3mfError::Validation(
251 "compositematerials missing matindices".to_string(),
252 )
253 })?;
254 let indices = matindices_str
255 .split_whitespace()
256 .map(|s| {
257 s.parse::<u32>().map_err(|_| {
258 Lib3mfError::Validation("Invalid matindices value".to_string())
259 })
260 })
261 .collect::<Result<Vec<u32>>>()?;
262 let group = parse_composite_materials(parser, id, matid, indices)?;
263 model.resources.add_composite_materials(group)?;
264 }
265 b"multiproperties" => {
266 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
267 let pids_str = get_attribute(&e, b"pids").ok_or_else(|| {
268 Lib3mfError::Validation("multiproperties missing pids".to_string())
269 })?;
270 let pids = pids_str
271 .split_whitespace()
272 .map(|s| {
273 s.parse::<u32>()
274 .map_err(|_| {
275 Lib3mfError::Validation("Invalid pid value".to_string())
276 })
277 .map(crate::model::ResourceId)
278 })
279 .collect::<Result<Vec<crate::model::ResourceId>>>()?;
280
281 let blend_methods =
282 if let Some(blendmethods_str) = get_attribute(&e, b"blendmethods") {
283 blendmethods_str
284 .split_whitespace()
285 .map(|s| match s {
286 "mix" => Ok(crate::model::BlendMethod::Mix),
287 "multiply" => Ok(crate::model::BlendMethod::Multiply),
288 _ => Err(Lib3mfError::Validation(format!(
289 "Invalid blend method: {}",
290 s
291 ))),
292 })
293 .collect::<Result<Vec<crate::model::BlendMethod>>>()?
294 } else {
295 vec![crate::model::BlendMethod::Multiply; pids.len()]
297 };
298
299 let group = parse_multi_properties(parser, id, pids, blend_methods)?;
300 model.resources.add_multi_properties(group)?;
301 }
302 b"slicestack" => {
303 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
304 let z_bottom = get_attribute_f32(&e, b"zbottom").unwrap_or(0.0);
305 let stack = parse_slice_stack_content(parser, id, z_bottom)?;
306 model.resources.add_slice_stack(stack)?;
307 }
308 b"volumetricstack" => {
309 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
310 let stack = parse_volumetric_stack_content(parser, id, 0.0)?;
311 model.resources.add_volumetric_stack(stack)?;
312 }
313 b"booleanshape" => {
314 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
315 let base_object_id =
316 crate::model::ResourceId(get_attribute_u32(&e, b"objectid")?);
317 let base_transform = if let Some(s) = get_attribute(&e, b"transform") {
318 crate::parser::component_parser::parse_transform(&s)?
319 } else {
320 glam::Mat4::IDENTITY
321 };
322 let base_path = get_attribute(&e, b"path")
323 .or_else(|| get_attribute(&e, b"p:path"))
324 .map(|s| s.into_owned());
325
326 let bool_shape =
327 parse_boolean_shape(parser, base_object_id, base_transform, base_path)?;
328
329 let object = Object {
331 id,
332 object_type: crate::model::ObjectType::Model,
333 name: None,
334 part_number: None,
335 uuid: None,
336 pid: None,
337 pindex: None,
338 thumbnail: None,
339 geometry: Geometry::BooleanShape(bool_shape),
340 };
341 model.resources.add_object(object)?;
342 }
343 b"displacement2d" => {
344 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
345 let path = get_attribute(&e, b"path")
346 .ok_or_else(|| {
347 Lib3mfError::Validation(
348 "displacement2d missing path attribute".to_string(),
349 )
350 })?
351 .into_owned();
352
353 let channel = if let Some(ch_str) = get_attribute(&e, b"channel") {
354 match ch_str.as_ref() {
355 "R" => crate::model::Channel::R,
356 "G" => crate::model::Channel::G,
357 "B" => crate::model::Channel::B,
358 "A" => crate::model::Channel::A,
359 _ => crate::model::Channel::G,
360 }
361 } else {
362 crate::model::Channel::G
363 };
364
365 let tile_style = if let Some(ts_str) = get_attribute(&e, b"tilestyle") {
366 match ts_str.to_lowercase().as_str() {
367 "wrap" => crate::model::TileStyle::Wrap,
368 "mirror" => crate::model::TileStyle::Mirror,
369 "clamp" => crate::model::TileStyle::Clamp,
370 "none" => crate::model::TileStyle::None,
371 _ => crate::model::TileStyle::Wrap,
372 }
373 } else {
374 crate::model::TileStyle::Wrap
375 };
376
377 let filter = if let Some(f_str) = get_attribute(&e, b"filter") {
378 match f_str.to_lowercase().as_str() {
379 "linear" => crate::model::FilterMode::Linear,
380 "nearest" => crate::model::FilterMode::Nearest,
381 _ => crate::model::FilterMode::Linear,
382 }
383 } else {
384 crate::model::FilterMode::Linear
385 };
386
387 let height = get_attribute_f32(&e, b"height")?;
388 let offset = get_attribute_f32(&e, b"offset").unwrap_or(0.0);
389
390 let displacement = parse_displacement_2d(
391 parser, id, path, channel, tile_style, filter, height, offset,
392 )?;
393 model.resources.add_displacement_2d(displacement)?;
394 }
395 _ => {}
396 }
397 }
398 Event::Empty(e) => {
399 let local_name = e.local_name();
401 match local_name.as_ref() {
402 b"colorgroup" => {
403 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
404 let group = ColorGroup {
405 id,
406 colors: Vec::new(),
407 };
408 model.resources.add_color_group(group)?;
409 }
410 b"texture2dgroup" => {
411 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
412 let texture_id = crate::model::ResourceId(get_attribute_u32(&e, b"texid")?);
413 let group = Texture2DGroup {
414 id,
415 texture_id,
416 coords: Vec::new(),
417 };
418 model.resources.add_texture_2d_group(group)?;
419 }
420 b"basematerials" => {
421 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
422 let group = BaseMaterialsGroup {
423 id,
424 materials: Vec::new(),
425 };
426 model.resources.add_base_materials(group)?;
427 }
428 b"compositematerials" => {
429 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
430 let base_material_id =
431 crate::model::ResourceId(get_attribute_u32(&e, b"matid")?);
432 let matindices_str = get_attribute(&e, b"matindices").ok_or_else(|| {
433 Lib3mfError::Validation(
434 "compositematerials missing matindices".to_string(),
435 )
436 })?;
437 let indices = matindices_str
438 .split_whitespace()
439 .map(|s| {
440 s.parse::<u32>().map_err(|_| {
441 Lib3mfError::Validation("Invalid matindices value".to_string())
442 })
443 })
444 .collect::<Result<Vec<u32>>>()?;
445 let group = CompositeMaterials {
446 id,
447 base_material_id,
448 indices,
449 composites: Vec::new(),
450 };
451 model.resources.add_composite_materials(group)?;
452 }
453 b"multiproperties" => {
454 let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
455 let pids_str = get_attribute(&e, b"pids").ok_or_else(|| {
456 Lib3mfError::Validation("multiproperties missing pids".to_string())
457 })?;
458 let pids = pids_str
459 .split_whitespace()
460 .map(|s| {
461 s.parse::<u32>()
462 .map_err(|_| {
463 Lib3mfError::Validation("Invalid pid value".to_string())
464 })
465 .map(crate::model::ResourceId)
466 })
467 .collect::<Result<Vec<crate::model::ResourceId>>>()?;
468
469 let blend_methods =
470 if let Some(blendmethods_str) = get_attribute(&e, b"blendmethods") {
471 blendmethods_str
472 .split_whitespace()
473 .map(|s| match s {
474 "mix" => Ok(crate::model::BlendMethod::Mix),
475 "multiply" => Ok(crate::model::BlendMethod::Multiply),
476 _ => Err(Lib3mfError::Validation(format!(
477 "Invalid blend method: {}",
478 s
479 ))),
480 })
481 .collect::<Result<Vec<crate::model::BlendMethod>>>()?
482 } else {
483 vec![crate::model::BlendMethod::Multiply; pids.len()]
485 };
486
487 let group = MultiProperties {
488 id,
489 pids,
490 blend_methods,
491 multis: Vec::new(),
492 };
493 model.resources.add_multi_properties(group)?;
494 }
495 _ => {}
496 }
497 }
498 Event::End(e) if e.name().as_ref() == b"resources" => break,
499 Event::Eof => {
500 return Err(Lib3mfError::Validation(
501 "Unexpected EOF in resources".to_string(),
502 ));
503 }
504 _ => {}
505 }
506 }
507 Ok(())
508}
509
510fn parse_object_geometry<R: BufRead>(parser: &mut XmlParser<R>) -> Result<Geometry> {
511 let mut geometry = Geometry::Mesh(crate::model::Mesh::default()); loop {
521 match parser.read_next_event()? {
522 Event::Start(e) => {
523 let local_name = e.local_name();
524 match local_name.as_ref() {
525 b"mesh" => {
526 geometry = Geometry::Mesh(parse_mesh(parser)?);
527 }
528 b"components" => {
529 geometry = Geometry::Components(parse_components(parser)?);
530 }
531 b"displacementmesh" => {
532 geometry = Geometry::DisplacementMesh(parse_displacement_mesh(parser)?);
533 }
534 _ => {}
535 }
536 }
537 Event::End(e) if e.name().as_ref() == b"object" => break,
538 Event::Eof => {
539 return Err(Lib3mfError::Validation(
540 "Unexpected EOF in object".to_string(),
541 ));
542 }
543 _ => {}
544 }
545 }
546 Ok(geometry)
547}