lib3mf_core/parser/
model_parser.rs

1use crate::error::{Lib3mfError, Result};
2use crate::model::{Geometry, Model, Object, Unit};
3use crate::parser::boolean_parser::parse_boolean_shape;
4use crate::parser::build_parser::parse_build;
5use crate::parser::component_parser::parse_components;
6use crate::parser::displacement_parser::{parse_displacement_2d, parse_displacement_mesh};
7use crate::parser::material_parser::{
8    parse_base_materials, parse_color_group, parse_composite_materials, parse_multi_properties,
9    parse_texture_2d_group,
10};
11use crate::parser::mesh_parser::parse_mesh;
12use crate::parser::slice_parser::parse_slice_stack_content;
13use crate::parser::volumetric_parser::parse_volumetric_stack_content;
14use crate::parser::xml_parser::{XmlParser, get_attribute, get_attribute_f32, get_attribute_u32};
15use quick_xml::events::Event;
16use std::io::BufRead;
17
18pub fn parse_model<R: BufRead>(reader: R) -> Result<Model> {
19    let mut parser = XmlParser::new(reader);
20    let mut model = Model::default();
21
22    loop {
23        match parser.read_next_event()? {
24            Event::Start(e) => match e.name().as_ref() {
25                b"model" => {
26                    if let Some(unit_str) = get_attribute(&e, b"unit") {
27                        model.unit = match unit_str.as_ref() {
28                            "micron" => Unit::Micron,
29                            "millimeter" => Unit::Millimeter,
30                            "centimeter" => Unit::Centimeter,
31                            "inch" => Unit::Inch,
32                            "foot" => Unit::Foot,
33                            "meter" => Unit::Meter,
34                            _ => Unit::Millimeter, // Default or warn?
35                        };
36                    }
37                    model.language = get_attribute(&e, b"xml:lang").map(|s| s.into_owned());
38                }
39                b"metadata" => {
40                    let name = get_attribute(&e, b"name")
41                        .ok_or(Lib3mfError::Validation("Metadata missing name".to_string()))?
42                        .into_owned();
43                    let content = parser.read_text_content()?;
44                    model.metadata.insert(name, content);
45                }
46                b"resources" => parse_resources(&mut parser, &mut model)?,
47                b"build" => {
48                    model.build = parse_build(&mut parser)?;
49                }
50                _ => {}
51            },
52            Event::Empty(e) => {
53                if e.name().as_ref() == b"metadata" {
54                    let name = get_attribute(&e, b"name")
55                        .ok_or(Lib3mfError::Validation("Metadata missing name".to_string()))?;
56                    model.metadata.insert(name.into_owned(), String::new());
57                }
58            }
59            Event::End(e) if e.name().as_ref() == b"model" => break,
60            Event::Eof => break,
61            _ => {}
62        }
63    }
64
65    Ok(model)
66}
67
68fn parse_resources<R: BufRead>(parser: &mut XmlParser<R>, model: &mut Model) -> Result<()> {
69    loop {
70        match parser.read_next_event()? {
71            Event::Start(e) => {
72                let local_name = e.local_name();
73                match local_name.as_ref() {
74                    b"object" => {
75                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
76                        let name = get_attribute(&e, b"name").map(|s| s.into_owned());
77                        let part_number = get_attribute(&e, b"partnumber").map(|s| s.into_owned());
78                        let pid = get_attribute_u32(&e, b"pid")
79                            .map(crate::model::ResourceId)
80                            .ok();
81                        let pindex = get_attribute_u32(&e, b"pindex").ok();
82                        let uuid = crate::parser::xml_parser::get_attribute_uuid(&e)?;
83
84                        // Check for slicestackid (default or prefixed)
85                        let slice_stack_id = get_attribute_u32(&e, b"slicestackid")
86                            .or_else(|_| get_attribute_u32(&e, b"s:slicestackid"))
87                            .map(crate::model::ResourceId)
88                            .ok();
89
90                        // Check for volumetricstackid (hypothetical prefix v:)
91                        let vol_stack_id = get_attribute_u32(&e, b"volumetricstackid")
92                            .or_else(|_| get_attribute_u32(&e, b"v:volumetricstackid"))
93                            .map(crate::model::ResourceId)
94                            .ok();
95
96                        let object_type = match get_attribute(&e, b"type") {
97                            Some(type_str) => match type_str.as_ref() {
98                                "model" => crate::model::ObjectType::Model,
99                                "support" => crate::model::ObjectType::Support,
100                                "solidsupport" => crate::model::ObjectType::SolidSupport,
101                                "surface" => crate::model::ObjectType::Surface,
102                                "other" => crate::model::ObjectType::Other,
103                                unknown => {
104                                    eprintln!(
105                                        "Warning: Unknown object type '{}', defaulting to 'model'",
106                                        unknown
107                                    );
108                                    crate::model::ObjectType::Model
109                                }
110                            },
111                            None => crate::model::ObjectType::Model,
112                        };
113
114                        let thumbnail = get_attribute(&e, b"thumbnail").map(|s| s.into_owned());
115
116                        let geometry_content = parse_object_geometry(parser)?;
117
118                        let geometry = if let Some(ssid) = slice_stack_id {
119                            if geometry_content.has_content() {
120                                eprintln!(
121                                    "Warning: Object {} has slicestackid but also contains geometry content; geometry will be ignored",
122                                    id.0
123                                );
124                            }
125                            crate::model::Geometry::SliceStack(ssid)
126                        } else if let Some(vsid) = vol_stack_id {
127                            if geometry_content.has_content() {
128                                eprintln!(
129                                    "Warning: Object {} has volumetricstackid but also contains geometry content; geometry will be ignored",
130                                    id.0
131                                );
132                            }
133                            crate::model::Geometry::VolumetricStack(vsid)
134                        } else {
135                            geometry_content
136                        };
137
138                        let object = Object {
139                            id,
140                            object_type,
141                            name,
142                            part_number,
143                            uuid,
144                            pid,
145                            pindex,
146                            thumbnail,
147                            geometry,
148                        };
149                        model.resources.add_object(object)?;
150                    }
151                    b"basematerials" => {
152                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
153                        let group = parse_base_materials(parser, id)?;
154                        model.resources.add_base_materials(group)?;
155                    }
156                    b"colorgroup" => {
157                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
158                        let group = parse_color_group(parser, id)?;
159                        model.resources.add_color_group(group)?;
160                    }
161                    b"texture2dgroup" => {
162                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
163                        let texid = crate::model::ResourceId(get_attribute_u32(&e, b"texid")?);
164                        let group = parse_texture_2d_group(parser, id, texid)?;
165                        model.resources.add_texture_2d_group(group)?;
166                    }
167                    b"compositematerials" => {
168                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
169                        let matid = crate::model::ResourceId(get_attribute_u32(&e, b"matid")?);
170                        let matindices_str = get_attribute(&e, b"matindices").ok_or_else(|| {
171                            Lib3mfError::Validation(
172                                "compositematerials missing matindices".to_string(),
173                            )
174                        })?;
175                        let indices = matindices_str
176                            .split_whitespace()
177                            .map(|s| {
178                                s.parse::<u32>().map_err(|_| {
179                                    Lib3mfError::Validation("Invalid matindices value".to_string())
180                                })
181                            })
182                            .collect::<Result<Vec<u32>>>()?;
183                        let group = parse_composite_materials(parser, id, matid, indices)?;
184                        model.resources.add_composite_materials(group)?;
185                    }
186                    b"multiproperties" => {
187                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
188                        let pids_str = get_attribute(&e, b"pids").ok_or_else(|| {
189                            Lib3mfError::Validation("multiproperties missing pids".to_string())
190                        })?;
191                        let pids = pids_str
192                            .split_whitespace()
193                            .map(|s| {
194                                s.parse::<u32>()
195                                    .map_err(|_| {
196                                        Lib3mfError::Validation("Invalid pid value".to_string())
197                                    })
198                                    .map(crate::model::ResourceId)
199                            })
200                            .collect::<Result<Vec<crate::model::ResourceId>>>()?;
201
202                        let blendmethods_str =
203                            get_attribute(&e, b"blendmethods").ok_or_else(|| {
204                                Lib3mfError::Validation(
205                                    "multiproperties missing blendmethods".to_string(),
206                                )
207                            })?;
208                        let blend_methods = blendmethods_str
209                            .split_whitespace()
210                            .map(|s| match s {
211                                "mix" => Ok(crate::model::BlendMethod::Mix),
212                                "multiply" => Ok(crate::model::BlendMethod::Multiply),
213                                _ => Err(Lib3mfError::Validation(format!(
214                                    "Invalid blend method: {}",
215                                    s
216                                ))),
217                            })
218                            .collect::<Result<Vec<crate::model::BlendMethod>>>()?;
219
220                        let group = parse_multi_properties(parser, id, pids, blend_methods)?;
221                        model.resources.add_multi_properties(group)?;
222                    }
223                    b"slicestack" => {
224                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
225                        let z_bottom = get_attribute_f32(&e, b"zbottom").unwrap_or(0.0);
226                        let stack = parse_slice_stack_content(parser, id, z_bottom)?;
227                        model.resources.add_slice_stack(stack)?;
228                    }
229                    b"volumetricstack" => {
230                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
231                        let stack = parse_volumetric_stack_content(parser, id, 0.0)?;
232                        model.resources.add_volumetric_stack(stack)?;
233                    }
234                    b"booleanshape" => {
235                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
236                        let base_object_id =
237                            crate::model::ResourceId(get_attribute_u32(&e, b"objectid")?);
238                        let base_transform = if let Some(s) = get_attribute(&e, b"transform") {
239                            crate::parser::component_parser::parse_transform(&s)?
240                        } else {
241                            glam::Mat4::IDENTITY
242                        };
243                        let base_path = get_attribute(&e, b"path")
244                            .or_else(|| get_attribute(&e, b"p:path"))
245                            .map(|s| s.into_owned());
246
247                        let bool_shape =
248                            parse_boolean_shape(parser, base_object_id, base_transform, base_path)?;
249
250                        // Per spec, booleanshape is a model-type object
251                        let object = Object {
252                            id,
253                            object_type: crate::model::ObjectType::Model,
254                            name: None,
255                            part_number: None,
256                            uuid: None,
257                            pid: None,
258                            pindex: None,
259                            thumbnail: None,
260                            geometry: Geometry::BooleanShape(bool_shape),
261                        };
262                        model.resources.add_object(object)?;
263                    }
264                    b"displacement2d" => {
265                        let id = crate::model::ResourceId(get_attribute_u32(&e, b"id")?);
266                        let path = get_attribute(&e, b"path")
267                            .ok_or_else(|| {
268                                Lib3mfError::Validation(
269                                    "displacement2d missing path attribute".to_string(),
270                                )
271                            })?
272                            .into_owned();
273
274                        let channel = if let Some(ch_str) = get_attribute(&e, b"channel") {
275                            match ch_str.as_ref() {
276                                "R" => crate::model::Channel::R,
277                                "G" => crate::model::Channel::G,
278                                "B" => crate::model::Channel::B,
279                                "A" => crate::model::Channel::A,
280                                _ => crate::model::Channel::G,
281                            }
282                        } else {
283                            crate::model::Channel::G
284                        };
285
286                        let tile_style = if let Some(ts_str) = get_attribute(&e, b"tilestyle") {
287                            match ts_str.to_lowercase().as_str() {
288                                "wrap" => crate::model::TileStyle::Wrap,
289                                "mirror" => crate::model::TileStyle::Mirror,
290                                "clamp" => crate::model::TileStyle::Clamp,
291                                "none" => crate::model::TileStyle::None,
292                                _ => crate::model::TileStyle::Wrap,
293                            }
294                        } else {
295                            crate::model::TileStyle::Wrap
296                        };
297
298                        let filter = if let Some(f_str) = get_attribute(&e, b"filter") {
299                            match f_str.to_lowercase().as_str() {
300                                "linear" => crate::model::FilterMode::Linear,
301                                "nearest" => crate::model::FilterMode::Nearest,
302                                _ => crate::model::FilterMode::Linear,
303                            }
304                        } else {
305                            crate::model::FilterMode::Linear
306                        };
307
308                        let height = get_attribute_f32(&e, b"height")?;
309                        let offset = get_attribute_f32(&e, b"offset").unwrap_or(0.0);
310
311                        let displacement = parse_displacement_2d(
312                            parser, id, path, channel, tile_style, filter, height, offset,
313                        )?;
314                        model.resources.add_displacement_2d(displacement)?;
315                    }
316                    _ => {}
317                }
318            }
319            Event::End(e) if e.name().as_ref() == b"resources" => break,
320            Event::Eof => {
321                return Err(Lib3mfError::Validation(
322                    "Unexpected EOF in resources".to_string(),
323                ));
324            }
325            _ => {}
326        }
327    }
328    Ok(())
329}
330
331fn parse_object_geometry<R: BufRead>(parser: &mut XmlParser<R>) -> Result<Geometry> {
332    // We are inside <object> tag. We expect either <mesh> or <components> next.
333    // NOTE: object is open. We read until </object>.
334
335    // Actually, parse_object_geometry needs to look for mesh/components.
336    // If <object> was Empty, we wouldn't be here (logic above needs check).
337    // The previous match Event::Start(object) means it has content.
338
339    let mut geometry = Geometry::Mesh(crate::model::Mesh::default()); // Default fallback? Or Option/Result?
340
341    loop {
342        match parser.read_next_event()? {
343            Event::Start(e) => {
344                let local_name = e.local_name();
345                match local_name.as_ref() {
346                    b"mesh" => {
347                        geometry = Geometry::Mesh(parse_mesh(parser)?);
348                    }
349                    b"components" => {
350                        geometry = Geometry::Components(parse_components(parser)?);
351                    }
352                    b"displacementmesh" => {
353                        geometry = Geometry::DisplacementMesh(parse_displacement_mesh(parser)?);
354                    }
355                    _ => {}
356                }
357            }
358            Event::End(e) if e.name().as_ref() == b"object" => break,
359            Event::Eof => {
360                return Err(Lib3mfError::Validation(
361                    "Unexpected EOF in object".to_string(),
362                ));
363            }
364            _ => {}
365        }
366    }
367    Ok(geometry)
368}