Skip to main content

animsmith_core/scale/
assembly_basis.rs

1//! Versioned semantic basis projected from an accepted scale plan.
2
3use super::{
4    ScaleError, ScaleOperation, ScalePlan, ScaleProjectedRole, ScaleSourceNodeKind,
5    ScaleTolerancePolicy,
6};
7use crate::model::{Document, SourceNodeLocalRest};
8use serde::Serialize;
9use std::collections::BTreeSet;
10
11/// Stable semantic version of [`AssemblyScaleBasis`].
12pub const ASSEMBLY_SCALE_BASIS_VERSION: u32 = 1;
13
14/// One named normalized node and its exact authored rest basis.
15#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
16pub struct AssemblyScaleNamedNode {
17    /// Stable node name used by assembly remapping.
18    pub name: String,
19    /// Parent node name, when any.
20    pub parent: Option<String>,
21    /// Translation component bits.
22    pub translation_bits: [u32; 3],
23    /// Rotation component bits in `[x, y, z, w]` order.
24    pub rotation_bits: [u32; 4],
25    /// Scale component bits.
26    pub scale_bits: [u32; 3],
27}
28
29/// One raw source node, including projected/helper layout.
30#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
31pub struct AssemblyScaleSourceNode {
32    /// Raw source node identity.
33    pub source_node_index: usize,
34    /// Raw parent identity.
35    pub parent_source_node_index: Option<usize>,
36    /// Authored name, when present.
37    pub name: Option<String>,
38    /// Stable projected/helper role.
39    pub role: String,
40    /// Authored local-rest representation and exact component bits.
41    pub local_rest: AssemblyScaleSourceRest,
42}
43
44/// Authored raw source-node rest representation retained by a basis.
45#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
46#[serde(tag = "kind", rename_all = "snake_case")]
47pub enum AssemblyScaleSourceRest {
48    /// Decomposed translation, quaternion, and scale.
49    Trs {
50        /// Translation component bits.
51        translation_bits: [u32; 3],
52        /// Quaternion component bits in `[x, y, z, w]` order.
53        rotation_bits: [u32; 4],
54        /// Scale component bits.
55        scale_bits: [u32; 3],
56    },
57    /// Exact authored column-major local matrix bits.
58    Matrix {
59        /// Matrix component bits.
60        matrix_bits: [u32; 16],
61    },
62}
63
64/// One animation channel target and the plan-owned effective multiplier.
65#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
66pub struct AssemblyScaleTargetPath {
67    /// Clip position in the input document.
68    pub clip_index: usize,
69    /// Track position inside the clip.
70    pub track_index: usize,
71    /// Named target used by assembly remapping.
72    pub bone: String,
73    /// Stable property name.
74    pub property: &'static str,
75    /// Effective multiplier encoded without float spelling ambiguity.
76    pub factor_bits: u64,
77}
78
79/// Complete versioned semantic basis for one assembly input.
80#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
81pub struct AssemblyScaleBasis {
82    /// Basis schema version.
83    pub version: u32,
84    /// Fixed model coordinate convention.
85    pub coordinate_convention: &'static str,
86    /// Tolerance identity used for semantic compatibility.
87    pub tolerance_policy_id: &'static str,
88    /// Selected source skin.
89    pub source_skin_index: usize,
90    /// Selected source root node.
91    pub source_root_node_index: usize,
92    /// Declared factor bits.
93    pub expected_factor_bits: u64,
94    /// Normalized named topology and rest/orientation basis.
95    pub named_nodes: Vec<AssemblyScaleNamedNode>,
96    /// Raw projected/helper topology and local-rest basis.
97    pub source_nodes: Vec<AssemblyScaleSourceNode>,
98    /// Animation target paths and effective factors.
99    pub target_paths: Vec<AssemblyScaleTargetPath>,
100}
101
102/// Why two independently supplied assembly inputs do not share one basis.
103#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
104#[error("assembly scale basis mismatch ({reason})")]
105pub struct AssemblyScaleCompatibilityError {
106    /// Stable machine-readable mismatch reason.
107    pub reason: &'static str,
108}
109
110/// Selector mode requested while deriving an assembly compatibility basis.
111///
112/// The request is not itself compatibility identity. The constructor checks
113/// it against the accepted basis and source document before producing the
114/// validated identity consumed by the comparator.
115#[derive(Debug, Clone, Copy)]
116#[non_exhaustive]
117pub enum AssemblyScaleSelectorRequest<'a> {
118    /// Preserve exact format-local source skin and root indices.
119    Indexed,
120    /// Derive identity from one exact normalized root name.
121    Named {
122        /// Exact, case-sensitive root name already selected by the caller.
123        root_node_name: &'a str,
124    },
125}
126
127#[derive(Debug, Clone)]
128enum AssemblyScaleSelectorIdentity {
129    Indexed,
130    Named {
131        root_node_name: String,
132        skin_joint_names: Vec<String>,
133    },
134}
135
136/// One assembly basis paired with core-validated selector identity.
137///
138/// Named identity cannot be assembled directly by a caller: it is derived
139/// from the source document's unique root and containing-skin facts.
140#[derive(Debug, Clone)]
141pub struct AssemblyScaleCompatibilityBasis {
142    basis: AssemblyScaleBasis,
143    selector: AssemblyScaleSelectorIdentity,
144}
145
146impl AssemblyScaleCompatibilityBasis {
147    /// The immutable evidence basis governed by this validated selector.
148    #[must_use]
149    pub fn basis(&self) -> &AssemblyScaleBasis {
150        &self.basis
151    }
152}
153
154/// Project an accepted rest/bind plan into the versioned assembly basis.
155///
156/// # Errors
157///
158/// Returns the plan's validation error, a selector-operation mismatch, or a
159/// duplicate/empty named-node error that would make name remapping ambiguous.
160pub fn assembly_scale_basis(
161    document: &Document,
162    plan: &ScalePlan,
163) -> Result<AssemblyScaleBasis, ScaleError> {
164    plan.validate_document_inventory(document)?;
165    let ScaleOperation::RestBindUniformScale {
166        source_skin_index,
167        source_root_node_index,
168        expected_factor,
169    } = plan.operation()
170    else {
171        return Err(ScaleError::PlanDocumentMismatch {
172            reason: "assembly_basis_requires_rest_bind",
173        });
174    };
175    let mut names = BTreeSet::new();
176    let mut named_nodes = Vec::with_capacity(document.skeleton.bones.len());
177    for (index, bone) in document.skeleton.bones.iter().enumerate() {
178        if bone.name.is_empty() || !names.insert(bone.name.as_str()) {
179            return Err(ScaleError::PlanDocumentMismatch {
180                reason: "assembly_basis_requires_unique_named_nodes",
181            });
182        }
183        named_nodes.push(AssemblyScaleNamedNode {
184            name: bone.name.clone(),
185            parent: bone
186                .parent
187                .and_then(|parent| document.skeleton.bones.get(parent))
188                .map(|parent| parent.name.clone()),
189            translation_bits: bone.rest.translation.to_array().map(f32::to_bits),
190            rotation_bits: bone.rest.rotation.to_array().map(f32::to_bits),
191            scale_bits: bone.rest.scale.to_array().map(f32::to_bits),
192        });
193        if bone.parent.is_some_and(|parent| parent >= index) {
194            return Err(ScaleError::PlanDocumentMismatch {
195                reason: "assembly_basis_parent_order",
196            });
197        }
198    }
199    let source_by_index = document
200        .assets
201        .source_skeleton
202        .nodes
203        .iter()
204        .map(|node| (node.source_node_index, node))
205        .collect::<std::collections::BTreeMap<_, _>>();
206    let mut source_nodes = Vec::new();
207    for row in plan.ledger().source_topology() {
208        let source = source_by_index.get(&row.source_node_index()).ok_or(
209            ScaleError::PlanDocumentMismatch {
210                reason: "assembly_basis_source_node_missing",
211            },
212        )?;
213        let local_rest = match source.local_rest {
214            SourceNodeLocalRest::Trs {
215                translation,
216                rotation,
217                scale,
218            } => AssemblyScaleSourceRest::Trs {
219                translation_bits: translation.to_array().map(f32::to_bits),
220                rotation_bits: rotation.to_array().map(f32::to_bits),
221                scale_bits: scale.to_array().map(f32::to_bits),
222            },
223            SourceNodeLocalRest::Matrix(matrix) => AssemblyScaleSourceRest::Matrix {
224                matrix_bits: matrix.to_cols_array().map(f32::to_bits),
225            },
226        };
227        let role = match row.kind() {
228            ScaleSourceNodeKind::Projected { role, .. } => match role {
229                ScaleProjectedRole::Root => "projected-root",
230                ScaleProjectedRole::Joint => "projected-joint",
231                ScaleProjectedRole::TransformOnly => "projected-transform-only",
232            },
233            ScaleSourceNodeKind::Connector => "connector",
234            ScaleSourceNodeKind::OutsideDomain { bone: Some(_) } => "outside-projected",
235            ScaleSourceNodeKind::OutsideDomain { bone: None } => "outside-helper",
236        };
237        source_nodes.push(AssemblyScaleSourceNode {
238            source_node_index: row.source_node_index(),
239            parent_source_node_index: row.parent_source_node_index(),
240            name: source.name.clone(),
241            role: role.to_owned(),
242            local_rest,
243        });
244    }
245    let mut target_paths = Vec::new();
246    for (clip_index, clip) in document.clips.iter().enumerate() {
247        for (track_index, track) in clip.tracks.iter().enumerate() {
248            let bone = document
249                .skeleton
250                .bones
251                .get(track.bone)
252                .ok_or(ScaleError::BoneIndexOutOfRange { index: track.bone })?;
253            target_paths.push(AssemblyScaleTargetPath {
254                clip_index,
255                track_index,
256                bone: bone.name.clone(),
257                property: track.property.as_str(),
258                factor_bits: plan
259                    .animation_target_factor_unchecked(document, track.bone, track.property)?
260                    .to_bits(),
261            });
262        }
263    }
264    Ok(AssemblyScaleBasis {
265        version: ASSEMBLY_SCALE_BASIS_VERSION,
266        coordinate_convention: "right-handed-y-up-metres",
267        tolerance_policy_id: plan.tolerance_policy().id,
268        source_skin_index,
269        source_root_node_index,
270        expected_factor_bits: expected_factor.to_bits(),
271        named_nodes,
272        source_nodes,
273        target_paths,
274    })
275}
276
277/// Project one accepted plan and pair it with selector identity derived from
278/// the same document.
279///
280/// # Errors
281///
282/// Returns the plan's validation error, or a plan/document mismatch if a named
283/// request does not resolve to exactly the planned root and skin, or if any
284/// selected skin joint lacks one normalized name.
285pub fn assembly_scale_compatibility_basis(
286    document: &Document,
287    plan: &ScalePlan,
288    selector: AssemblyScaleSelectorRequest<'_>,
289) -> Result<AssemblyScaleCompatibilityBasis, ScaleError> {
290    // Derive both halves from one document/plan pair so callers cannot seal a
291    // selector identity from one document around another document's basis.
292    let basis = assembly_scale_basis(document, plan)?;
293    let selector = match selector {
294        AssemblyScaleSelectorRequest::Indexed => AssemblyScaleSelectorIdentity::Indexed,
295        AssemblyScaleSelectorRequest::Named { root_node_name } => {
296            let root_matches = document
297                .assets
298                .source_skeleton
299                .nodes
300                .iter()
301                .filter(|node| {
302                    node.bone
303                        .and_then(|bone| document.skeleton.bones.get(bone))
304                        .is_some_and(|bone| bone.name == root_node_name)
305                })
306                .collect::<Vec<_>>();
307            let [root] = root_matches.as_slice() else {
308                return Err(ScaleError::PlanDocumentMismatch {
309                    reason: "assembly_basis_named_selector_root_not_unique",
310                });
311            };
312            if root.source_node_index != basis.source_root_node_index {
313                return Err(ScaleError::PlanDocumentMismatch {
314                    reason: "assembly_basis_named_selector_root_disagrees_with_plan",
315                });
316            }
317            let skin_matches = document
318                .assets
319                .source_skeleton
320                .skins
321                .iter()
322                .filter(|skin| {
323                    skin.joint_source_node_indices
324                        .contains(&root.source_node_index)
325                })
326                .collect::<Vec<_>>();
327            let [skin] = skin_matches.as_slice() else {
328                return Err(ScaleError::PlanDocumentMismatch {
329                    reason: "assembly_basis_named_selector_skin_not_unique",
330                });
331            };
332            if skin.source_skin_index != basis.source_skin_index {
333                return Err(ScaleError::PlanDocumentMismatch {
334                    reason: "assembly_basis_named_selector_skin_disagrees_with_plan",
335                });
336            }
337            let source_nodes = document
338                .assets
339                .source_skeleton
340                .nodes
341                .iter()
342                .map(|node| (node.source_node_index, node))
343                .collect::<std::collections::BTreeMap<_, _>>();
344            let skin_joint_names = skin
345                .joint_source_node_indices
346                .iter()
347                .map(|source_index| {
348                    source_nodes
349                        .get(source_index)
350                        .and_then(|node| node.bone)
351                        .and_then(|bone| document.skeleton.bones.get(bone))
352                        .map(|bone| bone.name.clone())
353                        .ok_or(ScaleError::PlanDocumentMismatch {
354                            reason: "assembly_basis_named_selector_joint_has_no_name",
355                        })
356                })
357                .collect::<Result<Vec<_>, _>>()?;
358            AssemblyScaleSelectorIdentity::Named {
359                root_node_name: root_node_name.to_owned(),
360                skin_joint_names,
361            }
362        }
363    };
364    Ok(AssemblyScaleCompatibilityBasis { basis, selector })
365}
366
367/// Require two bases to agree on every static semantic field.
368///
369/// Target paths intentionally remain per-input fingerprint material: clip
370/// files may contain different takes. Each target's named node and factor are
371/// checked against its own accepted plan when the basis is built.
372///
373/// # Errors
374///
375/// Returns the first stable mismatch category.
376pub fn require_assembly_scale_compatibility(
377    base: &AssemblyScaleBasis,
378    input: &AssemblyScaleBasis,
379) -> Result<(), AssemblyScaleCompatibilityError> {
380    require_assembly_scale_compatibility_inner(
381        base,
382        &AssemblyScaleSelectorIdentity::Indexed,
383        input,
384        &AssemblyScaleSelectorIdentity::Indexed,
385    )
386}
387
388/// Require two core-validated compatibility bases to agree.
389///
390/// # Errors
391///
392/// Returns the first stable mismatch category, including selector-mode or
393/// exact named-selector disagreement.
394pub fn require_assembly_scale_compatibility_with_selectors(
395    base: &AssemblyScaleCompatibilityBasis,
396    input: &AssemblyScaleCompatibilityBasis,
397) -> Result<(), AssemblyScaleCompatibilityError> {
398    require_assembly_scale_compatibility_inner(
399        &base.basis,
400        &base.selector,
401        &input.basis,
402        &input.selector,
403    )
404}
405
406fn require_assembly_scale_compatibility_inner(
407    base: &AssemblyScaleBasis,
408    base_selector: &AssemblyScaleSelectorIdentity,
409    input: &AssemblyScaleBasis,
410    input_selector: &AssemblyScaleSelectorIdentity,
411) -> Result<(), AssemblyScaleCompatibilityError> {
412    let tolerance = ScaleTolerancePolicy::APPENDIX_D_V6;
413    let named_selectors = match (base_selector, input_selector) {
414        (AssemblyScaleSelectorIdentity::Indexed, AssemblyScaleSelectorIdentity::Indexed) => None,
415        (
416            AssemblyScaleSelectorIdentity::Named {
417                root_node_name: base_root,
418                skin_joint_names: base_joints,
419            },
420            AssemblyScaleSelectorIdentity::Named {
421                root_node_name: input_root,
422                skin_joint_names: input_joints,
423            },
424        ) => Some((base_root, base_joints, input_root, input_joints)),
425        _ => {
426            return Err(AssemblyScaleCompatibilityError {
427                reason: "source-selector-mode",
428            });
429        }
430    };
431    let mismatch = if base.version != input.version {
432        Some("basis-version")
433    } else if base.coordinate_convention != input.coordinate_convention {
434        Some("coordinate-convention")
435    } else if base.tolerance_policy_id != input.tolerance_policy_id
436        || base.tolerance_policy_id != tolerance.id
437    {
438        Some("tolerance-policy")
439    } else if named_selectors.is_none() && base.source_skin_index != input.source_skin_index {
440        Some("source-skin-selector")
441    } else if named_selectors.is_none()
442        && base.source_root_node_index != input.source_root_node_index
443    {
444        Some("source-root-selector")
445    } else if named_selectors.is_some_and(|(base_root, base_joints, input_root, input_joints)| {
446        base_root != input_root || base_joints != input_joints
447    }) {
448        Some("source-name-selector")
449    } else if base.expected_factor_bits != input.expected_factor_bits {
450        Some("expected-factor")
451    } else if !same_named_topology(&base.named_nodes, &input.named_nodes) {
452        Some("named-topology")
453    } else if !same_named_rest(&base.named_nodes, &input.named_nodes, &tolerance) {
454        Some("named-rest-basis")
455    } else if !same_named_orientations(&base.named_nodes, &input.named_nodes, &tolerance) {
456        Some("named-orientation")
457    } else if (named_selectors.is_some()
458        && !same_named_source_layout(&base.source_nodes, &input.source_nodes))
459        || (named_selectors.is_none()
460            && !same_source_layout(&base.source_nodes, &input.source_nodes))
461    {
462        Some("source-helper-layout")
463    } else if (named_selectors.is_some()
464        && !same_named_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
465        || (named_selectors.is_none()
466            && !same_source_rest(&base.source_nodes, &input.source_nodes, &tolerance))
467    {
468        Some("source-helper-rest-basis")
469    } else {
470        None
471    };
472    mismatch.map_or(Ok(()), |reason| {
473        Err(AssemblyScaleCompatibilityError { reason })
474    })
475}
476
477fn same_named_topology(base: &[AssemblyScaleNamedNode], input: &[AssemblyScaleNamedNode]) -> bool {
478    base.len() == input.len()
479        && base
480            .iter()
481            .zip(input)
482            .all(|(base, input)| base.name == input.name && base.parent == input.parent)
483}
484
485fn same_named_rest(
486    base: &[AssemblyScaleNamedNode],
487    input: &[AssemblyScaleNamedNode],
488    tolerance: &ScaleTolerancePolicy,
489) -> bool {
490    base.iter().zip(input).all(|(base, input)| {
491        close_f32_bits(&base.translation_bits, &input.translation_bits, tolerance)
492            && close_f32_bits(&base.scale_bits, &input.scale_bits, tolerance)
493    })
494}
495
496fn same_named_orientations(
497    base: &[AssemblyScaleNamedNode],
498    input: &[AssemblyScaleNamedNode],
499    tolerance: &ScaleTolerancePolicy,
500) -> bool {
501    base.iter()
502        .zip(input)
503        .all(|(base, input)| same_quaternion(&base.rotation_bits, &input.rotation_bits, tolerance))
504}
505
506fn same_source_layout(base: &[AssemblyScaleSourceNode], input: &[AssemblyScaleSourceNode]) -> bool {
507    base.len() == input.len()
508        && base.iter().zip(input).all(|(base, input)| {
509            base.source_node_index == input.source_node_index
510                && base.parent_source_node_index == input.parent_source_node_index
511                && base.name == input.name
512                && base.role == input.role
513                && std::mem::discriminant(&base.local_rest)
514                    == std::mem::discriminant(&input.local_rest)
515        })
516}
517
518#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
519struct NamedSourcePath(Vec<(Option<String>, String, bool)>);
520
521fn named_source_paths(nodes: &[AssemblyScaleSourceNode]) -> Option<Vec<NamedSourcePath>> {
522    let by_index = nodes
523        .iter()
524        .enumerate()
525        .map(|(position, node)| (node.source_node_index, position))
526        .collect::<std::collections::BTreeMap<_, _>>();
527    nodes
528        .iter()
529        .map(|node| {
530            let mut path = Vec::new();
531            let mut current = Some(node.source_node_index);
532            for _ in 0..=nodes.len() {
533                let Some(index) = current else {
534                    path.reverse();
535                    return Some(NamedSourcePath(path));
536                };
537                let row = nodes.get(*by_index.get(&index)?)?;
538                path.push((
539                    row.name.clone(),
540                    row.role.clone(),
541                    matches!(&row.local_rest, AssemblyScaleSourceRest::Matrix { .. }),
542                ));
543                current = row.parent_source_node_index;
544            }
545            None
546        })
547        .collect()
548}
549
550fn same_named_source_layout(
551    base: &[AssemblyScaleSourceNode],
552    input: &[AssemblyScaleSourceNode],
553) -> bool {
554    let (Some(mut base), Some(mut input)) = (named_source_paths(base), named_source_paths(input))
555    else {
556        return false;
557    };
558    base.sort();
559    input.sort();
560    base == input
561}
562
563fn same_named_source_rest(
564    base: &[AssemblyScaleSourceNode],
565    input: &[AssemblyScaleSourceNode],
566    tolerance: &ScaleTolerancePolicy,
567) -> bool {
568    let (Some(base_paths), Some(input_paths)) =
569        (named_source_paths(base), named_source_paths(input))
570    else {
571        return false;
572    };
573    let mut matched = vec![false; input.len()];
574    base.iter().zip(base_paths).all(|(base_node, base_path)| {
575        input
576            .iter()
577            .zip(&input_paths)
578            .enumerate()
579            .find(|(index, (input_node, input_path))| {
580                !matched[*index]
581                    && **input_path == base_path
582                    && same_source_rest_node(base_node, input_node, tolerance)
583            })
584            .is_some_and(|(index, _)| {
585                matched[index] = true;
586                true
587            })
588    })
589}
590
591fn same_source_rest_node(
592    base: &AssemblyScaleSourceNode,
593    input: &AssemblyScaleSourceNode,
594    tolerance: &ScaleTolerancePolicy,
595) -> bool {
596    same_source_rest(
597        std::slice::from_ref(base),
598        std::slice::from_ref(input),
599        tolerance,
600    )
601}
602
603fn same_source_rest(
604    base: &[AssemblyScaleSourceNode],
605    input: &[AssemblyScaleSourceNode],
606    tolerance: &ScaleTolerancePolicy,
607) -> bool {
608    base.iter().zip(input).all(
609        |(base, input)| match (&base.local_rest, &input.local_rest) {
610            (
611                AssemblyScaleSourceRest::Trs {
612                    translation_bits: base_translation,
613                    rotation_bits: base_rotation,
614                    scale_bits: base_scale,
615                },
616                AssemblyScaleSourceRest::Trs {
617                    translation_bits: input_translation,
618                    rotation_bits: input_rotation,
619                    scale_bits: input_scale,
620                },
621            ) => {
622                close_f32_bits(base_translation, input_translation, tolerance)
623                    && close_f32_bits(base_scale, input_scale, tolerance)
624                    && same_quaternion(base_rotation, input_rotation, tolerance)
625            }
626            (
627                AssemblyScaleSourceRest::Matrix {
628                    matrix_bits: base_matrix,
629                },
630                AssemblyScaleSourceRest::Matrix {
631                    matrix_bits: input_matrix,
632                },
633            ) => close_f32_bits(base_matrix, input_matrix, tolerance),
634            _ => false,
635        },
636    )
637}
638
639fn close_f32_bits<const N: usize>(
640    base: &[u32; N],
641    input: &[u32; N],
642    tolerance: &ScaleTolerancePolicy,
643) -> bool {
644    base.iter().zip(input).all(|(&base, &input)| {
645        close_f64(
646            f32::from_bits(base) as f64,
647            f32::from_bits(input) as f64,
648            tolerance,
649        )
650    })
651}
652
653fn close_f64(base: f64, input: f64, tolerance: &ScaleTolerancePolicy) -> bool {
654    base.is_finite()
655        && input.is_finite()
656        && (base - input).abs()
657            <= tolerance.scalar_absolute + tolerance.scalar_relative * base.abs().max(input.abs())
658}
659
660fn same_quaternion(base: &[u32; 4], input: &[u32; 4], tolerance: &ScaleTolerancePolicy) -> bool {
661    let base = base.map(|bits| f32::from_bits(bits) as f64);
662    let input = input.map(|bits| f32::from_bits(bits) as f64);
663    if !base
664        .iter()
665        .chain(input.iter())
666        .all(|value| value.is_finite())
667    {
668        return false;
669    }
670    let base_norm = base.iter().map(|value| value * value).sum::<f64>().sqrt();
671    let input_norm = input.iter().map(|value| value * value).sum::<f64>().sqrt();
672    if base_norm == 0.0 || input_norm == 0.0 {
673        return false;
674    }
675    let dot = base
676        .iter()
677        .zip(input)
678        .map(|(base, input)| base * input)
679        .sum::<f64>()
680        / (base_norm * input_norm);
681    2.0 * dot.abs().clamp(-1.0, 1.0).acos() <= tolerance.rotation_residual_radians
682}