lib3mf_core/utils/
diff.rs

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