1use glam::{Mat3, Mat4, Quat, Vec3};
9use serde::{Deserialize, Serialize};
10use std::collections::{BTreeMap, BTreeSet};
11
12#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18#[non_exhaustive]
19pub enum AffineDomainViolation {
20 NonUniformScale,
22 Sheared,
24 Reflected,
26 Singular,
28 NonFinite,
30}
31
32#[derive(Debug, Clone, Copy, PartialEq)]
39pub(crate) struct PositiveUniformAffineTolerance {
40 pub(crate) equal_axis: f64,
41 pub(crate) relative_orthogonality: f64,
42 pub(crate) singular_determinant_relative: f64,
43}
44
45#[derive(Debug, Clone, Copy, PartialEq)]
52pub(crate) struct AffineGeometryFacts {
53 pub(crate) axis_lengths: [f64; 3],
54 pub(crate) mean_axis_length: f64,
55 pub(crate) determinant: f64,
56 pub(crate) axis_length_product: f64,
57 pub(crate) cross_axis_dots: [f64; 3],
59}
60
61impl AffineGeometryFacts {
62 pub(crate) fn from_linear(linear: Mat3) -> Result<Self, AffineDomainViolation> {
68 if !linear.is_finite() {
69 return Err(AffineDomainViolation::NonFinite);
70 }
71 let columns = [
72 linear.x_axis.as_dvec3(),
73 linear.y_axis.as_dvec3(),
74 linear.z_axis.as_dvec3(),
75 ];
76 let axis_lengths = affine_axis_lengths(linear);
77 let mean_axis_length = average_affine_axis_length(axis_lengths);
78 let determinant = columns[2].dot(columns[0].cross(columns[1]));
79 let axis_length_product = axis_lengths[0] * axis_lengths[1] * axis_lengths[2];
80 let cross_axis_dots = [
81 columns[0].dot(columns[1]),
82 columns[0].dot(columns[2]),
83 columns[1].dot(columns[2]),
84 ];
85 if axis_lengths.iter().any(|value| !value.is_finite())
86 || !mean_axis_length.is_finite()
87 || !determinant.is_finite()
88 || !axis_length_product.is_finite()
89 || cross_axis_dots.iter().any(|value| !value.is_finite())
90 {
91 return Err(AffineDomainViolation::NonFinite);
92 }
93 Ok(Self {
94 axis_lengths,
95 mean_axis_length,
96 determinant,
97 axis_length_product,
98 cross_axis_dots,
99 })
100 }
101
102 pub(crate) fn has_equal_axis_lengths(self, relative_tolerance: f64) -> bool {
108 values_equal_to_mean(
109 &self.axis_lengths,
110 self.mean_axis_length,
111 relative_tolerance,
112 )
113 }
114}
115
116pub(crate) fn values_equal_to_mean(values: &[f64], mean: f64, relative_tolerance: f64) -> bool {
119 values
120 .iter()
121 .all(|&value| (value - mean).abs() <= relative_tolerance * mean.abs().max(value.abs()))
122}
123
124pub(crate) fn classify_positive_uniform_affine(
131 linear: Mat3,
132 tolerance: PositiveUniformAffineTolerance,
133) -> Result<f64, AffineDomainViolation> {
134 let facts = AffineGeometryFacts::from_linear(linear)?;
135 if facts.mean_axis_length <= 0.0 {
136 return Err(AffineDomainViolation::Singular);
137 }
138
139 if facts.determinant.abs()
146 <= tolerance.singular_determinant_relative * facts.axis_length_product
147 {
148 return Err(AffineDomainViolation::Singular);
149 }
150 if !facts.has_equal_axis_lengths(tolerance.equal_axis) {
154 return Err(AffineDomainViolation::NonUniformScale);
155 }
156
157 let orthogonality_tolerance =
160 tolerance.relative_orthogonality * facts.mean_axis_length * facts.mean_axis_length;
161 if facts
162 .cross_axis_dots
163 .iter()
164 .any(|dot| dot.abs() > orthogonality_tolerance)
165 {
166 return Err(AffineDomainViolation::Sheared);
167 }
168 if facts.determinant < 0.0 {
169 return Err(AffineDomainViolation::Reflected);
170 }
171 Ok(facts.mean_axis_length)
172}
173
174pub(crate) fn affine_axis_lengths(linear: Mat3) -> [f64; 3] {
179 [
180 linear.x_axis.as_dvec3().length(),
181 linear.y_axis.as_dvec3().length(),
182 linear.z_axis.as_dvec3().length(),
183 ]
184}
185
186pub(crate) fn average_affine_axis_length(lengths: [f64; 3]) -> f64 {
192 let mut ascending = lengths;
193 ascending.sort_by(f64::total_cmp);
194 (ascending[0] + ascending[1] + ascending[2]) / 3.0
195}
196
197#[cfg(test)]
198pub(crate) mod affine_test_fixtures {
199 use super::{Mat3, Vec3};
200
201 pub(crate) fn tolerance_divergence_basis() -> Mat3 {
205 Mat3::from_diagonal(Vec3::new(1.0, 1.000_05, 1.0))
206 }
207
208 pub(crate) fn orthogonality_tolerance_divergence_basis() -> Mat3 {
211 Mat3::from_cols(Vec3::X, Vec3::new(5.0e-5, 1.0, 0.0), Vec3::Z)
212 }
213
214 pub(crate) fn appendix_d_v6_mean_permutations() -> [Mat3; 6] {
219 let columns = [
220 Vec3::new(
221 f32::from_bits(0x3f0e_8cbb),
222 f32::from_bits(0x3f26_fbbe),
223 f32::from_bits(0x3f21_9bc7),
224 ),
225 Vec3::new(
226 f32::from_bits(0x3d9c_b415),
227 f32::from_bits(0x3e92_d82b),
228 f32::from_bits(0x3f82_e85d),
229 ),
230 Vec3::new(
231 f32::from_bits(0x3f14_5226),
232 f32::from_bits(0x3e9e_e50d),
233 f32::from_bits(0x3f56_817c),
234 ),
235 ];
236 [
237 Mat3::from_cols(columns[0], columns[1], columns[2]),
238 Mat3::from_cols(-columns[0], columns[2], columns[1]),
239 Mat3::from_cols(-columns[1], columns[0], columns[2]),
240 Mat3::from_cols(columns[1], columns[2], columns[0]),
241 Mat3::from_cols(columns[2], columns[0], columns[1]),
242 Mat3::from_cols(-columns[2], columns[1], columns[0]),
243 ]
244 }
245}
246
247pub type BoneId = usize;
249
250#[derive(Debug, Clone, Copy, PartialEq)]
252pub struct Transform {
253 pub translation: Vec3,
255 pub rotation: Quat,
257 pub scale: Vec3,
259}
260
261impl Transform {
262 pub const IDENTITY: Self = Self {
265 translation: Vec3::ZERO,
266 rotation: Quat::IDENTITY,
267 scale: Vec3::ONE,
268 };
269
270 pub fn to_mat4(&self) -> Mat4 {
273 Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
274 }
275}
276
277impl Default for Transform {
278 fn default() -> Self {
279 Self::IDENTITY
280 }
281}
282
283#[derive(Debug, Clone)]
285pub struct Bone {
286 pub name: String,
288 pub parent: Option<BoneId>,
290 pub rest: Transform,
293 pub inverse_bind: Option<Mat4>,
295}
296
297#[derive(Debug, Clone, Default)]
300pub struct Skeleton {
301 pub bones: Vec<Bone>,
303}
304
305impl Skeleton {
306 pub fn bone_name(&self, id: BoneId) -> &str {
312 &self.bones[id].name
313 }
314}
315
316#[derive(Debug, Clone, Copy, PartialEq, Eq)]
324pub(crate) enum WorldMatrixError {
325 NonFiniteTransform {
328 node: BoneId,
330 },
331 InvalidParent {
334 node: BoneId,
336 parent: BoneId,
338 },
339}
340
341pub(crate) fn world_rest_matrices(skeleton: &Skeleton) -> Result<Vec<Mat4>, WorldMatrixError> {
351 let mut worlds = Vec::with_capacity(skeleton.bones.len());
352 for (node, bone) in skeleton.bones.iter().enumerate() {
353 let local = bone.rest.to_mat4();
354 if !mat4_is_finite(local) {
355 return Err(WorldMatrixError::NonFiniteTransform { node });
356 }
357 let world = match bone.parent {
358 Some(parent) if parent < node => worlds[parent] * local,
359 Some(parent) => return Err(WorldMatrixError::InvalidParent { node, parent }),
360 None => local,
361 };
362 if !mat4_is_finite(world) {
363 return Err(WorldMatrixError::NonFiniteTransform { node });
364 }
365 worlds.push(world);
366 }
367 Ok(worlds)
368}
369
370pub(crate) fn tolerant_world_rest_matrices(skeleton: &Skeleton) -> Vec<Option<Mat4>> {
376 let mut worlds = Vec::with_capacity(skeleton.bones.len());
377 for bone in &skeleton.bones {
378 let local = bone.rest.to_mat4();
379 let world = match bone.parent {
380 Some(parent) => worlds
381 .get(parent)
382 .copied()
383 .flatten()
384 .map(|parent_world| parent_world * local),
385 None => Some(local),
386 }
387 .filter(|matrix| mat4_is_finite(*matrix));
388 worlds.push(world);
389 }
390 worlds
391}
392
393pub(crate) fn mat4_is_finite(matrix: Mat4) -> bool {
394 matrix.to_cols_array().into_iter().all(f32::is_finite)
395}
396
397#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
399#[serde(rename_all = "snake_case")]
400pub enum Property {
401 Translation,
403 Rotation,
405 Scale,
407}
408
409impl Property {
410 pub fn as_str(self) -> &'static str {
413 match self {
414 Property::Translation => "translation",
415 Property::Rotation => "rotation",
416 Property::Scale => "scale",
417 }
418 }
419}
420
421#[derive(Debug, Clone, Copy, PartialEq, Eq)]
423pub enum Interpolation {
424 Linear,
426 Step,
428 CubicSpline,
432}
433
434#[derive(Debug, Clone)]
436pub enum TrackValues {
437 Vec3s(Vec<Vec3>),
439 Quats(Vec<Quat>),
441}
442
443impl TrackValues {
444 pub fn len(&self) -> usize {
447 match self {
448 TrackValues::Vec3s(v) => v.len(),
449 TrackValues::Quats(v) => v.len(),
450 }
451 }
452
453 pub fn is_empty(&self) -> bool {
455 self.len() == 0
456 }
457}
458
459#[derive(Debug, Clone)]
461pub struct Track {
462 pub bone: BoneId,
464 pub property: Property,
466 pub interpolation: Interpolation,
468 pub times: Vec<f32>,
471 pub values: TrackValues,
473}
474
475impl Track {
476 pub fn key_count(&self) -> usize {
478 self.times.len()
479 }
480
481 pub fn value_index(&self, k: usize) -> usize {
484 match self.interpolation {
485 Interpolation::CubicSpline => 3 * k + 1,
486 _ => k,
487 }
488 }
489
490 pub fn key_vec3(&self, k: usize) -> Option<Vec3> {
492 match &self.values {
493 TrackValues::Vec3s(v) => v.get(self.value_index(k)).copied(),
494 TrackValues::Quats(_) => None,
495 }
496 }
497
498 pub fn key_quat(&self, k: usize) -> Option<Quat> {
500 match &self.values {
501 TrackValues::Quats(v) => v.get(self.value_index(k)).copied(),
502 TrackValues::Vec3s(_) => None,
503 }
504 }
505
506 pub fn start_time(&self) -> f32 {
508 self.times.first().copied().unwrap_or(0.0)
509 }
510
511 pub fn end_time(&self) -> f32 {
513 self.times.last().copied().unwrap_or(0.0)
514 }
515}
516
517#[derive(Debug, Clone)]
519pub struct Clip {
520 pub name: String,
523 pub duration_s: f64,
525 pub tracks: Vec<Track>,
527}
528
529#[derive(Debug, Clone, Default)]
531pub struct SourceInfo {
532 pub path: Option<String>,
534 pub format: Option<String>,
536}
537
538#[derive(Debug, Clone, Default)]
545pub struct Document {
546 pub skeleton: Skeleton,
548 pub clips: Vec<Clip>,
550 pub assets: SceneAssets,
552 pub source: SourceInfo,
554}
555
556#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
562#[non_exhaustive]
563pub enum DocumentShapeError {
564 #[error("node {node} has a non-finite rest transform")]
567 NonFiniteSkeletonRest {
568 node: BoneId,
570 },
571 #[error("node {node} has invalid parent {parent}")]
573 InvalidSkeletonParent {
574 node: BoneId,
576 parent: BoneId,
578 },
579 #[error("source skeleton declares duplicate source node index {source_node_index}")]
581 DuplicateSourceNodeIndex {
582 source_node_index: usize,
584 },
585 #[error("source skeleton declares duplicate source skin index {source_skin_index}")]
587 DuplicateSourceSkinIndex {
588 source_skin_index: usize,
590 },
591 #[error(
593 "source node {source_node_index} contradicts the document skeleton's parent chain ({violation})"
594 )]
595 SourceProjection {
596 source_node_index: usize,
598 violation: SourceProjectionViolation,
600 },
601 #[error("clip {clip_index} declares duplicate {property:?} tracks for node {node}")]
603 DuplicateClipTrack {
604 clip_index: usize,
606 node: BoneId,
608 property: Property,
610 },
611 #[error("clip {clip_index} track for node {node} has an invalid shape ({violation})")]
613 TrackShape {
614 clip_index: usize,
616 node: BoneId,
618 violation: TrackShapeViolation,
620 },
621 #[error("mesh instance {instance_index} is invalid ({violation})")]
623 MeshInstanceShape {
624 instance_index: usize,
626 violation: MeshInstanceShapeViolation,
628 },
629 #[error("node {node} has a non-finite inverse-bind matrix")]
631 NonFiniteBoneInverseBind {
632 node: BoneId,
634 },
635}
636
637#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
639#[non_exhaustive]
640pub enum SourceProjectionViolation {
641 #[error("projected_bone_out_of_range")]
643 ProjectedBoneOutOfRange,
644 #[error("two_source_nodes_project_to_one_bone")]
646 TwoSourceNodesProjectToOneBone,
647 #[error("parent_source_node_is_missing")]
649 ParentSourceNodeMissing,
650 #[error("cyclic_unprojected_source_parent_chain")]
652 CyclicUnprojectedSourceParentChain,
653 #[error("projection_and_skeleton_parents_differ")]
655 NearestProjectedParentMismatch,
656 #[error("projected_bone_has_an_unprojected_skeleton_child")]
658 ProjectedBoneHasUnprojectedSkeletonChild,
659}
660
661#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
663#[non_exhaustive]
664pub enum TrackShapeViolation {
665 #[error("bone_index_out_of_range")]
667 BoneIndexOutOfRange,
668 #[error("empty_times")]
670 EmptyTimes,
671 #[error("non_finite_time")]
673 NonFiniteTime,
674 #[error("times_not_strictly_increasing")]
676 TimesNotStrictlyIncreasing,
677 #[error("value_count_mismatch")]
679 ValueCountMismatch,
680 #[error("value_type_mismatches_property")]
682 ValueTypeMismatchesProperty,
683 #[error("non_finite_value")]
685 NonFiniteValue,
686}
687
688#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
690#[non_exhaustive]
691pub enum MeshInstanceShapeViolation {
692 #[error("node_index_out_of_range")]
694 NodeIndexOutOfRange,
695 #[error("mesh_index_out_of_range")]
697 MeshIndexOutOfRange,
698 #[error("skin_joint_out_of_range")]
700 SkinJointOutOfRange,
701 #[error("skin_ibm_count_mismatch")]
703 SkinInverseBindCountMismatch,
704 #[error("non_finite_inverse_bind")]
706 NonFiniteSkinInverseBind,
707}
708
709#[derive(Debug, Clone, Default)]
725pub struct Primitive {
726 pub source_primitive_index: Option<usize>,
731 pub material: Option<usize>,
733 pub indices: Vec<u32>,
735 pub positions: Vec<Vec3>,
737 pub normals: Vec<Vec3>,
739 pub uvs: Vec<[f32; 2]>,
741 pub joints: Vec<[u16; 4]>,
743 pub weights: Vec<[f32; 4]>,
745 pub additional_influence_sets: Vec<AdditionalInfluenceSet>,
751}
752
753#[derive(Debug, Clone, Copy, PartialEq, Eq)]
760pub struct AdditionalInfluenceSet {
761 pub set_index: u32,
763 pub joints_present: bool,
765 pub weights_present: bool,
767}
768
769#[derive(Debug, Clone, Default)]
771pub struct MeshAsset {
772 pub name: String,
774 pub source_mesh_index: usize,
779 pub primitives: Vec<Primitive>,
781}
782
783#[derive(Debug, Clone, Default)]
785pub struct MeshInstance {
786 pub source_node_index: usize,
788 pub node: BoneId,
790 pub mesh: usize,
792 pub skin_joints: Vec<BoneId>,
794 pub skin_ibms: Vec<Mat4>,
799}
800
801#[derive(Debug, Clone, Default)]
803pub struct SceneAsset {
804 pub source_scene_index: usize,
806 pub name: Option<String>,
808 pub roots: Vec<BoneId>,
810}
811
812#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
819#[serde(rename_all = "snake_case")]
820pub enum SourceSkeletonCoverage {
821 #[default]
823 Unavailable,
824 Complete,
828}
829
830#[derive(Debug, Clone)]
842pub enum SourceNodeLocalRest {
843 Trs {
845 translation: Vec3,
847 rotation: Quat,
849 scale: Vec3,
851 },
852 Matrix(Mat4),
854}
855
856#[derive(Debug, Clone)]
870#[non_exhaustive]
871pub struct SourceNodeAsset {
872 pub source_node_index: usize,
874 pub name: Option<String>,
876 pub parent_source_node_index: Option<usize>,
878 pub scene_root_indices: Vec<usize>,
880 pub local_rest: SourceNodeLocalRest,
882 pub bone: Option<BoneId>,
902}
903
904impl SourceNodeAsset {
905 pub fn new(source_node_index: usize, local_rest: SourceNodeLocalRest) -> Self {
913 Self {
914 source_node_index,
915 name: None,
916 parent_source_node_index: None,
917 scene_root_indices: Vec::new(),
918 local_rest,
919 bone: None,
920 }
921 }
922}
923
924#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
932#[serde(rename_all = "snake_case")]
933pub enum SourceInverseBindAccessorStatus {
934 #[default]
936 Absent,
937 Available,
939 EmptyAccessor,
941 CountMismatch,
943 Unreadable,
945}
946
947#[derive(Debug, Clone, Default)]
949pub struct SourceInverseBindAccessor {
950 pub status: SourceInverseBindAccessorStatus,
952 pub declared_count: Option<usize>,
954 pub matrices: Vec<Mat4>,
963}
964
965#[derive(Debug, Clone)]
967pub struct SourceSkinAttachment {
968 pub source_node_index: usize,
970 pub source_mesh_index: Option<usize>,
975}
976
977#[derive(Debug, Clone, Default)]
979pub struct SourceSkinAsset {
980 pub source_skin_index: usize,
982 pub name: Option<String>,
984 pub skeleton_root_source_node_index: Option<usize>,
986 pub joint_source_node_indices: Vec<usize>,
988 pub inverse_bind_accessor: SourceInverseBindAccessor,
994 pub attachments: Vec<SourceSkinAttachment>,
996}
997
998#[derive(Debug, Clone, Default)]
1000pub struct SourceSkeletonAssets {
1001 pub coverage: SourceSkeletonCoverage,
1003 pub nodes: Vec<SourceNodeAsset>,
1005 pub skins: Vec<SourceSkinAsset>,
1007}
1008
1009#[derive(Debug, Clone)]
1012pub struct TextureAsset {
1013 pub bytes: Vec<u8>,
1015 pub mime: String,
1017}
1018
1019#[derive(Debug, Clone)]
1024pub struct NormalTextureAsset {
1025 pub texture: TextureAsset,
1027 pub scale: f32,
1029}
1030
1031#[derive(Debug, Clone)]
1037pub struct OcclusionTextureAsset {
1038 pub texture: TextureAsset,
1040 pub strength: f32,
1042}
1043
1044#[derive(Debug, Clone)]
1046pub struct MaterialAsset {
1047 pub name: String,
1049 pub base_color: [f32; 4],
1052 pub metallic: f32,
1054 pub roughness: f32,
1056 pub base_color_texture: Option<TextureAsset>,
1058 pub normal_texture: Option<NormalTextureAsset>,
1060 pub metallic_roughness_texture: Option<TextureAsset>,
1064 pub occlusion_texture: Option<OcclusionTextureAsset>,
1066}
1067
1068#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1074#[serde(rename_all = "snake_case")]
1075pub enum MaterialResourceCoverage {
1076 Complete,
1080 #[default]
1082 Unavailable,
1083}
1084
1085#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1090#[serde(rename_all = "snake_case")]
1091pub enum MaterialTextureSlot {
1092 BaseColor,
1094 Normal,
1096 MetallicRoughness,
1098 Occlusion,
1100 Emissive,
1102}
1103
1104#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1106pub struct SourceMaterialTextureBinding {
1107 pub slot: MaterialTextureSlot,
1109 pub texture_index: usize,
1111}
1112
1113#[derive(Debug, Clone, Default)]
1115pub struct SourceMaterialAsset {
1116 pub material_index: usize,
1118 pub name: Option<String>,
1120 pub texture_bindings: Vec<SourceMaterialTextureBinding>,
1122}
1123
1124#[derive(Debug, Clone, Default)]
1126pub struct SourceTextureAsset {
1127 pub texture_index: usize,
1129 pub name: Option<String>,
1131 pub image_index: usize,
1133}
1134
1135#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1137#[serde(rename_all = "snake_case")]
1138pub enum ImageSourceKind {
1139 Embedded,
1141 DataUri,
1143 External,
1145}
1146
1147#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1149#[serde(rename_all = "snake_case")]
1150pub enum ImageContainerFormat {
1151 Png,
1153 Jpeg,
1155}
1156
1157#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1159#[serde(rename_all = "snake_case")]
1160pub enum DecodedImageColorType {
1161 L8,
1163 La8,
1165 Rgb8,
1167 Rgba8,
1169 L16,
1171 La16,
1173 Rgb16,
1175 Rgba16,
1177}
1178
1179#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1181#[serde(rename_all = "snake_case")]
1182pub enum ImageUnavailableReason {
1183 SourceUnavailable,
1185 InvalidDataUri,
1187 UnsupportedContainer,
1189 DecodeFailed,
1191 ResourceLimit,
1193}
1194
1195#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1197pub enum SourceImageInspection {
1198 Available {
1200 width: u32,
1202 height: u32,
1204 channel_count: u8,
1206 color_type: DecodedImageColorType,
1208 },
1209 Unavailable {
1211 reason: ImageUnavailableReason,
1213 },
1214}
1215
1216#[derive(Debug, Clone)]
1218pub struct SourceImageAsset {
1219 pub image_index: usize,
1221 pub name: Option<String>,
1223 pub source_kind: ImageSourceKind,
1225 pub declared_mime_type: Option<String>,
1227 pub detected_container: Option<ImageContainerFormat>,
1229 pub leading_magic_hex: Option<String>,
1232 pub inspection: SourceImageInspection,
1234}
1235
1236#[derive(Debug, Clone, Default)]
1238pub struct MaterialResourceAssets {
1239 pub coverage: MaterialResourceCoverage,
1241 pub materials: Vec<SourceMaterialAsset>,
1243 pub textures: Vec<SourceTextureAsset>,
1245 pub images: Vec<SourceImageAsset>,
1247}
1248
1249impl Primitive {
1250 pub fn weld(&mut self) {
1254 if !self.indices.is_empty() || self.positions.is_empty() {
1255 return;
1256 }
1257 let corner_key = |i: usize| -> Vec<u8> {
1258 let mut key = Vec::with_capacity(64);
1259 let mut push_f32s = |vals: &[f32]| {
1260 for v in vals {
1261 key.extend_from_slice(&v.to_le_bytes());
1262 }
1263 };
1264 push_f32s(&self.positions[i].to_array());
1265 if let Some(n) = self.normals.get(i) {
1266 push_f32s(&n.to_array());
1267 }
1268 if let Some(uv) = self.uvs.get(i) {
1269 push_f32s(uv);
1270 }
1271 if let Some(w) = self.weights.get(i) {
1272 push_f32s(w);
1273 }
1274 if let Some(j) = self.joints.get(i) {
1275 for v in j {
1276 key.extend_from_slice(&v.to_le_bytes());
1277 }
1278 }
1279 key
1280 };
1281 let mut seen: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
1282 let mut indices = Vec::with_capacity(self.positions.len());
1283 let mut positions = Vec::new();
1284 let mut normals = Vec::new();
1285 let mut uvs = Vec::new();
1286 let mut joints = Vec::new();
1287 let mut weights = Vec::new();
1288 for i in 0..self.positions.len() {
1289 let index = *seen.entry(corner_key(i)).or_insert_with(|| {
1290 positions.push(self.positions[i]);
1291 if let Some(n) = self.normals.get(i) {
1292 normals.push(*n);
1293 }
1294 if let Some(uv) = self.uvs.get(i) {
1295 uvs.push(*uv);
1296 }
1297 if let Some(j) = self.joints.get(i) {
1298 joints.push(*j);
1299 }
1300 if let Some(w) = self.weights.get(i) {
1301 weights.push(*w);
1302 }
1303 (positions.len() - 1) as u32
1304 });
1305 indices.push(index);
1306 }
1307 self.indices = indices;
1308 self.positions = positions;
1309 self.normals = normals;
1310 self.uvs = uvs;
1311 self.joints = joints;
1312 self.weights = weights;
1313 }
1314}
1315
1316#[derive(Debug, Clone, Default)]
1319pub struct SceneAssets {
1320 pub meshes: Vec<MeshAsset>,
1323 pub instances: Vec<MeshInstance>,
1325 pub materials: Vec<MaterialAsset>,
1327 pub material_resources: MaterialResourceAssets,
1330 pub scenes: Vec<SceneAsset>,
1332 pub default_scene: Option<usize>,
1334 pub source_skeleton: SourceSkeletonAssets,
1341}
1342
1343pub fn validate_document_shape(document: &Document) -> Result<(), DocumentShapeError> {
1361 validate_document_structure_prefix(document)?;
1362 validate_clip_tracks(document)?;
1363 validate_mesh_instances(document)?;
1364 validate_bone_inverse_binds(&document.skeleton)
1365}
1366
1367fn validate_document_structure_prefix(document: &Document) -> Result<(), DocumentShapeError> {
1368 validate_skeleton_rest(&document.skeleton)?;
1369 validate_source_skeleton_identity(&document.assets.source_skeleton)?;
1370 validate_source_projection(document)
1371}
1372
1373fn validate_skeleton_rest(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1374 world_rest_matrices(skeleton)
1375 .map(|_| ())
1376 .map_err(|error| match error {
1377 WorldMatrixError::NonFiniteTransform { node } => {
1378 DocumentShapeError::NonFiniteSkeletonRest { node }
1379 }
1380 WorldMatrixError::InvalidParent { node, parent } => {
1381 DocumentShapeError::InvalidSkeletonParent { node, parent }
1382 }
1383 })
1384}
1385
1386fn validate_source_skeleton_identity(
1387 source_skeleton: &SourceSkeletonAssets,
1388) -> Result<(), DocumentShapeError> {
1389 let mut seen_nodes = BTreeSet::new();
1390 for node in &source_skeleton.nodes {
1391 if !seen_nodes.insert(node.source_node_index) {
1392 return Err(DocumentShapeError::DuplicateSourceNodeIndex {
1393 source_node_index: node.source_node_index,
1394 });
1395 }
1396 }
1397 let mut seen_skins = BTreeSet::new();
1398 for skin in &source_skeleton.skins {
1399 if !seen_skins.insert(skin.source_skin_index) {
1400 return Err(DocumentShapeError::DuplicateSourceSkinIndex {
1401 source_skin_index: skin.source_skin_index,
1402 });
1403 }
1404 }
1405 Ok(())
1406}
1407
1408fn validate_source_projection(document: &Document) -> Result<(), DocumentShapeError> {
1422 let source_skeleton = &document.assets.source_skeleton;
1423 if source_skeleton.coverage != SourceSkeletonCoverage::Complete {
1424 return Ok(());
1425 }
1426
1427 let bones = &document.skeleton.bones;
1428 let mut bone_of_source = BTreeMap::new();
1429 let mut source_of_bone = BTreeMap::new();
1430 let mut skeleton_parents = Vec::with_capacity(source_skeleton.nodes.len());
1431 for node in &source_skeleton.nodes {
1432 let Some(bone) = node.bone else {
1433 continue;
1434 };
1435 let skeleton_parent = bones
1436 .get(bone)
1437 .ok_or(DocumentShapeError::SourceProjection {
1438 source_node_index: node.source_node_index,
1439 violation: SourceProjectionViolation::ProjectedBoneOutOfRange,
1440 })?
1441 .parent;
1442 if source_of_bone
1443 .insert(bone, node.source_node_index)
1444 .is_some()
1445 {
1446 return Err(DocumentShapeError::SourceProjection {
1447 source_node_index: node.source_node_index,
1448 violation: SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
1449 });
1450 }
1451 bone_of_source.insert(node.source_node_index, bone);
1452 skeleton_parents.push((node, skeleton_parent));
1453 }
1454
1455 let by_source_index: BTreeMap<_, _> = source_skeleton
1456 .nodes
1457 .iter()
1458 .map(|node| (node.source_node_index, node))
1459 .collect();
1460 let unprojected_rows = source_skeleton.nodes.len() - bone_of_source.len();
1461 let mut resolved_unprojected = BTreeMap::<usize, Option<BoneId>>::new();
1465 for (node, skeleton_parent) in skeleton_parents {
1466 let mut cursor = node.parent_source_node_index;
1467 let mut unresolved_suffix = Vec::new();
1468 let projected_parent = loop {
1469 let Some(parent_source_node_index) = cursor else {
1470 break None;
1471 };
1472 if let Some(&bone) = bone_of_source.get(&parent_source_node_index) {
1473 break Some(bone);
1474 }
1475 if let Some(&projected_parent) = resolved_unprojected.get(&parent_source_node_index) {
1476 break projected_parent;
1477 }
1478 let parent = by_source_index.get(&parent_source_node_index).ok_or(
1479 DocumentShapeError::SourceProjection {
1480 source_node_index: node.source_node_index,
1481 violation: SourceProjectionViolation::ParentSourceNodeMissing,
1482 },
1483 )?;
1484 unresolved_suffix.push(parent_source_node_index);
1485 if unresolved_suffix.len() > unprojected_rows {
1486 return Err(DocumentShapeError::SourceProjection {
1487 source_node_index: node.source_node_index,
1488 violation: SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
1489 });
1490 }
1491 cursor = parent.parent_source_node_index;
1492 };
1493 for source_node_index in unresolved_suffix {
1494 resolved_unprojected.insert(source_node_index, projected_parent);
1495 }
1496 if projected_parent != skeleton_parent {
1497 return Err(DocumentShapeError::SourceProjection {
1498 source_node_index: node.source_node_index,
1499 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1500 });
1501 }
1502 }
1503
1504 for (bone, child) in bones.iter().enumerate() {
1505 if source_of_bone.contains_key(&bone) {
1506 continue;
1507 }
1508 if let Some(parent) = child.parent
1509 && let Some(&source_node_index) = source_of_bone.get(&parent)
1510 {
1511 return Err(DocumentShapeError::SourceProjection {
1512 source_node_index,
1513 violation: SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
1514 });
1515 }
1516 }
1517 Ok(())
1518}
1519
1520fn validate_clip_tracks(document: &Document) -> Result<(), DocumentShapeError> {
1521 let bone_count = document.skeleton.bones.len();
1522 for (clip_index, clip) in document.clips.iter().enumerate() {
1523 let mut seen = Vec::with_capacity(clip.tracks.len());
1524 for track in &clip.tracks {
1525 if track.bone >= bone_count {
1526 return Err(DocumentShapeError::TrackShape {
1527 clip_index,
1528 node: track.bone,
1529 violation: TrackShapeViolation::BoneIndexOutOfRange,
1530 });
1531 }
1532 if seen.contains(&(track.bone, track.property)) {
1533 return Err(DocumentShapeError::DuplicateClipTrack {
1534 clip_index,
1535 node: track.bone,
1536 property: track.property,
1537 });
1538 }
1539 seen.push((track.bone, track.property));
1540 validate_track_shape(clip_index, track)?;
1541 }
1542 }
1543 Ok(())
1544}
1545
1546pub(crate) fn validate_track_shape(
1547 clip_index: usize,
1548 track: &Track,
1549) -> Result<(), DocumentShapeError> {
1550 let violation = if track.times.is_empty() {
1551 Some(TrackShapeViolation::EmptyTimes)
1552 } else if track.times.iter().any(|time| !time.is_finite()) {
1553 Some(TrackShapeViolation::NonFiniteTime)
1554 } else if track.times.windows(2).any(|times| times[0] >= times[1]) {
1555 Some(TrackShapeViolation::TimesNotStrictlyIncreasing)
1556 } else {
1557 let expected_values = match track.interpolation {
1558 Interpolation::CubicSpline => track.times.len().checked_mul(3),
1559 Interpolation::Linear | Interpolation::Step => Some(track.times.len()),
1560 };
1561 if expected_values != Some(track.values.len()) {
1562 Some(TrackShapeViolation::ValueCountMismatch)
1563 } else if !matches!(
1564 (&track.values, track.property),
1565 (
1566 TrackValues::Vec3s(_),
1567 Property::Translation | Property::Scale
1568 ) | (TrackValues::Quats(_), Property::Rotation)
1569 ) {
1570 Some(TrackShapeViolation::ValueTypeMismatchesProperty)
1571 } else if match &track.values {
1572 TrackValues::Vec3s(values) => values.iter().any(|value| !value.is_finite()),
1573 TrackValues::Quats(values) => values.iter().any(|value| !value.is_finite()),
1574 } {
1575 Some(TrackShapeViolation::NonFiniteValue)
1576 } else {
1577 None
1578 }
1579 };
1580 violation.map_or(Ok(()), |violation| {
1581 Err(DocumentShapeError::TrackShape {
1582 clip_index,
1583 node: track.bone,
1584 violation,
1585 })
1586 })
1587}
1588
1589fn validate_mesh_instances(document: &Document) -> Result<(), DocumentShapeError> {
1590 let bone_count = document.skeleton.bones.len();
1591 let mesh_count = document.assets.meshes.len();
1592 for (instance_index, instance) in document.assets.instances.iter().enumerate() {
1593 let violation = if instance.node >= bone_count {
1594 Some(MeshInstanceShapeViolation::NodeIndexOutOfRange)
1595 } else if instance.mesh >= mesh_count {
1596 Some(MeshInstanceShapeViolation::MeshIndexOutOfRange)
1597 } else if instance
1598 .skin_joints
1599 .iter()
1600 .any(|&joint| joint >= bone_count)
1601 {
1602 Some(MeshInstanceShapeViolation::SkinJointOutOfRange)
1603 } else if !instance.skin_ibms.is_empty()
1604 && instance.skin_ibms.len() != instance.skin_joints.len()
1605 {
1606 Some(MeshInstanceShapeViolation::SkinInverseBindCountMismatch)
1607 } else if instance.skin_ibms.iter().any(|ibm| !mat4_is_finite(*ibm)) {
1608 Some(MeshInstanceShapeViolation::NonFiniteSkinInverseBind)
1609 } else {
1610 None
1611 };
1612 if let Some(violation) = violation {
1613 return Err(DocumentShapeError::MeshInstanceShape {
1614 instance_index,
1615 violation,
1616 });
1617 }
1618 }
1619 Ok(())
1620}
1621
1622fn validate_bone_inverse_binds(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1623 for (node, bone) in skeleton.bones.iter().enumerate() {
1624 if let Some(inverse_bind) = bone.inverse_bind
1625 && !mat4_is_finite(inverse_bind)
1626 {
1627 return Err(DocumentShapeError::NonFiniteBoneInverseBind { node });
1628 }
1629 }
1630 Ok(())
1631}
1632
1633#[cfg(test)]
1634mod tests {
1635 use super::*;
1636
1637 #[test]
1638 fn property_serde_uses_the_stable_trs_vocabulary() {
1639 assert_eq!(
1640 serde_json::to_value([Property::Translation, Property::Rotation, Property::Scale,])
1641 .expect("properties serialize"),
1642 serde_json::json!(["translation", "rotation", "scale"])
1643 );
1644 assert_eq!(
1645 serde_json::from_value::<Vec<Property>>(serde_json::json!([
1646 "translation",
1647 "rotation",
1648 "scale"
1649 ]))
1650 .expect("properties deserialize"),
1651 [Property::Translation, Property::Rotation, Property::Scale,]
1652 );
1653 }
1654
1655 fn bone(parent: Option<BoneId>) -> Bone {
1656 Bone {
1657 name: "bone".into(),
1658 parent,
1659 rest: Transform::IDENTITY,
1660 inverse_bind: None,
1661 }
1662 }
1663
1664 fn one_bone_document() -> Document {
1665 Document {
1666 skeleton: Skeleton {
1667 bones: vec![bone(None)],
1668 },
1669 ..Document::default()
1670 }
1671 }
1672
1673 fn source_node(
1674 source_node_index: usize,
1675 parent_source_node_index: Option<usize>,
1676 bone: Option<BoneId>,
1677 ) -> SourceNodeAsset {
1678 SourceNodeAsset {
1679 source_node_index,
1680 name: None,
1681 parent_source_node_index,
1682 scene_root_indices: Vec::new(),
1683 local_rest: SourceNodeLocalRest::Trs {
1684 translation: Vec3::ZERO,
1685 rotation: Quat::IDENTITY,
1686 scale: Vec3::ONE,
1687 },
1688 bone,
1689 }
1690 }
1691
1692 fn valid_track() -> Track {
1693 Track {
1694 bone: 0,
1695 property: Property::Translation,
1696 interpolation: Interpolation::Linear,
1697 times: vec![0.0],
1698 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
1699 }
1700 }
1701
1702 fn track_document(track: Track) -> Document {
1703 let mut document = one_bone_document();
1704 document.clips.push(Clip {
1705 name: "clip".into(),
1706 duration_s: 0.0,
1707 tracks: vec![track],
1708 });
1709 document
1710 }
1711
1712 fn instance_document() -> Document {
1713 let mut document = one_bone_document();
1714 document.assets.meshes.push(MeshAsset::default());
1715 document.assets.instances.push(MeshInstance {
1716 node: 0,
1717 mesh: 0,
1718 ..MeshInstance::default()
1719 });
1720 document
1721 }
1722
1723 #[test]
1724 fn document_shape_validation_accepts_a_complete_projection_with_an_unprojected_intermediate() {
1725 let mut document = Document {
1726 skeleton: Skeleton {
1727 bones: vec![bone(None), bone(Some(0))],
1728 },
1729 assets: SceneAssets {
1730 source_skeleton: SourceSkeletonAssets {
1731 coverage: SourceSkeletonCoverage::Complete,
1732 nodes: vec![
1733 source_node(10, None, Some(0)),
1734 source_node(11, Some(10), None),
1735 source_node(12, Some(11), Some(1)),
1736 ],
1737 ..SourceSkeletonAssets::default()
1738 },
1739 meshes: vec![MeshAsset::default()],
1740 instances: vec![MeshInstance {
1741 node: 1,
1742 mesh: 0,
1743 skin_joints: vec![0, 1],
1744 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
1745 ..MeshInstance::default()
1746 }],
1747 ..SceneAssets::default()
1748 },
1749 ..Document::default()
1750 };
1751 document.clips.push(Clip {
1752 name: "clip".into(),
1753 duration_s: 0.0,
1754 tracks: vec![valid_track()],
1755 });
1756
1757 assert_eq!(validate_document_shape(&document), Ok(()));
1758 }
1759
1760 #[test]
1761 fn shared_unprojected_parent_suffix_preserves_each_projected_parent() {
1762 const CONNECTORS: usize = 64;
1763 const PROJECTED_CHILDREN: usize = 64;
1764
1765 let mut nodes = Vec::with_capacity(1 + CONNECTORS + PROJECTED_CHILDREN);
1766 nodes.push(source_node(0, None, Some(0)));
1767 for source_node_index in 1..=CONNECTORS {
1768 nodes.push(source_node(
1769 source_node_index,
1770 Some(source_node_index - 1),
1771 None,
1772 ));
1773 }
1774 for child in 0..PROJECTED_CHILDREN {
1775 nodes.push(source_node(
1776 1 + CONNECTORS + child,
1777 Some(CONNECTORS),
1778 Some(1 + child),
1779 ));
1780 }
1781 let document = Document {
1782 skeleton: Skeleton {
1783 bones: std::iter::once(bone(None))
1784 .chain((0..PROJECTED_CHILDREN).map(|_| bone(Some(0))))
1785 .collect(),
1786 },
1787 assets: SceneAssets {
1788 source_skeleton: SourceSkeletonAssets {
1789 coverage: SourceSkeletonCoverage::Complete,
1790 nodes,
1791 ..SourceSkeletonAssets::default()
1792 },
1793 ..SceneAssets::default()
1794 },
1795 ..Document::default()
1796 };
1797
1798 assert_eq!(validate_document_shape(&document), Ok(()));
1799 let mut mismatched = document.clone();
1800 mismatched.skeleton.bones[PROJECTED_CHILDREN].parent = None;
1801 assert_eq!(
1802 validate_document_shape(&mismatched),
1803 Err(DocumentShapeError::SourceProjection {
1804 source_node_index: CONNECTORS + PROJECTED_CHILDREN,
1805 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1806 })
1807 );
1808 }
1809
1810 #[test]
1811 fn document_shape_validation_has_an_analytic_error_for_every_variant() {
1812 let projection_error =
1813 |source_node_index, violation| DocumentShapeError::SourceProjection {
1814 source_node_index,
1815 violation,
1816 };
1817 let track_error = |node, violation| DocumentShapeError::TrackShape {
1818 clip_index: 0,
1819 node,
1820 violation,
1821 };
1822 let instance_error = |violation| DocumentShapeError::MeshInstanceShape {
1823 instance_index: 0,
1824 violation,
1825 };
1826
1827 let mut non_finite_rest = one_bone_document();
1828 non_finite_rest.skeleton.bones[0].rest.translation.x = f32::NAN;
1829 let overflowed_rest_world = Document {
1830 skeleton: Skeleton {
1831 bones: vec![
1832 Bone {
1833 rest: Transform {
1834 scale: Vec3::splat(f32::MAX),
1835 ..Transform::IDENTITY
1836 },
1837 ..bone(None)
1838 },
1839 Bone {
1840 rest: Transform {
1841 translation: Vec3::splat(2.0),
1842 ..Transform::IDENTITY
1843 },
1844 ..bone(Some(0))
1845 },
1846 ],
1847 },
1848 ..Document::default()
1849 };
1850 let self_parent = Document {
1851 skeleton: Skeleton {
1852 bones: vec![bone(Some(0))],
1853 },
1854 ..Document::default()
1855 };
1856 let forward_parent = Document {
1857 skeleton: Skeleton {
1858 bones: vec![bone(Some(1)), bone(None)],
1859 },
1860 ..Document::default()
1861 };
1862 let far_parent = Document {
1863 skeleton: Skeleton {
1864 bones: vec![bone(Some(99))],
1865 },
1866 ..Document::default()
1867 };
1868 let duplicate_node = Document {
1869 assets: SceneAssets {
1870 source_skeleton: SourceSkeletonAssets {
1871 nodes: vec![
1872 source_node(9, None, None),
1873 source_node(10, None, None),
1874 source_node(9, None, None),
1875 ],
1876 ..SourceSkeletonAssets::default()
1877 },
1878 ..SceneAssets::default()
1879 },
1880 ..Document::default()
1881 };
1882 let duplicate_skin = Document {
1883 assets: SceneAssets {
1884 source_skeleton: SourceSkeletonAssets {
1885 skins: vec![
1886 SourceSkinAsset {
1887 source_skin_index: 4,
1888 ..SourceSkinAsset::default()
1889 },
1890 SourceSkinAsset {
1891 source_skin_index: 5,
1892 ..SourceSkinAsset::default()
1893 },
1894 SourceSkinAsset {
1895 source_skin_index: 4,
1896 ..SourceSkinAsset::default()
1897 },
1898 ],
1899 ..SourceSkeletonAssets::default()
1900 },
1901 ..SceneAssets::default()
1902 },
1903 ..Document::default()
1904 };
1905 let complete_projection = |nodes| SceneAssets {
1906 source_skeleton: SourceSkeletonAssets {
1907 coverage: SourceSkeletonCoverage::Complete,
1908 nodes,
1909 ..SourceSkeletonAssets::default()
1910 },
1911 ..SceneAssets::default()
1912 };
1913 let out_of_range_projection = Document {
1914 skeleton: Skeleton {
1915 bones: vec![bone(None)],
1916 },
1917 assets: complete_projection(vec![source_node(10, None, Some(1))]),
1918 ..Document::default()
1919 };
1920 let non_injective_projection = Document {
1921 skeleton: Skeleton {
1922 bones: vec![bone(None)],
1923 },
1924 assets: complete_projection(vec![
1925 source_node(10, None, Some(0)),
1926 source_node(11, None, Some(0)),
1927 ]),
1928 ..Document::default()
1929 };
1930 let missing_projection_parent = Document {
1931 skeleton: Skeleton {
1932 bones: vec![bone(None), bone(Some(0))],
1933 },
1934 assets: complete_projection(vec![source_node(11, Some(99), Some(1))]),
1935 ..Document::default()
1936 };
1937 let missing_projection_parent_at_cycle_bound = Document {
1942 skeleton: Skeleton {
1943 bones: vec![bone(None), bone(Some(0))],
1944 },
1945 assets: complete_projection(vec![
1946 source_node(10, None, Some(0)),
1947 source_node(11, Some(12), Some(1)),
1948 source_node(12, Some(99), None),
1949 ]),
1950 ..Document::default()
1951 };
1952 let cyclic_unprojected_parent = Document {
1953 skeleton: Skeleton {
1954 bones: vec![bone(None), bone(Some(0))],
1955 },
1956 assets: complete_projection(vec![
1957 source_node(11, Some(12), Some(1)),
1958 source_node(12, Some(12), None),
1959 ]),
1960 ..Document::default()
1961 };
1962 let cyclic_unprojected_parent_pair = Document {
1963 skeleton: Skeleton {
1964 bones: vec![bone(None), bone(Some(0))],
1965 },
1966 assets: complete_projection(vec![
1967 source_node(11, Some(12), Some(1)),
1968 source_node(12, Some(13), None),
1969 source_node(13, Some(12), None),
1970 ]),
1971 ..Document::default()
1972 };
1973 let mismatched_nearest_parent = Document {
1974 skeleton: Skeleton {
1975 bones: vec![bone(None), bone(Some(0))],
1976 },
1977 assets: complete_projection(vec![
1978 source_node(10, None, Some(0)),
1979 source_node(11, None, Some(1)),
1980 ]),
1981 ..Document::default()
1982 };
1983 let unprojected_child = Document {
1984 skeleton: Skeleton {
1985 bones: vec![bone(None), bone(Some(0))],
1986 },
1987 assets: complete_projection(vec![source_node(10, None, Some(0))]),
1988 ..Document::default()
1989 };
1990
1991 let duplicate_track = {
1992 let track = valid_track();
1993 let mut document = track_document(track.clone());
1994 document.clips[0].tracks.push(Track {
1995 property: Property::Scale,
1996 ..valid_track()
1997 });
1998 document.clips[0].tracks.push(track);
1999 document
2000 };
2001 let mut boundary_out_of_range_track = valid_track();
2002 boundary_out_of_range_track.bone = 1;
2003 let mut far_out_of_range_track = valid_track();
2004 far_out_of_range_track.bone = 99;
2005 let empty_track = Track {
2006 times: Vec::new(),
2007 values: TrackValues::Vec3s(Vec::new()),
2008 ..valid_track()
2009 };
2010 let non_finite_later_time = Track {
2011 times: vec![0.0, f32::NAN],
2012 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2013 ..valid_track()
2014 };
2015 let unordered_times = Track {
2016 times: vec![1.0, 0.0],
2017 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2018 ..valid_track()
2019 };
2020 let equal_times = Track {
2021 times: vec![0.0, 0.0],
2022 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2023 ..valid_track()
2024 };
2025 let wrong_linear_value_count = Track {
2026 values: TrackValues::Vec3s(Vec::new()),
2027 ..valid_track()
2028 };
2029 let excess_linear_value_count = Track {
2030 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2031 ..valid_track()
2032 };
2033 let wrong_step_value_count = Track {
2034 interpolation: Interpolation::Step,
2035 times: vec![0.0, 1.0],
2036 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2037 ..valid_track()
2038 };
2039 let excess_step_value_count = Track {
2040 interpolation: Interpolation::Step,
2041 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2042 ..valid_track()
2043 };
2044 let wrong_cubic_value_count = Track {
2045 interpolation: Interpolation::CubicSpline,
2046 times: vec![0.0, 1.0],
2047 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2048 ..valid_track()
2049 };
2050 let excess_cubic_value_count = Track {
2051 interpolation: Interpolation::CubicSpline,
2052 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2053 ..valid_track()
2054 };
2055 let wrong_translation_value_type = Track {
2056 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2057 ..valid_track()
2058 };
2059 let wrong_scale_value_type = Track {
2060 property: Property::Scale,
2061 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2062 ..valid_track()
2063 };
2064 let wrong_rotation_value_type = Track {
2065 property: Property::Rotation,
2066 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2067 ..valid_track()
2068 };
2069 let non_finite_value = Track {
2070 values: TrackValues::Vec3s(vec![Vec3::splat(f32::NAN)]),
2071 ..valid_track()
2072 };
2073
2074 let mut bad_instance_node = instance_document();
2075 bad_instance_node.assets.instances[0].node = 1;
2076 let mut far_instance_node = instance_document();
2077 far_instance_node.assets.instances[0].node = 99;
2078 let mut bad_instance_mesh = instance_document();
2079 bad_instance_mesh.assets.instances[0].mesh = 1;
2080 let mut far_instance_mesh = instance_document();
2081 far_instance_mesh.assets.instances[0].mesh = 99;
2082 let mut bad_instance_joint = instance_document();
2083 bad_instance_joint.assets.instances[0].skin_joints = vec![1];
2084 let mut far_instance_joint = instance_document();
2085 far_instance_joint.assets.instances[0].skin_joints = vec![99];
2086 let mut bad_instance_count = instance_document();
2087 bad_instance_count.assets.instances[0].skin_joints = vec![0];
2088 bad_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, Mat4::IDENTITY];
2089 let mut short_instance_count = instance_document();
2090 short_instance_count.skeleton.bones.push(bone(Some(0)));
2091 short_instance_count.assets.instances[0].skin_joints = vec![0, 1];
2092 short_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY];
2093 let mut bad_instance_ibm = instance_document();
2094 bad_instance_ibm.assets.instances[0].skin_joints = vec![0];
2095 bad_instance_ibm.assets.instances[0].skin_ibms =
2096 vec![Mat4::from_cols_array(&[f32::NAN; 16])];
2097 let mut bad_bone_ibm = one_bone_document();
2098 bad_bone_ibm.skeleton.bones[0].inverse_bind = Some(Mat4::from_cols_array(&[f32::NAN; 16]));
2099
2100 let cases = vec![
2101 (
2102 "non-finite rest",
2103 non_finite_rest,
2104 DocumentShapeError::NonFiniteSkeletonRest { node: 0 },
2105 ),
2106 (
2107 "non-finite composed rest world",
2108 overflowed_rest_world,
2109 DocumentShapeError::NonFiniteSkeletonRest { node: 1 },
2110 ),
2111 (
2112 "self parent",
2113 self_parent,
2114 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 0 },
2115 ),
2116 (
2117 "forward parent",
2118 forward_parent,
2119 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 1 },
2120 ),
2121 (
2122 "far parent",
2123 far_parent,
2124 DocumentShapeError::InvalidSkeletonParent {
2125 node: 0,
2126 parent: 99,
2127 },
2128 ),
2129 (
2130 "duplicate source node",
2131 duplicate_node,
2132 DocumentShapeError::DuplicateSourceNodeIndex {
2133 source_node_index: 9,
2134 },
2135 ),
2136 (
2137 "duplicate source skin",
2138 duplicate_skin,
2139 DocumentShapeError::DuplicateSourceSkinIndex {
2140 source_skin_index: 4,
2141 },
2142 ),
2143 (
2144 "projected bone range",
2145 out_of_range_projection,
2146 projection_error(10, SourceProjectionViolation::ProjectedBoneOutOfRange),
2147 ),
2148 (
2149 "projection injectivity",
2150 non_injective_projection,
2151 projection_error(
2152 11,
2153 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2154 ),
2155 ),
2156 (
2157 "missing projection parent",
2158 missing_projection_parent,
2159 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2160 ),
2161 (
2162 "missing projection parent at cycle bound",
2163 missing_projection_parent_at_cycle_bound,
2164 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2165 ),
2166 (
2167 "cyclic projection parent",
2168 cyclic_unprojected_parent,
2169 projection_error(
2170 11,
2171 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2172 ),
2173 ),
2174 (
2175 "cyclic projection parent pair",
2176 cyclic_unprojected_parent_pair,
2177 projection_error(
2178 11,
2179 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2180 ),
2181 ),
2182 (
2183 "nearest projection parent",
2184 mismatched_nearest_parent,
2185 projection_error(
2186 11,
2187 SourceProjectionViolation::NearestProjectedParentMismatch,
2188 ),
2189 ),
2190 (
2191 "projection downward closure",
2192 unprojected_child,
2193 projection_error(
2194 10,
2195 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2196 ),
2197 ),
2198 (
2199 "duplicate track",
2200 duplicate_track,
2201 DocumentShapeError::DuplicateClipTrack {
2202 clip_index: 0,
2203 node: 0,
2204 property: Property::Translation,
2205 },
2206 ),
2207 (
2208 "track bone range boundary",
2209 track_document(boundary_out_of_range_track),
2210 track_error(1, TrackShapeViolation::BoneIndexOutOfRange),
2211 ),
2212 (
2213 "track bone range far",
2214 track_document(far_out_of_range_track),
2215 track_error(99, TrackShapeViolation::BoneIndexOutOfRange),
2216 ),
2217 (
2218 "empty track",
2219 track_document(empty_track),
2220 track_error(0, TrackShapeViolation::EmptyTimes),
2221 ),
2222 (
2223 "non-finite time",
2224 track_document(non_finite_later_time),
2225 track_error(0, TrackShapeViolation::NonFiniteTime),
2226 ),
2227 (
2228 "unordered times",
2229 track_document(unordered_times),
2230 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2231 ),
2232 (
2233 "equal times",
2234 track_document(equal_times),
2235 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2236 ),
2237 (
2238 "linear value count",
2239 track_document(wrong_linear_value_count),
2240 track_error(0, TrackShapeViolation::ValueCountMismatch),
2241 ),
2242 (
2243 "linear excess value count",
2244 track_document(excess_linear_value_count),
2245 track_error(0, TrackShapeViolation::ValueCountMismatch),
2246 ),
2247 (
2248 "step value count",
2249 track_document(wrong_step_value_count),
2250 track_error(0, TrackShapeViolation::ValueCountMismatch),
2251 ),
2252 (
2253 "step excess value count",
2254 track_document(excess_step_value_count),
2255 track_error(0, TrackShapeViolation::ValueCountMismatch),
2256 ),
2257 (
2258 "cubic value count",
2259 track_document(wrong_cubic_value_count),
2260 track_error(0, TrackShapeViolation::ValueCountMismatch),
2261 ),
2262 (
2263 "cubic excess value count",
2264 track_document(excess_cubic_value_count),
2265 track_error(0, TrackShapeViolation::ValueCountMismatch),
2266 ),
2267 (
2268 "translation value type",
2269 track_document(wrong_translation_value_type),
2270 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2271 ),
2272 (
2273 "scale value type",
2274 track_document(wrong_scale_value_type),
2275 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2276 ),
2277 (
2278 "rotation value type",
2279 track_document(wrong_rotation_value_type),
2280 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2281 ),
2282 (
2283 "non-finite value",
2284 track_document(non_finite_value),
2285 track_error(0, TrackShapeViolation::NonFiniteValue),
2286 ),
2287 (
2288 "instance node boundary",
2289 bad_instance_node,
2290 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2291 ),
2292 (
2293 "instance node far",
2294 far_instance_node,
2295 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2296 ),
2297 (
2298 "instance mesh boundary",
2299 bad_instance_mesh,
2300 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2301 ),
2302 (
2303 "instance mesh far",
2304 far_instance_mesh,
2305 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2306 ),
2307 (
2308 "instance joint boundary",
2309 bad_instance_joint,
2310 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2311 ),
2312 (
2313 "instance joint far",
2314 far_instance_joint,
2315 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2316 ),
2317 (
2318 "instance ibm count excess",
2319 bad_instance_count,
2320 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2321 ),
2322 (
2323 "instance ibm count short",
2324 short_instance_count,
2325 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2326 ),
2327 (
2328 "instance ibm finite",
2329 bad_instance_ibm,
2330 instance_error(MeshInstanceShapeViolation::NonFiniteSkinInverseBind),
2331 ),
2332 (
2333 "bone ibm finite",
2334 bad_bone_ibm,
2335 DocumentShapeError::NonFiniteBoneInverseBind { node: 0 },
2336 ),
2337 ];
2338 for (name, document, expected) in cases {
2339 assert_eq!(validate_document_shape(&document), Err(expected), "{name}");
2340 }
2341 }
2342
2343 #[test]
2344 fn document_shape_finiteness_checks_every_stored_component() {
2345 for component in 0..3 {
2346 let mut translation = Vec3::ZERO.to_array();
2347 translation[component] = f32::NAN;
2348 let mut document = one_bone_document();
2349 document.skeleton.bones[0].rest.translation = Vec3::from_array(translation);
2350 assert_eq!(
2351 validate_document_shape(&document),
2352 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2353 "rest translation component {component}"
2354 );
2355
2356 let mut scale = Vec3::ONE.to_array();
2357 scale[component] = f32::NAN;
2358 let mut document = one_bone_document();
2359 document.skeleton.bones[0].rest.scale = Vec3::from_array(scale);
2360 assert_eq!(
2361 validate_document_shape(&document),
2362 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2363 "rest scale component {component}"
2364 );
2365
2366 let mut value = Vec3::ZERO.to_array();
2367 value[component] = f32::NAN;
2368 let document = track_document(Track {
2369 values: TrackValues::Vec3s(vec![Vec3::from_array(value)]),
2370 ..valid_track()
2371 });
2372 assert_eq!(
2373 validate_document_shape(&document),
2374 Err(DocumentShapeError::TrackShape {
2375 clip_index: 0,
2376 node: 0,
2377 violation: TrackShapeViolation::NonFiniteValue,
2378 }),
2379 "track Vec3 component {component}"
2380 );
2381 }
2382
2383 for component in 0..4 {
2384 let mut rotation = Quat::IDENTITY.to_array();
2385 rotation[component] = f32::NAN;
2386 let mut document = one_bone_document();
2387 document.skeleton.bones[0].rest.rotation = Quat::from_array(rotation);
2388 assert_eq!(
2389 validate_document_shape(&document),
2390 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2391 "rest rotation component {component}"
2392 );
2393
2394 let document = track_document(Track {
2395 property: Property::Rotation,
2396 values: TrackValues::Quats(vec![Quat::from_array(rotation)]),
2397 ..valid_track()
2398 });
2399 assert_eq!(
2400 validate_document_shape(&document),
2401 Err(DocumentShapeError::TrackShape {
2402 clip_index: 0,
2403 node: 0,
2404 violation: TrackShapeViolation::NonFiniteValue,
2405 }),
2406 "track quaternion component {component}"
2407 );
2408 }
2409
2410 for key in 0..3 {
2411 let mut times = vec![0.0, 1.0, 2.0];
2412 times[key] = f32::NAN;
2413 let document = track_document(Track {
2414 times,
2415 values: TrackValues::Vec3s(vec![Vec3::ZERO; 3]),
2416 ..valid_track()
2417 });
2418 assert_eq!(
2419 validate_document_shape(&document),
2420 Err(DocumentShapeError::TrackShape {
2421 clip_index: 0,
2422 node: 0,
2423 violation: TrackShapeViolation::NonFiniteTime,
2424 }),
2425 "track time {key}"
2426 );
2427 }
2428
2429 for component in 0..16 {
2430 let mut columns = Mat4::IDENTITY.to_cols_array();
2431 columns[component] = f32::NAN;
2432 let inverse_bind = Mat4::from_cols_array(&columns);
2433
2434 let mut instance_document = instance_document();
2435 instance_document.assets.instances[0].skin_joints = vec![0];
2436 instance_document.assets.instances[0].skin_ibms = vec![inverse_bind];
2437 assert_eq!(
2438 validate_document_shape(&instance_document),
2439 Err(DocumentShapeError::MeshInstanceShape {
2440 instance_index: 0,
2441 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2442 }),
2443 "instance inverse-bind component {component}"
2444 );
2445
2446 let mut bone_document = one_bone_document();
2447 bone_document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2448 assert_eq!(
2449 validate_document_shape(&bone_document),
2450 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2451 "bone inverse-bind component {component}"
2452 );
2453 }
2454 }
2455
2456 #[test]
2457 fn document_shape_rejects_duplicate_tracks_for_every_property() {
2458 let tracks = [
2459 (Property::Translation, TrackValues::Vec3s(vec![Vec3::ZERO])),
2460 (Property::Scale, TrackValues::Vec3s(vec![Vec3::ONE])),
2461 (Property::Rotation, TrackValues::Quats(vec![Quat::IDENTITY])),
2462 ];
2463
2464 for (property, values) in tracks {
2465 let track = Track {
2466 property,
2467 values,
2468 ..valid_track()
2469 };
2470 let mut document = track_document(track.clone());
2471 document.clips[0].tracks.push(track);
2472
2473 assert_eq!(
2474 validate_document_shape(&document),
2475 Err(DocumentShapeError::DuplicateClipTrack {
2476 clip_index: 0,
2477 node: 0,
2478 property,
2479 }),
2480 "duplicate {property:?} track"
2481 );
2482 }
2483 }
2484
2485 #[test]
2486 fn document_shape_rejects_infinite_times_quaternions_and_inverse_binds() {
2487 for non_finite in [f32::INFINITY, f32::NEG_INFINITY] {
2488 let document = track_document(Track {
2489 times: vec![non_finite],
2490 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2491 ..valid_track()
2492 });
2493 assert_eq!(
2494 validate_document_shape(&document),
2495 Err(DocumentShapeError::TrackShape {
2496 clip_index: 0,
2497 node: 0,
2498 violation: TrackShapeViolation::NonFiniteTime,
2499 }),
2500 "track time {non_finite}"
2501 );
2502
2503 let document = track_document(Track {
2504 property: Property::Rotation,
2505 values: TrackValues::Quats(vec![Quat::from_xyzw(non_finite, 0.0, 0.0, 1.0)]),
2506 ..valid_track()
2507 });
2508 assert_eq!(
2509 validate_document_shape(&document),
2510 Err(DocumentShapeError::TrackShape {
2511 clip_index: 0,
2512 node: 0,
2513 violation: TrackShapeViolation::NonFiniteValue,
2514 }),
2515 "track quaternion {non_finite}"
2516 );
2517
2518 let mut columns = Mat4::IDENTITY.to_cols_array();
2519 columns[0] = non_finite;
2520 let inverse_bind = Mat4::from_cols_array(&columns);
2521 let mut document = instance_document();
2522 document.assets.instances[0].skin_joints = vec![0];
2523 document.assets.instances[0].skin_ibms = vec![inverse_bind];
2524 assert_eq!(
2525 validate_document_shape(&document),
2526 Err(DocumentShapeError::MeshInstanceShape {
2527 instance_index: 0,
2528 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2529 }),
2530 "instance inverse bind {non_finite}"
2531 );
2532
2533 let mut document = one_bone_document();
2534 document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2535 assert_eq!(
2536 validate_document_shape(&document),
2537 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2538 "bone inverse bind {non_finite}"
2539 );
2540 }
2541 }
2542
2543 #[test]
2544 fn document_shape_checks_mesh_and_joint_references_on_later_instances() {
2545 let later_instance = MeshInstance {
2546 node: 0,
2547 mesh: 0,
2548 ..MeshInstance::default()
2549 };
2550
2551 let mut document = instance_document();
2552 document.assets.instances.push(later_instance.clone());
2553 document.assets.instances[1].mesh = 1;
2554 assert_eq!(
2555 validate_document_shape(&document),
2556 Err(DocumentShapeError::MeshInstanceShape {
2557 instance_index: 1,
2558 violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
2559 })
2560 );
2561
2562 let mut document = instance_document();
2563 document.assets.instances.push(later_instance);
2564 document.assets.instances[1].skin_joints = vec![1];
2565 assert_eq!(
2566 validate_document_shape(&document),
2567 Err(DocumentShapeError::MeshInstanceShape {
2568 instance_index: 1,
2569 violation: MeshInstanceShapeViolation::SkinJointOutOfRange,
2570 })
2571 );
2572 }
2573
2574 #[test]
2575 fn document_shape_finds_duplicates_that_do_not_involve_the_first_item() {
2576 let mut document = Document::default();
2577 document.assets.source_skeleton.skins = [4, 5, 5]
2578 .into_iter()
2579 .map(|source_skin_index| SourceSkinAsset {
2580 source_skin_index,
2581 ..SourceSkinAsset::default()
2582 })
2583 .collect();
2584 assert_eq!(
2585 validate_document_shape(&document),
2586 Err(DocumentShapeError::DuplicateSourceSkinIndex {
2587 source_skin_index: 5,
2588 })
2589 );
2590
2591 let scale_track = Track {
2592 property: Property::Scale,
2593 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2594 ..valid_track()
2595 };
2596 let mut document = track_document(valid_track());
2597 document.clips[0].tracks.push(scale_track.clone());
2598 document.clips[0].tracks.push(scale_track);
2599 assert_eq!(
2600 validate_document_shape(&document),
2601 Err(DocumentShapeError::DuplicateClipTrack {
2602 clip_index: 0,
2603 node: 0,
2604 property: Property::Scale,
2605 })
2606 );
2607 }
2608
2609 #[test]
2610 fn document_shape_checks_later_tracks_and_inverse_binds() {
2611 let mut document = track_document(valid_track());
2612 document.clips[0].tracks.push(Track {
2613 property: Property::Scale,
2614 times: Vec::new(),
2615 values: TrackValues::Vec3s(Vec::new()),
2616 ..valid_track()
2617 });
2618 assert_eq!(
2619 validate_document_shape(&document),
2620 Err(DocumentShapeError::TrackShape {
2621 clip_index: 0,
2622 node: 0,
2623 violation: TrackShapeViolation::EmptyTimes,
2624 })
2625 );
2626
2627 let scale_track = Track {
2628 property: Property::Scale,
2629 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2630 ..valid_track()
2631 };
2632 let mut document = track_document(valid_track());
2633 document.clips.push(Clip {
2634 name: "later".into(),
2635 duration_s: 0.0,
2636 tracks: vec![scale_track.clone(), scale_track],
2637 });
2638 assert_eq!(
2639 validate_document_shape(&document),
2640 Err(DocumentShapeError::DuplicateClipTrack {
2641 clip_index: 1,
2642 node: 0,
2643 property: Property::Scale,
2644 })
2645 );
2646
2647 let mut document = track_document(valid_track());
2648 document.clips.push(Clip {
2649 name: "later".into(),
2650 duration_s: 0.0,
2651 tracks: vec![Track {
2652 property: Property::Scale,
2653 times: Vec::new(),
2654 values: TrackValues::Vec3s(Vec::new()),
2655 ..valid_track()
2656 }],
2657 });
2658 assert_eq!(
2659 validate_document_shape(&document),
2660 Err(DocumentShapeError::TrackShape {
2661 clip_index: 1,
2662 node: 0,
2663 violation: TrackShapeViolation::EmptyTimes,
2664 })
2665 );
2666
2667 let mut columns = Mat4::IDENTITY.to_cols_array();
2668 columns[15] = f32::NAN;
2669 let non_finite_inverse_bind = Mat4::from_cols_array(&columns);
2670
2671 let mut document = instance_document();
2672 document.skeleton.bones.push(bone(Some(0)));
2673 document.assets.instances[0].skin_joints = vec![0, 1];
2674 document.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, non_finite_inverse_bind];
2675 assert_eq!(
2676 validate_document_shape(&document),
2677 Err(DocumentShapeError::MeshInstanceShape {
2678 instance_index: 0,
2679 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2680 })
2681 );
2682
2683 let mut document = instance_document();
2684 document.assets.instances.push(MeshInstance {
2685 node: 0,
2686 mesh: 0,
2687 skin_joints: vec![0],
2688 skin_ibms: vec![non_finite_inverse_bind],
2689 ..MeshInstance::default()
2690 });
2691 assert_eq!(
2692 validate_document_shape(&document),
2693 Err(DocumentShapeError::MeshInstanceShape {
2694 instance_index: 1,
2695 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2696 })
2697 );
2698
2699 let mut document = instance_document();
2700 document.assets.instances.push(MeshInstance {
2701 node: 0,
2702 mesh: 0,
2703 skin_joints: vec![0],
2704 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
2705 ..MeshInstance::default()
2706 });
2707 assert_eq!(
2708 validate_document_shape(&document),
2709 Err(DocumentShapeError::MeshInstanceShape {
2710 instance_index: 1,
2711 violation: MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2712 })
2713 );
2714
2715 let mut document = one_bone_document();
2716 document.skeleton.bones.push(Bone {
2717 inverse_bind: Some(non_finite_inverse_bind),
2718 ..bone(Some(0))
2719 });
2720 assert_eq!(
2721 validate_document_shape(&document),
2722 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 1 })
2723 );
2724 }
2725
2726 #[test]
2727 fn document_shape_violation_names_remain_machine_stable() {
2728 let source_projection = [
2729 (
2730 SourceProjectionViolation::ProjectedBoneOutOfRange,
2731 "projected_bone_out_of_range",
2732 ),
2733 (
2734 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2735 "two_source_nodes_project_to_one_bone",
2736 ),
2737 (
2738 SourceProjectionViolation::ParentSourceNodeMissing,
2739 "parent_source_node_is_missing",
2740 ),
2741 (
2742 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2743 "cyclic_unprojected_source_parent_chain",
2744 ),
2745 (
2746 SourceProjectionViolation::NearestProjectedParentMismatch,
2747 "projection_and_skeleton_parents_differ",
2748 ),
2749 (
2750 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2751 "projected_bone_has_an_unprojected_skeleton_child",
2752 ),
2753 ];
2754 for (violation, expected) in source_projection {
2755 assert_eq!(violation.to_string(), expected);
2756 }
2757
2758 let track = [
2759 (
2760 TrackShapeViolation::BoneIndexOutOfRange,
2761 "bone_index_out_of_range",
2762 ),
2763 (TrackShapeViolation::EmptyTimes, "empty_times"),
2764 (TrackShapeViolation::NonFiniteTime, "non_finite_time"),
2765 (
2766 TrackShapeViolation::TimesNotStrictlyIncreasing,
2767 "times_not_strictly_increasing",
2768 ),
2769 (
2770 TrackShapeViolation::ValueCountMismatch,
2771 "value_count_mismatch",
2772 ),
2773 (
2774 TrackShapeViolation::ValueTypeMismatchesProperty,
2775 "value_type_mismatches_property",
2776 ),
2777 (TrackShapeViolation::NonFiniteValue, "non_finite_value"),
2778 ];
2779 for (violation, expected) in track {
2780 assert_eq!(violation.to_string(), expected);
2781 }
2782
2783 let instance = [
2784 (
2785 MeshInstanceShapeViolation::NodeIndexOutOfRange,
2786 "node_index_out_of_range",
2787 ),
2788 (
2789 MeshInstanceShapeViolation::MeshIndexOutOfRange,
2790 "mesh_index_out_of_range",
2791 ),
2792 (
2793 MeshInstanceShapeViolation::SkinJointOutOfRange,
2794 "skin_joint_out_of_range",
2795 ),
2796 (
2797 MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2798 "skin_ibm_count_mismatch",
2799 ),
2800 (
2801 MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2802 "non_finite_inverse_bind",
2803 ),
2804 ];
2805 for (violation, expected) in instance {
2806 assert_eq!(violation.to_string(), expected);
2807 }
2808 }
2809
2810 #[test]
2811 fn tolerant_world_rests_keep_unrelated_partial_evidence() {
2812 let skeleton = Skeleton {
2813 bones: vec![
2814 bone(None),
2815 bone(Some(99)),
2816 Bone {
2817 rest: Transform {
2818 translation: Vec3::X,
2819 ..Transform::IDENTITY
2820 },
2821 ..bone(None)
2822 },
2823 Bone {
2824 rest: Transform {
2825 translation: Vec3::Y,
2826 ..Transform::IDENTITY
2827 },
2828 ..bone(Some(2))
2829 },
2830 bone(Some(1)),
2831 ],
2832 };
2833
2834 let worlds = tolerant_world_rest_matrices(&skeleton);
2835 assert_eq!(worlds.len(), 5);
2836 assert_eq!(worlds[0], Some(Mat4::IDENTITY));
2837 assert_eq!(worlds[1], None, "the malformed parent is unavailable");
2838 assert_eq!(worlds[2], Some(Mat4::from_translation(Vec3::X)));
2839 assert_eq!(
2840 worlds[3],
2841 Some(Mat4::from_translation(Vec3::new(1.0, 1.0, 0.0))),
2842 "a finite independent chain remains measurable"
2843 );
2844 assert_eq!(
2845 worlds[4], None,
2846 "a child of unavailable evidence is unavailable"
2847 );
2848 }
2849
2850 #[test]
2851 fn shared_affine_classifier_respects_distinct_caller_tolerances() {
2852 let equal_axis_basis = affine_test_fixtures::tolerance_divergence_basis();
2853 let strict = PositiveUniformAffineTolerance {
2854 equal_axis: 1.0e-5,
2855 relative_orthogonality: 1.0e-5,
2856 singular_determinant_relative: 1.0e-6,
2857 };
2858 let loose = PositiveUniformAffineTolerance {
2859 equal_axis: 1.0e-4,
2860 relative_orthogonality: 1.0e-4,
2861 singular_determinant_relative: 0.0,
2862 };
2863
2864 assert_eq!(
2865 classify_positive_uniform_affine(equal_axis_basis, strict),
2866 Err(AffineDomainViolation::NonUniformScale),
2867 "the stricter caller rejects this equal-axis difference"
2868 );
2869 assert!(
2870 classify_positive_uniform_affine(equal_axis_basis, loose).is_ok(),
2871 "the looser caller accepts this equal-axis difference"
2872 );
2873
2874 let orthogonality_basis = affine_test_fixtures::orthogonality_tolerance_divergence_basis();
2875 assert_eq!(
2876 classify_positive_uniform_affine(orthogonality_basis, strict),
2877 Err(AffineDomainViolation::Sheared),
2878 "the stricter caller rejects this cross-axis dot product"
2879 );
2880 assert!(
2881 classify_positive_uniform_affine(orthogonality_basis, loose).is_ok(),
2882 "the looser caller accepts this cross-axis dot product"
2883 );
2884 }
2885
2886 #[test]
2887 fn shared_affine_classifier_pins_its_symmetric_f64_formula() {
2888 let policy = PositiveUniformAffineTolerance {
2889 equal_axis: 1.0e-5,
2890 relative_orthogonality: 1.0e-5,
2891 singular_determinant_relative: 1.0e-6,
2892 };
2893
2894 let on_long_edge = Mat3::from_diagonal(Vec3::new(99_998.5, 99_998.5, 100_000.0));
2899 assert_eq!(
2900 classify_positive_uniform_affine(on_long_edge, policy),
2901 Ok(99_999.0)
2902 );
2903 let short = 99_998.5;
2904 let long = 100_000.0 + 0.007_812_5;
2905 for diagonal in [
2906 Vec3::new(long, short, short),
2907 Vec3::new(short, long, short),
2908 Vec3::new(short, short, long),
2909 ] {
2910 assert_eq!(
2911 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2912 Err(AffineDomainViolation::NonUniformScale)
2913 );
2914 }
2915
2916 let short = 1.0 - 2.0_f32.powi(-16);
2920 for diagonal in [
2921 Vec3::new(short, 1.0, 1.0),
2922 Vec3::new(1.0, short, 1.0),
2923 Vec3::new(1.0, 1.0, short),
2924 ] {
2925 assert_eq!(
2926 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2927 Err(AffineDomainViolation::NonUniformScale)
2928 );
2929 }
2930
2931 let c0 = Vec3::new(0.12792248, -0.99066633, -0.047073245);
2934 let c1 = Vec3::new(-0.34637994, -0.00016034879, -0.93809813);
2935 let c2 = Vec3::new(0.92933476, 0.13630849, -0.3431568);
2936 assert!((c1.dot(c2) as f64).abs() < 1.0e-5);
2937 assert!(c1.as_dvec3().dot(c2.as_dvec3()).abs() > 1.0e-5);
2938 assert_eq!(
2939 classify_positive_uniform_affine(Mat3::from_cols(c0, c1, c2), policy),
2940 Err(AffineDomainViolation::Sheared)
2941 );
2942
2943 for shear in [2.0_f32.powi(-15), -2.0_f32.powi(-15)] {
2946 let basis = Mat3::from_cols(Vec3::X, Vec3::new(shear, 1.0, 0.0), Vec3::Z);
2947 assert_eq!(
2948 classify_positive_uniform_affine(basis, policy),
2949 Err(AffineDomainViolation::Sheared)
2950 );
2951 }
2952 }
2953
2954 #[test]
2955 fn affine_axis_mean_is_ascending_and_column_order_invariant() {
2956 let lengths = [
2961 f64::from_bits(0x3ff1_09e7_e000_022c),
2962 f64::from_bits(0x3ff1_09ec_6000_0eb5),
2963 f64::from_bits(0x3ff1_09fa_e000_3cde),
2964 ];
2965 let expected = f64::from_bits(0x3ff1_09ef_b555_6f3f);
2966 let ascending = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
2967 let descending = (lengths[2] + lengths[1] + lengths[0]) / 3.0;
2968 assert_eq!(expected.to_bits(), 0x3ff1_09ef_b555_6f3f);
2969 assert_eq!(ascending.to_bits(), expected.to_bits());
2970 assert_eq!(descending.to_bits(), 0x3ff1_09ef_b555_6f40);
2971 for order in [
2972 [0, 1, 2],
2973 [0, 2, 1],
2974 [1, 0, 2],
2975 [1, 2, 0],
2976 [2, 0, 1],
2977 [2, 1, 0],
2978 ] {
2979 assert_eq!(
2980 average_affine_axis_length(order.map(|index| lengths[index])),
2981 expected,
2982 "axis order {order:?}"
2983 );
2984 }
2985
2986 let dyadic = [2.0_f64.powi(53), 1.0, 1.0];
2991 let ascending = (dyadic[1] + dyadic[2] + dyadic[0]) / 3.0;
2992 let descending = (dyadic[0] + dyadic[1] + dyadic[2]) / 3.0;
2993 assert_ne!(ascending, descending);
2994 assert_eq!(average_affine_axis_length(dyadic), ascending);
2995
2996 let permutations = affine_test_fixtures::appendix_d_v6_mean_permutations();
3002 let expected_length_bits = lengths.map(f64::to_bits);
3003 assert_eq!(
3004 affine_axis_lengths(permutations[0]).map(f64::to_bits),
3005 expected_length_bits
3006 );
3007 let tolerance = PositiveUniformAffineTolerance {
3008 equal_axis: 1.0e-5,
3009 relative_orthogonality: 1.0e-5,
3010 singular_determinant_relative: 1.0e-6,
3011 };
3012 for (permutation, linear) in permutations.into_iter().enumerate() {
3013 assert!(
3014 linear
3015 .x_axis
3016 .as_dvec3()
3017 .cross(linear.y_axis.as_dvec3())
3018 .dot(linear.z_axis.as_dvec3())
3019 > 0.0,
3020 "orientation for permutation {permutation}"
3021 );
3022 assert_eq!(
3023 average_affine_axis_length(affine_axis_lengths(linear)).to_bits(),
3024 expected.to_bits(),
3025 "mean for permutation {permutation}"
3026 );
3027 assert_eq!(
3028 classify_positive_uniform_affine(linear, tolerance),
3029 Err(AffineDomainViolation::NonUniformScale),
3030 "classification for permutation {permutation}"
3031 );
3032 }
3033 }
3034
3035 #[test]
3036 fn shared_affine_classifier_pins_f64_determinant_arithmetic() {
3037 let linear = Mat3::from_cols(
3042 Vec3::new(
3043 f32::from_bits(0x3ff3_5574),
3044 f32::from_bits(0x3f0e_fa3c),
3045 0.0,
3046 ),
3047 Vec3::new(
3048 f32::from_bits(0x3ff5_5e17),
3049 f32::from_bits(0x3f10_2c31),
3050 0.0,
3051 ),
3052 Vec3::Z,
3053 );
3054 let columns = [
3055 linear.x_axis.as_dvec3(),
3056 linear.y_axis.as_dvec3(),
3057 linear.z_axis.as_dvec3(),
3058 ];
3059 let determinant_f64 = columns[2].dot(columns[0].cross(columns[1]));
3060 let determinant_f32 = f64::from(linear.determinant());
3061 let lengths = affine_axis_lengths(linear);
3062 let threshold = (determinant_f64 + determinant_f32) / 2.0;
3063 assert!(determinant_f64 < threshold);
3064 assert!(determinant_f32 > threshold);
3065
3066 assert_eq!(
3067 classify_positive_uniform_affine(
3068 linear,
3069 PositiveUniformAffineTolerance {
3070 equal_axis: 10.0,
3071 relative_orthogonality: 10.0,
3072 singular_determinant_relative: threshold
3073 / (lengths[0] * lengths[1] * lengths[2]),
3074 },
3075 ),
3076 Err(AffineDomainViolation::Singular)
3077 );
3078
3079 let large_uniform = 2.0e19_f32;
3084 assert_eq!(
3085 classify_positive_uniform_affine(
3086 Mat3::from_diagonal(Vec3::splat(large_uniform)),
3087 PositiveUniformAffineTolerance {
3088 equal_axis: 1.0e-5,
3089 relative_orthogonality: 1.0e-5,
3090 singular_determinant_relative: 1.0e-6,
3091 },
3092 ),
3093 Ok(f64::from(large_uniform))
3094 );
3095 }
3096
3097 #[test]
3098 fn affine_geometry_facts_pin_every_widened_field_and_slot() {
3099 let linear = Mat3::from_cols(
3100 Vec3::new(1.0, 2.0, 3.0),
3101 Vec3::new(4.0, 5.0, 6.0),
3102 Vec3::new(7.0, 8.0, 10.0),
3103 );
3104
3105 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3106 assert_eq!(
3107 facts.axis_lengths.map(f64::to_bits),
3108 [
3109 0x400d_eeea_1168_3f49,
3110 0x4021_8cc8_21d6_d3e3,
3111 0x402d_3064_dcc8_ae67,
3112 ]
3113 );
3114 assert_eq!(facts.mean_axis_length.to_bits(), 0x4022_12f7_d653_30b4);
3115 assert_eq!(facts.determinant.to_bits(), 0xc008_0000_0000_0000);
3116 assert_eq!(facts.axis_length_product.to_bits(), 0x407d_f2e3_88f2_1b01);
3117 assert_eq!(
3118 facts.cross_axis_dots.map(f64::to_bits),
3119 [
3120 0x4040_0000_0000_0000,
3121 0x404a_8000_0000_0000,
3122 0x4060_0000_0000_0000,
3123 ],
3124 "cross-axis slots are XY, XZ, YZ"
3125 );
3126 }
3127
3128 #[test]
3129 fn affine_geometry_facts_widen_every_dot_product_before_multiplying() {
3130 let x = Vec3::new(
3131 f32::from_bits(0x3ff3_5574),
3132 f32::from_bits(0x3f0e_fa3c),
3133 0.0,
3134 );
3135 let y = Vec3::new(
3136 f32::from_bits(0x3ff5_5e17),
3137 f32::from_bits(0x3f10_2c31),
3138 0.0,
3139 );
3140 let widened_dot = x.as_dvec3().dot(y.as_dvec3());
3141 let f32_then_widened = f64::from(x.dot(y));
3142
3143 for (slot, linear) in [
3144 (0, Mat3::from_cols(x, y, Vec3::Z)),
3145 (1, Mat3::from_cols(x, Vec3::Z, y)),
3146 (2, Mat3::from_cols(Vec3::Z, x, y)),
3147 ] {
3148 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3149 assert_eq!(facts.cross_axis_dots[slot], widened_dot);
3150 assert_ne!(
3151 facts.cross_axis_dots[slot], f32_then_widened,
3152 "dot slot {slot} must multiply and add in f64, not widen an f32 result"
3153 );
3154 }
3155 }
3156
3157 #[test]
3158 fn weld_preserves_uv_seams_at_shared_positions() {
3159 let mut primitive = Primitive {
3160 positions: vec![Vec3::ZERO, Vec3::ZERO, Vec3::ZERO],
3161 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0]],
3162 ..Primitive::default()
3163 };
3164
3165 primitive.weld();
3166
3167 assert_eq!(primitive.positions.len(), 2);
3168 let reconstructed_corners = primitive
3169 .indices
3170 .iter()
3171 .map(|&index| {
3172 let index = index as usize;
3173 (primitive.positions[index], primitive.uvs[index])
3174 })
3175 .collect::<Vec<_>>();
3176 assert_eq!(
3177 reconstructed_corners,
3178 vec![
3179 (Vec3::ZERO, [0.0, 0.0]),
3180 (Vec3::ZERO, [1.0, 0.0]),
3181 (Vec3::ZERO, [0.0, 0.0]),
3182 ]
3183 );
3184 }
3185}