1mod beam_lattice;
12mod boolean_ops;
13mod core;
14mod displacement;
15mod material;
16mod production;
17mod slice;
18mod volumetric;
19
20pub 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
44use 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#[allow(dead_code)] pub fn validate_model(model: &Model) -> Result<()> {
69 validate_model_with_config(model, &ParserConfig::with_all_extensions())
71}
72
73#[allow(dead_code)] pub fn validate_model_with_config(model: &Model, config: &ParserConfig) -> Result<()> {
79 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 validate_production_extension_with_config(model, config)?;
90
91 config.registry().validate_all(model)?;
102
103 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 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)); 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)); 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 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 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 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 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 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 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 mesh.triangles.push(Triangle::new(0, 1, 2));
231
232 object.mesh = Some(mesh);
233 model.resources.objects.push(object);
234
235 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 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 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 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 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 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 let mut object = Object::new(1);
318 object.pid = Some(99); 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 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 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 let mut object = Object::new(1);
351 object.pid = Some(5);
352 object.pindex = Some(5); 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 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 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 let mut object = Object::new(1);
386 object.basematerialid = Some(5); 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 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 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 let mut object = Object::new(1);
419 object.basematerialid = Some(99); 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 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 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 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 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 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 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 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 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 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 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 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 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 let mut object = Object::new(1);
603 object.pid = Some(12); 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 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 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 let mut object = Object::new(1);
635 object.pid = Some(9); 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 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 let slice_stack = SliceStack::new(1, 0.0);
661 model.resources.slice_stacks.push(slice_stack);
662
663 let mut object = Object::new(1);
665 object.slicestackid = Some(1); 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 mesh.triangles.push(Triangle::new(0, 2, 1)); mesh.triangles.push(Triangle::new(0, 3, 2)); mesh.triangles.push(Triangle::new(0, 1, 3)); mesh.triangles.push(Triangle::new(1, 2, 3)); object.mesh = Some(mesh);
682 model.resources.objects.push(object);
683 model.build.items.push(BuildItem::new(1));
684
685 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 let mut object = Object::new(1);
700
701 let mut mesh = Mesh::new();
704 mesh.vertices.push(Vertex::new(0.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(10.0, 0.0, 0.0)); mesh.vertices.push(Vertex::new(10.0, 20.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 20.0, 0.0)); mesh.vertices.push(Vertex::new(0.0, 0.0, 30.0)); mesh.vertices.push(Vertex::new(10.0, 0.0, 30.0)); mesh.vertices.push(Vertex::new(10.0, 20.0, 30.0)); mesh.vertices.push(Vertex::new(0.0, 20.0, 30.0)); mesh.triangles.push(Triangle::new(1, 2, 3)); mesh.triangles.push(Triangle::new(3, 0, 1)); mesh.triangles.push(Triangle::new(6, 5, 4)); mesh.triangles.push(Triangle::new(4, 7, 6)); mesh.triangles.push(Triangle::new(5, 1, 0)); mesh.triangles.push(Triangle::new(0, 4, 5)); mesh.triangles.push(Triangle::new(6, 2, 1)); mesh.triangles.push(Triangle::new(1, 5, 6)); mesh.triangles.push(Triangle::new(7, 3, 2)); mesh.triangles.push(Triangle::new(2, 6, 7)); mesh.triangles.push(Triangle::new(4, 0, 3)); mesh.triangles.push(Triangle::new(3, 7, 4)); object.mesh = Some(mesh);
731 model.resources.objects.push(object);
732 model.build.items.push(BuildItem::new(1));
733
734 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 let slice_stack = SliceStack::new(1, 0.0);
751 model.resources.slice_stacks.push(slice_stack);
752
753 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 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 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 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 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 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 let mut color_group = ColorGroup::new(10);
823 color_group.parse_order = 1;
824 color_group.colors.push((255, 0, 0, 255)); model.resources.color_groups.push(color_group);
826
827 let mut multi = MultiProperties::new(20, vec![10, 10]); multi.parse_order = 2;
830 multi.multis.push(Multi::new(vec![0, 0]));
831 model.resources.multi_properties.push(multi);
832
833 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 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 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 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 let mut multi = MultiProperties::new(20, vec![6, 7, 5]); multi.parse_order = 4;
883 multi.multis.push(Multi::new(vec![0, 0, 0]));
884 model.resources.multi_properties.push(multi);
885
886 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 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 use crate::model::{BaseMaterial, BaseMaterialGroup, ColorGroup};
910
911 let mut model = Model::new();
912
913 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 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 let mut multi = MultiProperties::new(12, vec![6, 1]); multi.parse_order = 3;
930 multi.multis.push(Multi::new(vec![0, 0]));
931 model.resources.multi_properties.push(multi);
932
933 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 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 use crate::model::ColorGroup;
961
962 let mut model = Model::new();
963
964 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 let mut multi = MultiProperties::new(12, vec![5, 6]); multi.parse_order = 3;
978 multi.multis.push(Multi::new(vec![0, 0]));
979 model.resources.multi_properties.push(multi);
980
981 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 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 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 let mut triangle = Triangle::new(0, 1, 2);
1016 triangle.p1 = Some(0); 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 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 let mut tex_group = Texture2DGroup::new(10, 20);
1041 tex_group.parse_order = 1; model.resources.texture2d_groups.push(tex_group);
1043
1044 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; model.resources.texture2d_resources.push(texture);
1052
1053 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 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 let mut multi = MultiProperties::new(10, vec![20]);
1083 multi.parse_order = 1; multi.multis.push(Multi::new(vec![0]));
1085 model.resources.multi_properties.push(multi);
1086
1087 let mut color_group = ColorGroup::new(20);
1089 color_group.parse_order = 2; color_group.colors.push((255, 0, 0, 255));
1091 model.resources.color_groups.push(color_group);
1092
1093 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 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 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 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 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 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 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 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}