///|
/// A named scalar state used to model interaction modes and animation poses.
pub struct MotionStateNode {
name : String
value : Double
} derive(Debug)
///|
pub fn motion_state(
name : String,
value : Double,
) -> MotionStateNode raise MotionError {
if name.is_empty() {
raise MotionError::InvalidStateName
}
ensure_finite(value)
{ name, value }
}
///|
pub fn MotionStateNode::name(self : MotionStateNode) -> String {
self.name
}
///|
pub fn MotionStateNode::value(self : MotionStateNode) -> Double {
self.value
}
///|
/// A directed transition between two named motion states.
pub struct MotionStateTransition {
from : String
to : String
duration : Double
curve : Curve
} derive(Debug)
///|
pub fn motion_transition(
from : String,
to : String,
duration : Double,
curve? : Curve = Curve::builtin(Linear),
) -> MotionStateTransition raise MotionError {
if from.is_empty() || to.is_empty() {
raise MotionError::InvalidStateName
}
ensure_finite(duration)
if duration <= 0.0 {
raise MotionError::InvalidDuration(duration)
}
{ from, to, duration, curve }
}
///|
pub fn MotionStateTransition::from(self : MotionStateTransition) -> String {
self.from
}
///|
pub fn MotionStateTransition::to(self : MotionStateTransition) -> String {
self.to
}
///|
pub fn MotionStateTransition::duration(self : MotionStateTransition) -> Double {
self.duration
}
///|
pub fn MotionStateTransition::curve(self : MotionStateTransition) -> Curve {
self.curve
}
///|
/// A transition engine with explicit transport time. State changes are
/// deterministic and do not allocate per sample.
pub struct MotionStateMachine {
states : Array[MotionStateNode]
transitions : Array[MotionStateTransition]
mut current : String
mut transition_from : String
mut transition_to : String
mut transition_start : Double
mut transition_duration : Double
mut transition_curve : Curve
mut has_transition : Bool
} derive(Debug)
///|
fn state_index(states : Array[MotionStateNode], name : String) -> Int? {
for index in 0.. Unit raise MotionError {
if states.length() == 0 {
raise MotionError::EmptyTrack
}
for left in 0.. Unit raise MotionError {
for transition in transitions {
if state_index(states, transition.from) is None ||
state_index(states, transition.to) is None {
raise MotionError::InvalidStateName
}
}
}
///|
pub fn MotionStateMachine::new(
states : Array[MotionStateNode],
transitions? : Array[MotionStateTransition] = [],
initial? : String = "",
) -> MotionStateMachine raise MotionError {
validate_states(states)
validate_transitions(states, transitions)
let first = if initial.is_empty() { states[0].name } else { initial }
if state_index(states, first) is None {
raise MotionError::InvalidStateName
}
{
states: states.copy(),
transitions: transitions.copy(),
current: first,
transition_from: first,
transition_to: first,
transition_start: 0.0,
transition_duration: 0.0,
transition_curve: Curve::builtin(Linear),
has_transition: false,
}
}
///|
pub fn MotionStateMachine::states(
self : MotionStateMachine,
) -> Array[MotionStateNode] {
self.states.copy()
}
///|
pub fn MotionStateMachine::transitions(
self : MotionStateMachine,
) -> Array[MotionStateTransition] {
self.transitions.copy()
}
///|
pub fn MotionStateMachine::current(self : MotionStateMachine) -> String {
self.current
}
///|
pub fn MotionStateMachine::is_transitioning(self : MotionStateMachine) -> Bool {
self.has_transition
}
///|
fn MotionStateMachine::value_of(
self : MotionStateMachine,
name : String,
) -> Double {
match state_index(self.states, name) {
Some(index) => self.states[index].value
None => 0.0
}
}
///|
fn MotionStateMachine::find_transition(
self : MotionStateMachine,
from : String,
to : String,
) -> MotionStateTransition? {
for transition in self.transitions {
if transition.from == from && transition.to == to {
return Some(transition)
}
}
None
}
///|
/// Start a transition at an arbitrary transport timestamp.
pub fn MotionStateMachine::transition_to(
self : MotionStateMachine,
target : String,
now : Double,
duration? : Double = 0.0,
curve? : Curve = Curve::builtin(Linear),
) -> Unit raise MotionError {
ensure_finite(now)
if state_index(self.states, target) is None {
raise MotionError::InvalidStateName
}
let selected = if duration > 0.0 {
motion_transition(self.current, target, duration, curve~)
} else {
match self.find_transition(self.current, target) {
Some(value) => value
None => motion_transition(self.current, target, 0.000001, curve~)
}
}
self.transition_from = self.current
self.transition_to = target
self.transition_start = now
self.transition_duration = selected.duration
self.transition_curve = selected.curve
self.has_transition = true
}
///|
pub fn MotionStateMachine::sample(
self : MotionStateMachine,
now : Double,
) -> Double {
if !self.has_transition {
return self.value_of(self.current)
}
let ratio = (now - self.transition_start) / self.transition_duration
if ratio >= 1.0 {
self.current = self.transition_to
self.has_transition = false
return self.value_of(self.current)
}
if ratio <= 0.0 {
return self.value_of(self.transition_from)
}
let progress = self.transition_curve.apply(ratio)
let first = self.value_of(self.transition_from)
let second = self.value_of(self.transition_to)
first + (second - first) * progress
}
///|
pub fn MotionStateMachine::sample_motion(
self : MotionStateMachine,
now : Double,
) -> MotionSample {
let delta = 0.0001
let value = self.sample(now)
let before = self.sample(now - delta)
let after = self.sample(now + delta)
{
time: now,
value,
velocity: (after - before) / (2.0 * delta),
acceleration: (after - 2.0 * value + before) / (delta * delta),
}
}
///|
pub fn MotionStateMachine::complete(self : MotionStateMachine) -> Unit {
if self.has_transition {
self.current = self.transition_to
self.has_transition = false
}
}
///|
pub fn MotionStateMachine::reset(
self : MotionStateMachine,
state? : String = "",
) -> Unit raise MotionError {
let target = if state.is_empty() { self.states[0].name } else { state }
if state_index(self.states, target) is None {
raise MotionError::InvalidStateName
}
self.current = target
self.transition_from = target
self.transition_to = target
self.transition_start = 0.0
self.transition_duration = 0.0
self.has_transition = false
}
///|
pub fn MotionStateMachine::add_state(
self : MotionStateMachine,
state : MotionStateNode,
) -> Unit raise MotionError {
if state_index(self.states, state.name) is Some(_) {
raise MotionError::InvalidStateName
}
self.states.push(state)
}
///|
pub fn MotionStateMachine::add_transition(
self : MotionStateMachine,
transition : MotionStateTransition,
) -> Unit raise MotionError {
validate_transitions(self.states, [transition])
self.transitions.push(transition)
}
///|
pub fn MotionStateMachine::has_state(
self : MotionStateMachine,
name : String,
) -> Bool {
state_index(self.states, name) is Some(_)
}
///|
pub fn MotionStateMachine::state_value(
self : MotionStateMachine,
name : String,
) -> Double? {
match state_index(self.states, name) {
Some(index) => Some(self.states[index].value)
None => None
}
}