Add animation store for pre-made clips

aac.AnimationStore(folder) seeds a clip from an existing .anim asset and
returns a normal ClipBuilder, so pre-made clips work anywhere generated
clips do and can still be edited (looping, keyframes, toggles, blend
shapes). The Unity side clones the asset into the container and applies
the script's changes to the clone, leaving the original untouched.

- ClipData gains optional source; looping becomes Option<bool> so an
  untouched store clip keeps the asset's own looping setting.
- Add rust/examples/animation_store.rhai, a worked store example.
This commit is contained in:
2026-09-18 14:52:02 +01:00
parent 42a1234e7f
commit 2c3b670ab9
10 changed files with 377 additions and 17 deletions
+45
View File
@@ -87,6 +87,13 @@ pub struct ClipBuilder {
pub(crate) name: String,
}
/// A folder of pre-made `AnimationClip` assets, resolved on the Unity side at generation time.
#[derive(Clone)]
pub struct AnimationStore {
pub(crate) aac: Aac,
pub(crate) folder: String,
}
#[derive(Clone)]
pub struct BlendTreeBuilder {
pub(crate) aac: Aac,
@@ -197,6 +204,12 @@ impl ClipBuilder {
self.aac.write(|graph| clip_mut(graph, &name).map(f))
}
/// Override the clip's looping setting. A store clip with no override keeps the source asset's own setting.
pub fn set_looping(&self, value: bool) -> Result<(), String> {
self.data_mut(|data| data.looping = Some(value));
Ok(())
}
/// Append one keyframe to the curve for (path, property), creating the curve if needed.
pub fn push_key(&self, path: &str, property: &str, key: Keyframe) -> Result<(), String> {
let target = infer_target(property).ok_or_else(|| {
@@ -212,6 +225,38 @@ impl ClipBuilder {
}
}
impl AnimationStore {
/// Resolve `folder/<name>.anim` and declare it as a clip the first time it is requested. The clip
/// name is the asset path, so the same asset seen through different names is still one clip.
pub fn clip(&self, name: &str) -> Result<ClipBuilder, String> {
if name.trim().is_empty() {
return Err("animation store clip name is empty".to_string());
}
let mut path = format!(
"{}/{}",
self.folder.trim_end_matches('/'),
name.trim_start_matches('/')
);
if !path.ends_with(".anim") {
path.push_str(".anim");
}
self.aac.write(|graph| {
if !graph.clips.iter().any(|clip| clip.source.as_deref() == Some(path.as_str())) {
graph.clips.push(ClipData {
name: path.clone(),
looping: None,
curves: Vec::new(),
source: Some(path.clone()),
});
}
});
Ok(ClipBuilder {
aac: self.aac.clone(),
name: path,
})
}
}
impl BlendTreeBuilder {
pub fn data_mut<R>(&self, f: impl FnOnce(&mut BlendTreeData) -> R) -> Option<R> {
let name = self.name.clone();