Skip to main content

proof_engine/animation/
mod.rs

1//! Animation State Machine and Blend Trees.
2//!
3//! Provides a complete animation runtime for Proof Engine entities:
4//!
5//! - `AnimationClip`     — named sequence of keyframe channels over time
6//! - `AnimationCurve`    — per-property float curve (driven by MathFunction or raw keyframes)
7//! - `BlendTree`         — 1D/2D weighted blend of multiple clips
8//! - `AnimationLayer`    — masked layer (e.g., upper-body, full-body)
9//! - `AnimationState`    — named state that plays a clip or blend tree
10//! - `Transition`        — condition-triggered crossfade between states
11//! - `AnimatorController`— top-level controller driving all layers
12//!
13//! ## Quick Start
14//! ```rust,no_run
15//! use proof_engine::animation::*;
16//! let mut ctrl = AnimatorController::new();
17//! ctrl.add_state("idle",   AnimationState::clip("idle_clip",   1.0, true));
18//! ctrl.add_state("run",    AnimationState::clip("run_clip",    0.8, true));
19//! ctrl.add_state("attack", AnimationState::clip("attack_clip", 0.4, false));
20//! ctrl.add_transition("idle",   "run",    Condition::float_gt("speed", 0.1));
21//! ctrl.add_transition("run",    "idle",   Condition::float_lt("speed", 0.05));
22//! ctrl.add_transition("idle",   "attack", Condition::trigger("attack"));
23//! ctrl.start("idle");
24//! ```
25
26pub mod ik;
27
28use std::collections::HashMap;
29use crate::math::MathFunction;
30
31// ── AnimationCurve ─────────────────────────────────────────────────────────────
32
33/// A single float channel over normalized time [0, 1].
34#[derive(Debug, Clone)]
35pub enum AnimationCurve {
36    /// Constant value.
37    Constant(f32),
38    /// Linear keyframes: list of (time, value) pairs sorted by time.
39    Keyframes(Vec<(f32, f32)>),
40    /// Driven entirely by a MathFunction evaluated at time t.
41    MathDriven(MathFunction),
42    /// Cubic bezier keyframes: (time, value, in_tangent, out_tangent).
43    BezierKeyframes(Vec<BezierKey>),
44}
45
46#[derive(Debug, Clone)]
47pub struct BezierKey {
48    pub time:        f32,
49    pub value:       f32,
50    pub in_tangent:  f32,
51    pub out_tangent: f32,
52}
53
54impl AnimationCurve {
55    /// Evaluate the curve at normalized time `t` in [0, 1].
56    pub fn evaluate(&self, t: f32) -> f32 {
57        let t = t.clamp(0.0, 1.0);
58        match self {
59            AnimationCurve::Constant(v) => *v,
60            AnimationCurve::MathDriven(f) => f.evaluate(t, t),
61            AnimationCurve::Keyframes(keys) => {
62                if keys.is_empty() { return 0.0; }
63                if keys.len() == 1 { return keys[0].1; }
64                // Find surrounding keys
65                let idx = keys.partition_point(|(kt, _)| *kt <= t);
66                if idx == 0 { return keys[0].1; }
67                if idx >= keys.len() { return keys[keys.len()-1].1; }
68                let (t0, v0) = keys[idx-1];
69                let (t1, v1) = keys[idx];
70                let span = (t1 - t0).max(1e-7);
71                let alpha = (t - t0) / span;
72                v0 + (v1 - v0) * alpha
73            }
74            AnimationCurve::BezierKeyframes(keys) => {
75                if keys.is_empty() { return 0.0; }
76                if keys.len() == 1 { return keys[0].value; }
77                let idx = keys.partition_point(|k| k.time <= t);
78                if idx == 0 { return keys[0].value; }
79                if idx >= keys.len() { return keys[keys.len()-1].value; }
80                let k0 = &keys[idx-1];
81                let k1 = &keys[idx];
82                let span = (k1.time - k0.time).max(1e-7);
83                let u = (t - k0.time) / span;
84                // Cubic Hermite
85                let h00 = 2.0*u*u*u - 3.0*u*u + 1.0;
86                let h10 = u*u*u - 2.0*u*u + u;
87                let h01 = -2.0*u*u*u + 3.0*u*u;
88                let h11 = u*u*u - u*u;
89                h00*k0.value + h10*span*k0.out_tangent + h01*k1.value + h11*span*k1.in_tangent
90            }
91        }
92    }
93
94    /// Build a constant curve.
95    pub fn constant(v: f32) -> Self { Self::Constant(v) }
96
97    /// Build a linear ramp from `a` at t=0 to `b` at t=1.
98    pub fn linear(a: f32, b: f32) -> Self {
99        Self::Keyframes(vec![(0.0, a), (1.0, b)])
100    }
101
102    /// Build a curve from raw (time, value) pairs.
103    pub fn from_keys(mut keys: Vec<(f32, f32)>) -> Self {
104        keys.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap());
105        Self::Keyframes(keys)
106    }
107}
108
109// ── AnimationClip ──────────────────────────────────────────────────────────────
110
111/// Named set of channels that animate properties over time.
112#[derive(Debug, Clone)]
113pub struct AnimationClip {
114    pub name:     String,
115    pub duration: f32,
116    pub looping:  bool,
117    /// Property path -> curve. E.g. "position.x", "scale.x", "color.r".
118    pub channels: HashMap<String, AnimationCurve>,
119    /// Events fired at specific normalized times.
120    pub events:   Vec<AnimationEvent>,
121}
122
123#[derive(Debug, Clone)]
124pub struct AnimationEvent {
125    /// Normalized time [0, 1] when this fires.
126    pub time:    f32,
127    /// Arbitrary tag passed to event handlers.
128    pub tag:     String,
129    pub payload: f32,
130}
131
132impl AnimationClip {
133    pub fn new(name: impl Into<String>, duration: f32, looping: bool) -> Self {
134        Self {
135            name: name.into(),
136            duration,
137            looping,
138            channels: HashMap::new(),
139            events: Vec::new(),
140        }
141    }
142
143    /// Add a channel curve.
144    pub fn with_channel(mut self, path: impl Into<String>, curve: AnimationCurve) -> Self {
145        self.channels.insert(path.into(), curve);
146        self
147    }
148
149    /// Add an event that fires at normalized time `t`.
150    pub fn with_event(mut self, t: f32, tag: impl Into<String>, payload: f32) -> Self {
151        self.events.push(AnimationEvent { time: t, tag: tag.into(), payload });
152        self
153    }
154
155    /// Sample all channels at normalized time `t`, returning (path, value) pairs.
156    pub fn sample(&self, t: f32) -> Vec<(String, f32)> {
157        self.channels.iter()
158            .map(|(path, curve)| (path.clone(), curve.evaluate(t)))
159            .collect()
160    }
161}
162
163// ── BlendTree ─────────────────────────────────────────────────────────────────
164
165/// Blend mode for a BlendTree.
166#[derive(Debug, Clone)]
167pub enum BlendTreeKind {
168    /// Blend between clips based on a single float parameter.
169    Linear1D { param: String, thresholds: Vec<f32> },
170    /// Blend between clips in a 2D parameter space.
171    Cartesian2D { param_x: String, param_y: String, positions: Vec<(f32, f32)> },
172    /// Additive blend — base clip plus additive clips.
173    Additive { base_index: usize },
174    /// Override blend — highest-weight non-zero clip wins.
175    Override,
176}
177
178#[derive(Debug, Clone)]
179pub struct BlendTree {
180    pub kind:   BlendTreeKind,
181    pub clips:  Vec<AnimationClip>,
182    pub weights: Vec<f32>,
183}
184
185impl BlendTree {
186    /// Compute blend weights given current parameter values.
187    pub fn compute_weights(&mut self, params: &HashMap<String, ParamValue>) {
188        let n = self.clips.len();
189        if n == 0 { return; }
190        self.weights.resize(n, 0.0);
191
192        match &self.kind {
193            BlendTreeKind::Linear1D { param, thresholds } => {
194                let v = params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0);
195                let thresholds = thresholds.clone();
196                if thresholds.len() != n { return; }
197                // Find surrounding pair
198                let idx = thresholds.partition_point(|&t| t <= v);
199                for w in &mut self.weights { *w = 0.0; }
200                if idx == 0 {
201                    self.weights[0] = 1.0;
202                } else if idx >= n {
203                    self.weights[n-1] = 1.0;
204                } else {
205                    let t0 = thresholds[idx-1];
206                    let t1 = thresholds[idx];
207                    let span = (t1 - t0).max(1e-7);
208                    let alpha = (v - t0) / span;
209                    self.weights[idx-1] = 1.0 - alpha;
210                    self.weights[idx]   = alpha;
211                }
212            }
213            BlendTreeKind::Cartesian2D { param_x, param_y, positions } => {
214                let px = params.get(param_x).and_then(|p| p.as_float()).unwrap_or(0.0);
215                let py = params.get(param_y).and_then(|p| p.as_float()).unwrap_or(0.0);
216                let positions = positions.clone();
217                // Inverse Distance Weighting
218                let dists: Vec<f32> = positions.iter()
219                    .map(|(x, y)| ((px - x).powi(2) + (py - y).powi(2)).sqrt().max(1e-6))
220                    .collect();
221                let sum: f32 = dists.iter().map(|d| 1.0 / d).sum();
222                for (i, d) in dists.iter().enumerate() {
223                    self.weights[i] = (1.0 / d) / sum.max(1e-7);
224                }
225            }
226            BlendTreeKind::Additive { .. } | BlendTreeKind::Override => {
227                // Weights set externally
228            }
229        }
230    }
231
232    /// Sample blended output at normalized time `t`.
233    pub fn sample(&self, t: f32) -> Vec<(String, f32)> {
234        if self.clips.is_empty() { return Vec::new(); }
235        let mut accum: HashMap<String, f32> = HashMap::new();
236        let mut total_weight = 0.0_f32;
237
238        for (clip, &w) in self.clips.iter().zip(self.weights.iter()) {
239            if w < 1e-6 { continue; }
240            total_weight += w;
241            for (path, val) in clip.sample(t) {
242                *accum.entry(path).or_insert(0.0) += val * w;
243            }
244        }
245
246        if total_weight > 1e-6 {
247            for v in accum.values_mut() { *v /= total_weight; }
248        }
249        accum.into_iter().collect()
250    }
251}
252
253// ── Condition ─────────────────────────────────────────────────────────────────
254
255/// Condition that must be satisfied for a state transition to fire.
256#[derive(Debug, Clone)]
257pub enum Condition {
258    FloatGt { param: String, threshold: f32 },
259    FloatLt { param: String, threshold: f32 },
260    FloatGe { param: String, threshold: f32 },
261    FloatLe { param: String, threshold: f32 },
262    FloatEq { param: String, value: f32, tolerance: f32 },
263    BoolTrue  { param: String },
264    BoolFalse { param: String },
265    /// One-shot: fires once then resets to false.
266    Trigger   { param: String },
267    /// Always true — transition fires immediately when source state exits.
268    Always,
269    /// Multiple conditions all true.
270    All(Vec<Condition>),
271    /// At least one condition true.
272    Any(Vec<Condition>),
273}
274
275impl Condition {
276    pub fn float_gt(param: impl Into<String>, v: f32) -> Self {
277        Self::FloatGt { param: param.into(), threshold: v }
278    }
279    pub fn float_lt(param: impl Into<String>, v: f32) -> Self {
280        Self::FloatLt { param: param.into(), threshold: v }
281    }
282    pub fn float_ge(param: impl Into<String>, v: f32) -> Self {
283        Self::FloatGe { param: param.into(), threshold: v }
284    }
285    pub fn float_le(param: impl Into<String>, v: f32) -> Self {
286        Self::FloatLe { param: param.into(), threshold: v }
287    }
288    pub fn bool_true(param: impl Into<String>) -> Self {
289        Self::BoolTrue { param: param.into() }
290    }
291    pub fn trigger(param: impl Into<String>) -> Self {
292        Self::Trigger { param: param.into() }
293    }
294
295    /// Evaluate against current params. Returns (satisfied, consumed_triggers).
296    pub fn evaluate(&self, params: &HashMap<String, ParamValue>) -> (bool, Vec<String>) {
297        match self {
298            Self::FloatGt { param, threshold } =>
299                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) > *threshold, vec![]),
300            Self::FloatLt { param, threshold } =>
301                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) < *threshold, vec![]),
302            Self::FloatGe { param, threshold } =>
303                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) >= *threshold, vec![]),
304            Self::FloatLe { param, threshold } =>
305                (params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0) <= *threshold, vec![]),
306            Self::FloatEq { param, value, tolerance } => {
307                let v = params.get(param).and_then(|p| p.as_float()).unwrap_or(0.0);
308                ((v - value).abs() <= *tolerance, vec![])
309            }
310            Self::BoolTrue  { param } =>
311                (params.get(param).and_then(|p| p.as_bool()).unwrap_or(false), vec![]),
312            Self::BoolFalse { param } =>
313                (!params.get(param).and_then(|p| p.as_bool()).unwrap_or(false), vec![]),
314            Self::Trigger { param } => {
315                let v = params.get(param).and_then(|p| p.as_bool()).unwrap_or(false);
316                if v { (true, vec![param.clone()]) } else { (false, vec![]) }
317            }
318            Self::Always => (true, vec![]),
319            Self::All(conds) => {
320                let mut consumed = Vec::new();
321                for c in conds {
322                    let (ok, mut trig) = c.evaluate(params);
323                    if !ok { return (false, vec![]); }
324                    consumed.append(&mut trig);
325                }
326                (true, consumed)
327            }
328            Self::Any(conds) => {
329                for c in conds {
330                    let (ok, trig) = c.evaluate(params);
331                    if ok { return (true, trig); }
332                }
333                (false, vec![])
334            }
335        }
336    }
337}
338
339// ── ParamValue ────────────────────────────────────────────────────────────────
340
341#[derive(Debug, Clone)]
342pub enum ParamValue {
343    Float(f32),
344    Bool(bool),
345    Int(i32),
346}
347
348impl ParamValue {
349    pub fn as_float(&self) -> Option<f32> {
350        match self {
351            Self::Float(v) => Some(*v),
352            Self::Int(v) => Some(*v as f32),
353            _ => None,
354        }
355    }
356    pub fn as_bool(&self) -> Option<bool> {
357        if let Self::Bool(v) = self { Some(*v) } else { None }
358    }
359    pub fn as_int(&self) -> Option<i32> {
360        if let Self::Int(v) = self { Some(*v) } else { None }
361    }
362}
363
364// ── Transition ────────────────────────────────────────────────────────────────
365
366#[derive(Debug, Clone)]
367pub struct Transition {
368    pub from:            String,
369    pub to:              String,
370    pub condition:       Condition,
371    /// Crossfade duration in seconds.
372    pub duration:        f32,
373    /// If true, can interrupt an existing transition.
374    pub can_interrupt:   bool,
375    /// Minimum time in source state before transition is eligible (seconds).
376    pub exit_time:       Option<f32>,
377    /// Normalized exit time: transition fires when state reaches this fraction.
378    pub normalized_exit: Option<f32>,
379}
380
381impl Transition {
382    pub fn new(from: impl Into<String>, to: impl Into<String>, cond: Condition) -> Self {
383        Self {
384            from: from.into(),
385            to: to.into(),
386            condition: cond,
387            duration: 0.15,
388            can_interrupt: false,
389            exit_time: None,
390            normalized_exit: None,
391        }
392    }
393
394    pub fn with_duration(mut self, d: f32) -> Self { self.duration = d; self }
395    pub fn interruptible(mut self) -> Self { self.can_interrupt = true; self }
396    pub fn exit_at(mut self, t: f32) -> Self { self.exit_time = Some(t); self }
397    pub fn exit_normalized(mut self, t: f32) -> Self { self.normalized_exit = Some(t); self }
398}
399
400// ── AnimationState ────────────────────────────────────────────────────────────
401
402#[derive(Debug, Clone)]
403pub enum StateContent {
404    Clip(AnimationClip),
405    Tree(BlendTree),
406}
407
408#[derive(Debug, Clone)]
409pub struct AnimationState {
410    pub name:       String,
411    pub content:    StateContent,
412    pub speed:      f32,
413    pub mirror:     bool,
414    pub cyclic_offset: f32,
415    /// MathFunction modulating playback speed over normalized time.
416    pub speed_curve: Option<MathFunction>,
417}
418
419impl AnimationState {
420    pub fn clip(clip: AnimationClip) -> Self {
421        let name = clip.name.clone();
422        Self {
423            name,
424            content: StateContent::Clip(clip),
425            speed: 1.0,
426            mirror: false,
427            cyclic_offset: 0.0,
428            speed_curve: None,
429        }
430    }
431
432    pub fn tree(name: impl Into<String>, tree: BlendTree) -> Self {
433        Self {
434            name: name.into(),
435            content: StateContent::Tree(tree),
436            speed: 1.0,
437            mirror: false,
438            cyclic_offset: 0.0,
439            speed_curve: None,
440        }
441    }
442
443    pub fn with_speed(mut self, s: f32) -> Self { self.speed = s; self }
444    pub fn mirrored(mut self) -> Self { self.mirror = true; self }
445
446    pub fn duration(&self) -> f32 {
447        match &self.content {
448            StateContent::Clip(c) => c.duration,
449            StateContent::Tree(t) => t.clips.iter().map(|c| c.duration).fold(0.0, f32::max),
450        }
451    }
452}
453
454// ── AnimationLayer ────────────────────────────────────────────────────────────
455
456/// A layer runs its own state machine and blends on top of lower layers.
457#[derive(Debug, Clone)]
458pub struct AnimationLayer {
459    pub name:    String,
460    pub weight:  f32,
461    /// Property paths this layer affects. Empty = all properties.
462    pub mask:    Vec<String>,
463    pub additive: bool,
464    // Runtime state
465    pub current_state: Option<String>,
466    pub current_time:  f32,
467    pub transition:    Option<ActiveTransition>,
468}
469
470#[derive(Debug, Clone)]
471pub struct ActiveTransition {
472    pub target_state: String,
473    pub progress:     f32,   // 0..1
474    pub duration:     f32,
475    pub prev_time:    f32,
476    pub prev_state:   String,
477}
478
479impl AnimationLayer {
480    pub fn new(name: impl Into<String>) -> Self {
481        Self {
482            name: name.into(),
483            weight: 1.0,
484            mask: Vec::new(),
485            additive: false,
486            current_state: None,
487            current_time: 0.0,
488            transition: None,
489        }
490    }
491
492    pub fn with_weight(mut self, w: f32) -> Self { self.weight = w; self }
493    pub fn with_mask(mut self, mask: Vec<String>) -> Self { self.mask = mask; self }
494    pub fn as_additive(mut self) -> Self { self.additive = true; self }
495}
496
497// ── AnimatorController ────────────────────────────────────────────────────────
498
499/// Drives one or more AnimationLayers with shared parameter space.
500pub struct AnimatorController {
501    pub states:      HashMap<String, AnimationState>,
502    pub transitions: Vec<Transition>,
503    pub params:      HashMap<String, ParamValue>,
504    pub layers:      Vec<AnimationLayer>,
505    /// Fired events since last call to drain_events().
506    events:          Vec<FiredEvent>,
507    /// Clip library for fetching clips by name.
508    pub clips:       HashMap<String, AnimationClip>,
509}
510
511#[derive(Debug, Clone)]
512pub struct FiredEvent {
513    pub layer:   String,
514    pub state:   String,
515    pub tag:     String,
516    pub payload: f32,
517}
518
519/// Sampled property output from a full controller tick.
520#[derive(Debug, Default, Clone)]
521pub struct AnimationOutput {
522    pub channels: HashMap<String, f32>,
523}
524
525impl AnimatorController {
526    pub fn new() -> Self {
527        let mut layers = vec![AnimationLayer::new("Base Layer")];
528        layers[0].weight = 1.0;
529        Self {
530            states: HashMap::new(),
531            transitions: Vec::new(),
532            params: HashMap::new(),
533            layers,
534            events: Vec::new(),
535            clips: HashMap::new(),
536        }
537    }
538
539    // ── Builder ──────────────────────────────────────────────────────────────
540
541    pub fn add_state(&mut self, state: AnimationState) {
542        self.states.insert(state.name.clone(), state);
543    }
544
545    pub fn add_clip(&mut self, clip: AnimationClip) {
546        self.clips.insert(clip.name.clone(), clip);
547    }
548
549    pub fn add_transition(&mut self, t: Transition) {
550        self.transitions.push(t);
551    }
552
553    pub fn add_layer(&mut self, layer: AnimationLayer) {
554        self.layers.push(layer);
555    }
556
557    // ── Parameters ───────────────────────────────────────────────────────────
558
559    pub fn set_float(&mut self, name: &str, v: f32) {
560        self.params.insert(name.to_owned(), ParamValue::Float(v));
561    }
562
563    pub fn set_bool(&mut self, name: &str, v: bool) {
564        self.params.insert(name.to_owned(), ParamValue::Bool(v));
565    }
566
567    pub fn set_int(&mut self, name: &str, v: i32) {
568        self.params.insert(name.to_owned(), ParamValue::Int(v));
569    }
570
571    /// Set a trigger (one-shot bool that resets after being consumed).
572    pub fn set_trigger(&mut self, name: &str) {
573        self.params.insert(name.to_owned(), ParamValue::Bool(true));
574    }
575
576    pub fn get_float(&self, name: &str) -> f32 {
577        self.params.get(name).and_then(|p| p.as_float()).unwrap_or(0.0)
578    }
579
580    pub fn get_bool(&self, name: &str) -> bool {
581        self.params.get(name).and_then(|p| p.as_bool()).unwrap_or(false)
582    }
583
584    // ── Entry point ──────────────────────────────────────────────────────────
585
586    pub fn start(&mut self, state_name: &str) {
587        for layer in &mut self.layers {
588            layer.current_state = Some(state_name.to_owned());
589            layer.current_time  = 0.0;
590            layer.transition    = None;
591        }
592    }
593
594    pub fn start_layer(&mut self, layer_name: &str, state_name: &str) {
595        if let Some(layer) = self.layers.iter_mut().find(|l| l.name == layer_name) {
596            layer.current_state = Some(state_name.to_owned());
597            layer.current_time  = 0.0;
598            layer.transition    = None;
599        }
600    }
601
602    // ── Tick ─────────────────────────────────────────────────────────────────
603
604    /// Advance all layers by `dt` seconds and evaluate transitions.
605    /// Returns blended output across all layers.
606    pub fn tick(&mut self, dt: f32) -> AnimationOutput {
607        let mut output = AnimationOutput::default();
608
609        // Collect triggered params to reset
610        let mut consumed_triggers: Vec<String> = Vec::new();
611
612        for layer in &mut self.layers {
613            if layer.current_state.is_none() { continue; }
614            let cur_name = layer.current_state.clone().unwrap();
615
616            // Advance transition if active
617            if let Some(ref mut tr) = layer.transition {
618                tr.progress += dt / tr.duration.max(1e-4);
619                tr.prev_time += dt;
620                if tr.progress >= 1.0 {
621                    // Transition complete
622                    let new_state = tr.target_state.clone();
623                    layer.current_time = 0.0;
624                    layer.current_state = Some(new_state);
625                    layer.transition = None;
626                }
627            } else {
628                // Check transitions from current state
629                let applicable: Vec<Transition> = self.transitions.iter()
630                    .filter(|t| t.from == cur_name || t.from == "*")
631                    .cloned()
632                    .collect();
633
634                let state_dur = self.states.get(&cur_name).map(|s| s.duration()).unwrap_or(1.0);
635
636                for trans in applicable {
637                    // Check exit time constraints
638                    if let Some(min_exit) = trans.exit_time {
639                        if layer.current_time < min_exit { continue; }
640                    }
641                    if let Some(norm_exit) = trans.normalized_exit {
642                        let norm = if state_dur > 1e-6 { layer.current_time / state_dur } else { 1.0 };
643                        if norm < norm_exit { continue; }
644                    }
645
646                    let (ok, mut trig) = trans.condition.evaluate(&self.params);
647                    if ok {
648                        consumed_triggers.append(&mut trig);
649                        let prev = cur_name.clone();
650                        layer.transition = Some(ActiveTransition {
651                            target_state: trans.to.clone(),
652                            progress: 0.0,
653                            duration: trans.duration,
654                            prev_time: layer.current_time,
655                            prev_state: prev,
656                        });
657                        break;
658                    }
659                }
660
661                // Advance current state time
662                if let Some(state) = self.states.get(&cur_name) {
663                    let speed_mod = if let Some(ref sf) = state.speed_curve {
664                        let norm = if state.duration() > 1e-6 { layer.current_time / state.duration() } else { 0.0 };
665                        sf.evaluate(norm, norm)
666                    } else {
667                        1.0
668                    };
669                    layer.current_time += dt * state.speed * speed_mod;
670                    if state.duration() > 1e-6 {
671                        if let StateContent::Clip(ref clip) = state.content {
672                            if clip.looping {
673                                layer.current_time %= clip.duration.max(1e-4);
674                            } else {
675                                layer.current_time = layer.current_time.min(clip.duration);
676                            }
677                        }
678                    }
679                }
680            }
681
682            // Sample current state
683            let sample_t = {
684                let dur = self.states.get(layer.current_state.as_deref().unwrap_or(""))
685                    .map(|s| s.duration()).unwrap_or(1.0).max(1e-4);
686                (layer.current_time / dur).clamp(0.0, 1.0)
687            };
688
689            if let Some(state) = self.states.get(layer.current_state.as_deref().unwrap_or("")) {
690                let samples = match &state.content {
691                    StateContent::Clip(c) => c.sample(sample_t),
692                    StateContent::Tree(t) => t.sample(sample_t),
693                };
694                for (path, val) in samples {
695                    let entry = output.channels.entry(path).or_insert(0.0);
696                    if layer.additive {
697                        *entry += val * layer.weight;
698                    } else {
699                        *entry = *entry * (1.0 - layer.weight) + val * layer.weight;
700                    }
701                }
702            }
703        }
704
705        // Reset consumed triggers
706        for key in consumed_triggers {
707            self.params.insert(key, ParamValue::Bool(false));
708        }
709
710        output
711    }
712
713    /// Drain all fired animation events since last call.
714    pub fn drain_events(&mut self) -> Vec<FiredEvent> {
715        std::mem::take(&mut self.events)
716    }
717
718    /// Force the base layer into a specific state immediately.
719    pub fn play(&mut self, state_name: &str) {
720        if let Some(layer) = self.layers.first_mut() {
721            layer.current_state = Some(state_name.to_owned());
722            layer.current_time  = 0.0;
723            layer.transition    = None;
724        }
725    }
726
727    /// Cross-fade to a state over `duration` seconds.
728    pub fn cross_fade(&mut self, state_name: &str, duration: f32) {
729        if let Some(layer) = self.layers.first_mut() {
730            let prev = layer.current_state.clone().unwrap_or_default();
731            layer.transition = Some(ActiveTransition {
732                target_state: state_name.to_owned(),
733                progress: 0.0,
734                duration,
735                prev_time: layer.current_time,
736                prev_state: prev,
737            });
738        }
739    }
740
741    /// Current normalized time of the base layer's active state.
742    pub fn normalized_time(&self) -> f32 {
743        if let Some(layer) = self.layers.first() {
744            if let Some(name) = layer.current_state.as_deref() {
745                if let Some(state) = self.states.get(name) {
746                    let dur = state.duration().max(1e-4);
747                    return (layer.current_time / dur).clamp(0.0, 1.0);
748                }
749            }
750        }
751        0.0
752    }
753
754    /// Name of the currently active state on the base layer.
755    pub fn current_state(&self) -> Option<&str> {
756        self.layers.first()?.current_state.as_deref()
757    }
758
759    /// Whether the base layer is transitioning.
760    pub fn is_transitioning(&self) -> bool {
761        self.layers.first().map(|l| l.transition.is_some()).unwrap_or(false)
762    }
763}
764
765impl Default for AnimatorController {
766    fn default() -> Self { Self::new() }
767}
768
769// ── RootMotion ────────────────────────────────────────────────────────────────
770
771/// Root motion extracted from animation, applied to entity transform.
772#[derive(Debug, Clone, Default)]
773pub struct RootMotion {
774    pub delta_position: glam::Vec3,
775    pub delta_rotation: f32,
776    pub delta_scale:    glam::Vec3,
777}
778
779impl RootMotion {
780    pub fn from_output(output: &AnimationOutput) -> Self {
781        let get = |key: &str| output.channels.get(key).copied().unwrap_or(0.0);
782        Self {
783            delta_position: glam::Vec3::new(get("root.dx"), get("root.dy"), get("root.dz")),
784            delta_rotation: get("root.dr"),
785            delta_scale:    glam::Vec3::ONE,
786        }
787    }
788
789    pub fn is_zero(&self) -> bool {
790        self.delta_position.length_squared() < 1e-10 && self.delta_rotation.abs() < 1e-6
791    }
792}
793
794// ── AnimationMirror ───────────────────────────────────────────────────────────
795
796/// Mirrors an AnimationOutput left/right (negate X-axis channels).
797pub fn mirror_output(output: &mut AnimationOutput) {
798    for (key, val) in &mut output.channels {
799        if key.ends_with(".x") || key.ends_with("_x") || key.contains("left") {
800            *val = -*val;
801        }
802    }
803}
804
805// ── AnimationBlend helpers ────────────────────────────────────────────────────
806
807/// Linearly blend two AnimationOutputs by alpha (0 = a, 1 = b).
808pub fn blend_outputs(a: &AnimationOutput, b: &AnimationOutput, alpha: f32) -> AnimationOutput {
809    let mut out = a.clone();
810    for (key, bv) in &b.channels {
811        let av = a.channels.get(key).copied().unwrap_or(0.0);
812        out.channels.insert(key.clone(), av + (bv - av) * alpha);
813    }
814    out
815}
816
817/// Additively layer `additive` on top of `base` with `weight`.
818pub fn add_output(base: &AnimationOutput, additive: &AnimationOutput, weight: f32) -> AnimationOutput {
819    let mut out = base.clone();
820    for (key, av) in &additive.channels {
821        *out.channels.entry(key.clone()).or_insert(0.0) += av * weight;
822    }
823    out
824}