lib3mf_core/model/
stats_impl.rs1use crate::archive::ArchiveReader;
2use crate::error::Result;
3use crate::model::stats::{
4 DisplacementStats, GeometryStats, MaterialsStats, ModelStats, ProductionStats, VendorData,
5};
6use crate::model::{Geometry, Model};
7use crate::parser::bambu_config::parse_model_settings;
8
9impl Model {
10 pub fn compute_stats(&self, archiver: &mut impl ArchiveReader) -> Result<ModelStats> {
11 let mut resolver = crate::model::resolver::PartResolver::new(archiver, self.clone());
12 let mut geom_stats = GeometryStats::default();
13
14 for item in &self.build.items {
16 geom_stats.instance_count += 1;
17 self.accumulate_object_stats(
18 item.object_id,
19 item.path.as_deref(),
20 item.transform,
21 &mut resolver,
22 &mut geom_stats,
23 )?;
24 }
25
26 let prod_stats = ProductionStats {
28 uuid_count: 0, };
30
31 let mut vendor_data = VendorData::default();
33 let generator = self.metadata.get("Application").cloned();
34
35 if let Some(app) = &generator
36 && (app.contains("Bambu") || app.contains("Orca"))
37 && resolver
38 .archive_mut()
39 .entry_exists("Metadata/model_settings.config")
40 && let Ok(content) = resolver
41 .archive_mut()
42 .read_entry("Metadata/model_settings.config")
43 && let Ok(plates) = parse_model_settings(&content)
44 {
45 vendor_data.plates = plates;
46 }
47
48 let materials_stats = MaterialsStats {
50 base_materials_count: self.resources.base_material_groups_count(),
51 color_groups_count: self.resources.color_groups_count(),
52 texture_2d_groups_count: self.resources.texture_2d_groups_count(),
53 composite_materials_count: self.resources.composite_materials_count(),
54 multi_properties_count: self.resources.multi_properties_count(),
55 };
56
57 let displacement_stats = self.compute_displacement_stats();
59
60 let pkg_thumb = archiver.entry_exists("Metadata/thumbnail.png")
63 || archiver.entry_exists("/Metadata/thumbnail.png");
64 let obj_thumb_count = self
65 .resources
66 .iter_objects()
67 .filter(|o| o.thumbnail.is_some())
68 .count();
69
70 Ok(ModelStats {
71 unit: self.unit,
72 generator,
73 metadata: self.metadata.clone(),
74 geometry: geom_stats,
75 materials: materials_stats,
76 production: prod_stats,
77 displacement: displacement_stats,
78 vendor: vendor_data,
79 system_info: crate::utils::hardware::detect_capabilities(),
80 thumbnails: crate::model::stats::ThumbnailStats {
81 package_thumbnail_present: pkg_thumb,
82 object_thumbnail_count: obj_thumb_count,
83 },
84 })
85 }
86
87 fn compute_displacement_stats(&self) -> DisplacementStats {
88 let mut stats = DisplacementStats {
89 texture_count: self.resources.displacement_2d_count(),
90 ..Default::default()
91 };
92
93 for obj in self.resources.iter_objects() {
95 if let Geometry::DisplacementMesh(dmesh) = &obj.geometry {
96 stats.mesh_count += 1;
97 stats.normal_count += dmesh.normals.len() as u64;
98 stats.gradient_count += dmesh.gradients.as_ref().map_or(0, |g| g.len() as u64);
99 stats.total_triangle_count += dmesh.triangles.len() as u64;
100
101 for tri in &dmesh.triangles {
103 if tri.d1.is_some() || tri.d2.is_some() || tri.d3.is_some() {
104 stats.displaced_triangle_count += 1;
105 }
106 }
107 }
108 }
109
110 stats
111 }
112
113 fn accumulate_object_stats(
114 &self,
115 id: crate::model::ResourceId,
116 path: Option<&str>,
117 transform: glam::Mat4,
118 resolver: &mut crate::model::resolver::PartResolver<impl ArchiveReader>,
119 stats: &mut GeometryStats,
120 ) -> Result<()> {
121 let (geom, path_to_use, obj_type) = {
122 let resolved = resolver.resolve_object(id, path)?;
123 if let Some((_model, object)) = resolved {
124 let current_path = if path.is_none()
128 || path == Some("ROOT")
129 || path == Some("/3D/3dmodel.model")
130 || path == Some("3D/3dmodel.model")
131 {
132 None
133 } else {
134 path
135 };
136 (
137 Some(object.geometry.clone()),
138 current_path.map(|s| s.to_string()),
139 Some(object.object_type),
140 )
141 } else {
142 (None, None, None)
143 }
144 };
145
146 if let Some(geometry) = geom {
147 if let Some(ot) = obj_type {
149 *stats.type_counts.entry(ot.to_string()).or_insert(0) += 1;
150 }
151
152 match geometry {
153 Geometry::Mesh(mesh) => {
154 stats.object_count += 1;
155 stats.vertex_count += mesh.vertices.len() as u64;
156 stats.triangle_count += mesh.triangles.len() as u64;
157
158 if let Some(mesh_aabb) = mesh.compute_aabb() {
159 let transformed_aabb = mesh_aabb.transform(transform);
160 if let Some(total_aabb) = &mut stats.bounding_box {
161 total_aabb.min[0] = total_aabb.min[0].min(transformed_aabb.min[0]);
162 total_aabb.min[1] = total_aabb.min[1].min(transformed_aabb.min[1]);
163 total_aabb.min[2] = total_aabb.min[2].min(transformed_aabb.min[2]);
164 total_aabb.max[0] = total_aabb.max[0].max(transformed_aabb.max[0]);
165 total_aabb.max[1] = total_aabb.max[1].max(transformed_aabb.max[1]);
166 total_aabb.max[2] = total_aabb.max[2].max(transformed_aabb.max[2]);
167 } else {
168 stats.bounding_box = Some(transformed_aabb);
169 }
170 }
171
172 let (area, volume) = mesh.compute_area_and_volume();
173 let scale_det = transform.determinant().abs() as f64;
174 let area_scale = scale_det.powf(2.0 / 3.0);
175 stats.surface_area += area * area_scale;
176 stats.volume += volume * scale_det;
177 }
178 Geometry::Components(comps) => {
179 for comp in comps.components {
180 let next_path = comp.path.as_deref().or(path_to_use.as_deref());
184
185 self.accumulate_object_stats(
186 comp.object_id,
187 next_path,
188 transform * comp.transform,
189 resolver,
190 stats,
191 )?;
192 }
193 }
194 _ => {}
195 }
196 }
197 Ok(())
198 }
199}