1use super::{
4 ScaleError, ScaleOperation, ScalePlan, ScaleProjectedRole, ScaleSourceNodeKind,
5 ScaleTolerancePolicy,
6};
7use crate::model::{
8 Document, Property, SourceNodeLocalRest, SourceSkeletonCoverage, TrackValues,
9 validate_document_shape,
10};
11use serde::Serialize;
12use std::collections::{BTreeMap, BTreeSet};
13
14pub const ASSEMBLY_SCALE_BASIS_VERSION: u32 = 1;
16
17#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
19pub struct AssemblyScaleNamedNode {
20 pub name: String,
22 pub parent: Option<String>,
24 pub translation_bits: [u32; 3],
26 pub rotation_bits: [u32; 4],
28 pub scale_bits: [u32; 3],
30}
31
32#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
34pub struct AssemblyScaleSourceNode {
35 pub source_node_index: usize,
37 pub parent_source_node_index: Option<usize>,
39 pub name: Option<String>,
41 pub role: String,
43 pub local_rest: AssemblyScaleSourceRest,
45}
46
47#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
49#[serde(tag = "kind", rename_all = "snake_case")]
50pub enum AssemblyScaleSourceRest {
51 Trs {
53 translation_bits: [u32; 3],
55 rotation_bits: [u32; 4],
57 scale_bits: [u32; 3],
59 },
60 Matrix {
62 matrix_bits: [u32; 16],
64 },
65}
66
67#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
69pub struct AssemblyScaleTargetPath {
70 pub clip_index: usize,
72 pub track_index: usize,
74 pub bone: String,
76 pub property: &'static str,
78 pub factor_bits: u64,
80}
81
82#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
84pub struct AssemblyScaleBasis {
85 pub version: u32,
87 pub coordinate_convention: &'static str,
89 pub tolerance_policy_id: &'static str,
91 pub source_skin_index: usize,
93 pub source_root_node_index: usize,
95 pub expected_factor_bits: u64,
97 pub named_nodes: Vec<AssemblyScaleNamedNode>,
99 pub source_nodes: Vec<AssemblyScaleSourceNode>,
101 pub target_paths: Vec<AssemblyScaleTargetPath>,
103}
104
105#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
110pub struct AssemblyScaleSkinlessClipBasis {
111 pub version: u32,
113 pub coordinate_convention: &'static str,
115 pub tolerance_policy_id: &'static str,
117 pub root_node_name: String,
119 pub source_root_node_index: usize,
121 pub expected_factor_bits: u64,
123 pub named_nodes: Vec<AssemblyScaleNamedNode>,
125 pub target_paths: Vec<AssemblyScaleTargetPath>,
127}
128
129#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
131#[error("assembly scale basis mismatch ({reason})")]
132pub struct AssemblyScaleCompatibilityError {
133 pub reason: &'static str,
135}
136
137#[derive(Debug, Clone, Copy)]
143#[non_exhaustive]
144pub enum AssemblyScaleSelectorRequest<'a> {
145 Indexed,
147 Named {
149 root_node_name: &'a str,
151 },
152}
153
154#[derive(Debug, Clone, Copy, PartialEq, Eq)]
156pub struct AssemblyScaleResolvedNamedSelector {
157 pub source_skin_index: usize,
159 pub source_root_node_index: usize,
161}
162
163#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
165#[non_exhaustive]
166pub enum AssemblyScaleNamedSelectorResolutionError {
167 #[error("named assembly scale root resolves to {matches} source nodes; expected exactly one")]
169 RootNotUnique {
170 matches: usize,
172 },
173 #[error("named assembly scale root fully governs {matches} source skins; expected exactly one")]
175 SkinNotUnique {
176 matches: usize,
178 },
179}
180
181#[derive(Debug, Clone)]
182enum AssemblyScaleSelectorIdentity {
183 Indexed,
184 Named {
185 root_node_name: String,
186 skin_joint_names: Vec<String>,
187 },
188}
189
190#[derive(Debug, Clone)]
195pub struct AssemblyScaleCompatibilityBasis {
196 basis: AssemblyScaleBasis,
197 selector: AssemblyScaleSelectorIdentity,
198 animation_target_factors: BTreeMap<(String, Property), f64>,
199 affected_node_names: BTreeSet<String>,
200}
201
202fn named_nodes(document: &Document) -> Result<Vec<AssemblyScaleNamedNode>, ScaleError> {
203 let mut names = BTreeSet::new();
204 let mut named_nodes = Vec::with_capacity(document.skeleton.bones.len());
205 for (index, bone) in document.skeleton.bones.iter().enumerate() {
206 if bone.name.is_empty() || !names.insert(bone.name.as_str()) {
207 return Err(ScaleError::PlanDocumentMismatch {
208 reason: "assembly_basis_requires_unique_named_nodes",
209 });
210 }
211 named_nodes.push(AssemblyScaleNamedNode {
212 name: bone.name.clone(),
213 parent: bone
214 .parent
215 .and_then(|parent| document.skeleton.bones.get(parent))
216 .map(|parent| parent.name.clone()),
217 translation_bits: bone.rest.translation.to_array().map(f32::to_bits),
218 rotation_bits: bone.rest.rotation.to_array().map(f32::to_bits),
219 scale_bits: bone.rest.scale.to_array().map(f32::to_bits),
220 });
221 if bone.parent.is_some_and(|parent| parent >= index) {
222 return Err(ScaleError::PlanDocumentMismatch {
223 reason: "assembly_basis_parent_order",
224 });
225 }
226 }
227 Ok(named_nodes)
228}
229
230fn governed_source_node_indices(
231 parent_by_index: &BTreeMap<usize, Option<usize>>,
232 source_root_node_index: usize,
233) -> BTreeSet<usize> {
234 if !parent_by_index.contains_key(&source_root_node_index) {
235 return BTreeSet::new();
236 }
237
238 let mut children_by_index = BTreeMap::<usize, Vec<usize>>::new();
239 for (&source_node_index, parent_source_node_index) in parent_by_index {
240 if let Some(parent_source_node_index) = parent_source_node_index {
241 children_by_index
242 .entry(*parent_source_node_index)
243 .or_default()
244 .push(source_node_index);
245 }
246 }
247
248 let mut governed = BTreeSet::new();
249 let mut pending = vec![source_root_node_index];
250 while let Some(source_node_index) = pending.pop() {
251 if governed.insert(source_node_index)
252 && let Some(children) = children_by_index.get(&source_node_index)
253 {
254 pending.extend(children.iter().copied());
255 }
256 }
257 governed
258}
259
260fn source_skin_is_fully_governed(
261 governed_source_node_indices: &BTreeSet<usize>,
262 joint_source_node_indices: &[usize],
263) -> bool {
264 !joint_source_node_indices.is_empty()
265 && joint_source_node_indices
266 .iter()
267 .all(|joint| governed_source_node_indices.contains(joint))
268}
269
270pub fn resolve_assembly_scale_named_selector(
284 document: &Document,
285 root_node_name: &str,
286) -> Result<AssemblyScaleResolvedNamedSelector, AssemblyScaleNamedSelectorResolutionError> {
287 let root_matches = document
288 .assets
289 .source_skeleton
290 .nodes
291 .iter()
292 .filter_map(|node| {
293 node.bone
294 .and_then(|bone| document.skeleton.bones.get(bone))
295 .filter(|bone| bone.name == root_node_name)
296 .map(|_| node.source_node_index)
297 })
298 .collect::<Vec<_>>();
299 let [source_root_node_index] = root_matches.as_slice() else {
300 return Err(AssemblyScaleNamedSelectorResolutionError::RootNotUnique {
301 matches: root_matches.len(),
302 });
303 };
304 let parent_by_index = document
305 .assets
306 .source_skeleton
307 .nodes
308 .iter()
309 .map(|node| (node.source_node_index, node.parent_source_node_index))
310 .collect::<BTreeMap<_, _>>();
311 let governed_source_node_indices =
312 governed_source_node_indices(&parent_by_index, *source_root_node_index);
313 let skin_matches = document
314 .assets
315 .source_skeleton
316 .skins
317 .iter()
318 .filter(|skin| {
319 source_skin_is_fully_governed(
320 &governed_source_node_indices,
321 &skin.joint_source_node_indices,
322 )
323 })
324 .collect::<Vec<_>>();
325 let [skin] = skin_matches.as_slice() else {
326 return Err(AssemblyScaleNamedSelectorResolutionError::SkinNotUnique {
327 matches: skin_matches.len(),
328 });
329 };
330 Ok(AssemblyScaleResolvedNamedSelector {
331 source_skin_index: skin.source_skin_index,
332 source_root_node_index: *source_root_node_index,
333 })
334}
335
336impl AssemblyScaleCompatibilityBasis {
337 #[must_use]
339 pub fn basis(&self) -> &AssemblyScaleBasis {
340 &self.basis
341 }
342}
343
344pub fn assembly_scale_basis(
351 document: &Document,
352 plan: &ScalePlan,
353) -> Result<AssemblyScaleBasis, ScaleError> {
354 plan.validate_document_inventory(document)?;
355 let ScaleOperation::RestBindUniformScale {
356 source_skin_index,
357 source_root_node_index,
358 expected_factor,
359 } = plan.operation()
360 else {
361 return Err(ScaleError::PlanDocumentMismatch {
362 reason: "assembly_basis_requires_rest_bind",
363 });
364 };
365 let named_nodes = named_nodes(document)?;
366 let source_by_index = document
367 .assets
368 .source_skeleton
369 .nodes
370 .iter()
371 .map(|node| (node.source_node_index, node))
372 .collect::<std::collections::BTreeMap<_, _>>();
373 let mut source_nodes = Vec::new();
374 for row in plan.ledger().source_topology() {
375 let source = source_by_index.get(&row.source_node_index()).ok_or(
376 ScaleError::PlanDocumentMismatch {
377 reason: "assembly_basis_source_node_missing",
378 },
379 )?;
380 let local_rest = match source.local_rest {
381 SourceNodeLocalRest::Trs {
382 translation,
383 rotation,
384 scale,
385 } => AssemblyScaleSourceRest::Trs {
386 translation_bits: translation.to_array().map(f32::to_bits),
387 rotation_bits: rotation.to_array().map(f32::to_bits),
388 scale_bits: scale.to_array().map(f32::to_bits),
389 },
390 SourceNodeLocalRest::Matrix(matrix) => AssemblyScaleSourceRest::Matrix {
391 matrix_bits: matrix.to_cols_array().map(f32::to_bits),
392 },
393 };
394 let role = match row.kind() {
395 ScaleSourceNodeKind::Projected { role, .. } => match role {
396 ScaleProjectedRole::Root => "projected-root",
397 ScaleProjectedRole::Joint => "projected-joint",
398 ScaleProjectedRole::TransformOnly => "projected-transform-only",
399 },
400 ScaleSourceNodeKind::Connector => "connector",
401 ScaleSourceNodeKind::OutsideDomain { bone: Some(_) } => "outside-projected",
402 ScaleSourceNodeKind::OutsideDomain { bone: None } => "outside-helper",
403 };
404 source_nodes.push(AssemblyScaleSourceNode {
405 source_node_index: row.source_node_index(),
406 parent_source_node_index: row.parent_source_node_index(),
407 name: source.name.clone(),
408 role: role.to_owned(),
409 local_rest,
410 });
411 }
412 let mut target_paths = Vec::new();
413 for (clip_index, clip) in document.clips.iter().enumerate() {
414 for (track_index, track) in clip.tracks.iter().enumerate() {
415 let bone = document
416 .skeleton
417 .bones
418 .get(track.bone)
419 .ok_or(ScaleError::BoneIndexOutOfRange { index: track.bone })?;
420 target_paths.push(AssemblyScaleTargetPath {
421 clip_index,
422 track_index,
423 bone: bone.name.clone(),
424 property: track.property.as_str(),
425 factor_bits: plan
426 .animation_target_factor_unchecked(document, track.bone, track.property)?
427 .to_bits(),
428 });
429 }
430 }
431 Ok(AssemblyScaleBasis {
432 version: ASSEMBLY_SCALE_BASIS_VERSION,
433 coordinate_convention: "right-handed-y-up-metres",
434 tolerance_policy_id: plan.tolerance_policy().id,
435 source_skin_index,
436 source_root_node_index,
437 expected_factor_bits: expected_factor.to_bits(),
438 named_nodes,
439 source_nodes,
440 target_paths,
441 })
442}
443
444pub fn assembly_scale_compatibility_basis(
453 document: &Document,
454 plan: &ScalePlan,
455 selector: AssemblyScaleSelectorRequest<'_>,
456) -> Result<AssemblyScaleCompatibilityBasis, ScaleError> {
457 let basis = assembly_scale_basis(document, plan)?;
460 let selector = match selector {
461 AssemblyScaleSelectorRequest::Indexed => AssemblyScaleSelectorIdentity::Indexed,
462 AssemblyScaleSelectorRequest::Named { root_node_name } => {
463 let resolved = resolve_assembly_scale_named_selector(document, root_node_name)
464 .map_err(|error| ScaleError::PlanDocumentMismatch {
465 reason: match error {
466 AssemblyScaleNamedSelectorResolutionError::RootNotUnique { .. } => {
467 "assembly_basis_named_selector_root_not_unique"
468 }
469 AssemblyScaleNamedSelectorResolutionError::SkinNotUnique { .. } => {
470 "assembly_basis_named_selector_skin_not_unique"
471 }
472 },
473 })?;
474 if resolved.source_root_node_index != basis.source_root_node_index {
475 return Err(ScaleError::PlanDocumentMismatch {
476 reason: "assembly_basis_named_selector_root_disagrees_with_plan",
477 });
478 }
479 let skin = document
480 .assets
481 .source_skeleton
482 .skins
483 .iter()
484 .find(|skin| skin.source_skin_index == resolved.source_skin_index)
485 .ok_or(ScaleError::PlanDocumentMismatch {
486 reason: "assembly_basis_named_selector_skin_not_unique",
487 })?;
488 if resolved.source_skin_index != basis.source_skin_index {
489 return Err(ScaleError::PlanDocumentMismatch {
490 reason: "assembly_basis_named_selector_skin_disagrees_with_plan",
491 });
492 }
493 let source_nodes = document
494 .assets
495 .source_skeleton
496 .nodes
497 .iter()
498 .map(|node| (node.source_node_index, node))
499 .collect::<std::collections::BTreeMap<_, _>>();
500 let skin_joint_names = skin
501 .joint_source_node_indices
502 .iter()
503 .map(|source_index| {
504 source_nodes
505 .get(source_index)
506 .and_then(|node| node.bone)
507 .and_then(|bone| document.skeleton.bones.get(bone))
508 .map(|bone| bone.name.clone())
509 .ok_or(ScaleError::PlanDocumentMismatch {
510 reason: "assembly_basis_named_selector_joint_has_no_name",
511 })
512 })
513 .collect::<Result<Vec<_>, _>>()?;
514 AssemblyScaleSelectorIdentity::Named {
515 root_node_name: root_node_name.to_owned(),
516 skin_joint_names,
517 }
518 }
519 };
520 let mut animation_target_factors = BTreeMap::new();
521 for (bone, named) in document.skeleton.bones.iter().enumerate() {
522 for property in [Property::Translation, Property::Rotation, Property::Scale] {
523 animation_target_factors.insert(
524 (named.name.clone(), property),
525 plan.animation_target_factor_unchecked(document, bone, property)?,
526 );
527 }
528 }
529 let affected_node_names = plan
530 .affected_nodes()
531 .iter()
532 .filter_map(|&bone| document.skeleton.bones.get(bone))
533 .map(|bone| bone.name.clone())
534 .collect();
535 Ok(AssemblyScaleCompatibilityBasis {
536 basis,
537 selector,
538 animation_target_factors,
539 affected_node_names,
540 })
541}
542
543pub fn rebase_assembly_scale_skinless_clip(
564 base: &AssemblyScaleCompatibilityBasis,
565 document: &Document,
566 root_node_name: &str,
567) -> Result<(Document, AssemblyScaleSkinlessClipBasis), AssemblyScaleCompatibilityError> {
568 let AssemblyScaleSelectorIdentity::Named {
569 root_node_name: base_root,
570 skin_joint_names,
571 } = &base.selector
572 else {
573 return Err(AssemblyScaleCompatibilityError {
574 reason: "source-selector-mode",
575 });
576 };
577 if base_root != root_node_name {
578 return Err(AssemblyScaleCompatibilityError {
579 reason: "source-name-selector",
580 });
581 }
582 validate_document_shape(document).map_err(|_| AssemblyScaleCompatibilityError {
583 reason: "skinless-clip-invalid-document",
584 })?;
585 if document.assets.source_skeleton.coverage != SourceSkeletonCoverage::Complete {
586 return Err(AssemblyScaleCompatibilityError {
587 reason: "skinless-clip-source-coverage",
588 });
589 }
590 if !document.assets.source_skeleton.skins.is_empty() {
591 return Err(AssemblyScaleCompatibilityError {
592 reason: "skinless-clip-has-source-skins",
593 });
594 }
595 if !document.assets.instances.is_empty() {
596 return Err(AssemblyScaleCompatibilityError {
597 reason: "skinless-clip-has-mesh-instances",
598 });
599 }
600 let root_matches = document
601 .assets
602 .source_skeleton
603 .nodes
604 .iter()
605 .filter_map(|node| {
606 node.bone
607 .and_then(|bone| document.skeleton.bones.get(bone))
608 .filter(|bone| bone.name == root_node_name)
609 .map(|_| node.source_node_index)
610 })
611 .collect::<Vec<_>>();
612 let [source_root_node_index] = root_matches.as_slice() else {
613 return Err(AssemblyScaleCompatibilityError {
614 reason: "source-root-name-not-unique",
615 });
616 };
617 let input_named_nodes = named_nodes(document).map_err(|_| AssemblyScaleCompatibilityError {
618 reason: "named-topology",
619 })?;
620 let mut relevant_names = skin_joint_names.iter().cloned().collect::<BTreeSet<_>>();
626 relevant_names.insert(base_root.clone());
627 let mut universally_animated_properties = None::<BTreeMap<String, BTreeSet<Property>>>;
628 for clip in &document.clips {
629 let mut clip_properties = BTreeMap::<String, BTreeSet<Property>>::new();
630 for track in &clip.tracks {
631 let bone =
632 document
633 .skeleton
634 .bones
635 .get(track.bone)
636 .ok_or(AssemblyScaleCompatibilityError {
637 reason: "animation-target-bone",
638 })?;
639 if !base
640 .animation_target_factors
641 .contains_key(&(bone.name.clone(), track.property))
642 {
643 return Err(AssemblyScaleCompatibilityError {
644 reason: "animation-target-bone",
645 });
646 }
647 if !base.affected_node_names.contains(&bone.name) {
648 return Err(AssemblyScaleCompatibilityError {
649 reason: "animation-target-outside-scale-domain",
650 });
651 }
652 relevant_names.insert(bone.name.clone());
653 clip_properties
654 .entry(bone.name.clone())
655 .or_default()
656 .insert(track.property);
657 }
658 if let Some(properties) = &mut universally_animated_properties {
659 properties.retain(|name, properties| {
660 let Some(clip_properties) = clip_properties.get(name) else {
661 return false;
662 };
663 properties.retain(|property| clip_properties.contains(property));
664 !properties.is_empty()
665 });
666 } else {
667 universally_animated_properties = Some(clip_properties);
668 }
669 }
670 let universally_animated_properties = universally_animated_properties.unwrap_or_default();
671 loop {
672 let before = relevant_names.len();
673 for node in &base.basis.named_nodes {
674 if relevant_names.contains(&node.name)
675 && let Some(parent) = &node.parent
676 {
677 relevant_names.insert(parent.clone());
678 }
679 }
680 if relevant_names.len() == before {
681 break;
682 }
683 }
684 let base_named_nodes = base
685 .basis
686 .named_nodes
687 .iter()
688 .filter(|node| relevant_names.contains(&node.name))
689 .cloned()
690 .collect::<Vec<_>>();
691 let input_named_nodes = input_named_nodes
692 .into_iter()
693 .filter(|node| relevant_names.contains(&node.name))
694 .collect::<Vec<_>>();
695 let tolerance = ScaleTolerancePolicy::APPENDIX_D_V6;
696 if !same_named_topology(&base_named_nodes, &input_named_nodes) {
697 return Err(AssemblyScaleCompatibilityError {
698 reason: "named-topology",
699 });
700 }
701 if !same_named_rest(
702 &base_named_nodes,
703 &input_named_nodes,
704 &universally_animated_properties,
705 &tolerance,
706 ) {
707 return Err(AssemblyScaleCompatibilityError {
708 reason: "named-rest-basis",
709 });
710 }
711 if !same_named_orientations(
712 &base_named_nodes,
713 &input_named_nodes,
714 &universally_animated_properties,
715 &tolerance,
716 ) {
717 return Err(AssemblyScaleCompatibilityError {
718 reason: "named-orientation",
719 });
720 }
721
722 let mut rebased = document.clone();
723 let mut target_paths = Vec::new();
724 for (clip_index, clip) in rebased.clips.iter_mut().enumerate() {
725 for (track_index, track) in clip.tracks.iter_mut().enumerate() {
726 let bone =
727 document
728 .skeleton
729 .bones
730 .get(track.bone)
731 .ok_or(AssemblyScaleCompatibilityError {
732 reason: "animation-target-bone",
733 })?;
734 let factor = *base
735 .animation_target_factors
736 .get(&(bone.name.clone(), track.property))
737 .ok_or(AssemblyScaleCompatibilityError {
738 reason: "animation-target-bone",
739 })?;
740 match (&mut track.values, track.property) {
741 (TrackValues::Vec3s(values), Property::Translation) => {
742 let factor = factor as f32;
743 for value in values {
744 *value *= factor;
745 }
746 }
747 (TrackValues::Vec3s(values), Property::Scale) => {
748 for value in values {
749 *value = (value.as_dvec3() * factor).as_vec3();
750 }
751 }
752 (TrackValues::Quats(_), Property::Rotation) => {}
753 _ => {
754 return Err(AssemblyScaleCompatibilityError {
755 reason: "animation-value-kind",
756 });
757 }
758 }
759 target_paths.push(AssemblyScaleTargetPath {
760 clip_index,
761 track_index,
762 bone: bone.name.clone(),
763 property: track.property.as_str(),
764 factor_bits: factor.to_bits(),
765 });
766 }
767 }
768 validate_document_shape(&rebased).map_err(|_| AssemblyScaleCompatibilityError {
769 reason: "skinless-clip-rebase-invalid-document",
770 })?;
771 Ok((
772 rebased,
773 AssemblyScaleSkinlessClipBasis {
774 version: ASSEMBLY_SCALE_BASIS_VERSION,
775 coordinate_convention: base.basis.coordinate_convention,
776 tolerance_policy_id: base.basis.tolerance_policy_id,
777 root_node_name: root_node_name.to_owned(),
778 source_root_node_index: *source_root_node_index,
779 expected_factor_bits: base.basis.expected_factor_bits,
780 named_nodes: input_named_nodes,
781 target_paths,
782 },
783 ))
784}
785
786pub fn require_assembly_scale_compatibility(
796 base: &AssemblyScaleBasis,
797 input: &AssemblyScaleBasis,
798) -> Result<(), AssemblyScaleCompatibilityError> {
799 require_assembly_scale_compatibility_inner(
800 base,
801 &AssemblyScaleSelectorIdentity::Indexed,
802 input,
803 &AssemblyScaleSelectorIdentity::Indexed,
804 )
805}
806
807pub fn require_assembly_scale_compatibility_with_selectors(
814 base: &AssemblyScaleCompatibilityBasis,
815 input: &AssemblyScaleCompatibilityBasis,
816) -> Result<(), AssemblyScaleCompatibilityError> {
817 require_assembly_scale_compatibility_inner(
818 &base.basis,
819 &base.selector,
820 &input.basis,
821 &input.selector,
822 )
823}
824
825fn require_assembly_scale_compatibility_inner(
826 base: &AssemblyScaleBasis,
827 base_selector: &AssemblyScaleSelectorIdentity,
828 input: &AssemblyScaleBasis,
829 input_selector: &AssemblyScaleSelectorIdentity,
830) -> Result<(), AssemblyScaleCompatibilityError> {
831 let tolerance = ScaleTolerancePolicy::APPENDIX_D_V6;
832 let no_rest_waivers = BTreeMap::new();
833 let named_selectors = match (base_selector, input_selector) {
834 (AssemblyScaleSelectorIdentity::Indexed, AssemblyScaleSelectorIdentity::Indexed) => None,
835 (
836 AssemblyScaleSelectorIdentity::Named {
837 root_node_name: base_root,
838 skin_joint_names: base_joints,
839 },
840 AssemblyScaleSelectorIdentity::Named {
841 root_node_name: input_root,
842 skin_joint_names: input_joints,
843 },
844 ) => Some((base_root, base_joints, input_root, input_joints)),
845 _ => {
846 return Err(AssemblyScaleCompatibilityError {
847 reason: "source-selector-mode",
848 });
849 }
850 };
851 let mismatch = if base.version != input.version {
852 Some("basis-version")
853 } else if base.coordinate_convention != input.coordinate_convention {
854 Some("coordinate-convention")
855 } else if base.tolerance_policy_id != input.tolerance_policy_id
856 || base.tolerance_policy_id != tolerance.id
857 {
858 Some("tolerance-policy")
859 } else if named_selectors.is_none() && base.source_skin_index != input.source_skin_index {
860 Some("source-skin-selector")
861 } else if named_selectors.is_none()
862 && base.source_root_node_index != input.source_root_node_index
863 {
864 Some("source-root-selector")
865 } else if named_selectors.is_some_and(|(base_root, base_joints, input_root, input_joints)| {
866 base_root != input_root || base_joints != input_joints
867 }) {
868 Some("source-name-selector")
869 } else if base.expected_factor_bits != input.expected_factor_bits {
870 Some("expected-factor")
871 } else if !same_named_topology(&base.named_nodes, &input.named_nodes) {
872 Some("named-topology")
873 } else if !same_named_rest(
874 &base.named_nodes,
875 &input.named_nodes,
876 &no_rest_waivers,
877 &tolerance,
878 ) {
879 Some("named-rest-basis")
880 } else if !same_named_orientations(
881 &base.named_nodes,
882 &input.named_nodes,
883 &no_rest_waivers,
884 &tolerance,
885 ) {
886 Some("named-orientation")
887 } else if (named_selectors.is_some()
888 && !same_named_source_layout(&base.source_nodes, &input.source_nodes))
889 || (named_selectors.is_none()
890 && !same_source_layout(&base.source_nodes, &input.source_nodes))
891 {
892 Some("source-helper-layout")
893 } else if (named_selectors.is_some()
894 && !same_named_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
895 || (named_selectors.is_none()
896 && !same_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
897 {
898 Some("source-helper-rest-basis")
899 } else {
900 None
901 };
902 mismatch.map_or(Ok(()), |reason| {
903 Err(AssemblyScaleCompatibilityError { reason })
904 })
905}
906
907fn same_named_topology(base: &[AssemblyScaleNamedNode], input: &[AssemblyScaleNamedNode]) -> bool {
908 base.len() == input.len()
909 && base
910 .iter()
911 .zip(input)
912 .all(|(base, input)| base.name == input.name && base.parent == input.parent)
913}
914
915fn same_named_rest(
916 base: &[AssemblyScaleNamedNode],
917 input: &[AssemblyScaleNamedNode],
918 universally_animated_properties: &BTreeMap<String, BTreeSet<Property>>,
919 tolerance: &ScaleTolerancePolicy,
920) -> bool {
921 base.iter().zip(input).all(|(base, input)| {
922 (close_f32_bits(&base.translation_bits, &input.translation_bits, tolerance)
923 || named_property_is_universally_animated(
924 universally_animated_properties,
925 &base.name,
926 Property::Translation,
927 ))
928 && (close_f32_bits(&base.scale_bits, &input.scale_bits, tolerance)
929 || named_property_is_universally_animated(
930 universally_animated_properties,
931 &base.name,
932 Property::Scale,
933 ))
934 })
935}
936
937fn same_named_orientations(
938 base: &[AssemblyScaleNamedNode],
939 input: &[AssemblyScaleNamedNode],
940 universally_animated_properties: &BTreeMap<String, BTreeSet<Property>>,
941 tolerance: &ScaleTolerancePolicy,
942) -> bool {
943 base.iter().zip(input).all(|(base, input)| {
944 same_quaternion(&base.rotation_bits, &input.rotation_bits, tolerance)
945 || named_property_is_universally_animated(
946 universally_animated_properties,
947 &base.name,
948 Property::Rotation,
949 )
950 })
951}
952
953fn named_property_is_universally_animated(
954 universally_animated_properties: &BTreeMap<String, BTreeSet<Property>>,
955 name: &str,
956 property: Property,
957) -> bool {
958 universally_animated_properties
959 .get(name)
960 .is_some_and(|properties| properties.contains(&property))
961}
962
963fn same_source_layout(base: &[AssemblyScaleSourceNode], input: &[AssemblyScaleSourceNode]) -> bool {
964 base.len() == input.len()
965 && base.iter().zip(input).all(|(base, input)| {
966 base.source_node_index == input.source_node_index
967 && base.parent_source_node_index == input.parent_source_node_index
968 && base.name == input.name
969 && base.role == input.role
970 && std::mem::discriminant(&base.local_rest)
971 == std::mem::discriminant(&input.local_rest)
972 })
973}
974
975#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
976struct NamedSourcePath(Vec<(Option<String>, String, bool)>);
977
978fn named_source_paths(nodes: &[AssemblyScaleSourceNode]) -> Option<Vec<NamedSourcePath>> {
979 let by_index = nodes
980 .iter()
981 .enumerate()
982 .map(|(position, node)| (node.source_node_index, position))
983 .collect::<std::collections::BTreeMap<_, _>>();
984 nodes
985 .iter()
986 .map(|node| {
987 let mut path = Vec::new();
988 let mut current = Some(node.source_node_index);
989 for _ in 0..=nodes.len() {
990 let Some(index) = current else {
991 path.reverse();
992 return Some(NamedSourcePath(path));
993 };
994 let row = nodes.get(*by_index.get(&index)?)?;
995 path.push((
996 row.name.clone(),
997 row.role.clone(),
998 matches!(&row.local_rest, AssemblyScaleSourceRest::Matrix { .. }),
999 ));
1000 current = row.parent_source_node_index;
1001 }
1002 None
1003 })
1004 .collect()
1005}
1006
1007fn same_named_source_layout(
1008 base: &[AssemblyScaleSourceNode],
1009 input: &[AssemblyScaleSourceNode],
1010) -> bool {
1011 let (Some(mut base), Some(mut input)) = (named_source_paths(base), named_source_paths(input))
1012 else {
1013 return false;
1014 };
1015 base.sort();
1016 input.sort();
1017 base == input
1018}
1019
1020fn same_named_source_rest(
1021 base: &[AssemblyScaleSourceNode],
1022 input: &[AssemblyScaleSourceNode],
1023 tolerance: &ScaleTolerancePolicy,
1024) -> bool {
1025 let (Some(base_paths), Some(input_paths)) =
1026 (named_source_paths(base), named_source_paths(input))
1027 else {
1028 return false;
1029 };
1030 let mut matched = vec![false; input.len()];
1031 base.iter().zip(base_paths).all(|(base_node, base_path)| {
1032 input
1033 .iter()
1034 .zip(&input_paths)
1035 .enumerate()
1036 .find(|(index, (input_node, input_path))| {
1037 !matched[*index]
1038 && **input_path == base_path
1039 && same_source_rest_node(base_node, input_node, tolerance)
1040 })
1041 .is_some_and(|(index, _)| {
1042 matched[index] = true;
1043 true
1044 })
1045 })
1046}
1047
1048fn same_source_rest_node(
1049 base: &AssemblyScaleSourceNode,
1050 input: &AssemblyScaleSourceNode,
1051 tolerance: &ScaleTolerancePolicy,
1052) -> bool {
1053 same_source_rest(
1054 std::slice::from_ref(base),
1055 std::slice::from_ref(input),
1056 tolerance,
1057 )
1058}
1059
1060fn same_source_rest(
1061 base: &[AssemblyScaleSourceNode],
1062 input: &[AssemblyScaleSourceNode],
1063 tolerance: &ScaleTolerancePolicy,
1064) -> bool {
1065 base.iter().zip(input).all(
1066 |(base, input)| match (&base.local_rest, &input.local_rest) {
1067 (
1068 AssemblyScaleSourceRest::Trs {
1069 translation_bits: base_translation,
1070 rotation_bits: base_rotation,
1071 scale_bits: base_scale,
1072 },
1073 AssemblyScaleSourceRest::Trs {
1074 translation_bits: input_translation,
1075 rotation_bits: input_rotation,
1076 scale_bits: input_scale,
1077 },
1078 ) => {
1079 close_f32_bits(base_translation, input_translation, tolerance)
1080 && close_f32_bits(base_scale, input_scale, tolerance)
1081 && same_quaternion(base_rotation, input_rotation, tolerance)
1082 }
1083 (
1084 AssemblyScaleSourceRest::Matrix {
1085 matrix_bits: base_matrix,
1086 },
1087 AssemblyScaleSourceRest::Matrix {
1088 matrix_bits: input_matrix,
1089 },
1090 ) => close_f32_bits(base_matrix, input_matrix, tolerance),
1091 _ => false,
1092 },
1093 )
1094}
1095
1096fn close_f32_bits<const N: usize>(
1097 base: &[u32; N],
1098 input: &[u32; N],
1099 tolerance: &ScaleTolerancePolicy,
1100) -> bool {
1101 base.iter().zip(input).all(|(&base, &input)| {
1102 close_f64(
1103 f32::from_bits(base) as f64,
1104 f32::from_bits(input) as f64,
1105 tolerance,
1106 )
1107 })
1108}
1109
1110fn close_f64(base: f64, input: f64, tolerance: &ScaleTolerancePolicy) -> bool {
1111 base.is_finite()
1112 && input.is_finite()
1113 && (base - input).abs()
1114 <= tolerance.scalar_absolute + tolerance.scalar_relative * base.abs().max(input.abs())
1115}
1116
1117fn same_quaternion(base: &[u32; 4], input: &[u32; 4], tolerance: &ScaleTolerancePolicy) -> bool {
1118 let base = base.map(|bits| f32::from_bits(bits) as f64);
1119 let input = input.map(|bits| f32::from_bits(bits) as f64);
1120 if !base
1121 .iter()
1122 .chain(input.iter())
1123 .all(|value| value.is_finite())
1124 {
1125 return false;
1126 }
1127 let base_norm = base.iter().map(|value| value * value).sum::<f64>().sqrt();
1128 let input_norm = input.iter().map(|value| value * value).sum::<f64>().sqrt();
1129 if base_norm == 0.0 || input_norm == 0.0 {
1130 return false;
1131 }
1132 let dot = base
1133 .iter()
1134 .zip(input)
1135 .map(|(base, input)| base * input)
1136 .sum::<f64>()
1137 / (base_norm * input_norm);
1138 2.0 * dot.abs().clamp(-1.0, 1.0).acos() <= tolerance.rotation_residual_radians
1139}