lib3mf_core/validation/
displacement.rs

1use crate::model::{DisplacementMesh, Geometry, Model, ObjectType, ResourceId, Unit};
2use crate::validation::{ValidationLevel, ValidationReport};
3
4/// Validate displacement-specific resources and geometry.
5///
6/// This function validates:
7/// - Displacement2D texture resources (path, height, offset)
8/// - DisplacementMesh geometry (normals, gradients, texture coordinates)
9///
10/// Validation is progressive based on level:
11/// - Standard: Reference integrity, count matching
12/// - Paranoid: Geometric correctness (unit normals, finite values)
13pub fn validate_displacement(model: &Model, level: ValidationLevel, report: &mut ValidationReport) {
14    // Validate Displacement2D resources
15    validate_displacement_resources(model, level, report);
16
17    // Validate DisplacementMesh geometry
18    for object in model.resources.iter_objects() {
19        if let Geometry::DisplacementMesh(dmesh) = &object.geometry {
20            validate_displacement_mesh(dmesh, object.id, level, report, &model.resources);
21        }
22    }
23}
24
25/// Validate DisplacementMesh geometry (called from geometry validator).
26pub fn validate_displacement_mesh_geometry(
27    mesh: &DisplacementMesh,
28    oid: ResourceId,
29    _object_type: ObjectType,
30    level: ValidationLevel,
31    report: &mut ValidationReport,
32    _unit: Unit,
33) {
34    // For now, we don't need object_type or unit, but they're here for consistency
35    // with validate_mesh signature. Resources need to be passed differently.
36    // This is a simplified wrapper - full validation requires model context.
37
38    // Basic validation without resource context
39    if mesh.vertices.is_empty() {
40        report.add_error(
41            5010,
42            format!("DisplacementMesh object {} has no vertices", oid.0),
43        );
44    }
45
46    if mesh.triangles.is_empty() {
47        report.add_error(
48            5011,
49            format!("DisplacementMesh object {} has no triangles", oid.0),
50        );
51    }
52
53    // Critical: Normal count must match vertex count
54    if mesh.normals.len() != mesh.vertices.len() {
55        report.add_error(
56            5012,
57            format!(
58                "Object {} has {} vertices but {} normals",
59                oid.0,
60                mesh.vertices.len(),
61                mesh.normals.len()
62            ),
63        );
64    }
65
66    // Validate triangle vertex indices
67    let vertex_count = mesh.vertices.len();
68    for (i, tri) in mesh.triangles.iter().enumerate() {
69        if tri.v1 as usize >= vertex_count
70            || tri.v2 as usize >= vertex_count
71            || tri.v3 as usize >= vertex_count
72        {
73            report.add_error(
74                5013,
75                format!(
76                    "Triangle {} in object {} has out-of-bounds vertex index",
77                    i, oid.0
78                ),
79            );
80        }
81    }
82
83    // Validate gradient count if present
84    if let Some(gradients) = &mesh.gradients
85        && gradients.len() != mesh.vertices.len()
86    {
87        report.add_error(
88            5015,
89            format!(
90                "Object {} has {} vertices but {} gradient vectors",
91                oid.0,
92                mesh.vertices.len(),
93                gradients.len()
94            ),
95        );
96    }
97
98    // Paranoid level: Geometric correctness
99    if level >= ValidationLevel::Paranoid {
100        for (i, normal) in mesh.normals.iter().enumerate() {
101            if !normal.nx.is_finite() || !normal.ny.is_finite() || !normal.nz.is_finite() {
102                report.add_error(
103                    5020,
104                    format!(
105                        "Normal {} in object {} contains non-finite values",
106                        i, oid.0
107                    ),
108                );
109            }
110
111            // Check unit length (with tolerance)
112            let length_sq = normal.nx * normal.nx + normal.ny * normal.ny + normal.nz * normal.nz;
113            if (length_sq - 1.0).abs() > 1e-4 {
114                report.add_warning(
115                    5021,
116                    format!(
117                        "Normal {} in object {} is not unit length (length^2 = {})",
118                        i, oid.0, length_sq
119                    ),
120                );
121            }
122        }
123
124        // Validate gradient vectors
125        if let Some(gradients) = &mesh.gradients {
126            for (i, grad) in gradients.iter().enumerate() {
127                if !grad.gu.is_finite() || !grad.gv.is_finite() {
128                    report.add_error(
129                        5022,
130                        format!(
131                            "Gradient {} in object {} contains non-finite values",
132                            i, oid.0
133                        ),
134                    );
135                }
136            }
137        }
138    }
139}
140
141/// Validate Displacement2D texture resources.
142fn validate_displacement_resources(
143    model: &Model,
144    level: ValidationLevel,
145    report: &mut ValidationReport,
146) {
147    for res in model.resources.iter_displacement_2d() {
148        // Standard level: Basic path validation
149        if level >= ValidationLevel::Standard {
150            if res.path.is_empty() {
151                report.add_error(
152                    5001,
153                    format!("Displacement2D resource {} has empty path", res.id.0),
154                );
155            }
156
157            // Check if path references existing attachment (warning, not error)
158            if !res.path.is_empty() && !model.attachments.contains_key(&res.path) {
159                report.add_warning(
160                    5002,
161                    format!(
162                        "Displacement2D resource {} references non-existent attachment '{}'",
163                        res.id.0, res.path
164                    ),
165                );
166            }
167        }
168
169        // Paranoid level: Validate numeric parameters
170        if level >= ValidationLevel::Paranoid {
171            if !res.height.is_finite() {
172                report.add_error(
173                    5003,
174                    format!(
175                        "Displacement2D resource {} has non-finite height: {}",
176                        res.id.0, res.height
177                    ),
178                );
179            }
180
181            if !res.offset.is_finite() {
182                report.add_error(
183                    5004,
184                    format!(
185                        "Displacement2D resource {} has non-finite offset: {}",
186                        res.id.0, res.offset
187                    ),
188                );
189            }
190
191            // PNG validation (not yet implemented)
192            // TODO: Add PNG validation when png-validation feature is added
193            let _ = model; // Suppress unused variable warning
194        }
195    }
196}
197
198// PNG validation function removed - requires png-validation feature to be added to Cargo.toml
199// TODO: Add back when png dependency is added
200
201/// Validate DisplacementMesh geometry.
202fn validate_displacement_mesh(
203    mesh: &DisplacementMesh,
204    oid: ResourceId,
205    level: ValidationLevel,
206    report: &mut ValidationReport,
207    resources: &crate::model::ResourceCollection,
208) {
209    // Minimal level: Basic structural validation (always run)
210    if mesh.vertices.is_empty() {
211        report.add_error(
212            5010,
213            format!("DisplacementMesh object {} has no vertices", oid.0),
214        );
215    }
216
217    if mesh.triangles.is_empty() {
218        report.add_error(
219            5011,
220            format!("DisplacementMesh object {} has no triangles", oid.0),
221        );
222    }
223
224    // Critical: Normal count must match vertex count
225    if mesh.normals.len() != mesh.vertices.len() {
226        report.add_error(
227            5012,
228            format!(
229                "Object {} has {} vertices but {} normals",
230                oid.0,
231                mesh.vertices.len(),
232                mesh.normals.len()
233            ),
234        );
235    }
236
237    // Validate triangle vertex indices
238    let vertex_count = mesh.vertices.len();
239    for (i, tri) in mesh.triangles.iter().enumerate() {
240        if tri.v1 as usize >= vertex_count
241            || tri.v2 as usize >= vertex_count
242            || tri.v3 as usize >= vertex_count
243        {
244            report.add_error(
245                5013,
246                format!(
247                    "Triangle {} in object {} has out-of-bounds vertex index",
248                    i, oid.0
249                ),
250            );
251        }
252    }
253
254    // Standard level: Reference integrity
255    if level >= ValidationLevel::Standard {
256        // Validate displacement texture coordinate indices
257        for (i, tri) in mesh.triangles.iter().enumerate() {
258            if let Some(d1) = tri.d1 {
259                validate_displacement_index(oid, i, d1, resources, report);
260            }
261            if let Some(d2) = tri.d2 {
262                validate_displacement_index(oid, i, d2, resources, report);
263            }
264            if let Some(d3) = tri.d3 {
265                validate_displacement_index(oid, i, d3, resources, report);
266            }
267        }
268
269        // Validate gradient count if present
270        if let Some(gradients) = &mesh.gradients
271            && gradients.len() != mesh.vertices.len()
272        {
273            report.add_error(
274                5015,
275                format!(
276                    "Object {} has {} vertices but {} gradient vectors",
277                    oid.0,
278                    mesh.vertices.len(),
279                    gradients.len()
280                ),
281            );
282        }
283    }
284
285    // Paranoid level: Geometric correctness
286    if level >= ValidationLevel::Paranoid {
287        // Validate normal vectors
288        for (i, normal) in mesh.normals.iter().enumerate() {
289            // Check for finite values
290            if !normal.nx.is_finite() || !normal.ny.is_finite() || !normal.nz.is_finite() {
291                report.add_error(
292                    5020,
293                    format!(
294                        "Normal {} in object {} contains non-finite values",
295                        i, oid.0
296                    ),
297                );
298                continue;
299            }
300
301            // Check unit length (should be ~1.0)
302            let length_sq = normal.nx * normal.nx + normal.ny * normal.ny + normal.nz * normal.nz;
303            let length = length_sq.sqrt();
304            let tolerance = 1e-4;
305            if (length - 1.0).abs() > tolerance {
306                report.add_warning(
307                    5021,
308                    format!(
309                        "Normal {} in object {} is not unit length (length: {:.6})",
310                        i, oid.0, length
311                    ),
312                );
313            }
314        }
315
316        // Validate gradient vectors if present
317        if let Some(gradients) = &mesh.gradients {
318            for (i, gradient) in gradients.iter().enumerate() {
319                if !gradient.gu.is_finite() || !gradient.gv.is_finite() {
320                    report.add_error(
321                        5022,
322                        format!(
323                            "Gradient {} in object {} contains non-finite values",
324                            i, oid.0
325                        ),
326                    );
327                }
328            }
329
330            // Optionally check orthogonality (informational)
331            // This is a quality check - gradients should be orthogonal to normals
332            // for best displacement mapping results, but not strictly required
333            for (i, (normal, gradient)) in mesh.normals.iter().zip(gradients.iter()).enumerate() {
334                if normal.nx.is_finite()
335                    && normal.ny.is_finite()
336                    && normal.nz.is_finite()
337                    && gradient.gu.is_finite()
338                    && gradient.gv.is_finite()
339                {
340                    // For a proper check we'd need to convert 2D gradient to 3D
341                    // and check dot product with normal. This is complex and
342                    // extension-specific, so we just provide an info message
343                    // if gradients exist.
344                    if i == 0 {
345                        report.add_info(
346                            5023,
347                            format!(
348                                "Object {} has gradient vectors (orthogonality not verified)",
349                                oid.0
350                            ),
351                        );
352                        break; // Only report once per object
353                    }
354                }
355            }
356        }
357    }
358}
359
360/// Helper: Validate displacement texture coordinate index references.
361fn validate_displacement_index(
362    oid: ResourceId,
363    tri_idx: usize,
364    d_index: u32,
365    resources: &crate::model::ResourceCollection,
366    report: &mut ValidationReport,
367) {
368    // Displacement indices are stored as ResourceId in triangle
369    // They should reference Displacement2D resources
370    let rid = ResourceId(d_index);
371    if resources.get_displacement_2d(rid).is_none() {
372        report.add_error(
373            5014,
374            format!(
375                "Triangle {} in object {} references non-existent displacement texture {}",
376                tri_idx, oid.0, d_index
377            ),
378        );
379    }
380}