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> {
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 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, };
63 }
64 model.language = get_attribute(&e, b"xml:lang").map(|s| s.into_owned());
65
66 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 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 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 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 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 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 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 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 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 let mut geometry = Geometry::Mesh(crate::model::Mesh::default()); 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}