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