concinnity_core/gfx/anim_graph/graph.rs
1// The compiled state machine: its states, transitions, and parameters, plus the
2// clip-duration refresh a hot-reload applies.
3
4use super::StatePlay;
5use alloc::string::String;
6use alloc::vec::Vec;
7
8/// Comparison operator for a transition condition, evaluated as
9/// `parameter <op> value`. `eq` / `ne` compare exactly and are mainly useful
10/// for flag-like parameters holding whole numbers such as 0 and 1.
11#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, serde::Serialize, serde::Deserialize)]
12#[serde(rename_all = "lowercase")]
13pub enum CmpOp {
14 /// Less than.
15 #[default]
16 Lt,
17 /// Less than or equal.
18 Le,
19 /// Greater than.
20 Gt,
21 /// Greater than or equal.
22 Ge,
23 /// Equal (exact).
24 Eq,
25 /// Not equal (exact).
26 Ne,
27}
28
29impl CmpOp {
30 /// Evaluate `lhs <op> rhs`.
31 pub fn eval(self, lhs: f32, rhs: f32) -> bool {
32 match self {
33 CmpOp::Lt => lhs < rhs,
34 CmpOp::Le => lhs <= rhs,
35 CmpOp::Gt => lhs > rhs,
36 CmpOp::Ge => lhs >= rhs,
37 CmpOp::Eq => lhs == rhs,
38 CmpOp::Ne => lhs != rhs,
39 }
40 }
41}
42
43/// One compiled transition condition: parameter names are resolved to indices
44/// into the graph's parameter vector at compile time, so evaluation is a
45/// direct slice read (the runtime blob interner is empty; nothing resolves
46/// names at runtime).
47#[derive(Debug, Clone)]
48pub struct CompiledCondition {
49 /// Index into the graph's parameter vector.
50 pub param: usize,
51 /// Comparison applied between the parameter and `value`.
52 pub op: CmpOp,
53 /// The value the parameter is compared against.
54 pub value: f32,
55}
56
57/// One compiled outgoing transition. Conditions AND together; an empty list is
58/// always true (useful with `exit_time` alone).
59#[derive(Debug, Clone)]
60pub struct CompiledTransition {
61 /// Index of the state this transition enters.
62 pub to: usize,
63 /// Crossfade length between the outgoing and incoming state poses. Zero
64 /// snaps.
65 pub duration_secs: f32,
66 /// When set, the transition is gated until the state's normalized time
67 /// reaches this value (see `normalized_time`).
68 pub exit_time: Option<f32>,
69 /// Conditions that must all hold; an empty list is always true.
70 pub conditions: Vec<CompiledCondition>,
71}
72
73/// One compiled state: what it plays (a clip or a blendspace, at `rate`) and
74/// its outgoing transitions in declaration order (first match wins).
75#[derive(Debug, Clone)]
76pub struct CompiledState {
77 /// The state's authored name.
78 pub name: String,
79 /// Clock scale; 1.0 plays at authored speed.
80 pub rate: f32,
81 /// Whether this state's clock wraps. Single-clip states default to the
82 /// clip's own flag, blendspaces default to wrapping; `loop_override`
83 /// wins over both.
84 pub looping: bool,
85 /// What the state plays.
86 pub play: StatePlay,
87 /// Outgoing transitions in declaration order; first match wins.
88 pub transitions: Vec<CompiledTransition>,
89}
90
91/// A graph parameter: a named float, seeded to `default`.
92#[derive(Debug, Clone)]
93pub struct ParamSpec {
94 /// The parameter's authored name.
95 pub name: String,
96 /// Value the parameter is seeded with.
97 pub default: f32,
98}
99
100/// A compiled animation state machine. Built once from the `AnimationGraph` asset;
101/// read-only afterwards except for clip-duration refresh on hot-reload.
102#[derive(Debug, Clone)]
103pub struct CompiledGraph {
104 /// The graph's parameters, in index order.
105 pub params: Vec<ParamSpec>,
106 /// The graph's states, in index order.
107 pub states: Vec<CompiledState>,
108 /// Index of the state the graph starts in.
109 pub initial: usize,
110}
111
112impl CompiledGraph {
113 /// Index of a parameter by name, for surfaces (debug commands) that still
114 /// speak names.
115 pub fn param_index(&self, name: &str) -> Option<usize> {
116 self.params.iter().position(|p| p.name == name)
117 }
118
119 /// The parameter vector seeded to each parameter's declared default.
120 pub fn default_params(&self) -> Vec<f32> {
121 self.params.iter().map(|p| p.default).collect()
122 }
123
124 /// Update every member playing `clip` to a new duration, so wrap and
125 /// exit-time math keep tracking a hot-reloaded clip.
126 pub fn refresh_clip_duration(&mut self, clip: usize, duration_secs: f32) {
127 for state in &mut self.states {
128 for member in state.play.members_mut() {
129 if member.clip == clip {
130 member.duration_secs = duration_secs;
131 }
132 }
133 }
134 }
135}