Skip to main content

lib3mf/validator/
mod.rs

1//! Validation logic for 3MF models
2//!
3//! This module contains functions to validate 3MF models according to the
4//! 3MF Core Specification requirements. Validation ensures that:
5//! - All object IDs are unique and positive
6//! - Triangle vertex indices reference valid vertices
7//! - Triangles are not degenerate (all three vertices must be distinct)
8//! - Build items reference existing objects
9//! - Material, color group, and base material references are valid
10
11mod beam_lattice;
12mod boolean_ops;
13mod core;
14mod displacement;
15mod material;
16mod production;
17mod slice;
18mod volumetric;
19
20// Re-export public API functions
21pub use beam_lattice::validate_beam_lattice;
22pub use boolean_ops::validate_boolean_operations;
23pub use core::{
24    detect_circular_components, validate_build_references, validate_component_properties,
25    validate_component_references, validate_mesh_geometry, validate_mesh_manifold,
26};
27pub use displacement::validate_displacement_extension;
28pub use material::{
29    get_property_resource_size, validate_color_formats, validate_duplicate_resource_ids,
30    validate_material_references, validate_multiproperties_references,
31    validate_object_triangle_materials, validate_resource_ordering, validate_texture_paths,
32    validate_triangle_properties,
33};
34pub use production::{
35    validate_component_chain, validate_duplicate_uuids, validate_production_extension,
36    validate_production_extension_with_config, validate_production_paths,
37    validate_production_uuids_required, validate_uuid_formats,
38};
39pub use slice::{
40    validate_planar_transform, validate_slice, validate_slice_extension, validate_slices,
41};
42pub use volumetric::validate_volumetric_extension;
43
44// Re-import internal functions from submodules for use within this module
45use core::{
46    sorted_ids_from_set, validate_dtd_declaration, validate_mesh_volume, validate_object_ids,
47    validate_required_extensions, validate_required_structure, validate_thumbnail_format,
48    validate_thumbnail_jpeg_colorspace, validate_transform_matrices, validate_vertex_order,
49};
50
51use crate::error::{Error, Result};
52use crate::model::{Model, ParserConfig};
53
54/// Validate a parsed 3MF model
55///
56/// Performs comprehensive validation of the model structure, including:
57/// - Required model structure (objects and build items)
58/// - Object ID uniqueness
59/// - Triangle vertex bounds and degeneracy checks
60/// - Build item object references
61/// - Material, color group, and base material references
62/// - Component references and circular dependency detection
63/// - Mesh requirements (must have vertices)
64///
65/// Note: This function uses a default parser config for backward compatibility.
66/// For more control, use `validate_model_with_config`.
67#[allow(dead_code)] // Public API but may not be used internally
68pub fn validate_model(model: &Model) -> Result<()> {
69    // Delegate to validate_model_with_config with default config
70    validate_model_with_config(model, &ParserConfig::with_all_extensions())
71}
72
73/// Validate a parsed 3MF model with custom extension validation
74///
75/// This function performs the same validation as `validate_model` and additionally
76/// invokes any custom validation handlers registered in the parser configuration.
77#[allow(dead_code)] // Currently called during parsing; may be exposed publicly in future
78pub fn validate_model_with_config(model: &Model, config: &ParserConfig) -> Result<()> {
79    // Core validation (always required regardless of extensions)
80    validate_required_structure(model)?;
81    validate_object_ids(model)?;
82    validate_mesh_geometry(model)?;
83    validate_build_references(model)?;
84    validate_required_extensions(model)?;
85    validate_component_references(model)?;
86
87    // Production extension validation with config support for backward compatibility
88    // This is kept separate because it checks config.supports() for lenient validation
89    validate_production_extension_with_config(model, config)?;
90
91    // Extension registry validation (unified approach)
92    // This calls validate() on all registered extension handlers, which now handle:
93    // - Material extension: material references, texture paths, multiproperties,
94    //   triangle properties, color formats, resource ordering, duplicate resource IDs
95    // - Production extension: production extension, production paths, UUID formats,
96    //   production UUIDs required, duplicate UUIDs, component chain
97    // - Boolean operations extension
98    // - Displacement extension
99    // - Slice extension
100    // - Beam lattice extension
101    config.registry().validate_all(model)?;
102
103    // Custom extension validation (legacy pattern - deprecated)
104    // NOTE: This callback-based pattern is deprecated in favor of the ExtensionRegistry system.
105    // New code should use ExtensionHandler trait and register handlers via config.registry().
106    // This is kept for backward compatibility and will be removed in a future major version.
107    for ext_info in config.custom_extensions().values() {
108        if let Some(validator) = &ext_info.validation_handler {
109            validator(model)
110                .map_err(|e| Error::InvalidModel(format!("Custom validation failed: {}", e)))?;
111        }
112    }
113
114    // Additional core validations (not extension-specific)
115    validate_transform_matrices(model)?;
116    validate_thumbnail_format(model)?;
117    validate_mesh_volume(model)?;
118    validate_vertex_order(model)?;
119    validate_thumbnail_jpeg_colorspace(model)?;
120    validate_dtd_declaration(model)?;
121    validate_component_properties(model)?;
122
123    Ok(())
124}
125
126#[cfg(test)]
127mod tests {
128    use crate::model::{BuildItem, Mesh, Model, Multi, MultiProperties, Object, Triangle, Vertex};
129    use crate::validator::*;
130
131    #[test]
132    fn test_validate_duplicate_object_ids() {
133        let mut model = Model::new();
134        model.resources.objects.push(Object::new(1));
135        model.resources.objects.push(Object::new(1)); // Duplicate!
136
137        let result = validate_object_ids(&model);
138        assert!(result.is_err());
139        assert!(
140            result
141                .unwrap_err()
142                .to_string()
143                .contains("Duplicate object ID")
144        );
145    }
146
147    #[test]
148    fn test_validate_zero_object_id() {
149        let mut model = Model::new();
150        model.resources.objects.push(Object::new(0)); // Invalid!
151
152        let result = validate_object_ids(&model);
153        assert!(result.is_err());
154        assert!(result.unwrap_err().to_string().contains("positive integer"));
155    }
156
157    #[test]
158    fn test_validate_degenerate_triangle() {
159        let mut model = Model::new();
160        let mut object = Object::new(1);
161        let mut mesh = Mesh::new();
162
163        // Add vertices
164        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
165        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
166        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
167
168        // Add degenerate triangle (v1 == v2)
169        mesh.triangles.push(Triangle::new(0, 0, 2));
170
171        object.mesh = Some(mesh);
172        model.resources.objects.push(object);
173
174        let result = validate_mesh_geometry(&model);
175        assert!(result.is_err());
176        assert!(result.unwrap_err().to_string().contains("degenerate"));
177    }
178
179    #[test]
180    fn test_validate_vertex_out_of_bounds() {
181        let mut model = Model::new();
182        let mut object = Object::new(1);
183        let mut mesh = Mesh::new();
184
185        // Add only 2 vertices
186        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
187        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
188
189        // Add triangle with out-of-bounds vertex (v3 = 5, but only have 2 vertices)
190        mesh.triangles.push(Triangle::new(0, 1, 5));
191
192        object.mesh = Some(mesh);
193        model.resources.objects.push(object);
194
195        let result = validate_mesh_geometry(&model);
196        assert!(result.is_err());
197        assert!(result.unwrap_err().to_string().contains("out of bounds"));
198    }
199
200    #[test]
201    fn test_validate_build_item_invalid_reference() {
202        let mut model = Model::new();
203        model.resources.objects.push(Object::new(1));
204
205        // Build item references non-existent object 99
206        model.build.items.push(BuildItem::new(99));
207
208        let result = validate_build_references(&model);
209        assert!(result.is_err());
210        assert!(
211            result
212                .unwrap_err()
213                .to_string()
214                .contains("non-existent object")
215        );
216    }
217
218    #[test]
219    fn test_validate_valid_model() {
220        let mut model = Model::new();
221        let mut object = Object::new(1);
222        let mut mesh = Mesh::new();
223
224        // Add valid vertices
225        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
226        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
227        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
228
229        // Add valid triangle
230        mesh.triangles.push(Triangle::new(0, 1, 2));
231
232        object.mesh = Some(mesh);
233        model.resources.objects.push(object);
234
235        // Add valid build item
236        model.build.items.push(BuildItem::new(1));
237
238        let result = validate_model(&model);
239        assert!(result.is_ok());
240    }
241
242    #[test]
243    fn test_validate_empty_mesh() {
244        let mut model = Model::new();
245        let mut object = Object::new(1);
246        let mut mesh = Mesh::new();
247
248        // Add triangles but no vertices - this should fail
249        mesh.triangles.push(Triangle::new(0, 1, 2));
250
251        object.mesh = Some(mesh);
252        model.resources.objects.push(object);
253
254        let result = validate_mesh_geometry(&model);
255        assert!(result.is_err());
256        let err_msg = result.unwrap_err().to_string();
257        assert!(
258            err_msg.contains("triangle"),
259            "Error message should mention triangles"
260        );
261        assert!(
262            err_msg.contains("no vertices"),
263            "Error message should mention missing vertices"
264        );
265    }
266
267    #[test]
268    fn test_validate_base_material_reference() {
269        use crate::model::{BaseMaterial, BaseMaterialGroup};
270
271        let mut model = Model::new();
272
273        // Add a base material group with id=5
274        let mut base_group = BaseMaterialGroup::new(5);
275        base_group
276            .materials
277            .push(BaseMaterial::new("Red".to_string(), (255, 0, 0, 255)));
278        base_group
279            .materials
280            .push(BaseMaterial::new("Blue".to_string(), (0, 0, 255, 255)));
281        model.resources.base_material_groups.push(base_group);
282
283        // Create an object that references the base material group
284        let mut object = Object::new(1);
285        object.pid = Some(5);
286        object.pindex = Some(0);
287
288        let mut mesh = Mesh::new();
289        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
290        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
291        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
292        mesh.triangles.push(Triangle::new(0, 1, 2));
293
294        object.mesh = Some(mesh);
295        model.resources.objects.push(object);
296        model.build.items.push(BuildItem::new(1));
297
298        // Should pass validation
299        let result = validate_model(&model);
300        assert!(result.is_ok());
301    }
302
303    #[test]
304    fn test_validate_invalid_base_material_reference() {
305        use crate::model::{BaseMaterial, BaseMaterialGroup};
306
307        let mut model = Model::new();
308
309        // Add a base material group with id=5
310        let mut base_group = BaseMaterialGroup::new(5);
311        base_group
312            .materials
313            .push(BaseMaterial::new("Red".to_string(), (255, 0, 0, 255)));
314        model.resources.base_material_groups.push(base_group);
315
316        // Create an object that references a non-existent material group id=99
317        let mut object = Object::new(1);
318        object.pid = Some(99); // Invalid reference!
319
320        let mut mesh = Mesh::new();
321        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
322        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
323        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
324        mesh.triangles.push(Triangle::new(0, 1, 2));
325
326        object.mesh = Some(mesh);
327        model.resources.objects.push(object);
328        model.build.items.push(BuildItem::new(1));
329
330        // Should fail validation
331        let result = validate_material_references(&model);
332        assert!(result.is_err());
333        assert!(result.unwrap_err().to_string().contains("non-existent"));
334    }
335
336    #[test]
337    fn test_validate_base_material_pindex_out_of_bounds() {
338        use crate::model::{BaseMaterial, BaseMaterialGroup};
339
340        let mut model = Model::new();
341
342        // Add a base material group with only 1 material
343        let mut base_group = BaseMaterialGroup::new(5);
344        base_group
345            .materials
346            .push(BaseMaterial::new("Red".to_string(), (255, 0, 0, 255)));
347        model.resources.base_material_groups.push(base_group);
348
349        // Create an object with pindex=5 (out of bounds)
350        let mut object = Object::new(1);
351        object.pid = Some(5);
352        object.pindex = Some(5); // Out of bounds! Only index 0 is valid
353
354        let mut mesh = Mesh::new();
355        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
356        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
357        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
358        mesh.triangles.push(Triangle::new(0, 1, 2));
359
360        object.mesh = Some(mesh);
361        model.resources.objects.push(object);
362        model.build.items.push(BuildItem::new(1));
363
364        // Should fail validation
365        let result = validate_material_references(&model);
366        assert!(result.is_err());
367        assert!(result.unwrap_err().to_string().contains("out of bounds"));
368    }
369
370    #[test]
371    fn test_validate_basematerialid_valid() {
372        use crate::model::{BaseMaterial, BaseMaterialGroup};
373
374        let mut model = Model::new();
375
376        // Add a base material group with ID 5
377        let mut base_group = BaseMaterialGroup::new(5);
378        base_group.materials.push(BaseMaterial::new(
379            "Red Plastic".to_string(),
380            (255, 0, 0, 255),
381        ));
382        model.resources.base_material_groups.push(base_group);
383
384        // Create an object that references the base material group via basematerialid
385        let mut object = Object::new(1);
386        object.basematerialid = Some(5); // Valid reference
387
388        let mut mesh = Mesh::new();
389        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
390        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
391        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
392        mesh.triangles.push(Triangle::new(0, 1, 2));
393
394        object.mesh = Some(mesh);
395        model.resources.objects.push(object);
396        model.build.items.push(BuildItem::new(1));
397
398        // Should pass validation
399        let result = validate_material_references(&model);
400        assert!(result.is_ok());
401    }
402
403    #[test]
404    fn test_validate_basematerialid_invalid() {
405        use crate::model::{BaseMaterial, BaseMaterialGroup};
406
407        let mut model = Model::new();
408
409        // Add a base material group with ID 5
410        let mut base_group = BaseMaterialGroup::new(5);
411        base_group.materials.push(BaseMaterial::new(
412            "Red Plastic".to_string(),
413            (255, 0, 0, 255),
414        ));
415        model.resources.base_material_groups.push(base_group);
416
417        // Create an object that references a non-existent base material group
418        let mut object = Object::new(1);
419        object.basematerialid = Some(99); // Invalid reference!
420
421        let mut mesh = Mesh::new();
422        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
423        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
424        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
425        mesh.triangles.push(Triangle::new(0, 1, 2));
426
427        object.mesh = Some(mesh);
428        model.resources.objects.push(object);
429        model.build.items.push(BuildItem::new(1));
430
431        // Should fail validation
432        let result = validate_material_references(&model);
433        assert!(result.is_err());
434        assert!(
435            result
436                .unwrap_err()
437                .to_string()
438                .contains("non-existent base material group")
439        );
440    }
441
442    #[test]
443    fn test_validate_component_reference_invalid() {
444        use crate::model::Component;
445
446        let mut model = Model::new();
447
448        // Create object 1 with a component referencing non-existent object 99
449        let mut object1 = Object::new(1);
450        object1.components.push(Component::new(99));
451
452        let mut mesh = Mesh::new();
453        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
454        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
455        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
456        mesh.triangles.push(Triangle::new(0, 1, 2));
457        object1.mesh = Some(mesh);
458
459        model.resources.objects.push(object1);
460        model.build.items.push(BuildItem::new(1));
461
462        // Should fail validation
463        let result = validate_component_references(&model);
464        assert!(result.is_err());
465        assert!(
466            result
467                .unwrap_err()
468                .to_string()
469                .contains("non-existent object")
470        );
471    }
472
473    #[test]
474    fn test_validate_component_circular_dependency() {
475        use crate::model::Component;
476
477        let mut model = Model::new();
478
479        // Create object 1 with component referencing object 2
480        let mut object1 = Object::new(1);
481        object1.components.push(Component::new(2));
482
483        let mut mesh1 = Mesh::new();
484        mesh1.vertices.push(Vertex::new(0.0, 0.0, 0.0));
485        mesh1.vertices.push(Vertex::new(1.0, 0.0, 0.0));
486        mesh1.vertices.push(Vertex::new(0.5, 1.0, 0.0));
487        mesh1.triangles.push(Triangle::new(0, 1, 2));
488        object1.mesh = Some(mesh1);
489
490        // Create object 2 with component referencing object 1 (circular!)
491        let mut object2 = Object::new(2);
492        object2.components.push(Component::new(1));
493
494        let mut mesh2 = Mesh::new();
495        mesh2.vertices.push(Vertex::new(0.0, 0.0, 0.0));
496        mesh2.vertices.push(Vertex::new(1.0, 0.0, 0.0));
497        mesh2.vertices.push(Vertex::new(0.5, 1.0, 0.0));
498        mesh2.triangles.push(Triangle::new(0, 1, 2));
499        object2.mesh = Some(mesh2);
500
501        model.resources.objects.push(object1);
502        model.resources.objects.push(object2);
503        model.build.items.push(BuildItem::new(1));
504
505        // Should fail validation
506        let result = validate_component_references(&model);
507        assert!(result.is_err());
508        assert!(
509            result
510                .unwrap_err()
511                .to_string()
512                .contains("Circular component reference")
513        );
514    }
515
516    #[test]
517    fn test_validate_component_self_reference() {
518        use crate::model::Component;
519
520        let mut model = Model::new();
521
522        // Create object 1 with component referencing itself
523        let mut object1 = Object::new(1);
524        object1.components.push(Component::new(1));
525
526        let mut mesh = Mesh::new();
527        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
528        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
529        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
530        mesh.triangles.push(Triangle::new(0, 1, 2));
531        object1.mesh = Some(mesh);
532
533        model.resources.objects.push(object1);
534        model.build.items.push(BuildItem::new(1));
535
536        // Should fail validation (self-reference is a circular dependency)
537        let result = validate_component_references(&model);
538        assert!(result.is_err());
539        assert!(
540            result
541                .unwrap_err()
542                .to_string()
543                .contains("Circular component reference")
544        );
545    }
546
547    #[test]
548    fn test_validate_component_valid() {
549        use crate::model::Component;
550
551        let mut model = Model::new();
552
553        // Create base object 2 (no components)
554        let mut object2 = Object::new(2);
555        let mut mesh2 = Mesh::new();
556        mesh2.vertices.push(Vertex::new(0.0, 0.0, 0.0));
557        mesh2.vertices.push(Vertex::new(1.0, 0.0, 0.0));
558        mesh2.vertices.push(Vertex::new(0.5, 1.0, 0.0));
559        mesh2.triangles.push(Triangle::new(0, 1, 2));
560        object2.mesh = Some(mesh2);
561
562        // Create object 1 with component referencing object 2
563        let mut object1 = Object::new(1);
564        object1.components.push(Component::new(2));
565
566        let mut mesh1 = Mesh::new();
567        mesh1.vertices.push(Vertex::new(0.0, 0.0, 0.0));
568        mesh1.vertices.push(Vertex::new(1.0, 0.0, 0.0));
569        mesh1.vertices.push(Vertex::new(0.5, 1.0, 0.0));
570        mesh1.triangles.push(Triangle::new(0, 1, 2));
571        object1.mesh = Some(mesh1);
572
573        model.resources.objects.push(object1);
574        model.resources.objects.push(object2);
575        model.build.items.push(BuildItem::new(1));
576
577        // Should pass validation
578        let result = validate_component_references(&model);
579        assert!(result.is_ok());
580    }
581
582    #[test]
583    fn test_validate_multiproperties_reference() {
584        use crate::model::{Multi, MultiProperties};
585
586        let mut model = Model::new();
587
588        // Add a multiproperties group with ID 12
589        let mut multi_props = MultiProperties {
590            id: 12,
591            pids: vec![6, 9],
592            blendmethods: vec![],
593            multis: vec![],
594            parse_order: 0,
595        };
596        multi_props.multis.push(Multi {
597            pindices: vec![0, 0],
598        });
599        model.resources.multi_properties.push(multi_props);
600
601        // Create an object that references the multiproperties group
602        let mut object = Object::new(1);
603        object.pid = Some(12); // Should reference the multiproperties group
604        object.pindex = Some(0);
605
606        let mut mesh = Mesh::new();
607        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
608        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
609        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
610        mesh.triangles.push(Triangle::new(0, 1, 2));
611
612        object.mesh = Some(mesh);
613        model.resources.objects.push(object);
614        model.build.items.push(BuildItem::new(1));
615
616        // Should pass validation (multiproperties is a valid property group)
617        let result = validate_material_references(&model);
618        assert!(result.is_ok());
619    }
620
621    #[test]
622    fn test_validate_texture2d_group_reference() {
623        use crate::model::{Tex2Coord, Texture2DGroup};
624
625        let mut model = Model::new();
626
627        // Add a texture2d group with ID 9
628        let mut tex_group = Texture2DGroup::new(9, 4);
629        tex_group.tex2coords.push(Tex2Coord { u: 0.0, v: 0.0 });
630        tex_group.tex2coords.push(Tex2Coord { u: 1.0, v: 1.0 });
631        model.resources.texture2d_groups.push(tex_group);
632
633        // Create an object that references the texture2d group
634        let mut object = Object::new(1);
635        object.pid = Some(9); // Should reference the texture2d group
636
637        let mut mesh = Mesh::new();
638        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
639        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
640        mesh.vertices.push(Vertex::new(0.5, 1.0, 0.0));
641        mesh.triangles.push(Triangle::new(0, 1, 2));
642
643        object.mesh = Some(mesh);
644        model.resources.objects.push(object);
645        model.build.items.push(BuildItem::new(1));
646
647        // Should pass validation (texture2d group is a valid property group)
648        let result = validate_material_references(&model);
649        assert!(result.is_ok());
650    }
651
652    #[test]
653    #[cfg(feature = "mesh-ops")]
654    fn test_sliced_object_allows_negative_volume_mesh() {
655        use crate::model::SliceStack;
656
657        let mut model = Model::new();
658
659        // Add a slicestack
660        let slice_stack = SliceStack::new(1, 0.0);
661        model.resources.slice_stacks.push(slice_stack);
662
663        // Create an object with negative volume (inverted mesh) but with slicestackid
664        let mut object = Object::new(1);
665        object.slicestackid = Some(1); // References the slicestack
666
667        // Create a mesh with inverted triangles (negative volume)
668        // This is a simple inverted tetrahedron
669        let mut mesh = Mesh::new();
670        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
671        mesh.vertices.push(Vertex::new(10.0, 0.0, 0.0));
672        mesh.vertices.push(Vertex::new(5.0, 10.0, 0.0));
673        mesh.vertices.push(Vertex::new(5.0, 5.0, 10.0));
674
675        // Deliberately inverted winding order to create negative volume
676        mesh.triangles.push(Triangle::new(0, 2, 1)); // Inverted
677        mesh.triangles.push(Triangle::new(0, 3, 2)); // Inverted
678        mesh.triangles.push(Triangle::new(0, 1, 3)); // Inverted
679        mesh.triangles.push(Triangle::new(1, 2, 3)); // Inverted
680
681        object.mesh = Some(mesh);
682        model.resources.objects.push(object);
683        model.build.items.push(BuildItem::new(1));
684
685        // Should pass validation because object has slicestackid
686        let result = validate_mesh_volume(&model);
687        assert!(
688            result.is_ok(),
689            "Sliced object should allow negative volume mesh"
690        );
691    }
692
693    #[test]
694    #[cfg(feature = "mesh-ops")]
695    fn test_non_sliced_object_rejects_negative_volume() {
696        let mut model = Model::new();
697
698        // Create an object WITHOUT slicestackid
699        let mut object = Object::new(1);
700
701        // Create a box mesh with ALL triangles in inverted winding order
702        // Based on the standard box from test_files/core/box.3mf but with reversed winding
703        let mut mesh = Mesh::new();
704        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); // 0
705        mesh.vertices.push(Vertex::new(10.0, 0.0, 0.0)); // 1
706        mesh.vertices.push(Vertex::new(10.0, 20.0, 0.0)); // 2
707        mesh.vertices.push(Vertex::new(0.0, 20.0, 0.0)); // 3
708        mesh.vertices.push(Vertex::new(0.0, 0.0, 30.0)); // 4
709        mesh.vertices.push(Vertex::new(10.0, 0.0, 30.0)); // 5
710        mesh.vertices.push(Vertex::new(10.0, 20.0, 30.0)); // 6
711        mesh.vertices.push(Vertex::new(0.0, 20.0, 30.0)); // 7
712
713        // Correct winding from box.3mf:
714        // <triangle v1="3" v2="2" v3="1" />
715        // For negative volume, swap the first and third vertex indices: v1="1" v2="2" v3="3"
716        // All triangles with INVERTED winding (first and third indices swapped)
717        mesh.triangles.push(Triangle::new(1, 2, 3)); // Was (3, 2, 1)
718        mesh.triangles.push(Triangle::new(3, 0, 1)); // Was (1, 0, 3)
719        mesh.triangles.push(Triangle::new(6, 5, 4)); // Was (4, 5, 6)
720        mesh.triangles.push(Triangle::new(4, 7, 6)); // Was (6, 7, 4)
721        mesh.triangles.push(Triangle::new(5, 1, 0)); // Was (0, 1, 5)
722        mesh.triangles.push(Triangle::new(0, 4, 5)); // Was (5, 4, 0)
723        mesh.triangles.push(Triangle::new(6, 2, 1)); // Was (1, 2, 6)
724        mesh.triangles.push(Triangle::new(1, 5, 6)); // Was (6, 5, 1)
725        mesh.triangles.push(Triangle::new(7, 3, 2)); // Was (2, 3, 7)
726        mesh.triangles.push(Triangle::new(2, 6, 7)); // Was (7, 6, 2)
727        mesh.triangles.push(Triangle::new(4, 0, 3)); // Was (3, 0, 4)
728        mesh.triangles.push(Triangle::new(3, 7, 4)); // Was (4, 7, 3)
729
730        object.mesh = Some(mesh);
731        model.resources.objects.push(object);
732        model.build.items.push(BuildItem::new(1));
733
734        // Should fail validation for non-sliced object
735        let result = validate_mesh_volume(&model);
736        assert!(
737            result.is_err(),
738            "Non-sliced object should reject negative volume mesh"
739        );
740        assert!(result.unwrap_err().to_string().contains("negative volume"));
741    }
742
743    #[test]
744    fn test_sliced_object_allows_mirror_transform() {
745        use crate::model::SliceStack;
746
747        let mut model = Model::new();
748
749        // Add a slicestack
750        let slice_stack = SliceStack::new(1, 0.0);
751        model.resources.slice_stacks.push(slice_stack);
752
753        // Create an object with slicestackid
754        let mut object = Object::new(1);
755        object.slicestackid = Some(1);
756
757        let mut mesh = Mesh::new();
758        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
759        mesh.vertices.push(Vertex::new(10.0, 0.0, 0.0));
760        mesh.vertices.push(Vertex::new(5.0, 10.0, 0.0));
761        mesh.triangles.push(Triangle::new(0, 1, 2));
762
763        object.mesh = Some(mesh);
764        model.resources.objects.push(object);
765
766        // Add build item with mirror transformation (negative determinant)
767        // Transform with -1 scale in X axis (mirror): [-1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, 0]
768        let mut item = BuildItem::new(1);
769        item.transform = Some([-1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
770        model.build.items.push(item);
771
772        // Should pass validation because object has slicestackid
773        let result = validate_transform_matrices(&model);
774        assert!(
775            result.is_ok(),
776            "Sliced object should allow mirror transformation"
777        );
778    }
779
780    #[test]
781    fn test_non_sliced_object_rejects_mirror_transform() {
782        let mut model = Model::new();
783
784        // Create an object WITHOUT slicestackid
785        let mut object = Object::new(1);
786
787        let mut mesh = Mesh::new();
788        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
789        mesh.vertices.push(Vertex::new(10.0, 0.0, 0.0));
790        mesh.vertices.push(Vertex::new(5.0, 10.0, 0.0));
791        mesh.triangles.push(Triangle::new(0, 1, 2));
792
793        object.mesh = Some(mesh);
794        model.resources.objects.push(object);
795
796        // Add build item with mirror transformation (negative determinant)
797        let mut item = BuildItem::new(1);
798        item.transform = Some([-1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0, 1.0, 0.0, 0.0, 0.0]);
799        model.build.items.push(item);
800
801        // Should fail validation for non-sliced object
802        let result = validate_transform_matrices(&model);
803        assert!(
804            result.is_err(),
805            "Non-sliced object should reject mirror transformation"
806        );
807        assert!(
808            result
809                .unwrap_err()
810                .to_string()
811                .contains("negative determinant")
812        );
813    }
814
815    #[test]
816    fn test_multiproperties_duplicate_colorgroup() {
817        use crate::model::ColorGroup;
818
819        let mut model = Model::new();
820
821        // Add a colorgroup
822        let mut color_group = ColorGroup::new(10);
823        color_group.parse_order = 1;
824        color_group.colors.push((255, 0, 0, 255)); // Red
825        model.resources.color_groups.push(color_group);
826
827        // Add multiproperties that references the same colorgroup twice
828        let mut multi = MultiProperties::new(20, vec![10, 10]); // Duplicate reference to colorgroup 10
829        multi.parse_order = 2;
830        multi.multis.push(Multi::new(vec![0, 0]));
831        model.resources.multi_properties.push(multi);
832
833        // Add object and build item for basic structure
834        let mut object = Object::new(1);
835        let mut mesh = Mesh::new();
836        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
837        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
838        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
839        mesh.triangles.push(Triangle::new(0, 1, 2));
840        object.mesh = Some(mesh);
841        model.resources.objects.push(object);
842        model.build.items.push(BuildItem::new(1));
843
844        // Should fail validation (N_XXM_0604_01)
845        let result = validate_multiproperties_references(&model);
846        assert!(
847            result.is_err(),
848            "Should reject multiproperties with duplicate colorgroup references"
849        );
850        let error_msg = result.unwrap_err().to_string();
851        assert!(error_msg.contains("colorgroup"));
852        assert!(error_msg.contains("multiple times"));
853    }
854
855    #[test]
856    fn test_multiproperties_basematerials_at_layer_2() {
857        use crate::model::{BaseMaterial, BaseMaterialGroup, ColorGroup};
858
859        let mut model = Model::new();
860
861        // Add a basematerials group
862        let mut base_mat = BaseMaterialGroup::new(5);
863        base_mat.parse_order = 1;
864        base_mat
865            .materials
866            .push(BaseMaterial::new("Steel".to_string(), (128, 128, 128, 255)));
867        model.resources.base_material_groups.push(base_mat);
868
869        // Add colorgroups for layers 0 and 1
870        let mut cg1 = ColorGroup::new(6);
871        cg1.parse_order = 2;
872        cg1.colors.push((255, 0, 0, 255));
873        model.resources.color_groups.push(cg1);
874
875        let mut cg2 = ColorGroup::new(7);
876        cg2.parse_order = 3;
877        cg2.colors.push((0, 255, 0, 255));
878        model.resources.color_groups.push(cg2);
879
880        // Add multiproperties with basematerials at layer 2 (index 2) - INVALID
881        let mut multi = MultiProperties::new(20, vec![6, 7, 5]); // basematerials at index 2
882        multi.parse_order = 4;
883        multi.multis.push(Multi::new(vec![0, 0, 0]));
884        model.resources.multi_properties.push(multi);
885
886        // Add object and build item for basic structure
887        let mut object = Object::new(1);
888        let mut mesh = Mesh::new();
889        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
890        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
891        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
892        mesh.triangles.push(Triangle::new(0, 1, 2));
893        object.mesh = Some(mesh);
894        model.resources.objects.push(object);
895        model.build.items.push(BuildItem::new(1));
896
897        // Should fail validation (N_XXM_0604_03)
898        let result = validate_multiproperties_references(&model);
899        assert!(result.is_err(), "Should reject basematerials at layer >= 2");
900        let error_msg = result.unwrap_err().to_string();
901        assert!(error_msg.contains("basematerials"));
902        assert!(error_msg.contains("layer"));
903    }
904
905    #[test]
906    fn test_multiproperties_basematerials_at_layer_1() {
907        // Test case N_XXM_0604_03: basematerials at layer 1 (index 1) should be rejected
908        // Per spec, basematerials MUST be at layer 0 if included
909        use crate::model::{BaseMaterial, BaseMaterialGroup, ColorGroup};
910
911        let mut model = Model::new();
912
913        // Add a colorgroup for layer 0
914        let mut cg = ColorGroup::new(6);
915        cg.parse_order = 1;
916        cg.colors.push((255, 0, 0, 255));
917        model.resources.color_groups.push(cg);
918
919        // Add a basematerials group
920        let mut base_mat = BaseMaterialGroup::new(1);
921        base_mat.parse_order = 2;
922        base_mat
923            .materials
924            .push(BaseMaterial::new("Steel".to_string(), (128, 128, 128, 255)));
925        model.resources.base_material_groups.push(base_mat);
926
927        // Add multiproperties with basematerials at layer 1 (index 1) - INVALID
928        let mut multi = MultiProperties::new(12, vec![6, 1]); // colorgroup at 0, basematerials at 1
929        multi.parse_order = 3;
930        multi.multis.push(Multi::new(vec![0, 0]));
931        model.resources.multi_properties.push(multi);
932
933        // Add object and build item for basic structure
934        let mut object = Object::new(1);
935        let mut mesh = Mesh::new();
936        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
937        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
938        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
939        mesh.triangles.push(Triangle::new(0, 1, 2));
940        object.mesh = Some(mesh);
941        model.resources.objects.push(object);
942        model.build.items.push(BuildItem::new(1));
943
944        // Should fail validation (N_XXM_0604_03)
945        let result = validate_multiproperties_references(&model);
946        assert!(
947            result.is_err(),
948            "Should reject basematerials at layer 1 (must be at layer 0)"
949        );
950        let error_msg = result.unwrap_err().to_string();
951        assert!(error_msg.contains("basematerials"));
952        assert!(error_msg.contains("layer 1"));
953        assert!(error_msg.contains("first element"));
954    }
955
956    #[test]
957    fn test_multiproperties_two_different_colorgroups() {
958        // Test case N_XXM_0604_01: multiple different colorgroups should be rejected
959        // Per spec, pids list MUST NOT contain more than one reference to a colorgroup
960        use crate::model::ColorGroup;
961
962        let mut model = Model::new();
963
964        // Add two different colorgroups
965        let mut cg1 = ColorGroup::new(5);
966        cg1.parse_order = 1;
967        cg1.colors.push((255, 0, 0, 255));
968        model.resources.color_groups.push(cg1);
969
970        let mut cg2 = ColorGroup::new(6);
971        cg2.parse_order = 2;
972        cg2.colors.push((0, 255, 0, 255));
973        model.resources.color_groups.push(cg2);
974
975        // Add multiproperties that references both colorgroups
976        let mut multi = MultiProperties::new(12, vec![5, 6]); // Two different colorgroups
977        multi.parse_order = 3;
978        multi.multis.push(Multi::new(vec![0, 0]));
979        model.resources.multi_properties.push(multi);
980
981        // Add object and build item for basic structure
982        let mut object = Object::new(1);
983        let mut mesh = Mesh::new();
984        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
985        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
986        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
987        mesh.triangles.push(Triangle::new(0, 1, 2));
988        object.mesh = Some(mesh);
989        model.resources.objects.push(object);
990        model.build.items.push(BuildItem::new(1));
991
992        // Should fail validation (N_XXM_0604_01)
993        let result = validate_multiproperties_references(&model);
994        assert!(
995            result.is_err(),
996            "Should reject multiproperties with multiple different colorgroups"
997        );
998        let error_msg = result.unwrap_err().to_string();
999        assert!(error_msg.contains("multiple colorgroups"));
1000        assert!(error_msg.contains("[5, 6]") || error_msg.contains("[6, 5]"));
1001    }
1002
1003    #[test]
1004    fn test_triangle_material_without_object_default() {
1005        let mut model = Model::new();
1006
1007        // Create object WITHOUT default material (no pid)
1008        let mut object = Object::new(1);
1009        let mut mesh = Mesh::new();
1010        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
1011        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
1012        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
1013
1014        // Triangle with material property but object has no default
1015        let mut triangle = Triangle::new(0, 1, 2);
1016        triangle.p1 = Some(0); // Triangle has material property
1017        mesh.triangles.push(triangle);
1018
1019        object.mesh = Some(mesh);
1020        model.resources.objects.push(object);
1021        model.build.items.push(BuildItem::new(1));
1022
1023        // Should fail validation (N_XXM_0601_01)
1024        let result = validate_triangle_properties(&model);
1025        assert!(
1026            result.is_err(),
1027            "Should reject triangle with per-vertex properties when neither triangle nor object has pid"
1028        );
1029        let error_msg = result.unwrap_err().to_string();
1030        assert!(error_msg.contains("per-vertex material properties"));
1031    }
1032
1033    #[test]
1034    fn test_forward_reference_texture2dgroup_to_texture2d() {
1035        use crate::model::{Texture2D, Texture2DGroup};
1036
1037        let mut model = Model::new();
1038
1039        // Add texture2dgroup BEFORE texture2d (forward reference)
1040        let mut tex_group = Texture2DGroup::new(10, 20);
1041        tex_group.parse_order = 1; // Earlier in parse order
1042        model.resources.texture2d_groups.push(tex_group);
1043
1044        // Add texture2d AFTER texture2dgroup
1045        let mut texture = Texture2D::new(
1046            20,
1047            "/3D/Texture/image.png".to_string(),
1048            "image/png".to_string(),
1049        );
1050        texture.parse_order = 2; // Later in parse order
1051        model.resources.texture2d_resources.push(texture);
1052
1053        // Add object and build item for basic structure
1054        let mut object = Object::new(1);
1055        let mut mesh = Mesh::new();
1056        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
1057        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
1058        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
1059        mesh.triangles.push(Triangle::new(0, 1, 2));
1060        object.mesh = Some(mesh);
1061        model.resources.objects.push(object);
1062        model.build.items.push(BuildItem::new(1));
1063
1064        // Should fail validation (N_XXM_0606_01)
1065        let result = validate_resource_ordering(&model);
1066        assert!(
1067            result.is_err(),
1068            "Should reject forward reference from texture2dgroup to texture2d"
1069        );
1070        let error_msg = result.unwrap_err().to_string();
1071        assert!(error_msg.contains("Forward reference"));
1072        assert!(error_msg.contains("texture2d"));
1073    }
1074
1075    #[test]
1076    fn test_forward_reference_multiproperties_to_colorgroup() {
1077        use crate::model::ColorGroup;
1078
1079        let mut model = Model::new();
1080
1081        // Add multiproperties BEFORE colorgroup (forward reference)
1082        let mut multi = MultiProperties::new(10, vec![20]);
1083        multi.parse_order = 1; // Earlier in parse order
1084        multi.multis.push(Multi::new(vec![0]));
1085        model.resources.multi_properties.push(multi);
1086
1087        // Add colorgroup AFTER multiproperties
1088        let mut color_group = ColorGroup::new(20);
1089        color_group.parse_order = 2; // Later in parse order
1090        color_group.colors.push((255, 0, 0, 255));
1091        model.resources.color_groups.push(color_group);
1092
1093        // Add object and build item for basic structure
1094        let mut object = Object::new(1);
1095        let mut mesh = Mesh::new();
1096        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
1097        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
1098        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
1099        mesh.triangles.push(Triangle::new(0, 1, 2));
1100        object.mesh = Some(mesh);
1101        model.resources.objects.push(object);
1102        model.build.items.push(BuildItem::new(1));
1103
1104        // Should fail validation (N_XXM_0606_03)
1105        let result = validate_resource_ordering(&model);
1106        assert!(
1107            result.is_err(),
1108            "Should reject forward reference from multiproperties to colorgroup"
1109        );
1110        let error_msg = result.unwrap_err().to_string();
1111        assert!(error_msg.contains("Forward reference"));
1112        assert!(error_msg.contains("colorgroup"));
1113    }
1114
1115    #[test]
1116    fn test_texture_path_with_backslash() {
1117        use crate::model::Texture2D;
1118
1119        let mut model = Model::new();
1120
1121        // Add texture with backslash in path (invalid per OPC spec)
1122        let mut texture = Texture2D::new(
1123            10,
1124            "/3D\\Texture\\image.png".to_string(),
1125            "image/png".to_string(),
1126        );
1127        texture.parse_order = 1;
1128        model.resources.texture2d_resources.push(texture);
1129
1130        // Add object and build item for basic structure
1131        let mut object = Object::new(1);
1132        let mut mesh = Mesh::new();
1133        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
1134        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
1135        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
1136        mesh.triangles.push(Triangle::new(0, 1, 2));
1137        object.mesh = Some(mesh);
1138        model.resources.objects.push(object);
1139        model.build.items.push(BuildItem::new(1));
1140
1141        // Should fail validation (N_XXM_0610_01)
1142        let result = validate_texture_paths(&model);
1143        assert!(
1144            result.is_err(),
1145            "Should reject texture path with backslashes"
1146        );
1147        let error_msg = result.unwrap_err().to_string();
1148        assert!(error_msg.contains("backslash"));
1149    }
1150
1151    #[test]
1152    fn test_texture_path_empty() {
1153        use crate::model::Texture2D;
1154
1155        let mut model = Model::new();
1156
1157        // Add texture with empty path
1158        let mut texture = Texture2D::new(10, "".to_string(), "image/png".to_string());
1159        texture.parse_order = 1;
1160        model.resources.texture2d_resources.push(texture);
1161
1162        // Add object and build item for basic structure
1163        let mut object = Object::new(1);
1164        let mut mesh = Mesh::new();
1165        mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0));
1166        mesh.vertices.push(Vertex::new(1.0, 0.0, 0.0));
1167        mesh.vertices.push(Vertex::new(0.0, 1.0, 0.0));
1168        mesh.triangles.push(Triangle::new(0, 1, 2));
1169        object.mesh = Some(mesh);
1170        model.resources.objects.push(object);
1171        model.build.items.push(BuildItem::new(1));
1172
1173        // Should fail validation (N_XXM_0610_01)
1174        let result = validate_texture_paths(&model);
1175        assert!(result.is_err(), "Should reject empty texture path");
1176        let error_msg = result.unwrap_err().to_string();
1177        assert!(error_msg.contains("empty"));
1178    }
1179}