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.
213 lines
7.8 KiB
Rust
213 lines
7.8 KiB
Rust
use crate::builder::*;
|
|
use crate::graph::*;
|
|
use crate::rhai_api::err;
|
|
use rhai::{Engine, EvalAltResult};
|
|
|
|
pub fn register(engine: &mut Engine) {
|
|
register_clips(engine);
|
|
register_animation_store(engine);
|
|
register_blend_trees(engine);
|
|
register_state_motion(engine);
|
|
}
|
|
|
|
fn clip_ref(clip: &ClipBuilder) -> MotionRef {
|
|
MotionRef::Clip {
|
|
name: clip.name.clone(),
|
|
}
|
|
}
|
|
|
|
fn tree_ref(tree: &BlendTreeBuilder) -> MotionRef {
|
|
MotionRef::BlendTree {
|
|
name: tree.name.clone(),
|
|
}
|
|
}
|
|
|
|
fn register_clips(engine: &mut Engine) {
|
|
engine.register_fn("clip", |a: &mut Aac, name: &str| -> Result<ClipBuilder, Box<EvalAltResult>> {
|
|
if name.trim().is_empty() {
|
|
return Err(err("clip name is empty".to_string()));
|
|
}
|
|
if a.write(|graph| graph.clips.iter().any(|clip| clip.name == name)) {
|
|
return Err(err(format!("clip `{name}` is already declared")));
|
|
}
|
|
a.write(|graph| {
|
|
graph.clips.push(ClipData {
|
|
name: name.to_string(),
|
|
looping: Some(false),
|
|
curves: Vec::new(),
|
|
source: None,
|
|
})
|
|
});
|
|
Ok(ClipBuilder {
|
|
aac: a.clone(),
|
|
name: name.to_string(),
|
|
})
|
|
});
|
|
|
|
engine.register_fn("looping", |clip: ClipBuilder, value: bool| -> Result<ClipBuilder, Box<EvalAltResult>> {
|
|
clip.set_looping(value).map_err(err)?;
|
|
Ok(clip)
|
|
});
|
|
|
|
engine.register_fn(
|
|
"keyframe",
|
|
|clip: ClipBuilder, path: &str, property: &str, time: f64, value: f64| -> Result<ClipBuilder, Box<EvalAltResult>> {
|
|
clip.push_key(path, property, Keyframe::linear(time as f32, value as f32))
|
|
.map_err(err)?;
|
|
Ok(clip)
|
|
},
|
|
);
|
|
|
|
engine.register_fn("toggle", |clip: ClipBuilder, path: &str, value: bool| -> Result<ClipBuilder, Box<EvalAltResult>> {
|
|
let value = if value { 1.0 } else { 0.0 };
|
|
// A one-frame constant, matching AAC's toggling semantics.
|
|
clip.push_key(path, "m_IsActive", Keyframe::linear(0.0, value))
|
|
.map_err(err)?;
|
|
clip.push_key(path, "m_IsActive", Keyframe::linear(1.0 / 60.0, value))
|
|
.map_err(err)?;
|
|
Ok(clip)
|
|
});
|
|
|
|
engine.register_fn(
|
|
"blend_shape",
|
|
|clip: ClipBuilder, path: &str, shape: &str, time: f64, value: f64| -> Result<ClipBuilder, Box<EvalAltResult>> {
|
|
let property = format!("blendShape.{shape}");
|
|
clip.push_key(path, &property, Keyframe::linear(time as f32, value as f32))
|
|
.map_err(err)?;
|
|
Ok(clip)
|
|
},
|
|
);
|
|
}
|
|
|
|
fn register_animation_store(engine: &mut Engine) {
|
|
engine.register_fn(
|
|
"AnimationStore",
|
|
|a: &mut Aac, folder: &str| -> Result<AnimationStore, Box<EvalAltResult>> {
|
|
if folder.trim().is_empty() {
|
|
return Err(err("animation store folder is empty".to_string()));
|
|
}
|
|
Ok(AnimationStore {
|
|
aac: a.clone(),
|
|
folder: folder.to_string(),
|
|
})
|
|
},
|
|
);
|
|
|
|
engine.register_fn(
|
|
"clip",
|
|
|store: AnimationStore, name: &str| -> Result<ClipBuilder, Box<EvalAltResult>> {
|
|
store.clip(name).map_err(err)
|
|
},
|
|
);
|
|
}
|
|
|
|
fn register_blend_trees(engine: &mut Engine) {
|
|
engine.register_fn("blend_tree", |a: &mut Aac, name: &str| -> Result<BlendTreeBuilder, Box<EvalAltResult>> {
|
|
if name.trim().is_empty() {
|
|
return Err(err("blend tree name is empty".to_string()));
|
|
}
|
|
if a.write(|graph| graph.blend_trees.iter().any(|tree| tree.name == name)) {
|
|
return Err(err(format!("blend tree `{name}` is already declared")));
|
|
}
|
|
a.write(|graph| {
|
|
graph.blend_trees.push(BlendTreeData {
|
|
name: name.to_string(),
|
|
blend_type: BlendType::Simple1D,
|
|
param_x: String::new(),
|
|
param_y: None,
|
|
children: Vec::new(),
|
|
use_automatic_thresholds: false,
|
|
})
|
|
});
|
|
Ok(BlendTreeBuilder {
|
|
aac: a.clone(),
|
|
name: name.to_string(),
|
|
})
|
|
});
|
|
|
|
engine.register_fn("simple_1d", |tree: BlendTreeBuilder, x: FloatParam| {
|
|
tree.configure(BlendType::Simple1D, &x.name, None).map_err(err)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("simple_directional_2d", |tree: BlendTreeBuilder, x: FloatParam, y: FloatParam| {
|
|
tree.configure(BlendType::SimpleDirectional2D, &x.name, Some(&y.name))
|
|
.map_err(err)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("freeform_directional_2d", |tree: BlendTreeBuilder, x: FloatParam, y: FloatParam| {
|
|
tree.configure(BlendType::FreeformDirectional2D, &x.name, Some(&y.name))
|
|
.map_err(err)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("freeform_cartesian_2d", |tree: BlendTreeBuilder, x: FloatParam, y: FloatParam| {
|
|
tree.configure(BlendType::FreeformCartesian2D, &x.name, Some(&y.name))
|
|
.map_err(err)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("direct", |tree: BlendTreeBuilder| {
|
|
tree.configure(BlendType::Direct, "", None).map_err(err)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("automatic_thresholds", |tree: BlendTreeBuilder, value: bool| {
|
|
tree.data_mut(|data| data.use_automatic_thresholds = value);
|
|
tree
|
|
});
|
|
|
|
engine.register_fn("add_motion", |tree: BlendTreeBuilder, clip: ClipBuilder, threshold: f64| {
|
|
push_child(&tree, clip_ref(&clip), threshold, None, None)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("add_motion", |tree: BlendTreeBuilder, child: BlendTreeBuilder, threshold: f64| {
|
|
push_child(&tree, tree_ref(&child), threshold, None, None)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("add_motion", |tree: BlendTreeBuilder, clip: ClipBuilder, x: f64, y: f64| {
|
|
push_child(&tree, clip_ref(&clip), x, Some(y), None)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("add_motion", |tree: BlendTreeBuilder, child: BlendTreeBuilder, x: f64, y: f64| {
|
|
push_child(&tree, tree_ref(&child), x, Some(y), None)?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
|
|
engine.register_fn("add_motion_direct", |tree: BlendTreeBuilder, clip: ClipBuilder, parameter: FloatParam| {
|
|
push_child(&tree, clip_ref(&clip), 0.0, None, Some(¶meter.name))?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
engine.register_fn("add_motion_direct", |tree: BlendTreeBuilder, child: BlendTreeBuilder, parameter: FloatParam| {
|
|
push_child(&tree, tree_ref(&child), 0.0, None, Some(¶meter.name))?;
|
|
Ok::<BlendTreeBuilder, Box<EvalAltResult>>(tree)
|
|
});
|
|
}
|
|
|
|
fn register_state_motion(engine: &mut Engine) {
|
|
engine.register_fn("set_clip", |state: StateRef, clip: ClipBuilder| {
|
|
state.set_motion(clip_ref(&clip));
|
|
state
|
|
});
|
|
engine.register_fn("set_motion", |state: StateRef, clip: ClipBuilder| {
|
|
state.set_motion(clip_ref(&clip));
|
|
state
|
|
});
|
|
engine.register_fn("set_motion", |state: StateRef, tree: BlendTreeBuilder| {
|
|
state.set_motion(tree_ref(&tree));
|
|
state
|
|
});
|
|
}
|
|
|
|
fn push_child(
|
|
tree: &BlendTreeBuilder,
|
|
motion: MotionRef,
|
|
threshold: f64,
|
|
threshold_y: Option<f64>,
|
|
direct_param: Option<&str>,
|
|
) -> Result<(), Box<EvalAltResult>> {
|
|
let child = BlendChild {
|
|
motion,
|
|
threshold: threshold as f32,
|
|
threshold_y: threshold_y.map(|y| y as f32),
|
|
direct_param: direct_param.map(str::to_string),
|
|
};
|
|
tree.push_child(child).map_err(err)
|
|
}
|