lib3mf_core/validation/
semantic.rs

1use crate::model::{Geometry, Model, ResourceId};
2use crate::validation::report::ValidationReport;
3use std::collections::{HashMap, HashSet};
4
5pub fn validate_semantic(model: &Model, report: &mut ValidationReport) {
6    // Validate build items
7    validate_build_references(model, report);
8
9    // Validate boolean operation cycles
10    validate_boolean_cycles(model, report);
11
12    // Check Resources
13    for object in model.resources.iter_objects() {
14        // Check PID validity
15        if let Some(pid) = object.pid {
16            // Must exist in base_materials or color_groups or texture_groups
17            if !model.resources.exists(pid) {
18                report.add_error(
19                    2001,
20                    format!(
21                        "Object {} references non-existent property group {}",
22                        object.id.0, pid.0
23                    ),
24                );
25            }
26        }
27
28        match &object.geometry {
29            Geometry::Mesh(mesh) => {
30                for (i, tri) in mesh.triangles.iter().enumerate() {
31                    // Check indices bounds
32                    if tri.v1 as usize >= mesh.vertices.len()
33                        || tri.v2 as usize >= mesh.vertices.len()
34                        || tri.v3 as usize >= mesh.vertices.len()
35                    {
36                        report.add_error(
37                            3001,
38                            format!(
39                                "Triangle {} in Object {} references out-of-bounds vertex",
40                                i, object.id.0
41                            ),
42                        );
43                    }
44
45                    // Check PID
46                    if let Some(pid) = tri.pid.map(crate::model::ResourceId)
47                        && !model.resources.exists(pid)
48                    {
49                        report.add_error(2002, format!("Triangle {} in Object {} references non-existent property group {}", i, object.id.0, pid.0));
50                    }
51                }
52            }
53            Geometry::Components(comps) => {
54                for comp in &comps.components {
55                    // Only validate internal references (components without external path)
56                    if comp.path.is_none() && model.resources.get_object(comp.object_id).is_none() {
57                        report.add_error(
58                            2003,
59                            format!(
60                                "Component in Object {} references non-existent object {}",
61                                object.id.0, comp.object_id.0
62                            ),
63                        );
64                    }
65                }
66            }
67            Geometry::SliceStack(stack_id) => {
68                if model.resources.get_slice_stack(*stack_id).is_none() {
69                    report.add_error(
70                        2004,
71                        format!(
72                            "Object {} references non-existent slicestack {}",
73                            object.id.0, stack_id.0
74                        ),
75                    );
76                }
77            }
78            Geometry::VolumetricStack(stack_id) => {
79                if model.resources.get_volumetric_stack(*stack_id).is_none() {
80                    report.add_error(
81                        2005,
82                        format!(
83                            "Object {} references non-existent volumetricstack {}",
84                            object.id.0, stack_id.0
85                        ),
86                    );
87                }
88            }
89            Geometry::BooleanShape(bs) => {
90                // Validate base object exists and is valid type
91                if let Some(base_obj) = model.resources.get_object(bs.base_object_id) {
92                    // Base can be Mesh or another BooleanShape (per spec)
93                    match &base_obj.geometry {
94                        Geometry::Mesh(_) | Geometry::BooleanShape(_) => {
95                            // Valid base types
96                        }
97                        Geometry::Components(_) => {
98                            report.add_error(
99                                2101,
100                                format!(
101                                    "BooleanShape {} base object {} cannot be Components type",
102                                    object.id.0, bs.base_object_id.0
103                                ),
104                            );
105                        }
106                        _ => {
107                            // Other extensions (SliceStack, VolumetricStack) - allow per spec extensibility
108                        }
109                    }
110                } else {
111                    report.add_error(
112                        2102,
113                        format!(
114                            "BooleanShape {} references non-existent base object {}",
115                            object.id.0, bs.base_object_id.0
116                        ),
117                    );
118                }
119
120                // Validate each operation object
121                for (idx, op) in bs.operations.iter().enumerate() {
122                    if let Some(op_obj) = model.resources.get_object(op.object_id) {
123                        // Operation objects MUST be triangle meshes (not Components, not BooleanShape)
124                        match &op_obj.geometry {
125                            Geometry::Mesh(_) => {
126                                // Valid - mesh object
127                            }
128                            _ => {
129                                report.add_error(
130                                    2103,
131                                    format!(
132                                        "BooleanShape {} operation {} references non-mesh object {} (type must be mesh)",
133                                        object.id.0, idx, op.object_id.0
134                                    ),
135                                );
136                            }
137                        }
138                    } else {
139                        report.add_error(
140                            2104,
141                            format!(
142                                "BooleanShape {} operation {} references non-existent object {}",
143                                object.id.0, idx, op.object_id.0
144                            ),
145                        );
146                    }
147                }
148
149                // Validate base transformation matrix
150                if !is_transform_valid(&bs.base_transform) {
151                    report.add_error(
152                        2106,
153                        format!(
154                            "BooleanShape {} has invalid base transformation matrix (contains NaN or Infinity)",
155                            object.id.0
156                        ),
157                    );
158                }
159
160                // Validate operation transformation matrices
161                for (idx, op) in bs.operations.iter().enumerate() {
162                    if !is_transform_valid(&op.transform) {
163                        report.add_error(
164                            2105,
165                            format!(
166                                "BooleanShape {} operation {} has invalid transformation matrix (contains NaN or Infinity)",
167                                object.id.0, idx
168                            ),
169                        );
170                    }
171                }
172            }
173            Geometry::DisplacementMesh(_mesh) => {
174                // Displacement mesh validation will be implemented in displacement.rs
175                // For now, just allow it to pass semantic checks
176            }
177        }
178    }
179}
180
181fn validate_build_references(model: &Model, report: &mut ValidationReport) {
182    for (idx, item) in model.build.items.iter().enumerate() {
183        // Check if referenced object exists
184        if let Some(obj) = model.resources.get_object(item.object_id) {
185            // Check type constraint: Other cannot be in build
186            if !obj.object_type.can_be_in_build() {
187                report.add_error(
188                    3010,
189                    format!(
190                        "Build item {} references object {} with type '{}' which cannot be in build",
191                        idx, item.object_id.0, obj.object_type
192                    ),
193                );
194            }
195        } else {
196            // Existing check: object must exist
197            report.add_error(
198                3002,
199                format!(
200                    "Build item {} references non-existent object {}",
201                    idx, item.object_id.0
202                ),
203            );
204        }
205    }
206}
207
208/// Detects cycles in boolean operation graphs using DFS with recursion stack.
209fn validate_boolean_cycles(model: &Model, report: &mut ValidationReport) {
210    // Build adjacency list: BooleanShape -> referenced objects
211    let mut graph: HashMap<ResourceId, Vec<ResourceId>> = HashMap::new();
212
213    for obj in model.resources.iter_objects() {
214        if let Geometry::BooleanShape(bs) = &obj.geometry {
215            let mut refs = vec![bs.base_object_id];
216            refs.extend(bs.operations.iter().map(|op| op.object_id));
217            graph.insert(obj.id, refs);
218        }
219    }
220
221    // DFS for cycle detection
222    let mut visited = HashSet::new();
223    let mut rec_stack = HashSet::new();
224
225    for &start_id in graph.keys() {
226        if !visited.contains(&start_id)
227            && has_cycle_dfs(start_id, &graph, &mut visited, &mut rec_stack)
228        {
229            report.add_error(
230                2100,
231                format!(
232                    "Cycle detected in boolean operation graph involving object {}",
233                    start_id.0
234                ),
235            );
236        }
237    }
238}
239
240fn has_cycle_dfs(
241    node: ResourceId,
242    graph: &HashMap<ResourceId, Vec<ResourceId>>,
243    visited: &mut HashSet<ResourceId>,
244    rec_stack: &mut HashSet<ResourceId>,
245) -> bool {
246    visited.insert(node);
247    rec_stack.insert(node);
248
249    if let Some(neighbors) = graph.get(&node) {
250        for &neighbor in neighbors {
251            // Only follow edges to other BooleanShape objects (those in the graph)
252            if graph.contains_key(&neighbor) {
253                if !visited.contains(&neighbor) {
254                    if has_cycle_dfs(neighbor, graph, visited, rec_stack) {
255                        return true;
256                    }
257                } else if rec_stack.contains(&neighbor) {
258                    // Back edge found = cycle
259                    return true;
260                }
261            }
262        }
263    }
264
265    rec_stack.remove(&node);
266    false
267}
268
269/// Validates that a transformation matrix contains only finite values.
270fn is_transform_valid(mat: &glam::Mat4) -> bool {
271    mat.x_axis.is_finite()
272        && mat.y_axis.is_finite()
273        && mat.z_axis.is_finite()
274        && mat.w_axis.is_finite()
275}