use serde::{Deserialize, Serialize}; /// The complete description of one animator controller, produced entirely by a Rhai script. #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct ControllerGraph { pub system_name: String, pub asset_key: String, pub parameters: Vec, pub clips: Vec, pub blend_trees: Vec, pub controller: Controller, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Controller { pub layers: Vec, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct Layer { /// The layer suffix as written in the script. Unity's layer name is derived from it by the C# side. pub name: String, pub state_machine: StateMachine, } /// Grid position in Unity's animator graph. Serialized as an object so C# can bind it directly. #[derive(Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize)] pub struct GridPos { pub x: i32, pub y: i32, } impl GridPos { pub fn new(x: i32, y: i32) -> Self { Self { x, y } } } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct StateMachine { /// `None` for a layer's root machine, `Some` for a sub-state machine. pub name: Option, pub position: GridPos, pub states: Vec, pub sub_machines: Vec, pub any_state_transitions: Vec, } #[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)] pub struct State { pub name: String, pub position: GridPos, pub motion: Option, pub transitions: Vec, } /// References to entries in `ControllerGraph::clips` / `ControllerGraph::blend_trees`. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum MotionRef { Clip { name: String }, BlendTree { name: String }, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Transition { pub to: String, pub conditions: Vec, pub has_exit_time: bool, pub exit_time: f32, pub duration: f32, pub ordered_interruption: bool, pub source_interruption: bool, pub can_transition_to_self: bool, } impl Transition { /// Mirrors `AacDefaultsProvider.ConfigureTransition`, so unspecified fields match library defaults. pub fn new(to: impl Into) -> Self { Self { to: to.into(), conditions: Vec::new(), has_exit_time: false, exit_time: 0.0, duration: 0.0, ordered_interruption: true, source_interruption: false, can_transition_to_self: false, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Condition { pub parameter: String, pub mode: CondMode, pub threshold: f32, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum CondMode { Greater, Less, Equals, NotEqual, If, IfNot, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct ClipData { pub name: String, /// `None` for a store clip that leaves the source asset's looping setting alone. #[serde(default, skip_serializing_if = "Option::is_none")] pub looping: Option, pub curves: Vec, /// Set when the clip is seeded from an asset (see `AnimationStore`): the project-relative asset /// path. Omitted for generated clips so their JSON stays unchanged. #[serde(default, skip_serializing_if = "Option::is_none")] pub source: Option, } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct Curve { pub path: String, pub target: TargetType, pub property: String, pub keys: Vec, } /// The Unity component a curve binds to. Closed set: an unknown property is an error, not a guess. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum TargetType { GameObject, SkinnedMeshRenderer, } #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub struct Keyframe { pub time: f32, pub value: f32, pub in_tangent: f32, pub out_tangent: f32, } impl Keyframe { pub fn linear(time: f32, value: f32) -> Self { Self { time, value, in_tangent: 0.0, out_tangent: 0.0, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BlendTreeData { pub name: String, pub blend_type: BlendType, pub param_x: String, pub param_y: Option, pub children: Vec, pub use_automatic_thresholds: bool, } /// `rename_all = "snake_case"` would turn `SimpleDirectional2D` into `simple_directional2_d`, /// so every variant is renamed explicitly. #[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)] pub enum BlendType { #[serde(rename = "simple_1d")] Simple1D, #[serde(rename = "simple_directional_2d")] SimpleDirectional2D, #[serde(rename = "freeform_directional_2d")] FreeformDirectional2D, #[serde(rename = "freeform_cartesian_2d")] FreeformCartesian2D, #[serde(rename = "direct")] Direct, } impl BlendType { pub fn is_2d(self) -> bool { matches!( self, BlendType::SimpleDirectional2D | BlendType::FreeformDirectional2D | BlendType::FreeformCartesian2D ) } /// Blend trees that place children by threshold rather than by a direct blend parameter. pub fn uses_thresholds(self) -> bool { self != BlendType::Direct } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub struct BlendChild { pub motion: MotionRef, pub threshold: f32, pub threshold_y: Option, /// Only meaningful on `Direct` blend trees. pub direct_param: Option, } impl BlendChild { pub fn motion(motion: MotionRef) -> Self { Self { motion, threshold: 0.0, threshold_y: None, direct_param: None, } } } #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] #[serde(tag = "type", rename_all = "snake_case")] pub enum Parameter { Float { name: String, default: f32 }, Int { name: String, default: i32 }, Bool { name: String, default: bool }, } impl Parameter { pub fn name(&self) -> &str { match self { Parameter::Float { name, .. } | Parameter::Int { name, .. } | Parameter::Bool { name, .. } => name, } } } /// Property-name -> component-type inference. Deliberately strict; an unmapped property is an error. pub fn infer_target(property: &str) -> Option { if property.starts_with("blendShape.") { return Some(TargetType::SkinnedMeshRenderer); } match property { "m_IsActive" => Some(TargetType::GameObject), _ => None, } } impl ClipData { /// Find-or-create the curve for one (path, target, property) binding. pub fn curve_mut(&mut self, path: &str, target: TargetType, property: &str) -> &mut Curve { let index = match self .curves .iter() .position(|c| c.path == path && c.target == target && c.property == property) { Some(index) => index, None => { self.curves.push(Curve { path: path.to_string(), target, property: property.to_string(), keys: Vec::new(), }); self.curves.len() - 1 } }; &mut self.curves[index] } }