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.
273 lines
9.7 KiB
Rust
273 lines
9.7 KiB
Rust
use crate::builder::*;
|
|
use crate::graph::*;
|
|
use rhai::{Array, Engine, EvalAltResult, Position};
|
|
|
|
pub(crate) fn err(message: String) -> Box<EvalAltResult> {
|
|
Box::new(EvalAltResult::ErrorRuntime(message.into(), Position::NONE))
|
|
}
|
|
|
|
fn condition(parameter: &str, mode: CondMode, threshold: f32) -> Condition {
|
|
Condition {
|
|
parameter: parameter.to_string(),
|
|
mode,
|
|
threshold,
|
|
}
|
|
}
|
|
|
|
fn grid(value: i64) -> Result<i32, Box<EvalAltResult>> {
|
|
i32::try_from(value).map_err(|_| err(format!("grid position {value} does not fit in a 32-bit integer")))
|
|
}
|
|
|
|
fn add_state(aac: &Aac, layer: usize, machine: &[usize], name: &str, x: i64, y: i64) -> Result<StateRef, Box<EvalAltResult>> {
|
|
let position = GridPos::new(grid(x)?, grid(y)?);
|
|
let state = aac.write(|graph| {
|
|
let states = &mut machine_mut(graph, layer, machine).states;
|
|
states.push(State {
|
|
name: name.to_string(),
|
|
position,
|
|
..Default::default()
|
|
});
|
|
states.len() - 1
|
|
});
|
|
Ok(StateRef {
|
|
aac: aac.clone(),
|
|
layer,
|
|
machine: machine.to_vec(),
|
|
state,
|
|
})
|
|
}
|
|
|
|
fn add_sub_machine(
|
|
aac: &Aac,
|
|
layer: usize,
|
|
machine: &[usize],
|
|
name: &str,
|
|
x: i64,
|
|
y: i64,
|
|
) -> Result<MachineRef, Box<EvalAltResult>> {
|
|
let position = GridPos::new(grid(x)?, grid(y)?);
|
|
let index = aac.write(|graph| {
|
|
let sub_machines = &mut machine_mut(graph, layer, machine).sub_machines;
|
|
sub_machines.push(StateMachine {
|
|
name: Some(name.to_string()),
|
|
position,
|
|
..Default::default()
|
|
});
|
|
sub_machines.len() - 1
|
|
});
|
|
let mut path = machine.to_vec();
|
|
path.push(index);
|
|
Ok(MachineRef {
|
|
aac: aac.clone(),
|
|
layer,
|
|
machine: path,
|
|
})
|
|
}
|
|
|
|
fn assert_new_parameter(aac: &Aac, name: &str) -> Result<(), Box<EvalAltResult>> {
|
|
if name.trim().is_empty() {
|
|
return Err(err("parameter name is empty".to_string()));
|
|
}
|
|
let duplicate = aac.write(|graph| graph.parameters.iter().any(|p| p.name() == name));
|
|
if duplicate {
|
|
return Err(err(format!("parameter `{name}` is already declared")));
|
|
}
|
|
Ok(())
|
|
}
|
|
|
|
pub fn engine(aac: Aac) -> Engine {
|
|
let mut engine = Engine::new();
|
|
|
|
engine.register_type_with_name::<Aac>("Aac");
|
|
engine.register_type_with_name::<FloatParam>("FloatParam");
|
|
engine.register_type_with_name::<IntParam>("IntParam");
|
|
engine.register_type_with_name::<BoolParam>("BoolParam");
|
|
engine.register_type_with_name::<Condition>("Condition");
|
|
engine.register_type_with_name::<ControllerBuilder>("Controller");
|
|
engine.register_type_with_name::<LayerBuilder>("Layer");
|
|
engine.register_type_with_name::<MachineRef>("Machine");
|
|
engine.register_type_with_name::<StateRef>("State");
|
|
engine.register_type_with_name::<TransitionRef>("Transition");
|
|
engine.register_type_with_name::<ClipBuilder>("Clip");
|
|
engine.register_type_with_name::<AnimationStore>("AnimationStore");
|
|
engine.register_type_with_name::<BlendTreeBuilder>("BlendTree");
|
|
|
|
register_conditions(&mut engine);
|
|
register_setup(&mut engine, aac);
|
|
register_navigation(&mut engine);
|
|
register_transitions(&mut engine);
|
|
crate::motion_api::register(&mut engine);
|
|
|
|
engine
|
|
}
|
|
|
|
/// Leaf conditions come from overloaded comparison operators, because Rhai's `&&`/`||` cannot be
|
|
/// overloaded. Combine several conditions with `when_all([...])`.
|
|
fn register_conditions(engine: &mut Engine) {
|
|
engine.register_fn(">", |p: FloatParam, v: f64| condition(&p.name, CondMode::Greater, v as f32));
|
|
engine.register_fn("<", |p: FloatParam, v: f64| condition(&p.name, CondMode::Less, v as f32));
|
|
engine.register_fn("==", |p: FloatParam, v: f64| condition(&p.name, CondMode::Equals, v as f32));
|
|
engine.register_fn("!=", |p: FloatParam, v: f64| condition(&p.name, CondMode::NotEqual, v as f32));
|
|
engine.register_fn(">", |p: IntParam, v: i64| condition(&p.name, CondMode::Greater, v as f32));
|
|
engine.register_fn("<", |p: IntParam, v: i64| condition(&p.name, CondMode::Less, v as f32));
|
|
engine.register_fn("==", |p: IntParam, v: i64| condition(&p.name, CondMode::Equals, v as f32));
|
|
engine.register_fn("!=", |p: IntParam, v: i64| condition(&p.name, CondMode::NotEqual, v as f32));
|
|
engine.register_fn("==", |p: BoolParam, v: bool| {
|
|
condition(&p.name, if v { CondMode::If } else { CondMode::IfNot }, 0.0)
|
|
});
|
|
engine.register_fn("!=", |p: BoolParam, v: bool| {
|
|
condition(&p.name, if v { CondMode::IfNot } else { CondMode::If }, 0.0)
|
|
});
|
|
}
|
|
|
|
fn register_setup(engine: &mut Engine, aac: Aac) {
|
|
engine.register_fn("AnimatorAsCode", move || aac.clone());
|
|
|
|
engine.register_fn("system_name", |a: &mut Aac, name: &str| {
|
|
a.write(|graph| graph.system_name = name.to_string());
|
|
});
|
|
engine.register_fn("asset_key", |a: &mut Aac, key: &str| {
|
|
a.write(|graph| graph.asset_key = key.to_string());
|
|
});
|
|
|
|
engine.register_fn("float_param", |a: &mut Aac, name: &str, default: f64| -> Result<FloatParam, Box<EvalAltResult>> {
|
|
assert_new_parameter(a, name)?;
|
|
a.write(|graph| {
|
|
graph.parameters.push(Parameter::Float {
|
|
name: name.to_string(),
|
|
default: default as f32,
|
|
})
|
|
});
|
|
Ok(FloatParam {
|
|
name: name.to_string(),
|
|
})
|
|
});
|
|
engine.register_fn("int_param", |a: &mut Aac, name: &str, default: i64| -> Result<IntParam, Box<EvalAltResult>> {
|
|
assert_new_parameter(a, name)?;
|
|
a.write(|graph| {
|
|
graph.parameters.push(Parameter::Int {
|
|
name: name.to_string(),
|
|
default: default as i32,
|
|
})
|
|
});
|
|
Ok(IntParam {
|
|
name: name.to_string(),
|
|
})
|
|
});
|
|
engine.register_fn(
|
|
"bool_param",
|
|
|a: &mut Aac, name: &str, default: bool| -> Result<BoolParam, Box<EvalAltResult>> {
|
|
assert_new_parameter(a, name)?;
|
|
a.write(|graph| {
|
|
graph.parameters.push(Parameter::Bool {
|
|
name: name.to_string(),
|
|
default,
|
|
})
|
|
});
|
|
Ok(BoolParam {
|
|
name: name.to_string(),
|
|
})
|
|
},
|
|
);
|
|
|
|
engine.register_fn("new_controller", |a: &mut Aac| ControllerBuilder { aac: a.clone() });
|
|
}
|
|
|
|
fn register_navigation(engine: &mut Engine) {
|
|
// There is exactly one controller, so this is the only place layers are attached.
|
|
engine.register_fn("layer", |c: ControllerBuilder, name: &str| -> LayerBuilder {
|
|
let layer = c.aac.write(|graph| {
|
|
graph.controller.layers.push(Layer {
|
|
name: name.to_string(),
|
|
..Default::default()
|
|
});
|
|
graph.controller.layers.len() - 1
|
|
});
|
|
LayerBuilder {
|
|
aac: c.aac.clone(),
|
|
layer,
|
|
}
|
|
});
|
|
|
|
engine.register_fn("state", |l: LayerBuilder, name: &str, x: i64, y: i64| {
|
|
add_state(&l.aac, l.layer, &[], name, x, y)
|
|
});
|
|
engine.register_fn("state", |m: MachineRef, name: &str, x: i64, y: i64| {
|
|
add_state(&m.aac, m.layer, &m.machine, name, x, y)
|
|
});
|
|
|
|
engine.register_fn("sub_machine", |l: LayerBuilder, name: &str, x: i64, y: i64| {
|
|
add_sub_machine(&l.aac, l.layer, &[], name, x, y)
|
|
});
|
|
engine.register_fn("sub_machine", |m: MachineRef, name: &str, x: i64, y: i64| {
|
|
add_sub_machine(&m.aac, m.layer, &m.machine, name, x, y)
|
|
});
|
|
|
|
engine.register_fn("any_state", |l: LayerBuilder| AnyStateRef {
|
|
aac: l.aac.clone(),
|
|
layer: l.layer,
|
|
machine: Vec::new(),
|
|
});
|
|
engine.register_fn("any_state", |m: MachineRef| AnyStateRef {
|
|
aac: m.aac.clone(),
|
|
layer: m.layer,
|
|
machine: m.machine.clone(),
|
|
});
|
|
}
|
|
|
|
fn register_transitions(engine: &mut Engine) {
|
|
engine.register_fn("transition_to", |from: StateRef, to: StateRef| from.transition_to(&to));
|
|
engine.register_fn("transition_to", |from: AnyStateRef, to: StateRef| from.transition_to(&to));
|
|
|
|
engine.register_fn("when", |t: TransitionRef, c: Condition| {
|
|
t.add_conditions([c]);
|
|
t
|
|
});
|
|
engine.register_fn(
|
|
"when_all",
|
|
|t: TransitionRef, conditions: Array| -> Result<TransitionRef, Box<EvalAltResult>> {
|
|
let mut collected = Vec::with_capacity(conditions.len());
|
|
for value in conditions {
|
|
collected.push(
|
|
value
|
|
.try_cast::<Condition>()
|
|
.ok_or_else(|| err("when_all expects a list of conditions".to_string()))?,
|
|
);
|
|
}
|
|
t.add_conditions(collected);
|
|
Ok(t)
|
|
},
|
|
);
|
|
|
|
engine.register_fn("duration", |t: TransitionRef, seconds: f64| {
|
|
t.update(|transition| transition.duration = seconds as f32);
|
|
t
|
|
});
|
|
engine.register_fn("no_exit_time", |t: TransitionRef| {
|
|
t.update(|transition| {
|
|
transition.has_exit_time = false;
|
|
transition.exit_time = 0.0;
|
|
});
|
|
t
|
|
});
|
|
engine.register_fn("exit_time", |t: TransitionRef, normalized: f64| {
|
|
t.update(|transition| {
|
|
transition.has_exit_time = true;
|
|
transition.exit_time = normalized as f32;
|
|
});
|
|
t
|
|
});
|
|
engine.register_fn("ordered_interruption", |t: TransitionRef, value: bool| {
|
|
t.update(|transition| transition.ordered_interruption = value);
|
|
t
|
|
});
|
|
engine.register_fn("source_interruption", |t: TransitionRef| {
|
|
t.update(|transition| transition.source_interruption = true);
|
|
t
|
|
});
|
|
engine.register_fn("to_self", |t: TransitionRef| {
|
|
t.update(|transition| transition.can_transition_to_self = true);
|
|
t
|
|
});
|
|
}
|