1use crate::archive::ArchiveReader;
2use crate::error::{Lib3mfError, Result};
3use crate::model::{Geometry, Mesh, Model, Object, ObjectType, ResourceId, Unit};
4use crate::parser::model_parser::parse_model;
5use std::collections::HashMap;
6use std::io::Cursor;
7
8const ROOT_PATH: &str = "ROOT";
9const MAIN_MODEL_PART: &str = "3D/3dmodel.model";
10
11pub struct PartResolver<'a, A: ArchiveReader> {
13 archive: &'a mut A,
14 models: HashMap<String, Model>,
15}
16
17impl<'a, A: ArchiveReader> PartResolver<'a, A> {
18 pub fn new(archive: &'a mut A, root_model: Model) -> Self {
20 let mut models = HashMap::new();
21 models.insert(ROOT_PATH.to_string(), root_model);
22 Self { archive, models }
23 }
24
25 pub fn resolve_object(
27 &mut self,
28 id: ResourceId,
29 path: Option<&str>,
30 ) -> Result<Option<(&Model, &Object)>> {
31 let model = self.get_or_load_model(path)?;
32 Ok(model.resources.get_object(id).map(|obj| (model, obj)))
33 }
34
35 pub fn resolve_base_materials(
37 &mut self,
38 id: ResourceId,
39 path: Option<&str>,
40 ) -> Result<Option<&crate::model::BaseMaterialsGroup>> {
41 let model = self.get_or_load_model(path)?;
42 Ok(model.resources.get_base_materials(id))
43 }
44
45 pub fn resolve_color_group(
47 &mut self,
48 id: ResourceId,
49 path: Option<&str>,
50 ) -> Result<Option<&crate::model::ColorGroup>> {
51 let model = self.get_or_load_model(path)?;
52 Ok(model.resources.get_color_group(id))
53 }
54
55 fn get_or_load_model(&mut self, path: Option<&str>) -> Result<&Model> {
56 let part_path = match path {
57 Some(p) => {
58 let p = p.trim_start_matches('/');
59 if p.is_empty() || p.eq_ignore_ascii_case(MAIN_MODEL_PART) {
60 ROOT_PATH
61 } else {
62 p
63 }
64 }
65 None => ROOT_PATH,
66 };
67
68 if !self.models.contains_key(part_path) {
69 let data = self.archive.read_entry(part_path).or_else(|_| {
70 let alt = format!("/{}", part_path);
71 self.archive.read_entry(&alt)
72 })?;
73
74 let model = parse_model(Cursor::new(data))?;
75 self.models.insert(part_path.to_string(), model);
76 }
77
78 Ok(self.models.get(part_path).unwrap())
79 }
80
81 pub fn get_root_model(&self) -> &Model {
83 self.models.get("ROOT").unwrap()
84 }
85
86 pub fn archive_mut(&mut self) -> &mut A {
88 self.archive
89 }
90
91 pub fn resolve_meshes(&mut self, options: &ResolveOptions) -> Result<Vec<ResolvedMesh>> {
116 let build_items = self.get_root_model().build.items.clone();
119
120 let mut out = Vec::new();
121 let mut ancestry: Vec<(u32, String)> = Vec::new();
122
123 for item in &build_items {
124 if options.filter_non_printable && item.printable == Some(false) {
125 continue;
126 }
127
128 resolve_recursive(
129 item.object_id,
130 item.path.as_deref(),
131 item.transform,
132 0,
133 &mut ancestry,
134 options,
135 self,
136 &mut out,
137 )?;
138 }
139
140 Ok(out)
141 }
142}
143
144#[derive(Debug, Clone)]
157pub struct ResolvedMesh {
158 pub mesh: Mesh,
160 pub transform: glam::Mat4,
165 pub object_type: ObjectType,
167 pub name: Option<String>,
169 pub unit: Unit,
172}
173
174#[derive(Debug, Clone)]
176pub struct ResolveOptions {
177 pub filter_non_printable: bool,
181 pub filter_other_objects: bool,
186 pub max_depth: u32,
190}
191
192impl Default for ResolveOptions {
193 fn default() -> Self {
194 Self {
195 filter_non_printable: true,
196 filter_other_objects: true,
197 max_depth: 16,
198 }
199 }
200}
201
202fn canonical_path(path: Option<&str>) -> String {
208 match path {
209 None | Some(ROOT_PATH) => ROOT_PATH.to_string(),
210 Some(p) => {
211 let p = p.trim_start_matches('/');
212 if p.is_empty() || p.eq_ignore_ascii_case(MAIN_MODEL_PART) {
213 ROOT_PATH.to_string()
214 } else {
215 p.to_string()
216 }
217 }
218 }
219}
220
221#[allow(clippy::too_many_arguments)]
239fn resolve_recursive(
240 id: ResourceId,
241 path: Option<&str>,
242 transform: glam::Mat4,
243 depth: u32,
244 ancestry: &mut Vec<(u32, String)>,
245 options: &ResolveOptions,
246 resolver: &mut PartResolver<impl ArchiveReader>,
247 out: &mut Vec<ResolvedMesh>,
248) -> Result<()> {
249 if depth > options.max_depth {
251 return Err(Lib3mfError::InvalidStructure(format!(
252 "Component tree depth {} exceeds maximum of {}",
253 depth, options.max_depth
254 )));
255 }
256
257 let canonical = canonical_path(path);
260 let key = (id.0, canonical.clone());
261 if ancestry.contains(&key) {
262 return Err(Lib3mfError::InvalidStructure(format!(
263 "Cycle detected: object {} in path {:?} appears in current ancestry",
264 id.0, path
265 )));
266 }
267 ancestry.push(key.clone());
268
269 let (geom, inherited_path, obj_type, obj_name, obj_unit) = {
274 let resolved = resolver.resolve_object(id, path)?;
275 match resolved {
276 None => {
277 return Err(Lib3mfError::InvalidStructure(format!(
278 "Object {} not found in path {:?}",
279 id.0, path
280 )));
281 }
282 Some((model, object)) => {
283 let geom = object.geometry.clone();
284 let obj_type = object.object_type;
285 let obj_name = object.name.clone();
286 let obj_unit = model.unit;
287 let inherited = if canonical == ROOT_PATH {
293 None
294 } else {
295 Some(canonical.clone())
296 };
297 (geom, inherited, obj_type, obj_name, obj_unit)
298 }
299 }
300 };
301
302 match geom {
303 Geometry::Mesh(mesh) => {
304 if !options.filter_other_objects || obj_type != ObjectType::Other {
307 out.push(ResolvedMesh {
308 mesh,
309 transform,
310 object_type: obj_type,
311 name: obj_name,
312 unit: obj_unit,
313 });
314 }
315 }
316 Geometry::Components(comps) => {
317 for comp in comps.components {
318 let next_path = comp.path.as_deref().or(inherited_path.as_deref());
321
322 resolve_recursive(
325 comp.object_id,
326 next_path,
327 transform * comp.transform,
328 depth + 1,
329 ancestry,
330 options,
331 resolver,
332 out,
333 )?;
334 }
335 }
336 _ => {}
339 }
340
341 ancestry.pop();
344 Ok(())
345}
346
347#[cfg(test)]
348mod tests {
349 use super::*;
350 use crate::model::{
351 BuildItem, Component, Components, Geometry, Mesh, Model, Object, ObjectType, ResourceId,
352 Unit,
353 };
354 use std::collections::HashMap;
355 use std::io::{Cursor, Read, Seek, SeekFrom};
356
357 struct MockArchive {
362 entries: HashMap<String, Vec<u8>>,
363 cursor: Cursor<Vec<u8>>,
364 }
365
366 impl MockArchive {
367 fn new() -> Self {
368 Self {
369 entries: HashMap::new(),
370 cursor: Cursor::new(Vec::new()),
371 }
372 }
373
374 fn add_entry(&mut self, path: &str, data: Vec<u8>) {
375 self.entries.insert(path.to_string(), data);
376 }
377 }
378
379 impl Read for MockArchive {
380 fn read(&mut self, buf: &mut [u8]) -> std::io::Result<usize> {
381 self.cursor.read(buf)
382 }
383 }
384
385 impl Seek for MockArchive {
386 fn seek(&mut self, pos: SeekFrom) -> std::io::Result<u64> {
387 self.cursor.seek(pos)
388 }
389 }
390
391 impl ArchiveReader for MockArchive {
392 fn read_entry(&mut self, name: &str) -> Result<Vec<u8>> {
393 self.entries.get(name).cloned().ok_or_else(|| {
394 Lib3mfError::Io(std::io::Error::new(
395 std::io::ErrorKind::NotFound,
396 format!("Entry not found: {}", name),
397 ))
398 })
399 }
400
401 fn entry_exists(&mut self, name: &str) -> bool {
402 self.entries.contains_key(name)
403 }
404
405 fn list_entries(&mut self) -> Result<Vec<String>> {
406 Ok(self.entries.keys().cloned().collect())
407 }
408 }
409
410 fn model_to_xml_bytes(model: &Model) -> Vec<u8> {
416 let mut buf = Vec::new();
417 model.write_xml(&mut buf, None).expect("write_xml failed");
418 buf
419 }
420
421 fn simple_mesh() -> Mesh {
423 let mut mesh = Mesh::new();
424 mesh.add_vertex(0.0, 0.0, 0.0);
425 mesh.add_vertex(1.0, 0.0, 0.0);
426 mesh.add_vertex(0.0, 1.0, 0.0);
427 mesh.add_triangle(0, 1, 2);
428 mesh
429 }
430
431 fn mesh_object(id: u32, object_type: ObjectType, name: Option<&str>) -> Object {
433 Object {
434 id: ResourceId(id),
435 object_type,
436 name: name.map(|s| s.to_string()),
437 part_number: None,
438 uuid: None,
439 pid: None,
440 pindex: None,
441 thumbnail: None,
442 geometry: Geometry::Mesh(simple_mesh()),
443 }
444 }
445
446 fn components_object(id: u32, components: Vec<Component>) -> Object {
448 Object {
449 id: ResourceId(id),
450 object_type: ObjectType::Model,
451 name: None,
452 part_number: None,
453 uuid: None,
454 pid: None,
455 pindex: None,
456 thumbnail: None,
457 geometry: Geometry::Components(Components { components }),
458 }
459 }
460
461 fn component(object_id: u32) -> Component {
463 Component {
464 object_id: ResourceId(object_id),
465 path: None,
466 uuid: None,
467 transform: glam::Mat4::IDENTITY,
468 }
469 }
470
471 fn component_with_transform(object_id: u32, transform: glam::Mat4) -> Component {
473 Component {
474 object_id: ResourceId(object_id),
475 path: None,
476 uuid: None,
477 transform,
478 }
479 }
480
481 fn component_with_path(object_id: u32, path: &str, transform: glam::Mat4) -> Component {
483 Component {
484 object_id: ResourceId(object_id),
485 path: Some(path.to_string()),
486 uuid: None,
487 transform,
488 }
489 }
490
491 fn build_item(object_id: u32) -> BuildItem {
493 BuildItem {
494 object_id: ResourceId(object_id),
495 uuid: None,
496 path: None,
497 part_number: None,
498 transform: glam::Mat4::IDENTITY,
499 printable: None,
500 }
501 }
502
503 fn build_item_with_transform(object_id: u32, transform: glam::Mat4) -> BuildItem {
505 BuildItem {
506 object_id: ResourceId(object_id),
507 uuid: None,
508 path: None,
509 part_number: None,
510 transform,
511 printable: None,
512 }
513 }
514
515 fn build_item_printable(object_id: u32, printable: Option<bool>) -> BuildItem {
517 BuildItem {
518 object_id: ResourceId(object_id),
519 uuid: None,
520 path: None,
521 part_number: None,
522 transform: glam::Mat4::IDENTITY,
523 printable,
524 }
525 }
526
527 #[test]
532 fn test_resolve_same_file_components() {
533 let mut model = Model::default();
537 let obj1 = mesh_object(1, ObjectType::Model, None);
538 let obj2 = components_object(2, vec![component(1)]);
539 model.resources.add_object(obj1).unwrap();
540 model.resources.add_object(obj2).unwrap();
541 model.build.items.push(build_item(2));
542
543 let mut archive = MockArchive::new();
544 let mut resolver = PartResolver::new(&mut archive, model);
545 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
546
547 assert_eq!(meshes.len(), 1);
548 assert_eq!(meshes[0].mesh.vertices.len(), 3);
549 assert_eq!(meshes[0].mesh.triangles.len(), 1);
550 assert_eq!(meshes[0].transform, glam::Mat4::IDENTITY);
551 }
552
553 #[test]
554 fn test_resolve_transform_accumulation() {
555 let comp_transform = glam::Mat4::from_translation(glam::Vec3::new(0.0, 0.0, 10.0));
559 let build_transform = glam::Mat4::from_translation(glam::Vec3::new(5.0, 0.0, 0.0));
560
561 let mut model = Model::default();
562 let obj1 = mesh_object(1, ObjectType::Model, None);
563 let obj2 = components_object(2, vec![component_with_transform(1, comp_transform)]);
564 model.resources.add_object(obj1).unwrap();
565 model.resources.add_object(obj2).unwrap();
566 model
567 .build
568 .items
569 .push(build_item_with_transform(2, build_transform));
570
571 let mut archive = MockArchive::new();
572 let mut resolver = PartResolver::new(&mut archive, model);
573 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
574
575 assert_eq!(meshes.len(), 1);
576 let expected_transform = build_transform * comp_transform;
577 assert_eq!(meshes[0].transform, expected_transform);
578 }
579
580 #[test]
581 fn test_resolve_filters_other_objects() {
582 let mut model = Model::default();
587 let obj1 = mesh_object(1, ObjectType::Model, None);
588 let obj2 = mesh_object(2, ObjectType::Other, None);
589 let obj3 = components_object(3, vec![component(1), component(2)]);
590 model.resources.add_object(obj1).unwrap();
591 model.resources.add_object(obj2).unwrap();
592 model.resources.add_object(obj3).unwrap();
593 model.build.items.push(build_item(3));
594
595 let mut archive = MockArchive::new();
596
597 let mut resolver = PartResolver::new(&mut archive, model.clone());
599 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
600 assert_eq!(meshes.len(), 1);
601 assert_eq!(meshes[0].object_type, ObjectType::Model);
602
603 let mut resolver = PartResolver::new(&mut archive, model);
605 let opts = ResolveOptions {
606 filter_other_objects: false,
607 ..Default::default()
608 };
609 let meshes = resolver.resolve_meshes(&opts).unwrap();
610 assert_eq!(meshes.len(), 2);
611 }
612
613 #[test]
614 fn test_resolve_filters_non_printable() {
615 let mut model = Model::default();
619 let obj1 = mesh_object(1, ObjectType::Model, None);
620 model.resources.add_object(obj1).unwrap();
621 model.build.items.push(build_item_printable(1, Some(true)));
622 model.build.items.push(build_item_printable(1, Some(false)));
623
624 let mut archive = MockArchive::new();
625
626 let mut resolver = PartResolver::new(&mut archive, model.clone());
628 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
629 assert_eq!(meshes.len(), 1);
630
631 let mut resolver = PartResolver::new(&mut archive, model);
633 let opts = ResolveOptions {
634 filter_non_printable: false,
635 ..Default::default()
636 };
637 let meshes = resolver.resolve_meshes(&opts).unwrap();
638 assert_eq!(meshes.len(), 2);
639 }
640
641 #[test]
642 fn test_resolve_cycle_detection() {
643 let mut model = Model::default();
647 let obj1 = components_object(1, vec![component(2)]);
648 let obj2 = components_object(2, vec![component(1)]);
649 model.resources.add_object(obj1).unwrap();
650 model.resources.add_object(obj2).unwrap();
651 model.build.items.push(build_item(1));
652
653 let mut archive = MockArchive::new();
654 let mut resolver = PartResolver::new(&mut archive, model);
655 let result = resolver.resolve_meshes(&ResolveOptions::default());
656
657 assert!(result.is_err());
658 let msg = result.unwrap_err().to_string();
659 assert!(msg.contains("Cycle"), "Expected 'Cycle' in error: {}", msg);
660 }
661
662 #[test]
663 fn test_resolve_depth_limit() {
664 let mut model = Model::default();
668
669 for i in 1u32..18 {
671 let obj = components_object(i, vec![component(i + 1)]);
672 model.resources.add_object(obj).unwrap();
673 }
674 let leaf = mesh_object(18, ObjectType::Model, None);
676 model.resources.add_object(leaf).unwrap();
677 model.build.items.push(build_item(1));
678
679 let mut archive = MockArchive::new();
680
681 let mut resolver = PartResolver::new(&mut archive, model.clone());
683 let result = resolver.resolve_meshes(&ResolveOptions::default());
684 assert!(
685 result.is_err(),
686 "Expected depth limit error with 17-level chain and max_depth=16"
687 );
688
689 let mut resolver = PartResolver::new(&mut archive, model);
691 let opts = ResolveOptions {
692 max_depth: 20,
693 ..Default::default()
694 };
695 let meshes = resolver.resolve_meshes(&opts).unwrap();
696 assert_eq!(meshes.len(), 1);
697 }
698
699 #[test]
700 fn test_resolve_dangling_reference() {
701 let mut model = Model::default();
704 let obj1 = components_object(1, vec![component(999)]);
705 model.resources.add_object(obj1).unwrap();
706 model.build.items.push(build_item(1));
707
708 let mut archive = MockArchive::new();
709 let mut resolver = PartResolver::new(&mut archive, model);
710 let result = resolver.resolve_meshes(&ResolveOptions::default());
711
712 assert!(result.is_err());
713 let msg = result.unwrap_err().to_string();
714 assert!(
715 msg.contains("not found"),
716 "Expected 'not found' in error: {}",
717 msg
718 );
719 }
720
721 #[test]
722 fn test_resolve_instancing_not_false_cycle() {
723 let mut model = Model::default();
728 let obj1 = mesh_object(1, ObjectType::Model, None);
729 let obj2 = components_object(2, vec![component(1), component(1)]);
730 model.resources.add_object(obj1).unwrap();
731 model.resources.add_object(obj2).unwrap();
732 model.build.items.push(build_item(2));
733
734 let mut archive = MockArchive::new();
735 let mut resolver = PartResolver::new(&mut archive, model);
736 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
737
738 assert_eq!(
739 meshes.len(),
740 2,
741 "Instancing (same object referenced twice) should produce 2 ResolvedMesh entries"
742 );
743 }
744
745 #[test]
746 fn test_resolve_unit_carried() {
747 let mut model = Model::default();
750 model.unit = Unit::Inch;
751 let obj1 = mesh_object(1, ObjectType::Model, None);
752 model.resources.add_object(obj1).unwrap();
753 model.build.items.push(build_item(1));
754
755 let mut archive = MockArchive::new();
756 let mut resolver = PartResolver::new(&mut archive, model);
757 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
758
759 assert_eq!(meshes.len(), 1);
760 assert_eq!(meshes[0].unit, Unit::Inch);
761 }
762
763 #[test]
764 fn test_resolve_empty_build() {
765 let model = Model::default();
767 let mut archive = MockArchive::new();
768 let mut resolver = PartResolver::new(&mut archive, model);
769 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
770 assert!(meshes.is_empty());
771 }
772
773 #[test]
774 fn test_resolve_object_name_carried() {
775 let mut model = Model::default();
778 let obj1 = mesh_object(1, ObjectType::Model, Some("MyObject"));
779 model.resources.add_object(obj1).unwrap();
780 model.build.items.push(build_item(1));
781
782 let mut archive = MockArchive::new();
783 let mut resolver = PartResolver::new(&mut archive, model);
784 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
785
786 assert_eq!(meshes.len(), 1);
787 assert_eq!(meshes[0].name, Some("MyObject".to_string()));
788 }
789
790 #[test]
791 fn test_resolve_cross_file_components() {
792 let mut sub_model = Model::default();
800 let sub_obj1 = mesh_object(1, ObjectType::Model, None);
801 let sub_obj2 = mesh_object(2, ObjectType::Other, None);
802 sub_model.resources.add_object(sub_obj1).unwrap();
803 sub_model.resources.add_object(sub_obj2).unwrap();
804 let sub_xml = model_to_xml_bytes(&sub_model);
805
806 let sub_path = "3D/Objects/object_1.model";
808 let mut root_model = Model::default();
809 let comp1 = component_with_path(1, sub_path, glam::Mat4::IDENTITY);
810 let comp2 = component_with_path(2, sub_path, glam::Mat4::IDENTITY);
811 let root_obj = components_object(8, vec![comp1, comp2]);
812 root_model.resources.add_object(root_obj).unwrap();
813 root_model.build.items.push(build_item(8));
814
815 let mut archive = MockArchive::new();
817 archive.add_entry(sub_path, sub_xml);
818
819 let mut resolver = PartResolver::new(&mut archive, root_model);
820
821 let meshes = resolver.resolve_meshes(&ResolveOptions::default()).unwrap();
823 assert_eq!(
824 meshes.len(),
825 1,
826 "Expected 1 mesh (type=other filtered out), got {}",
827 meshes.len()
828 );
829
830 let mut archive2 = MockArchive::new();
832 let sub_xml2 = model_to_xml_bytes(&sub_model);
833 archive2.add_entry(sub_path, sub_xml2);
834 let mut resolver2 = PartResolver::new(&mut archive2, {
835 let sub_path2 = "3D/Objects/object_1.model";
836 let mut root_model2 = Model::default();
837 let comp1b = component_with_path(1, sub_path2, glam::Mat4::IDENTITY);
838 let comp2b = component_with_path(2, sub_path2, glam::Mat4::IDENTITY);
839 let root_obj2 = components_object(8, vec![comp1b, comp2b]);
840 root_model2.resources.add_object(root_obj2).unwrap();
841 root_model2.build.items.push(build_item(8));
842 root_model2
843 });
844 let opts = ResolveOptions {
845 filter_other_objects: false,
846 ..Default::default()
847 };
848 let meshes2 = resolver2.resolve_meshes(&opts).unwrap();
849 assert_eq!(
850 meshes2.len(),
851 2,
852 "Expected 2 meshes when filter_other_objects=false"
853 );
854 }
855}