use crate::graph::*; use std::sync::{Arc, Mutex, MutexGuard}; /// Shared, thread-safe handle to the graph being built. Every builder carries a clone. #[derive(Clone)] pub struct Aac { pub(crate) graph: Arc>, } impl Default for Aac { fn default() -> Self { Self::new() } } impl Aac { pub fn new() -> Self { Self { graph: Arc::new(Mutex::new(ControllerGraph::default())), } } /// A poisoned lock still holds a usable graph; recovering avoids panicking across the FFI boundary. fn lock(&self) -> MutexGuard<'_, ControllerGraph> { self.graph.lock().unwrap_or_else(|e| e.into_inner()) } pub fn write(&self, f: impl FnOnce(&mut ControllerGraph) -> R) -> R { f(&mut self.lock()) } } /// Parameter handles only carry the name; conditions read nothing else from them. #[derive(Clone)] pub struct FloatParam { pub(crate) name: String, } #[derive(Clone)] pub struct IntParam { pub(crate) name: String, } #[derive(Clone)] pub struct BoolParam { pub(crate) name: String, } #[derive(Clone)] pub struct ControllerBuilder { pub(crate) aac: Aac, } #[derive(Clone)] pub struct LayerBuilder { pub(crate) aac: Aac, pub(crate) layer: usize, } /// A state machine. `machine` is the chain of sub-machine indices from the layer root, so the empty /// chain is the layer's root machine and indices stay stable because machines are only ever appended. #[derive(Clone)] pub struct MachineRef { pub(crate) aac: Aac, pub(crate) layer: usize, pub(crate) machine: Vec, } #[derive(Clone)] pub struct StateRef { pub(crate) aac: Aac, pub(crate) layer: usize, pub(crate) machine: Vec, pub(crate) state: usize, } #[derive(Clone)] pub struct AnyStateRef { pub(crate) aac: Aac, pub(crate) layer: usize, pub(crate) machine: Vec, } #[derive(Clone)] pub struct ClipBuilder { pub(crate) aac: Aac, 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, pub(crate) name: String, } /// `source` is the originating state, or `None` for an any-state transition. #[derive(Clone)] pub struct TransitionRef { pub(crate) aac: Aac, pub(crate) layer: usize, pub(crate) machine: Vec, pub(crate) source: Option, pub(crate) index: usize, } pub(crate) fn machine_mut<'a>( graph: &'a mut ControllerGraph, layer: usize, path: &[usize], ) -> &'a mut StateMachine { let mut machine = &mut graph.controller.layers[layer].state_machine; for &index in path { machine = &mut machine.sub_machines[index]; } machine } pub(crate) fn clip_mut<'a>(graph: &'a mut ControllerGraph, name: &str) -> Option<&'a mut ClipData> { graph.clips.iter_mut().find(|clip| clip.name == name) } pub(crate) fn blend_tree_mut<'a>( graph: &'a mut ControllerGraph, name: &str, ) -> Option<&'a mut BlendTreeData> { graph.blend_trees.iter_mut().find(|tree| tree.name == name) } impl StateRef { pub fn state_name(&self) -> String { self.aac .write(|graph| machine_mut(graph, self.layer, &self.machine).states[self.state].name.clone()) } pub fn set_motion(&self, motion: MotionRef) { self.aac.write(|graph| { machine_mut(graph, self.layer, &self.machine).states[self.state].motion = Some(motion); }); } pub fn transition_to(&self, target: &StateRef) -> TransitionRef { let to = target.state_name(); let index = self.aac.write(|graph| { let state = &mut machine_mut(graph, self.layer, &self.machine).states[self.state]; state.transitions.push(Transition::new(to)); state.transitions.len() - 1 }); TransitionRef { aac: self.aac.clone(), layer: self.layer, machine: self.machine.clone(), source: Some(self.state), index, } } } impl AnyStateRef { pub fn transition_to(&self, target: &StateRef) -> TransitionRef { let to = target.state_name(); let index = self.aac.write(|graph| { let machine = machine_mut(graph, self.layer, &self.machine); machine.any_state_transitions.push(Transition::new(to)); machine.any_state_transitions.len() - 1 }); TransitionRef { aac: self.aac.clone(), layer: self.layer, machine: self.machine.clone(), source: None, index, } } } impl TransitionRef { /// Apply a mutation to the transition this handle points at. pub fn update(&self, f: impl FnOnce(&mut Transition)) { self.aac.write(|graph| { let machine = machine_mut(graph, self.layer, &self.machine); let transitions = match self.source { Some(state) => &mut machine.states[state].transitions, None => &mut machine.any_state_transitions, }; f(&mut transitions[self.index]); }); } pub fn add_conditions(&self, conditions: impl IntoIterator) { self.update(|transition| transition.conditions.extend(conditions)); } } impl ClipBuilder { pub fn data_mut(&self, f: impl FnOnce(&mut ClipData) -> R) -> Option { let name = self.name.clone(); 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(|| { format!( "cannot tell which component `{property}` belongs to; \ known properties are `m_IsActive` and `blendShape.*`" ) })?; self.data_mut(|clip| { clip.curve_mut(path, target, property).keys.push(key); }) .ok_or_else(|| format!("unknown clip `{}`", self.name)) } } impl AnimationStore { /// Resolve `folder/.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 { 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(&self, f: impl FnOnce(&mut BlendTreeData) -> R) -> Option { let name = self.name.clone(); self.aac.write(|graph| blend_tree_mut(graph, &name).map(f)) } pub fn configure(&self, blend_type: BlendType, param_x: &str, param_y: Option<&str>) -> Result<(), String> { self.data_mut(|tree| { tree.blend_type = blend_type; tree.param_x = param_x.to_string(); tree.param_y = param_y.map(str::to_string); }) .ok_or_else(|| format!("unknown blend tree `{}`", self.name)) } pub fn push_child(&self, child: BlendChild) -> Result<(), String> { self.data_mut(|tree| tree.children.push(child)) .ok_or_else(|| format!("unknown blend tree `{}`", self.name)) } }