use crate::model::BoneId;
use crate::profile::{ResolvedRoles, Role};
use crate::sample::PoseGrid;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum StanceSideV1 {
Left,
Right,
}
impl StanceSideV1 {
pub(crate) const fn label(self) -> &'static str {
match self {
Self::Left => "left",
Self::Right => "right",
}
}
fn resolved_role(self, roles: &ResolvedRoles) -> Option<(Role, BoneId)> {
let preferred = match self {
Self::Left => [Role::LeftFoot, Role::LeftToe],
Self::Right => [Role::RightFoot, Role::RightToe],
};
preferred
.into_iter()
.find_map(|role| roles.get(role).map(|bone| (role, bone)))
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct StanceSupportRunV1 {
pub start_frame: usize,
pub end_frame: usize,
}
#[derive(Debug)]
pub struct ResolvedStanceSupportV1<'a> {
grid: &'a PoseGrid,
role: Role,
bone: BoneId,
ground_y_m: f64,
contact_height_m: f64,
}
pub fn resolve_stance_support_v1<'a>(
grid: &'a PoseGrid,
roles: &ResolvedRoles,
side: StanceSideV1,
contact_height_m: f64,
) -> Option<ResolvedStanceSupportV1<'a>> {
let (role, bone) = side.resolved_role(roles)?;
let ground_y_m = (0..grid.frame_count())
.map(|frame| grid.model_position(frame, bone).y as f64)
.fold(f64::MAX, f64::min);
Some(ResolvedStanceSupportV1 {
grid,
role,
bone,
ground_y_m,
contact_height_m,
})
}
impl ResolvedStanceSupportV1<'_> {
pub const fn role(&self) -> Role {
self.role
}
pub const fn bone(&self) -> BoneId {
self.bone
}
pub fn supported_adjacent_frames(&self) -> impl Iterator<Item = usize> + '_ {
(1..self.grid.frame_count())
.filter(|&frame| self.is_support_frame(frame - 1) && self.is_support_frame(frame))
}
pub fn retained_runs(&self) -> impl Iterator<Item = StanceSupportRunV1> + '_ {
let mut next_frame = 0;
std::iter::from_fn(move || {
loop {
while next_frame < self.grid.frame_count() && !self.is_support_frame(next_frame) {
next_frame += 1;
}
if next_frame == self.grid.frame_count() {
return None;
}
let start_frame = next_frame;
while next_frame < self.grid.frame_count() && self.is_support_frame(next_frame) {
next_frame += 1;
}
let end_frame = next_frame - 1;
if end_frame > start_frame {
return Some(StanceSupportRunV1 {
start_frame,
end_frame,
});
}
}
})
}
fn is_support_frame(&self, frame: usize) -> bool {
let above_threshold = (self.grid.model_position(frame, self.bone).y as f64)
> self.ground_y_m + self.contact_height_m;
!above_threshold
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::model::{
Bone, Clip, Interpolation, Property, Skeleton, Track, TrackValues, Transform,
};
use crate::sample::sample_clip;
use glam::Vec3;
fn skeleton(names: &[(&str, f32)]) -> Skeleton {
Skeleton {
bones: names
.iter()
.map(|(name, y)| Bone {
name: (*name).into(),
parent: None,
rest: Transform {
translation: Vec3::new(0.0, *y, 0.0),
..Transform::IDENTITY
},
inverse_bind: None,
})
.collect(),
}
}
fn grid(skeleton: Skeleton, tracks: Vec<Track>) -> (Skeleton, PoseGrid) {
let clip = Clip {
name: "stance".into(),
duration_s: 1.0,
tracks,
};
let grid = sample_clip(&skeleton, &clip, 3);
(skeleton, grid)
}
fn y_track(bone: BoneId, values: Vec<f32>) -> Track {
Track {
bone,
property: Property::Translation,
interpolation: Interpolation::Linear,
times: vec![0.0, 0.5, 1.0],
values: TrackValues::Vec3s(
values.into_iter().map(|y| Vec3::new(0.0, y, 0.0)).collect(),
),
}
}
#[test]
fn each_side_uses_its_own_model_y_minimum() {
let (skeleton, grid) = grid(
skeleton(&[("left", 0.0), ("right", 10.0)]),
vec![
y_track(0, vec![0.0, 0.5, 0.0]),
y_track(1, vec![10.0, 10.5, 10.0]),
],
);
let roles = ResolvedRoles::from_names(
&skeleton,
[
(Role::LeftFoot, "left".to_string()),
(Role::RightFoot, "right".to_string()),
],
);
assert_eq!(grid.model_position(0, 0).y, 0.0);
assert_eq!(grid.model_position(0, 1).y, 10.0);
let left = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.5).unwrap();
let right = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Right, 0.5).unwrap();
assert_eq!(left.supported_adjacent_frames().collect::<Vec<_>>(), [1, 2]);
assert_eq!(
right.supported_adjacent_frames().collect::<Vec<_>>(),
[1, 2]
);
}
#[test]
fn foot_and_toe_selection_is_independent_per_side() {
let (skeleton, grid) = grid(
skeleton(&[("left-foot", 0.0), ("left-toe", 0.0), ("right-toe", 0.0)]),
vec![y_track(0, vec![0.0, 0.0, 0.0])],
);
let roles = ResolvedRoles::from_names(
&skeleton,
[
(Role::LeftFoot, "left-foot".to_string()),
(Role::LeftToe, "left-toe".to_string()),
(Role::RightToe, "right-toe".to_string()),
],
);
let left = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.0).unwrap();
let right = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Right, 0.0).unwrap();
assert_eq!((left.role(), left.bone()), (Role::LeftFoot, 0));
assert_eq!((right.role(), right.bone()), (Role::RightToe, 2));
}
#[test]
fn retained_runs_are_maximal_and_match_adjacent_pairs() {
let skeleton = skeleton(&[("left", 0.0)]);
let clip = Clip {
name: "stance".into(),
duration_s: 1.0,
tracks: vec![Track {
bone: 0,
property: Property::Translation,
interpolation: Interpolation::Linear,
times: (0..8).map(|index| index as f32 / 7.0).collect(),
values: TrackValues::Vec3s(
[0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0]
.into_iter()
.map(|y| Vec3::new(0.0, y, 0.0))
.collect(),
),
}],
};
let grid = sample_clip(&skeleton, &clip, 8);
let roles = ResolvedRoles::from_names(&skeleton, [(Role::LeftFoot, "left".to_string())]);
let support = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.0).unwrap();
assert_eq!(
support.supported_adjacent_frames().collect::<Vec<_>>(),
[1, 6, 7]
);
assert_eq!(
support.retained_runs().collect::<Vec<_>>(),
[
StanceSupportRunV1 {
start_frame: 0,
end_frame: 1,
},
StanceSupportRunV1 {
start_frame: 5,
end_frame: 7,
},
]
);
}
#[test]
fn non_finite_samples_keep_the_legacy_predicate() {
for (values, expected_endpoints) in [
(vec![f32::NAN, 0.0, 0.0], vec![1, 2]),
(vec![f32::INFINITY; 3], vec![]),
] {
let (skeleton, grid) = grid(skeleton(&[("left", 0.0)]), vec![y_track(0, values)]);
let roles =
ResolvedRoles::from_names(&skeleton, [(Role::LeftFoot, "left".to_string())]);
let support =
resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.0).unwrap();
assert_eq!(
support.supported_adjacent_frames().collect::<Vec<_>>(),
expected_endpoints
);
}
}
}