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}