1use super::{
4 ScaleError, ScaleOperation, ScalePlan, ScaleProjectedRole, ScaleSourceNodeKind,
5 ScaleTolerancePolicy,
6};
7use crate::model::{Document, SourceNodeLocalRest};
8use serde::Serialize;
9use std::collections::{BTreeMap, BTreeSet};
10
11pub const ASSEMBLY_SCALE_BASIS_VERSION: u32 = 1;
13
14#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct AssemblyScaleNamedNode {
17 pub name: String,
19 pub parent: Option<String>,
21 pub translation_bits: [u32; 3],
23 pub rotation_bits: [u32; 4],
25 pub scale_bits: [u32; 3],
27}
28
29#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
31pub struct AssemblyScaleSourceNode {
32 pub source_node_index: usize,
34 pub parent_source_node_index: Option<usize>,
36 pub name: Option<String>,
38 pub role: String,
40 pub local_rest: AssemblyScaleSourceRest,
42}
43
44#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46#[serde(tag = "kind", rename_all = "snake_case")]
47pub enum AssemblyScaleSourceRest {
48 Trs {
50 translation_bits: [u32; 3],
52 rotation_bits: [u32; 4],
54 scale_bits: [u32; 3],
56 },
57 Matrix {
59 matrix_bits: [u32; 16],
61 },
62}
63
64#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66pub struct AssemblyScaleTargetPath {
67 pub clip_index: usize,
69 pub track_index: usize,
71 pub bone: String,
73 pub property: &'static str,
75 pub factor_bits: u64,
77}
78
79#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81pub struct AssemblyScaleBasis {
82 pub version: u32,
84 pub coordinate_convention: &'static str,
86 pub tolerance_policy_id: &'static str,
88 pub source_skin_index: usize,
90 pub source_root_node_index: usize,
92 pub expected_factor_bits: u64,
94 pub named_nodes: Vec<AssemblyScaleNamedNode>,
96 pub source_nodes: Vec<AssemblyScaleSourceNode>,
98 pub target_paths: Vec<AssemblyScaleTargetPath>,
100}
101
102#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
104#[error("assembly scale basis mismatch ({reason})")]
105pub struct AssemblyScaleCompatibilityError {
106 pub reason: &'static str,
108}
109
110#[derive(Debug, Clone, Copy)]
116#[non_exhaustive]
117pub enum AssemblyScaleSelectorRequest<'a> {
118 Indexed,
120 Named {
122 root_node_name: &'a str,
124 },
125}
126
127#[derive(Debug, Clone, Copy, PartialEq, Eq)]
129pub struct AssemblyScaleResolvedNamedSelector {
130 pub source_skin_index: usize,
132 pub source_root_node_index: usize,
134}
135
136#[derive(Debug, Clone, Copy, PartialEq, Eq, thiserror::Error)]
138#[non_exhaustive]
139pub enum AssemblyScaleNamedSelectorResolutionError {
140 #[error("named assembly scale root resolves to {matches} source nodes; expected exactly one")]
142 RootNotUnique {
143 matches: usize,
145 },
146 #[error("named assembly scale root fully governs {matches} source skins; expected exactly one")]
148 SkinNotUnique {
149 matches: usize,
151 },
152}
153
154#[derive(Debug, Clone)]
155enum AssemblyScaleSelectorIdentity {
156 Indexed,
157 Named {
158 root_node_name: String,
159 skin_joint_names: Vec<String>,
160 },
161}
162
163#[derive(Debug, Clone)]
168pub struct AssemblyScaleCompatibilityBasis {
169 basis: AssemblyScaleBasis,
170 selector: AssemblyScaleSelectorIdentity,
171}
172
173fn governed_source_node_indices(
174 parent_by_index: &BTreeMap<usize, Option<usize>>,
175 source_root_node_index: usize,
176) -> BTreeSet<usize> {
177 if !parent_by_index.contains_key(&source_root_node_index) {
178 return BTreeSet::new();
179 }
180
181 let mut children_by_index = BTreeMap::<usize, Vec<usize>>::new();
182 for (&source_node_index, parent_source_node_index) in parent_by_index {
183 if let Some(parent_source_node_index) = parent_source_node_index {
184 children_by_index
185 .entry(*parent_source_node_index)
186 .or_default()
187 .push(source_node_index);
188 }
189 }
190
191 let mut governed = BTreeSet::new();
192 let mut pending = vec![source_root_node_index];
193 while let Some(source_node_index) = pending.pop() {
194 if governed.insert(source_node_index)
195 && let Some(children) = children_by_index.get(&source_node_index)
196 {
197 pending.extend(children.iter().copied());
198 }
199 }
200 governed
201}
202
203fn source_skin_is_fully_governed(
204 governed_source_node_indices: &BTreeSet<usize>,
205 joint_source_node_indices: &[usize],
206) -> bool {
207 !joint_source_node_indices.is_empty()
208 && joint_source_node_indices
209 .iter()
210 .all(|joint| governed_source_node_indices.contains(joint))
211}
212
213pub fn resolve_assembly_scale_named_selector(
227 document: &Document,
228 root_node_name: &str,
229) -> Result<AssemblyScaleResolvedNamedSelector, AssemblyScaleNamedSelectorResolutionError> {
230 let root_matches = document
231 .assets
232 .source_skeleton
233 .nodes
234 .iter()
235 .filter_map(|node| {
236 node.bone
237 .and_then(|bone| document.skeleton.bones.get(bone))
238 .filter(|bone| bone.name == root_node_name)
239 .map(|_| node.source_node_index)
240 })
241 .collect::<Vec<_>>();
242 let [source_root_node_index] = root_matches.as_slice() else {
243 return Err(AssemblyScaleNamedSelectorResolutionError::RootNotUnique {
244 matches: root_matches.len(),
245 });
246 };
247 let parent_by_index = document
248 .assets
249 .source_skeleton
250 .nodes
251 .iter()
252 .map(|node| (node.source_node_index, node.parent_source_node_index))
253 .collect::<BTreeMap<_, _>>();
254 let governed_source_node_indices =
255 governed_source_node_indices(&parent_by_index, *source_root_node_index);
256 let skin_matches = document
257 .assets
258 .source_skeleton
259 .skins
260 .iter()
261 .filter(|skin| {
262 source_skin_is_fully_governed(
263 &governed_source_node_indices,
264 &skin.joint_source_node_indices,
265 )
266 })
267 .collect::<Vec<_>>();
268 let [skin] = skin_matches.as_slice() else {
269 return Err(AssemblyScaleNamedSelectorResolutionError::SkinNotUnique {
270 matches: skin_matches.len(),
271 });
272 };
273 Ok(AssemblyScaleResolvedNamedSelector {
274 source_skin_index: skin.source_skin_index,
275 source_root_node_index: *source_root_node_index,
276 })
277}
278
279impl AssemblyScaleCompatibilityBasis {
280 #[must_use]
282 pub fn basis(&self) -> &AssemblyScaleBasis {
283 &self.basis
284 }
285}
286
287pub fn assembly_scale_basis(
294 document: &Document,
295 plan: &ScalePlan,
296) -> Result<AssemblyScaleBasis, ScaleError> {
297 plan.validate_document_inventory(document)?;
298 let ScaleOperation::RestBindUniformScale {
299 source_skin_index,
300 source_root_node_index,
301 expected_factor,
302 } = plan.operation()
303 else {
304 return Err(ScaleError::PlanDocumentMismatch {
305 reason: "assembly_basis_requires_rest_bind",
306 });
307 };
308 let mut names = BTreeSet::new();
309 let mut named_nodes = Vec::with_capacity(document.skeleton.bones.len());
310 for (index, bone) in document.skeleton.bones.iter().enumerate() {
311 if bone.name.is_empty() || !names.insert(bone.name.as_str()) {
312 return Err(ScaleError::PlanDocumentMismatch {
313 reason: "assembly_basis_requires_unique_named_nodes",
314 });
315 }
316 named_nodes.push(AssemblyScaleNamedNode {
317 name: bone.name.clone(),
318 parent: bone
319 .parent
320 .and_then(|parent| document.skeleton.bones.get(parent))
321 .map(|parent| parent.name.clone()),
322 translation_bits: bone.rest.translation.to_array().map(f32::to_bits),
323 rotation_bits: bone.rest.rotation.to_array().map(f32::to_bits),
324 scale_bits: bone.rest.scale.to_array().map(f32::to_bits),
325 });
326 if bone.parent.is_some_and(|parent| parent >= index) {
327 return Err(ScaleError::PlanDocumentMismatch {
328 reason: "assembly_basis_parent_order",
329 });
330 }
331 }
332 let source_by_index = document
333 .assets
334 .source_skeleton
335 .nodes
336 .iter()
337 .map(|node| (node.source_node_index, node))
338 .collect::<std::collections::BTreeMap<_, _>>();
339 let mut source_nodes = Vec::new();
340 for row in plan.ledger().source_topology() {
341 let source = source_by_index.get(&row.source_node_index()).ok_or(
342 ScaleError::PlanDocumentMismatch {
343 reason: "assembly_basis_source_node_missing",
344 },
345 )?;
346 let local_rest = match source.local_rest {
347 SourceNodeLocalRest::Trs {
348 translation,
349 rotation,
350 scale,
351 } => AssemblyScaleSourceRest::Trs {
352 translation_bits: translation.to_array().map(f32::to_bits),
353 rotation_bits: rotation.to_array().map(f32::to_bits),
354 scale_bits: scale.to_array().map(f32::to_bits),
355 },
356 SourceNodeLocalRest::Matrix(matrix) => AssemblyScaleSourceRest::Matrix {
357 matrix_bits: matrix.to_cols_array().map(f32::to_bits),
358 },
359 };
360 let role = match row.kind() {
361 ScaleSourceNodeKind::Projected { role, .. } => match role {
362 ScaleProjectedRole::Root => "projected-root",
363 ScaleProjectedRole::Joint => "projected-joint",
364 ScaleProjectedRole::TransformOnly => "projected-transform-only",
365 },
366 ScaleSourceNodeKind::Connector => "connector",
367 ScaleSourceNodeKind::OutsideDomain { bone: Some(_) } => "outside-projected",
368 ScaleSourceNodeKind::OutsideDomain { bone: None } => "outside-helper",
369 };
370 source_nodes.push(AssemblyScaleSourceNode {
371 source_node_index: row.source_node_index(),
372 parent_source_node_index: row.parent_source_node_index(),
373 name: source.name.clone(),
374 role: role.to_owned(),
375 local_rest,
376 });
377 }
378 let mut target_paths = Vec::new();
379 for (clip_index, clip) in document.clips.iter().enumerate() {
380 for (track_index, track) in clip.tracks.iter().enumerate() {
381 let bone = document
382 .skeleton
383 .bones
384 .get(track.bone)
385 .ok_or(ScaleError::BoneIndexOutOfRange { index: track.bone })?;
386 target_paths.push(AssemblyScaleTargetPath {
387 clip_index,
388 track_index,
389 bone: bone.name.clone(),
390 property: track.property.as_str(),
391 factor_bits: plan
392 .animation_target_factor_unchecked(document, track.bone, track.property)?
393 .to_bits(),
394 });
395 }
396 }
397 Ok(AssemblyScaleBasis {
398 version: ASSEMBLY_SCALE_BASIS_VERSION,
399 coordinate_convention: "right-handed-y-up-metres",
400 tolerance_policy_id: plan.tolerance_policy().id,
401 source_skin_index,
402 source_root_node_index,
403 expected_factor_bits: expected_factor.to_bits(),
404 named_nodes,
405 source_nodes,
406 target_paths,
407 })
408}
409
410pub fn assembly_scale_compatibility_basis(
419 document: &Document,
420 plan: &ScalePlan,
421 selector: AssemblyScaleSelectorRequest<'_>,
422) -> Result<AssemblyScaleCompatibilityBasis, ScaleError> {
423 let basis = assembly_scale_basis(document, plan)?;
426 let selector = match selector {
427 AssemblyScaleSelectorRequest::Indexed => AssemblyScaleSelectorIdentity::Indexed,
428 AssemblyScaleSelectorRequest::Named { root_node_name } => {
429 let resolved = resolve_assembly_scale_named_selector(document, root_node_name)
430 .map_err(|error| ScaleError::PlanDocumentMismatch {
431 reason: match error {
432 AssemblyScaleNamedSelectorResolutionError::RootNotUnique { .. } => {
433 "assembly_basis_named_selector_root_not_unique"
434 }
435 AssemblyScaleNamedSelectorResolutionError::SkinNotUnique { .. } => {
436 "assembly_basis_named_selector_skin_not_unique"
437 }
438 },
439 })?;
440 if resolved.source_root_node_index != basis.source_root_node_index {
441 return Err(ScaleError::PlanDocumentMismatch {
442 reason: "assembly_basis_named_selector_root_disagrees_with_plan",
443 });
444 }
445 let skin = document
446 .assets
447 .source_skeleton
448 .skins
449 .iter()
450 .find(|skin| skin.source_skin_index == resolved.source_skin_index)
451 .ok_or(ScaleError::PlanDocumentMismatch {
452 reason: "assembly_basis_named_selector_skin_not_unique",
453 })?;
454 if resolved.source_skin_index != basis.source_skin_index {
455 return Err(ScaleError::PlanDocumentMismatch {
456 reason: "assembly_basis_named_selector_skin_disagrees_with_plan",
457 });
458 }
459 let source_nodes = document
460 .assets
461 .source_skeleton
462 .nodes
463 .iter()
464 .map(|node| (node.source_node_index, node))
465 .collect::<std::collections::BTreeMap<_, _>>();
466 let skin_joint_names = skin
467 .joint_source_node_indices
468 .iter()
469 .map(|source_index| {
470 source_nodes
471 .get(source_index)
472 .and_then(|node| node.bone)
473 .and_then(|bone| document.skeleton.bones.get(bone))
474 .map(|bone| bone.name.clone())
475 .ok_or(ScaleError::PlanDocumentMismatch {
476 reason: "assembly_basis_named_selector_joint_has_no_name",
477 })
478 })
479 .collect::<Result<Vec<_>, _>>()?;
480 AssemblyScaleSelectorIdentity::Named {
481 root_node_name: root_node_name.to_owned(),
482 skin_joint_names,
483 }
484 }
485 };
486 Ok(AssemblyScaleCompatibilityBasis { basis, selector })
487}
488
489pub fn require_assembly_scale_compatibility(
499 base: &AssemblyScaleBasis,
500 input: &AssemblyScaleBasis,
501) -> Result<(), AssemblyScaleCompatibilityError> {
502 require_assembly_scale_compatibility_inner(
503 base,
504 &AssemblyScaleSelectorIdentity::Indexed,
505 input,
506 &AssemblyScaleSelectorIdentity::Indexed,
507 )
508}
509
510pub fn require_assembly_scale_compatibility_with_selectors(
517 base: &AssemblyScaleCompatibilityBasis,
518 input: &AssemblyScaleCompatibilityBasis,
519) -> Result<(), AssemblyScaleCompatibilityError> {
520 require_assembly_scale_compatibility_inner(
521 &base.basis,
522 &base.selector,
523 &input.basis,
524 &input.selector,
525 )
526}
527
528fn require_assembly_scale_compatibility_inner(
529 base: &AssemblyScaleBasis,
530 base_selector: &AssemblyScaleSelectorIdentity,
531 input: &AssemblyScaleBasis,
532 input_selector: &AssemblyScaleSelectorIdentity,
533) -> Result<(), AssemblyScaleCompatibilityError> {
534 let tolerance = ScaleTolerancePolicy::APPENDIX_D_V6;
535 let named_selectors = match (base_selector, input_selector) {
536 (AssemblyScaleSelectorIdentity::Indexed, AssemblyScaleSelectorIdentity::Indexed) => None,
537 (
538 AssemblyScaleSelectorIdentity::Named {
539 root_node_name: base_root,
540 skin_joint_names: base_joints,
541 },
542 AssemblyScaleSelectorIdentity::Named {
543 root_node_name: input_root,
544 skin_joint_names: input_joints,
545 },
546 ) => Some((base_root, base_joints, input_root, input_joints)),
547 _ => {
548 return Err(AssemblyScaleCompatibilityError {
549 reason: "source-selector-mode",
550 });
551 }
552 };
553 let mismatch = if base.version != input.version {
554 Some("basis-version")
555 } else if base.coordinate_convention != input.coordinate_convention {
556 Some("coordinate-convention")
557 } else if base.tolerance_policy_id != input.tolerance_policy_id
558 || base.tolerance_policy_id != tolerance.id
559 {
560 Some("tolerance-policy")
561 } else if named_selectors.is_none() && base.source_skin_index != input.source_skin_index {
562 Some("source-skin-selector")
563 } else if named_selectors.is_none()
564 && base.source_root_node_index != input.source_root_node_index
565 {
566 Some("source-root-selector")
567 } else if named_selectors.is_some_and(|(base_root, base_joints, input_root, input_joints)| {
568 base_root != input_root || base_joints != input_joints
569 }) {
570 Some("source-name-selector")
571 } else if base.expected_factor_bits != input.expected_factor_bits {
572 Some("expected-factor")
573 } else if !same_named_topology(&base.named_nodes, &input.named_nodes) {
574 Some("named-topology")
575 } else if !same_named_rest(&base.named_nodes, &input.named_nodes, &tolerance) {
576 Some("named-rest-basis")
577 } else if !same_named_orientations(&base.named_nodes, &input.named_nodes, &tolerance) {
578 Some("named-orientation")
579 } else if (named_selectors.is_some()
580 && !same_named_source_layout(&base.source_nodes, &input.source_nodes))
581 || (named_selectors.is_none()
582 && !same_source_layout(&base.source_nodes, &input.source_nodes))
583 {
584 Some("source-helper-layout")
585 } else if (named_selectors.is_some()
586 && !same_named_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
587 || (named_selectors.is_none()
588 && !same_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
589 {
590 Some("source-helper-rest-basis")
591 } else {
592 None
593 };
594 mismatch.map_or(Ok(()), |reason| {
595 Err(AssemblyScaleCompatibilityError { reason })
596 })
597}
598
599fn same_named_topology(base: &[AssemblyScaleNamedNode], input: &[AssemblyScaleNamedNode]) -> bool {
600 base.len() == input.len()
601 && base
602 .iter()
603 .zip(input)
604 .all(|(base, input)| base.name == input.name && base.parent == input.parent)
605}
606
607fn same_named_rest(
608 base: &[AssemblyScaleNamedNode],
609 input: &[AssemblyScaleNamedNode],
610 tolerance: &ScaleTolerancePolicy,
611) -> bool {
612 base.iter().zip(input).all(|(base, input)| {
613 close_f32_bits(&base.translation_bits, &input.translation_bits, tolerance)
614 && close_f32_bits(&base.scale_bits, &input.scale_bits, tolerance)
615 })
616}
617
618fn same_named_orientations(
619 base: &[AssemblyScaleNamedNode],
620 input: &[AssemblyScaleNamedNode],
621 tolerance: &ScaleTolerancePolicy,
622) -> bool {
623 base.iter()
624 .zip(input)
625 .all(|(base, input)| same_quaternion(&base.rotation_bits, &input.rotation_bits, tolerance))
626}
627
628fn same_source_layout(base: &[AssemblyScaleSourceNode], input: &[AssemblyScaleSourceNode]) -> bool {
629 base.len() == input.len()
630 && base.iter().zip(input).all(|(base, input)| {
631 base.source_node_index == input.source_node_index
632 && base.parent_source_node_index == input.parent_source_node_index
633 && base.name == input.name
634 && base.role == input.role
635 && std::mem::discriminant(&base.local_rest)
636 == std::mem::discriminant(&input.local_rest)
637 })
638}
639
640#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
641struct NamedSourcePath(Vec<(Option<String>, String, bool)>);
642
643fn named_source_paths(nodes: &[AssemblyScaleSourceNode]) -> Option<Vec<NamedSourcePath>> {
644 let by_index = nodes
645 .iter()
646 .enumerate()
647 .map(|(position, node)| (node.source_node_index, position))
648 .collect::<std::collections::BTreeMap<_, _>>();
649 nodes
650 .iter()
651 .map(|node| {
652 let mut path = Vec::new();
653 let mut current = Some(node.source_node_index);
654 for _ in 0..=nodes.len() {
655 let Some(index) = current else {
656 path.reverse();
657 return Some(NamedSourcePath(path));
658 };
659 let row = nodes.get(*by_index.get(&index)?)?;
660 path.push((
661 row.name.clone(),
662 row.role.clone(),
663 matches!(&row.local_rest, AssemblyScaleSourceRest::Matrix { .. }),
664 ));
665 current = row.parent_source_node_index;
666 }
667 None
668 })
669 .collect()
670}
671
672fn same_named_source_layout(
673 base: &[AssemblyScaleSourceNode],
674 input: &[AssemblyScaleSourceNode],
675) -> bool {
676 let (Some(mut base), Some(mut input)) = (named_source_paths(base), named_source_paths(input))
677 else {
678 return false;
679 };
680 base.sort();
681 input.sort();
682 base == input
683}
684
685fn same_named_source_rest(
686 base: &[AssemblyScaleSourceNode],
687 input: &[AssemblyScaleSourceNode],
688 tolerance: &ScaleTolerancePolicy,
689) -> bool {
690 let (Some(base_paths), Some(input_paths)) =
691 (named_source_paths(base), named_source_paths(input))
692 else {
693 return false;
694 };
695 let mut matched = vec![false; input.len()];
696 base.iter().zip(base_paths).all(|(base_node, base_path)| {
697 input
698 .iter()
699 .zip(&input_paths)
700 .enumerate()
701 .find(|(index, (input_node, input_path))| {
702 !matched[*index]
703 && **input_path == base_path
704 && same_source_rest_node(base_node, input_node, tolerance)
705 })
706 .is_some_and(|(index, _)| {
707 matched[index] = true;
708 true
709 })
710 })
711}
712
713fn same_source_rest_node(
714 base: &AssemblyScaleSourceNode,
715 input: &AssemblyScaleSourceNode,
716 tolerance: &ScaleTolerancePolicy,
717) -> bool {
718 same_source_rest(
719 std::slice::from_ref(base),
720 std::slice::from_ref(input),
721 tolerance,
722 )
723}
724
725fn same_source_rest(
726 base: &[AssemblyScaleSourceNode],
727 input: &[AssemblyScaleSourceNode],
728 tolerance: &ScaleTolerancePolicy,
729) -> bool {
730 base.iter().zip(input).all(
731 |(base, input)| match (&base.local_rest, &input.local_rest) {
732 (
733 AssemblyScaleSourceRest::Trs {
734 translation_bits: base_translation,
735 rotation_bits: base_rotation,
736 scale_bits: base_scale,
737 },
738 AssemblyScaleSourceRest::Trs {
739 translation_bits: input_translation,
740 rotation_bits: input_rotation,
741 scale_bits: input_scale,
742 },
743 ) => {
744 close_f32_bits(base_translation, input_translation, tolerance)
745 && close_f32_bits(base_scale, input_scale, tolerance)
746 && same_quaternion(base_rotation, input_rotation, tolerance)
747 }
748 (
749 AssemblyScaleSourceRest::Matrix {
750 matrix_bits: base_matrix,
751 },
752 AssemblyScaleSourceRest::Matrix {
753 matrix_bits: input_matrix,
754 },
755 ) => close_f32_bits(base_matrix, input_matrix, tolerance),
756 _ => false,
757 },
758 )
759}
760
761fn close_f32_bits<const N: usize>(
762 base: &[u32; N],
763 input: &[u32; N],
764 tolerance: &ScaleTolerancePolicy,
765) -> bool {
766 base.iter().zip(input).all(|(&base, &input)| {
767 close_f64(
768 f32::from_bits(base) as f64,
769 f32::from_bits(input) as f64,
770 tolerance,
771 )
772 })
773}
774
775fn close_f64(base: f64, input: f64, tolerance: &ScaleTolerancePolicy) -> bool {
776 base.is_finite()
777 && input.is_finite()
778 && (base - input).abs()
779 <= tolerance.scalar_absolute + tolerance.scalar_relative * base.abs().max(input.abs())
780}
781
782fn same_quaternion(base: &[u32; 4], input: &[u32; 4], tolerance: &ScaleTolerancePolicy) -> bool {
783 let base = base.map(|bits| f32::from_bits(bits) as f64);
784 let input = input.map(|bits| f32::from_bits(bits) as f64);
785 if !base
786 .iter()
787 .chain(input.iter())
788 .all(|value| value.is_finite())
789 {
790 return false;
791 }
792 let base_norm = base.iter().map(|value| value * value).sum::<f64>().sqrt();
793 let input_norm = input.iter().map(|value| value * value).sum::<f64>().sqrt();
794 if base_norm == 0.0 || input_norm == 0.0 {
795 return false;
796 }
797 let dot = base
798 .iter()
799 .zip(input)
800 .map(|(base, input)| base * input)
801 .sum::<f64>()
802 / (base_norm * input_norm);
803 2.0 * dot.abs().clamp(-1.0, 1.0).acos() <= tolerance.rotation_residual_radians
804}