use crate::model::{
AffineDomainViolation, BoneId, Document, DocumentShapeError, Interpolation, Property,
SourceInverseBindAccessorStatus, SourceSkeletonCoverage,
};
#[cfg(test)]
use crate::model::{
Clip, MeshInstanceShapeViolation, Skeleton, TrackValues, Transform, mat4_is_finite,
};
#[cfg(test)]
use glam::{Mat3, Mat4, Vec4};
#[cfg(test)]
use std::collections::BTreeMap;
use std::collections::BTreeSet;
mod assembly_basis;
mod numeric;
mod planning;
mod proof;
mod reference;
mod validation;
pub use assembly_basis::{
ASSEMBLY_SCALE_BASIS_VERSION, AssemblyScaleBasis, AssemblyScaleCompatibilityBasis,
AssemblyScaleCompatibilityError, AssemblyScaleNamedNode,
AssemblyScaleNamedSelectorResolutionError, AssemblyScaleResolvedNamedSelector,
AssemblyScaleSelectorRequest, AssemblyScaleSkinlessClipBasis, AssemblyScaleSourceNode,
AssemblyScaleSourceRest, AssemblyScaleTargetPath, assembly_scale_basis,
assembly_scale_compatibility_basis, rebase_assembly_scale_skinless_clip,
require_assembly_scale_compatibility, require_assembly_scale_compatibility_with_selectors,
resolve_assembly_scale_named_selector,
};
pub use planning::plan_scale;
use planning::validate_plan_document_inventory;
pub use proof::{ScaleProof, ScaleProofResidual, prove_scale};
pub use reference::ScaleCandidate;
#[cfg(test)]
use proof::{
BoundsAccumulator, SkinSlot, accumulate_skinned_bounds, check_residual, check_sampling_budget,
observed_factor_from_source, per_sample_work_units, skin_influence_magnitude, world_at_time,
};
#[cfg(test)]
use numeric::{
column_operand_magnitude, largest_entry, mat4_abs, product_operand_magnitude,
scale_translation_only, translation_composition_rounding_base,
};
#[cfg(test)]
use planning::classify_affine;
#[cfg(any(test, feature = "fixtures"))]
pub(crate) use reference::build_scale_candidate;
#[cfg(test)]
use reference::{build_rest_bind, build_whole_document};
#[cfg(test)]
use validation::{
WorldBonePose, WorldPose, affected_skin_instance_indices, child_translation_rounding_magnitude,
instance_bind, rest_world_pose, source_node_index_map, validate_scale_input,
};
#[cfg(test)]
use validation::{
affected_skin_classification_steps, derive_rest_bind_plan_domain,
reset_affected_skin_classification_steps, resolve_rest_bind_skin, rest_bind_affected_closure,
source_world_matrix, world_rests,
};
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub struct ScaleTolerancePolicy {
pub id: &'static str,
pub relative_orthogonality: f64,
pub equal_axis: f64,
pub common_factor: f64,
pub singular_determinant_relative: f64,
pub scalar_absolute: f64,
pub scalar_relative: f64,
pub rotation_residual_radians: f64,
pub postcondition_unit_scale_residual: f64,
pub proof_sample_work_budget: u64,
pub f32_rounding_ulps: u32,
}
impl ScaleTolerancePolicy {
pub const UNIT_SCALE_BANDS: f64 = 4.0;
pub const APPENDIX_D_V6: Self = Self {
id: "appendix-d-v6",
relative_orthogonality: 1e-5,
equal_axis: 1e-5,
common_factor: 1e-5,
singular_determinant_relative: 1e-6,
scalar_absolute: 1e-6,
scalar_relative: 1e-5,
rotation_residual_radians: 1e-5,
postcondition_unit_scale_residual: 6.103_515_625e-5,
proof_sample_work_budget: 400_000_000,
f32_rounding_ulps: 4,
};
pub fn observed_factor_divergence_ceiling(&self) -> f64 {
self.common_factor + self.postcondition_unit_scale_residual
}
pub fn scalar_tolerance(&self, before: f64, after: f64) -> f64 {
self.scalar_absolute + self.scalar_relative * before.abs().max(after.abs())
}
pub fn f32_rounded_tolerance(&self, before: f64, after: f64, magnitude: f64) -> f64 {
self.scalar_tolerance(before, after)
+ f64::from(self.f32_rounding_ulps) * magnitude.abs() * f64::from(f32::EPSILON)
}
fn relative(&self, tolerance: f64, a: f64, b: f64) -> bool {
(a - b).abs() <= tolerance * a.abs().max(b.abs())
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ScaleCapabilityCoverage {
#[default]
Unavailable,
Complete,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
#[non_exhaustive]
pub struct ScaleCapabilityFacts {
pub coverage: ScaleCapabilityCoverage,
pub morphs_present: bool,
pub morph_weights_present: bool,
pub whole_document_morphs_preservable: bool,
pub cameras_present: bool,
pub lights_present: bool,
pub instancing_present: bool,
pub unregistered_extensions_present: bool,
pub extras_present: bool,
pub unknown_source_members_present: bool,
pub non_triangle_primitives_present: bool,
pub unsupported_vertex_attributes_present: bool,
pub secondary_skin_influences_present: bool,
pub inverse_bind_issues_present: bool,
pub unsafe_accessor_layout_present: bool,
pub external_resources_present: bool,
}
impl ScaleCapabilityFacts {
pub fn is_supported(&self) -> bool {
self.common_domains_supported() && !self.morphs_present && !self.morph_weights_present
}
pub fn is_supported_for(&self, operation: ScaleOperation) -> bool {
self.common_domains_supported()
&& match operation {
ScaleOperation::WholeDocumentLinearUnits { .. } => {
(!self.morphs_present && !self.morph_weights_present)
|| self.whole_document_morphs_preservable
}
ScaleOperation::RestBindUniformScale { .. } => {
!self.morphs_present && !self.morph_weights_present
}
}
}
fn common_domains_supported(&self) -> bool {
self.coverage == ScaleCapabilityCoverage::Complete
&& !self.cameras_present
&& !self.lights_present
&& !self.instancing_present
&& !self.unregistered_extensions_present
&& !self.extras_present
&& !self.unknown_source_members_present
&& !self.non_triangle_primitives_present
&& !self.unsupported_vertex_attributes_present
&& !self.secondary_skin_influences_present
&& !self.inverse_bind_issues_present
&& !self.unsafe_accessor_layout_present
&& !self.external_resources_present
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
#[non_exhaustive]
pub enum ScaleOperation {
WholeDocumentLinearUnits {
factor: f64,
},
RestBindUniformScale {
source_skin_index: usize,
source_root_node_index: usize,
expected_factor: f64,
},
}
#[derive(Debug, Clone, Copy)]
pub struct ScaleRequest<'a> {
pub operation: ScaleOperation,
pub document: &'a Document,
pub capability: &'a ScaleCapabilityFacts,
}
#[derive(Debug, Clone, Copy, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum ScaleError {
#[error("scale factor must be finite and positive, got {factor}")]
InvalidFactor {
factor: f64,
},
#[error("rest/bind expected factor must be finite and positive, got {factor}")]
InvalidExpectedFactor {
factor: f64,
},
#[error(
"factor {factor} (derived from declared factor {declared}) is not representable at the f32 writer model boundary: it narrows to {narrowed}"
)]
FactorNotRepresentable {
declared: f64,
factor: f64,
narrowed: f32,
},
#[error(
"source root node index {source_root_node_index} is not a source node in the document's source skeleton"
)]
InvalidRootSelector {
source_root_node_index: usize,
},
#[error(
"source skin index {source_skin_index} is not a skin in the document's source skeleton, or has no joints"
)]
InvalidSkinSelector {
source_skin_index: usize,
},
#[error("capability projection is incomplete or declares unsupported domain(s)")]
IncompleteCapability,
#[error(
"document.assets.source_skeleton coverage is not complete: rest/bind planning requires a format-neutral source-node/source-skin projection"
)]
IncompleteSourceSkeleton,
#[error("source node {source_node_index} did not normalize to a document skeleton bone")]
SourceNodeNotNormalized {
source_node_index: usize,
},
#[error("source node {source_node_index} has a non-finite raw rest transform")]
NonFiniteSourceTransform {
source_node_index: usize,
},
#[error("node {node} has a non-finite rest transform")]
NonFiniteTransform {
node: BoneId,
},
#[error("node {node} has invalid parent {parent}")]
InvalidParent {
node: BoneId,
parent: BoneId,
},
#[error("bone index {index} is out of range for this document")]
BoneIndexOutOfRange {
index: usize,
},
#[error("plan does not describe the supplied document: {reason}")]
PlanDocumentMismatch {
reason: &'static str,
},
#[error("affected domain closure is not complete: {reason}")]
IncompleteClosure {
reason: &'static str,
},
#[error("node {node} carries unskinned geometry inside the affected closure")]
UnsupportedUnskinnedGeometry {
node: BoneId,
},
#[error(
"node {node} rest-world linear part is not orientation-preserving positive uniform scale ({reason:?})"
)]
InvalidAffineDomain {
node: BoneId,
reason: AffineDomainViolation,
},
#[error("declared expected factor {expected} does not match observed source factor {observed}")]
FactorMismatch {
expected: f64,
observed: f64,
},
#[error("node {node} effective factor {observed} differs from common factor {expected}")]
MixedFactor {
expected: f64,
observed: f64,
node: BoneId,
},
#[error("proof residual {observed} for {kind:?} exceeds tolerance {tolerance}")]
ProofResidualExceeded {
kind: ProofResidualKind,
observed: f64,
tolerance: f64,
},
#[error(
"proof sampling work {work} ({sample_times} sample times x {per_sample_cost} work units) exceeds the {policy_id} budget {budget}"
)]
ProofSamplingBudgetExceeded {
policy_id: &'static str,
sample_times: u64,
per_sample_cost: u64,
work: u64,
budget: u64,
},
#[error("proof obligation {kind:?} could not find expected evidence ({detail})")]
MissingProofEvidence {
kind: ProofResidualKind,
detail: &'static str,
},
#[error(transparent)]
InvalidDocumentShape(#[from] DocumentShapeError),
#[error("no inverse-bind evidence for skin joint {node}")]
MissingInverseBind {
node: BoneId,
},
#[error("mesh {mesh_index} primitive {primitive_index} is invalid ({reason})")]
InvalidMeshPrimitive {
mesh_index: usize,
primitive_index: usize,
reason: &'static str,
},
#[error(
"mesh {mesh_index} primitive {primitive_index} vertex {vertex_index} primary skin influence {influence_index} has a negative weight"
)]
NegativeSkinWeight {
mesh_index: usize,
primitive_index: usize,
vertex_index: usize,
influence_index: usize,
},
#[error("instance {instance_index} primitive {primitive_index} is invalid ({reason})")]
InvalidSkinnedPrimitive {
instance_index: usize,
primitive_index: usize,
reason: &'static str,
},
#[error("candidate document structure does not match source ({reason})")]
CandidateStructureMismatch {
reason: &'static str,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ProofResidualKind {
RestTranslation,
RestRotation,
UnitScale,
TransformOnlyAffine,
TrackValue,
MeshPosition,
KeyTranslation,
CubicInterior,
Trajectory,
SkinMatrix,
Bounds,
UnaffectedInverseBind,
ObservedFactor,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleRewriteRule {
WholeDocumentLength,
RestBindParentBasis,
RestBindLocalScale,
RestBindNodeBasis,
RestBindSourceLocal {
connector_tail: Option<usize>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleFieldDisposition {
PreserveExact,
Rewrite(ScaleRewriteRule),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleBoneRestField {
Translation,
Rotation,
Scale,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleSourceRestField {
Translation,
Rotation,
Scale,
MatrixLinear,
MatrixTranslation,
MatrixHomogeneous,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleFieldTarget {
BoneRest {
bone: BoneId,
field: ScaleBoneRestField,
},
SourceNodeRest {
source_node_index: usize,
field: ScaleSourceRestField,
},
AnimationValues {
clip_index: usize,
track_index: usize,
bone: BoneId,
property: Property,
},
BoneInverseBind {
bone: BoneId,
},
InstanceInverseBind {
instance_index: usize,
slot: usize,
joint: BoneId,
},
MeshPositions {
mesh_index: usize,
primitive_index: usize,
},
MeshNormals {
mesh_index: usize,
primitive_index: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScaleFieldPlan {
target: ScaleFieldTarget,
disposition: ScaleFieldDisposition,
element_count: usize,
}
impl ScaleFieldPlan {
pub fn target(&self) -> ScaleFieldTarget {
self.target
}
pub fn disposition(&self) -> ScaleFieldDisposition {
self.disposition
}
pub fn element_count(&self) -> usize {
self.element_count
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScalePayloadShapeRow {
Document {
bone_count: usize,
source_node_count: usize,
source_coverage: SourceSkeletonCoverage,
clip_count: usize,
instance_count: usize,
mesh_count: usize,
},
Bone {
bone: BoneId,
parent: Option<BoneId>,
},
SourceSkin {
source_skin_index: usize,
skeleton_root_source_node_index: Option<usize>,
joint_count: usize,
attachment_count: usize,
inverse_bind_status: SourceInverseBindAccessorStatus,
inverse_bind_declared_count: Option<usize>,
inverse_bind_matrix_count: usize,
},
SourceSkinJoint {
source_skin_index: usize,
slot: usize,
source_node_index: usize,
},
SourceSkinAttachment {
source_skin_index: usize,
attachment_index: usize,
source_node_index: usize,
source_mesh_index: Option<usize>,
},
Clip {
clip_index: usize,
track_count: usize,
},
Track {
clip_index: usize,
track_index: usize,
bone: BoneId,
property: Property,
interpolation: Interpolation,
key_count: usize,
value_count: usize,
},
Instance {
instance_index: usize,
node: BoneId,
source_node_index: usize,
mesh: usize,
joint_count: usize,
inverse_bind_count: usize,
},
InstanceJoint {
instance_index: usize,
slot: usize,
joint: BoneId,
},
Mesh {
mesh_index: usize,
source_mesh_index: usize,
primitive_count: usize,
},
Primitive {
mesh_index: usize,
primitive_index: usize,
position_count: usize,
normal_count: usize,
joint_count: usize,
weight_count: usize,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleProofObligation {
ExactTopology,
ExactPayloadIdentity,
ExactUnchangedWorldRest,
RestWorld,
RestWorldAndUnitScale,
TransformOnlyAffine,
TrackValues,
MeshPositions,
KeyTranslations,
CubicInteriors,
Trajectories,
SkinAndBounds,
AffectedInverseBinds,
UnaffectedInverseBinds,
ExactConnectorProjection,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleProjectedRole {
Root,
Joint,
TransformOnly,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum ScaleSourceNodeKind {
Projected {
bone: BoneId,
role: ScaleProjectedRole,
projected_parent: Option<usize>,
incoming_connector_tail: Option<usize>,
},
Connector,
OutsideDomain {
bone: Option<BoneId>,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct ScaleSourceTopologyRow {
source_node_index: usize,
parent_source_node_index: Option<usize>,
kind: ScaleSourceNodeKind,
}
#[derive(Debug, Clone, PartialEq, Eq)]
struct ScaleLedger {
field_rows: Vec<ScaleFieldPlan>,
payload_shapes: Vec<ScalePayloadShapeRow>,
obligations: Vec<ScaleProofObligation>,
}
#[derive(Debug, Clone, PartialEq)]
struct WholeDocumentParams {
factor: f64,
}
#[derive(Debug, Clone, PartialEq)]
struct RestBindParams {
source_skin_index: usize,
source_root_node_index: usize,
expected_factor: f64,
transform_only_attachments: Vec<BoneId>,
}
#[derive(Debug, Clone, PartialEq)]
enum ScaleCompiledPlan {
WholeDocument(WholeDocumentParams),
RestBind(RestBindParams),
}
#[derive(Debug, Clone, Copy)]
pub struct ScalePlanLedger<'a> {
plan: &'a ScalePlan,
}
impl<'a> ScalePlanLedger<'a> {
fn ledger(self) -> &'a ScaleLedger {
&self.plan.ledger
}
pub fn field_rows(self) -> std::slice::Iter<'a, ScaleFieldPlan> {
self.ledger().field_rows.iter()
}
pub fn payload_shapes(self) -> std::slice::Iter<'a, ScalePayloadShapeRow> {
self.ledger().payload_shapes.iter()
}
pub fn obligations(self) -> std::slice::Iter<'a, ScaleProofObligation> {
self.ledger().obligations.iter()
}
pub fn source_topology(self) -> std::slice::Iter<'a, ScaleSourceTopologyRow> {
self.plan.source_topology.iter()
}
}
impl ScaleSourceTopologyRow {
pub fn source_node_index(&self) -> usize {
self.source_node_index
}
pub fn parent_source_node_index(&self) -> Option<usize> {
self.parent_source_node_index
}
pub fn kind(&self) -> ScaleSourceNodeKind {
self.kind
}
}
#[derive(Debug, Clone, PartialEq)]
#[non_exhaustive]
pub struct ScalePlan {
tolerance_policy: ScaleTolerancePolicy,
observed_factor: f64,
affected_nodes: Vec<BoneId>,
source_topology: Vec<ScaleSourceTopologyRow>,
ledger: ScaleLedger,
compiled: ScaleCompiledPlan,
}
impl ScalePlan {
pub fn operation(&self) -> ScaleOperation {
match &self.compiled {
ScaleCompiledPlan::WholeDocument(plan) => ScaleOperation::WholeDocumentLinearUnits {
factor: plan.factor,
},
ScaleCompiledPlan::RestBind(plan) => ScaleOperation::RestBindUniformScale {
source_skin_index: plan.source_skin_index,
source_root_node_index: plan.source_root_node_index,
expected_factor: plan.expected_factor,
},
}
}
pub fn tolerance_policy(&self) -> ScaleTolerancePolicy {
self.tolerance_policy
}
pub fn affected_nodes(&self) -> &[BoneId] {
&self.affected_nodes
}
pub fn transform_only_attachments(&self) -> &[BoneId] {
match &self.compiled {
ScaleCompiledPlan::WholeDocument(_) => &[],
ScaleCompiledPlan::RestBind(plan) => &plan.transform_only_attachments,
}
}
pub fn common_factor(&self) -> f64 {
match &self.compiled {
ScaleCompiledPlan::WholeDocument(plan) => plan.factor,
ScaleCompiledPlan::RestBind(plan) => plan.expected_factor,
}
}
pub fn observed_factor(&self) -> f64 {
self.observed_factor
}
pub fn validate_document_inventory(&self, document: &Document) -> Result<(), ScaleError> {
validate_plan_document_inventory(document, self)
}
pub fn ledger(&self) -> ScalePlanLedger<'_> {
ScalePlanLedger { plan: self }
}
pub fn animation_value_factor(
&self,
document: &Document,
clip_index: usize,
track_index: usize,
) -> Result<f64, ScaleError> {
validate_plan_document_inventory(document, self)?;
let row = self
.field_rows()
.iter()
.find(|row| {
matches!(
row.target(),
ScaleFieldTarget::AnimationValues {
clip_index: candidate_clip,
track_index: candidate_track,
..
} if candidate_clip == clip_index && candidate_track == track_index
)
})
.ok_or(ScaleError::PlanDocumentMismatch {
reason: "compiled_animation_row_missing",
})?;
let ScaleFieldTarget::AnimationValues { bone, property, .. } = row.target() else {
unreachable!("the selected row is an animation row")
};
self.animation_target_factor_unchecked(document, bone, property)
}
pub fn animation_target_factor(
&self,
document: &Document,
bone: BoneId,
property: Property,
) -> Result<f64, ScaleError> {
validate_plan_document_inventory(document, self)?;
self.animation_target_factor_unchecked(document, bone, property)
}
pub(in crate::scale) fn animation_target_factor_unchecked(
&self,
document: &Document,
bone: BoneId,
property: Property,
) -> Result<f64, ScaleError> {
let affected = self.affected_set();
let node_factor = if affected.contains(&bone) {
self.common_factor()
} else {
1.0
};
let parent_factor = document
.skeleton
.bones
.get(bone)
.ok_or(ScaleError::BoneIndexOutOfRange { index: bone })?
.parent
.filter(|parent| affected.contains(parent))
.map_or(1.0, |_| self.common_factor());
Ok(match (self.operation(), property) {
(ScaleOperation::WholeDocumentLinearUnits { .. }, Property::Translation) => {
self.common_factor()
}
(ScaleOperation::WholeDocumentLinearUnits { .. }, _) => 1.0,
(ScaleOperation::RestBindUniformScale { .. }, Property::Translation) => parent_factor,
(ScaleOperation::RestBindUniformScale { .. }, Property::Scale) => {
parent_factor / node_factor
}
(ScaleOperation::RestBindUniformScale { .. }, Property::Rotation) => 1.0,
})
}
fn affected_set(&self) -> BTreeSet<BoneId> {
self.affected_nodes().iter().copied().collect()
}
fn field_rows(&self) -> &[ScaleFieldPlan] {
&self.ledger.field_rows
}
fn obligations(&self) -> &[ScaleProofObligation] {
&self.ledger.obligations
}
}
#[cfg(test)]
mod tests;