Skip to main content

lib3mf_core/utils/
diff.rs

1use crate::model::Model;
2use serde::{Deserialize, Serialize};
3
4/// The computed diff between two 3MF models.
5#[derive(Debug, Default, Serialize, Deserialize)]
6pub struct ModelDiff {
7    /// Differences in metadata key-value pairs.
8    pub metadata_diffs: Vec<MetadataDiff>,
9    /// Differences in resource objects (added, removed, or changed).
10    pub resource_diffs: Vec<ResourceDiff>,
11    /// Differences in build item lists.
12    pub build_diffs: Vec<BuildDiff>,
13}
14
15/// A difference in a single metadata key between two models.
16#[derive(Debug, Serialize, Deserialize)]
17pub struct MetadataDiff {
18    /// The metadata key that differs.
19    pub key: String,
20    /// The value in model A (or `None` if absent).
21    pub old_value: Option<String>,
22    /// The value in model B (or `None` if absent).
23    pub new_value: Option<String>,
24}
25
26/// A difference in a resource between two models.
27#[derive(Debug, Serialize, Deserialize)]
28pub enum ResourceDiff {
29    /// A resource was added in model B.
30    Added {
31        /// Resource ID.
32        id: u32,
33        /// Type name of the resource (e.g., `"Mesh"`, `"Components"`).
34        type_name: String,
35    },
36    /// A resource was removed from model A.
37    Removed {
38        /// Resource ID.
39        id: u32,
40        /// Type name of the resource.
41        type_name: String,
42    },
43    /// A resource changed between the two models.
44    Changed {
45        /// Resource ID.
46        id: u32,
47        /// Human-readable descriptions of what changed.
48        details: Vec<String>,
49    },
50}
51
52/// A difference in the build item list between two models.
53#[derive(Debug, Serialize, Deserialize)]
54pub enum BuildDiff {
55    /// A build item was added in model B.
56    Added {
57        /// Object ID of the added build item.
58        object_id: u32,
59    },
60    /// A build item was removed from model A.
61    Removed {
62        /// Object ID of the removed build item.
63        object_id: u32,
64    },
65    /// A build item changed (e.g., count or transform).
66    Changed {
67        /// Object ID of the changed build item.
68        object_id: u32,
69        /// Human-readable descriptions of what changed.
70        details: Vec<String>,
71    },
72}
73
74impl ModelDiff {
75    /// Returns `true` if there are no differences between the two models.
76    pub fn is_empty(&self) -> bool {
77        self.metadata_diffs.is_empty()
78            && self.resource_diffs.is_empty()
79            && self.build_diffs.is_empty()
80    }
81}
82
83/// Compares two 3MF models and returns a `ModelDiff` describing the differences.
84pub fn compare_models(model_a: &Model, model_b: &Model) -> ModelDiff {
85    let mut diff = ModelDiff::default();
86
87    // 1. Compare Metadata
88    // Combine keys
89    let mut all_keys: Vec<_> = model_a.metadata.keys().collect();
90    for k in model_b.metadata.keys() {
91        if !all_keys.contains(&k) {
92            all_keys.push(k);
93        }
94    }
95    all_keys.sort();
96    all_keys.dedup();
97
98    for key in all_keys {
99        let val_a = model_a.metadata.get(key);
100        let val_b = model_b.metadata.get(key);
101
102        if val_a != val_b {
103            diff.metadata_diffs.push(MetadataDiff {
104                key: key.clone(),
105                old_value: val_a.cloned(),
106                new_value: val_b.cloned(),
107            });
108        }
109    }
110
111    // 2. Compare Resources
112    // Strategy: Match by ID.
113    // In strict 3MF, IDs are local to the package/model stream.
114    // If we compare different files, IDs might differ but content be same.
115    // However, usually we want to diff the *structure* as preserved.
116    // If comparing two versions of same file, ID matching is appropriate.
117    // If comparing totally different files, this naive ID matching might be noisy.
118    // We assume "semantic version diff" here, so ID matching is primary.
119
120    let resources_a = &model_a.resources;
121    let resources_b = &model_b.resources;
122
123    // Check Removed or Changed
124    for res_a in resources_a.iter_objects() {
125        match resources_b.get_object(res_a.id) {
126            Some(res_b) => {
127                let type_a = get_geometry_type_name(&res_a.geometry);
128                let type_b = get_geometry_type_name(&res_b.geometry);
129
130                if type_a != type_b {
131                    diff.resource_diffs.push(ResourceDiff::Changed {
132                        id: res_a.id.0,
133                        details: vec![format!("Type changed: {} -> {}", type_a, type_b)],
134                    });
135                } else {
136                    // Check mesh data if mesh
137                    if let (
138                        crate::model::Geometry::Mesh(mesh_a),
139                        crate::model::Geometry::Mesh(mesh_b),
140                    ) = (&res_a.geometry, &res_b.geometry)
141                    {
142                        let mut details = Vec::new();
143                        if mesh_a.vertices.len() != mesh_b.vertices.len() {
144                            details.push(format!(
145                                "Vertex count changed: {} -> {}",
146                                mesh_a.vertices.len(),
147                                mesh_b.vertices.len()
148                            ));
149                        }
150                        if mesh_a.triangles.len() != mesh_b.triangles.len() {
151                            details.push(format!(
152                                "Triangle count changed: {} -> {}",
153                                mesh_a.triangles.len(),
154                                mesh_b.triangles.len()
155                            ));
156                        }
157                        // TODO: Implement deeper hash comparison
158
159                        if !details.is_empty() {
160                            diff.resource_diffs.push(ResourceDiff::Changed {
161                                id: res_a.id.0,
162                                details,
163                            });
164                        }
165                    }
166                }
167            }
168            None => {
169                diff.resource_diffs.push(ResourceDiff::Removed {
170                    id: res_a.id.0,
171                    type_name: get_geometry_type_name(&res_a.geometry).to_string(),
172                });
173            }
174        }
175    }
176
177    // Check Added
178    for res_b in resources_b.iter_objects() {
179        if !resources_a.exists(res_b.id) {
180            diff.resource_diffs.push(ResourceDiff::Added {
181                id: res_b.id.0,
182                type_name: get_geometry_type_name(&res_b.geometry).to_string(),
183            });
184        }
185    }
186
187    // 3. Compare Build Items
188    // Naive matching by object_id.
189    // Build items are a list, order matters technically for printing but usually we treat as set of items to build.
190    // But duplicate instances allowed? Spec says "one or more item elements".
191    // We'll compare by (object_id, transform) tuples roughly.
192    // Or just ID existence.
193    // Let's iterate both lists.
194
195    if model_a.build.items.len() != model_b.build.items.len() {
196        diff.build_diffs.push(BuildDiff::Changed {
197            object_id: 0, // Global placeholder
198            details: vec![format!(
199                "Item count changed: {} -> {}",
200                model_a.build.items.len(),
201                model_b.build.items.len()
202            )],
203        });
204    }
205
206    diff
207}
208
209fn get_geometry_type_name(g: &crate::model::Geometry) -> &'static str {
210    match g {
211        crate::model::Geometry::Mesh(_) => "Mesh",
212        crate::model::Geometry::Components(_) => "Components",
213        crate::model::Geometry::SliceStack(_) => "SliceStack",
214        crate::model::Geometry::VolumetricStack(_) => "VolumetricStack",
215        crate::model::Geometry::BooleanShape(_) => "BooleanShape",
216        crate::model::Geometry::DisplacementMesh(_) => "DisplacementMesh",
217    }
218}