Skip to main content

lib3mf_core/model/
stats.rs

1use crate::model::Unit;
2use crate::utils::hardware::HardwareCapabilities;
3use serde::{Deserialize, Serialize};
4use std::collections::HashMap;
5
6/// Comprehensive statistics and metadata for a 3MF model.
7///
8/// Aggregates various statistics about the model's geometry, materials,
9/// production metadata, and vendor-specific information. Used by the CLI
10/// `stats` command and for model analysis.
11#[derive(Debug, Clone, Default, Serialize, Deserialize)]
12pub struct ModelStats {
13    /// Unit of measurement for the model
14    pub unit: Unit,
15    /// Software that generated the model (from metadata)
16    pub generator: Option<String>,
17    /// Custom metadata key-value pairs from the model
18    pub metadata: HashMap<String, String>,
19    /// Geometric statistics (vertices, triangles, volume, etc.)
20    pub geometry: GeometryStats,
21    /// Material and property statistics
22    pub materials: MaterialsStats,
23    /// Production extension metadata statistics
24    pub production: ProductionStats,
25    /// Displacement extension statistics
26    pub displacement: DisplacementStats,
27    /// Vendor-specific data (e.g., Bambu Studio project info)
28    pub vendor: VendorData,
29    /// System hardware capabilities info
30    pub system_info: HardwareCapabilities,
31    /// Thumbnail statistics
32    pub thumbnails: ThumbnailStats,
33}
34
35#[derive(Debug, Clone, Default, Serialize, Deserialize)]
36pub struct ThumbnailStats {
37    pub package_thumbnail_present: bool,
38    pub object_thumbnail_count: usize,
39}
40
41#[derive(Debug, Clone, Default, Serialize, Deserialize)]
42pub struct MaterialsStats {
43    pub base_materials_count: usize,
44    pub color_groups_count: usize,
45    pub texture_2d_groups_count: usize,
46    pub composite_materials_count: usize,
47    pub multi_properties_count: usize,
48}
49
50/// Geometric statistics for the model.
51///
52/// Aggregates counts and measurements of the model's geometry including
53/// vertices, triangles, bounding box, surface area, and volume.
54#[derive(Debug, Clone, Default, Serialize, Deserialize)]
55pub struct GeometryStats {
56    /// Total number of object resources
57    pub object_count: usize,
58    /// Number of build items (instances to print)
59    pub instance_count: usize,
60    /// Total number of triangles across all meshes
61    pub triangle_count: u64,
62    /// Total number of vertices across all meshes
63    pub vertex_count: u64,
64    /// Axis-aligned bounding box of the entire model
65    pub bounding_box: Option<BoundingBox>,
66    /// Total surface area in square model units
67    pub surface_area: f64,
68    /// Total volume in cubic model units
69    pub volume: f64,
70    /// Whether all meshes are manifold (watertight)
71    pub is_manifold: bool,
72    /// Count of objects by type (e.g., {"model": 5, "support": 2})
73    #[serde(default)]
74    pub type_counts: HashMap<String, usize>,
75}
76
77/// An axis-aligned bounding box in 3D space.
78///
79/// Represents the smallest box (aligned with coordinate axes) that
80/// contains all geometry. Useful for understanding model size and
81/// for spatial queries.
82#[derive(Debug, Clone, Copy, Default, Serialize, Deserialize)]
83pub struct BoundingBox {
84    /// Minimum corner coordinates [x, y, z]
85    pub min: [f32; 3],
86    /// Maximum corner coordinates [x, y, z]
87    pub max: [f32; 3],
88}
89
90impl BoundingBox {
91    pub fn transform(&self, matrix: glam::Mat4) -> Self {
92        let corners = [
93            glam::Vec3::new(self.min[0], self.min[1], self.min[2]),
94            glam::Vec3::new(self.min[0], self.min[1], self.max[2]),
95            glam::Vec3::new(self.min[0], self.max[1], self.min[2]),
96            glam::Vec3::new(self.min[0], self.max[1], self.max[2]),
97            glam::Vec3::new(self.max[0], self.min[1], self.min[2]),
98            glam::Vec3::new(self.max[0], self.min[1], self.max[2]),
99            glam::Vec3::new(self.max[0], self.max[1], self.min[2]),
100            glam::Vec3::new(self.max[0], self.max[1], self.max[2]),
101        ];
102
103        let mut transformed_min = glam::Vec3::splat(f32::INFINITY);
104        let mut transformed_max = glam::Vec3::splat(f32::NEG_INFINITY);
105
106        for corner in corners {
107            let transformed = matrix.transform_point3(corner);
108            transformed_min = transformed_min.min(transformed);
109            transformed_max = transformed_max.max(transformed);
110        }
111
112        Self {
113            min: [transformed_min.x, transformed_min.y, transformed_min.z],
114            max: [transformed_max.x, transformed_max.y, transformed_max.z],
115        }
116    }
117}
118
119#[derive(Debug, Clone, Default, Serialize, Deserialize)]
120pub struct ProductionStats {
121    pub uuid_count: usize,
122}
123
124#[derive(Debug, Clone, Default, Serialize, Deserialize)]
125pub struct DisplacementStats {
126    pub mesh_count: usize,
127    pub texture_count: usize,
128    pub normal_count: u64,
129    pub gradient_count: u64,
130    pub displaced_triangle_count: u64,
131    pub total_triangle_count: u64,
132}
133
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
135pub struct VendorData {
136    pub printer_model: Option<String>,
137    pub filaments: Vec<FilamentInfo>,
138    pub plates: Vec<PlateInfo>,
139    pub print_time_estimate: Option<String>,
140    pub slicer_version: Option<String>,
141    pub nozzle_diameter: Option<f32>,
142    pub slicer_warnings: Vec<SlicerWarning>,
143    pub object_metadata: Vec<BambuObjectMetadata>,
144    pub project_settings: Option<BambuProjectSettings>,
145    pub profile_configs: Vec<BambuProfileConfig>,
146    pub assembly_info: Vec<AssemblyItem>,
147    /// Path to Bambu cover thumbnail (from OPC relationship), e.g., "Metadata/plate_1.png"
148    pub bambu_cover_thumbnail: Option<String>,
149    /// Path to Bambu embedded gcode (from OPC relationship), e.g., "Metadata/plate_1.gcode"
150    pub bambu_gcode: Option<String>,
151}
152
153#[derive(Debug, Clone, Default, Serialize, Deserialize)]
154pub struct FilamentInfo {
155    pub id: u32,
156    pub tray_info_idx: Option<String>,
157    pub type_: String,
158    pub color: Option<String>,
159    pub used_m: Option<f32>,
160    pub used_g: Option<f32>,
161}
162
163#[derive(Debug, Clone, Default, Serialize, Deserialize)]
164pub struct PlateInfo {
165    pub id: u32,
166    pub name: Option<String>,
167    pub locked: bool,
168    pub gcode_file: Option<String>,
169    pub thumbnail_file: Option<String>,
170    pub items: Vec<PlateModelInstance>,
171}
172
173#[derive(Debug, Clone, Default, Serialize, Deserialize)]
174pub struct PlateModelInstance {
175    pub object_id: u32,
176    pub instance_id: u32,
177    pub identify_id: Option<u32>,
178}
179
180#[derive(Debug, Clone, Default, Serialize, Deserialize)]
181pub struct SlicerWarning {
182    pub msg: String,
183    pub level: Option<String>,
184    pub error_code: Option<String>,
185}
186
187#[derive(Debug, Clone, Default, Serialize, Deserialize)]
188pub struct BambuObjectMetadata {
189    pub id: u32,
190    pub name: Option<String>,
191    pub extruder: Option<u32>,
192    pub face_count: Option<u64>,
193    pub parts: Vec<BambuPartMetadata>,
194}
195
196#[derive(Debug, Clone, Default, Serialize, Deserialize)]
197pub struct BambuPartMetadata {
198    pub id: u32,
199    pub subtype: PartSubtype,
200    pub name: Option<String>,
201    pub matrix: Option<String>,
202    pub source: Option<BambuPartSource>,
203    pub mesh_stat: Option<BambuMeshStat>,
204    pub print_overrides: HashMap<String, String>,
205}
206
207#[derive(Debug, Clone, Default, Serialize, Deserialize, PartialEq)]
208pub enum PartSubtype {
209    #[default]
210    NormalPart,
211    ModifierPart,
212    SupportBlocker,
213    SupportEnforcer,
214    Other(String),
215}
216
217impl PartSubtype {
218    /// Parse a Bambu part subtype string into a `PartSubtype` variant.
219    pub fn parse(s: &str) -> Self {
220        match s {
221            "normal_part" => Self::NormalPart,
222            "modifier_part" => Self::ModifierPart,
223            "support_blocker" => Self::SupportBlocker,
224            "support_enforcer" => Self::SupportEnforcer,
225            other => Self::Other(other.to_string()),
226        }
227    }
228}
229
230#[derive(Debug, Clone, Default, Serialize, Deserialize)]
231pub struct BambuPartSource {
232    pub volume_id: Option<u32>,
233    pub offset_x: Option<f64>,
234    pub offset_y: Option<f64>,
235    pub offset_z: Option<f64>,
236}
237
238#[derive(Debug, Clone, Default, Serialize, Deserialize)]
239pub struct BambuMeshStat {
240    pub edges_fixed: Option<u32>,
241    pub degenerate_facets: Option<u32>,
242    pub facets_removed: Option<u32>,
243    pub facets_reversed: Option<u32>,
244    pub backwards_edges: Option<u32>,
245}
246
247#[derive(Debug, Clone, Default, Serialize, Deserialize)]
248pub struct BambuProjectSettings {
249    pub printer_model: Option<String>,
250    pub printer_inherits: Option<String>,
251    pub bed_type: Option<String>,
252    pub layer_height: Option<f32>,
253    pub first_layer_height: Option<f32>,
254    pub filament_type: Vec<String>,
255    pub filament_colour: Vec<String>,
256    pub nozzle_diameter: Vec<f32>,
257    pub print_sequence: Option<String>,
258    pub wall_loops: Option<u32>,
259    pub infill_density: Option<String>,
260    pub support_type: Option<String>,
261    pub extras: HashMap<String, serde_json::Value>,
262}
263
264#[derive(Debug, Clone, Default, Serialize, Deserialize)]
265pub struct BambuProfileConfig {
266    pub config_type: String, // "filament", "machine", "process"
267    pub index: u32,          // the N in filament_settings_N.config
268    pub inherits: Option<String>,
269    pub name: Option<String>,
270    pub extras: HashMap<String, serde_json::Value>,
271}
272
273#[derive(Debug, Clone, Default, Serialize, Deserialize)]
274pub struct AssemblyItem {
275    pub object_id: u32,
276    pub instance_count: u32,
277    pub transform: Option<String>,
278    pub offset: Option<String>,
279}