myth_animation/binder.rs
1use crate::binding::{ClipBinding, Rig, TrackBinding};
2use crate::clip::AnimationClip;
3use crate::target::AnimationTarget;
4use myth_core::NodeHandle;
5
6/// Resolves animation clip track targets to logical bone indices via a [`Rig`].
7///
8/// Two main workflows:
9///
10/// 1. **Rig construction** — [`build_rig`](Self::build_rig) walks the scene
11/// subtree to produce a [`Rig`] with ordered bone handles and paths.
12/// 2. **Clip binding** — [`build_clip_binding`](Self::build_clip_binding) maps
13/// each track's path to a bone index, lazily recording rest poses for
14/// newly animated nodes.
15pub struct Binder;
16
17impl Binder {
18 /// Builds a [`Rig`] by walking the subtree rooted at `root_node`.
19 ///
20 /// Every named descendant (and the root itself) becomes a bone entry.
21 /// Bone paths are recorded relative to `root_node` so that the same
22 /// rig topology can be matched against multiple clips.
23 pub fn build_rig(target: &dyn AnimationTarget, root_node: NodeHandle) -> Rig {
24 let mut bones = Vec::new();
25 let mut bone_paths = Vec::new();
26
27 // Depth-first walk collecting (handle, path) pairs.
28 let mut stack: Vec<(NodeHandle, Vec<String>)> = Vec::new();
29
30 // Seed with root's direct children.
31 if let Some(children) = target.node_children(root_node) {
32 for child in children {
33 if let Some(name) = target.node_name(child) {
34 stack.push((child, vec![name]));
35 }
36 }
37 }
38
39 // Also include root itself if it has a name.
40 if let Some(name) = target.node_name(root_node) {
41 bones.push(root_node);
42 bone_paths.push(vec![name]);
43 }
44
45 while let Some((handle, path)) = stack.pop() {
46 // Only register named nodes (unnamed internal nodes are skipped).
47 if target.node_name(handle).is_some() {
48 bones.push(handle);
49 bone_paths.push(path.clone());
50 }
51
52 if let Some(children) = target.node_children(handle) {
53 for child in children {
54 if let Some(child_name) = target.node_name(child) {
55 let mut child_path = path.clone();
56 child_path.push(child_name);
57 stack.push((child, child_path));
58 }
59 }
60 }
61 }
62
63 Rig { bones, bone_paths }
64 }
65
66 /// Builds a [`ClipBinding`] mapping clip tracks to rig bone indices.
67 ///
68 /// For every track whose [`crate::clip::TrackMeta::path`] matches a bone path in the
69 /// rig, a [`TrackBinding`] is emitted. Nodes receiving their first
70 /// animation binding get their current transform lazily recorded as
71 /// the rest pose.
72 pub fn build_clip_binding(
73 target: &mut dyn AnimationTarget,
74 rig: &Rig,
75 clip: &AnimationClip,
76 ) -> ClipBinding {
77 let mut bindings = Vec::with_capacity(clip.tracks.len());
78
79 for (track_idx, track) in clip.tracks.iter().enumerate() {
80 let t = track.meta.target;
81
82 // Match track path against rig bone paths.
83 let bone_index = rig.bone_paths.iter().position(|p| *p == track.meta.path);
84
85 if let Some(bone_index) = bone_index {
86 let node_handle = rig.bones[bone_index];
87
88 // Lazy record rest pose on first encounter.
89 if !target.has_rest_transform(node_handle)
90 && let Some(transform) = target.node_transform(node_handle)
91 {
92 target.store_rest_transform(node_handle, transform);
93 }
94
95 bindings.push(TrackBinding {
96 track_index: track_idx,
97 bone_index,
98 target: t,
99 });
100 }
101 }
102
103 ClipBinding { bindings }
104 }
105
106 /// Convenience: builds a rig *and* a clip binding in one call.
107 ///
108 /// Equivalent to calling [`build_rig`](Self::build_rig) followed by
109 /// [`build_clip_binding`](Self::build_clip_binding).
110 pub fn bind(
111 target: &mut dyn AnimationTarget,
112 root_node: NodeHandle,
113 clip: &AnimationClip,
114 ) -> (Rig, ClipBinding) {
115 let rig = Self::build_rig(target, root_node);
116 let binding = Self::build_clip_binding(target, &rig, clip);
117 (rig, binding)
118 }
119}