1use serde::Serialize;
11use std::collections::BTreeMap;
12use std::io::{self, Write};
13
14use crate::model::validate_track_shape;
15use crate::{
16 Bone, Clip, DependencyClosureIdentityV1, DependencyClosureV1, Document, InputIdentity,
17 LoadedSource, Property, Skeleton, Track, TrackSample, TransitionFamilyBoundaryV1,
18 TransitionFamilyDeclarationInputV1, TransitionFamilyManifestIdentityV1,
19 TransitionFamilyTolerancesV1,
20};
21
22pub const TRANSITION_POSE_EVALUATION_V1_ID: &str =
24 "urn:animsmith:schema:transition-pose-evaluation:1";
25pub const TRANSITION_POSE_EVALUATION_V1_SCHEMA_VERSION: u32 = 1;
27pub const TRANSITION_POSE_EVALUATION_V1_MAX_BONES: usize = 4_096;
29pub const TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES: usize =
34 crate::TRANSITION_FAMILY_V1_MAX_NORMALIZED_BYTES as usize;
35pub const TRANSITION_POSE_EVALUATION_V1_MAX_DOCUMENT_CLIPS: usize = 4_096;
42pub const TRANSITION_POSE_EVALUATION_V1_MAX_RAW_TRACK_ROWS_PER_CLIP: usize =
48 TRANSITION_POSE_EVALUATION_V1_MAX_BONES * 3;
49pub const TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP: usize =
52 TRANSITION_POSE_EVALUATION_V1_MAX_BONES * 2;
53pub const TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS: usize =
56 TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS;
57pub const TRANSITION_POSE_EVALUATION_V1_MAX_FAMILY_PAIR_BOUNDARIES: usize = 4_096;
59pub const TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_PAIR_BOUNDARIES: usize = 65_536;
61pub const TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS: usize = 16_777_216;
63pub const TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS: usize = 16;
65pub const TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS: usize = 16;
67pub const TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_OFFENDERS: usize = 65_536;
69pub const TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES: usize = 256 * 1024 * 1024;
71const MAX_DETAILED_PAIR_FIXED_BYTES: usize = 4_096;
75
76#[derive(Debug, Clone, PartialEq, Serialize)]
78pub struct SkeletonBasisBoneV1 {
79 ordinal: usize,
80 name: String,
81 #[serde(skip_serializing_if = "Option::is_none")]
82 parent_ordinal: Option<usize>,
83 rest_translation_m: [f64; 3],
84 rest_rotation: [f64; 4],
85}
86
87impl SkeletonBasisBoneV1 {
88 pub const fn ordinal(&self) -> usize {
90 self.ordinal
91 }
92 pub fn name(&self) -> &str {
94 &self.name
95 }
96 pub const fn parent_ordinal(&self) -> Option<usize> {
98 self.parent_ordinal
99 }
100 pub const fn rest_translation_m(&self) -> [f64; 3] {
102 self.rest_translation_m
103 }
104 pub const fn rest_rotation(&self) -> [f64; 4] {
106 self.rest_rotation
107 }
108}
109
110#[derive(Debug, Clone, PartialEq, Serialize)]
112pub struct SkeletonBasisV1 {
113 schema: &'static str,
114 schema_version: u32,
115 bones: Vec<SkeletonBasisBoneV1>,
116 #[serde(skip)]
117 identity: InputIdentity,
118}
119
120impl SkeletonBasisV1 {
121 pub fn from_skeleton(skeleton: &Skeleton) -> Result<Self, SkeletonBasisError> {
123 if skeleton.bones.len() > TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
124 return Err(SkeletonBasisError::TooManyBones);
125 }
126 if !skeleton_text_is_within_limit(
127 skeleton,
128 TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES,
129 ) {
130 return Err(SkeletonBasisError::TooMuchText);
131 }
132 let mut bones = Vec::with_capacity(skeleton.bones.len());
133 for (ordinal, bone) in skeleton.bones.iter().enumerate() {
134 let parent_ordinal = match bone.parent {
135 Some(parent) if parent < ordinal => Some(parent),
136 Some(_) => return Err(SkeletonBasisError::InvalidParent { ordinal }),
137 None => None,
138 };
139 bones.push(SkeletonBasisBoneV1 {
140 ordinal,
141 name: bone.name.clone(),
142 parent_ordinal,
143 rest_translation_m: finite_translation(bone, ordinal)?,
144 rest_rotation: canonical_quaternion(bone.rest.rotation, ordinal)?,
145 });
146 }
147 let wire = SkeletonBasisWire {
148 schema: "urn:animsmith:schema:skeleton-basis:1",
149 schema_version: 1,
150 bones: &bones,
151 };
152 let bytes = canonical_bytes(&wire, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
153 .map_err(|_| SkeletonBasisError::IdentityTooLarge)?;
154 Ok(Self {
155 schema: "urn:animsmith:schema:skeleton-basis:1",
156 schema_version: 1,
157 bones,
158 identity: InputIdentity::from_bytes(&bytes),
159 })
160 }
161 pub const fn schema(&self) -> &'static str {
163 self.schema
164 }
165 pub const fn schema_version(&self) -> u32 {
167 self.schema_version
168 }
169 pub fn bones(&self) -> &[SkeletonBasisBoneV1] {
171 &self.bones
172 }
173 pub const fn identity(&self) -> &InputIdentity {
175 &self.identity
176 }
177}
178
179#[derive(Serialize)]
180struct SkeletonBasisWire<'a> {
181 schema: &'static str,
182 schema_version: u32,
183 bones: &'a [SkeletonBasisBoneV1],
184}
185
186#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
188#[non_exhaustive]
189pub enum SkeletonBasisError {
190 #[error("transition-pose skeleton basis exceeds the bone cap")]
192 TooManyBones,
193 #[error("transition-pose skeleton basis exceeds the bone-name text cap")]
195 TooMuchText,
196 #[error("transition-pose skeleton basis has an invalid parent at bone {ordinal}")]
198 InvalidParent {
199 ordinal: usize,
201 },
202 #[error("transition-pose skeleton basis has an invalid rest transform at bone {ordinal}")]
204 InvalidRest {
205 ordinal: usize,
207 },
208 #[error("transition-pose skeleton basis identity exceeds the result cap")]
210 IdentityTooLarge,
211}
212
213#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
215#[serde(rename_all = "snake_case")]
216#[allow(missing_docs)]
217pub enum TransitionPoseStatusV1 {
218 Complete,
219 Incomplete,
220}
221#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
223#[serde(rename_all = "snake_case")]
224#[allow(missing_docs)]
225pub enum TransitionPoseDecisionV1 {
226 Pass,
227 Finding,
228 NotEvaluated,
229}
230#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
232#[serde(rename_all = "snake_case")]
233#[allow(missing_docs)]
234pub enum TransitionPoseReasonV1 {
235 NoConfiguredFamilies,
236 DependencyClosureIncomplete,
237 MemberUnavailable,
238 ZeroDuration,
239 SkeletonBasisMismatch,
240 TimeToleranceUnsupported,
241 UnsupportedSampling,
242 InputLimit,
243 FamilyWorkLimit,
244 AggregateWorkLimit,
245 RetentionLimit,
246 ResultLimit,
247}
248
249#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
251pub struct TransitionPoseMemberV1 {
252 take_index: u64,
253 take_name: String,
254 source_input: Option<InputIdentity>,
255 #[serde(skip_serializing_if = "Option::is_none")]
256 source_dependency_closure_identity: Option<DependencyClosureIdentityV1>,
257}
258impl TransitionPoseMemberV1 {
259 pub const fn take_index(&self) -> u64 {
261 self.take_index
262 }
263 pub fn take_name(&self) -> &str {
265 &self.take_name
266 }
267 pub const fn source_input(&self) -> Option<&InputIdentity> {
269 self.source_input.as_ref()
270 }
271 pub const fn source_dependency_closure_identity(&self) -> Option<&DependencyClosureIdentityV1> {
273 self.source_dependency_closure_identity.as_ref()
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Serialize)]
279pub struct TransitionPoseTranslationOffenderV1 {
280 bone_ordinal: usize,
281 bone_name: String,
282 delta_m: f64,
283}
284impl TransitionPoseTranslationOffenderV1 {
285 pub const fn bone_ordinal(&self) -> usize {
287 self.bone_ordinal
288 }
289 pub fn bone_name(&self) -> &str {
291 &self.bone_name
292 }
293 pub const fn delta_m(&self) -> f64 {
295 self.delta_m
296 }
297}
298#[derive(Debug, Clone, PartialEq, Serialize)]
300pub struct TransitionPoseRotationOffenderV1 {
301 bone_ordinal: usize,
302 bone_name: String,
303 delta_deg: f64,
304}
305impl TransitionPoseRotationOffenderV1 {
306 pub const fn bone_ordinal(&self) -> usize {
308 self.bone_ordinal
309 }
310 pub fn bone_name(&self) -> &str {
312 &self.bone_name
313 }
314 pub const fn delta_deg(&self) -> f64 {
316 self.delta_deg
317 }
318}
319
320#[derive(Debug, Clone, PartialEq, Serialize)]
322pub struct TransitionPosePairEvaluationV1 {
323 member_indices: [usize; 2],
324 boundary: TransitionFamilyBoundaryV1,
325 max_translation_delta_m: f64,
326 max_rotation_delta_deg: f64,
327 translation_tolerance_m: f64,
328 rotation_tolerance_deg: f64,
329 translation_offenders: Vec<TransitionPoseTranslationOffenderV1>,
330 rotation_offenders: Vec<TransitionPoseRotationOffenderV1>,
331}
332impl TransitionPosePairEvaluationV1 {
333 pub const fn member_indices(&self) -> [usize; 2] {
335 self.member_indices
336 }
337 pub const fn boundary(&self) -> TransitionFamilyBoundaryV1 {
339 self.boundary
340 }
341 pub const fn max_translation_delta_m(&self) -> f64 {
343 self.max_translation_delta_m
344 }
345 pub const fn max_rotation_delta_deg(&self) -> f64 {
347 self.max_rotation_delta_deg
348 }
349 pub const fn translation_tolerance_m(&self) -> f64 {
351 self.translation_tolerance_m
352 }
353 pub const fn rotation_tolerance_deg(&self) -> f64 {
355 self.rotation_tolerance_deg
356 }
357 pub fn translation_offenders(&self) -> &[TransitionPoseTranslationOffenderV1] {
359 &self.translation_offenders
360 }
361 pub fn rotation_offenders(&self) -> &[TransitionPoseRotationOffenderV1] {
363 &self.rotation_offenders
364 }
365}
366
367#[derive(Debug, Clone, PartialEq, Serialize)]
369pub struct TransitionPoseFamilyEvaluationV1 {
370 family_id: String,
371 status: TransitionPoseStatusV1,
372 decision: TransitionPoseDecisionV1,
373 #[serde(skip_serializing_if = "Option::is_none")]
374 reason: Option<TransitionPoseReasonV1>,
375 members: Vec<TransitionPoseMemberV1>,
376 #[serde(skip_serializing_if = "Option::is_none")]
377 skeleton_basis_input: Option<InputIdentity>,
378 pairs: Vec<TransitionPosePairEvaluationV1>,
379}
380impl TransitionPoseFamilyEvaluationV1 {
381 pub fn family_id(&self) -> &str {
383 &self.family_id
384 }
385 pub const fn status(&self) -> TransitionPoseStatusV1 {
387 self.status
388 }
389 pub const fn decision(&self) -> TransitionPoseDecisionV1 {
391 self.decision
392 }
393 pub const fn reason(&self) -> Option<TransitionPoseReasonV1> {
395 self.reason
396 }
397 pub fn members(&self) -> &[TransitionPoseMemberV1] {
399 &self.members
400 }
401 pub const fn skeleton_basis_input(&self) -> Option<&InputIdentity> {
403 self.skeleton_basis_input.as_ref()
404 }
405 pub fn pairs(&self) -> &[TransitionPosePairEvaluationV1] {
407 &self.pairs
408 }
409}
410
411#[derive(Debug, Clone, PartialEq, Serialize)]
413pub struct TransitionPoseEvaluationV1 {
414 schema: &'static str,
415 schema_version: u32,
416 status: TransitionPoseStatusV1,
417 decision: TransitionPoseDecisionV1,
418 #[serde(skip_serializing_if = "Option::is_none")]
419 reason: Option<TransitionPoseReasonV1>,
420 declaration_input: InputIdentity,
421 declaration_normalized: InputIdentity,
422 subject_input: InputIdentity,
423 #[serde(skip_serializing_if = "Option::is_none")]
424 subject_dependency_closure_identity: Option<DependencyClosureIdentityV1>,
425 families: Vec<TransitionPoseFamilyEvaluationV1>,
426}
427
428impl TransitionPoseEvaluationV1 {
429 pub const fn schema(&self) -> &'static str {
431 self.schema
432 }
433 pub const fn schema_version(&self) -> u32 {
435 self.schema_version
436 }
437 pub const fn status(&self) -> TransitionPoseStatusV1 {
439 self.status
440 }
441 pub const fn decision(&self) -> TransitionPoseDecisionV1 {
443 self.decision
444 }
445 pub const fn reason(&self) -> Option<TransitionPoseReasonV1> {
447 self.reason
448 }
449 pub const fn declaration_input(&self) -> &InputIdentity {
451 &self.declaration_input
452 }
453 pub const fn declaration_normalized(&self) -> &InputIdentity {
455 &self.declaration_normalized
456 }
457 pub const fn subject_input(&self) -> &InputIdentity {
462 &self.subject_input
463 }
464 pub const fn subject_dependency_closure_identity(
472 &self,
473 ) -> Option<&DependencyClosureIdentityV1> {
474 self.subject_dependency_closure_identity.as_ref()
475 }
476 pub fn families(&self) -> &[TransitionPoseFamilyEvaluationV1] {
478 &self.families
479 }
480 pub fn normalized_jcs(&self) -> Result<Vec<u8>, TransitionPoseEvaluationControlError> {
482 canonical_bytes(self, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
483 .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)
484 }
485}
486
487#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
490#[non_exhaustive]
491pub enum TransitionPoseEvaluationControlError {
492 #[error("document transition-pose evaluation requires a document declaration")]
494 WrongDeclarationScope,
495 #[error("transition-pose skeleton basis is invalid: {0}")]
497 InvalidSkeletonBasis(SkeletonBasisError),
498 #[error("transition-pose declaration member witness is structurally contradictory")]
500 InvalidMemberWitness,
501 #[error("transition-pose result exceeds the V1 result cap")]
503 ResultTooLarge,
504 #[error("collection transition-pose declaration member witness is structurally contradictory")]
507 InvalidCollectionMemberWitness,
508 #[error("collection transition-pose manifest authority is structurally contradictory")]
510 InvalidCollectionManifestBinding,
511}
512
513#[derive(Clone, Copy)]
521pub struct CollectionTransitionPoseMemberInputV1<'a> {
522 logical_id: &'a crate::CollectionLogicalIdV1,
523 source: &'a crate::CollectionSourceKeyV1,
524 take_index: u64,
525 take_name: &'a str,
526 source_input: Option<&'a InputIdentity>,
527 loaded_source: Option<&'a LoadedSource>,
528 unavailable_reason: Option<TransitionPoseReasonV1>,
529}
530
531impl<'a> CollectionTransitionPoseMemberInputV1<'a> {
532 #[must_use]
535 pub const fn available(
536 logical_id: &'a crate::CollectionLogicalIdV1,
537 source: &'a crate::CollectionSourceKeyV1,
538 take_index: u64,
539 take_name: &'a str,
540 loaded_source: &'a LoadedSource,
541 ) -> Self {
542 Self {
543 logical_id,
544 source,
545 take_index,
546 take_name,
547 source_input: None,
548 loaded_source: Some(loaded_source),
549 unavailable_reason: None,
550 }
551 }
552
553 #[must_use]
556 pub const fn unavailable(
557 logical_id: &'a crate::CollectionLogicalIdV1,
558 source: &'a crate::CollectionSourceKeyV1,
559 take_index: u64,
560 take_name: &'a str,
561 ) -> Self {
562 Self {
563 logical_id,
564 source,
565 take_index,
566 take_name,
567 source_input: None,
568 loaded_source: None,
569 unavailable_reason: Some(TransitionPoseReasonV1::MemberUnavailable),
570 }
571 }
572
573 #[must_use]
576 pub const fn unavailable_with_source_input(
577 logical_id: &'a crate::CollectionLogicalIdV1,
578 source: &'a crate::CollectionSourceKeyV1,
579 take_index: u64,
580 take_name: &'a str,
581 source_input: &'a InputIdentity,
582 ) -> Self {
583 Self {
584 logical_id,
585 source,
586 take_index,
587 take_name,
588 source_input: Some(source_input),
589 loaded_source: None,
590 unavailable_reason: Some(TransitionPoseReasonV1::MemberUnavailable),
591 }
592 }
593
594 #[must_use]
598 pub const fn dependency_closure_incomplete(
599 logical_id: &'a crate::CollectionLogicalIdV1,
600 source: &'a crate::CollectionSourceKeyV1,
601 take_index: u64,
602 take_name: &'a str,
603 loaded_source: &'a LoadedSource,
604 ) -> Self {
605 Self {
606 logical_id,
607 source,
608 take_index,
609 take_name,
610 source_input: None,
611 loaded_source: Some(loaded_source),
612 unavailable_reason: Some(TransitionPoseReasonV1::DependencyClosureIncomplete),
613 }
614 }
615
616 fn primary_input(self) -> Option<&'a InputIdentity> {
617 self.loaded_source
618 .map(|source| source.source_facts().primary_identity())
619 .or(self.source_input)
620 }
621
622 fn dependency_closure(self) -> Option<&'a DependencyClosureV1> {
623 self.loaded_source.map(LoadedSource::dependency_closure)
624 }
625}
626
627pub fn evaluate_document_transition_poses_v1(
641 declaration: &TransitionFamilyDeclarationInputV1,
642 dependency_closure: &DependencyClosureV1,
643 document: &Document,
644) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
645 evaluate_document_transition_poses_v1_with_result_limit(
646 declaration,
647 dependency_closure,
648 document,
649 TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES,
650 )
651}
652
653pub fn evaluate_collection_transition_poses_v1(
668 declaration: &TransitionFamilyDeclarationInputV1,
669 manifest: &TransitionFamilyManifestIdentityV1,
670 members: &[CollectionTransitionPoseMemberInputV1<'_>],
671) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
672 evaluate_collection_transition_poses_v1_with_probes(
673 declaration,
674 manifest,
675 members,
676 |_| {},
677 |_| {},
678 )
679}
680
681fn evaluate_collection_transition_poses_v1_with_probes(
682 declaration: &TransitionFamilyDeclarationInputV1,
683 manifest: &TransitionFamilyManifestIdentityV1,
684 members: &[CollectionTransitionPoseMemberInputV1<'_>],
685 mut observe_basis_build: impl FnMut(&LoadedSource),
686 mut observe_track_visit: impl FnMut(&Track),
687) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
688 let declared_manifest = match declaration.declaration() {
689 crate::TransitionFamilyDeclarationV1::Collection { manifest, .. } => manifest,
690 _ => return Err(TransitionPoseEvaluationControlError::WrongDeclarationScope),
691 };
692 if declared_manifest != manifest {
693 return Err(TransitionPoseEvaluationControlError::InvalidCollectionManifestBinding);
694 }
695 let families = declaration
696 .declaration()
697 .collection_families()
698 .ok_or(TransitionPoseEvaluationControlError::WrongDeclarationScope)?;
699 let expected_members = families.iter().try_fold(0usize, |total, family| {
700 total.checked_add(family.members().len())
701 });
702 let Some(expected_members) = expected_members else {
703 return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
704 };
705 if expected_members != members.len() {
706 return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
707 }
708 let mut result = TransitionPoseEvaluationV1 {
709 schema: TRANSITION_POSE_EVALUATION_V1_ID,
710 schema_version: TRANSITION_POSE_EVALUATION_V1_SCHEMA_VERSION,
711 status: TransitionPoseStatusV1::Complete,
712 decision: TransitionPoseDecisionV1::Pass,
713 reason: None,
714 declaration_input: declaration.source_identity().clone(),
715 declaration_normalized: declaration.normalized_identity().clone(),
716 subject_input: manifest.input().clone(),
717 subject_dependency_closure_identity: None,
718 families: Vec::with_capacity(families.len()),
719 };
720 let mut source_authorities = BTreeMap::<&str, CollectionSourceAuthority<'_>>::new();
726 let mut cursor = 0usize;
727 let mut structurally_ready = Vec::<Option<CollectionFamilyReady<'_>>>::new();
728 for (family_index, family) in families.iter().enumerate() {
729 let family_members = &members[cursor..cursor + family.members().len()];
730 cursor += family_members.len();
731 let mut row = collection_family_result_row(family, family_members)?;
732 let mut selected = Vec::with_capacity(family_members.len());
733 for (declared, prepared) in family.members().iter().zip(family_members) {
734 if declared.logical_id() != prepared.logical_id
735 || declared.source() != prepared.source
736 || declared.take_index() != prepared.take_index
737 || declared.take_name() != prepared.take_name
738 {
739 return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
740 }
741 let authority = CollectionSourceAuthority::from_member(*prepared);
742 match source_authorities.entry(declared.source().as_str()) {
743 std::collections::btree_map::Entry::Vacant(entry) => {
744 entry.insert(authority);
745 }
746 std::collections::btree_map::Entry::Occupied(entry)
747 if !entry.get().same_authority(authority) =>
748 {
749 return Err(
750 TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness,
751 );
752 }
753 std::collections::btree_map::Entry::Occupied(_) => {}
754 }
755 if let Some(reason) = prepared.unavailable_reason {
756 retain_collection_runtime_reason(&mut row.reason, reason);
757 }
758 let Some(loaded_source) = prepared.loaded_source else {
759 if prepared.unavailable_reason.is_none() {
760 return Err(
761 TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness,
762 );
763 }
764 continue;
765 };
766 let source_input = loaded_source.source_facts().primary_identity();
767 let dependency_closure = loaded_source.dependency_closure();
768 let document = loaded_source.document();
769 let closure_is_complete = dependency_closure.primary_input() == source_input
770 && dependency_closure.coverage().is_complete()
771 && dependency_closure.identity().is_some();
772 if prepared.unavailable_reason
773 == Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
774 && closure_is_complete
775 {
776 return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
777 }
778 if !closure_is_complete {
779 retain_collection_runtime_reason(
780 &mut row.reason,
781 TransitionPoseReasonV1::DependencyClosureIncomplete,
782 );
783 }
784 let Some(clip) = usize::try_from(declared.take_index())
785 .ok()
786 .and_then(|index| document.clips.get(index))
787 .filter(|clip| clip.name == declared.take_name())
788 else {
789 retain_collection_runtime_reason(
790 &mut row.reason,
791 TransitionPoseReasonV1::MemberUnavailable,
792 );
793 continue;
794 };
795 if prepared.unavailable_reason.is_none() && closure_is_complete {
796 selected.push(CollectionResolvedMember {
797 source: declared.source(),
798 take_index: declared.take_index(),
799 loaded_source,
800 document,
801 clip,
802 });
803 }
804 }
805 let row_index = result.families.len();
806 result.families.push(row);
807 if selected.len() == family_members.len() {
808 structurally_ready.push(Some(CollectionFamilyReady {
809 family_index,
810 row_index,
811 family,
812 members: selected,
813 basis_cache_index: None,
814 }));
815 } else {
816 structurally_ready.push(None);
817 }
818 }
819
820 let policy_rejected = families
824 .iter()
825 .map(|family| family.tolerances().time_normalized() != 0.0)
826 .collect::<Vec<_>>();
827 let plans = plan_collection_families(families, &structurally_ready, &policy_rejected);
828 for ready in structurally_ready.iter().flatten() {
829 let row = &mut result.families[ready.row_index];
830 if policy_rejected[ready.family_index] {
831 row.reason = Some(TransitionPoseReasonV1::TimeToleranceUnsupported);
832 } else if let Some(reason) = plans[ready.family_index] {
833 row.reason = Some(reason);
834 }
835 }
836
837 let mut basis_by_source = BTreeMap::<&str, usize>::new();
841 let mut basis_cache = Vec::<CollectionCachedBasis>::new();
842 for ready in structurally_ready.iter_mut().flatten() {
843 if result.families[ready.row_index].reason.is_some() {
844 continue;
845 }
846 let mut family_basis_index: Option<usize> = None;
847 for member in &ready.members {
848 let source_key = member.source.as_str();
849 let cache_index = if let Some(&index) = basis_by_source.get(&source_key) {
850 index
851 } else {
852 observe_basis_build(member.loaded_source);
853 let cached = CollectionCachedBasis::build(&member.document.skeleton);
854 let index = basis_cache.len();
855 basis_cache.push(cached);
856 basis_by_source.insert(source_key, index);
857 index
858 };
859 let Some(current) = basis_cache[cache_index].basis.as_ref() else {
860 retain_collection_runtime_reason(
861 &mut result.families[ready.row_index].reason,
862 TransitionPoseReasonV1::MemberUnavailable,
863 );
864 continue;
865 };
866 if let Some(existing_index) = family_basis_index {
867 let existing = basis_cache[existing_index]
868 .basis
869 .as_ref()
870 .expect("validated cached collection basis");
871 if existing.identity() != current.identity() {
872 retain_collection_runtime_reason(
873 &mut result.families[ready.row_index].reason,
874 TransitionPoseReasonV1::SkeletonBasisMismatch,
875 );
876 }
877 } else {
878 family_basis_index = Some(cache_index);
879 }
880 }
881 if result.families[ready.row_index].reason.is_none() {
882 let basis_index = family_basis_index.expect("collection families have members");
883 ready.basis_cache_index = Some(basis_index);
884 result.families[ready.row_index].skeleton_basis_input = Some(
885 basis_cache[basis_index]
886 .basis
887 .as_ref()
888 .expect("validated cached collection basis")
889 .identity()
890 .clone(),
891 );
892 }
893 }
894
895 let mut detailed_name_budget = collection_detailed_budget_after_base(&result)?;
899 for ready_family in structurally_ready.iter().flatten() {
900 let row = &mut result.families[ready_family.row_index];
901 let Some(basis_index) = ready_family.basis_cache_index else {
902 continue;
903 };
904 if !reserve_collection_detailed_name_budget(
905 &mut detailed_name_budget,
906 ready_family.family,
907 &basis_cache[basis_index],
908 ) {
909 row.reason = Some(TransitionPoseReasonV1::ResultLimit);
910 }
911 }
912
913 let mut track_cache = BTreeMap::<(&str, u64), CollectionTrackAdmission>::new();
918 let mut aggregate_track_elements = 0usize;
919 for ready_family in structurally_ready.iter().flatten() {
920 let row = &mut result.families[ready_family.row_index];
921 if row.reason.is_some() {
922 continue;
923 }
924 for member in &ready_family.members {
925 let key = (member.source.as_str(), member.take_index);
926 let admission = if let Some(cached) = track_cache.get(&key) {
927 *cached
928 } else {
929 let inspected = inspect_collection_track_admission(
930 member.clip,
931 member.document.skeleton.bones.len(),
932 &mut observe_track_visit,
933 );
934 let admission = match inspected {
935 Ok(elements) => {
936 let total = aggregate_track_elements.checked_add(elements);
937 if total.is_some_and(|total| {
938 total <= TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS
939 }) {
940 aggregate_track_elements = total.expect("checked aggregate");
941 CollectionTrackAdmission::Accepted
942 } else {
943 CollectionTrackAdmission::Rejected(TransitionPoseReasonV1::InputLimit)
944 }
945 }
946 Err(reason) => CollectionTrackAdmission::Rejected(reason),
947 };
948 track_cache.insert(key, admission);
949 admission
950 };
951 if let CollectionTrackAdmission::Rejected(reason) = admission {
952 row.reason = Some(reason);
953 break;
954 }
955 }
956 if row.reason.is_none() {
957 let endpoints = match strict_collection_endpoints(
958 &ready_family.members,
959 ready_family.family.boundary(),
960 ) {
961 Ok(value) => value,
962 Err(reason) => {
963 row.reason = Some(reason);
964 continue;
965 }
966 };
967 row.pairs = match compare_pairs(
968 &endpoints,
969 ready_family.family.boundary(),
970 ready_family.family.tolerances(),
971 &ready_family.members[0].document.skeleton,
972 ) {
973 Ok(pairs) => pairs,
974 Err(reason) => {
975 row.reason = Some(reason);
976 continue;
977 }
978 };
979 row.status = TransitionPoseStatusV1::Complete;
980 row.decision = if row.pairs.iter().any(pair_has_finding) {
981 TransitionPoseDecisionV1::Finding
982 } else {
983 TransitionPoseDecisionV1::Pass
984 };
985 }
986 }
987 derive_result_state(&mut result);
988 enforce_result_limit(&mut result, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)?;
989 Ok(result)
990}
991
992fn retain_collection_runtime_reason(
998 current: &mut Option<TransitionPoseReasonV1>,
999 candidate: TransitionPoseReasonV1,
1000) {
1001 fn priority(reason: TransitionPoseReasonV1) -> u8 {
1002 match reason {
1003 TransitionPoseReasonV1::DependencyClosureIncomplete => 3,
1004 TransitionPoseReasonV1::MemberUnavailable => 2,
1005 TransitionPoseReasonV1::SkeletonBasisMismatch => 1,
1006 _ => 0,
1007 }
1008 }
1009 if current.is_none_or(|previous| priority(candidate) > priority(previous)) {
1010 *current = Some(candidate);
1011 }
1012}
1013
1014struct CollectionFamilyReady<'a> {
1015 family_index: usize,
1016 row_index: usize,
1017 family: &'a crate::CollectionTransitionFamilyV1,
1018 members: Vec<CollectionResolvedMember<'a>>,
1019 basis_cache_index: Option<usize>,
1020}
1021
1022#[derive(Clone, Copy)]
1023struct CollectionResolvedMember<'a> {
1024 source: &'a crate::CollectionSourceKeyV1,
1025 take_index: u64,
1026 loaded_source: &'a LoadedSource,
1027 document: &'a Document,
1028 clip: &'a Clip,
1029}
1030
1031#[derive(Clone, Copy)]
1032enum CollectionSourceAuthority<'a> {
1033 Unavailable {
1034 primary: Option<&'a InputIdentity>,
1035 reason: TransitionPoseReasonV1,
1036 },
1037 Loaded {
1038 source: &'a LoadedSource,
1039 reason: Option<TransitionPoseReasonV1>,
1040 },
1041}
1042
1043impl<'a> CollectionSourceAuthority<'a> {
1044 fn from_member(member: CollectionTransitionPoseMemberInputV1<'a>) -> Self {
1045 if let Some(loaded) = member.loaded_source {
1046 Self::Loaded {
1047 source: loaded,
1048 reason: member.unavailable_reason,
1049 }
1050 } else {
1051 Self::Unavailable {
1052 primary: member.source_input,
1053 reason: member
1054 .unavailable_reason
1055 .expect("collection input without a loaded source is unavailable"),
1056 }
1057 }
1058 }
1059
1060 fn same_authority(self, other: Self) -> bool {
1061 match (self, other) {
1062 (
1063 Self::Unavailable {
1064 primary: left_primary,
1065 reason: left_reason,
1066 },
1067 Self::Unavailable {
1068 primary: right_primary,
1069 reason: right_reason,
1070 },
1071 ) => left_primary == right_primary && left_reason == right_reason,
1072 (
1073 Self::Loaded {
1074 source: left_source,
1075 reason: left_reason,
1076 },
1077 Self::Loaded {
1078 source: right_source,
1079 reason: right_reason,
1080 },
1081 ) => std::ptr::eq(left_source, right_source) && left_reason == right_reason,
1082 _ => false,
1083 }
1084 }
1085}
1086
1087struct CollectionCachedBasis {
1088 basis: Option<SkeletonBasisV1>,
1089 max_escaped_name_bytes: usize,
1090}
1091
1092impl CollectionCachedBasis {
1093 fn build(skeleton: &Skeleton) -> Self {
1094 let Ok(basis) = SkeletonBasisV1::from_skeleton(skeleton) else {
1095 return Self {
1096 basis: None,
1097 max_escaped_name_bytes: 0,
1098 };
1099 };
1100 let max_escaped_name_bytes = basis
1101 .bones()
1102 .iter()
1103 .map(|bone| bone.name().len().saturating_mul(6))
1104 .max()
1105 .unwrap_or(0);
1106 Self {
1107 basis: Some(basis),
1108 max_escaped_name_bytes,
1109 }
1110 }
1111}
1112
1113#[derive(Clone, Copy)]
1114enum CollectionTrackAdmission {
1115 Accepted,
1116 Rejected(TransitionPoseReasonV1),
1117}
1118
1119fn collection_family_result_row(
1120 family: &crate::CollectionTransitionFamilyV1,
1121 members: &[CollectionTransitionPoseMemberInputV1<'_>],
1122) -> Result<TransitionPoseFamilyEvaluationV1, TransitionPoseEvaluationControlError> {
1123 if family.members().len() != members.len() {
1124 return Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness);
1125 }
1126 Ok(TransitionPoseFamilyEvaluationV1 {
1127 family_id: family.family_id().as_str().to_owned(),
1128 status: TransitionPoseStatusV1::Incomplete,
1129 decision: TransitionPoseDecisionV1::NotEvaluated,
1130 reason: None,
1131 members: family
1132 .members()
1133 .iter()
1134 .zip(members)
1135 .map(|(declared, prepared)| TransitionPoseMemberV1 {
1136 take_index: declared.take_index(),
1137 take_name: declared.take_name().to_owned(),
1138 source_input: prepared.primary_input().cloned(),
1139 source_dependency_closure_identity: prepared
1140 .dependency_closure()
1141 .and_then(complete_dependency_closure_identity),
1142 })
1143 .collect(),
1144 skeleton_basis_input: None,
1145 pairs: Vec::new(),
1146 })
1147}
1148
1149fn plan_collection_families(
1150 families: &[crate::CollectionTransitionFamilyV1],
1151 ready: &[Option<CollectionFamilyReady<'_>>],
1152 policy_rejected: &[bool],
1153) -> Vec<Option<TransitionPoseReasonV1>> {
1154 let mut aggregate_pairs = 0usize;
1155 let mut aggregate_comparisons = 0usize;
1156 let mut aggregate_retention = 0usize;
1157 families
1158 .iter()
1159 .enumerate()
1160 .map(|(index, family)| {
1161 let ready = ready[index].as_ref()?;
1162 if policy_rejected[index] {
1163 return None;
1164 }
1165 let boundaries = match family.boundary() {
1166 TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1167 TransitionFamilyBoundaryV1::Both => 2usize,
1168 };
1169 let pair_boundaries = checked_pair_count(family.members().len())
1170 .and_then(|pairs| pairs.checked_mul(boundaries));
1171 let bones = ready.members[0].document.skeleton.bones.len();
1172 let comparisons = pair_boundaries.and_then(|pairs| pairs.checked_mul(bones));
1173 let retention = pair_boundaries.and_then(|pairs| {
1174 bones
1175 .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1176 .checked_add(bones.min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS))
1177 .and_then(|per_pair| pairs.checked_mul(per_pair))
1178 });
1179 match (pair_boundaries, comparisons, retention) {
1180 (Some(pairs), _, _)
1181 if pairs > TRANSITION_POSE_EVALUATION_V1_MAX_FAMILY_PAIR_BOUNDARIES =>
1182 {
1183 Some(TransitionPoseReasonV1::FamilyWorkLimit)
1184 }
1185 (Some(pairs), Some(comparisons), Some(_))
1186 if aggregate_pairs.checked_add(pairs).is_none_or(|value| {
1187 value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_PAIR_BOUNDARIES
1188 }) || aggregate_comparisons.checked_add(comparisons).is_none_or(
1189 |value| value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS,
1190 ) =>
1191 {
1192 Some(TransitionPoseReasonV1::AggregateWorkLimit)
1193 }
1194 (Some(_), Some(_), Some(retention))
1195 if aggregate_retention
1196 .checked_add(retention)
1197 .is_none_or(|value| {
1198 value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_OFFENDERS
1199 }) =>
1200 {
1201 Some(TransitionPoseReasonV1::RetentionLimit)
1202 }
1203 (Some(pairs), Some(comparisons), Some(retention)) => {
1204 aggregate_pairs += pairs;
1205 aggregate_comparisons += comparisons;
1206 aggregate_retention += retention;
1207 None
1208 }
1209 _ => Some(TransitionPoseReasonV1::AggregateWorkLimit),
1210 }
1211 })
1212 .collect()
1213}
1214
1215fn collection_detailed_budget_after_base(
1216 result: &TransitionPoseEvaluationV1,
1217) -> Result<usize, TransitionPoseEvaluationControlError> {
1218 let mut base = result.clone();
1219 base.status = TransitionPoseStatusV1::Incomplete;
1220 base.decision = TransitionPoseDecisionV1::NotEvaluated;
1221 for family in &mut base.families {
1222 if family.reason.is_none() {
1227 family.reason = Some(TransitionPoseReasonV1::DependencyClosureIncomplete);
1228 }
1229 family.pairs.clear();
1230 }
1231 let bytes = canonical_bytes(&base, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
1232 .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1233 .len();
1234 let absent_source_reserve = base
1238 .families
1239 .iter()
1240 .flat_map(|family| family.members.iter())
1241 .filter(|member| member.source_input.is_none())
1242 .count()
1243 .checked_mul(max_member_input_identity_replacement_bytes()?)
1244 .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)?;
1245 let absent_closure_reserve = base
1246 .families
1247 .iter()
1248 .flat_map(|family| family.members.iter())
1249 .filter(|member| member.source_dependency_closure_identity.is_none())
1250 .count()
1251 .checked_mul(max_member_closure_identity_addition_bytes()?)
1252 .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)?;
1253 TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES
1254 .checked_sub(bytes)
1255 .and_then(|remaining| remaining.checked_sub(absent_source_reserve))
1256 .and_then(|remaining| remaining.checked_sub(absent_closure_reserve))
1257 .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1258}
1259
1260fn max_member_input_identity_replacement_bytes()
1266-> Result<usize, TransitionPoseEvaluationControlError> {
1267 let identity = InputIdentity::from_sha256_digest([0xff; 32], u64::MAX);
1268 let identity_bytes = canonical_bytes(&identity, 256)
1269 .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1270 .len();
1271 identity_bytes
1272 .checked_sub(b"null".len())
1273 .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1274}
1275
1276fn max_member_closure_identity_addition_bytes()
1281-> Result<usize, TransitionPoseEvaluationControlError> {
1282 let identity = InputIdentity::from_sha256_digest([0xff; 32], u64::MAX);
1283 let value = canonical_bytes(&identity, 256)
1284 .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1285 .len();
1286 b",\"source_dependency_closure_identity\":"
1287 .len()
1288 .checked_add(value)
1289 .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1290}
1291
1292fn reserve_collection_detailed_name_budget(
1293 remaining: &mut usize,
1294 family: &crate::CollectionTransitionFamilyV1,
1295 cached: &CollectionCachedBasis,
1296) -> bool {
1297 let Some(basis) = cached.basis.as_ref() else {
1298 return false;
1299 };
1300 let boundaries = match family.boundary() {
1301 TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1302 TransitionFamilyBoundaryV1::Both => 2usize,
1303 };
1304 let Some(pair_boundaries) =
1305 checked_pair_count(family.members().len()).and_then(|pairs| pairs.checked_mul(boundaries))
1306 else {
1307 return false;
1308 };
1309 let Some(rows) = basis
1310 .bones()
1311 .len()
1312 .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1313 .checked_add(
1314 basis
1315 .bones()
1316 .len()
1317 .min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS),
1318 )
1319 else {
1320 return false;
1321 };
1322 let Some(bytes) = pair_boundaries
1323 .checked_mul(MAX_DETAILED_PAIR_FIXED_BYTES)
1324 .and_then(|fixed| {
1325 pair_boundaries
1326 .checked_mul(rows)?
1327 .checked_mul(cached.max_escaped_name_bytes)
1328 .and_then(|names| fixed.checked_add(names))
1329 })
1330 else {
1331 return false;
1332 };
1333 if bytes > *remaining {
1334 return false;
1335 }
1336 *remaining -= bytes;
1337 true
1338}
1339
1340fn strict_collection_endpoints(
1341 members: &[CollectionResolvedMember<'_>],
1342 boundary: TransitionFamilyBoundaryV1,
1343) -> Result<Vec<Endpoints>, TransitionPoseReasonV1> {
1344 members
1345 .iter()
1346 .map(|member| {
1347 let document = member.document;
1348 let clip = member.clip;
1349 if clip.duration_s == 0.0 {
1350 return Err(TransitionPoseReasonV1::ZeroDuration);
1351 }
1352 if !clip.duration_s.is_finite() || clip.duration_s < 0.0 {
1353 return Err(TransitionPoseReasonV1::UnsupportedSampling);
1354 }
1355 let entry = matches!(
1356 boundary,
1357 TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Both
1358 )
1359 .then(|| strict_endpoint(&document.skeleton, clip, 0.0))
1360 .transpose()?;
1361 let exit = matches!(
1362 boundary,
1363 TransitionFamilyBoundaryV1::Exit | TransitionFamilyBoundaryV1::Both
1364 )
1365 .then(|| {
1366 let time = clip.duration_s as f32;
1367 if !time.is_finite() || f64::from(time) != clip.duration_s {
1368 return Err(TransitionPoseReasonV1::UnsupportedSampling);
1369 }
1370 strict_endpoint(&document.skeleton, clip, time)
1371 })
1372 .transpose()?;
1373 Ok(Endpoints { entry, exit })
1374 })
1375 .collect()
1376}
1377
1378fn inspect_collection_track_admission(
1379 clip: &Clip,
1380 bone_count: usize,
1381 observe_track_visit: &mut impl FnMut(&Track),
1382) -> Result<usize, TransitionPoseReasonV1> {
1383 let (Some(raw_track_limit), Some(selected_track_limit)) =
1384 (bone_count.checked_mul(3), bone_count.checked_mul(2))
1385 else {
1386 return Err(TransitionPoseReasonV1::InputLimit);
1387 };
1388 if clip.tracks.len() > raw_track_limit {
1389 return Err(TransitionPoseReasonV1::InputLimit);
1390 }
1391 let mut selected_tracks = 0usize;
1392 let mut selected_elements = 0usize;
1393 let mut seen = vec![false; selected_track_limit];
1394 let mut unsupported = false;
1395 for track in &clip.tracks {
1396 observe_track_visit(track);
1397 let Some(channel) = transition_pose_channel(track.property) else {
1398 continue;
1399 };
1400 selected_tracks = selected_tracks
1401 .checked_add(1)
1402 .ok_or(TransitionPoseReasonV1::InputLimit)?;
1403 if selected_tracks > selected_track_limit {
1404 return Err(TransitionPoseReasonV1::InputLimit);
1405 }
1406 let elements = track
1407 .times
1408 .len()
1409 .checked_add(track.values.len())
1410 .ok_or(TransitionPoseReasonV1::InputLimit)?;
1411 selected_elements = selected_elements
1412 .checked_add(elements)
1413 .ok_or(TransitionPoseReasonV1::InputLimit)?;
1414 if selected_elements > TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS {
1415 return Err(TransitionPoseReasonV1::InputLimit);
1416 }
1417 let target = track
1418 .bone
1419 .checked_mul(2)
1420 .and_then(|base| base.checked_add(channel));
1421 match target {
1422 Some(target)
1423 if target < seen.len()
1424 && !seen[target]
1425 && validate_track_shape(0, track).is_ok() =>
1426 {
1427 seen[target] = true;
1428 }
1429 _ => unsupported = true,
1430 }
1431 }
1432 if unsupported {
1433 Err(TransitionPoseReasonV1::UnsupportedSampling)
1434 } else {
1435 Ok(selected_elements)
1436 }
1437}
1438
1439fn evaluate_document_transition_poses_v1_with_result_limit(
1440 declaration: &TransitionFamilyDeclarationInputV1,
1441 dependency_closure: &DependencyClosureV1,
1442 document: &Document,
1443 result_limit: usize,
1444) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
1445 let families = declaration
1446 .declaration()
1447 .document_families()
1448 .ok_or(TransitionPoseEvaluationControlError::WrongDeclarationScope)?;
1449 let mut result = TransitionPoseEvaluationV1 {
1450 schema: TRANSITION_POSE_EVALUATION_V1_ID,
1451 schema_version: TRANSITION_POSE_EVALUATION_V1_SCHEMA_VERSION,
1452 status: TransitionPoseStatusV1::Complete,
1453 decision: TransitionPoseDecisionV1::Pass,
1454 reason: None,
1455 declaration_input: declaration.source_identity().clone(),
1456 declaration_normalized: declaration.normalized_identity().clone(),
1457 subject_input: dependency_closure.primary_input().clone(),
1458 subject_dependency_closure_identity: complete_dependency_closure_identity(
1459 dependency_closure,
1460 ),
1461 families: Vec::with_capacity(families.len()),
1462 };
1463 if families.is_empty() {
1464 result.reason = Some(TransitionPoseReasonV1::NoConfiguredFamilies);
1465 return Ok(result);
1466 }
1467 let clip_admission = resolve_document_family_clips(document, families)?;
1471 if result.subject_dependency_closure_identity.is_none() {
1472 for family in families {
1473 let mut row = family_result_row(family, dependency_closure, None);
1474 row.reason = Some(TransitionPoseReasonV1::DependencyClosureIncomplete);
1475 result.families.push(row);
1476 }
1477 derive_result_state(&mut result);
1478 canonical_bytes(&result, result_limit)
1479 .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?;
1480 return Ok(result);
1481 }
1482 let resolved_clips = match clip_admission {
1483 DocumentClipAdmission::InputLimit => {
1484 for family in families {
1485 let mut row = family_result_row(family, dependency_closure, None);
1486 row.reason = Some(TransitionPoseReasonV1::InputLimit);
1487 result.families.push(row);
1488 }
1489 derive_result_state(&mut result);
1490 return Ok(result);
1491 }
1492 DocumentClipAdmission::Resolved(clips) => clips,
1493 };
1494 if !skeleton_input_is_within_limits(&document.skeleton) {
1495 for family in families {
1496 let mut row = family_result_row(family, dependency_closure, None);
1497 row.reason = Some(TransitionPoseReasonV1::InputLimit);
1498 result.families.push(row);
1499 }
1500 derive_result_state(&mut result);
1501 return Ok(result);
1502 }
1503 validate_transition_pose_skeleton(&document.skeleton)
1504 .map_err(TransitionPoseEvaluationControlError::InvalidSkeletonBasis)?;
1505 let basis = SkeletonBasisV1::from_skeleton(&document.skeleton)
1506 .map_err(TransitionPoseEvaluationControlError::InvalidSkeletonBasis)?;
1507 let policy_rejected = families
1508 .iter()
1509 .map(|family| family.tolerances().time_normalized() != 0.0)
1510 .collect::<Vec<_>>();
1511 let track_limits =
1512 plan_selected_track_input_limits(&resolved_clips, basis.bones().len(), &policy_rejected);
1513 let plans = plan_families(families, basis.bones().len(), &track_limits);
1514 let mut detailed_name_budget = detailed_result_budget_after_base(
1515 &result,
1516 families,
1517 dependency_closure,
1518 basis.identity(),
1519 result_limit,
1520 )?;
1521 for (((family, plan), clips), track_limit) in families
1522 .iter()
1523 .zip(plans)
1524 .zip(resolved_clips)
1525 .zip(track_limits)
1526 {
1527 let mut row = family_result_row(family, dependency_closure, Some(basis.identity()));
1528 if family.tolerances().time_normalized() != 0.0 {
1529 row.reason = Some(TransitionPoseReasonV1::TimeToleranceUnsupported);
1530 } else if let Some(reason) = plan.reason {
1531 row.reason = Some(reason);
1532 } else if track_limit {
1533 row.reason = Some(TransitionPoseReasonV1::InputLimit);
1534 } else if !reserve_detailed_name_budget(
1535 &mut detailed_name_budget,
1536 family,
1537 &document.skeleton,
1538 ) {
1539 row.reason = Some(TransitionPoseReasonV1::ResultLimit);
1540 } else {
1541 let endpoints = match strict_endpoints(document, &clips, family.boundary()) {
1542 Ok(value) => value,
1543 Err(reason) => {
1544 row.reason = Some(reason);
1545 result.families.push(row);
1546 continue;
1547 }
1548 };
1549 row.pairs = match compare_pairs(
1550 &endpoints,
1551 family.boundary(),
1552 family.tolerances(),
1553 &document.skeleton,
1554 ) {
1555 Ok(pairs) => pairs,
1556 Err(reason) => {
1557 row.reason = Some(reason);
1558 result.families.push(row);
1559 continue;
1560 }
1561 };
1562 row.status = TransitionPoseStatusV1::Complete;
1563 row.decision = if row.pairs.iter().any(pair_has_finding) {
1564 TransitionPoseDecisionV1::Finding
1565 } else {
1566 TransitionPoseDecisionV1::Pass
1567 };
1568 }
1569 result.families.push(row);
1570 }
1571 derive_result_state(&mut result);
1572 enforce_result_limit(&mut result, result_limit)?;
1573 Ok(result)
1574}
1575
1576fn family_result_row(
1577 family: &crate::DocumentTransitionFamilyV1,
1578 dependency_closure: &DependencyClosureV1,
1579 skeleton_basis_input: Option<&InputIdentity>,
1580) -> TransitionPoseFamilyEvaluationV1 {
1581 TransitionPoseFamilyEvaluationV1 {
1582 family_id: family.family_id().to_owned(),
1583 status: TransitionPoseStatusV1::Incomplete,
1584 decision: TransitionPoseDecisionV1::NotEvaluated,
1585 reason: None,
1586 members: family
1587 .members()
1588 .iter()
1589 .map(|member| TransitionPoseMemberV1 {
1590 take_index: member.take_index(),
1591 take_name: member.take_name().to_owned(),
1592 source_input: Some(dependency_closure.primary_input().clone()),
1593 source_dependency_closure_identity: complete_dependency_closure_identity(
1594 dependency_closure,
1595 ),
1596 })
1597 .collect(),
1598 skeleton_basis_input: skeleton_basis_input.cloned(),
1599 pairs: Vec::new(),
1600 }
1601}
1602
1603fn complete_dependency_closure_identity(
1604 dependency_closure: &DependencyClosureV1,
1605) -> Option<DependencyClosureIdentityV1> {
1606 if !dependency_closure.coverage().is_complete() {
1607 return None;
1608 }
1609 dependency_closure.identity().cloned()
1610}
1611
1612fn enforce_result_limit(
1616 result: &mut TransitionPoseEvaluationV1,
1617 limit: usize,
1618) -> Result<(), TransitionPoseEvaluationControlError> {
1619 if canonical_bytes(result, limit).is_ok() {
1620 return Ok(());
1621 }
1622 for family in &mut result.families {
1623 family.status = TransitionPoseStatusV1::Incomplete;
1624 family.decision = TransitionPoseDecisionV1::NotEvaluated;
1625 family.reason = Some(TransitionPoseReasonV1::ResultLimit);
1626 family.pairs.clear();
1627 }
1628 derive_result_state(result);
1629 canonical_bytes(result, limit)
1630 .map(|_| ())
1631 .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)
1632}
1633
1634#[derive(Clone, Copy)]
1635struct FamilyPlan {
1636 reason: Option<TransitionPoseReasonV1>,
1637}
1638
1639fn plan_families(
1640 families: &[crate::DocumentTransitionFamilyV1],
1641 bone_count: usize,
1642 input_limited: &[bool],
1643) -> Vec<FamilyPlan> {
1644 let mut aggregate_pairs = 0usize;
1645 let mut aggregate_comparisons = 0usize;
1646 let mut aggregate_retention = 0usize;
1647 families
1648 .iter()
1649 .zip(input_limited.iter().copied())
1650 .map(|(family, input_limited)| {
1651 if input_limited || family.tolerances().time_normalized() != 0.0 {
1652 return FamilyPlan { reason: None };
1653 }
1654 let boundaries = match family.boundary() {
1655 TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1656 TransitionFamilyBoundaryV1::Both => 2usize,
1657 };
1658 let pairs = checked_pair_count(family.members().len());
1659 let pair_boundaries = pairs.and_then(|value| value.checked_mul(boundaries));
1660 let comparisons = pair_boundaries.and_then(|value| value.checked_mul(bone_count));
1661 let retained_per_pair_boundary = bone_count
1662 .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1663 .checked_add(bone_count.min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS));
1664 let retention = pair_boundaries.and_then(|value| {
1665 retained_per_pair_boundary.and_then(|cap| value.checked_mul(cap))
1666 });
1667 let reason = match (pair_boundaries, comparisons, retention) {
1668 (Some(pair_boundaries), _, _)
1669 if pair_boundaries
1670 > TRANSITION_POSE_EVALUATION_V1_MAX_FAMILY_PAIR_BOUNDARIES =>
1671 {
1672 Some(TransitionPoseReasonV1::FamilyWorkLimit)
1673 }
1674 (Some(pair_boundaries), Some(comparisons), Some(_retention))
1675 if aggregate_pairs
1676 .checked_add(pair_boundaries)
1677 .is_none_or(|value| {
1678 value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_PAIR_BOUNDARIES
1679 })
1680 || aggregate_comparisons
1681 .checked_add(comparisons)
1682 .is_none_or(|value| {
1683 value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_COMPARISONS
1684 }) =>
1685 {
1686 Some(TransitionPoseReasonV1::AggregateWorkLimit)
1687 }
1688 (Some(_pair_boundaries), Some(_comparisons), Some(retention))
1689 if aggregate_retention
1690 .checked_add(retention)
1691 .is_none_or(|value| {
1692 value > TRANSITION_POSE_EVALUATION_V1_MAX_AGGREGATE_OFFENDERS
1693 }) =>
1694 {
1695 Some(TransitionPoseReasonV1::RetentionLimit)
1696 }
1697 (Some(pair_boundaries), Some(comparisons), Some(retention)) => {
1698 aggregate_pairs += pair_boundaries;
1699 aggregate_comparisons += comparisons;
1700 aggregate_retention += retention;
1701 None
1702 }
1703 _ => Some(TransitionPoseReasonV1::AggregateWorkLimit),
1704 };
1705 FamilyPlan { reason }
1706 })
1707 .collect()
1708}
1709
1710fn checked_pair_count(member_count: usize) -> Option<usize> {
1711 member_count
1712 .checked_mul(member_count.checked_sub(1)?)
1713 .and_then(|value| value.checked_div(2))
1714}
1715
1716enum DocumentClipAdmission<'a> {
1717 InputLimit,
1718 Resolved(Vec<Vec<&'a Clip>>),
1719}
1720
1721fn resolve_document_family_clips<'a>(
1726 document: &'a Document,
1727 families: &[crate::DocumentTransitionFamilyV1],
1728) -> Result<DocumentClipAdmission<'a>, TransitionPoseEvaluationControlError> {
1729 let mut names = BTreeMap::<&str, usize>::new();
1730 for family in families {
1731 for member in family.members() {
1732 let index = usize::try_from(member.take_index())
1733 .map_err(|_| TransitionPoseEvaluationControlError::InvalidMemberWitness)?;
1734 document
1735 .clips
1736 .get(index)
1737 .filter(|clip| clip.name == member.take_name())
1738 .ok_or(TransitionPoseEvaluationControlError::InvalidMemberWitness)?;
1739 names.entry(member.take_name()).or_insert(0);
1740 }
1741 }
1742 if document.clips.len() > TRANSITION_POSE_EVALUATION_V1_MAX_DOCUMENT_CLIPS {
1743 return Ok(DocumentClipAdmission::InputLimit);
1744 }
1745 for clip in &document.clips {
1746 if let Some(count) = names.get_mut(clip.name.as_str()) {
1747 *count = count.saturating_add(1);
1748 }
1749 }
1750 if names.values().any(|&count| count != 1) {
1751 return Err(TransitionPoseEvaluationControlError::InvalidMemberWitness);
1752 }
1753 families
1754 .iter()
1755 .map(|family| {
1756 family
1757 .members()
1758 .iter()
1759 .map(|member| {
1760 let index = usize::try_from(member.take_index())
1761 .map_err(|_| TransitionPoseEvaluationControlError::InvalidMemberWitness)?;
1762 document
1763 .clips
1764 .get(index)
1765 .ok_or(TransitionPoseEvaluationControlError::InvalidMemberWitness)
1766 })
1767 .collect()
1768 })
1769 .collect::<Result<Vec<Vec<_>>, _>>()
1770 .map(DocumentClipAdmission::Resolved)
1771}
1772
1773fn skeleton_input_is_within_limits(skeleton: &Skeleton) -> bool {
1775 skeleton_input_is_within_limits_with(
1776 skeleton,
1777 TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES,
1778 )
1779}
1780
1781fn skeleton_input_is_within_limits_with(skeleton: &Skeleton, text_limit: usize) -> bool {
1782 if skeleton.bones.len() > TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
1783 return false;
1784 }
1785 skeleton_text_is_within_limit(skeleton, text_limit)
1786}
1787
1788fn skeleton_text_is_within_limit(skeleton: &Skeleton, text_limit: usize) -> bool {
1789 skeleton
1790 .bones
1791 .iter()
1792 .try_fold(0usize, |total, bone| total.checked_add(bone.name.len()))
1793 .is_some_and(|total| total <= text_limit)
1794}
1795
1796fn validate_transition_pose_skeleton(skeleton: &Skeleton) -> Result<(), SkeletonBasisError> {
1800 for (ordinal, bone) in skeleton.bones.iter().enumerate() {
1801 if matches!(bone.parent, Some(parent) if parent >= ordinal) {
1802 return Err(SkeletonBasisError::InvalidParent { ordinal });
1803 }
1804 if finite_translation(bone, ordinal).is_err()
1805 || canonical_quaternion(bone.rest.rotation, ordinal).is_err()
1806 {
1807 return Err(SkeletonBasisError::InvalidRest { ordinal });
1808 }
1809 }
1810 Ok(())
1811}
1812
1813fn plan_selected_track_input_limits(
1818 resolved_clips: &[Vec<&Clip>],
1819 bone_count: usize,
1820 policy_rejected: &[bool],
1821) -> Vec<bool> {
1822 let (Some(raw_track_limit), Some(track_limit)) =
1823 (bone_count.checked_mul(3), bone_count.checked_mul(2))
1824 else {
1825 return vec![true; resolved_clips.len()];
1826 };
1827 plan_selected_track_input_limits_with(
1828 resolved_clips,
1829 raw_track_limit,
1830 track_limit,
1831 TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACK_ELEMENTS,
1832 policy_rejected,
1833 )
1834}
1835
1836fn plan_selected_track_input_limits_with(
1837 resolved_clips: &[Vec<&Clip>],
1838 raw_track_limit: usize,
1839 track_limit: usize,
1840 element_limit: usize,
1841 policy_rejected: &[bool],
1842) -> Vec<bool> {
1843 let mut aggregate_elements = 0usize;
1844 resolved_clips
1845 .iter()
1846 .zip(policy_rejected.iter().copied())
1847 .map(|(clips, policy_rejected)| {
1848 if policy_rejected {
1849 return false;
1850 }
1851 let mut family_elements = 0usize;
1852 for clip in clips {
1853 if clip.tracks.len() > raw_track_limit {
1854 return true;
1855 }
1856 let mut selected_tracks = 0usize;
1857 for track in clip
1858 .tracks
1859 .iter()
1860 .filter(|track| is_transition_pose_property(track.property))
1861 {
1862 let Some(count) = selected_tracks.checked_add(1) else {
1863 return true;
1864 };
1865 if count > track_limit {
1866 return true;
1867 }
1868 selected_tracks = count;
1869 let elements = track.times.len().checked_add(track.values.len());
1870 let Some(total) =
1871 elements.and_then(|elements| family_elements.checked_add(elements))
1872 else {
1873 return true;
1874 };
1875 if total > element_limit {
1876 return true;
1877 }
1878 family_elements = total;
1879 }
1880 }
1881 let Some(total) = aggregate_elements.checked_add(family_elements) else {
1882 return true;
1883 };
1884 if total > element_limit {
1885 return true;
1886 }
1887 aggregate_elements = total;
1888 false
1889 })
1890 .collect()
1891}
1892
1893fn detailed_result_budget_after_base(
1898 result: &TransitionPoseEvaluationV1,
1899 families: &[crate::DocumentTransitionFamilyV1],
1900 dependency_closure: &DependencyClosureV1,
1901 basis_input: &InputIdentity,
1902 limit: usize,
1903) -> Result<usize, TransitionPoseEvaluationControlError> {
1904 let mut base = result.clone();
1905 base.status = TransitionPoseStatusV1::Incomplete;
1906 base.decision = TransitionPoseDecisionV1::NotEvaluated;
1907 base.reason = None;
1908 base.families = families
1909 .iter()
1910 .map(|family| {
1911 let mut row = family_result_row(family, dependency_closure, Some(basis_input));
1912 row.reason = Some(TransitionPoseReasonV1::TimeToleranceUnsupported);
1913 row
1914 })
1915 .collect();
1916 let base_bytes = canonical_bytes(&base, limit)
1917 .map_err(|_| TransitionPoseEvaluationControlError::ResultTooLarge)?
1918 .len();
1919 limit
1920 .checked_sub(base_bytes)
1921 .ok_or(TransitionPoseEvaluationControlError::ResultTooLarge)
1922}
1923
1924fn reserve_detailed_name_budget(
1931 remaining: &mut usize,
1932 family: &crate::DocumentTransitionFamilyV1,
1933 skeleton: &Skeleton,
1934) -> bool {
1935 let boundaries = match family.boundary() {
1936 TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Exit => 1usize,
1937 TransitionFamilyBoundaryV1::Both => 2usize,
1938 };
1939 let Some(pair_boundaries) =
1940 checked_pair_count(family.members().len()).and_then(|pairs| pairs.checked_mul(boundaries))
1941 else {
1942 return false;
1943 };
1944 let Some(rows_per_pair_boundary) = skeleton
1945 .bones
1946 .len()
1947 .min(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS)
1948 .checked_add(
1949 skeleton
1950 .bones
1951 .len()
1952 .min(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS),
1953 )
1954 else {
1955 return false;
1956 };
1957 let fixed_bytes = pair_boundaries.checked_mul(MAX_DETAILED_PAIR_FIXED_BYTES);
1958 let max_escaped_name_bytes = skeleton
1959 .bones
1960 .iter()
1961 .map(|bone| bone.name.len().checked_mul(6))
1962 .max()
1963 .unwrap_or(Some(0));
1964 let Some(bytes) = fixed_bytes.and_then(|fixed| {
1965 max_escaped_name_bytes.and_then(|name| {
1966 pair_boundaries
1967 .checked_mul(rows_per_pair_boundary)?
1968 .checked_mul(name)
1969 .and_then(|names| fixed.checked_add(names))
1970 })
1971 }) else {
1972 return false;
1973 };
1974 if bytes > *remaining {
1975 return false;
1976 }
1977 *remaining -= bytes;
1978 true
1979}
1980
1981#[derive(Clone)]
1982struct Endpoints {
1983 entry: Option<Vec<EndpointPose>>,
1984 exit: Option<Vec<EndpointPose>>,
1985}
1986
1987#[derive(Clone, Copy)]
1988struct EndpointPose {
1989 translation: [f64; 3],
1990 rotation: [f64; 4],
1991}
1992
1993fn strict_endpoints(
1994 document: &Document,
1995 clips: &[&Clip],
1996 boundary: TransitionFamilyBoundaryV1,
1997) -> Result<Vec<Endpoints>, TransitionPoseReasonV1> {
1998 clips
1999 .iter()
2000 .map(|clip| {
2001 if !selected_tracks_are_strict(clip, document.skeleton.bones.len()) {
2002 return Err(TransitionPoseReasonV1::UnsupportedSampling);
2003 }
2004 if clip.duration_s == 0.0 {
2005 return Err(TransitionPoseReasonV1::ZeroDuration);
2006 }
2007 if !clip.duration_s.is_finite() || clip.duration_s < 0.0 {
2008 return Err(TransitionPoseReasonV1::UnsupportedSampling);
2009 }
2010 let needs_exit = matches!(
2011 boundary,
2012 TransitionFamilyBoundaryV1::Exit | TransitionFamilyBoundaryV1::Both
2013 );
2014 let exit_time = needs_exit.then(|| {
2015 let time = clip.duration_s as f32;
2016 if !time.is_finite() || f64::from(time) != clip.duration_s {
2017 Err(TransitionPoseReasonV1::UnsupportedSampling)
2018 } else {
2019 Ok(time)
2020 }
2021 });
2022 Ok(Endpoints {
2023 entry: matches!(
2024 boundary,
2025 TransitionFamilyBoundaryV1::Entry | TransitionFamilyBoundaryV1::Both
2026 )
2027 .then(|| strict_endpoint(&document.skeleton, clip, 0.0))
2028 .transpose()?,
2029 exit: exit_time
2030 .transpose()?
2031 .map(|time| strict_endpoint(&document.skeleton, clip, time))
2032 .transpose()?,
2033 })
2034 })
2035 .collect()
2036}
2037
2038fn selected_tracks_are_strict(clip: &Clip, bone_count: usize) -> bool {
2039 selected_tracks_are_strict_with(clip, bone_count, |_| {})
2040}
2041
2042fn selected_tracks_are_strict_with(
2046 clip: &Clip,
2047 bone_count: usize,
2048 mut observe_selected: impl FnMut(&Track),
2049) -> bool {
2050 let (Some(raw_track_limit), Some(seen_len)) =
2051 (bone_count.checked_mul(3), bone_count.checked_mul(2))
2052 else {
2053 return false;
2054 };
2055 if clip.tracks.len() > raw_track_limit {
2056 return false;
2057 }
2058 let mut seen = vec![false; seen_len];
2059 for track in &clip.tracks {
2060 let Some(channel) = transition_pose_channel(track.property) else {
2061 continue;
2062 };
2063 observe_selected(track);
2064 if track.bone >= bone_count {
2065 return false;
2066 }
2067 let Some(index) = track
2068 .bone
2069 .checked_mul(2)
2070 .and_then(|base| base.checked_add(channel))
2071 else {
2072 return false;
2073 };
2074 if seen[index] || validate_track_shape(0, track).is_err() {
2075 return false;
2076 }
2077 seen[index] = true;
2078 }
2079 true
2080}
2081
2082fn is_transition_pose_property(property: Property) -> bool {
2083 transition_pose_channel(property).is_some()
2084}
2085
2086fn transition_pose_channel(property: Property) -> Option<usize> {
2087 match property {
2088 Property::Translation => Some(0),
2089 Property::Rotation => Some(1),
2090 Property::Scale => None,
2091 }
2092}
2093
2094fn strict_endpoint(
2095 skeleton: &Skeleton,
2096 clip: &Clip,
2097 time: f32,
2098) -> Result<Vec<EndpointPose>, TransitionPoseReasonV1> {
2099 if !time.is_finite() {
2100 return Err(TransitionPoseReasonV1::UnsupportedSampling);
2101 }
2102 let mut poses = skeleton
2103 .bones
2104 .iter()
2105 .enumerate()
2106 .map(|(ordinal, bone)| {
2107 Ok(EndpointPose {
2108 translation: finite_translation(bone, ordinal)
2109 .map_err(|_| TransitionPoseReasonV1::UnsupportedSampling)?,
2110 rotation: canonical_quaternion(bone.rest.rotation, ordinal)
2111 .map_err(|_| TransitionPoseReasonV1::UnsupportedSampling)?,
2112 })
2113 })
2114 .collect::<Result<Vec<_>, _>>()?;
2115 for track in &clip.tracks {
2116 match track.property {
2117 Property::Scale => continue,
2118 Property::Translation => match crate::sample_track(track, time) {
2119 TrackSample::Vec3(value) => {
2120 let target = poses
2121 .get_mut(track.bone)
2122 .ok_or(TransitionPoseReasonV1::UnsupportedSampling)?;
2123 target.translation =
2124 finite_vec3(value).ok_or(TransitionPoseReasonV1::UnsupportedSampling)?;
2125 }
2126 _ => return Err(TransitionPoseReasonV1::UnsupportedSampling),
2127 },
2128 Property::Rotation => match crate::sample_track(track, time) {
2129 TrackSample::Quat(value) => {
2130 let target = poses
2131 .get_mut(track.bone)
2132 .ok_or(TransitionPoseReasonV1::UnsupportedSampling)?;
2133 target.rotation = canonical_quaternion(value, track.bone)
2134 .map_err(|_| TransitionPoseReasonV1::UnsupportedSampling)?;
2135 }
2136 _ => return Err(TransitionPoseReasonV1::UnsupportedSampling),
2137 },
2138 }
2139 }
2140 Ok(poses)
2141}
2142
2143fn compare_pairs(
2144 endpoints: &[Endpoints],
2145 boundary: TransitionFamilyBoundaryV1,
2146 tolerances: TransitionFamilyTolerancesV1,
2147 skeleton: &Skeleton,
2148) -> Result<Vec<TransitionPosePairEvaluationV1>, TransitionPoseReasonV1> {
2149 let boundaries: &[TransitionFamilyBoundaryV1] = match boundary {
2150 TransitionFamilyBoundaryV1::Entry => &[TransitionFamilyBoundaryV1::Entry],
2151 TransitionFamilyBoundaryV1::Exit => &[TransitionFamilyBoundaryV1::Exit],
2152 TransitionFamilyBoundaryV1::Both => &[
2153 TransitionFamilyBoundaryV1::Entry,
2154 TransitionFamilyBoundaryV1::Exit,
2155 ],
2156 };
2157 let mut output = Vec::new();
2158 for left in 0..endpoints.len() {
2159 for right in left + 1..endpoints.len() {
2160 for &boundary in boundaries {
2161 let (left_pose, right_pose) = if boundary == TransitionFamilyBoundaryV1::Entry {
2162 (
2163 endpoints[left].entry.as_deref(),
2164 endpoints[right].entry.as_deref(),
2165 )
2166 } else {
2167 (
2168 endpoints[left].exit.as_deref(),
2169 endpoints[right].exit.as_deref(),
2170 )
2171 };
2172 let (left_pose, right_pose) = match (left_pose, right_pose) {
2173 (Some(left_pose), Some(right_pose)) => (left_pose, right_pose),
2174 _ => return Err(TransitionPoseReasonV1::UnsupportedSampling),
2175 };
2176 output.push(compare_one_pair(
2177 [left, right],
2178 boundary,
2179 left_pose,
2180 right_pose,
2181 tolerances,
2182 skeleton,
2183 ));
2184 }
2185 }
2186 }
2187 Ok(output)
2188}
2189
2190fn compare_one_pair(
2191 member_indices: [usize; 2],
2192 boundary: TransitionFamilyBoundaryV1,
2193 left: &[EndpointPose],
2194 right: &[EndpointPose],
2195 tolerances: TransitionFamilyTolerancesV1,
2196 skeleton: &Skeleton,
2197) -> TransitionPosePairEvaluationV1 {
2198 let mut max_translation_delta_m = 0.0f64;
2199 let mut max_rotation_delta_deg = 0.0f64;
2200 let mut translation_candidates =
2201 Vec::with_capacity(TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS);
2202 let mut rotation_candidates =
2203 Vec::with_capacity(TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS);
2204 for (ordinal, (left, right)) in left.iter().zip(right).enumerate() {
2205 let translation = translation_delta(left.translation, right.translation);
2206 let rotation = rotation_delta_deg(left.rotation, right.rotation);
2207 max_translation_delta_m = max_translation_delta_m.max(translation);
2208 max_rotation_delta_deg = max_rotation_delta_deg.max(rotation);
2209 if translation > tolerances.translation_m() {
2210 retain_top_candidate(
2211 &mut translation_candidates,
2212 ordinal,
2213 translation,
2214 TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS,
2215 );
2216 }
2217 if rotation > tolerances.rotation_deg() {
2218 retain_top_candidate(
2219 &mut rotation_candidates,
2220 ordinal,
2221 rotation,
2222 TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS,
2223 );
2224 }
2225 }
2226 sort_top_candidates(&mut translation_candidates);
2227 sort_top_candidates(&mut rotation_candidates);
2228 let translation_offenders = translation_candidates
2229 .into_iter()
2230 .map(
2231 |(bone_ordinal, delta_m)| TransitionPoseTranslationOffenderV1 {
2232 bone_ordinal,
2233 bone_name: skeleton.bones[bone_ordinal].name.clone(),
2234 delta_m,
2235 },
2236 )
2237 .collect();
2238 let rotation_offenders = rotation_candidates
2239 .into_iter()
2240 .map(
2241 |(bone_ordinal, delta_deg)| TransitionPoseRotationOffenderV1 {
2242 bone_ordinal,
2243 bone_name: skeleton.bones[bone_ordinal].name.clone(),
2244 delta_deg,
2245 },
2246 )
2247 .collect();
2248 TransitionPosePairEvaluationV1 {
2249 member_indices,
2250 boundary,
2251 max_translation_delta_m,
2252 max_rotation_delta_deg,
2253 translation_tolerance_m: tolerances.translation_m(),
2254 rotation_tolerance_deg: tolerances.rotation_deg(),
2255 translation_offenders,
2256 rotation_offenders,
2257 }
2258}
2259
2260fn retain_top_candidate(
2265 candidates: &mut Vec<(usize, f64)>,
2266 ordinal: usize,
2267 delta: f64,
2268 cap: usize,
2269) {
2270 let candidate = (ordinal, delta);
2271 if candidates.len() < cap {
2272 candidates.push(candidate);
2273 return;
2274 }
2275 let worst = candidates
2276 .iter()
2277 .enumerate()
2278 .reduce(|worst, current| {
2279 if candidate_precedes(worst.1, current.1) {
2280 current
2281 } else {
2282 worst
2283 }
2284 })
2285 .map(|(index, _)| index);
2286 if let Some(worst) = worst
2287 && candidate_precedes(&candidate, &candidates[worst])
2288 {
2289 candidates[worst] = candidate;
2290 }
2291}
2292
2293fn sort_top_candidates(candidates: &mut [(usize, f64)]) {
2294 candidates.sort_by(candidate_order);
2295}
2296
2297fn candidate_precedes(left: &(usize, f64), right: &(usize, f64)) -> bool {
2298 candidate_order(left, right).is_lt()
2299}
2300
2301fn candidate_order(left: &(usize, f64), right: &(usize, f64)) -> std::cmp::Ordering {
2302 right
2303 .1
2304 .total_cmp(&left.1)
2305 .then_with(|| left.0.cmp(&right.0))
2306}
2307
2308fn pair_has_finding(pair: &TransitionPosePairEvaluationV1) -> bool {
2309 !pair.translation_offenders.is_empty() || !pair.rotation_offenders.is_empty()
2310}
2311
2312fn derive_result_state(result: &mut TransitionPoseEvaluationV1) {
2313 if result
2314 .families
2315 .iter()
2316 .any(|family| family.status == TransitionPoseStatusV1::Incomplete)
2317 {
2318 result.status = TransitionPoseStatusV1::Incomplete;
2319 result.decision = TransitionPoseDecisionV1::NotEvaluated;
2320 result.reason = None;
2321 } else if result
2322 .families
2323 .iter()
2324 .any(|family| family.decision == TransitionPoseDecisionV1::Finding)
2325 {
2326 result.status = TransitionPoseStatusV1::Complete;
2327 result.decision = TransitionPoseDecisionV1::Finding;
2328 result.reason = None;
2329 } else {
2330 result.status = TransitionPoseStatusV1::Complete;
2331 result.decision = TransitionPoseDecisionV1::Pass;
2332 result.reason = None;
2333 }
2334}
2335
2336fn finite_translation(bone: &Bone, ordinal: usize) -> Result<[f64; 3], SkeletonBasisError> {
2337 finite_vec3(bone.rest.translation).ok_or(SkeletonBasisError::InvalidRest { ordinal })
2338}
2339
2340fn finite_vec3(value: crate::glam::Vec3) -> Option<[f64; 3]> {
2341 if !value.is_finite() {
2342 return None;
2343 }
2344 Some([
2345 canonical_zero(f64::from(value.x)),
2346 canonical_zero(f64::from(value.y)),
2347 canonical_zero(f64::from(value.z)),
2348 ])
2349}
2350
2351fn canonical_quaternion(
2352 value: crate::glam::Quat,
2353 ordinal: usize,
2354) -> Result<[f64; 4], SkeletonBasisError> {
2355 let mut q = [
2356 f64::from(value.x),
2357 f64::from(value.y),
2358 f64::from(value.z),
2359 f64::from(value.w),
2360 ];
2361 if q.iter().any(|value| !value.is_finite()) {
2362 return Err(SkeletonBasisError::InvalidRest { ordinal });
2363 }
2364 let norm_squared = q.iter().map(|value| value * value).sum::<f64>();
2365 if !norm_squared.is_finite() || norm_squared == 0.0 {
2366 return Err(SkeletonBasisError::InvalidRest { ordinal });
2367 }
2368 let norm = norm_squared.sqrt();
2369 if !norm.is_finite() || norm == 0.0 {
2370 return Err(SkeletonBasisError::InvalidRest { ordinal });
2371 }
2372 for component in &mut q {
2373 *component /= norm;
2374 }
2375 if q.iter().any(|value| !value.is_finite()) {
2376 return Err(SkeletonBasisError::InvalidRest { ordinal });
2377 }
2378 if hemisphere_negative(q) {
2379 for component in &mut q {
2380 *component = -*component;
2381 }
2382 }
2383 for component in &mut q {
2384 *component = canonical_zero(*component);
2385 }
2386 Ok(q)
2387}
2388
2389fn hemisphere_negative(q: [f64; 4]) -> bool {
2390 for index in [3usize, 0, 1, 2] {
2391 if q[index] < 0.0 {
2392 return true;
2393 }
2394 if q[index] > 0.0 {
2395 return false;
2396 }
2397 }
2398 false
2399}
2400
2401fn canonical_zero(value: f64) -> f64 {
2402 if value == 0.0 { 0.0 } else { value }
2403}
2404
2405fn translation_delta(left: [f64; 3], right: [f64; 3]) -> f64 {
2406 let dx = left[0] - right[0];
2407 let dy = left[1] - right[1];
2408 let dz = left[2] - right[2];
2409 (dx * dx + dy * dy + dz * dz).sqrt()
2410}
2411
2412fn rotation_delta_deg(left: [f64; 4], right: [f64; 4]) -> f64 {
2413 if left == right
2414 || left
2415 .into_iter()
2416 .zip(right)
2417 .all(|(left, right)| left == -right)
2418 {
2419 return 0.0;
2420 }
2421 let dot = left
2422 .into_iter()
2423 .zip(right)
2424 .map(|(left, right)| left * right)
2425 .sum::<f64>();
2426 let right = if dot < 0.0 {
2427 right.map(|value| -value)
2428 } else {
2429 right
2430 };
2431 let vector = [
2434 left[3] * right[0] - left[0] * right[3] - left[1] * right[2] + left[2] * right[1],
2435 left[3] * right[1] + left[0] * right[2] - left[1] * right[3] - left[2] * right[0],
2436 left[3] * right[2] - left[0] * right[1] + left[1] * right[0] - left[2] * right[3],
2437 ];
2438 let vector_norm = vector
2439 .into_iter()
2440 .map(|value| value * value)
2441 .sum::<f64>()
2442 .sqrt();
2443 let scalar =
2444 (left[3] * right[3] + left[0] * right[0] + left[1] * right[1] + left[2] * right[2]).abs();
2445 (2.0 * vector_norm.atan2(scalar)).to_degrees()
2446}
2447
2448fn canonical_bytes(value: &impl Serialize, limit: usize) -> io::Result<Vec<u8>> {
2449 let mut writer = BoundedWriter {
2450 bytes: Vec::new(),
2451 limit,
2452 overflow: false,
2453 };
2454 serde_jcs::to_writer(&mut writer, value).map_err(io::Error::other)?;
2455 if writer.overflow {
2456 return Err(io::Error::other("bounded JCS result exceeded cap"));
2457 }
2458 Ok(writer.bytes)
2459}
2460
2461struct BoundedWriter {
2462 bytes: Vec<u8>,
2463 limit: usize,
2464 overflow: bool,
2465}
2466impl Write for BoundedWriter {
2467 fn write(&mut self, buffer: &[u8]) -> io::Result<usize> {
2468 if self
2469 .bytes
2470 .len()
2471 .checked_add(buffer.len())
2472 .is_none_or(|length| length > self.limit)
2473 {
2474 self.overflow = true;
2475 return Err(io::Error::other("bounded JCS result exceeded cap"));
2476 }
2477 self.bytes.extend_from_slice(buffer);
2478 Ok(buffer.len())
2479 }
2480 fn flush(&mut self) -> io::Result<()> {
2481 Ok(())
2482 }
2483}
2484
2485#[cfg(test)]
2486mod tests {
2487 use super::*;
2488 use crate::{
2489 Bone, Clip, CollectionIdV1, CollectionLogicalIdV1, CollectionSourceKeyV1,
2490 CollectionTransitionFamilyMemberV1, CollectionTransitionFamilyV1,
2491 DependencyClosureBuilderV1, DependencyResourceKeyV1, Document,
2492 DocumentTransitionFamilyMemberV1, DocumentTransitionFamilyV1, Interpolation,
2493 RawSourceFactsBuilderV1, ResourceKeySyntaxV1, Skeleton, SourceFactDomainV1, SourceFormatV1,
2494 SourceResourceKindV1, SourceSetCoverageV1, SourceUnavailableReasonV1, Track, TrackValues,
2495 Transform, TransitionFamilyDeclarationV1, TransitionFamilyManifestIdentityV1,
2496 };
2497
2498 fn document(second_translation: Option<f32>) -> Document {
2499 let mut second = Clip {
2500 name: "Run".into(),
2501 duration_s: 1.0,
2502 tracks: Vec::new(),
2503 };
2504 if let Some(value) = second_translation {
2505 second.tracks.push(Track {
2506 bone: 0,
2507 property: Property::Translation,
2508 interpolation: Interpolation::Linear,
2509 times: vec![0.0, 1.0],
2510 values: TrackValues::Vec3s(vec![
2511 crate::glam::Vec3::splat(value),
2512 crate::glam::Vec3::splat(value),
2513 ]),
2514 });
2515 }
2516 Document {
2517 skeleton: Skeleton {
2518 bones: vec![Bone {
2519 name: "root".into(),
2520 parent: None,
2521 rest: Transform::IDENTITY,
2522 inverse_bind: None,
2523 }],
2524 },
2525 clips: vec![
2526 Clip {
2527 name: "Walk".into(),
2528 duration_s: 1.0,
2529 tracks: Vec::new(),
2530 },
2531 second,
2532 ],
2533 ..Document::default()
2534 }
2535 }
2536
2537 fn declaration(
2538 boundary: TransitionFamilyBoundaryV1,
2539 translation_m: f64,
2540 time_normalized: f64,
2541 ) -> TransitionFamilyDeclarationInputV1 {
2542 let family = DocumentTransitionFamilyV1::new(
2543 "walk_to_run".into(),
2544 boundary,
2545 TransitionFamilyTolerancesV1::new(translation_m, 180.0, time_normalized).unwrap(),
2546 vec![
2547 DocumentTransitionFamilyMemberV1::new(0, "Walk".into()).unwrap(),
2548 DocumentTransitionFamilyMemberV1::new(1, "Run".into()).unwrap(),
2549 ],
2550 )
2551 .unwrap();
2552 TransitionFamilyDeclarationInputV1::new(
2553 TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
2554 b"declaration",
2555 )
2556 .unwrap()
2557 }
2558
2559 fn complete_closure(primary_input: InputIdentity) -> DependencyClosureV1 {
2560 DependencyClosureBuilderV1::new(primary_input, SourceSetCoverageV1::complete(), 0)
2561 .finish()
2562 .unwrap()
2563 }
2564
2565 fn loaded_source(document: Document, bytes: &[u8]) -> LoadedSource {
2566 let primary = InputIdentity::from_bytes(bytes);
2567 loaded_source_with_closure(document, primary.clone(), complete_closure(primary))
2568 }
2569
2570 fn loaded_source_with_closure(
2571 document: Document,
2572 primary: InputIdentity,
2573 closure: DependencyClosureV1,
2574 ) -> LoadedSource {
2575 let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, primary.clone());
2576 for domain in [
2577 SourceFactDomainV1::Clips,
2578 SourceFactDomainV1::Constructs,
2579 SourceFactDomainV1::Resources,
2580 ] {
2581 facts.mark_complete(domain);
2582 }
2583 facts
2584 .finish_with_dependency_closure(document, closure)
2585 .unwrap()
2586 }
2587
2588 fn collection_declaration(
2589 families: Vec<CollectionTransitionFamilyV1>,
2590 manifest: TransitionFamilyManifestIdentityV1,
2591 ) -> TransitionFamilyDeclarationInputV1 {
2592 TransitionFamilyDeclarationInputV1::new(
2593 TransitionFamilyDeclarationV1::collection(manifest, families).unwrap(),
2594 b"collection",
2595 )
2596 .unwrap()
2597 }
2598
2599 fn collection_family(
2600 id: &str,
2601 members: Vec<CollectionTransitionFamilyMemberV1>,
2602 ) -> CollectionTransitionFamilyV1 {
2603 collection_family_with(id, TransitionFamilyBoundaryV1::Entry, 0.0, members)
2604 }
2605
2606 fn collection_family_with(
2607 id: &str,
2608 boundary: TransitionFamilyBoundaryV1,
2609 time_normalized: f64,
2610 members: Vec<CollectionTransitionFamilyMemberV1>,
2611 ) -> CollectionTransitionFamilyV1 {
2612 CollectionTransitionFamilyV1::new(
2613 CollectionLogicalIdV1::new(id).unwrap(),
2614 boundary,
2615 TransitionFamilyTolerancesV1::new(0.0, 0.0, time_normalized).unwrap(),
2616 members,
2617 )
2618 .unwrap()
2619 }
2620
2621 fn closure_with_external_buffer(
2622 primary_input: InputIdentity,
2623 buffer_bytes: &[u8],
2624 ) -> DependencyClosureV1 {
2625 let key =
2626 DependencyResourceKeyV1::from_source_str("animation.bin", ResourceKeySyntaxV1::GltfUri)
2627 .unwrap();
2628 let mut builder =
2629 DependencyClosureBuilderV1::new(primary_input, SourceSetCoverageV1::complete(), 1);
2630 assert!(builder.begin_reference("animation.bin".len(), 1));
2631 assert_eq!(builder.prepare_external_key(&key).unwrap(), Some(true));
2632 builder.record_external_open_attempt(&key).unwrap();
2633 assert!(
2634 builder
2635 .push_captured_external(
2636 0,
2637 SourceResourceKindV1::Buffer,
2638 0,
2639 key,
2640 InputIdentity::from_bytes(buffer_bytes),
2641 )
2642 .unwrap()
2643 );
2644 builder.finish().unwrap()
2645 }
2646
2647 fn evaluate_document_transition_poses_v1(
2648 declaration: &TransitionFamilyDeclarationInputV1,
2649 subject_input: InputIdentity,
2650 document: &Document,
2651 ) -> Result<TransitionPoseEvaluationV1, TransitionPoseEvaluationControlError> {
2652 let closure = complete_closure(subject_input);
2653 super::evaluate_document_transition_poses_v1(declaration, &closure, document)
2654 }
2655
2656 #[test]
2657 fn collection_adapter_binds_manifest_and_routes_runtime_take_drift() {
2658 let collection_id = CollectionIdV1::new("collection").unwrap();
2659 let manifest = TransitionFamilyManifestIdentityV1::new(
2660 collection_id,
2661 InputIdentity::from_bytes(b"manifest"),
2662 )
2663 .unwrap();
2664 let logical_a = CollectionLogicalIdV1::new("collection/walk").unwrap();
2665 let logical_b = CollectionLogicalIdV1::new("collection/run").unwrap();
2666 let source_a = CollectionSourceKeyV1::new("walk").unwrap();
2667 let source_b = CollectionSourceKeyV1::new("run").unwrap();
2668 let family = collection_family(
2669 "collection/family",
2670 vec![
2671 CollectionTransitionFamilyMemberV1::new(
2672 logical_a.clone(),
2673 source_a.clone(),
2674 0,
2675 "Walk".into(),
2676 )
2677 .unwrap(),
2678 CollectionTransitionFamilyMemberV1::new(
2679 logical_b.clone(),
2680 source_b.clone(),
2681 1,
2682 "Run".into(),
2683 )
2684 .unwrap(),
2685 ],
2686 );
2687 let declaration = collection_declaration(vec![family], manifest.clone());
2688 let left = loaded_source(document(None), b"left");
2689 let right = loaded_source(document(None), b"right");
2690 let members = [
2691 CollectionTransitionPoseMemberInputV1::available(
2692 &logical_a, &source_a, 0, "Walk", &left,
2693 ),
2694 CollectionTransitionPoseMemberInputV1::available(
2695 &logical_b, &source_b, 1, "Run", &right,
2696 ),
2697 ];
2698 assert_eq!(
2699 evaluate_collection_transition_poses_v1(
2700 &declaration,
2701 &TransitionFamilyManifestIdentityV1::new(
2702 CollectionIdV1::new("other").unwrap(),
2703 InputIdentity::from_bytes(b"manifest"),
2704 )
2705 .unwrap(),
2706 &members,
2707 ),
2708 Err(TransitionPoseEvaluationControlError::InvalidCollectionManifestBinding)
2709 );
2710 assert_eq!(
2711 evaluate_collection_transition_poses_v1(&declaration, &manifest, &members)
2712 .unwrap()
2713 .decision(),
2714 TransitionPoseDecisionV1::Pass
2715 );
2716 let mut drift = document(None);
2717 drift.clips[1].name = "Renamed".into();
2718 let drift = loaded_source(drift, b"drift");
2719 let drift_members = [
2720 CollectionTransitionPoseMemberInputV1::available(
2721 &logical_a, &source_a, 0, "Walk", &left,
2722 ),
2723 CollectionTransitionPoseMemberInputV1::available(
2724 &logical_b, &source_b, 1, "Run", &drift,
2725 ),
2726 ];
2727 let result =
2728 evaluate_collection_transition_poses_v1(&declaration, &manifest, &drift_members)
2729 .unwrap();
2730 assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
2731 assert_eq!(
2732 result.families()[0].reason(),
2733 Some(TransitionPoseReasonV1::MemberUnavailable)
2734 );
2735 assert!(result.families()[0].members()[1].source_input().is_some());
2736 assert!(result.families()[0].pairs().is_empty());
2737 }
2738
2739 #[test]
2740 fn collection_control_rejects_length_order_staleness_and_split_source_authority() {
2741 let manifest = TransitionFamilyManifestIdentityV1::new(
2742 CollectionIdV1::new("collection").unwrap(),
2743 InputIdentity::from_bytes(b"manifest"),
2744 )
2745 .unwrap();
2746 let logical_a = CollectionLogicalIdV1::new("collection/a").unwrap();
2747 let logical_b = CollectionLogicalIdV1::new("collection/b").unwrap();
2748 let source = CollectionSourceKeyV1::new("shared").unwrap();
2749 let declaration = collection_declaration(
2750 vec![collection_family(
2751 "collection/family",
2752 vec![
2753 CollectionTransitionFamilyMemberV1::new(
2754 logical_a.clone(),
2755 source.clone(),
2756 0,
2757 "Walk".into(),
2758 )
2759 .unwrap(),
2760 CollectionTransitionFamilyMemberV1::new(
2761 logical_b.clone(),
2762 source.clone(),
2763 1,
2764 "Run".into(),
2765 )
2766 .unwrap(),
2767 ],
2768 )],
2769 manifest.clone(),
2770 );
2771 let first = loaded_source(document(None), b"same");
2772 let second = loaded_source(document(None), b"same");
2773 let valid = [
2774 CollectionTransitionPoseMemberInputV1::available(
2775 &logical_a, &source, 0, "Walk", &first,
2776 ),
2777 CollectionTransitionPoseMemberInputV1::available(&logical_b, &source, 1, "Run", &first),
2778 ];
2779 assert_eq!(
2780 evaluate_collection_transition_poses_v1(&declaration, &manifest, &valid[..1]),
2781 Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2782 );
2783 assert_eq!(
2784 evaluate_collection_transition_poses_v1(&declaration, &manifest, &[valid[1], valid[0]],),
2785 Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2786 );
2787 let stale = [
2788 CollectionTransitionPoseMemberInputV1::available(
2789 &logical_a, &source, 0, "Walk", &first,
2790 ),
2791 CollectionTransitionPoseMemberInputV1::available(&logical_b, &source, 0, "Run", &first),
2792 ];
2793 assert_eq!(
2794 evaluate_collection_transition_poses_v1(&declaration, &manifest, &stale),
2795 Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2796 );
2797 let split_loaded = [
2798 CollectionTransitionPoseMemberInputV1::available(
2799 &logical_a, &source, 0, "Walk", &first,
2800 ),
2801 CollectionTransitionPoseMemberInputV1::available(
2802 &logical_b, &source, 1, "Run", &second,
2803 ),
2804 ];
2805 assert_eq!(
2806 evaluate_collection_transition_poses_v1(&declaration, &manifest, &split_loaded),
2807 Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2808 );
2809 let split_state = [
2810 CollectionTransitionPoseMemberInputV1::available(
2811 &logical_a, &source, 0, "Walk", &first,
2812 ),
2813 CollectionTransitionPoseMemberInputV1::unavailable(&logical_b, &source, 1, "Run"),
2814 ];
2815 assert_eq!(
2816 evaluate_collection_transition_poses_v1(&declaration, &manifest, &split_state),
2817 Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2818 );
2819 let unavailable_left = InputIdentity::from_bytes(b"missing-left");
2820 let unavailable_right = InputIdentity::from_bytes(b"missing-right");
2821 let split_unavailable_identity = [
2822 CollectionTransitionPoseMemberInputV1::unavailable_with_source_input(
2823 &logical_a,
2824 &source,
2825 0,
2826 "Walk",
2827 &unavailable_left,
2828 ),
2829 CollectionTransitionPoseMemberInputV1::unavailable_with_source_input(
2830 &logical_b,
2831 &source,
2832 1,
2833 "Run",
2834 &unavailable_right,
2835 ),
2836 ];
2837 assert_eq!(
2838 evaluate_collection_transition_poses_v1(
2839 &declaration,
2840 &manifest,
2841 &split_unavailable_identity,
2842 ),
2843 Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2844 );
2845 }
2846
2847 #[test]
2848 fn collection_control_rejects_false_incomplete_closure_state() {
2849 let manifest = TransitionFamilyManifestIdentityV1::new(
2850 CollectionIdV1::new("collection").unwrap(),
2851 InputIdentity::from_bytes(b"manifest"),
2852 )
2853 .unwrap();
2854 let logical = [
2855 CollectionLogicalIdV1::new("collection/a").unwrap(),
2856 CollectionLogicalIdV1::new("collection/b").unwrap(),
2857 ];
2858 let sources = [
2859 CollectionSourceKeyV1::new("a").unwrap(),
2860 CollectionSourceKeyV1::new("b").unwrap(),
2861 ];
2862 let declaration = collection_declaration(
2863 vec![collection_family(
2864 "collection/family",
2865 logical
2866 .iter()
2867 .zip(&sources)
2868 .map(|(logical, source)| {
2869 CollectionTransitionFamilyMemberV1::new(
2870 logical.clone(),
2871 source.clone(),
2872 0,
2873 "Walk".into(),
2874 )
2875 .unwrap()
2876 })
2877 .collect(),
2878 )],
2879 manifest.clone(),
2880 );
2881 let complete = loaded_source(document(None), b"complete");
2882 let members = [
2883 CollectionTransitionPoseMemberInputV1::available(
2884 &logical[0],
2885 &sources[0],
2886 0,
2887 "Walk",
2888 &complete,
2889 ),
2890 CollectionTransitionPoseMemberInputV1::dependency_closure_incomplete(
2891 &logical[1],
2892 &sources[1],
2893 0,
2894 "Walk",
2895 &complete,
2896 ),
2897 ];
2898
2899 assert_eq!(
2900 evaluate_collection_transition_poses_v1(&declaration, &manifest, &members),
2901 Err(TransitionPoseEvaluationControlError::InvalidCollectionMemberWitness)
2902 );
2903 }
2904
2905 #[test]
2906 fn collection_available_partial_closure_normalizes_to_whole_family_gap() {
2907 let manifest = TransitionFamilyManifestIdentityV1::new(
2908 CollectionIdV1::new("collection").unwrap(),
2909 InputIdentity::from_bytes(b"manifest"),
2910 )
2911 .unwrap();
2912 let logical = [
2913 CollectionLogicalIdV1::new("collection/a").unwrap(),
2914 CollectionLogicalIdV1::new("collection/b").unwrap(),
2915 ];
2916 let source = CollectionSourceKeyV1::new("shared").unwrap();
2917 let declaration = collection_declaration(
2918 vec![collection_family(
2919 "collection/family",
2920 vec![
2921 CollectionTransitionFamilyMemberV1::new(
2922 logical[0].clone(),
2923 source.clone(),
2924 0,
2925 "Walk".into(),
2926 )
2927 .unwrap(),
2928 CollectionTransitionFamilyMemberV1::new(
2929 logical[1].clone(),
2930 source.clone(),
2931 1,
2932 "Run".into(),
2933 )
2934 .unwrap(),
2935 ],
2936 )],
2937 manifest.clone(),
2938 );
2939 let primary = InputIdentity::from_bytes(b"partial");
2940 let closure = DependencyClosureBuilderV1::new(
2941 primary.clone(),
2942 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded),
2943 0,
2944 )
2945 .finish()
2946 .unwrap();
2947 let mut facts = RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, primary.clone());
2948 facts.mark_complete(SourceFactDomainV1::Clips);
2949 facts.mark_complete(SourceFactDomainV1::Constructs);
2950 facts.mark_partial(
2951 SourceFactDomainV1::Resources,
2952 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
2953 );
2954 let partial = facts
2955 .finish_with_dependency_closure(document(None), closure)
2956 .unwrap();
2957 let members = [
2958 CollectionTransitionPoseMemberInputV1::available(
2959 &logical[0],
2960 &source,
2961 0,
2962 "Walk",
2963 &partial,
2964 ),
2965 CollectionTransitionPoseMemberInputV1::available(
2966 &logical[1],
2967 &source,
2968 1,
2969 "Run",
2970 &partial,
2971 ),
2972 ];
2973
2974 let result =
2975 evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
2976 let family = &result.families()[0];
2977 assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
2978 assert_eq!(result.decision(), TransitionPoseDecisionV1::NotEvaluated);
2979 assert_eq!(
2980 family.reason(),
2981 Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
2982 );
2983 assert!(family.skeleton_basis_input().is_none());
2984 assert!(family.pairs().is_empty());
2985 assert!(family.members().iter().all(|member| {
2986 member.source_input() == Some(&primary)
2987 && member.source_dependency_closure_identity().is_none()
2988 }));
2989 }
2990
2991 #[test]
2992 fn collection_runtime_gaps_preserve_priority_evidence_and_never_form_a_subset() {
2993 let manifest = TransitionFamilyManifestIdentityV1::new(
2994 CollectionIdV1::new("collection").unwrap(),
2995 InputIdentity::from_bytes(b"manifest"),
2996 )
2997 .unwrap();
2998 let logical = [
2999 CollectionLogicalIdV1::new("collection/a").unwrap(),
3000 CollectionLogicalIdV1::new("collection/b").unwrap(),
3001 CollectionLogicalIdV1::new("collection/c").unwrap(),
3002 ];
3003 let sources = [
3004 CollectionSourceKeyV1::new("a").unwrap(),
3005 CollectionSourceKeyV1::new("b").unwrap(),
3006 CollectionSourceKeyV1::new("c").unwrap(),
3007 ];
3008 let family = collection_family(
3009 "collection/family",
3010 logical
3011 .iter()
3012 .zip(&sources)
3013 .map(|(logical, source)| {
3014 CollectionTransitionFamilyMemberV1::new(
3015 logical.clone(),
3016 source.clone(),
3017 0,
3018 "Walk".into(),
3019 )
3020 .unwrap()
3021 })
3022 .collect(),
3023 );
3024 let declaration = collection_declaration(vec![family], manifest.clone());
3025 let available = loaded_source(document(None), b"available");
3026 let partial_primary = InputIdentity::from_bytes(b"partial");
3027 let partial_closure = DependencyClosureBuilderV1::new(
3028 partial_primary.clone(),
3029 SourceSetCoverageV1::partial(SourceUnavailableReasonV1::ProjectionBudgetExceeded),
3030 0,
3031 )
3032 .finish()
3033 .unwrap();
3034 let mut partial_facts =
3035 RawSourceFactsBuilderV1::new(SourceFormatV1::Glb, partial_primary.clone());
3036 partial_facts.mark_complete(SourceFactDomainV1::Clips);
3037 partial_facts.mark_complete(SourceFactDomainV1::Constructs);
3038 partial_facts.mark_partial(
3039 SourceFactDomainV1::Resources,
3040 SourceUnavailableReasonV1::ProjectionBudgetExceeded,
3041 );
3042 let partial = partial_facts
3043 .finish_with_dependency_closure(document(None), partial_closure)
3044 .unwrap();
3045 let missing_primary = InputIdentity::from_bytes(b"missing");
3046 let members = [
3047 CollectionTransitionPoseMemberInputV1::available(
3048 &logical[0],
3049 &sources[0],
3050 0,
3051 "Walk",
3052 &available,
3053 ),
3054 CollectionTransitionPoseMemberInputV1::dependency_closure_incomplete(
3055 &logical[1],
3056 &sources[1],
3057 0,
3058 "Walk",
3059 &partial,
3060 ),
3061 CollectionTransitionPoseMemberInputV1::unavailable_with_source_input(
3062 &logical[2],
3063 &sources[2],
3064 0,
3065 "Walk",
3066 &missing_primary,
3067 ),
3068 ];
3069 let result =
3070 evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
3071 let row = &result.families()[0];
3072 assert_eq!(
3073 row.reason(),
3074 Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
3075 );
3076 assert!(row.pairs().is_empty());
3077 assert!(row.skeleton_basis_input().is_none());
3078 assert_eq!(row.members()[1].source_input(), Some(&partial_primary));
3079 assert!(
3080 row.members()[1]
3081 .source_dependency_closure_identity()
3082 .is_none()
3083 );
3084 assert_eq!(row.members()[2].source_input(), Some(&missing_primary));
3085 }
3086
3087 #[test]
3088 fn collection_invalid_and_mismatched_skeletons_are_whole_family_gaps() {
3089 let manifest = TransitionFamilyManifestIdentityV1::new(
3090 CollectionIdV1::new("collection").unwrap(),
3091 InputIdentity::from_bytes(b"manifest"),
3092 )
3093 .unwrap();
3094 let logical = [
3095 CollectionLogicalIdV1::new("collection/a").unwrap(),
3096 CollectionLogicalIdV1::new("collection/b").unwrap(),
3097 CollectionLogicalIdV1::new("collection/c").unwrap(),
3098 ];
3099 let sources = [
3100 CollectionSourceKeyV1::new("a").unwrap(),
3101 CollectionSourceKeyV1::new("b").unwrap(),
3102 CollectionSourceKeyV1::new("c").unwrap(),
3103 ];
3104 let declaration = collection_declaration(
3105 vec![collection_family(
3106 "collection/family",
3107 logical
3108 .iter()
3109 .zip(&sources)
3110 .map(|(logical, source)| {
3111 CollectionTransitionFamilyMemberV1::new(
3112 logical.clone(),
3113 source.clone(),
3114 0,
3115 "Walk".into(),
3116 )
3117 .unwrap()
3118 })
3119 .collect(),
3120 )],
3121 manifest.clone(),
3122 );
3123 let same = loaded_source(document(None), b"same");
3124 let mut different_document = document(None);
3125 different_document.skeleton.bones[0].name = "different".into();
3126 let different = loaded_source(different_document, b"different");
3127 let mismatch = [
3128 CollectionTransitionPoseMemberInputV1::available(
3129 &logical[0],
3130 &sources[0],
3131 0,
3132 "Walk",
3133 &same,
3134 ),
3135 CollectionTransitionPoseMemberInputV1::available(
3136 &logical[1],
3137 &sources[1],
3138 0,
3139 "Walk",
3140 &same,
3141 ),
3142 CollectionTransitionPoseMemberInputV1::available(
3143 &logical[2],
3144 &sources[2],
3145 0,
3146 "Walk",
3147 &different,
3148 ),
3149 ];
3150 let result =
3151 evaluate_collection_transition_poses_v1(&declaration, &manifest, &mismatch).unwrap();
3152 assert_eq!(
3153 result.families()[0].reason(),
3154 Some(TransitionPoseReasonV1::SkeletonBasisMismatch)
3155 );
3156 assert!(result.families()[0].pairs().is_empty());
3157
3158 let mut invalid_document = document(None);
3159 invalid_document.skeleton.bones[0].rest.rotation =
3160 crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
3161 let invalid = loaded_source(invalid_document, b"invalid");
3162 let invalid_members = [
3163 CollectionTransitionPoseMemberInputV1::available(
3164 &logical[0],
3165 &sources[0],
3166 0,
3167 "Walk",
3168 &invalid,
3169 ),
3170 CollectionTransitionPoseMemberInputV1::available(
3171 &logical[1],
3172 &sources[1],
3173 0,
3174 "Walk",
3175 &same,
3176 ),
3177 CollectionTransitionPoseMemberInputV1::available(
3178 &logical[2],
3179 &sources[2],
3180 0,
3181 "Walk",
3182 &different,
3183 ),
3184 ];
3185 let invalid_result =
3186 evaluate_collection_transition_poses_v1(&declaration, &manifest, &invalid_members)
3187 .unwrap();
3188 assert_eq!(
3189 invalid_result.families()[0].reason(),
3190 Some(TransitionPoseReasonV1::MemberUnavailable)
3191 );
3192 assert!(
3193 invalid_result.families()[0]
3194 .skeleton_basis_input()
3195 .is_none()
3196 );
3197 assert!(invalid_result.families()[0].pairs().is_empty());
3198 }
3199
3200 #[test]
3201 fn collection_repeated_families_cache_basis_and_track_admission_by_source_take() {
3202 let manifest = TransitionFamilyManifestIdentityV1::new(
3203 CollectionIdV1::new("collection").unwrap(),
3204 InputIdentity::from_bytes(b"manifest"),
3205 )
3206 .unwrap();
3207 let source = CollectionSourceKeyV1::new("shared").unwrap();
3208 let logical = [
3209 CollectionLogicalIdV1::new("collection/walk").unwrap(),
3210 CollectionLogicalIdV1::new("collection/run").unwrap(),
3211 ];
3212 let family_members = || {
3213 vec![
3214 CollectionTransitionFamilyMemberV1::new(
3215 logical[0].clone(),
3216 source.clone(),
3217 0,
3218 "Walk".into(),
3219 )
3220 .unwrap(),
3221 CollectionTransitionFamilyMemberV1::new(
3222 logical[1].clone(),
3223 source.clone(),
3224 1,
3225 "Run".into(),
3226 )
3227 .unwrap(),
3228 ]
3229 };
3230 let declaration = collection_declaration(
3231 vec![
3232 collection_family("collection/first", family_members()),
3233 collection_family("collection/second", family_members()),
3234 ],
3235 manifest.clone(),
3236 );
3237 let shared = loaded_source(document(Some(0.0)), b"shared");
3238 let members = [
3239 CollectionTransitionPoseMemberInputV1::available(
3240 &logical[0],
3241 &source,
3242 0,
3243 "Walk",
3244 &shared,
3245 ),
3246 CollectionTransitionPoseMemberInputV1::available(
3247 &logical[1],
3248 &source,
3249 1,
3250 "Run",
3251 &shared,
3252 ),
3253 CollectionTransitionPoseMemberInputV1::available(
3254 &logical[0],
3255 &source,
3256 0,
3257 "Walk",
3258 &shared,
3259 ),
3260 CollectionTransitionPoseMemberInputV1::available(
3261 &logical[1],
3262 &source,
3263 1,
3264 "Run",
3265 &shared,
3266 ),
3267 ];
3268 let mut basis_builds = 0usize;
3269 let mut track_visits = 0usize;
3270 let result = evaluate_collection_transition_poses_v1_with_probes(
3271 &declaration,
3272 &manifest,
3273 &members,
3274 |_| basis_builds += 1,
3275 |_| track_visits += 1,
3276 )
3277 .unwrap();
3278 assert_eq!(basis_builds, 1, "one source key builds one basis");
3279 assert_eq!(track_visits, 1, "one authored track is admitted once");
3280 assert!(
3281 result
3282 .families()
3283 .iter()
3284 .all(|family| family.status() == TransitionPoseStatusV1::Complete)
3285 );
3286 }
3287
3288 #[test]
3289 fn collection_distinct_family_bases_remain_independently_valid() {
3290 let manifest = TransitionFamilyManifestIdentityV1::new(
3291 CollectionIdV1::new("collection").unwrap(),
3292 InputIdentity::from_bytes(b"manifest"),
3293 )
3294 .unwrap();
3295 let logical = (0..4)
3296 .map(|index| CollectionLogicalIdV1::new(format!("collection/m{index}")).unwrap())
3297 .collect::<Vec<_>>();
3298 let sources = (0..4)
3299 .map(|index| CollectionSourceKeyV1::new(format!("s{index}")).unwrap())
3300 .collect::<Vec<_>>();
3301 let family = |id: &str, range: std::ops::Range<usize>| {
3302 collection_family(
3303 id,
3304 range
3305 .map(|index| {
3306 CollectionTransitionFamilyMemberV1::new(
3307 logical[index].clone(),
3308 sources[index].clone(),
3309 0,
3310 "Walk".into(),
3311 )
3312 .unwrap()
3313 })
3314 .collect(),
3315 )
3316 };
3317 let declaration = collection_declaration(
3318 vec![
3319 family("collection/first", 0..2),
3320 family("collection/second", 2..4),
3321 ],
3322 manifest.clone(),
3323 );
3324 let first = loaded_source(document(None), b"first");
3325 let mut second_document = document(None);
3326 second_document.skeleton.bones[0].name = "second-root".into();
3327 let second = loaded_source(second_document, b"second");
3328 let members = [
3329 CollectionTransitionPoseMemberInputV1::available(
3330 &logical[0],
3331 &sources[0],
3332 0,
3333 "Walk",
3334 &first,
3335 ),
3336 CollectionTransitionPoseMemberInputV1::available(
3337 &logical[1],
3338 &sources[1],
3339 0,
3340 "Walk",
3341 &first,
3342 ),
3343 CollectionTransitionPoseMemberInputV1::available(
3344 &logical[2],
3345 &sources[2],
3346 0,
3347 "Walk",
3348 &second,
3349 ),
3350 CollectionTransitionPoseMemberInputV1::available(
3351 &logical[3],
3352 &sources[3],
3353 0,
3354 "Walk",
3355 &second,
3356 ),
3357 ];
3358 let result =
3359 evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
3360 assert_eq!(result.status(), TransitionPoseStatusV1::Complete);
3361 assert_ne!(
3362 result.families()[0].skeleton_basis_input(),
3363 result.families()[1].skeleton_basis_input()
3364 );
3365 }
3366
3367 #[test]
3368 fn collection_policy_and_pair_work_refusals_do_not_visit_skeletons_or_tracks() {
3369 let manifest = TransitionFamilyManifestIdentityV1::new(
3370 CollectionIdV1::new("collection").unwrap(),
3371 InputIdentity::from_bytes(b"manifest"),
3372 )
3373 .unwrap();
3374 let source = CollectionSourceKeyV1::new("shared").unwrap();
3375 let policy_logical = [
3376 CollectionLogicalIdV1::new("collection/policy-a").unwrap(),
3377 CollectionLogicalIdV1::new("collection/policy-b").unwrap(),
3378 ];
3379 let policy_family = collection_family_with(
3380 "collection/policy",
3381 TransitionFamilyBoundaryV1::Entry,
3382 0.1,
3383 policy_logical
3384 .iter()
3385 .map(|logical| {
3386 CollectionTransitionFamilyMemberV1::new(
3387 logical.clone(),
3388 source.clone(),
3389 0,
3390 "Walk".into(),
3391 )
3392 .unwrap()
3393 })
3394 .collect(),
3395 );
3396 let work_logical = (0..92)
3397 .map(|index| CollectionLogicalIdV1::new(format!("collection/work-{index}")).unwrap())
3398 .collect::<Vec<_>>();
3399 let work_family = collection_family(
3400 "collection/work",
3401 work_logical
3402 .iter()
3403 .map(|logical| {
3404 CollectionTransitionFamilyMemberV1::new(
3405 logical.clone(),
3406 source.clone(),
3407 0,
3408 "Walk".into(),
3409 )
3410 .unwrap()
3411 })
3412 .collect(),
3413 );
3414 let declaration =
3415 collection_declaration(vec![policy_family, work_family], manifest.clone());
3416 let mut poisoned = document(None);
3417 poisoned.skeleton.bones[0].rest.rotation = crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
3418 poisoned.clips[0].tracks.push(Track {
3419 bone: usize::MAX,
3420 property: Property::Translation,
3421 interpolation: Interpolation::Linear,
3422 times: vec![],
3423 values: TrackValues::Vec3s(vec![]),
3424 });
3425 let shared = loaded_source(poisoned, b"shared");
3426 let mut members = policy_logical
3427 .iter()
3428 .map(|logical| {
3429 CollectionTransitionPoseMemberInputV1::available(
3430 logical, &source, 0, "Walk", &shared,
3431 )
3432 })
3433 .collect::<Vec<_>>();
3434 members.extend(work_logical.iter().map(|logical| {
3435 CollectionTransitionPoseMemberInputV1::available(logical, &source, 0, "Walk", &shared)
3436 }));
3437 let mut basis_builds = 0usize;
3438 let mut track_visits = 0usize;
3439 let result = evaluate_collection_transition_poses_v1_with_probes(
3440 &declaration,
3441 &manifest,
3442 &members,
3443 |_| basis_builds += 1,
3444 |_| track_visits += 1,
3445 )
3446 .unwrap();
3447 assert_eq!(basis_builds, 0);
3448 assert_eq!(track_visits, 0);
3449 assert_eq!(
3450 result.families()[0].reason(),
3451 Some(TransitionPoseReasonV1::TimeToleranceUnsupported)
3452 );
3453 assert_eq!(
3454 result.families()[1].reason(),
3455 Some(TransitionPoseReasonV1::FamilyWorkLimit)
3456 );
3457 }
3458
3459 #[test]
3460 fn collection_raw_and_selected_track_caps_precede_endpoint_sampling() {
3461 let manifest = TransitionFamilyManifestIdentityV1::new(
3462 CollectionIdV1::new("collection").unwrap(),
3463 InputIdentity::from_bytes(b"manifest"),
3464 )
3465 .unwrap();
3466 let logical = [
3467 CollectionLogicalIdV1::new("collection/walk").unwrap(),
3468 CollectionLogicalIdV1::new("collection/run").unwrap(),
3469 ];
3470 let source = CollectionSourceKeyV1::new("shared").unwrap();
3471 let declaration = collection_declaration(
3472 vec![collection_family(
3473 "collection/family",
3474 vec![
3475 CollectionTransitionFamilyMemberV1::new(
3476 logical[0].clone(),
3477 source.clone(),
3478 0,
3479 "Walk".into(),
3480 )
3481 .unwrap(),
3482 CollectionTransitionFamilyMemberV1::new(
3483 logical[1].clone(),
3484 source.clone(),
3485 1,
3486 "Run".into(),
3487 )
3488 .unwrap(),
3489 ],
3490 )],
3491 manifest.clone(),
3492 );
3493 let scale = Track {
3494 bone: 0,
3495 property: Property::Scale,
3496 interpolation: Interpolation::Linear,
3497 times: vec![],
3498 values: TrackValues::Vec3s(vec![]),
3499 };
3500 let mut raw_document = document(None);
3501 raw_document.clips[1].tracks = vec![scale; 4];
3502 let raw = loaded_source(raw_document, b"raw");
3503 let raw_members = [
3504 CollectionTransitionPoseMemberInputV1::available(&logical[0], &source, 0, "Walk", &raw),
3505 CollectionTransitionPoseMemberInputV1::available(&logical[1], &source, 1, "Run", &raw),
3506 ];
3507 let mut raw_visits = 0usize;
3508 let raw_result = evaluate_collection_transition_poses_v1_with_probes(
3509 &declaration,
3510 &manifest,
3511 &raw_members,
3512 |_| {},
3513 |_| raw_visits += 1,
3514 )
3515 .unwrap();
3516 assert_eq!(raw_visits, 0, "raw length rejects without a track walk");
3517 assert_eq!(
3518 raw_result.families()[0].reason(),
3519 Some(TransitionPoseReasonV1::InputLimit)
3520 );
3521
3522 let selected_track = Track {
3523 bone: 0,
3524 property: Property::Translation,
3525 interpolation: Interpolation::Linear,
3526 times: vec![0.0],
3527 values: TrackValues::Vec3s(vec![crate::glam::Vec3::ZERO]),
3528 };
3529 let mut selected_document = document(None);
3530 selected_document.clips[1].tracks = vec![selected_track; 3];
3531 let selected = loaded_source(selected_document, b"selected");
3532 let selected_members = [
3533 CollectionTransitionPoseMemberInputV1::available(
3534 &logical[0],
3535 &source,
3536 0,
3537 "Walk",
3538 &selected,
3539 ),
3540 CollectionTransitionPoseMemberInputV1::available(
3541 &logical[1],
3542 &source,
3543 1,
3544 "Run",
3545 &selected,
3546 ),
3547 ];
3548 let mut selected_visits = 0usize;
3549 let selected_result = evaluate_collection_transition_poses_v1_with_probes(
3550 &declaration,
3551 &manifest,
3552 &selected_members,
3553 |_| {},
3554 |_| selected_visits += 1,
3555 )
3556 .unwrap();
3557 assert_eq!(selected_visits, 3);
3558 assert_eq!(
3559 selected_result.families()[0].reason(),
3560 Some(TransitionPoseReasonV1::InputLimit)
3561 );
3562 }
3563
3564 #[test]
3565 fn collection_aggregate_work_and_retention_plans_precede_rejected_family_traversal() {
3566 let manifest = TransitionFamilyManifestIdentityV1::new(
3567 CollectionIdV1::new("collection").unwrap(),
3568 InputIdentity::from_bytes(b"manifest"),
3569 )
3570 .unwrap();
3571 let source = CollectionSourceKeyV1::new("shared").unwrap();
3572 let logical = (0..64)
3573 .map(|index| CollectionLogicalIdV1::new(format!("collection/m{index}")).unwrap())
3574 .collect::<Vec<_>>();
3575 let family_members = || {
3576 logical
3577 .iter()
3578 .map(|logical| {
3579 CollectionTransitionFamilyMemberV1::new(
3580 logical.clone(),
3581 source.clone(),
3582 0,
3583 "Walk".into(),
3584 )
3585 .unwrap()
3586 })
3587 .collect()
3588 };
3589
3590 let aggregate_declaration = collection_declaration(
3591 (0..17)
3592 .map(|index| {
3593 collection_family_with(
3594 &format!("collection/a{index}"),
3595 TransitionFamilyBoundaryV1::Both,
3596 0.0,
3597 family_members(),
3598 )
3599 })
3600 .collect(),
3601 manifest.clone(),
3602 );
3603 let mut aggregate_document = document(None);
3604 aggregate_document.skeleton.bones.clear();
3605 let aggregate_source = loaded_source(aggregate_document, b"aggregate");
3606 let aggregate_members = (0..17)
3607 .flat_map(|_| {
3608 logical.iter().map(|logical| {
3609 CollectionTransitionPoseMemberInputV1::available(
3610 logical,
3611 &source,
3612 0,
3613 "Walk",
3614 &aggregate_source,
3615 )
3616 })
3617 })
3618 .collect::<Vec<_>>();
3619 let mut basis_builds = 0usize;
3620 let aggregate = evaluate_collection_transition_poses_v1_with_probes(
3621 &aggregate_declaration,
3622 &manifest,
3623 &aggregate_members,
3624 |_| basis_builds += 1,
3625 |_| panic!("aggregate-rejected families must not visit tracks"),
3626 )
3627 .unwrap();
3628 assert_eq!(
3629 basis_builds, 1,
3630 "only eligible source authority is built once"
3631 );
3632 assert_eq!(
3633 aggregate.families()[16].reason(),
3634 Some(TransitionPoseReasonV1::AggregateWorkLimit)
3635 );
3636
3637 let retention_declaration = collection_declaration(
3638 vec![
3639 collection_family("collection/r1", family_members()),
3640 collection_family("collection/r2", family_members()),
3641 ],
3642 manifest.clone(),
3643 );
3644 let mut retention_document = document(None);
3645 retention_document.skeleton.bones = (0..16)
3646 .map(|index| Bone {
3647 name: format!("b{index}"),
3648 parent: None,
3649 rest: Transform::IDENTITY,
3650 inverse_bind: None,
3651 })
3652 .collect();
3653 let retention_source = loaded_source(retention_document, b"retention");
3654 let retention_members = (0..2)
3655 .flat_map(|_| {
3656 logical.iter().map(|logical| {
3657 CollectionTransitionPoseMemberInputV1::available(
3658 logical,
3659 &source,
3660 0,
3661 "Walk",
3662 &retention_source,
3663 )
3664 })
3665 })
3666 .collect::<Vec<_>>();
3667 let retention = evaluate_collection_transition_poses_v1(
3668 &retention_declaration,
3669 &manifest,
3670 &retention_members,
3671 )
3672 .unwrap();
3673 assert_eq!(
3674 retention.families()[1].reason(),
3675 Some(TransitionPoseReasonV1::RetentionLimit)
3676 );
3677 }
3678
3679 #[test]
3680 fn collection_result_budget_refusal_retains_basis_and_nullable_member_authority() {
3681 let manifest = TransitionFamilyManifestIdentityV1::new(
3682 CollectionIdV1::new("collection").unwrap(),
3683 InputIdentity::from_bytes(b"manifest"),
3684 )
3685 .unwrap();
3686 let source = CollectionSourceKeyV1::new("large").unwrap();
3687 let large_logical = (0..16)
3688 .map(|index| CollectionLogicalIdV1::new(format!("collection/large-{index}")).unwrap())
3689 .collect::<Vec<_>>();
3690 let missing_logical = [
3691 CollectionLogicalIdV1::new("collection/missing-a").unwrap(),
3692 CollectionLogicalIdV1::new("collection/missing-b").unwrap(),
3693 ];
3694 let missing_sources = [
3695 CollectionSourceKeyV1::new("missing-a").unwrap(),
3696 CollectionSourceKeyV1::new("missing-b").unwrap(),
3697 ];
3698 let declaration = collection_declaration(
3699 vec![
3700 collection_family(
3701 "collection/large",
3702 large_logical
3703 .iter()
3704 .map(|logical| {
3705 CollectionTransitionFamilyMemberV1::new(
3706 logical.clone(),
3707 source.clone(),
3708 0,
3709 "Walk".into(),
3710 )
3711 .unwrap()
3712 })
3713 .collect(),
3714 ),
3715 collection_family(
3716 "collection/missing",
3717 missing_logical
3718 .iter()
3719 .zip(&missing_sources)
3720 .map(|(logical, source)| {
3721 CollectionTransitionFamilyMemberV1::new(
3722 logical.clone(),
3723 source.clone(),
3724 0,
3725 "Walk".into(),
3726 )
3727 .unwrap()
3728 })
3729 .collect(),
3730 ),
3731 ],
3732 manifest.clone(),
3733 );
3734 let mut large_document = document(None);
3735 large_document.skeleton.bones[0].name =
3736 "x".repeat(TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES);
3737 let large = loaded_source(large_document, b"large");
3738 let mut members = large_logical
3739 .iter()
3740 .map(|logical| {
3741 CollectionTransitionPoseMemberInputV1::available(
3742 logical, &source, 0, "Walk", &large,
3743 )
3744 })
3745 .collect::<Vec<_>>();
3746 members.extend(
3747 missing_logical
3748 .iter()
3749 .zip(&missing_sources)
3750 .map(|(logical, source)| {
3751 CollectionTransitionPoseMemberInputV1::unavailable(logical, source, 0, "Walk")
3752 }),
3753 );
3754 let result =
3755 evaluate_collection_transition_poses_v1(&declaration, &manifest, &members).unwrap();
3756 assert_eq!(
3757 result.families()[0].reason(),
3758 Some(TransitionPoseReasonV1::ResultLimit)
3759 );
3760 assert!(result.families()[0].skeleton_basis_input().is_some());
3761 assert_eq!(
3762 result.families()[1].reason(),
3763 Some(TransitionPoseReasonV1::MemberUnavailable)
3764 );
3765 assert!(result.families()[1].skeleton_basis_input().is_none());
3766 assert!(result.families()[1].members()[0].source_input().is_none());
3767 result.normalized_jcs().unwrap();
3768 }
3769
3770 #[test]
3771 fn basis_normalizes_quaternions_and_excludes_scale_bind_and_assets() {
3772 let first = document(None);
3773 let mut second = first.clone();
3774 second.skeleton.bones[0].rest.rotation = crate::glam::Quat::from_xyzw(0.0, -0.0, 0.0, -1.0);
3775 second.skeleton.bones[0].rest.scale = crate::glam::Vec3::splat(7.0);
3776 second.skeleton.bones[0].inverse_bind =
3777 Some(crate::glam::Mat4::from_scale(crate::glam::Vec3::splat(3.0)));
3778 let left = SkeletonBasisV1::from_skeleton(&first.skeleton).unwrap();
3779 let right = SkeletonBasisV1::from_skeleton(&second.skeleton).unwrap();
3780 assert_eq!(left.identity(), right.identity());
3781 second.skeleton.bones[0].name = "other".into();
3782 assert_ne!(
3783 left.identity(),
3784 SkeletonBasisV1::from_skeleton(&second.skeleton)
3785 .unwrap()
3786 .identity()
3787 );
3788 let parent_after_child = Skeleton {
3789 bones: vec![
3790 Bone {
3791 name: "child".into(),
3792 parent: Some(1),
3793 rest: Transform::IDENTITY,
3794 inverse_bind: None,
3795 },
3796 Bone {
3797 name: "parent".into(),
3798 parent: None,
3799 rest: Transform::IDENTITY,
3800 inverse_bind: None,
3801 },
3802 ],
3803 };
3804 assert_eq!(
3805 SkeletonBasisV1::from_skeleton(&parent_after_child),
3806 Err(SkeletonBasisError::InvalidParent { ordinal: 0 })
3807 );
3808 }
3809
3810 #[test]
3811 fn endpoint_comparison_is_inclusive_and_binds_raw_and_normalized_identities() {
3812 let declared = declaration(TransitionFamilyBoundaryV1::Both, 3.0_f64.sqrt(), 0.0);
3813 let document = document(Some(1.0));
3814 let raw = InputIdentity::from_bytes(b"document\r\n");
3815 let result =
3816 evaluate_document_transition_poses_v1(&declared, raw.clone(), &document).unwrap();
3817 assert_eq!(result.status(), TransitionPoseStatusV1::Complete);
3818 assert_eq!(result.decision(), TransitionPoseDecisionV1::Pass);
3819 assert_eq!(result.subject_input(), &raw);
3820 assert_eq!(result.declaration_input(), declared.source_identity());
3821 assert_eq!(
3822 result.declaration_normalized(),
3823 declared.normalized_identity()
3824 );
3825 assert_eq!(result.families()[0].pairs.len(), 2);
3826 assert!(
3827 result.families()[0]
3828 .pairs
3829 .iter()
3830 .all(|pair| pair.translation_offenders.is_empty())
3831 );
3832
3833 let finding = evaluate_document_transition_poses_v1(
3834 &declaration(TransitionFamilyBoundaryV1::Entry, 1.0, 0.0),
3835 raw,
3836 &document,
3837 )
3838 .unwrap();
3839 assert_eq!(finding.decision(), TransitionPoseDecisionV1::Finding);
3840 assert_eq!(
3841 finding.families()[0].pairs[0].translation_offenders.len(),
3842 1
3843 );
3844 }
3845
3846 #[test]
3847 fn complete_results_bind_external_dependency_changes_beyond_primary_bytes() {
3848 let declared = declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0);
3849 let primary = InputIdentity::from_bytes(br#"{"buffers":[{"uri":"animation.bin"}]}"#);
3850 let first_closure = closure_with_external_buffer(primary.clone(), b"first animation");
3851 let second_closure = closure_with_external_buffer(primary.clone(), b"second animation");
3852
3853 let first = super::evaluate_document_transition_poses_v1(
3854 &declared,
3855 &first_closure,
3856 &document(Some(1.0)),
3857 )
3858 .unwrap();
3859 let second = super::evaluate_document_transition_poses_v1(
3860 &declared,
3861 &second_closure,
3862 &document(Some(2.0)),
3863 )
3864 .unwrap();
3865
3866 assert_eq!(first.subject_input(), second.subject_input());
3867 assert_eq!(first.subject_input(), &primary);
3868 assert_ne!(
3869 first.subject_dependency_closure_identity(),
3870 second.subject_dependency_closure_identity()
3871 );
3872 assert_eq!(first.status(), TransitionPoseStatusV1::Complete);
3873 assert_eq!(second.status(), TransitionPoseStatusV1::Complete);
3874 assert_ne!(
3875 first.families()[0].pairs()[0].max_translation_delta_m(),
3876 second.families()[0].pairs()[0].max_translation_delta_m()
3877 );
3878 for result in [&first, &second] {
3879 let subject_closure = result
3880 .subject_dependency_closure_identity()
3881 .expect("complete result closure identity");
3882 assert!(result.families()[0].members().iter().all(|member| {
3883 member.source_input() == Some(result.subject_input())
3884 && member.source_dependency_closure_identity() == Some(subject_closure)
3885 }));
3886 }
3887 assert_ne!(
3888 first.normalized_jcs().unwrap(),
3889 second.normalized_jcs().unwrap()
3890 );
3891 }
3892
3893 #[test]
3894 fn incomplete_dependency_closure_cannot_produce_a_complete_outcome() {
3895 let primary = InputIdentity::from_bytes(b"document");
3896 let closure = DependencyClosureV1::unavailable(primary.clone());
3897 assert!(!closure.coverage().is_complete());
3898 assert!(closure.identity().is_none());
3899
3900 let result = super::evaluate_document_transition_poses_v1(
3901 &declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0),
3902 &closure,
3903 &document(Some(1.0)),
3904 )
3905 .unwrap();
3906 assert_eq!(result.subject_input(), &primary);
3907 assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
3908 assert_eq!(result.decision(), TransitionPoseDecisionV1::NotEvaluated);
3909 assert!(result.subject_dependency_closure_identity().is_none());
3910 assert_eq!(
3911 result.families()[0].reason(),
3912 Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
3913 );
3914 assert!(result.families()[0].members().iter().all(|member| {
3915 member.source_input() == Some(&primary)
3916 && member.source_dependency_closure_identity().is_none()
3917 }));
3918 let serialized_len = result.normalized_jcs().unwrap().len();
3919 let exact = evaluate_document_transition_poses_v1_with_result_limit(
3920 &declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0),
3921 &closure,
3922 &document(Some(1.0)),
3923 serialized_len,
3924 )
3925 .unwrap();
3926 assert_eq!(
3927 exact.families()[0].reason(),
3928 Some(TransitionPoseReasonV1::DependencyClosureIncomplete)
3929 );
3930 assert_eq!(
3931 evaluate_document_transition_poses_v1_with_result_limit(
3932 &declaration(TransitionFamilyBoundaryV1::Entry, 100.0, 0.0),
3933 &closure,
3934 &document(Some(1.0)),
3935 serialized_len - 1,
3936 ),
3937 Err(TransitionPoseEvaluationControlError::ResultTooLarge)
3938 );
3939
3940 let empty = TransitionFamilyDeclarationInputV1::new(
3941 TransitionFamilyDeclarationV1::document(Vec::new()).unwrap(),
3942 b"empty",
3943 )
3944 .unwrap();
3945 let no_config =
3946 super::evaluate_document_transition_poses_v1(&empty, &closure, &document(None))
3947 .unwrap();
3948 assert_eq!(no_config.status(), TransitionPoseStatusV1::Complete);
3949 assert_eq!(no_config.decision(), TransitionPoseDecisionV1::Pass);
3950 assert_eq!(
3951 no_config.reason(),
3952 Some(TransitionPoseReasonV1::NoConfiguredFamilies)
3953 );
3954 assert!(no_config.subject_dependency_closure_identity().is_none());
3955 }
3956
3957 #[test]
3958 fn rotation_delta_is_reflexive_sign_invariant_and_has_the_right_angle() {
3959 let axis = crate::glam::Vec3::new(1.0, 2.0, 3.0).normalize();
3960 for angle in [0.1, 0.9, 1.7] {
3961 let rotation =
3962 canonical_quaternion(crate::glam::Quat::from_axis_angle(axis, angle), 0).unwrap();
3963 assert_eq!(rotation_delta_deg(rotation, rotation), 0.0);
3964 assert_eq!(
3965 rotation_delta_deg(rotation, rotation.map(|value| -value)),
3966 0.0
3967 );
3968 }
3969 let left = canonical_quaternion(
3970 crate::glam::Quat::from_axis_angle(axis, 40f32.to_radians()),
3971 0,
3972 )
3973 .unwrap();
3974 let right = canonical_quaternion(
3975 crate::glam::Quat::from_axis_angle(axis, 130f32.to_radians()),
3976 0,
3977 )
3978 .unwrap();
3979 assert!((rotation_delta_deg(left, right) - 90.0).abs() < 1e-5);
3980
3981 let mut document = document(None);
3982 let rotation = crate::glam::Quat::from_axis_angle(axis, 0.9);
3983 for clip in &mut document.clips {
3984 clip.tracks.push(Track {
3985 bone: 0,
3986 property: Property::Rotation,
3987 interpolation: Interpolation::Linear,
3988 times: vec![0.0],
3989 values: TrackValues::Quats(vec![rotation]),
3990 });
3991 }
3992 let result = evaluate_document_transition_poses_v1(
3993 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
3994 InputIdentity::from_bytes(b"document"),
3995 &document,
3996 )
3997 .unwrap();
3998 assert_eq!(result.decision(), TransitionPoseDecisionV1::Pass);
3999 assert_eq!(
4000 result.families()[0].pairs()[0].max_rotation_delta_deg(),
4001 0.0
4002 );
4003 document.clips[1].tracks[0].values = TrackValues::Quats(vec![-rotation]);
4004 let sign_equivalent = evaluate_document_transition_poses_v1(
4005 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4006 InputIdentity::from_bytes(b"document"),
4007 &document,
4008 )
4009 .unwrap();
4010 assert_eq!(sign_equivalent.decision(), TransitionPoseDecisionV1::Pass);
4011 assert_eq!(
4012 sign_equivalent.families()[0].pairs()[0].max_rotation_delta_deg(),
4013 0.0
4014 );
4015 }
4016
4017 #[test]
4018 fn scale_tracks_are_wholly_ignored_by_v1_admission_validation_and_sampling() {
4019 let declared = declaration(TransitionFamilyBoundaryV1::Both, 0.0, 0.0);
4020 let raw = InputIdentity::from_bytes(b"document");
4021 let baseline =
4022 evaluate_document_transition_poses_v1(&declared, raw.clone(), &document(Some(1.0)))
4023 .unwrap();
4024 let mut with_scale_noise = document(Some(1.0));
4025 let malformed_scale = Track {
4026 bone: usize::MAX,
4027 property: Property::Scale,
4028 interpolation: Interpolation::Linear,
4029 times: vec![f32::NAN],
4030 values: TrackValues::Vec3s(Vec::new()),
4031 };
4032 with_scale_noise.clips[1]
4036 .tracks
4037 .extend(std::iter::repeat_n(malformed_scale.clone(), 2));
4038 let with_scale_noise =
4039 evaluate_document_transition_poses_v1(&declared, raw.clone(), &with_scale_noise)
4040 .unwrap();
4041 assert_eq!(with_scale_noise, baseline);
4042
4043 let mut one_too_many = document(Some(1.0));
4044 one_too_many.clips[1]
4045 .tracks
4046 .extend(std::iter::repeat_n(malformed_scale, 3));
4047 let one_too_many =
4048 evaluate_document_transition_poses_v1(&declared, raw, &one_too_many).unwrap();
4049 assert_eq!(
4050 one_too_many.families()[0].reason(),
4051 Some(TransitionPoseReasonV1::InputLimit)
4052 );
4053 }
4054
4055 #[test]
4056 fn selected_track_validation_is_linear_and_rejects_duplicate_tr_channels() {
4057 let mut tracks =
4058 Vec::with_capacity(TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP);
4059 for bone in 0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
4060 tracks.push(Track {
4061 bone,
4062 property: Property::Translation,
4063 interpolation: Interpolation::Linear,
4064 times: vec![0.0],
4065 values: TrackValues::Vec3s(vec![crate::glam::Vec3::ZERO]),
4066 });
4067 tracks.push(Track {
4068 bone,
4069 property: Property::Rotation,
4070 interpolation: Interpolation::Linear,
4071 times: vec![0.0],
4072 values: TrackValues::Quats(vec![crate::glam::Quat::IDENTITY]),
4073 });
4074 }
4075 let mut clip = Clip {
4076 name: "all-tr".into(),
4077 duration_s: 1.0,
4078 tracks,
4079 };
4080 let mut visits = 0usize;
4081 assert!(selected_tracks_are_strict_with(
4082 &clip,
4083 TRANSITION_POSE_EVALUATION_V1_MAX_BONES,
4084 |_| visits += 1,
4085 ));
4086 assert_eq!(
4087 visits,
4088 TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP
4089 );
4090
4091 clip.tracks.push(clip.tracks[0].clone());
4092 assert!(!selected_tracks_are_strict(
4093 &clip,
4094 TRANSITION_POSE_EVALUATION_V1_MAX_BONES
4095 ));
4096 }
4097
4098 #[test]
4099 fn time_and_duration_refusals_are_complete_family_unavailability() {
4100 let document = document(None);
4101 let time = evaluate_document_transition_poses_v1(
4102 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.01),
4103 InputIdentity::from_bytes(b"document"),
4104 &document,
4105 )
4106 .unwrap();
4107 assert_eq!(time.status(), TransitionPoseStatusV1::Incomplete);
4108 assert_eq!(
4109 time.families()[0].reason,
4110 Some(TransitionPoseReasonV1::TimeToleranceUnsupported)
4111 );
4112
4113 let mut zero = document;
4114 zero.clips[1].duration_s = 0.0;
4115 let duration = evaluate_document_transition_poses_v1(
4116 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4117 InputIdentity::from_bytes(b"document"),
4118 &zero,
4119 )
4120 .unwrap();
4121 assert_eq!(
4122 duration.families()[0].reason,
4123 Some(TransitionPoseReasonV1::ZeroDuration)
4124 );
4125 }
4126
4127 #[test]
4128 fn no_configured_families_does_not_traverse_mutable_document_payloads() {
4129 let mut document = document(None);
4130 document.skeleton.bones[0].parent = Some(0);
4131 let empty = TransitionFamilyDeclarationInputV1::new(
4132 TransitionFamilyDeclarationV1::document(Vec::new()).unwrap(),
4133 b"empty",
4134 )
4135 .unwrap();
4136 let result = evaluate_document_transition_poses_v1(
4137 &empty,
4138 InputIdentity::from_bytes(b"document"),
4139 &document,
4140 )
4141 .unwrap();
4142 assert_eq!(
4143 result.reason(),
4144 Some(TransitionPoseReasonV1::NoConfiguredFamilies)
4145 );
4146 }
4147
4148 #[test]
4149 fn admission_limits_precede_selected_allocation_but_not_witness_control() {
4150 let mut oversized = document(None);
4151 oversized.skeleton.bones = (0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES + 1)
4152 .map(|ordinal| Bone {
4153 name: format!("bone-{ordinal}"),
4154 parent: None,
4155 rest: Transform::IDENTITY,
4156 inverse_bind: None,
4157 })
4158 .collect();
4159 let invalid = DocumentTransitionFamilyV1::new(
4160 "invalid".into(),
4161 TransitionFamilyBoundaryV1::Entry,
4162 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4163 vec![
4164 DocumentTransitionFamilyMemberV1::new(2, "missing-a".into()).unwrap(),
4165 DocumentTransitionFamilyMemberV1::new(3, "missing-b".into()).unwrap(),
4166 ],
4167 )
4168 .unwrap();
4169 let invalid = TransitionFamilyDeclarationInputV1::new(
4170 TransitionFamilyDeclarationV1::document(vec![invalid]).unwrap(),
4171 b"declaration",
4172 )
4173 .unwrap();
4174 assert_eq!(
4175 evaluate_document_transition_poses_v1(
4176 &invalid,
4177 InputIdentity::from_bytes(b"document"),
4178 &oversized,
4179 ),
4180 Err(TransitionPoseEvaluationControlError::InvalidMemberWitness)
4181 );
4182
4183 let mut selected = document(None);
4184 let track = Track {
4185 bone: 0,
4186 property: Property::Translation,
4187 interpolation: Interpolation::Linear,
4188 times: vec![0.0],
4189 values: TrackValues::Vec3s(vec![crate::glam::Vec3::ZERO]),
4190 };
4191 selected.clips[1].tracks =
4192 vec![track.clone(); TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP + 1];
4193 let limited = evaluate_document_transition_poses_v1(
4194 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4195 InputIdentity::from_bytes(b"document"),
4196 &selected,
4197 )
4198 .unwrap();
4199 assert_eq!(
4200 limited.families()[0].reason(),
4201 Some(TransitionPoseReasonV1::InputLimit)
4202 );
4203
4204 let small_selected = document(Some(1.0));
4205 let selected_clip = &small_selected.clips[1];
4206 assert_eq!(
4207 plan_selected_track_input_limits_with(&[vec![selected_clip]], 1, 1, 1, &[false]),
4208 vec![true]
4209 );
4210 let oversized_tracks = Clip {
4211 name: "many".into(),
4212 duration_s: 1.0,
4213 tracks: vec![track.clone(); 2],
4214 };
4215 let small = Clip {
4216 name: "small".into(),
4217 duration_s: 1.0,
4218 tracks: vec![track.clone()],
4219 };
4220 assert_eq!(
4221 plan_selected_track_input_limits_with(
4222 &[vec![&oversized_tracks], vec![&small]],
4223 1,
4224 1,
4225 8,
4226 &[false, false],
4227 ),
4228 vec![true, false]
4229 );
4230 assert_eq!(
4231 plan_selected_track_input_limits_with(
4232 &[vec![&small], vec![&oversized_tracks], vec![&small]],
4233 3,
4234 3,
4235 5,
4236 &[false, false, false],
4237 ),
4238 vec![false, true, false]
4239 );
4240 let huge_unsupported = Clip {
4241 name: "policy-only".into(),
4242 duration_s: 1.0,
4243 tracks: vec![track.clone(); 4],
4244 };
4245 assert_eq!(
4246 plan_selected_track_input_limits_with(
4247 &[vec![&huge_unsupported], vec![&small]],
4248 3,
4249 3,
4250 5,
4251 &[true, false],
4252 ),
4253 vec![false, false]
4254 );
4255 let named = Skeleton {
4256 bones: vec![Bone {
4257 name: "x".repeat(16),
4258 parent: None,
4259 rest: Transform::IDENTITY,
4260 inverse_bind: None,
4261 }],
4262 };
4263 assert!(!skeleton_input_is_within_limits_with(&named, 15));
4264 assert_eq!(
4265 TRANSITION_POSE_EVALUATION_V1_MAX_RAW_TRACK_ROWS_PER_CLIP,
4266 TRANSITION_POSE_EVALUATION_V1_MAX_BONES * 3
4267 );
4268 let oversized_name = Skeleton {
4269 bones: vec![Bone {
4270 name: "x".repeat(TRANSITION_POSE_EVALUATION_V1_MAX_BASIS_TEXT_BYTES + 1),
4271 parent: None,
4272 rest: Transform::IDENTITY,
4273 inverse_bind: None,
4274 }],
4275 };
4276 assert_eq!(
4277 SkeletonBasisV1::from_skeleton(&oversized_name),
4278 Err(SkeletonBasisError::TooMuchText)
4279 );
4280
4281 let four_tracks = Clip {
4282 name: "four".into(),
4283 duration_s: 1.0,
4284 tracks: vec![track.clone(); 4],
4285 };
4286 assert_eq!(
4287 plan_selected_track_input_limits(&[vec![&four_tracks]], 1, &[false]),
4288 vec![true]
4289 );
4290
4291 let mut scaled = document(None);
4292 scaled.skeleton.bones[0].rest.scale = crate::glam::Vec3::NAN;
4293 assert_eq!(
4294 evaluate_document_transition_poses_v1(
4295 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4296 InputIdentity::from_bytes(b"document"),
4297 &scaled,
4298 )
4299 .unwrap()
4300 .status(),
4301 TransitionPoseStatusV1::Complete
4302 );
4303 }
4304
4305 #[test]
4306 fn clip_admission_bounds_global_witness_lookup_after_direct_witness_checks() {
4307 let declared = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
4308 let mut over_cap = document(None);
4309 for ordinal in over_cap.clips.len()..TRANSITION_POSE_EVALUATION_V1_MAX_DOCUMENT_CLIPS {
4310 over_cap.clips.push(Clip {
4311 name: format!("extra-{ordinal}"),
4312 duration_s: 1.0,
4313 tracks: Vec::new(),
4314 });
4315 }
4316 over_cap.clips.push(Clip {
4319 name: "Walk".into(),
4320 duration_s: 1.0,
4321 tracks: Vec::new(),
4322 });
4323 let limited = evaluate_document_transition_poses_v1(
4324 &declared,
4325 InputIdentity::from_bytes(b"document"),
4326 &over_cap,
4327 )
4328 .unwrap();
4329 assert_eq!(
4330 limited.families()[0].reason(),
4331 Some(TransitionPoseReasonV1::InputLimit)
4332 );
4333
4334 over_cap.clips[0].name = "stale".into();
4335 assert_eq!(
4336 evaluate_document_transition_poses_v1(
4337 &declared,
4338 InputIdentity::from_bytes(b"document"),
4339 &over_cap,
4340 ),
4341 Err(TransitionPoseEvaluationControlError::InvalidMemberWitness)
4342 );
4343 }
4344
4345 #[test]
4346 fn unrelated_assets_and_clips_do_not_block_transition_pose() {
4347 let mut document = document(None);
4348 document.assets.instances.push(crate::model::MeshInstance {
4349 node: 99,
4350 ..crate::model::MeshInstance::default()
4351 });
4352 let track = Track {
4353 bone: 99,
4354 property: Property::Translation,
4355 interpolation: Interpolation::Linear,
4356 times: Vec::new(),
4357 values: TrackValues::Vec3s(Vec::new()),
4358 };
4359 document.clips.push(Clip {
4360 name: "Unselected".into(),
4361 duration_s: 1.0,
4362 tracks: vec![track; TRANSITION_POSE_EVALUATION_V1_MAX_SELECTED_TRACKS_PER_CLIP + 1],
4363 });
4364 assert_eq!(
4365 evaluate_document_transition_poses_v1(
4366 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4367 InputIdentity::from_bytes(b"document"),
4368 &document,
4369 )
4370 .unwrap()
4371 .status(),
4372 TransitionPoseStatusV1::Complete
4373 );
4374 }
4375
4376 #[test]
4377 fn public_evaluator_rejects_wrong_scope_and_oversized_basis() {
4378 let collection_id = CollectionIdV1::new("collection").unwrap();
4379 let manifest = TransitionFamilyManifestIdentityV1::new(
4380 collection_id.clone(),
4381 InputIdentity::from_bytes(b"manifest"),
4382 )
4383 .unwrap();
4384 let members = ["one", "two"]
4385 .into_iter()
4386 .map(|suffix| {
4387 CollectionTransitionFamilyMemberV1::new(
4388 CollectionLogicalIdV1::new(format!("collection/{suffix}")).unwrap(),
4389 CollectionSourceKeyV1::new("source").unwrap(),
4390 0,
4391 suffix.into(),
4392 )
4393 .unwrap()
4394 })
4395 .collect();
4396 let collection = TransitionFamilyDeclarationV1::collection(
4397 manifest,
4398 vec![
4399 CollectionTransitionFamilyV1::new(
4400 CollectionLogicalIdV1::new("collection/family").unwrap(),
4401 TransitionFamilyBoundaryV1::Entry,
4402 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4403 members,
4404 )
4405 .unwrap(),
4406 ],
4407 )
4408 .unwrap();
4409 let collection =
4410 TransitionFamilyDeclarationInputV1::new(collection, b"collection").unwrap();
4411 assert_eq!(
4412 evaluate_document_transition_poses_v1(
4413 &collection,
4414 InputIdentity::from_bytes(b"document"),
4415 &Document::default(),
4416 ),
4417 Err(TransitionPoseEvaluationControlError::WrongDeclarationScope)
4418 );
4419
4420 let mut oversized = document(None);
4421 oversized.skeleton.bones = (0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES + 1)
4422 .map(|ordinal| Bone {
4423 name: format!("bone-{ordinal}"),
4424 parent: None,
4425 rest: Transform::IDENTITY,
4426 inverse_bind: None,
4427 })
4428 .collect();
4429 let limited = evaluate_document_transition_poses_v1(
4430 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4431 InputIdentity::from_bytes(b"document"),
4432 &oversized,
4433 )
4434 .unwrap();
4435 assert_eq!(limited.status(), TransitionPoseStatusV1::Incomplete);
4436 assert_eq!(
4437 limited.families()[0].reason(),
4438 Some(TransitionPoseReasonV1::InputLimit)
4439 );
4440 assert_eq!(limited.families()[0].skeleton_basis_input(), None);
4441 }
4442
4443 #[test]
4444 fn selected_track_shape_gaps_are_incomplete_but_unselected_tracks_are_ignored() {
4445 let declaration = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
4446 let mut variants = Vec::new();
4447
4448 let mut empty = document(Some(1.0));
4449 empty.clips[1].tracks[0].times.clear();
4450 variants.push(empty);
4451
4452 let mut nonfinite_time = document(Some(1.0));
4453 nonfinite_time.clips[1].tracks[0].times[0] = f32::NAN;
4454 variants.push(nonfinite_time);
4455
4456 let mut non_increasing = document(Some(1.0));
4457 non_increasing.clips[1].tracks[0].times[1] = 0.0;
4458 variants.push(non_increasing);
4459
4460 let mut wrong_cardinality = document(Some(1.0));
4461 wrong_cardinality.clips[1].tracks[0].values = TrackValues::Vec3s(Vec::new());
4462 variants.push(wrong_cardinality);
4463
4464 let mut wrong_type = document(Some(1.0));
4465 wrong_type.clips[1].tracks[0].values = TrackValues::Quats(vec![
4466 crate::glam::Quat::IDENTITY,
4467 crate::glam::Quat::IDENTITY,
4468 ]);
4469 variants.push(wrong_type);
4470
4471 let mut nonfinite_value = document(Some(1.0));
4472 nonfinite_value.clips[1].tracks[0].values =
4473 TrackValues::Vec3s(vec![crate::glam::Vec3::NAN, crate::glam::Vec3::ZERO]);
4474 variants.push(nonfinite_value);
4475
4476 let mut duplicate = document(Some(1.0));
4477 let duplicate_track = duplicate.clips[1].tracks[0].clone();
4478 duplicate.clips[1].tracks.push(duplicate_track);
4479 variants.push(duplicate);
4480
4481 let mut out_of_range = document(Some(1.0));
4482 out_of_range.clips[1].tracks[0].bone = 1;
4483 variants.push(out_of_range);
4484
4485 for document in variants {
4486 let result = evaluate_document_transition_poses_v1(
4487 &declaration,
4488 InputIdentity::from_bytes(b"document"),
4489 &document,
4490 )
4491 .expect("selected track gaps are result data, not control errors");
4492 assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
4493 assert_eq!(
4494 result.families()[0].reason,
4495 Some(TransitionPoseReasonV1::UnsupportedSampling)
4496 );
4497 }
4498
4499 let mut unrelated = document(None);
4500 unrelated.clips.push(Clip {
4501 name: "Unselected".into(),
4502 duration_s: 1.0,
4503 tracks: vec![Track {
4504 bone: 99,
4505 property: Property::Translation,
4506 interpolation: Interpolation::Linear,
4507 times: Vec::new(),
4508 values: TrackValues::Vec3s(Vec::new()),
4509 }],
4510 });
4511 let result = evaluate_document_transition_poses_v1(
4512 &declaration,
4513 InputIdentity::from_bytes(b"document"),
4514 &unrelated,
4515 )
4516 .unwrap();
4517 assert_eq!(result.status(), TransitionPoseStatusV1::Complete);
4518 }
4519
4520 #[test]
4521 fn endpoint_sampling_does_not_consume_the_unconfigured_boundary() {
4522 let mut exit_only_gap = document(Some(0.0));
4523 exit_only_gap.clips[1].tracks.push(Track {
4524 bone: 0,
4525 property: Property::Rotation,
4526 interpolation: Interpolation::Linear,
4527 times: vec![0.0, 1.0],
4528 values: TrackValues::Quats(vec![
4529 crate::glam::Quat::IDENTITY,
4530 crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
4531 ]),
4532 });
4533 let entry = evaluate_document_transition_poses_v1(
4534 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4535 InputIdentity::from_bytes(b"document"),
4536 &exit_only_gap,
4537 )
4538 .unwrap();
4539 assert_eq!(entry.status(), TransitionPoseStatusV1::Complete);
4540 let exit = evaluate_document_transition_poses_v1(
4541 &declaration(TransitionFamilyBoundaryV1::Exit, 0.0, 0.0),
4542 InputIdentity::from_bytes(b"document"),
4543 &exit_only_gap,
4544 )
4545 .unwrap();
4546 assert_eq!(exit.status(), TransitionPoseStatusV1::Incomplete);
4547 assert_eq!(
4548 exit.families()[0].reason,
4549 Some(TransitionPoseReasonV1::UnsupportedSampling)
4550 );
4551
4552 let mut entry_only_gap = document(Some(0.0));
4553 entry_only_gap.clips[1].tracks.push(Track {
4554 bone: 0,
4555 property: Property::Rotation,
4556 interpolation: Interpolation::Linear,
4557 times: vec![0.0, 1.0],
4558 values: TrackValues::Quats(vec![
4559 crate::glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
4560 crate::glam::Quat::IDENTITY,
4561 ]),
4562 });
4563 let entry = evaluate_document_transition_poses_v1(
4564 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4565 InputIdentity::from_bytes(b"document"),
4566 &entry_only_gap,
4567 )
4568 .unwrap();
4569 assert_eq!(entry.status(), TransitionPoseStatusV1::Incomplete);
4570 let exit = evaluate_document_transition_poses_v1(
4571 &declaration(TransitionFamilyBoundaryV1::Exit, 0.0, 0.0),
4572 InputIdentity::from_bytes(b"document"),
4573 &entry_only_gap,
4574 )
4575 .unwrap();
4576 assert_eq!(exit.status(), TransitionPoseStatusV1::Complete);
4577
4578 let mut nonrepresentable_duration = document(None);
4579 nonrepresentable_duration.clips[1].duration_s = 0.1;
4580 let entry = evaluate_document_transition_poses_v1(
4581 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
4582 InputIdentity::from_bytes(b"document"),
4583 &nonrepresentable_duration,
4584 )
4585 .unwrap();
4586 assert_eq!(entry.status(), TransitionPoseStatusV1::Complete);
4587 let exit = evaluate_document_transition_poses_v1(
4588 &declaration(TransitionFamilyBoundaryV1::Exit, 0.0, 0.0),
4589 InputIdentity::from_bytes(b"document"),
4590 &nonrepresentable_duration,
4591 )
4592 .unwrap();
4593 assert_eq!(exit.status(), TransitionPoseStatusV1::Incomplete);
4594 assert_eq!(
4595 exit.families()[0].reason,
4596 Some(TransitionPoseReasonV1::UnsupportedSampling)
4597 );
4598 }
4599
4600 #[test]
4601 fn multi_bone_pairs_and_independent_offender_caps_are_canonical() {
4602 let bones = (0..17)
4603 .map(|ordinal| Bone {
4604 name: format!("bone-{ordinal:02}"),
4605 parent: None,
4606 rest: Transform::IDENTITY,
4607 inverse_bind: None,
4608 })
4609 .collect::<Vec<_>>();
4610 let animated = |name: &str, translation_multiplier: f32| Clip {
4611 name: name.into(),
4612 duration_s: 1.0,
4613 tracks: (0..17)
4614 .flat_map(|ordinal| {
4615 [
4616 Track {
4617 bone: ordinal,
4618 property: Property::Translation,
4619 interpolation: Interpolation::Linear,
4620 times: vec![0.0, 1.0],
4621 values: TrackValues::Vec3s(vec![
4622 crate::glam::Vec3::X
4623 * ((ordinal + 1) as f32
4624 * translation_multiplier);
4625 2
4626 ]),
4627 },
4628 Track {
4629 bone: ordinal,
4630 property: Property::Rotation,
4631 interpolation: Interpolation::Linear,
4632 times: vec![0.0, 1.0],
4633 values: TrackValues::Quats(vec![
4634 crate::glam::Quat::from_rotation_x(
4635 std::f32::consts::FRAC_PI_2
4636 );
4637 2
4638 ]),
4639 },
4640 ]
4641 })
4642 .collect(),
4643 };
4644 let document = Document {
4645 skeleton: Skeleton { bones },
4646 clips: vec![
4647 Clip {
4648 name: "A".into(),
4649 duration_s: 1.0,
4650 tracks: Vec::new(),
4651 },
4652 animated("B", 1.0),
4653 animated("C", 2.0),
4654 ],
4655 ..Document::default()
4656 };
4657 let family = DocumentTransitionFamilyV1::new(
4658 "three_way".into(),
4659 TransitionFamilyBoundaryV1::Entry,
4660 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4661 ["A", "B", "C"]
4662 .into_iter()
4663 .enumerate()
4664 .map(|(index, name)| {
4665 DocumentTransitionFamilyMemberV1::new(index as u64, name.into())
4666 })
4667 .collect::<Result<Vec<_>, _>>()
4668 .unwrap(),
4669 )
4670 .unwrap();
4671 let declaration = TransitionFamilyDeclarationInputV1::new(
4672 TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
4673 b"declaration",
4674 )
4675 .unwrap();
4676 let result = evaluate_document_transition_poses_v1(
4677 &declaration,
4678 InputIdentity::from_bytes(b"document"),
4679 &document,
4680 )
4681 .unwrap();
4682 let pairs = result.families()[0].pairs();
4683 assert_eq!(
4684 pairs
4685 .iter()
4686 .map(TransitionPosePairEvaluationV1::member_indices)
4687 .collect::<Vec<_>>(),
4688 vec![[0, 1], [0, 2], [1, 2]]
4689 );
4690 let first = &pairs[0];
4691 assert_eq!(first.translation_offenders().len(), 16);
4692 assert_eq!(first.rotation_offenders().len(), 16);
4693 assert_eq!(
4694 first
4695 .translation_offenders()
4696 .iter()
4697 .map(TransitionPoseTranslationOffenderV1::bone_ordinal)
4698 .collect::<Vec<_>>(),
4699 (1..17).rev().collect::<Vec<_>>()
4700 );
4701 assert_eq!(
4702 first
4703 .rotation_offenders()
4704 .iter()
4705 .map(TransitionPoseRotationOffenderV1::bone_ordinal)
4706 .collect::<Vec<_>>(),
4707 (0..16).collect::<Vec<_>>()
4708 );
4709 assert!(
4710 first
4711 .translation_offenders()
4712 .iter()
4713 .any(|offender| offender.bone_ordinal() == 1)
4714 );
4715 assert!(
4716 first
4717 .rotation_offenders()
4718 .iter()
4719 .any(|offender| offender.bone_ordinal() == 1)
4720 );
4721 }
4722
4723 #[test]
4724 fn offender_selection_never_retains_more_than_its_final_cap() {
4725 let mut descending = Vec::new();
4726 for ordinal in 0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES {
4727 retain_top_candidate(
4728 &mut descending,
4729 ordinal,
4730 ordinal as f64,
4731 TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS,
4732 );
4733 assert!(descending.len() <= TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS);
4734 }
4735 sort_top_candidates(&mut descending);
4736 assert_eq!(
4737 descending
4738 .iter()
4739 .map(|(ordinal, _)| *ordinal)
4740 .collect::<Vec<_>>(),
4741 (TRANSITION_POSE_EVALUATION_V1_MAX_BONES
4742 - TRANSITION_POSE_EVALUATION_V1_MAX_TRANSLATION_OFFENDERS
4743 ..TRANSITION_POSE_EVALUATION_V1_MAX_BONES)
4744 .rev()
4745 .collect::<Vec<_>>()
4746 );
4747
4748 let mut tied = Vec::new();
4749 for ordinal in (0..TRANSITION_POSE_EVALUATION_V1_MAX_BONES).rev() {
4750 retain_top_candidate(
4751 &mut tied,
4752 ordinal,
4753 1.0,
4754 TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS,
4755 );
4756 assert!(tied.len() <= TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS);
4757 }
4758 sort_top_candidates(&mut tied);
4759 assert_eq!(
4760 tied.iter().map(|(ordinal, _)| *ordinal).collect::<Vec<_>>(),
4761 (0..TRANSITION_POSE_EVALUATION_V1_MAX_ROTATION_OFFENDERS).collect::<Vec<_>>()
4762 );
4763 }
4764
4765 fn numbered_members(count: u64) -> Vec<DocumentTransitionFamilyMemberV1> {
4766 (0..count)
4767 .map(|index| DocumentTransitionFamilyMemberV1::new(index, format!("clip-{index}")))
4768 .collect::<Result<Vec<_>, _>>()
4769 .unwrap()
4770 }
4771
4772 fn numbered_family(
4773 family_id: String,
4774 count: u64,
4775 boundary: TransitionFamilyBoundaryV1,
4776 ) -> DocumentTransitionFamilyV1 {
4777 DocumentTransitionFamilyV1::new(
4778 family_id,
4779 boundary,
4780 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4781 numbered_members(count),
4782 )
4783 .unwrap()
4784 }
4785
4786 fn numbered_document(clips: u64, bones: usize) -> Document {
4787 Document {
4788 skeleton: Skeleton {
4789 bones: (0..bones)
4790 .map(|ordinal| Bone {
4791 name: format!("bone-{ordinal}"),
4792 parent: None,
4793 rest: Transform::IDENTITY,
4794 inverse_bind: None,
4795 })
4796 .collect(),
4797 },
4798 clips: (0..clips)
4799 .map(|index| Clip {
4800 name: format!("clip-{index}"),
4801 duration_s: 1.0,
4802 tracks: Vec::new(),
4803 })
4804 .collect(),
4805 ..Document::default()
4806 }
4807 }
4808
4809 #[test]
4810 fn member_witnesses_are_control_preflight_before_all_unavailability_reasons() {
4811 let input = |families| {
4812 TransitionFamilyDeclarationInputV1::new(
4813 TransitionFamilyDeclarationV1::document(families).unwrap(),
4814 b"declaration",
4815 )
4816 .unwrap()
4817 };
4818 let assert_invalid_witness = |input: &TransitionFamilyDeclarationInputV1,
4819 document: &Document| {
4820 assert_eq!(
4821 evaluate_document_transition_poses_v1(
4822 input,
4823 InputIdentity::from_bytes(b"document"),
4824 document,
4825 ),
4826 Err(TransitionPoseEvaluationControlError::InvalidMemberWitness)
4827 );
4828 };
4829
4830 let time_family = DocumentTransitionFamilyV1::new(
4831 "time".into(),
4832 TransitionFamilyBoundaryV1::Entry,
4833 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.1).unwrap(),
4834 vec![
4835 DocumentTransitionFamilyMemberV1::new(2, "missing-a".into()).unwrap(),
4836 DocumentTransitionFamilyMemberV1::new(3, "missing-b".into()).unwrap(),
4837 ],
4838 )
4839 .unwrap();
4840 assert_invalid_witness(&input(vec![time_family]), &document(None));
4841
4842 let family_work =
4843 numbered_family("family_work".into(), 65, TransitionFamilyBoundaryV1::Both);
4844 assert_eq!(
4845 plan_families(std::slice::from_ref(&family_work), 1, &[false])[0].reason,
4846 Some(TransitionPoseReasonV1::FamilyWorkLimit)
4847 );
4848 assert_invalid_witness(&input(vec![family_work]), &Document::default());
4849
4850 let aggregate = (0..33)
4851 .map(|ordinal| {
4852 numbered_family(
4853 format!("aggregate-{ordinal}"),
4854 65,
4855 TransitionFamilyBoundaryV1::Entry,
4856 )
4857 })
4858 .collect::<Vec<_>>();
4859 assert!(
4860 plan_families(&aggregate, 0, &vec![false; aggregate.len()])
4861 .iter()
4862 .any(|plan| plan.reason == Some(TransitionPoseReasonV1::AggregateWorkLimit))
4863 );
4864 assert_invalid_witness(&input(aggregate), &Document::default());
4865
4866 let retention = vec![
4867 numbered_family("retention-a".into(), 64, TransitionFamilyBoundaryV1::Entry),
4868 numbered_family("retention-b".into(), 64, TransitionFamilyBoundaryV1::Entry),
4869 ];
4870 assert_eq!(
4871 plan_families(&retention, 16, &[false, false])[1].reason,
4872 Some(TransitionPoseReasonV1::RetentionLimit)
4873 );
4874 assert_invalid_witness(&input(retention), &numbered_document(0, 16));
4875
4876 let earlier_time = DocumentTransitionFamilyV1::new(
4877 "a-time".into(),
4878 TransitionFamilyBoundaryV1::Entry,
4879 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.1).unwrap(),
4880 vec![
4881 DocumentTransitionFamilyMemberV1::new(0, "Walk".into()).unwrap(),
4882 DocumentTransitionFamilyMemberV1::new(1, "Run".into()).unwrap(),
4883 ],
4884 )
4885 .unwrap();
4886 let later_invalid = DocumentTransitionFamilyV1::new(
4887 "z-invalid".into(),
4888 TransitionFamilyBoundaryV1::Entry,
4889 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4890 vec![
4891 DocumentTransitionFamilyMemberV1::new(2, "missing-a".into()).unwrap(),
4892 DocumentTransitionFamilyMemberV1::new(3, "missing-b".into()).unwrap(),
4893 ],
4894 )
4895 .unwrap();
4896 assert_invalid_witness(&input(vec![earlier_time, later_invalid]), &document(None));
4897 }
4898
4899 #[test]
4900 fn pair_work_math_is_checked_before_sampling() {
4901 let family = numbered_family(
4902 "too_many_pairs".into(),
4903 65,
4904 TransitionFamilyBoundaryV1::Both,
4905 );
4906 let input = TransitionFamilyDeclarationInputV1::new(
4907 TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
4908 b"declaration",
4909 )
4910 .unwrap();
4911 let result = evaluate_document_transition_poses_v1(
4912 &input,
4913 InputIdentity::from_bytes(b"document"),
4914 &numbered_document(65, 1),
4915 )
4916 .unwrap();
4917 assert_eq!(
4918 result.families()[0].reason,
4919 Some(TransitionPoseReasonV1::FamilyWorkLimit)
4920 );
4921 assert_eq!(checked_pair_count(64), Some(2_016));
4922 assert_eq!(checked_pair_count(usize::MAX), None);
4923 }
4924
4925 #[test]
4926 fn retention_preflight_uses_the_actual_bone_bounded_offender_capacity() {
4927 let members = (0..64)
4928 .map(|index| DocumentTransitionFamilyMemberV1::new(index, format!("clip-{index}")))
4929 .collect::<Result<Vec<_>, _>>()
4930 .unwrap();
4931 let family = DocumentTransitionFamilyV1::new(
4932 "near_retention_limit".into(),
4933 TransitionFamilyBoundaryV1::Entry,
4934 TransitionFamilyTolerancesV1::new(0.0, 0.0, 0.0).unwrap(),
4935 members,
4936 )
4937 .unwrap();
4938
4939 let low_bone_families = vec![family.clone(); 16];
4942 assert!(
4943 plan_families(&low_bone_families, 1, &vec![false; low_bone_families.len()])
4944 .iter()
4945 .all(|plan| plan.reason.is_none())
4946 );
4947
4948 let high_bone_plans = plan_families(&[family.clone(), family], 16, &[false, false]);
4951 assert_eq!(high_bone_plans[0].reason, None);
4952 assert_eq!(
4953 high_bone_plans[1].reason,
4954 Some(TransitionPoseReasonV1::RetentionLimit)
4955 );
4956 }
4957
4958 #[test]
4959 fn long_bone_names_hit_result_limit_before_offender_name_cloning() {
4960 let family = numbered_family("long_names".into(), 64, TransitionFamilyBoundaryV1::Entry);
4961 let input = TransitionFamilyDeclarationInputV1::new(
4962 TransitionFamilyDeclarationV1::document(vec![family]).unwrap(),
4963 b"declaration",
4964 )
4965 .unwrap();
4966 let mut document = numbered_document(64, 1);
4967 document.skeleton.bones[0].name = "x".repeat(200_000);
4968 let result = evaluate_document_transition_poses_v1(
4969 &input,
4970 InputIdentity::from_bytes(b"document"),
4971 &document,
4972 )
4973 .unwrap();
4974 assert_eq!(
4975 result.families()[0].reason(),
4976 Some(TransitionPoseReasonV1::ResultLimit)
4977 );
4978 assert!(result.families()[0].pairs().is_empty());
4979 }
4980
4981 #[test]
4982 fn detailed_reservation_subtracts_pair_free_authority_before_name_rows() {
4983 let declared = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
4984 let families = declared.declaration().document_families().unwrap();
4985 let mut document = document(Some(1.0));
4986 document.skeleton.bones[0].name = "n".repeat(128);
4987 let subject = InputIdentity::from_bytes(b"document");
4988 let closure = complete_closure(subject.clone());
4989 let result =
4990 evaluate_document_transition_poses_v1(&declared, subject.clone(), &document).unwrap();
4991 let basis = SkeletonBasisV1::from_skeleton(&document.skeleton).unwrap();
4992 let remaining = detailed_result_budget_after_base(
4993 &result,
4994 families,
4995 &closure,
4996 basis.identity(),
4997 TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES,
4998 )
4999 .unwrap();
5000 let base_bytes = TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES - remaining;
5001 let mut reservation = remaining;
5002 assert!(reserve_detailed_name_budget(
5003 &mut reservation,
5004 &families[0],
5005 &document.skeleton,
5006 ));
5007 let detailed_bytes = remaining - reservation;
5008 let cap = base_bytes + detailed_bytes - 1;
5009 let result = evaluate_document_transition_poses_v1_with_result_limit(
5010 &declared, &closure, &document, cap,
5011 )
5012 .unwrap();
5013 assert_eq!(
5014 result.families()[0].reason(),
5015 Some(TransitionPoseReasonV1::ResultLimit)
5016 );
5017 assert!(result.families()[0].pairs().is_empty());
5018 }
5019
5020 #[test]
5021 fn result_limit_retry_retains_bindings_and_terminates_at_a_tiny_seam() {
5022 let declaration = declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0);
5023 let mut result = evaluate_document_transition_poses_v1(
5024 &declaration,
5025 InputIdentity::from_bytes(b"document"),
5026 &document(Some(1.0)),
5027 )
5028 .unwrap();
5029 let before_members = result.families()[0].members().to_vec();
5030 let before_basis = result.families()[0].skeleton_basis_input().cloned();
5031 let before_declaration = result.declaration_input().clone();
5032 let before_normalized = result.declaration_normalized().clone();
5033 let before_subject = result.subject_input().clone();
5034 let before_subject_closure = result.subject_dependency_closure_identity().cloned();
5035 let before_member_closures = result.families()[0]
5036 .members()
5037 .iter()
5038 .map(|member| member.source_dependency_closure_identity().cloned())
5039 .collect::<Vec<_>>();
5040 let detailed_bytes =
5041 canonical_bytes(&result, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
5042 .unwrap()
5043 .len();
5044
5045 let mut degraded = result.clone();
5046 for family in &mut degraded.families {
5047 family.status = TransitionPoseStatusV1::Incomplete;
5048 family.decision = TransitionPoseDecisionV1::NotEvaluated;
5049 family.reason = Some(TransitionPoseReasonV1::ResultLimit);
5050 family.pairs.clear();
5051 }
5052 derive_result_state(&mut degraded);
5053 let degraded_bytes =
5054 canonical_bytes(°raded, TRANSITION_POSE_EVALUATION_V1_MAX_RESULT_BYTES)
5055 .unwrap()
5056 .len();
5057 assert!(degraded_bytes < detailed_bytes);
5058
5059 enforce_result_limit(&mut result, degraded_bytes).unwrap();
5060 assert_eq!(result.status(), TransitionPoseStatusV1::Incomplete);
5061 assert_eq!(result.decision(), TransitionPoseDecisionV1::NotEvaluated);
5062 assert_eq!(result.declaration_input(), &before_declaration);
5063 assert_eq!(result.declaration_normalized(), &before_normalized);
5064 assert_eq!(result.subject_input(), &before_subject);
5065 assert_eq!(
5066 result.subject_dependency_closure_identity(),
5067 before_subject_closure.as_ref()
5068 );
5069 assert_eq!(result.families()[0].members(), before_members);
5070 assert_eq!(
5071 result.families()[0]
5072 .members()
5073 .iter()
5074 .map(|member| member.source_dependency_closure_identity().cloned())
5075 .collect::<Vec<_>>(),
5076 before_member_closures
5077 );
5078 assert_eq!(
5079 result.families()[0].skeleton_basis_input(),
5080 before_basis.as_ref()
5081 );
5082 assert_eq!(result.families()[0].pairs(), &[]);
5083 assert_eq!(
5084 result.families()[0].reason(),
5085 Some(TransitionPoseReasonV1::ResultLimit)
5086 );
5087 assert!(canonical_bytes(&result, degraded_bytes).is_ok());
5088 }
5089
5090 #[test]
5091 fn bounded_writer_refuses_its_first_excess_byte() {
5092 assert!(canonical_bytes(&"1234", 6).is_ok());
5093 assert!(canonical_bytes(&"1234", 5).is_err());
5094 }
5095
5096 #[test]
5097 fn result_wire_covers_the_closed_no_config_pass_finding_and_incomplete_matrix() {
5098 let empty = TransitionFamilyDeclarationInputV1::new(
5099 TransitionFamilyDeclarationV1::document(Vec::new()).unwrap(),
5100 b"empty",
5101 )
5102 .unwrap();
5103 let pass = evaluate_document_transition_poses_v1(
5104 &empty,
5105 InputIdentity::from_bytes(b"document"),
5106 &document(None),
5107 )
5108 .unwrap();
5109 let pass_wire = serde_json::to_value(&pass).unwrap();
5110 assert_eq!(pass_wire["schema"], TRANSITION_POSE_EVALUATION_V1_ID);
5111 assert_eq!(pass_wire["status"], "complete");
5112 assert_eq!(pass_wire["decision"], "pass");
5113 assert_eq!(pass_wire["reason"], "no_configured_families");
5114
5115 let finding = evaluate_document_transition_poses_v1(
5116 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.0),
5117 InputIdentity::from_bytes(b"document"),
5118 &document(Some(1.0)),
5119 )
5120 .unwrap();
5121 let finding_wire = serde_json::to_value(&finding).unwrap();
5122 assert_eq!(finding_wire["status"], "complete");
5123 assert_eq!(finding_wire["decision"], "finding");
5124 assert!(finding_wire.get("reason").is_none());
5125
5126 let incomplete = evaluate_document_transition_poses_v1(
5127 &declaration(TransitionFamilyBoundaryV1::Entry, 0.0, 0.1),
5128 InputIdentity::from_bytes(b"document"),
5129 &document(None),
5130 )
5131 .unwrap();
5132 let incomplete_wire = serde_json::to_value(&incomplete).unwrap();
5133 assert_eq!(incomplete_wire["status"], "incomplete");
5134 assert_eq!(incomplete_wire["decision"], "not_evaluated");
5135 assert_eq!(
5136 incomplete_wire["families"][0]["reason"],
5137 "time_tolerance_unsupported"
5138 );
5139 }
5140}