lib3mf_core/model/
stats_impl.rs

1use 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        // 1. Process Build Items (Entry points)
15        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        // 2. Production Stats
27        let prod_stats = ProductionStats {
28            uuid_count: 0, // Placeholder
29        };
30
31        // 3. Vendor Data
32        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        // 4. Material Stats
49        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        // 5. Displacement Stats
58        let displacement_stats = self.compute_displacement_stats();
59
60        // 6. Thumbnails
61        // Check archiver for package thumbnail (attachments may not be loaded)
62        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        // Count DisplacementMesh objects and accumulate metrics
94        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                // Count triangles with displacement indices
102                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                // Determine the next path to use for children.
125                // If this object was found in a specific path, children inherit it
126                // UNLESS they specify their own.
127                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            // Count object by type
148            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                        // Priority:
181                        // 1. component's own path
182                        // 2. path inherited from parent (path_to_use)
183                        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}