1use crate::model::BoneId;
8use crate::profile::{ResolvedRoles, Role};
9use crate::sample::PoseGrid;
10
11#[derive(Debug, Clone, Copy, PartialEq, Eq)]
13pub enum StanceSideV1 {
14 Left,
16 Right,
18}
19
20impl StanceSideV1 {
21 pub(crate) const fn label(self) -> &'static str {
22 match self {
23 Self::Left => "left",
24 Self::Right => "right",
25 }
26 }
27
28 fn resolved_role(self, roles: &ResolvedRoles) -> Option<(Role, BoneId)> {
29 let preferred = match self {
30 Self::Left => [Role::LeftFoot, Role::LeftToe],
31 Self::Right => [Role::RightFoot, Role::RightToe],
32 };
33 preferred
34 .into_iter()
35 .find_map(|role| roles.get(role).map(|bone| (role, bone)))
36 }
37}
38
39#[derive(Debug, Clone, Copy, PartialEq, Eq)]
41pub struct StanceSupportRunV1 {
42 pub start_frame: usize,
44 pub end_frame: usize,
46}
47
48#[derive(Debug)]
55pub struct ResolvedStanceSupportV1<'a> {
56 grid: &'a PoseGrid,
57 role: Role,
58 bone: BoneId,
59 ground_y_m: f64,
60 contact_height_m: f64,
61}
62
63pub fn resolve_stance_support_v1<'a>(
70 grid: &'a PoseGrid,
71 roles: &ResolvedRoles,
72 side: StanceSideV1,
73 contact_height_m: f64,
74) -> Option<ResolvedStanceSupportV1<'a>> {
75 let (role, bone) = side.resolved_role(roles)?;
76 let ground_y_m = (0..grid.frame_count())
77 .map(|frame| grid.model_position(frame, bone).y as f64)
78 .fold(f64::MAX, f64::min);
79 Some(ResolvedStanceSupportV1 {
80 grid,
81 role,
82 bone,
83 ground_y_m,
84 contact_height_m,
85 })
86}
87
88impl ResolvedStanceSupportV1<'_> {
89 pub const fn role(&self) -> Role {
91 self.role
92 }
93
94 pub const fn bone(&self) -> BoneId {
96 self.bone
97 }
98
99 pub fn supported_adjacent_frames(&self) -> impl Iterator<Item = usize> + '_ {
105 (1..self.grid.frame_count())
106 .filter(|&frame| self.is_support_frame(frame - 1) && self.is_support_frame(frame))
107 }
108
109 pub fn retained_runs(&self) -> impl Iterator<Item = StanceSupportRunV1> + '_ {
115 let mut next_frame = 0;
116 std::iter::from_fn(move || {
117 loop {
118 while next_frame < self.grid.frame_count() && !self.is_support_frame(next_frame) {
119 next_frame += 1;
120 }
121 if next_frame == self.grid.frame_count() {
122 return None;
123 }
124 let start_frame = next_frame;
125 while next_frame < self.grid.frame_count() && self.is_support_frame(next_frame) {
126 next_frame += 1;
127 }
128 let end_frame = next_frame - 1;
129 if end_frame > start_frame {
130 return Some(StanceSupportRunV1 {
131 start_frame,
132 end_frame,
133 });
134 }
135 }
136 })
137 }
138
139 fn is_support_frame(&self, frame: usize) -> bool {
140 let above_threshold = (self.grid.model_position(frame, self.bone).y as f64)
144 > self.ground_y_m + self.contact_height_m;
145 !above_threshold
146 }
147}
148
149#[cfg(test)]
150mod tests {
151 use super::*;
152 use crate::model::{
153 Bone, Clip, Interpolation, Property, Skeleton, Track, TrackValues, Transform,
154 };
155 use crate::sample::sample_clip;
156 use glam::Vec3;
157
158 fn skeleton(names: &[(&str, f32)]) -> Skeleton {
159 Skeleton {
160 bones: names
161 .iter()
162 .map(|(name, y)| Bone {
163 name: (*name).into(),
164 parent: None,
165 rest: Transform {
166 translation: Vec3::new(0.0, *y, 0.0),
167 ..Transform::IDENTITY
168 },
169 inverse_bind: None,
170 })
171 .collect(),
172 }
173 }
174
175 fn grid(skeleton: Skeleton, tracks: Vec<Track>) -> (Skeleton, PoseGrid) {
176 let clip = Clip {
177 name: "stance".into(),
178 duration_s: 1.0,
179 tracks,
180 };
181 let grid = sample_clip(&skeleton, &clip, 3);
182 (skeleton, grid)
183 }
184
185 fn y_track(bone: BoneId, values: Vec<f32>) -> Track {
186 Track {
187 bone,
188 property: Property::Translation,
189 interpolation: Interpolation::Linear,
190 times: vec![0.0, 0.5, 1.0],
191 values: TrackValues::Vec3s(
192 values.into_iter().map(|y| Vec3::new(0.0, y, 0.0)).collect(),
193 ),
194 }
195 }
196
197 #[test]
198 fn each_side_uses_its_own_model_y_minimum() {
199 let (skeleton, grid) = grid(
200 skeleton(&[("left", 0.0), ("right", 10.0)]),
201 vec![
202 y_track(0, vec![0.0, 0.5, 0.0]),
203 y_track(1, vec![10.0, 10.5, 10.0]),
204 ],
205 );
206 let roles = ResolvedRoles::from_names(
207 &skeleton,
208 [
209 (Role::LeftFoot, "left".to_string()),
210 (Role::RightFoot, "right".to_string()),
211 ],
212 );
213
214 assert_eq!(grid.model_position(0, 0).y, 0.0);
215 assert_eq!(grid.model_position(0, 1).y, 10.0);
216 let left = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.5).unwrap();
217 let right = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Right, 0.5).unwrap();
218 assert_eq!(left.supported_adjacent_frames().collect::<Vec<_>>(), [1, 2]);
219 assert_eq!(
220 right.supported_adjacent_frames().collect::<Vec<_>>(),
221 [1, 2]
222 );
223 }
224
225 #[test]
226 fn foot_and_toe_selection_is_independent_per_side() {
227 let (skeleton, grid) = grid(
228 skeleton(&[("left-foot", 0.0), ("left-toe", 0.0), ("right-toe", 0.0)]),
229 vec![y_track(0, vec![0.0, 0.0, 0.0])],
230 );
231 let roles = ResolvedRoles::from_names(
232 &skeleton,
233 [
234 (Role::LeftFoot, "left-foot".to_string()),
235 (Role::LeftToe, "left-toe".to_string()),
236 (Role::RightToe, "right-toe".to_string()),
237 ],
238 );
239
240 let left = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.0).unwrap();
241 let right = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Right, 0.0).unwrap();
242 assert_eq!((left.role(), left.bone()), (Role::LeftFoot, 0));
243 assert_eq!((right.role(), right.bone()), (Role::RightToe, 2));
244 }
245
246 #[test]
247 fn retained_runs_are_maximal_and_match_adjacent_pairs() {
248 let skeleton = skeleton(&[("left", 0.0)]);
249 let clip = Clip {
250 name: "stance".into(),
251 duration_s: 1.0,
252 tracks: vec![Track {
253 bone: 0,
254 property: Property::Translation,
255 interpolation: Interpolation::Linear,
256 times: (0..8).map(|index| index as f32 / 7.0).collect(),
257 values: TrackValues::Vec3s(
258 [0.0, 0.0, 0.1, 0.0, 0.1, 0.0, 0.0, 0.0]
259 .into_iter()
260 .map(|y| Vec3::new(0.0, y, 0.0))
261 .collect(),
262 ),
263 }],
264 };
265 let grid = sample_clip(&skeleton, &clip, 8);
266 let roles = ResolvedRoles::from_names(&skeleton, [(Role::LeftFoot, "left".to_string())]);
267 let support = resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.0).unwrap();
268
269 assert_eq!(
270 support.supported_adjacent_frames().collect::<Vec<_>>(),
271 [1, 6, 7]
272 );
273 assert_eq!(
274 support.retained_runs().collect::<Vec<_>>(),
275 [
276 StanceSupportRunV1 {
277 start_frame: 0,
278 end_frame: 1,
279 },
280 StanceSupportRunV1 {
281 start_frame: 5,
282 end_frame: 7,
283 },
284 ]
285 );
286 }
287
288 #[test]
289 fn non_finite_samples_keep_the_legacy_predicate() {
290 for (values, expected_endpoints) in [
291 (vec![f32::NAN, 0.0, 0.0], vec![1, 2]),
292 (vec![f32::INFINITY; 3], vec![]),
293 ] {
294 let (skeleton, grid) = grid(skeleton(&[("left", 0.0)]), vec![y_track(0, values)]);
295 let roles =
296 ResolvedRoles::from_names(&skeleton, [(Role::LeftFoot, "left".to_string())]);
297 let support =
298 resolve_stance_support_v1(&grid, &roles, StanceSideV1::Left, 0.0).unwrap();
299 assert_eq!(
300 support.supported_adjacent_frames().collect::<Vec<_>>(),
301 expected_endpoints
302 );
303 }
304 }
305}