use crate::metrics::foot_cycle_metrics;
use crate::model::{Clip, Interpolation, Skeleton, Track, TrackValues};
use crate::profile::ResolvedRoles;
use crate::sample::{TrackSample, sample_clip, sample_track};
pub fn slice(clip: &mut Clip, start_s: f64, end_s: f64, fps: f64) {
let eps = (0.5 / fps) as f32;
let (start, end) = (start_s as f32, end_s as f32);
let duration = (end - start).max(0.0);
for track in &mut clip.tracks {
let mut kept: Vec<(usize, f32)> = (0..track.key_count())
.filter(|&k| track.times[k] >= start - eps && track.times[k] <= end + eps)
.map(|k| (k, (track.times[k] - start).clamp(0.0, duration)))
.collect();
kept.retain({
let times: Vec<f32> = kept.iter().map(|&(_, t)| t).collect();
let mut i = 0;
move |_| {
let t = times[i];
let keep = if t <= 0.0 {
times.get(i + 1).is_none_or(|&next| next > 0.0)
} else if t >= duration {
i == 0 || times[i - 1] < duration
} else {
true
};
i += 1;
keep
}
});
track.times = kept.iter().map(|&(_, t)| t).collect();
let per_key = match track.interpolation {
Interpolation::CubicSpline => 3,
_ => 1,
};
match &mut track.values {
TrackValues::Vec3s(v) => {
let old = std::mem::take(v);
*v = kept
.iter()
.flat_map(|&(k, _)| old[k * per_key..(k + 1) * per_key].to_vec())
.collect();
}
TrackValues::Quats(v) => {
let old = std::mem::take(v);
*v = kept
.iter()
.flat_map(|&(k, _)| old[k * per_key..(k + 1) * per_key].to_vec())
.collect();
}
}
}
clip.duration_s = (end_s - start_s).max(0.0);
clip.tracks.retain(|t| t.key_count() > 0);
}
pub fn hold_extend(clip: &mut Clip, hold_s: f64) {
for track in &mut clip.tracks {
let Some(&last) = track.times.last() else {
continue;
};
let key = track.key_count() - 1;
track.times.push(last + hold_s as f32);
let value_index = track.value_index(key);
match &mut track.values {
TrackValues::Vec3s(v) => {
let value = v[value_index];
match track.interpolation {
Interpolation::CubicSpline => {
v[key * 3 + 2] = glam::Vec3::ZERO;
v.extend_from_slice(&[glam::Vec3::ZERO, value, glam::Vec3::ZERO]);
}
_ => v.push(value),
}
}
TrackValues::Quats(v) => {
let value = v[value_index];
match track.interpolation {
Interpolation::CubicSpline => {
v[key * 3 + 2] = glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
v.extend_from_slice(&[
glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
value,
glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
]);
}
_ => v.push(value),
}
}
}
clip.duration_s = clip.duration_s.max((last + hold_s as f32) as f64);
}
}
#[derive(Debug, Clone)]
#[non_exhaustive]
pub struct GaitAlignOutcome {
pub phase_before: f64,
pub phase_after: f64,
pub seam_after: Option<f64>,
pub frame_offset: i32,
}
pub fn align_gait_anchor(
skeleton: &Skeleton,
clip: &mut Clip,
roles: &ResolvedRoles,
fps: f64,
) -> Result<GaitAlignOutcome, String> {
let measure = |c: &Clip| -> Option<(f64, Option<f64>, f64)> {
let frames = crate::metrics::metric_frame_count(c)?;
let grid = sample_clip(skeleton, c, frames);
let m = foot_cycle_metrics(&grid, roles, crate::metrics::MIN_STRIDE_STEP_M)?;
Some((m.gait_phase?, m.loop_seam_ratio, m.lr_amplitude_m))
};
let Some((phase_before, _, amplitude)) = measure(clip) else {
return Err(
"no usable stride anchor (hips/foot roles unresolved or clip too short)".into(),
);
};
if amplitude < 0.03 {
return Err(format!(
"no usable stride anchor (L−R amplitude {amplitude:.4} m) — a ring clip must \
alternate its feet for anchor alignment to mean anything"
));
}
let unrotatable: Vec<String> = clip
.tracks
.iter()
.filter(|t| t.interpolation == Interpolation::CubicSpline && !is_constant_track(t))
.map(|t| format!("{} bone {}", t.property.as_str(), t.bone))
.collect();
if !unrotatable.is_empty() {
return Err(format!(
"cannot gait-anchor: these animated tracks need lossless resampling that is \
not yet supported ({}); retime them to LINEAR first",
unrotatable.join(", ")
));
}
let original = clip.clone();
let mut best: Option<(f64, GaitAlignOutcome, Clip)> = None;
for frame_offset in [0i32, -1, 1] {
let mut candidate = original.clone();
rotate_values(&mut candidate, phase_before, fps, frame_offset);
let Some((phase_after, seam_after, _)) = measure(&candidate) else {
continue;
};
let rank = seam_after.unwrap_or(f64::MAX);
if best.as_ref().is_none_or(|(r, _, _)| rank < *r) {
best = Some((
rank,
GaitAlignOutcome {
phase_before,
phase_after,
seam_after,
frame_offset,
},
candidate,
));
}
}
let Some((_, outcome, rotated)) = best else {
return Err("no rotation candidate was measurable".into());
};
*clip = rotated;
Ok(outcome)
}
fn is_constant_track(track: &Track) -> bool {
let n = track.key_count();
if n <= 1 {
return true;
}
let cubic = track.interpolation == Interpolation::CubicSpline;
fn constant<T: Copy + PartialEq>(v: &[T], n: usize, cubic: bool, zero: T) -> bool {
let value = |k: usize| if cubic { 3 * k + 1 } else { k };
let Some(&first) = v.get(value(0)) else {
return false;
};
(0..n).all(|k| {
v.get(value(k)) == Some(&first)
&& (!cubic || (v.get(3 * k) == Some(&zero) && v.get(3 * k + 2) == Some(&zero)))
})
}
match &track.values {
TrackValues::Vec3s(v) => constant(v, n, cubic, glam::Vec3::ZERO),
TrackValues::Quats(v) => constant(v, n, cubic, glam::Quat::from_xyzw(0.0, 0.0, 0.0, 0.0)),
}
}
fn rotate_values(clip: &mut Clip, phase: f64, fps: f64, frame_offset: i32) {
let duration = clip
.tracks
.iter()
.map(Track::end_time)
.fold(0.0f32, f32::max) as f64;
if duration <= 0.0 {
return;
}
let period = duration + 1.0 / fps;
let mut shift = ((phase * period * fps).round() + frame_offset as f64) / fps;
shift = shift.rem_euclid(period);
for track in &mut clip.tracks {
if is_constant_track(track) {
continue;
}
let sampled: Vec<TrackSample> = track
.times
.iter()
.map(|&t| sample_track(track, ((t as f64 + shift) % period) as f32))
.collect();
match &mut track.values {
TrackValues::Vec3s(v) => {
for (slot, s) in v.iter_mut().zip(&sampled) {
if let TrackSample::Vec3(x) = s {
*slot = *x;
}
}
}
TrackValues::Quats(v) => {
for (slot, s) in v.iter_mut().zip(&sampled) {
if let TrackSample::Quat(q) = s {
*slot = *q;
}
}
}
}
}
}