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> { 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> { clip.set_looping(value).map_err(err)?; Ok(clip) }); engine.register_fn( "keyframe", |clip: ClipBuilder, path: &str, property: &str, time: f64, value: f64| -> Result> { 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> { 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> { 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> { 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> { store.clip(name).map_err(err) }, ); } fn register_blend_trees(engine: &mut Engine) { engine.register_fn("blend_tree", |a: &mut Aac, name: &str| -> Result> { 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::>(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::>(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::>(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::>(tree) }); engine.register_fn("direct", |tree: BlendTreeBuilder| { tree.configure(BlendType::Direct, "", None).map_err(err)?; Ok::>(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::>(tree) }); engine.register_fn("add_motion", |tree: BlendTreeBuilder, child: BlendTreeBuilder, threshold: f64| { push_child(&tree, tree_ref(&child), threshold, None, None)?; Ok::>(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::>(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::>(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::>(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::>(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, direct_param: Option<&str>, ) -> Result<(), Box> { 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) }