Skip to main content

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