use crate::model::{ClipBody, Drive, Motor, Shape, Study, DEFAULT_ACCEL};
use std::collections::BTreeMap;
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub struct MateOverride {
pub value: Option<f64>,
pub enabled: Option<bool>,
}
#[derive(Debug, Clone, PartialEq)]
pub struct CameraSample {
pub from: String,
pub to: String,
pub t: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct SectionSample {
pub view: String,
pub offset: f64,
}
#[derive(Debug, Clone, PartialEq)]
pub struct ExplodeSample {
pub view: String,
pub progress: f64,
}
#[derive(Debug, Clone, PartialEq, Default)]
pub struct FrameSample {
pub mates: BTreeMap<String, MateOverride>,
pub camera: Option<CameraSample>,
pub hidden: Vec<String>,
pub opacity: BTreeMap<String, f64>,
pub section: Option<SectionSample>,
pub explode: Vec<ExplodeSample>,
}
pub fn shape_at(shape: Shape, accel: Option<f64>, u: f64) -> f64 {
let u = u.clamp(0.0, 1.0);
match shape {
Shape::Linear => u,
Shape::Ease => u * u * (3.0 - 2.0 * u),
Shape::Trapezoid => {
let a = accel.unwrap_or(DEFAULT_ACCEL).clamp(1e-6, 0.5);
let peak = 1.0 / (1.0 - a);
if u < a {
0.5 * peak * u * u / a
} else if u <= 1.0 - a {
peak * (u - 0.5 * a)
} else {
1.0 - 0.5 * peak * (1.0 - u) * (1.0 - u) / a
}
}
}
}
impl Motor {
pub fn value_at(&self, time: f64, base: f64) -> Option<f64> {
if time < self.from {
return None;
}
let elapsed = time.min(self.to) - self.from;
Some(match &self.drive {
Drive::Speed { speed } => base + speed * elapsed,
Drive::Profile { shape, start, end, accel } => start + (end - start) * shape_at(*shape, *accel, elapsed / (self.to - self.from)),
})
}
}
impl Study {
pub fn sample(&self, time: f64, base: &dyn Fn(&str) -> Option<f64>) -> FrameSample {
let mut out = FrameSample::default();
for motor in &self.motors {
if let Some(value) = motor.value_at(time, base(&motor.constraint).unwrap_or(0.0)) {
let entry = out.mates.entry(motor.constraint.clone()).or_default();
let later = self
.motors
.iter()
.any(|other| other.constraint == motor.constraint && other.from > motor.from && other.from <= time);
if !later {
entry.value = Some(value);
}
}
}
let mut clips: Vec<_> = self.clips.iter().collect();
clips.sort_by(|a, b| a.from.total_cmp(&b.from));
let mut camera: Option<CameraSample> = None;
for clip in clips {
let inside = time >= clip.from && time < clip.to;
let u = ((time - clip.from) / (clip.to - clip.from)).clamp(0.0, 1.0);
match &clip.body {
ClipBody::Suppress { constraint } if inside => {
out.mates.entry(constraint.clone()).or_default().enabled = Some(false);
}
ClipBody::Camera { views } if time >= clip.from => {
camera = Some(if views.len() == 1 {
CameraSample { from: views[0].clone(), to: views[0].clone(), t: 0.0 }
} else {
let along = u * (views.len() - 1) as f64;
let at = (along.floor() as usize).min(views.len() - 2);
CameraSample { from: views[at].clone(), to: views[at + 1].clone(), t: along - at as f64 }
});
}
ClipBody::Visibility { occurrences } if inside => {
for occurrence in occurrences {
if !out.hidden.contains(occurrence) {
out.hidden.push(occurrence.clone());
}
}
}
ClipBody::Transparency { occurrences, opacity } if inside => {
let value = opacity[0] + (opacity[1] - opacity[0]) * u;
for occurrence in occurrences {
out.opacity.insert(occurrence.clone(), value);
}
}
ClipBody::Section { view, sweep } if inside => {
let offset = sweep.map_or(0.0, |s| s[0] + (s[1] - s[0]) * u);
out.section = Some(SectionSample { view: view.clone(), offset });
}
ClipBody::Explode { view, collapse } if time >= clip.from => {
let progress = if *collapse { 1.0 - u } else { u };
out.explode.retain(|e| e.view != *view);
if progress > 0.0 {
out.explode.push(ExplodeSample { view: view.clone(), progress });
}
}
_ => {}
}
}
out.camera = camera;
out
}
}