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