lib3mf_core/parser/
model_parser.rs

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                    // Validate that xml:space attribute is not present
45                    // The 3MF spec does not allow xml:space on the model element
46                    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, // Default or warn?
61                        };
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                        // Check for slicestackid (default or prefixed)
133                        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                        // Check for volumetricstackid (hypothetical prefix v:)
139                        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                        // Validate content type - must be a valid image MIME type
222                        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                                // Default to Multiply for all pids when blendmethods not specified
296                                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                        // Per spec, booleanshape is a model-type object
330                        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                // Handle self-closing elements like <colorgroup id="5"/>
400                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                                // Default to Multiply for all pids when blendmethods not specified
484                                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    // We are inside <object> tag. We expect either <mesh> or <components> next.
512    // NOTE: object is open. We read until </object>.
513
514    // Actually, parse_object_geometry needs to look for mesh/components.
515    // If <object> was Empty, we wouldn't be here (logic above needs check).
516    // The previous match Event::Start(object) means it has content.
517
518    let mut geometry = Geometry::Mesh(crate::model::Mesh::default()); // Default fallback? Or Option/Result?
519
520    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}