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:
+118
-1
@@ -2,6 +2,7 @@ use aac::graph::*;
|
||||
use serde_json::Value;
|
||||
|
||||
const AVATAR: &str = include_str!("../examples/avatar.rhai");
|
||||
const ANIMATION_STORE: &str = include_str!("../examples/animation_store.rhai");
|
||||
|
||||
fn json(script: &str) -> Value {
|
||||
serde_json::from_str(&aac::evaluate_to_json(script).expect("script should evaluate")).expect("valid json")
|
||||
@@ -37,7 +38,7 @@ fn example_script_describes_the_whole_controller() {
|
||||
assert_eq!(value["system_name"], "MyAvatar");
|
||||
assert_eq!(value["asset_key"], "AAC_");
|
||||
assert_eq!(value["parameters"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(value["clips"].as_array().unwrap().len(), 3);
|
||||
assert_eq!(value["clips"].as_array().unwrap().len(), 4);
|
||||
assert_eq!(value["blend_trees"].as_array().unwrap().len(), 1);
|
||||
|
||||
let layers = value["controller"]["layers"].as_array().unwrap();
|
||||
@@ -107,6 +108,122 @@ fn curve_of<'a>(clip: &'a Value, property: &str) -> &'a Value {
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_store_clips_are_referenced_not_generated() {
|
||||
let value = json(
|
||||
r#"
|
||||
let aac = AnimatorAsCode();
|
||||
aac.system_name("S");
|
||||
aac.asset_key("K");
|
||||
let store = aac.AnimationStore("Assets/Doloro/Clips/");
|
||||
let walk = store.clip("Walk");
|
||||
let ctrl = aac.new_controller();
|
||||
let base = ctrl.layer("Base");
|
||||
let s = base.state("S", 0, 0);
|
||||
s.set_clip(walk);
|
||||
"#,
|
||||
);
|
||||
let clips = value["clips"].as_array().unwrap();
|
||||
assert_eq!(clips.len(), 1);
|
||||
assert_eq!(clips[0]["name"], "Assets/Doloro/Clips/Walk.anim");
|
||||
assert_eq!(clips[0]["source"], "Assets/Doloro/Clips/Walk.anim");
|
||||
assert!(clips[0]["curves"].as_array().unwrap().is_empty());
|
||||
// Untouched, it keeps the source asset's looping setting.
|
||||
assert!(clips[0].get("looping").is_none());
|
||||
|
||||
// A generated clip keeps its old shape: no `source` key at all.
|
||||
let generated = json(
|
||||
r#"
|
||||
let aac = AnimatorAsCode();
|
||||
aac.system_name("S");
|
||||
aac.asset_key("K");
|
||||
let c = aac.clip("c");
|
||||
"#,
|
||||
);
|
||||
assert!(generated["clips"][0].get("source").is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_store_resolves_one_clip_per_asset() {
|
||||
// The same asset asked for twice, with and without the extension, stays a single clip.
|
||||
let value = json(
|
||||
r#"
|
||||
let aac = AnimatorAsCode();
|
||||
aac.system_name("S");
|
||||
aac.asset_key("K");
|
||||
let store = aac.AnimationStore("Assets/Doloro/Clips");
|
||||
let x = store.clip("Walk");
|
||||
let y = store.clip("Walk.anim");
|
||||
let ctrl = aac.new_controller();
|
||||
let base = ctrl.layer("Base");
|
||||
let s = base.state("S", 0, 0);
|
||||
s.set_clip(x);
|
||||
"#,
|
||||
);
|
||||
assert_eq!(value["clips"].as_array().unwrap().len(), 1);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn animation_store_clips_can_be_edited_on_a_clone() {
|
||||
let value = json(
|
||||
r#"
|
||||
let aac = AnimatorAsCode();
|
||||
aac.system_name("S");
|
||||
aac.asset_key("K");
|
||||
let store = aac.AnimationStore("Assets/Doloro/Clips");
|
||||
let walk = store.clip("Walk");
|
||||
walk.looping(true);
|
||||
walk.keyframe("Body", "m_IsActive", 0.0, 1.0);
|
||||
let ctrl = aac.new_controller();
|
||||
let base = ctrl.layer("Base");
|
||||
let s = base.state("S", 0, 0);
|
||||
s.set_clip(walk);
|
||||
"#,
|
||||
);
|
||||
let clip = &value["clips"][0];
|
||||
// The source is still carried, so the Unity side knows which asset to clone.
|
||||
assert_eq!(clip["source"], "Assets/Doloro/Clips/Walk.anim");
|
||||
assert_eq!(clip["looping"], true);
|
||||
assert_eq!(clip["curves"][0]["property"], "m_IsActive");
|
||||
}
|
||||
|
||||
/// The store example is a whole controller, so evaluating it exercises every store path end to end.
|
||||
#[test]
|
||||
fn animation_store_example_sources_every_clip() {
|
||||
let value = json(ANIMATION_STORE);
|
||||
|
||||
assert_eq!(value["system_name"], "AnimationStore");
|
||||
assert_eq!(value["parameters"].as_array().unwrap().len(), 5);
|
||||
assert_eq!(value["controller"]["layers"].as_array().unwrap().len(), 2);
|
||||
|
||||
// Every clip comes from an asset, and each asset is declared exactly once.
|
||||
let clips = value["clips"].as_array().unwrap();
|
||||
let sources: Vec<&str> = clips
|
||||
.iter()
|
||||
.map(|clip| clip["source"].as_str().expect("every clip has a source"))
|
||||
.collect();
|
||||
let unique: std::collections::HashSet<&str> = sources.iter().copied().collect();
|
||||
assert_eq!(unique.len(), sources.len(), "the same asset must not be declared twice");
|
||||
assert_eq!(clips.len(), 11);
|
||||
|
||||
// An edited clip carries its override and curves; an untouched one carries neither.
|
||||
let walk = find_clip(&value, "Assets/Doloro/Clips/Locomotion/Walk.anim");
|
||||
assert_eq!(walk["looping"], true);
|
||||
assert_eq!(curve_of(walk, "m_IsActive")["target"], "game_object");
|
||||
let idle = find_clip(&value, "Assets/Doloro/Clips/Locomotion/Idle.anim");
|
||||
assert!(idle.get("looping").is_none());
|
||||
assert!(idle["curves"].as_array().unwrap().is_empty());
|
||||
|
||||
// A store clip feeds a state and a blend tree, and a tree may nest a tree declared before it.
|
||||
let trees = value["blend_trees"].as_array().unwrap();
|
||||
assert_eq!(trees.len(), 4);
|
||||
let movement = trees.iter().find(|t| t["name"] == "movement").unwrap();
|
||||
assert_eq!(movement["children"].as_array().unwrap().len(), 3);
|
||||
let full_body = trees.iter().find(|t| t["name"] == "full_body").unwrap();
|
||||
assert_eq!(full_body["children"][0]["motion"]["type"], "blend_tree");
|
||||
assert_eq!(full_body["children"][0]["motion"]["name"], "movement");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn keyframes_are_sorted_by_time() {
|
||||
let value = json(
|
||||
|
||||
Reference in New Issue
Block a user