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)]
399pub enum Property {
400 Translation,
402 Rotation,
404 Scale,
406}
407
408impl Property {
409 pub fn as_str(self) -> &'static str {
412 match self {
413 Property::Translation => "translation",
414 Property::Rotation => "rotation",
415 Property::Scale => "scale",
416 }
417 }
418}
419
420#[derive(Debug, Clone, Copy, PartialEq, Eq)]
422pub enum Interpolation {
423 Linear,
425 Step,
427 CubicSpline,
431}
432
433#[derive(Debug, Clone)]
435pub enum TrackValues {
436 Vec3s(Vec<Vec3>),
438 Quats(Vec<Quat>),
440}
441
442impl TrackValues {
443 pub fn len(&self) -> usize {
446 match self {
447 TrackValues::Vec3s(v) => v.len(),
448 TrackValues::Quats(v) => v.len(),
449 }
450 }
451
452 pub fn is_empty(&self) -> bool {
454 self.len() == 0
455 }
456}
457
458#[derive(Debug, Clone)]
460pub struct Track {
461 pub bone: BoneId,
463 pub property: Property,
465 pub interpolation: Interpolation,
467 pub times: Vec<f32>,
470 pub values: TrackValues,
472}
473
474impl Track {
475 pub fn key_count(&self) -> usize {
477 self.times.len()
478 }
479
480 pub fn value_index(&self, k: usize) -> usize {
483 match self.interpolation {
484 Interpolation::CubicSpline => 3 * k + 1,
485 _ => k,
486 }
487 }
488
489 pub fn key_vec3(&self, k: usize) -> Option<Vec3> {
491 match &self.values {
492 TrackValues::Vec3s(v) => v.get(self.value_index(k)).copied(),
493 TrackValues::Quats(_) => None,
494 }
495 }
496
497 pub fn key_quat(&self, k: usize) -> Option<Quat> {
499 match &self.values {
500 TrackValues::Quats(v) => v.get(self.value_index(k)).copied(),
501 TrackValues::Vec3s(_) => None,
502 }
503 }
504
505 pub fn start_time(&self) -> f32 {
507 self.times.first().copied().unwrap_or(0.0)
508 }
509
510 pub fn end_time(&self) -> f32 {
512 self.times.last().copied().unwrap_or(0.0)
513 }
514}
515
516#[derive(Debug, Clone)]
518pub struct Clip {
519 pub name: String,
522 pub duration_s: f64,
524 pub tracks: Vec<Track>,
526}
527
528#[derive(Debug, Clone, Default)]
530pub struct SourceInfo {
531 pub path: Option<String>,
533 pub format: Option<String>,
535}
536
537#[derive(Debug, Clone, Default)]
544pub struct Document {
545 pub skeleton: Skeleton,
547 pub clips: Vec<Clip>,
549 pub assets: SceneAssets,
551 pub source: SourceInfo,
553}
554
555#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
561#[non_exhaustive]
562pub enum DocumentShapeError {
563 #[error("node {node} has a non-finite rest transform")]
566 NonFiniteSkeletonRest {
567 node: BoneId,
569 },
570 #[error("node {node} has invalid parent {parent}")]
572 InvalidSkeletonParent {
573 node: BoneId,
575 parent: BoneId,
577 },
578 #[error("source skeleton declares duplicate source node index {source_node_index}")]
580 DuplicateSourceNodeIndex {
581 source_node_index: usize,
583 },
584 #[error("source skeleton declares duplicate source skin index {source_skin_index}")]
586 DuplicateSourceSkinIndex {
587 source_skin_index: usize,
589 },
590 #[error(
592 "source node {source_node_index} contradicts the document skeleton's parent chain ({violation})"
593 )]
594 SourceProjection {
595 source_node_index: usize,
597 violation: SourceProjectionViolation,
599 },
600 #[error("clip {clip_index} declares duplicate {property:?} tracks for node {node}")]
602 DuplicateClipTrack {
603 clip_index: usize,
605 node: BoneId,
607 property: Property,
609 },
610 #[error("clip {clip_index} track for node {node} has an invalid shape ({violation})")]
612 TrackShape {
613 clip_index: usize,
615 node: BoneId,
617 violation: TrackShapeViolation,
619 },
620 #[error("mesh instance {instance_index} is invalid ({violation})")]
622 MeshInstanceShape {
623 instance_index: usize,
625 violation: MeshInstanceShapeViolation,
627 },
628 #[error("node {node} has a non-finite inverse-bind matrix")]
630 NonFiniteBoneInverseBind {
631 node: BoneId,
633 },
634}
635
636#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
638#[non_exhaustive]
639pub enum SourceProjectionViolation {
640 #[error("projected_bone_out_of_range")]
642 ProjectedBoneOutOfRange,
643 #[error("two_source_nodes_project_to_one_bone")]
645 TwoSourceNodesProjectToOneBone,
646 #[error("parent_source_node_is_missing")]
648 ParentSourceNodeMissing,
649 #[error("cyclic_unprojected_source_parent_chain")]
651 CyclicUnprojectedSourceParentChain,
652 #[error("projection_and_skeleton_parents_differ")]
654 NearestProjectedParentMismatch,
655 #[error("projected_bone_has_an_unprojected_skeleton_child")]
657 ProjectedBoneHasUnprojectedSkeletonChild,
658}
659
660#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
662#[non_exhaustive]
663pub enum TrackShapeViolation {
664 #[error("bone_index_out_of_range")]
666 BoneIndexOutOfRange,
667 #[error("empty_times")]
669 EmptyTimes,
670 #[error("non_finite_time")]
672 NonFiniteTime,
673 #[error("times_not_strictly_increasing")]
675 TimesNotStrictlyIncreasing,
676 #[error("value_count_mismatch")]
678 ValueCountMismatch,
679 #[error("value_type_mismatches_property")]
681 ValueTypeMismatchesProperty,
682 #[error("non_finite_value")]
684 NonFiniteValue,
685}
686
687#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
689#[non_exhaustive]
690pub enum MeshInstanceShapeViolation {
691 #[error("node_index_out_of_range")]
693 NodeIndexOutOfRange,
694 #[error("mesh_index_out_of_range")]
696 MeshIndexOutOfRange,
697 #[error("skin_joint_out_of_range")]
699 SkinJointOutOfRange,
700 #[error("skin_ibm_count_mismatch")]
702 SkinInverseBindCountMismatch,
703 #[error("non_finite_inverse_bind")]
705 NonFiniteSkinInverseBind,
706}
707
708#[derive(Debug, Clone, Default)]
724pub struct Primitive {
725 pub material: Option<usize>,
727 pub indices: Vec<u32>,
729 pub positions: Vec<Vec3>,
731 pub normals: Vec<Vec3>,
733 pub uvs: Vec<[f32; 2]>,
735 pub joints: Vec<[u16; 4]>,
737 pub weights: Vec<[f32; 4]>,
739 pub additional_influence_sets: Vec<AdditionalInfluenceSet>,
745}
746
747#[derive(Debug, Clone, Copy, PartialEq, Eq)]
754pub struct AdditionalInfluenceSet {
755 pub set_index: u32,
757 pub joints_present: bool,
759 pub weights_present: bool,
761}
762
763#[derive(Debug, Clone, Default)]
765pub struct MeshAsset {
766 pub name: String,
768 pub source_mesh_index: usize,
773 pub primitives: Vec<Primitive>,
775}
776
777#[derive(Debug, Clone, Default)]
779pub struct MeshInstance {
780 pub source_node_index: usize,
782 pub node: BoneId,
784 pub mesh: usize,
786 pub skin_joints: Vec<BoneId>,
788 pub skin_ibms: Vec<Mat4>,
793}
794
795#[derive(Debug, Clone, Default)]
797pub struct SceneAsset {
798 pub source_scene_index: usize,
800 pub name: Option<String>,
802 pub roots: Vec<BoneId>,
804}
805
806#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
813#[serde(rename_all = "snake_case")]
814pub enum SourceSkeletonCoverage {
815 #[default]
817 Unavailable,
818 Complete,
822}
823
824#[derive(Debug, Clone)]
836pub enum SourceNodeLocalRest {
837 Trs {
839 translation: Vec3,
841 rotation: Quat,
843 scale: Vec3,
845 },
846 Matrix(Mat4),
848}
849
850#[derive(Debug, Clone)]
864#[non_exhaustive]
865pub struct SourceNodeAsset {
866 pub source_node_index: usize,
868 pub name: Option<String>,
870 pub parent_source_node_index: Option<usize>,
872 pub scene_root_indices: Vec<usize>,
874 pub local_rest: SourceNodeLocalRest,
876 pub bone: Option<BoneId>,
896}
897
898impl SourceNodeAsset {
899 pub fn new(source_node_index: usize, local_rest: SourceNodeLocalRest) -> Self {
907 Self {
908 source_node_index,
909 name: None,
910 parent_source_node_index: None,
911 scene_root_indices: Vec::new(),
912 local_rest,
913 bone: None,
914 }
915 }
916}
917
918#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
926#[serde(rename_all = "snake_case")]
927pub enum SourceInverseBindAccessorStatus {
928 #[default]
930 Absent,
931 Available,
933 EmptyAccessor,
935 CountMismatch,
937 Unreadable,
939}
940
941#[derive(Debug, Clone, Default)]
943pub struct SourceInverseBindAccessor {
944 pub status: SourceInverseBindAccessorStatus,
946 pub declared_count: Option<usize>,
948 pub matrices: Vec<Mat4>,
957}
958
959#[derive(Debug, Clone)]
961pub struct SourceSkinAttachment {
962 pub source_node_index: usize,
964 pub source_mesh_index: Option<usize>,
969}
970
971#[derive(Debug, Clone, Default)]
973pub struct SourceSkinAsset {
974 pub source_skin_index: usize,
976 pub name: Option<String>,
978 pub skeleton_root_source_node_index: Option<usize>,
980 pub joint_source_node_indices: Vec<usize>,
982 pub inverse_bind_accessor: SourceInverseBindAccessor,
988 pub attachments: Vec<SourceSkinAttachment>,
990}
991
992#[derive(Debug, Clone, Default)]
994pub struct SourceSkeletonAssets {
995 pub coverage: SourceSkeletonCoverage,
997 pub nodes: Vec<SourceNodeAsset>,
999 pub skins: Vec<SourceSkinAsset>,
1001}
1002
1003#[derive(Debug, Clone)]
1006pub struct TextureAsset {
1007 pub bytes: Vec<u8>,
1009 pub mime: String,
1011}
1012
1013#[derive(Debug, Clone)]
1018pub struct NormalTextureAsset {
1019 pub texture: TextureAsset,
1021 pub scale: f32,
1023}
1024
1025#[derive(Debug, Clone)]
1031pub struct OcclusionTextureAsset {
1032 pub texture: TextureAsset,
1034 pub strength: f32,
1036}
1037
1038#[derive(Debug, Clone)]
1040pub struct MaterialAsset {
1041 pub name: String,
1043 pub base_color: [f32; 4],
1046 pub metallic: f32,
1048 pub roughness: f32,
1050 pub base_color_texture: Option<TextureAsset>,
1052 pub normal_texture: Option<NormalTextureAsset>,
1054 pub metallic_roughness_texture: Option<TextureAsset>,
1058 pub occlusion_texture: Option<OcclusionTextureAsset>,
1060}
1061
1062#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
1068#[serde(rename_all = "snake_case")]
1069pub enum MaterialResourceCoverage {
1070 Complete,
1074 #[default]
1076 Unavailable,
1077}
1078
1079#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
1084#[serde(rename_all = "snake_case")]
1085pub enum MaterialTextureSlot {
1086 BaseColor,
1088 Normal,
1090 MetallicRoughness,
1092 Occlusion,
1094 Emissive,
1096}
1097
1098#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1100pub struct SourceMaterialTextureBinding {
1101 pub slot: MaterialTextureSlot,
1103 pub texture_index: usize,
1105}
1106
1107#[derive(Debug, Clone, Default)]
1109pub struct SourceMaterialAsset {
1110 pub material_index: usize,
1112 pub name: Option<String>,
1114 pub texture_bindings: Vec<SourceMaterialTextureBinding>,
1116}
1117
1118#[derive(Debug, Clone, Default)]
1120pub struct SourceTextureAsset {
1121 pub texture_index: usize,
1123 pub name: Option<String>,
1125 pub image_index: usize,
1127}
1128
1129#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1131#[serde(rename_all = "snake_case")]
1132pub enum ImageSourceKind {
1133 Embedded,
1135 DataUri,
1137 External,
1139}
1140
1141#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1143#[serde(rename_all = "snake_case")]
1144pub enum ImageContainerFormat {
1145 Png,
1147 Jpeg,
1149}
1150
1151#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1153#[serde(rename_all = "snake_case")]
1154pub enum DecodedImageColorType {
1155 L8,
1157 La8,
1159 Rgb8,
1161 Rgba8,
1163 L16,
1165 La16,
1167 Rgb16,
1169 Rgba16,
1171}
1172
1173#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
1175#[serde(rename_all = "snake_case")]
1176pub enum ImageUnavailableReason {
1177 SourceUnavailable,
1179 InvalidDataUri,
1181 UnsupportedContainer,
1183 DecodeFailed,
1185 ResourceLimit,
1187}
1188
1189#[derive(Debug, Clone, Copy, PartialEq, Eq)]
1191pub enum SourceImageInspection {
1192 Available {
1194 width: u32,
1196 height: u32,
1198 channel_count: u8,
1200 color_type: DecodedImageColorType,
1202 },
1203 Unavailable {
1205 reason: ImageUnavailableReason,
1207 },
1208}
1209
1210#[derive(Debug, Clone)]
1212pub struct SourceImageAsset {
1213 pub image_index: usize,
1215 pub name: Option<String>,
1217 pub source_kind: ImageSourceKind,
1219 pub declared_mime_type: Option<String>,
1221 pub detected_container: Option<ImageContainerFormat>,
1223 pub inspection: SourceImageInspection,
1225}
1226
1227#[derive(Debug, Clone, Default)]
1229pub struct MaterialResourceAssets {
1230 pub coverage: MaterialResourceCoverage,
1232 pub materials: Vec<SourceMaterialAsset>,
1234 pub textures: Vec<SourceTextureAsset>,
1236 pub images: Vec<SourceImageAsset>,
1238}
1239
1240impl Primitive {
1241 pub fn weld(&mut self) {
1245 if !self.indices.is_empty() || self.positions.is_empty() {
1246 return;
1247 }
1248 let corner_key = |i: usize| -> Vec<u8> {
1249 let mut key = Vec::with_capacity(64);
1250 let mut push_f32s = |vals: &[f32]| {
1251 for v in vals {
1252 key.extend_from_slice(&v.to_le_bytes());
1253 }
1254 };
1255 push_f32s(&self.positions[i].to_array());
1256 if let Some(n) = self.normals.get(i) {
1257 push_f32s(&n.to_array());
1258 }
1259 if let Some(uv) = self.uvs.get(i) {
1260 push_f32s(uv);
1261 }
1262 if let Some(w) = self.weights.get(i) {
1263 push_f32s(w);
1264 }
1265 if let Some(j) = self.joints.get(i) {
1266 for v in j {
1267 key.extend_from_slice(&v.to_le_bytes());
1268 }
1269 }
1270 key
1271 };
1272 let mut seen: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
1273 let mut indices = Vec::with_capacity(self.positions.len());
1274 let mut positions = Vec::new();
1275 let mut normals = Vec::new();
1276 let mut uvs = Vec::new();
1277 let mut joints = Vec::new();
1278 let mut weights = Vec::new();
1279 for i in 0..self.positions.len() {
1280 let index = *seen.entry(corner_key(i)).or_insert_with(|| {
1281 positions.push(self.positions[i]);
1282 if let Some(n) = self.normals.get(i) {
1283 normals.push(*n);
1284 }
1285 if let Some(uv) = self.uvs.get(i) {
1286 uvs.push(*uv);
1287 }
1288 if let Some(j) = self.joints.get(i) {
1289 joints.push(*j);
1290 }
1291 if let Some(w) = self.weights.get(i) {
1292 weights.push(*w);
1293 }
1294 (positions.len() - 1) as u32
1295 });
1296 indices.push(index);
1297 }
1298 self.indices = indices;
1299 self.positions = positions;
1300 self.normals = normals;
1301 self.uvs = uvs;
1302 self.joints = joints;
1303 self.weights = weights;
1304 }
1305}
1306
1307#[derive(Debug, Clone, Default)]
1310pub struct SceneAssets {
1311 pub meshes: Vec<MeshAsset>,
1314 pub instances: Vec<MeshInstance>,
1316 pub materials: Vec<MaterialAsset>,
1318 pub material_resources: MaterialResourceAssets,
1321 pub scenes: Vec<SceneAsset>,
1323 pub default_scene: Option<usize>,
1325 pub source_skeleton: SourceSkeletonAssets,
1332}
1333
1334pub fn validate_document_shape(document: &Document) -> Result<(), DocumentShapeError> {
1352 validate_skeleton_rest(&document.skeleton)?;
1353 validate_source_skeleton_identity(&document.assets.source_skeleton)?;
1354 validate_source_projection(document)?;
1355 validate_clip_tracks(document)?;
1356 validate_mesh_instances(document)?;
1357 validate_bone_inverse_binds(&document.skeleton)
1358}
1359
1360fn validate_skeleton_rest(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1361 world_rest_matrices(skeleton)
1362 .map(|_| ())
1363 .map_err(|error| match error {
1364 WorldMatrixError::NonFiniteTransform { node } => {
1365 DocumentShapeError::NonFiniteSkeletonRest { node }
1366 }
1367 WorldMatrixError::InvalidParent { node, parent } => {
1368 DocumentShapeError::InvalidSkeletonParent { node, parent }
1369 }
1370 })
1371}
1372
1373fn validate_source_skeleton_identity(
1374 source_skeleton: &SourceSkeletonAssets,
1375) -> Result<(), DocumentShapeError> {
1376 let mut seen_nodes = BTreeSet::new();
1377 for node in &source_skeleton.nodes {
1378 if !seen_nodes.insert(node.source_node_index) {
1379 return Err(DocumentShapeError::DuplicateSourceNodeIndex {
1380 source_node_index: node.source_node_index,
1381 });
1382 }
1383 }
1384 let mut seen_skins = BTreeSet::new();
1385 for skin in &source_skeleton.skins {
1386 if !seen_skins.insert(skin.source_skin_index) {
1387 return Err(DocumentShapeError::DuplicateSourceSkinIndex {
1388 source_skin_index: skin.source_skin_index,
1389 });
1390 }
1391 }
1392 Ok(())
1393}
1394
1395fn validate_source_projection(document: &Document) -> Result<(), DocumentShapeError> {
1409 let source_skeleton = &document.assets.source_skeleton;
1410 if source_skeleton.coverage != SourceSkeletonCoverage::Complete {
1411 return Ok(());
1412 }
1413
1414 let bones = &document.skeleton.bones;
1415 let mut bone_of_source = BTreeMap::new();
1416 let mut source_of_bone = BTreeMap::new();
1417 let mut skeleton_parents = Vec::with_capacity(source_skeleton.nodes.len());
1418 for node in &source_skeleton.nodes {
1419 let Some(bone) = node.bone else {
1420 continue;
1421 };
1422 let skeleton_parent = bones
1423 .get(bone)
1424 .ok_or(DocumentShapeError::SourceProjection {
1425 source_node_index: node.source_node_index,
1426 violation: SourceProjectionViolation::ProjectedBoneOutOfRange,
1427 })?
1428 .parent;
1429 if source_of_bone
1430 .insert(bone, node.source_node_index)
1431 .is_some()
1432 {
1433 return Err(DocumentShapeError::SourceProjection {
1434 source_node_index: node.source_node_index,
1435 violation: SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
1436 });
1437 }
1438 bone_of_source.insert(node.source_node_index, bone);
1439 skeleton_parents.push((node, skeleton_parent));
1440 }
1441
1442 let by_source_index: BTreeMap<_, _> = source_skeleton
1443 .nodes
1444 .iter()
1445 .map(|node| (node.source_node_index, node))
1446 .collect();
1447 let unprojected_rows = source_skeleton.nodes.len() - bone_of_source.len();
1448 let mut resolved_unprojected = BTreeMap::<usize, Option<BoneId>>::new();
1452 for (node, skeleton_parent) in skeleton_parents {
1453 let mut cursor = node.parent_source_node_index;
1454 let mut unresolved_suffix = Vec::new();
1455 let projected_parent = loop {
1456 let Some(parent_source_node_index) = cursor else {
1457 break None;
1458 };
1459 if let Some(&bone) = bone_of_source.get(&parent_source_node_index) {
1460 break Some(bone);
1461 }
1462 if let Some(&projected_parent) = resolved_unprojected.get(&parent_source_node_index) {
1463 break projected_parent;
1464 }
1465 let parent = by_source_index.get(&parent_source_node_index).ok_or(
1466 DocumentShapeError::SourceProjection {
1467 source_node_index: node.source_node_index,
1468 violation: SourceProjectionViolation::ParentSourceNodeMissing,
1469 },
1470 )?;
1471 unresolved_suffix.push(parent_source_node_index);
1472 if unresolved_suffix.len() > unprojected_rows {
1473 return Err(DocumentShapeError::SourceProjection {
1474 source_node_index: node.source_node_index,
1475 violation: SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
1476 });
1477 }
1478 cursor = parent.parent_source_node_index;
1479 };
1480 for source_node_index in unresolved_suffix {
1481 resolved_unprojected.insert(source_node_index, projected_parent);
1482 }
1483 if projected_parent != skeleton_parent {
1484 return Err(DocumentShapeError::SourceProjection {
1485 source_node_index: node.source_node_index,
1486 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1487 });
1488 }
1489 }
1490
1491 for (bone, child) in bones.iter().enumerate() {
1492 if source_of_bone.contains_key(&bone) {
1493 continue;
1494 }
1495 if let Some(parent) = child.parent
1496 && let Some(&source_node_index) = source_of_bone.get(&parent)
1497 {
1498 return Err(DocumentShapeError::SourceProjection {
1499 source_node_index,
1500 violation: SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
1501 });
1502 }
1503 }
1504 Ok(())
1505}
1506
1507fn validate_clip_tracks(document: &Document) -> Result<(), DocumentShapeError> {
1508 let bone_count = document.skeleton.bones.len();
1509 for (clip_index, clip) in document.clips.iter().enumerate() {
1510 let mut seen = Vec::with_capacity(clip.tracks.len());
1511 for track in &clip.tracks {
1512 if track.bone >= bone_count {
1513 return Err(DocumentShapeError::TrackShape {
1514 clip_index,
1515 node: track.bone,
1516 violation: TrackShapeViolation::BoneIndexOutOfRange,
1517 });
1518 }
1519 if seen.contains(&(track.bone, track.property)) {
1520 return Err(DocumentShapeError::DuplicateClipTrack {
1521 clip_index,
1522 node: track.bone,
1523 property: track.property,
1524 });
1525 }
1526 seen.push((track.bone, track.property));
1527 validate_track_shape(clip_index, track)?;
1528 }
1529 }
1530 Ok(())
1531}
1532
1533fn validate_track_shape(clip_index: usize, track: &Track) -> Result<(), DocumentShapeError> {
1534 let violation = if track.times.is_empty() {
1535 Some(TrackShapeViolation::EmptyTimes)
1536 } else if track.times.iter().any(|time| !time.is_finite()) {
1537 Some(TrackShapeViolation::NonFiniteTime)
1538 } else if track.times.windows(2).any(|times| times[0] >= times[1]) {
1539 Some(TrackShapeViolation::TimesNotStrictlyIncreasing)
1540 } else {
1541 let expected_values = match track.interpolation {
1542 Interpolation::CubicSpline => track.times.len().checked_mul(3),
1543 Interpolation::Linear | Interpolation::Step => Some(track.times.len()),
1544 };
1545 if expected_values != Some(track.values.len()) {
1546 Some(TrackShapeViolation::ValueCountMismatch)
1547 } else if !matches!(
1548 (&track.values, track.property),
1549 (
1550 TrackValues::Vec3s(_),
1551 Property::Translation | Property::Scale
1552 ) | (TrackValues::Quats(_), Property::Rotation)
1553 ) {
1554 Some(TrackShapeViolation::ValueTypeMismatchesProperty)
1555 } else if match &track.values {
1556 TrackValues::Vec3s(values) => values.iter().any(|value| !value.is_finite()),
1557 TrackValues::Quats(values) => values.iter().any(|value| !value.is_finite()),
1558 } {
1559 Some(TrackShapeViolation::NonFiniteValue)
1560 } else {
1561 None
1562 }
1563 };
1564 violation.map_or(Ok(()), |violation| {
1565 Err(DocumentShapeError::TrackShape {
1566 clip_index,
1567 node: track.bone,
1568 violation,
1569 })
1570 })
1571}
1572
1573fn validate_mesh_instances(document: &Document) -> Result<(), DocumentShapeError> {
1574 let bone_count = document.skeleton.bones.len();
1575 let mesh_count = document.assets.meshes.len();
1576 for (instance_index, instance) in document.assets.instances.iter().enumerate() {
1577 let violation = if instance.node >= bone_count {
1578 Some(MeshInstanceShapeViolation::NodeIndexOutOfRange)
1579 } else if instance.mesh >= mesh_count {
1580 Some(MeshInstanceShapeViolation::MeshIndexOutOfRange)
1581 } else if instance
1582 .skin_joints
1583 .iter()
1584 .any(|&joint| joint >= bone_count)
1585 {
1586 Some(MeshInstanceShapeViolation::SkinJointOutOfRange)
1587 } else if !instance.skin_ibms.is_empty()
1588 && instance.skin_ibms.len() != instance.skin_joints.len()
1589 {
1590 Some(MeshInstanceShapeViolation::SkinInverseBindCountMismatch)
1591 } else if instance.skin_ibms.iter().any(|ibm| !mat4_is_finite(*ibm)) {
1592 Some(MeshInstanceShapeViolation::NonFiniteSkinInverseBind)
1593 } else {
1594 None
1595 };
1596 if let Some(violation) = violation {
1597 return Err(DocumentShapeError::MeshInstanceShape {
1598 instance_index,
1599 violation,
1600 });
1601 }
1602 }
1603 Ok(())
1604}
1605
1606fn validate_bone_inverse_binds(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
1607 for (node, bone) in skeleton.bones.iter().enumerate() {
1608 if let Some(inverse_bind) = bone.inverse_bind
1609 && !mat4_is_finite(inverse_bind)
1610 {
1611 return Err(DocumentShapeError::NonFiniteBoneInverseBind { node });
1612 }
1613 }
1614 Ok(())
1615}
1616
1617#[cfg(test)]
1618mod tests {
1619 use super::*;
1620
1621 fn bone(parent: Option<BoneId>) -> Bone {
1622 Bone {
1623 name: "bone".into(),
1624 parent,
1625 rest: Transform::IDENTITY,
1626 inverse_bind: None,
1627 }
1628 }
1629
1630 fn one_bone_document() -> Document {
1631 Document {
1632 skeleton: Skeleton {
1633 bones: vec![bone(None)],
1634 },
1635 ..Document::default()
1636 }
1637 }
1638
1639 fn source_node(
1640 source_node_index: usize,
1641 parent_source_node_index: Option<usize>,
1642 bone: Option<BoneId>,
1643 ) -> SourceNodeAsset {
1644 SourceNodeAsset {
1645 source_node_index,
1646 name: None,
1647 parent_source_node_index,
1648 scene_root_indices: Vec::new(),
1649 local_rest: SourceNodeLocalRest::Trs {
1650 translation: Vec3::ZERO,
1651 rotation: Quat::IDENTITY,
1652 scale: Vec3::ONE,
1653 },
1654 bone,
1655 }
1656 }
1657
1658 fn valid_track() -> Track {
1659 Track {
1660 bone: 0,
1661 property: Property::Translation,
1662 interpolation: Interpolation::Linear,
1663 times: vec![0.0],
1664 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
1665 }
1666 }
1667
1668 fn track_document(track: Track) -> Document {
1669 let mut document = one_bone_document();
1670 document.clips.push(Clip {
1671 name: "clip".into(),
1672 duration_s: 0.0,
1673 tracks: vec![track],
1674 });
1675 document
1676 }
1677
1678 fn instance_document() -> Document {
1679 let mut document = one_bone_document();
1680 document.assets.meshes.push(MeshAsset::default());
1681 document.assets.instances.push(MeshInstance {
1682 node: 0,
1683 mesh: 0,
1684 ..MeshInstance::default()
1685 });
1686 document
1687 }
1688
1689 #[test]
1690 fn document_shape_validation_accepts_a_complete_projection_with_an_unprojected_intermediate() {
1691 let mut document = Document {
1692 skeleton: Skeleton {
1693 bones: vec![bone(None), bone(Some(0))],
1694 },
1695 assets: SceneAssets {
1696 source_skeleton: SourceSkeletonAssets {
1697 coverage: SourceSkeletonCoverage::Complete,
1698 nodes: vec![
1699 source_node(10, None, Some(0)),
1700 source_node(11, Some(10), None),
1701 source_node(12, Some(11), Some(1)),
1702 ],
1703 ..SourceSkeletonAssets::default()
1704 },
1705 meshes: vec![MeshAsset::default()],
1706 instances: vec![MeshInstance {
1707 node: 1,
1708 mesh: 0,
1709 skin_joints: vec![0, 1],
1710 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
1711 ..MeshInstance::default()
1712 }],
1713 ..SceneAssets::default()
1714 },
1715 ..Document::default()
1716 };
1717 document.clips.push(Clip {
1718 name: "clip".into(),
1719 duration_s: 0.0,
1720 tracks: vec![valid_track()],
1721 });
1722
1723 assert_eq!(validate_document_shape(&document), Ok(()));
1724 }
1725
1726 #[test]
1727 fn shared_unprojected_parent_suffix_preserves_each_projected_parent() {
1728 const CONNECTORS: usize = 64;
1729 const PROJECTED_CHILDREN: usize = 64;
1730
1731 let mut nodes = Vec::with_capacity(1 + CONNECTORS + PROJECTED_CHILDREN);
1732 nodes.push(source_node(0, None, Some(0)));
1733 for source_node_index in 1..=CONNECTORS {
1734 nodes.push(source_node(
1735 source_node_index,
1736 Some(source_node_index - 1),
1737 None,
1738 ));
1739 }
1740 for child in 0..PROJECTED_CHILDREN {
1741 nodes.push(source_node(
1742 1 + CONNECTORS + child,
1743 Some(CONNECTORS),
1744 Some(1 + child),
1745 ));
1746 }
1747 let document = Document {
1748 skeleton: Skeleton {
1749 bones: std::iter::once(bone(None))
1750 .chain((0..PROJECTED_CHILDREN).map(|_| bone(Some(0))))
1751 .collect(),
1752 },
1753 assets: SceneAssets {
1754 source_skeleton: SourceSkeletonAssets {
1755 coverage: SourceSkeletonCoverage::Complete,
1756 nodes,
1757 ..SourceSkeletonAssets::default()
1758 },
1759 ..SceneAssets::default()
1760 },
1761 ..Document::default()
1762 };
1763
1764 assert_eq!(validate_document_shape(&document), Ok(()));
1765 let mut mismatched = document.clone();
1766 mismatched.skeleton.bones[PROJECTED_CHILDREN].parent = None;
1767 assert_eq!(
1768 validate_document_shape(&mismatched),
1769 Err(DocumentShapeError::SourceProjection {
1770 source_node_index: CONNECTORS + PROJECTED_CHILDREN,
1771 violation: SourceProjectionViolation::NearestProjectedParentMismatch,
1772 })
1773 );
1774 }
1775
1776 #[test]
1777 fn document_shape_validation_has_an_analytic_error_for_every_variant() {
1778 let projection_error =
1779 |source_node_index, violation| DocumentShapeError::SourceProjection {
1780 source_node_index,
1781 violation,
1782 };
1783 let track_error = |node, violation| DocumentShapeError::TrackShape {
1784 clip_index: 0,
1785 node,
1786 violation,
1787 };
1788 let instance_error = |violation| DocumentShapeError::MeshInstanceShape {
1789 instance_index: 0,
1790 violation,
1791 };
1792
1793 let mut non_finite_rest = one_bone_document();
1794 non_finite_rest.skeleton.bones[0].rest.translation.x = f32::NAN;
1795 let overflowed_rest_world = Document {
1796 skeleton: Skeleton {
1797 bones: vec![
1798 Bone {
1799 rest: Transform {
1800 scale: Vec3::splat(f32::MAX),
1801 ..Transform::IDENTITY
1802 },
1803 ..bone(None)
1804 },
1805 Bone {
1806 rest: Transform {
1807 translation: Vec3::splat(2.0),
1808 ..Transform::IDENTITY
1809 },
1810 ..bone(Some(0))
1811 },
1812 ],
1813 },
1814 ..Document::default()
1815 };
1816 let self_parent = Document {
1817 skeleton: Skeleton {
1818 bones: vec![bone(Some(0))],
1819 },
1820 ..Document::default()
1821 };
1822 let forward_parent = Document {
1823 skeleton: Skeleton {
1824 bones: vec![bone(Some(1)), bone(None)],
1825 },
1826 ..Document::default()
1827 };
1828 let far_parent = Document {
1829 skeleton: Skeleton {
1830 bones: vec![bone(Some(99))],
1831 },
1832 ..Document::default()
1833 };
1834 let duplicate_node = Document {
1835 assets: SceneAssets {
1836 source_skeleton: SourceSkeletonAssets {
1837 nodes: vec![
1838 source_node(9, None, None),
1839 source_node(10, None, None),
1840 source_node(9, None, None),
1841 ],
1842 ..SourceSkeletonAssets::default()
1843 },
1844 ..SceneAssets::default()
1845 },
1846 ..Document::default()
1847 };
1848 let duplicate_skin = Document {
1849 assets: SceneAssets {
1850 source_skeleton: SourceSkeletonAssets {
1851 skins: vec![
1852 SourceSkinAsset {
1853 source_skin_index: 4,
1854 ..SourceSkinAsset::default()
1855 },
1856 SourceSkinAsset {
1857 source_skin_index: 5,
1858 ..SourceSkinAsset::default()
1859 },
1860 SourceSkinAsset {
1861 source_skin_index: 4,
1862 ..SourceSkinAsset::default()
1863 },
1864 ],
1865 ..SourceSkeletonAssets::default()
1866 },
1867 ..SceneAssets::default()
1868 },
1869 ..Document::default()
1870 };
1871 let complete_projection = |nodes| SceneAssets {
1872 source_skeleton: SourceSkeletonAssets {
1873 coverage: SourceSkeletonCoverage::Complete,
1874 nodes,
1875 ..SourceSkeletonAssets::default()
1876 },
1877 ..SceneAssets::default()
1878 };
1879 let out_of_range_projection = Document {
1880 skeleton: Skeleton {
1881 bones: vec![bone(None)],
1882 },
1883 assets: complete_projection(vec![source_node(10, None, Some(1))]),
1884 ..Document::default()
1885 };
1886 let non_injective_projection = Document {
1887 skeleton: Skeleton {
1888 bones: vec![bone(None)],
1889 },
1890 assets: complete_projection(vec![
1891 source_node(10, None, Some(0)),
1892 source_node(11, None, Some(0)),
1893 ]),
1894 ..Document::default()
1895 };
1896 let missing_projection_parent = Document {
1897 skeleton: Skeleton {
1898 bones: vec![bone(None), bone(Some(0))],
1899 },
1900 assets: complete_projection(vec![source_node(11, Some(99), Some(1))]),
1901 ..Document::default()
1902 };
1903 let missing_projection_parent_at_cycle_bound = Document {
1908 skeleton: Skeleton {
1909 bones: vec![bone(None), bone(Some(0))],
1910 },
1911 assets: complete_projection(vec![
1912 source_node(10, None, Some(0)),
1913 source_node(11, Some(12), Some(1)),
1914 source_node(12, Some(99), None),
1915 ]),
1916 ..Document::default()
1917 };
1918 let cyclic_unprojected_parent = Document {
1919 skeleton: Skeleton {
1920 bones: vec![bone(None), bone(Some(0))],
1921 },
1922 assets: complete_projection(vec![
1923 source_node(11, Some(12), Some(1)),
1924 source_node(12, Some(12), None),
1925 ]),
1926 ..Document::default()
1927 };
1928 let cyclic_unprojected_parent_pair = Document {
1929 skeleton: Skeleton {
1930 bones: vec![bone(None), bone(Some(0))],
1931 },
1932 assets: complete_projection(vec![
1933 source_node(11, Some(12), Some(1)),
1934 source_node(12, Some(13), None),
1935 source_node(13, Some(12), None),
1936 ]),
1937 ..Document::default()
1938 };
1939 let mismatched_nearest_parent = Document {
1940 skeleton: Skeleton {
1941 bones: vec![bone(None), bone(Some(0))],
1942 },
1943 assets: complete_projection(vec![
1944 source_node(10, None, Some(0)),
1945 source_node(11, None, Some(1)),
1946 ]),
1947 ..Document::default()
1948 };
1949 let unprojected_child = Document {
1950 skeleton: Skeleton {
1951 bones: vec![bone(None), bone(Some(0))],
1952 },
1953 assets: complete_projection(vec![source_node(10, None, Some(0))]),
1954 ..Document::default()
1955 };
1956
1957 let duplicate_track = {
1958 let track = valid_track();
1959 let mut document = track_document(track.clone());
1960 document.clips[0].tracks.push(Track {
1961 property: Property::Scale,
1962 ..valid_track()
1963 });
1964 document.clips[0].tracks.push(track);
1965 document
1966 };
1967 let mut boundary_out_of_range_track = valid_track();
1968 boundary_out_of_range_track.bone = 1;
1969 let mut far_out_of_range_track = valid_track();
1970 far_out_of_range_track.bone = 99;
1971 let empty_track = Track {
1972 times: Vec::new(),
1973 values: TrackValues::Vec3s(Vec::new()),
1974 ..valid_track()
1975 };
1976 let non_finite_later_time = Track {
1977 times: vec![0.0, f32::NAN],
1978 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1979 ..valid_track()
1980 };
1981 let unordered_times = Track {
1982 times: vec![1.0, 0.0],
1983 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1984 ..valid_track()
1985 };
1986 let equal_times = Track {
1987 times: vec![0.0, 0.0],
1988 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1989 ..valid_track()
1990 };
1991 let wrong_linear_value_count = Track {
1992 values: TrackValues::Vec3s(Vec::new()),
1993 ..valid_track()
1994 };
1995 let excess_linear_value_count = Track {
1996 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
1997 ..valid_track()
1998 };
1999 let wrong_step_value_count = Track {
2000 interpolation: Interpolation::Step,
2001 times: vec![0.0, 1.0],
2002 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2003 ..valid_track()
2004 };
2005 let excess_step_value_count = Track {
2006 interpolation: Interpolation::Step,
2007 values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
2008 ..valid_track()
2009 };
2010 let wrong_cubic_value_count = Track {
2011 interpolation: Interpolation::CubicSpline,
2012 times: vec![0.0, 1.0],
2013 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2014 ..valid_track()
2015 };
2016 let excess_cubic_value_count = Track {
2017 interpolation: Interpolation::CubicSpline,
2018 values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
2019 ..valid_track()
2020 };
2021 let wrong_translation_value_type = Track {
2022 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2023 ..valid_track()
2024 };
2025 let wrong_scale_value_type = Track {
2026 property: Property::Scale,
2027 values: TrackValues::Quats(vec![Quat::IDENTITY]),
2028 ..valid_track()
2029 };
2030 let wrong_rotation_value_type = Track {
2031 property: Property::Rotation,
2032 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2033 ..valid_track()
2034 };
2035 let non_finite_value = Track {
2036 values: TrackValues::Vec3s(vec![Vec3::splat(f32::NAN)]),
2037 ..valid_track()
2038 };
2039
2040 let mut bad_instance_node = instance_document();
2041 bad_instance_node.assets.instances[0].node = 1;
2042 let mut far_instance_node = instance_document();
2043 far_instance_node.assets.instances[0].node = 99;
2044 let mut bad_instance_mesh = instance_document();
2045 bad_instance_mesh.assets.instances[0].mesh = 1;
2046 let mut far_instance_mesh = instance_document();
2047 far_instance_mesh.assets.instances[0].mesh = 99;
2048 let mut bad_instance_joint = instance_document();
2049 bad_instance_joint.assets.instances[0].skin_joints = vec![1];
2050 let mut far_instance_joint = instance_document();
2051 far_instance_joint.assets.instances[0].skin_joints = vec![99];
2052 let mut bad_instance_count = instance_document();
2053 bad_instance_count.assets.instances[0].skin_joints = vec![0];
2054 bad_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, Mat4::IDENTITY];
2055 let mut short_instance_count = instance_document();
2056 short_instance_count.skeleton.bones.push(bone(Some(0)));
2057 short_instance_count.assets.instances[0].skin_joints = vec![0, 1];
2058 short_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY];
2059 let mut bad_instance_ibm = instance_document();
2060 bad_instance_ibm.assets.instances[0].skin_joints = vec![0];
2061 bad_instance_ibm.assets.instances[0].skin_ibms =
2062 vec![Mat4::from_cols_array(&[f32::NAN; 16])];
2063 let mut bad_bone_ibm = one_bone_document();
2064 bad_bone_ibm.skeleton.bones[0].inverse_bind = Some(Mat4::from_cols_array(&[f32::NAN; 16]));
2065
2066 let cases = vec![
2067 (
2068 "non-finite rest",
2069 non_finite_rest,
2070 DocumentShapeError::NonFiniteSkeletonRest { node: 0 },
2071 ),
2072 (
2073 "non-finite composed rest world",
2074 overflowed_rest_world,
2075 DocumentShapeError::NonFiniteSkeletonRest { node: 1 },
2076 ),
2077 (
2078 "self parent",
2079 self_parent,
2080 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 0 },
2081 ),
2082 (
2083 "forward parent",
2084 forward_parent,
2085 DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 1 },
2086 ),
2087 (
2088 "far parent",
2089 far_parent,
2090 DocumentShapeError::InvalidSkeletonParent {
2091 node: 0,
2092 parent: 99,
2093 },
2094 ),
2095 (
2096 "duplicate source node",
2097 duplicate_node,
2098 DocumentShapeError::DuplicateSourceNodeIndex {
2099 source_node_index: 9,
2100 },
2101 ),
2102 (
2103 "duplicate source skin",
2104 duplicate_skin,
2105 DocumentShapeError::DuplicateSourceSkinIndex {
2106 source_skin_index: 4,
2107 },
2108 ),
2109 (
2110 "projected bone range",
2111 out_of_range_projection,
2112 projection_error(10, SourceProjectionViolation::ProjectedBoneOutOfRange),
2113 ),
2114 (
2115 "projection injectivity",
2116 non_injective_projection,
2117 projection_error(
2118 11,
2119 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2120 ),
2121 ),
2122 (
2123 "missing projection parent",
2124 missing_projection_parent,
2125 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2126 ),
2127 (
2128 "missing projection parent at cycle bound",
2129 missing_projection_parent_at_cycle_bound,
2130 projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
2131 ),
2132 (
2133 "cyclic projection parent",
2134 cyclic_unprojected_parent,
2135 projection_error(
2136 11,
2137 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2138 ),
2139 ),
2140 (
2141 "cyclic projection parent pair",
2142 cyclic_unprojected_parent_pair,
2143 projection_error(
2144 11,
2145 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2146 ),
2147 ),
2148 (
2149 "nearest projection parent",
2150 mismatched_nearest_parent,
2151 projection_error(
2152 11,
2153 SourceProjectionViolation::NearestProjectedParentMismatch,
2154 ),
2155 ),
2156 (
2157 "projection downward closure",
2158 unprojected_child,
2159 projection_error(
2160 10,
2161 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2162 ),
2163 ),
2164 (
2165 "duplicate track",
2166 duplicate_track,
2167 DocumentShapeError::DuplicateClipTrack {
2168 clip_index: 0,
2169 node: 0,
2170 property: Property::Translation,
2171 },
2172 ),
2173 (
2174 "track bone range boundary",
2175 track_document(boundary_out_of_range_track),
2176 track_error(1, TrackShapeViolation::BoneIndexOutOfRange),
2177 ),
2178 (
2179 "track bone range far",
2180 track_document(far_out_of_range_track),
2181 track_error(99, TrackShapeViolation::BoneIndexOutOfRange),
2182 ),
2183 (
2184 "empty track",
2185 track_document(empty_track),
2186 track_error(0, TrackShapeViolation::EmptyTimes),
2187 ),
2188 (
2189 "non-finite time",
2190 track_document(non_finite_later_time),
2191 track_error(0, TrackShapeViolation::NonFiniteTime),
2192 ),
2193 (
2194 "unordered times",
2195 track_document(unordered_times),
2196 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2197 ),
2198 (
2199 "equal times",
2200 track_document(equal_times),
2201 track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
2202 ),
2203 (
2204 "linear value count",
2205 track_document(wrong_linear_value_count),
2206 track_error(0, TrackShapeViolation::ValueCountMismatch),
2207 ),
2208 (
2209 "linear excess value count",
2210 track_document(excess_linear_value_count),
2211 track_error(0, TrackShapeViolation::ValueCountMismatch),
2212 ),
2213 (
2214 "step value count",
2215 track_document(wrong_step_value_count),
2216 track_error(0, TrackShapeViolation::ValueCountMismatch),
2217 ),
2218 (
2219 "step excess value count",
2220 track_document(excess_step_value_count),
2221 track_error(0, TrackShapeViolation::ValueCountMismatch),
2222 ),
2223 (
2224 "cubic value count",
2225 track_document(wrong_cubic_value_count),
2226 track_error(0, TrackShapeViolation::ValueCountMismatch),
2227 ),
2228 (
2229 "cubic excess value count",
2230 track_document(excess_cubic_value_count),
2231 track_error(0, TrackShapeViolation::ValueCountMismatch),
2232 ),
2233 (
2234 "translation value type",
2235 track_document(wrong_translation_value_type),
2236 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2237 ),
2238 (
2239 "scale value type",
2240 track_document(wrong_scale_value_type),
2241 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2242 ),
2243 (
2244 "rotation value type",
2245 track_document(wrong_rotation_value_type),
2246 track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
2247 ),
2248 (
2249 "non-finite value",
2250 track_document(non_finite_value),
2251 track_error(0, TrackShapeViolation::NonFiniteValue),
2252 ),
2253 (
2254 "instance node boundary",
2255 bad_instance_node,
2256 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2257 ),
2258 (
2259 "instance node far",
2260 far_instance_node,
2261 instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
2262 ),
2263 (
2264 "instance mesh boundary",
2265 bad_instance_mesh,
2266 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2267 ),
2268 (
2269 "instance mesh far",
2270 far_instance_mesh,
2271 instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
2272 ),
2273 (
2274 "instance joint boundary",
2275 bad_instance_joint,
2276 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2277 ),
2278 (
2279 "instance joint far",
2280 far_instance_joint,
2281 instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
2282 ),
2283 (
2284 "instance ibm count excess",
2285 bad_instance_count,
2286 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2287 ),
2288 (
2289 "instance ibm count short",
2290 short_instance_count,
2291 instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
2292 ),
2293 (
2294 "instance ibm finite",
2295 bad_instance_ibm,
2296 instance_error(MeshInstanceShapeViolation::NonFiniteSkinInverseBind),
2297 ),
2298 (
2299 "bone ibm finite",
2300 bad_bone_ibm,
2301 DocumentShapeError::NonFiniteBoneInverseBind { node: 0 },
2302 ),
2303 ];
2304 for (name, document, expected) in cases {
2305 assert_eq!(validate_document_shape(&document), Err(expected), "{name}");
2306 }
2307 }
2308
2309 #[test]
2310 fn document_shape_finiteness_checks_every_stored_component() {
2311 for component in 0..3 {
2312 let mut translation = Vec3::ZERO.to_array();
2313 translation[component] = f32::NAN;
2314 let mut document = one_bone_document();
2315 document.skeleton.bones[0].rest.translation = Vec3::from_array(translation);
2316 assert_eq!(
2317 validate_document_shape(&document),
2318 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2319 "rest translation component {component}"
2320 );
2321
2322 let mut scale = Vec3::ONE.to_array();
2323 scale[component] = f32::NAN;
2324 let mut document = one_bone_document();
2325 document.skeleton.bones[0].rest.scale = Vec3::from_array(scale);
2326 assert_eq!(
2327 validate_document_shape(&document),
2328 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2329 "rest scale component {component}"
2330 );
2331
2332 let mut value = Vec3::ZERO.to_array();
2333 value[component] = f32::NAN;
2334 let document = track_document(Track {
2335 values: TrackValues::Vec3s(vec![Vec3::from_array(value)]),
2336 ..valid_track()
2337 });
2338 assert_eq!(
2339 validate_document_shape(&document),
2340 Err(DocumentShapeError::TrackShape {
2341 clip_index: 0,
2342 node: 0,
2343 violation: TrackShapeViolation::NonFiniteValue,
2344 }),
2345 "track Vec3 component {component}"
2346 );
2347 }
2348
2349 for component in 0..4 {
2350 let mut rotation = Quat::IDENTITY.to_array();
2351 rotation[component] = f32::NAN;
2352 let mut document = one_bone_document();
2353 document.skeleton.bones[0].rest.rotation = Quat::from_array(rotation);
2354 assert_eq!(
2355 validate_document_shape(&document),
2356 Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
2357 "rest rotation component {component}"
2358 );
2359
2360 let document = track_document(Track {
2361 property: Property::Rotation,
2362 values: TrackValues::Quats(vec![Quat::from_array(rotation)]),
2363 ..valid_track()
2364 });
2365 assert_eq!(
2366 validate_document_shape(&document),
2367 Err(DocumentShapeError::TrackShape {
2368 clip_index: 0,
2369 node: 0,
2370 violation: TrackShapeViolation::NonFiniteValue,
2371 }),
2372 "track quaternion component {component}"
2373 );
2374 }
2375
2376 for key in 0..3 {
2377 let mut times = vec![0.0, 1.0, 2.0];
2378 times[key] = f32::NAN;
2379 let document = track_document(Track {
2380 times,
2381 values: TrackValues::Vec3s(vec![Vec3::ZERO; 3]),
2382 ..valid_track()
2383 });
2384 assert_eq!(
2385 validate_document_shape(&document),
2386 Err(DocumentShapeError::TrackShape {
2387 clip_index: 0,
2388 node: 0,
2389 violation: TrackShapeViolation::NonFiniteTime,
2390 }),
2391 "track time {key}"
2392 );
2393 }
2394
2395 for component in 0..16 {
2396 let mut columns = Mat4::IDENTITY.to_cols_array();
2397 columns[component] = f32::NAN;
2398 let inverse_bind = Mat4::from_cols_array(&columns);
2399
2400 let mut instance_document = instance_document();
2401 instance_document.assets.instances[0].skin_joints = vec![0];
2402 instance_document.assets.instances[0].skin_ibms = vec![inverse_bind];
2403 assert_eq!(
2404 validate_document_shape(&instance_document),
2405 Err(DocumentShapeError::MeshInstanceShape {
2406 instance_index: 0,
2407 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2408 }),
2409 "instance inverse-bind component {component}"
2410 );
2411
2412 let mut bone_document = one_bone_document();
2413 bone_document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2414 assert_eq!(
2415 validate_document_shape(&bone_document),
2416 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2417 "bone inverse-bind component {component}"
2418 );
2419 }
2420 }
2421
2422 #[test]
2423 fn document_shape_rejects_duplicate_tracks_for_every_property() {
2424 let tracks = [
2425 (Property::Translation, TrackValues::Vec3s(vec![Vec3::ZERO])),
2426 (Property::Scale, TrackValues::Vec3s(vec![Vec3::ONE])),
2427 (Property::Rotation, TrackValues::Quats(vec![Quat::IDENTITY])),
2428 ];
2429
2430 for (property, values) in tracks {
2431 let track = Track {
2432 property,
2433 values,
2434 ..valid_track()
2435 };
2436 let mut document = track_document(track.clone());
2437 document.clips[0].tracks.push(track);
2438
2439 assert_eq!(
2440 validate_document_shape(&document),
2441 Err(DocumentShapeError::DuplicateClipTrack {
2442 clip_index: 0,
2443 node: 0,
2444 property,
2445 }),
2446 "duplicate {property:?} track"
2447 );
2448 }
2449 }
2450
2451 #[test]
2452 fn document_shape_rejects_infinite_times_quaternions_and_inverse_binds() {
2453 for non_finite in [f32::INFINITY, f32::NEG_INFINITY] {
2454 let document = track_document(Track {
2455 times: vec![non_finite],
2456 values: TrackValues::Vec3s(vec![Vec3::ZERO]),
2457 ..valid_track()
2458 });
2459 assert_eq!(
2460 validate_document_shape(&document),
2461 Err(DocumentShapeError::TrackShape {
2462 clip_index: 0,
2463 node: 0,
2464 violation: TrackShapeViolation::NonFiniteTime,
2465 }),
2466 "track time {non_finite}"
2467 );
2468
2469 let document = track_document(Track {
2470 property: Property::Rotation,
2471 values: TrackValues::Quats(vec![Quat::from_xyzw(non_finite, 0.0, 0.0, 1.0)]),
2472 ..valid_track()
2473 });
2474 assert_eq!(
2475 validate_document_shape(&document),
2476 Err(DocumentShapeError::TrackShape {
2477 clip_index: 0,
2478 node: 0,
2479 violation: TrackShapeViolation::NonFiniteValue,
2480 }),
2481 "track quaternion {non_finite}"
2482 );
2483
2484 let mut columns = Mat4::IDENTITY.to_cols_array();
2485 columns[0] = non_finite;
2486 let inverse_bind = Mat4::from_cols_array(&columns);
2487 let mut document = instance_document();
2488 document.assets.instances[0].skin_joints = vec![0];
2489 document.assets.instances[0].skin_ibms = vec![inverse_bind];
2490 assert_eq!(
2491 validate_document_shape(&document),
2492 Err(DocumentShapeError::MeshInstanceShape {
2493 instance_index: 0,
2494 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2495 }),
2496 "instance inverse bind {non_finite}"
2497 );
2498
2499 let mut document = one_bone_document();
2500 document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
2501 assert_eq!(
2502 validate_document_shape(&document),
2503 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
2504 "bone inverse bind {non_finite}"
2505 );
2506 }
2507 }
2508
2509 #[test]
2510 fn document_shape_checks_mesh_and_joint_references_on_later_instances() {
2511 let later_instance = MeshInstance {
2512 node: 0,
2513 mesh: 0,
2514 ..MeshInstance::default()
2515 };
2516
2517 let mut document = instance_document();
2518 document.assets.instances.push(later_instance.clone());
2519 document.assets.instances[1].mesh = 1;
2520 assert_eq!(
2521 validate_document_shape(&document),
2522 Err(DocumentShapeError::MeshInstanceShape {
2523 instance_index: 1,
2524 violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
2525 })
2526 );
2527
2528 let mut document = instance_document();
2529 document.assets.instances.push(later_instance);
2530 document.assets.instances[1].skin_joints = vec![1];
2531 assert_eq!(
2532 validate_document_shape(&document),
2533 Err(DocumentShapeError::MeshInstanceShape {
2534 instance_index: 1,
2535 violation: MeshInstanceShapeViolation::SkinJointOutOfRange,
2536 })
2537 );
2538 }
2539
2540 #[test]
2541 fn document_shape_finds_duplicates_that_do_not_involve_the_first_item() {
2542 let mut document = Document::default();
2543 document.assets.source_skeleton.skins = [4, 5, 5]
2544 .into_iter()
2545 .map(|source_skin_index| SourceSkinAsset {
2546 source_skin_index,
2547 ..SourceSkinAsset::default()
2548 })
2549 .collect();
2550 assert_eq!(
2551 validate_document_shape(&document),
2552 Err(DocumentShapeError::DuplicateSourceSkinIndex {
2553 source_skin_index: 5,
2554 })
2555 );
2556
2557 let scale_track = Track {
2558 property: Property::Scale,
2559 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2560 ..valid_track()
2561 };
2562 let mut document = track_document(valid_track());
2563 document.clips[0].tracks.push(scale_track.clone());
2564 document.clips[0].tracks.push(scale_track);
2565 assert_eq!(
2566 validate_document_shape(&document),
2567 Err(DocumentShapeError::DuplicateClipTrack {
2568 clip_index: 0,
2569 node: 0,
2570 property: Property::Scale,
2571 })
2572 );
2573 }
2574
2575 #[test]
2576 fn document_shape_checks_later_tracks_and_inverse_binds() {
2577 let mut document = track_document(valid_track());
2578 document.clips[0].tracks.push(Track {
2579 property: Property::Scale,
2580 times: Vec::new(),
2581 values: TrackValues::Vec3s(Vec::new()),
2582 ..valid_track()
2583 });
2584 assert_eq!(
2585 validate_document_shape(&document),
2586 Err(DocumentShapeError::TrackShape {
2587 clip_index: 0,
2588 node: 0,
2589 violation: TrackShapeViolation::EmptyTimes,
2590 })
2591 );
2592
2593 let scale_track = Track {
2594 property: Property::Scale,
2595 values: TrackValues::Vec3s(vec![Vec3::ONE]),
2596 ..valid_track()
2597 };
2598 let mut document = track_document(valid_track());
2599 document.clips.push(Clip {
2600 name: "later".into(),
2601 duration_s: 0.0,
2602 tracks: vec![scale_track.clone(), scale_track],
2603 });
2604 assert_eq!(
2605 validate_document_shape(&document),
2606 Err(DocumentShapeError::DuplicateClipTrack {
2607 clip_index: 1,
2608 node: 0,
2609 property: Property::Scale,
2610 })
2611 );
2612
2613 let mut document = track_document(valid_track());
2614 document.clips.push(Clip {
2615 name: "later".into(),
2616 duration_s: 0.0,
2617 tracks: vec![Track {
2618 property: Property::Scale,
2619 times: Vec::new(),
2620 values: TrackValues::Vec3s(Vec::new()),
2621 ..valid_track()
2622 }],
2623 });
2624 assert_eq!(
2625 validate_document_shape(&document),
2626 Err(DocumentShapeError::TrackShape {
2627 clip_index: 1,
2628 node: 0,
2629 violation: TrackShapeViolation::EmptyTimes,
2630 })
2631 );
2632
2633 let mut columns = Mat4::IDENTITY.to_cols_array();
2634 columns[15] = f32::NAN;
2635 let non_finite_inverse_bind = Mat4::from_cols_array(&columns);
2636
2637 let mut document = instance_document();
2638 document.skeleton.bones.push(bone(Some(0)));
2639 document.assets.instances[0].skin_joints = vec![0, 1];
2640 document.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, non_finite_inverse_bind];
2641 assert_eq!(
2642 validate_document_shape(&document),
2643 Err(DocumentShapeError::MeshInstanceShape {
2644 instance_index: 0,
2645 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2646 })
2647 );
2648
2649 let mut document = instance_document();
2650 document.assets.instances.push(MeshInstance {
2651 node: 0,
2652 mesh: 0,
2653 skin_joints: vec![0],
2654 skin_ibms: vec![non_finite_inverse_bind],
2655 ..MeshInstance::default()
2656 });
2657 assert_eq!(
2658 validate_document_shape(&document),
2659 Err(DocumentShapeError::MeshInstanceShape {
2660 instance_index: 1,
2661 violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2662 })
2663 );
2664
2665 let mut document = instance_document();
2666 document.assets.instances.push(MeshInstance {
2667 node: 0,
2668 mesh: 0,
2669 skin_joints: vec![0],
2670 skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
2671 ..MeshInstance::default()
2672 });
2673 assert_eq!(
2674 validate_document_shape(&document),
2675 Err(DocumentShapeError::MeshInstanceShape {
2676 instance_index: 1,
2677 violation: MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2678 })
2679 );
2680
2681 let mut document = one_bone_document();
2682 document.skeleton.bones.push(Bone {
2683 inverse_bind: Some(non_finite_inverse_bind),
2684 ..bone(Some(0))
2685 });
2686 assert_eq!(
2687 validate_document_shape(&document),
2688 Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 1 })
2689 );
2690 }
2691
2692 #[test]
2693 fn document_shape_violation_names_remain_machine_stable() {
2694 let source_projection = [
2695 (
2696 SourceProjectionViolation::ProjectedBoneOutOfRange,
2697 "projected_bone_out_of_range",
2698 ),
2699 (
2700 SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
2701 "two_source_nodes_project_to_one_bone",
2702 ),
2703 (
2704 SourceProjectionViolation::ParentSourceNodeMissing,
2705 "parent_source_node_is_missing",
2706 ),
2707 (
2708 SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
2709 "cyclic_unprojected_source_parent_chain",
2710 ),
2711 (
2712 SourceProjectionViolation::NearestProjectedParentMismatch,
2713 "projection_and_skeleton_parents_differ",
2714 ),
2715 (
2716 SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
2717 "projected_bone_has_an_unprojected_skeleton_child",
2718 ),
2719 ];
2720 for (violation, expected) in source_projection {
2721 assert_eq!(violation.to_string(), expected);
2722 }
2723
2724 let track = [
2725 (
2726 TrackShapeViolation::BoneIndexOutOfRange,
2727 "bone_index_out_of_range",
2728 ),
2729 (TrackShapeViolation::EmptyTimes, "empty_times"),
2730 (TrackShapeViolation::NonFiniteTime, "non_finite_time"),
2731 (
2732 TrackShapeViolation::TimesNotStrictlyIncreasing,
2733 "times_not_strictly_increasing",
2734 ),
2735 (
2736 TrackShapeViolation::ValueCountMismatch,
2737 "value_count_mismatch",
2738 ),
2739 (
2740 TrackShapeViolation::ValueTypeMismatchesProperty,
2741 "value_type_mismatches_property",
2742 ),
2743 (TrackShapeViolation::NonFiniteValue, "non_finite_value"),
2744 ];
2745 for (violation, expected) in track {
2746 assert_eq!(violation.to_string(), expected);
2747 }
2748
2749 let instance = [
2750 (
2751 MeshInstanceShapeViolation::NodeIndexOutOfRange,
2752 "node_index_out_of_range",
2753 ),
2754 (
2755 MeshInstanceShapeViolation::MeshIndexOutOfRange,
2756 "mesh_index_out_of_range",
2757 ),
2758 (
2759 MeshInstanceShapeViolation::SkinJointOutOfRange,
2760 "skin_joint_out_of_range",
2761 ),
2762 (
2763 MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
2764 "skin_ibm_count_mismatch",
2765 ),
2766 (
2767 MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
2768 "non_finite_inverse_bind",
2769 ),
2770 ];
2771 for (violation, expected) in instance {
2772 assert_eq!(violation.to_string(), expected);
2773 }
2774 }
2775
2776 #[test]
2777 fn tolerant_world_rests_keep_unrelated_partial_evidence() {
2778 let skeleton = Skeleton {
2779 bones: vec![
2780 bone(None),
2781 bone(Some(99)),
2782 Bone {
2783 rest: Transform {
2784 translation: Vec3::X,
2785 ..Transform::IDENTITY
2786 },
2787 ..bone(None)
2788 },
2789 Bone {
2790 rest: Transform {
2791 translation: Vec3::Y,
2792 ..Transform::IDENTITY
2793 },
2794 ..bone(Some(2))
2795 },
2796 bone(Some(1)),
2797 ],
2798 };
2799
2800 let worlds = tolerant_world_rest_matrices(&skeleton);
2801 assert_eq!(worlds.len(), 5);
2802 assert_eq!(worlds[0], Some(Mat4::IDENTITY));
2803 assert_eq!(worlds[1], None, "the malformed parent is unavailable");
2804 assert_eq!(worlds[2], Some(Mat4::from_translation(Vec3::X)));
2805 assert_eq!(
2806 worlds[3],
2807 Some(Mat4::from_translation(Vec3::new(1.0, 1.0, 0.0))),
2808 "a finite independent chain remains measurable"
2809 );
2810 assert_eq!(
2811 worlds[4], None,
2812 "a child of unavailable evidence is unavailable"
2813 );
2814 }
2815
2816 #[test]
2817 fn shared_affine_classifier_respects_distinct_caller_tolerances() {
2818 let equal_axis_basis = affine_test_fixtures::tolerance_divergence_basis();
2819 let strict = PositiveUniformAffineTolerance {
2820 equal_axis: 1.0e-5,
2821 relative_orthogonality: 1.0e-5,
2822 singular_determinant_relative: 1.0e-6,
2823 };
2824 let loose = PositiveUniformAffineTolerance {
2825 equal_axis: 1.0e-4,
2826 relative_orthogonality: 1.0e-4,
2827 singular_determinant_relative: 0.0,
2828 };
2829
2830 assert_eq!(
2831 classify_positive_uniform_affine(equal_axis_basis, strict),
2832 Err(AffineDomainViolation::NonUniformScale),
2833 "the stricter caller rejects this equal-axis difference"
2834 );
2835 assert!(
2836 classify_positive_uniform_affine(equal_axis_basis, loose).is_ok(),
2837 "the looser caller accepts this equal-axis difference"
2838 );
2839
2840 let orthogonality_basis = affine_test_fixtures::orthogonality_tolerance_divergence_basis();
2841 assert_eq!(
2842 classify_positive_uniform_affine(orthogonality_basis, strict),
2843 Err(AffineDomainViolation::Sheared),
2844 "the stricter caller rejects this cross-axis dot product"
2845 );
2846 assert!(
2847 classify_positive_uniform_affine(orthogonality_basis, loose).is_ok(),
2848 "the looser caller accepts this cross-axis dot product"
2849 );
2850 }
2851
2852 #[test]
2853 fn shared_affine_classifier_pins_its_symmetric_f64_formula() {
2854 let policy = PositiveUniformAffineTolerance {
2855 equal_axis: 1.0e-5,
2856 relative_orthogonality: 1.0e-5,
2857 singular_determinant_relative: 1.0e-6,
2858 };
2859
2860 let on_long_edge = Mat3::from_diagonal(Vec3::new(99_998.5, 99_998.5, 100_000.0));
2865 assert_eq!(
2866 classify_positive_uniform_affine(on_long_edge, policy),
2867 Ok(99_999.0)
2868 );
2869 let short = 99_998.5;
2870 let long = 100_000.0 + 0.007_812_5;
2871 for diagonal in [
2872 Vec3::new(long, short, short),
2873 Vec3::new(short, long, short),
2874 Vec3::new(short, short, long),
2875 ] {
2876 assert_eq!(
2877 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2878 Err(AffineDomainViolation::NonUniformScale)
2879 );
2880 }
2881
2882 let short = 1.0 - 2.0_f32.powi(-16);
2886 for diagonal in [
2887 Vec3::new(short, 1.0, 1.0),
2888 Vec3::new(1.0, short, 1.0),
2889 Vec3::new(1.0, 1.0, short),
2890 ] {
2891 assert_eq!(
2892 classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
2893 Err(AffineDomainViolation::NonUniformScale)
2894 );
2895 }
2896
2897 let c0 = Vec3::new(0.12792248, -0.99066633, -0.047073245);
2900 let c1 = Vec3::new(-0.34637994, -0.00016034879, -0.93809813);
2901 let c2 = Vec3::new(0.92933476, 0.13630849, -0.3431568);
2902 assert!((c1.dot(c2) as f64).abs() < 1.0e-5);
2903 assert!(c1.as_dvec3().dot(c2.as_dvec3()).abs() > 1.0e-5);
2904 assert_eq!(
2905 classify_positive_uniform_affine(Mat3::from_cols(c0, c1, c2), policy),
2906 Err(AffineDomainViolation::Sheared)
2907 );
2908
2909 for shear in [2.0_f32.powi(-15), -2.0_f32.powi(-15)] {
2912 let basis = Mat3::from_cols(Vec3::X, Vec3::new(shear, 1.0, 0.0), Vec3::Z);
2913 assert_eq!(
2914 classify_positive_uniform_affine(basis, policy),
2915 Err(AffineDomainViolation::Sheared)
2916 );
2917 }
2918 }
2919
2920 #[test]
2921 fn affine_axis_mean_is_ascending_and_column_order_invariant() {
2922 let lengths = [
2927 f64::from_bits(0x3ff1_09e7_e000_022c),
2928 f64::from_bits(0x3ff1_09ec_6000_0eb5),
2929 f64::from_bits(0x3ff1_09fa_e000_3cde),
2930 ];
2931 let expected = f64::from_bits(0x3ff1_09ef_b555_6f3f);
2932 let ascending = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
2933 let descending = (lengths[2] + lengths[1] + lengths[0]) / 3.0;
2934 assert_eq!(expected.to_bits(), 0x3ff1_09ef_b555_6f3f);
2935 assert_eq!(ascending.to_bits(), expected.to_bits());
2936 assert_eq!(descending.to_bits(), 0x3ff1_09ef_b555_6f40);
2937 for order in [
2938 [0, 1, 2],
2939 [0, 2, 1],
2940 [1, 0, 2],
2941 [1, 2, 0],
2942 [2, 0, 1],
2943 [2, 1, 0],
2944 ] {
2945 assert_eq!(
2946 average_affine_axis_length(order.map(|index| lengths[index])),
2947 expected,
2948 "axis order {order:?}"
2949 );
2950 }
2951
2952 let dyadic = [2.0_f64.powi(53), 1.0, 1.0];
2957 let ascending = (dyadic[1] + dyadic[2] + dyadic[0]) / 3.0;
2958 let descending = (dyadic[0] + dyadic[1] + dyadic[2]) / 3.0;
2959 assert_ne!(ascending, descending);
2960 assert_eq!(average_affine_axis_length(dyadic), ascending);
2961
2962 let permutations = affine_test_fixtures::appendix_d_v6_mean_permutations();
2968 let expected_length_bits = lengths.map(f64::to_bits);
2969 assert_eq!(
2970 affine_axis_lengths(permutations[0]).map(f64::to_bits),
2971 expected_length_bits
2972 );
2973 let tolerance = PositiveUniformAffineTolerance {
2974 equal_axis: 1.0e-5,
2975 relative_orthogonality: 1.0e-5,
2976 singular_determinant_relative: 1.0e-6,
2977 };
2978 for (permutation, linear) in permutations.into_iter().enumerate() {
2979 assert!(
2980 linear
2981 .x_axis
2982 .as_dvec3()
2983 .cross(linear.y_axis.as_dvec3())
2984 .dot(linear.z_axis.as_dvec3())
2985 > 0.0,
2986 "orientation for permutation {permutation}"
2987 );
2988 assert_eq!(
2989 average_affine_axis_length(affine_axis_lengths(linear)).to_bits(),
2990 expected.to_bits(),
2991 "mean for permutation {permutation}"
2992 );
2993 assert_eq!(
2994 classify_positive_uniform_affine(linear, tolerance),
2995 Err(AffineDomainViolation::NonUniformScale),
2996 "classification for permutation {permutation}"
2997 );
2998 }
2999 }
3000
3001 #[test]
3002 fn shared_affine_classifier_pins_f64_determinant_arithmetic() {
3003 let linear = Mat3::from_cols(
3008 Vec3::new(
3009 f32::from_bits(0x3ff3_5574),
3010 f32::from_bits(0x3f0e_fa3c),
3011 0.0,
3012 ),
3013 Vec3::new(
3014 f32::from_bits(0x3ff5_5e17),
3015 f32::from_bits(0x3f10_2c31),
3016 0.0,
3017 ),
3018 Vec3::Z,
3019 );
3020 let columns = [
3021 linear.x_axis.as_dvec3(),
3022 linear.y_axis.as_dvec3(),
3023 linear.z_axis.as_dvec3(),
3024 ];
3025 let determinant_f64 = columns[2].dot(columns[0].cross(columns[1]));
3026 let determinant_f32 = f64::from(linear.determinant());
3027 let lengths = affine_axis_lengths(linear);
3028 let threshold = (determinant_f64 + determinant_f32) / 2.0;
3029 assert!(determinant_f64 < threshold);
3030 assert!(determinant_f32 > threshold);
3031
3032 assert_eq!(
3033 classify_positive_uniform_affine(
3034 linear,
3035 PositiveUniformAffineTolerance {
3036 equal_axis: 10.0,
3037 relative_orthogonality: 10.0,
3038 singular_determinant_relative: threshold
3039 / (lengths[0] * lengths[1] * lengths[2]),
3040 },
3041 ),
3042 Err(AffineDomainViolation::Singular)
3043 );
3044
3045 let large_uniform = 2.0e19_f32;
3050 assert_eq!(
3051 classify_positive_uniform_affine(
3052 Mat3::from_diagonal(Vec3::splat(large_uniform)),
3053 PositiveUniformAffineTolerance {
3054 equal_axis: 1.0e-5,
3055 relative_orthogonality: 1.0e-5,
3056 singular_determinant_relative: 1.0e-6,
3057 },
3058 ),
3059 Ok(f64::from(large_uniform))
3060 );
3061 }
3062
3063 #[test]
3064 fn affine_geometry_facts_pin_every_widened_field_and_slot() {
3065 let linear = Mat3::from_cols(
3066 Vec3::new(1.0, 2.0, 3.0),
3067 Vec3::new(4.0, 5.0, 6.0),
3068 Vec3::new(7.0, 8.0, 10.0),
3069 );
3070
3071 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3072 assert_eq!(
3073 facts.axis_lengths.map(f64::to_bits),
3074 [
3075 0x400d_eeea_1168_3f49,
3076 0x4021_8cc8_21d6_d3e3,
3077 0x402d_3064_dcc8_ae67,
3078 ]
3079 );
3080 assert_eq!(facts.mean_axis_length.to_bits(), 0x4022_12f7_d653_30b4);
3081 assert_eq!(facts.determinant.to_bits(), 0xc008_0000_0000_0000);
3082 assert_eq!(facts.axis_length_product.to_bits(), 0x407d_f2e3_88f2_1b01);
3083 assert_eq!(
3084 facts.cross_axis_dots.map(f64::to_bits),
3085 [
3086 0x4040_0000_0000_0000,
3087 0x404a_8000_0000_0000,
3088 0x4060_0000_0000_0000,
3089 ],
3090 "cross-axis slots are XY, XZ, YZ"
3091 );
3092 }
3093
3094 #[test]
3095 fn affine_geometry_facts_widen_every_dot_product_before_multiplying() {
3096 let x = Vec3::new(
3097 f32::from_bits(0x3ff3_5574),
3098 f32::from_bits(0x3f0e_fa3c),
3099 0.0,
3100 );
3101 let y = Vec3::new(
3102 f32::from_bits(0x3ff5_5e17),
3103 f32::from_bits(0x3f10_2c31),
3104 0.0,
3105 );
3106 let widened_dot = x.as_dvec3().dot(y.as_dvec3());
3107 let f32_then_widened = f64::from(x.dot(y));
3108
3109 for (slot, linear) in [
3110 (0, Mat3::from_cols(x, y, Vec3::Z)),
3111 (1, Mat3::from_cols(x, Vec3::Z, y)),
3112 (2, Mat3::from_cols(Vec3::Z, x, y)),
3113 ] {
3114 let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
3115 assert_eq!(facts.cross_axis_dots[slot], widened_dot);
3116 assert_ne!(
3117 facts.cross_axis_dots[slot], f32_then_widened,
3118 "dot slot {slot} must multiply and add in f64, not widen an f32 result"
3119 );
3120 }
3121 }
3122
3123 #[test]
3124 fn weld_preserves_uv_seams_at_shared_positions() {
3125 let mut primitive = Primitive {
3126 positions: vec![Vec3::ZERO, Vec3::ZERO, Vec3::ZERO],
3127 uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0]],
3128 ..Primitive::default()
3129 };
3130
3131 primitive.weld();
3132
3133 assert_eq!(primitive.positions.len(), 2);
3134 let reconstructed_corners = primitive
3135 .indices
3136 .iter()
3137 .map(|&index| {
3138 let index = index as usize;
3139 (primitive.positions[index], primitive.uvs[index])
3140 })
3141 .collect::<Vec<_>>();
3142 assert_eq!(
3143 reconstructed_corners,
3144 vec![
3145 (Vec3::ZERO, [0.0, 0.0]),
3146 (Vec3::ZERO, [1.0, 0.0]),
3147 (Vec3::ZERO, [0.0, 0.0]),
3148 ]
3149 );
3150 }
3151}