use glam::{Mat3, Mat4, Quat, Vec3};
use serde::{Deserialize, Serialize};
use std::collections::{BTreeMap, BTreeSet};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum AffineDomainViolation {
NonUniformScale,
Sheared,
Reflected,
Singular,
NonFinite,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct PositiveUniformAffineTolerance {
pub(crate) equal_axis: f64,
pub(crate) relative_orthogonality: f64,
pub(crate) singular_determinant_relative: f64,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct AffineGeometryFacts {
pub(crate) axis_lengths: [f64; 3],
pub(crate) mean_axis_length: f64,
pub(crate) determinant: f64,
pub(crate) axis_length_product: f64,
pub(crate) cross_axis_dots: [f64; 3],
}
impl AffineGeometryFacts {
pub(crate) fn from_linear(linear: Mat3) -> Result<Self, AffineDomainViolation> {
if !linear.is_finite() {
return Err(AffineDomainViolation::NonFinite);
}
let columns = [
linear.x_axis.as_dvec3(),
linear.y_axis.as_dvec3(),
linear.z_axis.as_dvec3(),
];
let axis_lengths = affine_axis_lengths(linear);
let mean_axis_length = average_affine_axis_length(axis_lengths);
let determinant = columns[2].dot(columns[0].cross(columns[1]));
let axis_length_product = axis_lengths[0] * axis_lengths[1] * axis_lengths[2];
let cross_axis_dots = [
columns[0].dot(columns[1]),
columns[0].dot(columns[2]),
columns[1].dot(columns[2]),
];
if axis_lengths.iter().any(|value| !value.is_finite())
|| !mean_axis_length.is_finite()
|| !determinant.is_finite()
|| !axis_length_product.is_finite()
|| cross_axis_dots.iter().any(|value| !value.is_finite())
{
return Err(AffineDomainViolation::NonFinite);
}
Ok(Self {
axis_lengths,
mean_axis_length,
determinant,
axis_length_product,
cross_axis_dots,
})
}
pub(crate) fn has_equal_axis_lengths(self, relative_tolerance: f64) -> bool {
values_equal_to_mean(
&self.axis_lengths,
self.mean_axis_length,
relative_tolerance,
)
}
}
pub(crate) fn values_equal_to_mean(values: &[f64], mean: f64, relative_tolerance: f64) -> bool {
values
.iter()
.all(|&value| (value - mean).abs() <= relative_tolerance * mean.abs().max(value.abs()))
}
pub(crate) fn classify_positive_uniform_affine(
linear: Mat3,
tolerance: PositiveUniformAffineTolerance,
) -> Result<f64, AffineDomainViolation> {
let facts = AffineGeometryFacts::from_linear(linear)?;
if facts.mean_axis_length <= 0.0 {
return Err(AffineDomainViolation::Singular);
}
if facts.determinant.abs()
<= tolerance.singular_determinant_relative * facts.axis_length_product
{
return Err(AffineDomainViolation::Singular);
}
if !facts.has_equal_axis_lengths(tolerance.equal_axis) {
return Err(AffineDomainViolation::NonUniformScale);
}
let orthogonality_tolerance =
tolerance.relative_orthogonality * facts.mean_axis_length * facts.mean_axis_length;
if facts
.cross_axis_dots
.iter()
.any(|dot| dot.abs() > orthogonality_tolerance)
{
return Err(AffineDomainViolation::Sheared);
}
if facts.determinant < 0.0 {
return Err(AffineDomainViolation::Reflected);
}
Ok(facts.mean_axis_length)
}
pub(crate) fn affine_axis_lengths(linear: Mat3) -> [f64; 3] {
[
linear.x_axis.as_dvec3().length(),
linear.y_axis.as_dvec3().length(),
linear.z_axis.as_dvec3().length(),
]
}
pub(crate) fn average_affine_axis_length(lengths: [f64; 3]) -> f64 {
let mut ascending = lengths;
ascending.sort_by(f64::total_cmp);
(ascending[0] + ascending[1] + ascending[2]) / 3.0
}
#[cfg(test)]
pub(crate) mod affine_test_fixtures {
use super::{Mat3, Vec3};
pub(crate) fn tolerance_divergence_basis() -> Mat3 {
Mat3::from_diagonal(Vec3::new(1.0, 1.000_05, 1.0))
}
pub(crate) fn orthogonality_tolerance_divergence_basis() -> Mat3 {
Mat3::from_cols(Vec3::X, Vec3::new(5.0e-5, 1.0, 0.0), Vec3::Z)
}
pub(crate) fn appendix_d_v6_mean_permutations() -> [Mat3; 6] {
let columns = [
Vec3::new(
f32::from_bits(0x3f0e_8cbb),
f32::from_bits(0x3f26_fbbe),
f32::from_bits(0x3f21_9bc7),
),
Vec3::new(
f32::from_bits(0x3d9c_b415),
f32::from_bits(0x3e92_d82b),
f32::from_bits(0x3f82_e85d),
),
Vec3::new(
f32::from_bits(0x3f14_5226),
f32::from_bits(0x3e9e_e50d),
f32::from_bits(0x3f56_817c),
),
];
[
Mat3::from_cols(columns[0], columns[1], columns[2]),
Mat3::from_cols(-columns[0], columns[2], columns[1]),
Mat3::from_cols(-columns[1], columns[0], columns[2]),
Mat3::from_cols(columns[1], columns[2], columns[0]),
Mat3::from_cols(columns[2], columns[0], columns[1]),
Mat3::from_cols(-columns[2], columns[1], columns[0]),
]
}
}
pub type BoneId = usize;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Transform {
pub translation: Vec3,
pub rotation: Quat,
pub scale: Vec3,
}
impl Transform {
pub const IDENTITY: Self = Self {
translation: Vec3::ZERO,
rotation: Quat::IDENTITY,
scale: Vec3::ONE,
};
pub fn to_mat4(&self) -> Mat4 {
Mat4::from_scale_rotation_translation(self.scale, self.rotation, self.translation)
}
}
impl Default for Transform {
fn default() -> Self {
Self::IDENTITY
}
}
#[derive(Debug, Clone)]
pub struct Bone {
pub name: String,
pub parent: Option<BoneId>,
pub rest: Transform,
pub inverse_bind: Option<Mat4>,
}
#[derive(Debug, Clone, Default)]
pub struct Skeleton {
pub bones: Vec<Bone>,
}
impl Skeleton {
pub fn bone_name(&self, id: BoneId) -> &str {
&self.bones[id].name
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum WorldMatrixError {
NonFiniteTransform {
node: BoneId,
},
InvalidParent {
node: BoneId,
parent: BoneId,
},
}
pub(crate) fn world_rest_matrices(skeleton: &Skeleton) -> Result<Vec<Mat4>, WorldMatrixError> {
let mut worlds = Vec::with_capacity(skeleton.bones.len());
for (node, bone) in skeleton.bones.iter().enumerate() {
let local = bone.rest.to_mat4();
if !mat4_is_finite(local) {
return Err(WorldMatrixError::NonFiniteTransform { node });
}
let world = match bone.parent {
Some(parent) if parent < node => worlds[parent] * local,
Some(parent) => return Err(WorldMatrixError::InvalidParent { node, parent }),
None => local,
};
if !mat4_is_finite(world) {
return Err(WorldMatrixError::NonFiniteTransform { node });
}
worlds.push(world);
}
Ok(worlds)
}
pub(crate) fn tolerant_world_rest_matrices(skeleton: &Skeleton) -> Vec<Option<Mat4>> {
let mut worlds = Vec::with_capacity(skeleton.bones.len());
for bone in &skeleton.bones {
let local = bone.rest.to_mat4();
let world = match bone.parent {
Some(parent) => worlds
.get(parent)
.copied()
.flatten()
.map(|parent_world| parent_world * local),
None => Some(local),
}
.filter(|matrix| mat4_is_finite(*matrix));
worlds.push(world);
}
worlds
}
pub(crate) fn mat4_is_finite(matrix: Mat4) -> bool {
matrix.to_cols_array().into_iter().all(f32::is_finite)
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum Property {
Translation,
Rotation,
Scale,
}
impl Property {
pub fn as_str(self) -> &'static str {
match self {
Property::Translation => "translation",
Property::Rotation => "rotation",
Property::Scale => "scale",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Interpolation {
Linear,
Step,
CubicSpline,
}
#[derive(Debug, Clone)]
pub enum TrackValues {
Vec3s(Vec<Vec3>),
Quats(Vec<Quat>),
}
impl TrackValues {
pub fn len(&self) -> usize {
match self {
TrackValues::Vec3s(v) => v.len(),
TrackValues::Quats(v) => v.len(),
}
}
pub fn is_empty(&self) -> bool {
self.len() == 0
}
}
#[derive(Debug, Clone)]
pub struct Track {
pub bone: BoneId,
pub property: Property,
pub interpolation: Interpolation,
pub times: Vec<f32>,
pub values: TrackValues,
}
impl Track {
pub fn key_count(&self) -> usize {
self.times.len()
}
pub fn value_index(&self, k: usize) -> usize {
match self.interpolation {
Interpolation::CubicSpline => 3 * k + 1,
_ => k,
}
}
pub fn key_vec3(&self, k: usize) -> Option<Vec3> {
match &self.values {
TrackValues::Vec3s(v) => v.get(self.value_index(k)).copied(),
TrackValues::Quats(_) => None,
}
}
pub fn key_quat(&self, k: usize) -> Option<Quat> {
match &self.values {
TrackValues::Quats(v) => v.get(self.value_index(k)).copied(),
TrackValues::Vec3s(_) => None,
}
}
pub fn start_time(&self) -> f32 {
self.times.first().copied().unwrap_or(0.0)
}
pub fn end_time(&self) -> f32 {
self.times.last().copied().unwrap_or(0.0)
}
}
#[derive(Debug, Clone)]
pub struct Clip {
pub name: String,
pub duration_s: f64,
pub tracks: Vec<Track>,
}
#[derive(Debug, Clone, Default)]
pub struct SourceInfo {
pub path: Option<String>,
pub format: Option<String>,
}
#[derive(Debug, Clone, Default)]
pub struct Document {
pub skeleton: Skeleton,
pub clips: Vec<Clip>,
pub assets: SceneAssets,
pub source: SourceInfo,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum DocumentShapeError {
#[error("node {node} has a non-finite rest transform")]
NonFiniteSkeletonRest {
node: BoneId,
},
#[error("node {node} has invalid parent {parent}")]
InvalidSkeletonParent {
node: BoneId,
parent: BoneId,
},
#[error("source skeleton declares duplicate source node index {source_node_index}")]
DuplicateSourceNodeIndex {
source_node_index: usize,
},
#[error("source skeleton declares duplicate source skin index {source_skin_index}")]
DuplicateSourceSkinIndex {
source_skin_index: usize,
},
#[error(
"source node {source_node_index} contradicts the document skeleton's parent chain ({violation})"
)]
SourceProjection {
source_node_index: usize,
violation: SourceProjectionViolation,
},
#[error("clip {clip_index} declares duplicate {property:?} tracks for node {node}")]
DuplicateClipTrack {
clip_index: usize,
node: BoneId,
property: Property,
},
#[error("clip {clip_index} track for node {node} has an invalid shape ({violation})")]
TrackShape {
clip_index: usize,
node: BoneId,
violation: TrackShapeViolation,
},
#[error("mesh instance {instance_index} is invalid ({violation})")]
MeshInstanceShape {
instance_index: usize,
violation: MeshInstanceShapeViolation,
},
#[error("node {node} has a non-finite inverse-bind matrix")]
NonFiniteBoneInverseBind {
node: BoneId,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum SourceProjectionViolation {
#[error("projected_bone_out_of_range")]
ProjectedBoneOutOfRange,
#[error("two_source_nodes_project_to_one_bone")]
TwoSourceNodesProjectToOneBone,
#[error("parent_source_node_is_missing")]
ParentSourceNodeMissing,
#[error("cyclic_unprojected_source_parent_chain")]
CyclicUnprojectedSourceParentChain,
#[error("projection_and_skeleton_parents_differ")]
NearestProjectedParentMismatch,
#[error("projected_bone_has_an_unprojected_skeleton_child")]
ProjectedBoneHasUnprojectedSkeletonChild,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum TrackShapeViolation {
#[error("bone_index_out_of_range")]
BoneIndexOutOfRange,
#[error("empty_times")]
EmptyTimes,
#[error("non_finite_time")]
NonFiniteTime,
#[error("times_not_strictly_increasing")]
TimesNotStrictlyIncreasing,
#[error("value_count_mismatch")]
ValueCountMismatch,
#[error("value_type_mismatches_property")]
ValueTypeMismatchesProperty,
#[error("non_finite_value")]
NonFiniteValue,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
#[non_exhaustive]
pub enum MeshInstanceShapeViolation {
#[error("node_index_out_of_range")]
NodeIndexOutOfRange,
#[error("mesh_index_out_of_range")]
MeshIndexOutOfRange,
#[error("skin_joint_out_of_range")]
SkinJointOutOfRange,
#[error("skin_ibm_count_mismatch")]
SkinInverseBindCountMismatch,
#[error("non_finite_inverse_bind")]
NonFiniteSkinInverseBind,
}
#[derive(Debug, Clone, Default)]
pub struct Primitive {
pub material: Option<usize>,
pub indices: Vec<u32>,
pub positions: Vec<Vec3>,
pub normals: Vec<Vec3>,
pub uvs: Vec<[f32; 2]>,
pub joints: Vec<[u16; 4]>,
pub weights: Vec<[f32; 4]>,
pub additional_influence_sets: Vec<AdditionalInfluenceSet>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AdditionalInfluenceSet {
pub set_index: u32,
pub joints_present: bool,
pub weights_present: bool,
}
#[derive(Debug, Clone, Default)]
pub struct MeshAsset {
pub name: String,
pub source_mesh_index: usize,
pub primitives: Vec<Primitive>,
}
#[derive(Debug, Clone, Default)]
pub struct MeshInstance {
pub source_node_index: usize,
pub node: BoneId,
pub mesh: usize,
pub skin_joints: Vec<BoneId>,
pub skin_ibms: Vec<Mat4>,
}
#[derive(Debug, Clone, Default)]
pub struct SceneAsset {
pub source_scene_index: usize,
pub name: Option<String>,
pub roots: Vec<BoneId>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceSkeletonCoverage {
#[default]
Unavailable,
Complete,
}
#[derive(Debug, Clone)]
pub enum SourceNodeLocalRest {
Trs {
translation: Vec3,
rotation: Quat,
scale: Vec3,
},
Matrix(Mat4),
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct SourceNodeAsset {
pub source_node_index: usize,
pub name: Option<String>,
pub parent_source_node_index: Option<usize>,
pub scene_root_indices: Vec<usize>,
pub local_rest: SourceNodeLocalRest,
pub bone: Option<BoneId>,
}
impl SourceNodeAsset {
pub fn new(source_node_index: usize, local_rest: SourceNodeLocalRest) -> Self {
Self {
source_node_index,
name: None,
parent_source_node_index: None,
scene_root_indices: Vec::new(),
local_rest,
bone: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum SourceInverseBindAccessorStatus {
#[default]
Absent,
Available,
EmptyAccessor,
CountMismatch,
Unreadable,
}
#[derive(Debug, Clone, Default)]
pub struct SourceInverseBindAccessor {
pub status: SourceInverseBindAccessorStatus,
pub declared_count: Option<usize>,
pub matrices: Vec<Mat4>,
}
#[derive(Debug, Clone)]
pub struct SourceSkinAttachment {
pub source_node_index: usize,
pub source_mesh_index: Option<usize>,
}
#[derive(Debug, Clone, Default)]
pub struct SourceSkinAsset {
pub source_skin_index: usize,
pub name: Option<String>,
pub skeleton_root_source_node_index: Option<usize>,
pub joint_source_node_indices: Vec<usize>,
pub inverse_bind_accessor: SourceInverseBindAccessor,
pub attachments: Vec<SourceSkinAttachment>,
}
#[derive(Debug, Clone, Default)]
pub struct SourceSkeletonAssets {
pub coverage: SourceSkeletonCoverage,
pub nodes: Vec<SourceNodeAsset>,
pub skins: Vec<SourceSkinAsset>,
}
#[derive(Debug, Clone)]
pub struct TextureAsset {
pub bytes: Vec<u8>,
pub mime: String,
}
#[derive(Debug, Clone)]
pub struct NormalTextureAsset {
pub texture: TextureAsset,
pub scale: f32,
}
#[derive(Debug, Clone)]
pub struct OcclusionTextureAsset {
pub texture: TextureAsset,
pub strength: f32,
}
#[derive(Debug, Clone)]
pub struct MaterialAsset {
pub name: String,
pub base_color: [f32; 4],
pub metallic: f32,
pub roughness: f32,
pub base_color_texture: Option<TextureAsset>,
pub normal_texture: Option<NormalTextureAsset>,
pub metallic_roughness_texture: Option<TextureAsset>,
pub occlusion_texture: Option<OcclusionTextureAsset>,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MaterialResourceCoverage {
Complete,
#[default]
Unavailable,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum MaterialTextureSlot {
BaseColor,
Normal,
MetallicRoughness,
Occlusion,
Emissive,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SourceMaterialTextureBinding {
pub slot: MaterialTextureSlot,
pub texture_index: usize,
}
#[derive(Debug, Clone, Default)]
pub struct SourceMaterialAsset {
pub material_index: usize,
pub name: Option<String>,
pub texture_bindings: Vec<SourceMaterialTextureBinding>,
}
#[derive(Debug, Clone, Default)]
pub struct SourceTextureAsset {
pub texture_index: usize,
pub name: Option<String>,
pub image_index: usize,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImageSourceKind {
Embedded,
DataUri,
External,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImageContainerFormat {
Png,
Jpeg,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum DecodedImageColorType {
L8,
La8,
Rgb8,
Rgba8,
L16,
La16,
Rgb16,
Rgba16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ImageUnavailableReason {
SourceUnavailable,
InvalidDataUri,
UnsupportedContainer,
DecodeFailed,
ResourceLimit,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SourceImageInspection {
Available {
width: u32,
height: u32,
channel_count: u8,
color_type: DecodedImageColorType,
},
Unavailable {
reason: ImageUnavailableReason,
},
}
#[derive(Debug, Clone)]
pub struct SourceImageAsset {
pub image_index: usize,
pub name: Option<String>,
pub source_kind: ImageSourceKind,
pub declared_mime_type: Option<String>,
pub detected_container: Option<ImageContainerFormat>,
pub inspection: SourceImageInspection,
}
#[derive(Debug, Clone, Default)]
pub struct MaterialResourceAssets {
pub coverage: MaterialResourceCoverage,
pub materials: Vec<SourceMaterialAsset>,
pub textures: Vec<SourceTextureAsset>,
pub images: Vec<SourceImageAsset>,
}
impl Primitive {
pub fn weld(&mut self) {
if !self.indices.is_empty() || self.positions.is_empty() {
return;
}
let corner_key = |i: usize| -> Vec<u8> {
let mut key = Vec::with_capacity(64);
let mut push_f32s = |vals: &[f32]| {
for v in vals {
key.extend_from_slice(&v.to_le_bytes());
}
};
push_f32s(&self.positions[i].to_array());
if let Some(n) = self.normals.get(i) {
push_f32s(&n.to_array());
}
if let Some(uv) = self.uvs.get(i) {
push_f32s(uv);
}
if let Some(w) = self.weights.get(i) {
push_f32s(w);
}
if let Some(j) = self.joints.get(i) {
for v in j {
key.extend_from_slice(&v.to_le_bytes());
}
}
key
};
let mut seen: std::collections::HashMap<Vec<u8>, u32> = std::collections::HashMap::new();
let mut indices = Vec::with_capacity(self.positions.len());
let mut positions = Vec::new();
let mut normals = Vec::new();
let mut uvs = Vec::new();
let mut joints = Vec::new();
let mut weights = Vec::new();
for i in 0..self.positions.len() {
let index = *seen.entry(corner_key(i)).or_insert_with(|| {
positions.push(self.positions[i]);
if let Some(n) = self.normals.get(i) {
normals.push(*n);
}
if let Some(uv) = self.uvs.get(i) {
uvs.push(*uv);
}
if let Some(j) = self.joints.get(i) {
joints.push(*j);
}
if let Some(w) = self.weights.get(i) {
weights.push(*w);
}
(positions.len() - 1) as u32
});
indices.push(index);
}
self.indices = indices;
self.positions = positions;
self.normals = normals;
self.uvs = uvs;
self.joints = joints;
self.weights = weights;
}
}
#[derive(Debug, Clone, Default)]
pub struct SceneAssets {
pub meshes: Vec<MeshAsset>,
pub instances: Vec<MeshInstance>,
pub materials: Vec<MaterialAsset>,
pub material_resources: MaterialResourceAssets,
pub scenes: Vec<SceneAsset>,
pub default_scene: Option<usize>,
pub source_skeleton: SourceSkeletonAssets,
}
pub fn validate_document_shape(document: &Document) -> Result<(), DocumentShapeError> {
validate_skeleton_rest(&document.skeleton)?;
validate_source_skeleton_identity(&document.assets.source_skeleton)?;
validate_source_projection(document)?;
validate_clip_tracks(document)?;
validate_mesh_instances(document)?;
validate_bone_inverse_binds(&document.skeleton)
}
fn validate_skeleton_rest(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
world_rest_matrices(skeleton)
.map(|_| ())
.map_err(|error| match error {
WorldMatrixError::NonFiniteTransform { node } => {
DocumentShapeError::NonFiniteSkeletonRest { node }
}
WorldMatrixError::InvalidParent { node, parent } => {
DocumentShapeError::InvalidSkeletonParent { node, parent }
}
})
}
fn validate_source_skeleton_identity(
source_skeleton: &SourceSkeletonAssets,
) -> Result<(), DocumentShapeError> {
let mut seen_nodes = BTreeSet::new();
for node in &source_skeleton.nodes {
if !seen_nodes.insert(node.source_node_index) {
return Err(DocumentShapeError::DuplicateSourceNodeIndex {
source_node_index: node.source_node_index,
});
}
}
let mut seen_skins = BTreeSet::new();
for skin in &source_skeleton.skins {
if !seen_skins.insert(skin.source_skin_index) {
return Err(DocumentShapeError::DuplicateSourceSkinIndex {
source_skin_index: skin.source_skin_index,
});
}
}
Ok(())
}
fn validate_source_projection(document: &Document) -> Result<(), DocumentShapeError> {
let source_skeleton = &document.assets.source_skeleton;
if source_skeleton.coverage != SourceSkeletonCoverage::Complete {
return Ok(());
}
let bones = &document.skeleton.bones;
let mut bone_of_source = BTreeMap::new();
let mut source_of_bone = BTreeMap::new();
let mut skeleton_parents = Vec::with_capacity(source_skeleton.nodes.len());
for node in &source_skeleton.nodes {
let Some(bone) = node.bone else {
continue;
};
let skeleton_parent = bones
.get(bone)
.ok_or(DocumentShapeError::SourceProjection {
source_node_index: node.source_node_index,
violation: SourceProjectionViolation::ProjectedBoneOutOfRange,
})?
.parent;
if source_of_bone
.insert(bone, node.source_node_index)
.is_some()
{
return Err(DocumentShapeError::SourceProjection {
source_node_index: node.source_node_index,
violation: SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
});
}
bone_of_source.insert(node.source_node_index, bone);
skeleton_parents.push((node, skeleton_parent));
}
let by_source_index: BTreeMap<_, _> = source_skeleton
.nodes
.iter()
.map(|node| (node.source_node_index, node))
.collect();
let unprojected_rows = source_skeleton.nodes.len() - bone_of_source.len();
let mut resolved_unprojected = BTreeMap::<usize, Option<BoneId>>::new();
for (node, skeleton_parent) in skeleton_parents {
let mut cursor = node.parent_source_node_index;
let mut unresolved_suffix = Vec::new();
let projected_parent = loop {
let Some(parent_source_node_index) = cursor else {
break None;
};
if let Some(&bone) = bone_of_source.get(&parent_source_node_index) {
break Some(bone);
}
if let Some(&projected_parent) = resolved_unprojected.get(&parent_source_node_index) {
break projected_parent;
}
let parent = by_source_index.get(&parent_source_node_index).ok_or(
DocumentShapeError::SourceProjection {
source_node_index: node.source_node_index,
violation: SourceProjectionViolation::ParentSourceNodeMissing,
},
)?;
unresolved_suffix.push(parent_source_node_index);
if unresolved_suffix.len() > unprojected_rows {
return Err(DocumentShapeError::SourceProjection {
source_node_index: node.source_node_index,
violation: SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
});
}
cursor = parent.parent_source_node_index;
};
for source_node_index in unresolved_suffix {
resolved_unprojected.insert(source_node_index, projected_parent);
}
if projected_parent != skeleton_parent {
return Err(DocumentShapeError::SourceProjection {
source_node_index: node.source_node_index,
violation: SourceProjectionViolation::NearestProjectedParentMismatch,
});
}
}
for (bone, child) in bones.iter().enumerate() {
if source_of_bone.contains_key(&bone) {
continue;
}
if let Some(parent) = child.parent
&& let Some(&source_node_index) = source_of_bone.get(&parent)
{
return Err(DocumentShapeError::SourceProjection {
source_node_index,
violation: SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
});
}
}
Ok(())
}
fn validate_clip_tracks(document: &Document) -> Result<(), DocumentShapeError> {
let bone_count = document.skeleton.bones.len();
for (clip_index, clip) in document.clips.iter().enumerate() {
let mut seen = Vec::with_capacity(clip.tracks.len());
for track in &clip.tracks {
if track.bone >= bone_count {
return Err(DocumentShapeError::TrackShape {
clip_index,
node: track.bone,
violation: TrackShapeViolation::BoneIndexOutOfRange,
});
}
if seen.contains(&(track.bone, track.property)) {
return Err(DocumentShapeError::DuplicateClipTrack {
clip_index,
node: track.bone,
property: track.property,
});
}
seen.push((track.bone, track.property));
validate_track_shape(clip_index, track)?;
}
}
Ok(())
}
pub(crate) fn validate_track_shape(
clip_index: usize,
track: &Track,
) -> Result<(), DocumentShapeError> {
let violation = if track.times.is_empty() {
Some(TrackShapeViolation::EmptyTimes)
} else if track.times.iter().any(|time| !time.is_finite()) {
Some(TrackShapeViolation::NonFiniteTime)
} else if track.times.windows(2).any(|times| times[0] >= times[1]) {
Some(TrackShapeViolation::TimesNotStrictlyIncreasing)
} else {
let expected_values = match track.interpolation {
Interpolation::CubicSpline => track.times.len().checked_mul(3),
Interpolation::Linear | Interpolation::Step => Some(track.times.len()),
};
if expected_values != Some(track.values.len()) {
Some(TrackShapeViolation::ValueCountMismatch)
} else if !matches!(
(&track.values, track.property),
(
TrackValues::Vec3s(_),
Property::Translation | Property::Scale
) | (TrackValues::Quats(_), Property::Rotation)
) {
Some(TrackShapeViolation::ValueTypeMismatchesProperty)
} else if match &track.values {
TrackValues::Vec3s(values) => values.iter().any(|value| !value.is_finite()),
TrackValues::Quats(values) => values.iter().any(|value| !value.is_finite()),
} {
Some(TrackShapeViolation::NonFiniteValue)
} else {
None
}
};
violation.map_or(Ok(()), |violation| {
Err(DocumentShapeError::TrackShape {
clip_index,
node: track.bone,
violation,
})
})
}
fn validate_mesh_instances(document: &Document) -> Result<(), DocumentShapeError> {
let bone_count = document.skeleton.bones.len();
let mesh_count = document.assets.meshes.len();
for (instance_index, instance) in document.assets.instances.iter().enumerate() {
let violation = if instance.node >= bone_count {
Some(MeshInstanceShapeViolation::NodeIndexOutOfRange)
} else if instance.mesh >= mesh_count {
Some(MeshInstanceShapeViolation::MeshIndexOutOfRange)
} else if instance
.skin_joints
.iter()
.any(|&joint| joint >= bone_count)
{
Some(MeshInstanceShapeViolation::SkinJointOutOfRange)
} else if !instance.skin_ibms.is_empty()
&& instance.skin_ibms.len() != instance.skin_joints.len()
{
Some(MeshInstanceShapeViolation::SkinInverseBindCountMismatch)
} else if instance.skin_ibms.iter().any(|ibm| !mat4_is_finite(*ibm)) {
Some(MeshInstanceShapeViolation::NonFiniteSkinInverseBind)
} else {
None
};
if let Some(violation) = violation {
return Err(DocumentShapeError::MeshInstanceShape {
instance_index,
violation,
});
}
}
Ok(())
}
fn validate_bone_inverse_binds(skeleton: &Skeleton) -> Result<(), DocumentShapeError> {
for (node, bone) in skeleton.bones.iter().enumerate() {
if let Some(inverse_bind) = bone.inverse_bind
&& !mat4_is_finite(inverse_bind)
{
return Err(DocumentShapeError::NonFiniteBoneInverseBind { node });
}
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn property_serde_uses_the_stable_trs_vocabulary() {
assert_eq!(
serde_json::to_value([Property::Translation, Property::Rotation, Property::Scale,])
.expect("properties serialize"),
serde_json::json!(["translation", "rotation", "scale"])
);
assert_eq!(
serde_json::from_value::<Vec<Property>>(serde_json::json!([
"translation",
"rotation",
"scale"
]))
.expect("properties deserialize"),
[Property::Translation, Property::Rotation, Property::Scale,]
);
}
fn bone(parent: Option<BoneId>) -> Bone {
Bone {
name: "bone".into(),
parent,
rest: Transform::IDENTITY,
inverse_bind: None,
}
}
fn one_bone_document() -> Document {
Document {
skeleton: Skeleton {
bones: vec![bone(None)],
},
..Document::default()
}
}
fn source_node(
source_node_index: usize,
parent_source_node_index: Option<usize>,
bone: Option<BoneId>,
) -> SourceNodeAsset {
SourceNodeAsset {
source_node_index,
name: None,
parent_source_node_index,
scene_root_indices: Vec::new(),
local_rest: SourceNodeLocalRest::Trs {
translation: Vec3::ZERO,
rotation: Quat::IDENTITY,
scale: Vec3::ONE,
},
bone,
}
}
fn valid_track() -> Track {
Track {
bone: 0,
property: Property::Translation,
interpolation: Interpolation::Linear,
times: vec![0.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO]),
}
}
fn track_document(track: Track) -> Document {
let mut document = one_bone_document();
document.clips.push(Clip {
name: "clip".into(),
duration_s: 0.0,
tracks: vec![track],
});
document
}
fn instance_document() -> Document {
let mut document = one_bone_document();
document.assets.meshes.push(MeshAsset::default());
document.assets.instances.push(MeshInstance {
node: 0,
mesh: 0,
..MeshInstance::default()
});
document
}
#[test]
fn document_shape_validation_accepts_a_complete_projection_with_an_unprojected_intermediate() {
let mut document = Document {
skeleton: Skeleton {
bones: vec![bone(None), bone(Some(0))],
},
assets: SceneAssets {
source_skeleton: SourceSkeletonAssets {
coverage: SourceSkeletonCoverage::Complete,
nodes: vec![
source_node(10, None, Some(0)),
source_node(11, Some(10), None),
source_node(12, Some(11), Some(1)),
],
..SourceSkeletonAssets::default()
},
meshes: vec![MeshAsset::default()],
instances: vec![MeshInstance {
node: 1,
mesh: 0,
skin_joints: vec![0, 1],
skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
..MeshInstance::default()
}],
..SceneAssets::default()
},
..Document::default()
};
document.clips.push(Clip {
name: "clip".into(),
duration_s: 0.0,
tracks: vec![valid_track()],
});
assert_eq!(validate_document_shape(&document), Ok(()));
}
#[test]
fn shared_unprojected_parent_suffix_preserves_each_projected_parent() {
const CONNECTORS: usize = 64;
const PROJECTED_CHILDREN: usize = 64;
let mut nodes = Vec::with_capacity(1 + CONNECTORS + PROJECTED_CHILDREN);
nodes.push(source_node(0, None, Some(0)));
for source_node_index in 1..=CONNECTORS {
nodes.push(source_node(
source_node_index,
Some(source_node_index - 1),
None,
));
}
for child in 0..PROJECTED_CHILDREN {
nodes.push(source_node(
1 + CONNECTORS + child,
Some(CONNECTORS),
Some(1 + child),
));
}
let document = Document {
skeleton: Skeleton {
bones: std::iter::once(bone(None))
.chain((0..PROJECTED_CHILDREN).map(|_| bone(Some(0))))
.collect(),
},
assets: SceneAssets {
source_skeleton: SourceSkeletonAssets {
coverage: SourceSkeletonCoverage::Complete,
nodes,
..SourceSkeletonAssets::default()
},
..SceneAssets::default()
},
..Document::default()
};
assert_eq!(validate_document_shape(&document), Ok(()));
let mut mismatched = document.clone();
mismatched.skeleton.bones[PROJECTED_CHILDREN].parent = None;
assert_eq!(
validate_document_shape(&mismatched),
Err(DocumentShapeError::SourceProjection {
source_node_index: CONNECTORS + PROJECTED_CHILDREN,
violation: SourceProjectionViolation::NearestProjectedParentMismatch,
})
);
}
#[test]
fn document_shape_validation_has_an_analytic_error_for_every_variant() {
let projection_error =
|source_node_index, violation| DocumentShapeError::SourceProjection {
source_node_index,
violation,
};
let track_error = |node, violation| DocumentShapeError::TrackShape {
clip_index: 0,
node,
violation,
};
let instance_error = |violation| DocumentShapeError::MeshInstanceShape {
instance_index: 0,
violation,
};
let mut non_finite_rest = one_bone_document();
non_finite_rest.skeleton.bones[0].rest.translation.x = f32::NAN;
let overflowed_rest_world = Document {
skeleton: Skeleton {
bones: vec![
Bone {
rest: Transform {
scale: Vec3::splat(f32::MAX),
..Transform::IDENTITY
},
..bone(None)
},
Bone {
rest: Transform {
translation: Vec3::splat(2.0),
..Transform::IDENTITY
},
..bone(Some(0))
},
],
},
..Document::default()
};
let self_parent = Document {
skeleton: Skeleton {
bones: vec![bone(Some(0))],
},
..Document::default()
};
let forward_parent = Document {
skeleton: Skeleton {
bones: vec![bone(Some(1)), bone(None)],
},
..Document::default()
};
let far_parent = Document {
skeleton: Skeleton {
bones: vec![bone(Some(99))],
},
..Document::default()
};
let duplicate_node = Document {
assets: SceneAssets {
source_skeleton: SourceSkeletonAssets {
nodes: vec![
source_node(9, None, None),
source_node(10, None, None),
source_node(9, None, None),
],
..SourceSkeletonAssets::default()
},
..SceneAssets::default()
},
..Document::default()
};
let duplicate_skin = Document {
assets: SceneAssets {
source_skeleton: SourceSkeletonAssets {
skins: vec![
SourceSkinAsset {
source_skin_index: 4,
..SourceSkinAsset::default()
},
SourceSkinAsset {
source_skin_index: 5,
..SourceSkinAsset::default()
},
SourceSkinAsset {
source_skin_index: 4,
..SourceSkinAsset::default()
},
],
..SourceSkeletonAssets::default()
},
..SceneAssets::default()
},
..Document::default()
};
let complete_projection = |nodes| SceneAssets {
source_skeleton: SourceSkeletonAssets {
coverage: SourceSkeletonCoverage::Complete,
nodes,
..SourceSkeletonAssets::default()
},
..SceneAssets::default()
};
let out_of_range_projection = Document {
skeleton: Skeleton {
bones: vec![bone(None)],
},
assets: complete_projection(vec![source_node(10, None, Some(1))]),
..Document::default()
};
let non_injective_projection = Document {
skeleton: Skeleton {
bones: vec![bone(None)],
},
assets: complete_projection(vec![
source_node(10, None, Some(0)),
source_node(11, None, Some(0)),
]),
..Document::default()
};
let missing_projection_parent = Document {
skeleton: Skeleton {
bones: vec![bone(None), bone(Some(0))],
},
assets: complete_projection(vec![source_node(11, Some(99), Some(1))]),
..Document::default()
};
let missing_projection_parent_at_cycle_bound = Document {
skeleton: Skeleton {
bones: vec![bone(None), bone(Some(0))],
},
assets: complete_projection(vec![
source_node(10, None, Some(0)),
source_node(11, Some(12), Some(1)),
source_node(12, Some(99), None),
]),
..Document::default()
};
let cyclic_unprojected_parent = Document {
skeleton: Skeleton {
bones: vec![bone(None), bone(Some(0))],
},
assets: complete_projection(vec![
source_node(11, Some(12), Some(1)),
source_node(12, Some(12), None),
]),
..Document::default()
};
let cyclic_unprojected_parent_pair = Document {
skeleton: Skeleton {
bones: vec![bone(None), bone(Some(0))],
},
assets: complete_projection(vec![
source_node(11, Some(12), Some(1)),
source_node(12, Some(13), None),
source_node(13, Some(12), None),
]),
..Document::default()
};
let mismatched_nearest_parent = Document {
skeleton: Skeleton {
bones: vec![bone(None), bone(Some(0))],
},
assets: complete_projection(vec![
source_node(10, None, Some(0)),
source_node(11, None, Some(1)),
]),
..Document::default()
};
let unprojected_child = Document {
skeleton: Skeleton {
bones: vec![bone(None), bone(Some(0))],
},
assets: complete_projection(vec![source_node(10, None, Some(0))]),
..Document::default()
};
let duplicate_track = {
let track = valid_track();
let mut document = track_document(track.clone());
document.clips[0].tracks.push(Track {
property: Property::Scale,
..valid_track()
});
document.clips[0].tracks.push(track);
document
};
let mut boundary_out_of_range_track = valid_track();
boundary_out_of_range_track.bone = 1;
let mut far_out_of_range_track = valid_track();
far_out_of_range_track.bone = 99;
let empty_track = Track {
times: Vec::new(),
values: TrackValues::Vec3s(Vec::new()),
..valid_track()
};
let non_finite_later_time = Track {
times: vec![0.0, f32::NAN],
values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
..valid_track()
};
let unordered_times = Track {
times: vec![1.0, 0.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
..valid_track()
};
let equal_times = Track {
times: vec![0.0, 0.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
..valid_track()
};
let wrong_linear_value_count = Track {
values: TrackValues::Vec3s(Vec::new()),
..valid_track()
};
let excess_linear_value_count = Track {
values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
..valid_track()
};
let wrong_step_value_count = Track {
interpolation: Interpolation::Step,
times: vec![0.0, 1.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO]),
..valid_track()
};
let excess_step_value_count = Track {
interpolation: Interpolation::Step,
values: TrackValues::Vec3s(vec![Vec3::ZERO, Vec3::ZERO]),
..valid_track()
};
let wrong_cubic_value_count = Track {
interpolation: Interpolation::CubicSpline,
times: vec![0.0, 1.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
..valid_track()
};
let excess_cubic_value_count = Track {
interpolation: Interpolation::CubicSpline,
values: TrackValues::Vec3s(vec![Vec3::ZERO; 4]),
..valid_track()
};
let wrong_translation_value_type = Track {
values: TrackValues::Quats(vec![Quat::IDENTITY]),
..valid_track()
};
let wrong_scale_value_type = Track {
property: Property::Scale,
values: TrackValues::Quats(vec![Quat::IDENTITY]),
..valid_track()
};
let wrong_rotation_value_type = Track {
property: Property::Rotation,
values: TrackValues::Vec3s(vec![Vec3::ZERO]),
..valid_track()
};
let non_finite_value = Track {
values: TrackValues::Vec3s(vec![Vec3::splat(f32::NAN)]),
..valid_track()
};
let mut bad_instance_node = instance_document();
bad_instance_node.assets.instances[0].node = 1;
let mut far_instance_node = instance_document();
far_instance_node.assets.instances[0].node = 99;
let mut bad_instance_mesh = instance_document();
bad_instance_mesh.assets.instances[0].mesh = 1;
let mut far_instance_mesh = instance_document();
far_instance_mesh.assets.instances[0].mesh = 99;
let mut bad_instance_joint = instance_document();
bad_instance_joint.assets.instances[0].skin_joints = vec![1];
let mut far_instance_joint = instance_document();
far_instance_joint.assets.instances[0].skin_joints = vec![99];
let mut bad_instance_count = instance_document();
bad_instance_count.assets.instances[0].skin_joints = vec![0];
bad_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, Mat4::IDENTITY];
let mut short_instance_count = instance_document();
short_instance_count.skeleton.bones.push(bone(Some(0)));
short_instance_count.assets.instances[0].skin_joints = vec![0, 1];
short_instance_count.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY];
let mut bad_instance_ibm = instance_document();
bad_instance_ibm.assets.instances[0].skin_joints = vec![0];
bad_instance_ibm.assets.instances[0].skin_ibms =
vec![Mat4::from_cols_array(&[f32::NAN; 16])];
let mut bad_bone_ibm = one_bone_document();
bad_bone_ibm.skeleton.bones[0].inverse_bind = Some(Mat4::from_cols_array(&[f32::NAN; 16]));
let cases = vec![
(
"non-finite rest",
non_finite_rest,
DocumentShapeError::NonFiniteSkeletonRest { node: 0 },
),
(
"non-finite composed rest world",
overflowed_rest_world,
DocumentShapeError::NonFiniteSkeletonRest { node: 1 },
),
(
"self parent",
self_parent,
DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 0 },
),
(
"forward parent",
forward_parent,
DocumentShapeError::InvalidSkeletonParent { node: 0, parent: 1 },
),
(
"far parent",
far_parent,
DocumentShapeError::InvalidSkeletonParent {
node: 0,
parent: 99,
},
),
(
"duplicate source node",
duplicate_node,
DocumentShapeError::DuplicateSourceNodeIndex {
source_node_index: 9,
},
),
(
"duplicate source skin",
duplicate_skin,
DocumentShapeError::DuplicateSourceSkinIndex {
source_skin_index: 4,
},
),
(
"projected bone range",
out_of_range_projection,
projection_error(10, SourceProjectionViolation::ProjectedBoneOutOfRange),
),
(
"projection injectivity",
non_injective_projection,
projection_error(
11,
SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
),
),
(
"missing projection parent",
missing_projection_parent,
projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
),
(
"missing projection parent at cycle bound",
missing_projection_parent_at_cycle_bound,
projection_error(11, SourceProjectionViolation::ParentSourceNodeMissing),
),
(
"cyclic projection parent",
cyclic_unprojected_parent,
projection_error(
11,
SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
),
),
(
"cyclic projection parent pair",
cyclic_unprojected_parent_pair,
projection_error(
11,
SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
),
),
(
"nearest projection parent",
mismatched_nearest_parent,
projection_error(
11,
SourceProjectionViolation::NearestProjectedParentMismatch,
),
),
(
"projection downward closure",
unprojected_child,
projection_error(
10,
SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
),
),
(
"duplicate track",
duplicate_track,
DocumentShapeError::DuplicateClipTrack {
clip_index: 0,
node: 0,
property: Property::Translation,
},
),
(
"track bone range boundary",
track_document(boundary_out_of_range_track),
track_error(1, TrackShapeViolation::BoneIndexOutOfRange),
),
(
"track bone range far",
track_document(far_out_of_range_track),
track_error(99, TrackShapeViolation::BoneIndexOutOfRange),
),
(
"empty track",
track_document(empty_track),
track_error(0, TrackShapeViolation::EmptyTimes),
),
(
"non-finite time",
track_document(non_finite_later_time),
track_error(0, TrackShapeViolation::NonFiniteTime),
),
(
"unordered times",
track_document(unordered_times),
track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
),
(
"equal times",
track_document(equal_times),
track_error(0, TrackShapeViolation::TimesNotStrictlyIncreasing),
),
(
"linear value count",
track_document(wrong_linear_value_count),
track_error(0, TrackShapeViolation::ValueCountMismatch),
),
(
"linear excess value count",
track_document(excess_linear_value_count),
track_error(0, TrackShapeViolation::ValueCountMismatch),
),
(
"step value count",
track_document(wrong_step_value_count),
track_error(0, TrackShapeViolation::ValueCountMismatch),
),
(
"step excess value count",
track_document(excess_step_value_count),
track_error(0, TrackShapeViolation::ValueCountMismatch),
),
(
"cubic value count",
track_document(wrong_cubic_value_count),
track_error(0, TrackShapeViolation::ValueCountMismatch),
),
(
"cubic excess value count",
track_document(excess_cubic_value_count),
track_error(0, TrackShapeViolation::ValueCountMismatch),
),
(
"translation value type",
track_document(wrong_translation_value_type),
track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
),
(
"scale value type",
track_document(wrong_scale_value_type),
track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
),
(
"rotation value type",
track_document(wrong_rotation_value_type),
track_error(0, TrackShapeViolation::ValueTypeMismatchesProperty),
),
(
"non-finite value",
track_document(non_finite_value),
track_error(0, TrackShapeViolation::NonFiniteValue),
),
(
"instance node boundary",
bad_instance_node,
instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
),
(
"instance node far",
far_instance_node,
instance_error(MeshInstanceShapeViolation::NodeIndexOutOfRange),
),
(
"instance mesh boundary",
bad_instance_mesh,
instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
),
(
"instance mesh far",
far_instance_mesh,
instance_error(MeshInstanceShapeViolation::MeshIndexOutOfRange),
),
(
"instance joint boundary",
bad_instance_joint,
instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
),
(
"instance joint far",
far_instance_joint,
instance_error(MeshInstanceShapeViolation::SkinJointOutOfRange),
),
(
"instance ibm count excess",
bad_instance_count,
instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
),
(
"instance ibm count short",
short_instance_count,
instance_error(MeshInstanceShapeViolation::SkinInverseBindCountMismatch),
),
(
"instance ibm finite",
bad_instance_ibm,
instance_error(MeshInstanceShapeViolation::NonFiniteSkinInverseBind),
),
(
"bone ibm finite",
bad_bone_ibm,
DocumentShapeError::NonFiniteBoneInverseBind { node: 0 },
),
];
for (name, document, expected) in cases {
assert_eq!(validate_document_shape(&document), Err(expected), "{name}");
}
}
#[test]
fn document_shape_finiteness_checks_every_stored_component() {
for component in 0..3 {
let mut translation = Vec3::ZERO.to_array();
translation[component] = f32::NAN;
let mut document = one_bone_document();
document.skeleton.bones[0].rest.translation = Vec3::from_array(translation);
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
"rest translation component {component}"
);
let mut scale = Vec3::ONE.to_array();
scale[component] = f32::NAN;
let mut document = one_bone_document();
document.skeleton.bones[0].rest.scale = Vec3::from_array(scale);
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
"rest scale component {component}"
);
let mut value = Vec3::ZERO.to_array();
value[component] = f32::NAN;
let document = track_document(Track {
values: TrackValues::Vec3s(vec![Vec3::from_array(value)]),
..valid_track()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::TrackShape {
clip_index: 0,
node: 0,
violation: TrackShapeViolation::NonFiniteValue,
}),
"track Vec3 component {component}"
);
}
for component in 0..4 {
let mut rotation = Quat::IDENTITY.to_array();
rotation[component] = f32::NAN;
let mut document = one_bone_document();
document.skeleton.bones[0].rest.rotation = Quat::from_array(rotation);
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::NonFiniteSkeletonRest { node: 0 }),
"rest rotation component {component}"
);
let document = track_document(Track {
property: Property::Rotation,
values: TrackValues::Quats(vec![Quat::from_array(rotation)]),
..valid_track()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::TrackShape {
clip_index: 0,
node: 0,
violation: TrackShapeViolation::NonFiniteValue,
}),
"track quaternion component {component}"
);
}
for key in 0..3 {
let mut times = vec![0.0, 1.0, 2.0];
times[key] = f32::NAN;
let document = track_document(Track {
times,
values: TrackValues::Vec3s(vec![Vec3::ZERO; 3]),
..valid_track()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::TrackShape {
clip_index: 0,
node: 0,
violation: TrackShapeViolation::NonFiniteTime,
}),
"track time {key}"
);
}
for component in 0..16 {
let mut columns = Mat4::IDENTITY.to_cols_array();
columns[component] = f32::NAN;
let inverse_bind = Mat4::from_cols_array(&columns);
let mut instance_document = instance_document();
instance_document.assets.instances[0].skin_joints = vec![0];
instance_document.assets.instances[0].skin_ibms = vec![inverse_bind];
assert_eq!(
validate_document_shape(&instance_document),
Err(DocumentShapeError::MeshInstanceShape {
instance_index: 0,
violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
}),
"instance inverse-bind component {component}"
);
let mut bone_document = one_bone_document();
bone_document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
assert_eq!(
validate_document_shape(&bone_document),
Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
"bone inverse-bind component {component}"
);
}
}
#[test]
fn document_shape_rejects_duplicate_tracks_for_every_property() {
let tracks = [
(Property::Translation, TrackValues::Vec3s(vec![Vec3::ZERO])),
(Property::Scale, TrackValues::Vec3s(vec![Vec3::ONE])),
(Property::Rotation, TrackValues::Quats(vec![Quat::IDENTITY])),
];
for (property, values) in tracks {
let track = Track {
property,
values,
..valid_track()
};
let mut document = track_document(track.clone());
document.clips[0].tracks.push(track);
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::DuplicateClipTrack {
clip_index: 0,
node: 0,
property,
}),
"duplicate {property:?} track"
);
}
}
#[test]
fn document_shape_rejects_infinite_times_quaternions_and_inverse_binds() {
for non_finite in [f32::INFINITY, f32::NEG_INFINITY] {
let document = track_document(Track {
times: vec![non_finite],
values: TrackValues::Vec3s(vec![Vec3::ZERO]),
..valid_track()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::TrackShape {
clip_index: 0,
node: 0,
violation: TrackShapeViolation::NonFiniteTime,
}),
"track time {non_finite}"
);
let document = track_document(Track {
property: Property::Rotation,
values: TrackValues::Quats(vec![Quat::from_xyzw(non_finite, 0.0, 0.0, 1.0)]),
..valid_track()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::TrackShape {
clip_index: 0,
node: 0,
violation: TrackShapeViolation::NonFiniteValue,
}),
"track quaternion {non_finite}"
);
let mut columns = Mat4::IDENTITY.to_cols_array();
columns[0] = non_finite;
let inverse_bind = Mat4::from_cols_array(&columns);
let mut document = instance_document();
document.assets.instances[0].skin_joints = vec![0];
document.assets.instances[0].skin_ibms = vec![inverse_bind];
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::MeshInstanceShape {
instance_index: 0,
violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
}),
"instance inverse bind {non_finite}"
);
let mut document = one_bone_document();
document.skeleton.bones[0].inverse_bind = Some(inverse_bind);
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 0 }),
"bone inverse bind {non_finite}"
);
}
}
#[test]
fn document_shape_checks_mesh_and_joint_references_on_later_instances() {
let later_instance = MeshInstance {
node: 0,
mesh: 0,
..MeshInstance::default()
};
let mut document = instance_document();
document.assets.instances.push(later_instance.clone());
document.assets.instances[1].mesh = 1;
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::MeshInstanceShape {
instance_index: 1,
violation: MeshInstanceShapeViolation::MeshIndexOutOfRange,
})
);
let mut document = instance_document();
document.assets.instances.push(later_instance);
document.assets.instances[1].skin_joints = vec![1];
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::MeshInstanceShape {
instance_index: 1,
violation: MeshInstanceShapeViolation::SkinJointOutOfRange,
})
);
}
#[test]
fn document_shape_finds_duplicates_that_do_not_involve_the_first_item() {
let mut document = Document::default();
document.assets.source_skeleton.skins = [4, 5, 5]
.into_iter()
.map(|source_skin_index| SourceSkinAsset {
source_skin_index,
..SourceSkinAsset::default()
})
.collect();
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::DuplicateSourceSkinIndex {
source_skin_index: 5,
})
);
let scale_track = Track {
property: Property::Scale,
values: TrackValues::Vec3s(vec![Vec3::ONE]),
..valid_track()
};
let mut document = track_document(valid_track());
document.clips[0].tracks.push(scale_track.clone());
document.clips[0].tracks.push(scale_track);
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::DuplicateClipTrack {
clip_index: 0,
node: 0,
property: Property::Scale,
})
);
}
#[test]
fn document_shape_checks_later_tracks_and_inverse_binds() {
let mut document = track_document(valid_track());
document.clips[0].tracks.push(Track {
property: Property::Scale,
times: Vec::new(),
values: TrackValues::Vec3s(Vec::new()),
..valid_track()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::TrackShape {
clip_index: 0,
node: 0,
violation: TrackShapeViolation::EmptyTimes,
})
);
let scale_track = Track {
property: Property::Scale,
values: TrackValues::Vec3s(vec![Vec3::ONE]),
..valid_track()
};
let mut document = track_document(valid_track());
document.clips.push(Clip {
name: "later".into(),
duration_s: 0.0,
tracks: vec![scale_track.clone(), scale_track],
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::DuplicateClipTrack {
clip_index: 1,
node: 0,
property: Property::Scale,
})
);
let mut document = track_document(valid_track());
document.clips.push(Clip {
name: "later".into(),
duration_s: 0.0,
tracks: vec![Track {
property: Property::Scale,
times: Vec::new(),
values: TrackValues::Vec3s(Vec::new()),
..valid_track()
}],
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::TrackShape {
clip_index: 1,
node: 0,
violation: TrackShapeViolation::EmptyTimes,
})
);
let mut columns = Mat4::IDENTITY.to_cols_array();
columns[15] = f32::NAN;
let non_finite_inverse_bind = Mat4::from_cols_array(&columns);
let mut document = instance_document();
document.skeleton.bones.push(bone(Some(0)));
document.assets.instances[0].skin_joints = vec![0, 1];
document.assets.instances[0].skin_ibms = vec![Mat4::IDENTITY, non_finite_inverse_bind];
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::MeshInstanceShape {
instance_index: 0,
violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
})
);
let mut document = instance_document();
document.assets.instances.push(MeshInstance {
node: 0,
mesh: 0,
skin_joints: vec![0],
skin_ibms: vec![non_finite_inverse_bind],
..MeshInstance::default()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::MeshInstanceShape {
instance_index: 1,
violation: MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
})
);
let mut document = instance_document();
document.assets.instances.push(MeshInstance {
node: 0,
mesh: 0,
skin_joints: vec![0],
skin_ibms: vec![Mat4::IDENTITY, Mat4::IDENTITY],
..MeshInstance::default()
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::MeshInstanceShape {
instance_index: 1,
violation: MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
})
);
let mut document = one_bone_document();
document.skeleton.bones.push(Bone {
inverse_bind: Some(non_finite_inverse_bind),
..bone(Some(0))
});
assert_eq!(
validate_document_shape(&document),
Err(DocumentShapeError::NonFiniteBoneInverseBind { node: 1 })
);
}
#[test]
fn document_shape_violation_names_remain_machine_stable() {
let source_projection = [
(
SourceProjectionViolation::ProjectedBoneOutOfRange,
"projected_bone_out_of_range",
),
(
SourceProjectionViolation::TwoSourceNodesProjectToOneBone,
"two_source_nodes_project_to_one_bone",
),
(
SourceProjectionViolation::ParentSourceNodeMissing,
"parent_source_node_is_missing",
),
(
SourceProjectionViolation::CyclicUnprojectedSourceParentChain,
"cyclic_unprojected_source_parent_chain",
),
(
SourceProjectionViolation::NearestProjectedParentMismatch,
"projection_and_skeleton_parents_differ",
),
(
SourceProjectionViolation::ProjectedBoneHasUnprojectedSkeletonChild,
"projected_bone_has_an_unprojected_skeleton_child",
),
];
for (violation, expected) in source_projection {
assert_eq!(violation.to_string(), expected);
}
let track = [
(
TrackShapeViolation::BoneIndexOutOfRange,
"bone_index_out_of_range",
),
(TrackShapeViolation::EmptyTimes, "empty_times"),
(TrackShapeViolation::NonFiniteTime, "non_finite_time"),
(
TrackShapeViolation::TimesNotStrictlyIncreasing,
"times_not_strictly_increasing",
),
(
TrackShapeViolation::ValueCountMismatch,
"value_count_mismatch",
),
(
TrackShapeViolation::ValueTypeMismatchesProperty,
"value_type_mismatches_property",
),
(TrackShapeViolation::NonFiniteValue, "non_finite_value"),
];
for (violation, expected) in track {
assert_eq!(violation.to_string(), expected);
}
let instance = [
(
MeshInstanceShapeViolation::NodeIndexOutOfRange,
"node_index_out_of_range",
),
(
MeshInstanceShapeViolation::MeshIndexOutOfRange,
"mesh_index_out_of_range",
),
(
MeshInstanceShapeViolation::SkinJointOutOfRange,
"skin_joint_out_of_range",
),
(
MeshInstanceShapeViolation::SkinInverseBindCountMismatch,
"skin_ibm_count_mismatch",
),
(
MeshInstanceShapeViolation::NonFiniteSkinInverseBind,
"non_finite_inverse_bind",
),
];
for (violation, expected) in instance {
assert_eq!(violation.to_string(), expected);
}
}
#[test]
fn tolerant_world_rests_keep_unrelated_partial_evidence() {
let skeleton = Skeleton {
bones: vec![
bone(None),
bone(Some(99)),
Bone {
rest: Transform {
translation: Vec3::X,
..Transform::IDENTITY
},
..bone(None)
},
Bone {
rest: Transform {
translation: Vec3::Y,
..Transform::IDENTITY
},
..bone(Some(2))
},
bone(Some(1)),
],
};
let worlds = tolerant_world_rest_matrices(&skeleton);
assert_eq!(worlds.len(), 5);
assert_eq!(worlds[0], Some(Mat4::IDENTITY));
assert_eq!(worlds[1], None, "the malformed parent is unavailable");
assert_eq!(worlds[2], Some(Mat4::from_translation(Vec3::X)));
assert_eq!(
worlds[3],
Some(Mat4::from_translation(Vec3::new(1.0, 1.0, 0.0))),
"a finite independent chain remains measurable"
);
assert_eq!(
worlds[4], None,
"a child of unavailable evidence is unavailable"
);
}
#[test]
fn shared_affine_classifier_respects_distinct_caller_tolerances() {
let equal_axis_basis = affine_test_fixtures::tolerance_divergence_basis();
let strict = PositiveUniformAffineTolerance {
equal_axis: 1.0e-5,
relative_orthogonality: 1.0e-5,
singular_determinant_relative: 1.0e-6,
};
let loose = PositiveUniformAffineTolerance {
equal_axis: 1.0e-4,
relative_orthogonality: 1.0e-4,
singular_determinant_relative: 0.0,
};
assert_eq!(
classify_positive_uniform_affine(equal_axis_basis, strict),
Err(AffineDomainViolation::NonUniformScale),
"the stricter caller rejects this equal-axis difference"
);
assert!(
classify_positive_uniform_affine(equal_axis_basis, loose).is_ok(),
"the looser caller accepts this equal-axis difference"
);
let orthogonality_basis = affine_test_fixtures::orthogonality_tolerance_divergence_basis();
assert_eq!(
classify_positive_uniform_affine(orthogonality_basis, strict),
Err(AffineDomainViolation::Sheared),
"the stricter caller rejects this cross-axis dot product"
);
assert!(
classify_positive_uniform_affine(orthogonality_basis, loose).is_ok(),
"the looser caller accepts this cross-axis dot product"
);
}
#[test]
fn shared_affine_classifier_pins_its_symmetric_f64_formula() {
let policy = PositiveUniformAffineTolerance {
equal_axis: 1.0e-5,
relative_orthogonality: 1.0e-5,
singular_determinant_relative: 1.0e-6,
};
let on_long_edge = Mat3::from_diagonal(Vec3::new(99_998.5, 99_998.5, 100_000.0));
assert_eq!(
classify_positive_uniform_affine(on_long_edge, policy),
Ok(99_999.0)
);
let short = 99_998.5;
let long = 100_000.0 + 0.007_812_5;
for diagonal in [
Vec3::new(long, short, short),
Vec3::new(short, long, short),
Vec3::new(short, short, long),
] {
assert_eq!(
classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
Err(AffineDomainViolation::NonUniformScale)
);
}
let short = 1.0 - 2.0_f32.powi(-16);
for diagonal in [
Vec3::new(short, 1.0, 1.0),
Vec3::new(1.0, short, 1.0),
Vec3::new(1.0, 1.0, short),
] {
assert_eq!(
classify_positive_uniform_affine(Mat3::from_diagonal(diagonal), policy),
Err(AffineDomainViolation::NonUniformScale)
);
}
let c0 = Vec3::new(0.12792248, -0.99066633, -0.047073245);
let c1 = Vec3::new(-0.34637994, -0.00016034879, -0.93809813);
let c2 = Vec3::new(0.92933476, 0.13630849, -0.3431568);
assert!((c1.dot(c2) as f64).abs() < 1.0e-5);
assert!(c1.as_dvec3().dot(c2.as_dvec3()).abs() > 1.0e-5);
assert_eq!(
classify_positive_uniform_affine(Mat3::from_cols(c0, c1, c2), policy),
Err(AffineDomainViolation::Sheared)
);
for shear in [2.0_f32.powi(-15), -2.0_f32.powi(-15)] {
let basis = Mat3::from_cols(Vec3::X, Vec3::new(shear, 1.0, 0.0), Vec3::Z);
assert_eq!(
classify_positive_uniform_affine(basis, policy),
Err(AffineDomainViolation::Sheared)
);
}
}
#[test]
fn affine_axis_mean_is_ascending_and_column_order_invariant() {
let lengths = [
f64::from_bits(0x3ff1_09e7_e000_022c),
f64::from_bits(0x3ff1_09ec_6000_0eb5),
f64::from_bits(0x3ff1_09fa_e000_3cde),
];
let expected = f64::from_bits(0x3ff1_09ef_b555_6f3f);
let ascending = (lengths[0] + lengths[1] + lengths[2]) / 3.0;
let descending = (lengths[2] + lengths[1] + lengths[0]) / 3.0;
assert_eq!(expected.to_bits(), 0x3ff1_09ef_b555_6f3f);
assert_eq!(ascending.to_bits(), expected.to_bits());
assert_eq!(descending.to_bits(), 0x3ff1_09ef_b555_6f40);
for order in [
[0, 1, 2],
[0, 2, 1],
[1, 0, 2],
[1, 2, 0],
[2, 0, 1],
[2, 1, 0],
] {
assert_eq!(
average_affine_axis_length(order.map(|index| lengths[index])),
expected,
"axis order {order:?}"
);
}
let dyadic = [2.0_f64.powi(53), 1.0, 1.0];
let ascending = (dyadic[1] + dyadic[2] + dyadic[0]) / 3.0;
let descending = (dyadic[0] + dyadic[1] + dyadic[2]) / 3.0;
assert_ne!(ascending, descending);
assert_eq!(average_affine_axis_length(dyadic), ascending);
let permutations = affine_test_fixtures::appendix_d_v6_mean_permutations();
let expected_length_bits = lengths.map(f64::to_bits);
assert_eq!(
affine_axis_lengths(permutations[0]).map(f64::to_bits),
expected_length_bits
);
let tolerance = PositiveUniformAffineTolerance {
equal_axis: 1.0e-5,
relative_orthogonality: 1.0e-5,
singular_determinant_relative: 1.0e-6,
};
for (permutation, linear) in permutations.into_iter().enumerate() {
assert!(
linear
.x_axis
.as_dvec3()
.cross(linear.y_axis.as_dvec3())
.dot(linear.z_axis.as_dvec3())
> 0.0,
"orientation for permutation {permutation}"
);
assert_eq!(
average_affine_axis_length(affine_axis_lengths(linear)).to_bits(),
expected.to_bits(),
"mean for permutation {permutation}"
);
assert_eq!(
classify_positive_uniform_affine(linear, tolerance),
Err(AffineDomainViolation::NonUniformScale),
"classification for permutation {permutation}"
);
}
}
#[test]
fn shared_affine_classifier_pins_f64_determinant_arithmetic() {
let linear = Mat3::from_cols(
Vec3::new(
f32::from_bits(0x3ff3_5574),
f32::from_bits(0x3f0e_fa3c),
0.0,
),
Vec3::new(
f32::from_bits(0x3ff5_5e17),
f32::from_bits(0x3f10_2c31),
0.0,
),
Vec3::Z,
);
let columns = [
linear.x_axis.as_dvec3(),
linear.y_axis.as_dvec3(),
linear.z_axis.as_dvec3(),
];
let determinant_f64 = columns[2].dot(columns[0].cross(columns[1]));
let determinant_f32 = f64::from(linear.determinant());
let lengths = affine_axis_lengths(linear);
let threshold = (determinant_f64 + determinant_f32) / 2.0;
assert!(determinant_f64 < threshold);
assert!(determinant_f32 > threshold);
assert_eq!(
classify_positive_uniform_affine(
linear,
PositiveUniformAffineTolerance {
equal_axis: 10.0,
relative_orthogonality: 10.0,
singular_determinant_relative: threshold
/ (lengths[0] * lengths[1] * lengths[2]),
},
),
Err(AffineDomainViolation::Singular)
);
let large_uniform = 2.0e19_f32;
assert_eq!(
classify_positive_uniform_affine(
Mat3::from_diagonal(Vec3::splat(large_uniform)),
PositiveUniformAffineTolerance {
equal_axis: 1.0e-5,
relative_orthogonality: 1.0e-5,
singular_determinant_relative: 1.0e-6,
},
),
Ok(f64::from(large_uniform))
);
}
#[test]
fn affine_geometry_facts_pin_every_widened_field_and_slot() {
let linear = Mat3::from_cols(
Vec3::new(1.0, 2.0, 3.0),
Vec3::new(4.0, 5.0, 6.0),
Vec3::new(7.0, 8.0, 10.0),
);
let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
assert_eq!(
facts.axis_lengths.map(f64::to_bits),
[
0x400d_eeea_1168_3f49,
0x4021_8cc8_21d6_d3e3,
0x402d_3064_dcc8_ae67,
]
);
assert_eq!(facts.mean_axis_length.to_bits(), 0x4022_12f7_d653_30b4);
assert_eq!(facts.determinant.to_bits(), 0xc008_0000_0000_0000);
assert_eq!(facts.axis_length_product.to_bits(), 0x407d_f2e3_88f2_1b01);
assert_eq!(
facts.cross_axis_dots.map(f64::to_bits),
[
0x4040_0000_0000_0000,
0x404a_8000_0000_0000,
0x4060_0000_0000_0000,
],
"cross-axis slots are XY, XZ, YZ"
);
}
#[test]
fn affine_geometry_facts_widen_every_dot_product_before_multiplying() {
let x = Vec3::new(
f32::from_bits(0x3ff3_5574),
f32::from_bits(0x3f0e_fa3c),
0.0,
);
let y = Vec3::new(
f32::from_bits(0x3ff5_5e17),
f32::from_bits(0x3f10_2c31),
0.0,
);
let widened_dot = x.as_dvec3().dot(y.as_dvec3());
let f32_then_widened = f64::from(x.dot(y));
for (slot, linear) in [
(0, Mat3::from_cols(x, y, Vec3::Z)),
(1, Mat3::from_cols(x, Vec3::Z, y)),
(2, Mat3::from_cols(Vec3::Z, x, y)),
] {
let facts = AffineGeometryFacts::from_linear(linear).expect("finite widened facts");
assert_eq!(facts.cross_axis_dots[slot], widened_dot);
assert_ne!(
facts.cross_axis_dots[slot], f32_then_widened,
"dot slot {slot} must multiply and add in f64, not widen an f32 result"
);
}
}
#[test]
fn weld_preserves_uv_seams_at_shared_positions() {
let mut primitive = Primitive {
positions: vec![Vec3::ZERO, Vec3::ZERO, Vec3::ZERO],
uvs: vec![[0.0, 0.0], [1.0, 0.0], [0.0, 0.0]],
..Primitive::default()
};
primitive.weld();
assert_eq!(primitive.positions.len(), 2);
let reconstructed_corners = primitive
.indices
.iter()
.map(|&index| {
let index = index as usize;
(primitive.positions[index], primitive.uvs[index])
})
.collect::<Vec<_>>();
assert_eq!(
reconstructed_corners,
vec![
(Vec3::ZERO, [0.0, 0.0]),
(Vec3::ZERO, [1.0, 0.0]),
(Vec3::ZERO, [0.0, 0.0]),
]
);
}
}