use std::collections::BTreeSet;
use std::mem::size_of;
use glam::{Quat, Vec3};
use crate::{
Clip, ContactTimeWarpControlPointV1, ContactTransformOperationV1, DocumentShapeError,
FootCycleMemberPlanV1, Interpolation, Track, TrackSample, TrackValues, sample_track,
};
pub const FOOT_CYCLE_CLIP_V1_MAX_TRACKS: usize = 4_096;
pub const FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS: usize = 1_048_576;
pub const FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES: usize = 3 * FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS;
pub const FOOT_CYCLE_CLIP_V1_MAX_GENERATED_KEYS: usize = 1_048_576;
pub const FOOT_CYCLE_CLIP_V1_MAX_WORK: usize = 8_388_608;
pub const FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES: usize = 65_536;
pub const FOOT_CYCLE_CLIP_V1_MAX_CANDIDATE_BYTES: usize = FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES
+ FOOT_CYCLE_CLIP_V1_MAX_TRACKS * size_of::<Track>()
+ FOOT_CYCLE_CLIP_V1_MAX_GENERATED_KEYS * size_of::<f32>()
+ FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES * size_of::<Quat>();
const _: () = assert!(FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES == 3 * FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS);
const _: () = assert!(FOOT_CYCLE_CLIP_V1_MAX_CANDIDATE_BYTES < usize::MAX);
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FootCycleClipResourceV1 {
Tracks,
InputKeys,
InputValues,
GeneratedKeys,
Work,
NameBytes,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub enum FootCycleCubicSplineRefusalV1 {
DifferingValues,
NonZeroTangent,
}
#[derive(Debug, Clone, PartialEq, thiserror::Error)]
#[non_exhaustive]
pub enum FootCycleClipWarpError {
#[error("foot-cycle clip candidate requires a time_warp operation")]
UnsupportedOperation,
#[error("unsupported foot-cycle time-warp version {version}")]
UnsupportedVersion {
version: u32,
},
#[error("clip duration {duration_s} does not narrow to a finite positive binary32 duration")]
InvalidClipDuration {
duration_s: f64,
},
#[error(
"time-warp output duration {operation_duration_s} does not equal clip duration {clip_duration_s}"
)]
DurationMismatch {
clip_duration_s: f64,
operation_duration_s: f64,
},
#[error("time-warp declares {found} control points; expected 2..={maximum}")]
InvalidControlPointCount {
found: usize,
maximum: usize,
},
#[error("time-warp control point {index} is not finite and normalized")]
InvalidControlPoint {
index: usize,
},
#[error("time-warp must map exact endpoints (0,0) and (1,1)")]
InvalidMapEndpoints,
#[error("time-warp is not strictly increasing at control point {index}")]
NonMonotoneMap {
index: usize,
},
#[error("track {track_index} is malformed: {source}")]
InvalidTrack {
track_index: usize,
source: DocumentShapeError,
},
#[error("track {track_index} duplicates {property:?} for node {bone}")]
DuplicateTrackTarget {
track_index: usize,
bone: usize,
property: crate::Property,
},
#[error("track {track_index} key {key_index} at {time_s} is outside [0, {duration_s}]")]
TrackTimeOutOfRange {
track_index: usize,
key_index: usize,
time_s: f32,
duration_s: f32,
},
#[error("track {track_index} cubic spline is not safely constant: {reason:?}")]
UnsupportedCubicSpline {
track_index: usize,
reason: FootCycleCubicSplineRefusalV1,
},
#[error("track {track_index} quaternion key {key_index} has invalid binary32 magnitude")]
InvalidQuaternionKey {
track_index: usize,
key_index: usize,
},
#[error("track {track_index} generated a binary32 time collision")]
TimeCollision {
track_index: usize,
},
#[error("track {track_index} generated a binary32 source-time collision")]
SourceTimeCollision {
track_index: usize,
},
#[error("foot-cycle clip {resource:?} count {observed} exceeds V1 limit {maximum}")]
LimitExceeded {
resource: FootCycleClipResourceV1,
observed: usize,
maximum: usize,
},
#[error("foot-cycle clip {resource:?} count overflowed")]
CountOverflow {
resource: FootCycleClipResourceV1,
},
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[non_exhaustive]
pub struct FootCycleClipPreflightV1 {
tracks: usize,
input_keys: usize,
input_values: usize,
candidate_keys: usize,
candidate_values: usize,
name_bytes: usize,
candidate_bytes: usize,
work: usize,
}
impl FootCycleClipPreflightV1 {
pub const fn tracks(self) -> usize {
self.tracks
}
pub const fn input_keys(self) -> usize {
self.input_keys
}
pub const fn input_values(self) -> usize {
self.input_values
}
pub const fn candidate_keys(self) -> usize {
self.candidate_keys
}
pub const fn candidate_values(self) -> usize {
self.candidate_values
}
pub const fn name_bytes(self) -> usize {
self.name_bytes
}
pub const fn candidate_bytes(self) -> usize {
self.candidate_bytes
}
pub const fn work(self) -> usize {
self.work
}
}
#[derive(Debug)]
struct PreparedClipWarp<'a> {
output_duration_s: f64,
points: &'a [ContactTimeWarpControlPointV1],
duration: f32,
identity: bool,
counts: FootCycleClipPreflightV1,
track_candidate_keys: Vec<usize>,
}
#[derive(Debug, Clone, Copy)]
struct CandidateKey {
output_time: f32,
value: CandidateValue,
}
#[derive(Debug, Clone, Copy)]
enum CandidateValue {
Authored(usize),
Sampled(f32),
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct FootCycleClipWarpKnotV1 {
control_point_index: usize,
source_time: f32,
output_time: f32,
}
impl FootCycleClipWarpKnotV1 {
#[must_use]
pub const fn control_point_index(self) -> usize {
self.control_point_index
}
#[must_use]
pub const fn source_time(self) -> f32 {
self.source_time
}
#[must_use]
pub const fn output_time(self) -> f32 {
self.output_time
}
#[must_use]
pub fn coincides_with(self, time: f32) -> bool {
self.source_time == time
|| self.source_time.next_up() == time
|| time.next_up() == self.source_time
}
}
fn time_warp_knots_v1<'a>(
control_points: &'a [ContactTimeWarpControlPointV1],
duration: f32,
track: &Track,
) -> impl Iterator<Item = FootCycleClipWarpKnotV1> + 'a {
let start_time = track.start_time();
let end_time = track.end_time();
let interior = if track.interpolation == Interpolation::Linear {
control_points
.get(1..control_points.len().saturating_sub(1))
.unwrap_or_default()
} else {
&[]
};
interior
.iter()
.enumerate()
.map(move |(offset, point)| FootCycleClipWarpKnotV1 {
control_point_index: offset + 1,
source_time: (point.input_time() * f64::from(duration)) as f32,
output_time: (point.output_time() * f64::from(duration)) as f32,
})
.filter(move |knot| {
(knot.coincides_with(start_time)
|| knot.coincides_with(end_time)
|| (knot.source_time > start_time && knot.source_time < end_time))
&& !knot.coincides_with(0.0)
&& !knot.coincides_with(duration)
})
}
pub fn time_warp_clip_v1(
input: &Clip,
plan: &FootCycleMemberPlanV1,
) -> Result<Clip, FootCycleClipWarpError> {
let prepared = prepare_clip_warp(input, plan)?;
debug_assert!(prepared.counts.tracks <= FOOT_CYCLE_CLIP_V1_MAX_TRACKS);
if prepared.identity {
return Ok(input.clone());
}
let mut tracks = Vec::with_capacity(input.tracks.len());
debug_assert_eq!(prepared.track_candidate_keys.len(), input.tracks.len());
for (track_index, track) in input.tracks.iter().enumerate() {
let candidate_keys = prepared.track_candidate_keys[track_index];
tracks.push(warp_track(
track,
track_index,
prepared.points,
prepared.duration,
candidate_keys,
)?);
}
Ok(Clip {
name: input.name.clone(),
duration_s: prepared.output_duration_s,
tracks,
})
}
pub fn preflight_time_warp_clip_v1(
input: &Clip,
plan: &FootCycleMemberPlanV1,
) -> Result<FootCycleClipPreflightV1, FootCycleClipWarpError> {
Ok(prepare_clip_warp(input, plan)?.counts)
}
fn prepare_clip_warp<'a>(
input: &Clip,
plan: &'a FootCycleMemberPlanV1,
) -> Result<PreparedClipWarp<'a>, FootCycleClipWarpError> {
let (output_duration_s, points) = validate_operation(input, plan.operation())?;
let duration = input.duration_s as f32;
let identity = points
.iter()
.all(|point| point.input_time() == point.output_time());
let preflight = preflight(input, points, duration, identity)?;
Ok(PreparedClipWarp {
output_duration_s,
points,
duration,
identity,
counts: preflight.counts,
track_candidate_keys: preflight.track_candidate_keys,
})
}
fn validate_operation<'a>(
input: &Clip,
operation: &'a ContactTransformOperationV1,
) -> Result<(f64, &'a [ContactTimeWarpControlPointV1]), FootCycleClipWarpError> {
let duration = input.duration_s as f32;
if !input.duration_s.is_finite()
|| input.duration_s <= 0.0
|| !duration.is_finite()
|| duration <= 0.0
{
return Err(FootCycleClipWarpError::InvalidClipDuration {
duration_s: input.duration_s,
});
}
let ContactTransformOperationV1::TimeWarp {
version,
output_duration_s,
control_points,
} = operation
else {
return Err(FootCycleClipWarpError::UnsupportedOperation);
};
if *version != 1 {
return Err(FootCycleClipWarpError::UnsupportedVersion { version: *version });
}
if *output_duration_s != input.duration_s {
return Err(FootCycleClipWarpError::DurationMismatch {
clip_duration_s: input.duration_s,
operation_duration_s: *output_duration_s,
});
}
if !(2..=crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS).contains(&control_points.len())
{
return Err(FootCycleClipWarpError::InvalidControlPointCount {
found: control_points.len(),
maximum: crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS,
});
}
for (index, point) in control_points.iter().enumerate() {
if !point.input_time().is_finite()
|| !point.output_time().is_finite()
|| !(0.0..=1.0).contains(&point.input_time())
|| !(0.0..=1.0).contains(&point.output_time())
{
return Err(FootCycleClipWarpError::InvalidControlPoint { index });
}
}
if control_points
.first()
.is_none_or(|point| point.input_time() != 0.0 || point.output_time() != 0.0)
|| control_points
.last()
.is_none_or(|point| point.input_time() != 1.0 || point.output_time() != 1.0)
{
return Err(FootCycleClipWarpError::InvalidMapEndpoints);
}
for (index, pair) in control_points.windows(2).enumerate() {
if pair[0].input_time() >= pair[1].input_time()
|| pair[0].output_time() >= pair[1].output_time()
{
return Err(FootCycleClipWarpError::NonMonotoneMap { index: index + 1 });
}
}
Ok((*output_duration_s, control_points))
}
#[derive(Debug)]
struct PreflightedClipWarp {
counts: FootCycleClipPreflightV1,
track_candidate_keys: Vec<usize>,
}
fn preflight(
input: &Clip,
points: &[ContactTimeWarpControlPointV1],
duration: f32,
identity: bool,
) -> Result<PreflightedClipWarp, FootCycleClipWarpError> {
let mut counts = FootCycleClipPreflightV1 {
tracks: input.tracks.len(),
input_keys: 0,
input_values: 0,
candidate_keys: 0,
candidate_values: 0,
name_bytes: input.name.len(),
candidate_bytes: 0,
work: 0,
};
check_limit(
FootCycleClipResourceV1::Tracks,
counts.tracks,
FOOT_CYCLE_CLIP_V1_MAX_TRACKS,
)?;
check_limit(
FootCycleClipResourceV1::NameBytes,
counts.name_bytes,
FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES,
)?;
counts.candidate_bytes = counts.name_bytes + counts.tracks * size_of::<Track>();
let mut targets = BTreeSet::new();
let mut track_candidate_keys = Vec::with_capacity(input.tracks.len());
for (track_index, track) in input.tracks.iter().enumerate() {
if !targets.insert((track.bone, track.property)) {
return Err(FootCycleClipWarpError::DuplicateTrackTarget {
track_index,
bone: track.bone,
property: track.property,
});
}
counts.input_keys = checked_add(
FootCycleClipResourceV1::InputKeys,
counts.input_keys,
track.times.len(),
)?;
check_limit(
FootCycleClipResourceV1::InputKeys,
counts.input_keys,
FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS,
)?;
counts.input_values = checked_add(
FootCycleClipResourceV1::InputValues,
counts.input_values,
track.values.len(),
)?;
check_limit(
FootCycleClipResourceV1::InputValues,
counts.input_values,
FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES,
)?;
crate::model::validate_track_shape(0, track).map_err(|source| {
FootCycleClipWarpError::InvalidTrack {
track_index,
source,
}
})?;
for (key_index, &time) in track.times.iter().enumerate() {
if time < 0.0 || time > duration {
return Err(FootCycleClipWarpError::TrackTimeOutOfRange {
track_index,
key_index,
time_s: time,
duration_s: duration,
});
}
}
if let TrackValues::Quats(values) = &track.values {
for key_index in 0..track.times.len() {
let value = values[track.value_index(key_index)];
if !value.length_squared().is_finite() || value.length_squared() <= 0.0 {
return Err(FootCycleClipWarpError::InvalidQuaternionKey {
track_index,
key_index,
});
}
}
}
if track.interpolation == Interpolation::CubicSpline {
validate_cubic(track, track_index)?;
}
let mut generated = 0;
if !identity && track.interpolation != Interpolation::CubicSpline {
visit_warp_track_keys(track, track_index, points, duration, |_key| {
generated = checked_add(FootCycleClipResourceV1::GeneratedKeys, generated, 1)?;
Ok(())
})?;
} else {
generated = track.times.len();
}
track_candidate_keys.push(generated);
counts.candidate_keys = checked_add(
FootCycleClipResourceV1::GeneratedKeys,
counts.candidate_keys,
generated,
)?;
check_limit(
FootCycleClipResourceV1::GeneratedKeys,
counts.candidate_keys,
FOOT_CYCLE_CLIP_V1_MAX_GENERATED_KEYS,
)?;
let candidate_values = if track.interpolation == Interpolation::CubicSpline {
track.values.len()
} else {
generated
};
let governing_resource = if track.interpolation == Interpolation::CubicSpline {
FootCycleClipResourceV1::InputValues
} else {
FootCycleClipResourceV1::GeneratedKeys
};
counts.candidate_values = checked_add(
governing_resource,
counts.candidate_values,
candidate_values,
)?;
counts.candidate_bytes += candidate_track_bytes(track, generated, candidate_values);
counts.work = checked_add(
FootCycleClipResourceV1::Work,
counts.work,
track.times.len(),
)?;
if !identity && track.interpolation == Interpolation::Linear {
counts.work = checked_add(FootCycleClipResourceV1::Work, counts.work, points.len())?;
}
counts.work = checked_add(FootCycleClipResourceV1::Work, counts.work, generated)?;
check_limit(
FootCycleClipResourceV1::Work,
counts.work,
FOOT_CYCLE_CLIP_V1_MAX_WORK,
)?;
}
Ok(PreflightedClipWarp {
counts,
track_candidate_keys,
})
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum FootCycleClipWarpRowV1 {
Authored(usize),
Knot(FootCycleClipWarpKnotV1),
Coalesced(usize, FootCycleClipWarpKnotV1),
}
pub fn time_warp_rows_v1<'a>(
control_points: &'a [ContactTimeWarpControlPointV1],
duration: f32,
track: &'a Track,
) -> impl Iterator<Item = FootCycleClipWarpRowV1> + 'a {
WarpRows {
times: &track.times,
authored_index: 0,
knots: time_warp_knots_v1(control_points, duration, track).peekable(),
}
}
struct WarpRows<'a, K: Iterator<Item = FootCycleClipWarpKnotV1>> {
times: &'a [f32],
authored_index: usize,
knots: std::iter::Peekable<K>,
}
impl<K: Iterator<Item = FootCycleClipWarpKnotV1>> WarpRows<'_, K> {
fn next_authored(&mut self) -> Option<FootCycleClipWarpRowV1> {
let index = self.authored_index;
self.times.get(index)?;
self.authored_index += 1;
Some(FootCycleClipWarpRowV1::Authored(index))
}
}
impl<K: Iterator<Item = FootCycleClipWarpKnotV1>> Iterator for WarpRows<'_, K> {
type Item = FootCycleClipWarpRowV1;
fn next(&mut self) -> Option<Self::Item> {
let Some(knot) = self.knots.peek().copied() else {
return self.next_authored();
};
if let Some(&authored_time) = self.times.get(self.authored_index) {
let equal_key_follows = self
.times
.get(self.authored_index + 1)
.is_some_and(|&next| next == knot.source_time());
if knot.coincides_with(authored_time) && !equal_key_follows {
self.knots.next();
let index = self.authored_index;
self.authored_index += 1;
return Some(FootCycleClipWarpRowV1::Coalesced(index, knot));
}
if authored_time < knot.source_time() {
return self.next_authored();
}
}
self.knots.next();
Some(FootCycleClipWarpRowV1::Knot(knot))
}
}
fn visit_warp_track_keys(
track: &Track,
track_index: usize,
points: &[ContactTimeWarpControlPointV1],
duration: f32,
mut visit: impl FnMut(CandidateKey) -> Result<(), FootCycleClipWarpError>,
) -> Result<(), FootCycleClipWarpError> {
let mut previous = None;
let mut previous_knot: Option<(FootCycleClipWarpKnotV1, f32)> = None;
for row in time_warp_rows_v1(points, duration, track) {
let named = match row {
FootCycleClipWarpRowV1::Authored(_) => None,
FootCycleClipWarpRowV1::Knot(knot) => Some((knot, knot.source_time())),
FootCycleClipWarpRowV1::Coalesced(index, knot) => Some((knot, track.times[index])),
};
if let (Some((previous, named_instant)), Some((knot, _))) = (previous_knot, named)
&& (knot.coincides_with(previous.source_time()) || knot.coincides_with(named_instant))
{
return Err(FootCycleClipWarpError::SourceTimeCollision { track_index });
}
if named.is_some() {
previous_knot = named;
}
let key = match row {
FootCycleClipWarpRowV1::Authored(index) => CandidateKey {
output_time: map_time(track.times[index], duration, points),
value: CandidateValue::Authored(index),
},
FootCycleClipWarpRowV1::Knot(knot) => CandidateKey {
output_time: knot.output_time(),
value: CandidateValue::Sampled(knot.source_time()),
},
FootCycleClipWarpRowV1::Coalesced(index, knot) => CandidateKey {
output_time: knot.output_time(),
value: CandidateValue::Authored(index),
},
};
validate_candidate_key(previous, key, track_index)?;
previous = Some(key);
visit(key)?;
}
Ok(())
}
fn validate_cubic(track: &Track, track_index: usize) -> Result<(), FootCycleClipWarpError> {
if track.times.len() <= 1 {
return Ok(());
}
match &track.values {
TrackValues::Vec3s(values) => {
let reference = values[1];
for key in 0..track.times.len() {
if !same_vec3(values[3 * key + 1], reference) {
return Err(FootCycleClipWarpError::UnsupportedCubicSpline {
track_index,
reason: FootCycleCubicSplineRefusalV1::DifferingValues,
});
}
if values[3 * key] != Vec3::ZERO || values[3 * key + 2] != Vec3::ZERO {
return Err(FootCycleClipWarpError::UnsupportedCubicSpline {
track_index,
reason: FootCycleCubicSplineRefusalV1::NonZeroTangent,
});
}
}
}
TrackValues::Quats(values) => {
let reference = values[1];
for key in 0..track.times.len() {
if !same_quat(values[3 * key + 1], reference) {
return Err(FootCycleClipWarpError::UnsupportedCubicSpline {
track_index,
reason: FootCycleCubicSplineRefusalV1::DifferingValues,
});
}
if values[3 * key] != Quat::from_xyzw(0.0, 0.0, 0.0, 0.0)
|| values[3 * key + 2] != Quat::from_xyzw(0.0, 0.0, 0.0, 0.0)
{
return Err(FootCycleClipWarpError::UnsupportedCubicSpline {
track_index,
reason: FootCycleCubicSplineRefusalV1::NonZeroTangent,
});
}
}
}
}
Ok(())
}
fn same_vec3(left: Vec3, right: Vec3) -> bool {
left.to_array()
.into_iter()
.zip(right.to_array())
.all(|(left, right)| left.to_bits() == right.to_bits())
}
fn same_quat(left: Quat, right: Quat) -> bool {
left.to_array()
.into_iter()
.zip(right.to_array())
.all(|(left, right)| left.to_bits() == right.to_bits())
}
fn validate_candidate_key(
previous: Option<CandidateKey>,
key: CandidateKey,
track_index: usize,
) -> Result<(), FootCycleClipWarpError> {
if let Some(previous) = previous
&& previous.output_time >= key.output_time
{
return Err(FootCycleClipWarpError::TimeCollision { track_index });
}
Ok(())
}
fn candidate_value(track: &Track, key: CandidateKey) -> TrackSample {
match (&track.values, key.value) {
(TrackValues::Vec3s(authored), CandidateValue::Authored(index)) => {
TrackSample::Vec3(authored[index])
}
(TrackValues::Quats(authored), CandidateValue::Authored(index)) => {
TrackSample::Quat(authored[index])
}
(_, CandidateValue::Sampled(source_time)) => sample_track(track, source_time),
}
}
fn warp_track(
track: &Track,
track_index: usize,
points: &[ContactTimeWarpControlPointV1],
duration: f32,
candidate_keys: usize,
) -> Result<Track, FootCycleClipWarpError> {
if track.interpolation == Interpolation::CubicSpline {
return Ok(track.clone());
}
let mut times = Vec::with_capacity(candidate_keys);
let (mut vec3s, mut quats) = match &track.values {
TrackValues::Vec3s(_) => (Vec::with_capacity(candidate_keys), Vec::new()),
TrackValues::Quats(_) => (Vec::new(), Vec::with_capacity(candidate_keys)),
};
visit_warp_track_keys(track, track_index, points, duration, |key| {
times.push(key.output_time);
match candidate_value(track, key) {
TrackSample::Vec3(value) => vec3s.push(value),
TrackSample::Quat(value) => quats.push(value),
}
Ok(())
})?;
let values = match &track.values {
TrackValues::Vec3s(_) => TrackValues::Vec3s(vec3s),
TrackValues::Quats(_) => TrackValues::Quats(quats),
};
Ok(Track {
bone: track.bone,
property: track.property,
interpolation: track.interpolation,
times,
values,
})
}
fn map_time(source_time: f32, duration: f32, points: &[ContactTimeWarpControlPointV1]) -> f32 {
let normalized = f64::from(source_time) / f64::from(duration);
let upper = points.partition_point(|point| point.input_time() <= normalized);
let right = upper.clamp(1, points.len() - 1);
let left = right - 1;
let x0 = points[left].input_time();
let x1 = points[right].input_time();
let y0 = points[left].output_time();
let y1 = points[right].output_time();
let fraction = (normalized - x0) / (x1 - x0);
let mapped = (y0 + fraction * (y1 - y0)) * f64::from(duration);
mapped as f32
}
fn checked_add(
resource: FootCycleClipResourceV1,
left: usize,
right: usize,
) -> Result<usize, FootCycleClipWarpError> {
left.checked_add(right)
.ok_or(FootCycleClipWarpError::CountOverflow { resource })
}
fn candidate_track_bytes(track: &Track, candidate_keys: usize, candidate_values: usize) -> usize {
candidate_keys * size_of::<f32>()
+ candidate_values
* match &track.values {
TrackValues::Vec3s(_) => size_of::<Vec3>(),
TrackValues::Quats(_) => size_of::<Quat>(),
}
}
fn check_limit(
resource: FootCycleClipResourceV1,
observed: usize,
maximum: usize,
) -> Result<(), FootCycleClipWarpError> {
if observed > maximum {
return Err(FootCycleClipWarpError::LimitExceeded {
resource,
observed,
maximum,
});
}
Ok(())
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{
ContactTransformIntervalV1, Property, TrackShapeViolation,
foot_cycle::clip_test_member_plan,
};
fn point(input: f64, output: f64) -> ContactTimeWarpControlPointV1 {
ContactTimeWarpControlPointV1::new(input, output)
}
fn plan(duration: f64, points: &[(f64, f64)]) -> FootCycleMemberPlanV1 {
clip_test_member_plan(ContactTransformOperationV1::time_warp(
duration,
points
.iter()
.map(|&(input, output)| point(input, output))
.collect(),
))
}
fn dense_points(count: usize, identity: bool) -> Vec<(f64, f64)> {
(0..count)
.map(|index| {
let input = index as f64 / (count - 1) as f64;
let output = if identity || index == 0 || index + 1 == count {
input
} else {
0.05 + 0.9 * input
};
(input, output)
})
.collect()
}
fn dense_vec_track(interpolation: Interpolation, count: usize) -> Track {
let denominator = (count - 1) as f32;
vec_track(
interpolation,
(0..count).map(|index| index as f32 / denominator).collect(),
vec![Vec3::ZERO; count],
)
}
fn non_identity_plan(duration: f64) -> FootCycleMemberPlanV1 {
plan(duration, &[(0.0, 0.0), (0.25, 0.5), (1.0, 1.0)])
}
fn vec_track(interpolation: Interpolation, times: Vec<f32>, values: Vec<Vec3>) -> Track {
Track {
bone: 7,
property: Property::Translation,
interpolation,
times,
values: TrackValues::Vec3s(values),
}
}
fn quat_track(interpolation: Interpolation, times: Vec<f32>, values: Vec<Quat>) -> Track {
Track {
bone: 9,
property: Property::Rotation,
interpolation,
times,
values: TrackValues::Quats(values),
}
}
fn clip(duration_s: f64, tracks: Vec<Track>) -> Clip {
Clip {
name: "walk_forward".into(),
duration_s,
tracks,
}
}
fn vec_values(track: &Track) -> &[Vec3] {
match &track.values {
TrackValues::Vec3s(values) => values,
TrackValues::Quats(_) => panic!("expected Vec3 values"),
}
}
fn quat_values(track: &Track) -> &[Quat] {
match &track.values {
TrackValues::Quats(values) => values,
TrackValues::Vec3s(_) => panic!("expected quaternion values"),
}
}
fn assert_clip_bits_equal(left: &Clip, right: &Clip) {
assert_eq!(left.name, right.name);
assert_eq!(left.duration_s.to_bits(), right.duration_s.to_bits());
assert_eq!(left.tracks.len(), right.tracks.len());
for (left, right) in left.tracks.iter().zip(&right.tracks) {
assert_eq!(left.bone, right.bone);
assert_eq!(left.property, right.property);
assert_eq!(left.interpolation, right.interpolation);
assert_eq!(
left.times
.iter()
.map(|time| time.to_bits())
.collect::<Vec<_>>(),
right
.times
.iter()
.map(|time| time.to_bits())
.collect::<Vec<_>>()
);
match (&left.values, &right.values) {
(TrackValues::Vec3s(left), TrackValues::Vec3s(right)) => assert_eq!(
left.iter()
.flat_map(|value| value.to_array().map(f32::to_bits))
.collect::<Vec<_>>(),
right
.iter()
.flat_map(|value| value.to_array().map(f32::to_bits))
.collect::<Vec<_>>()
),
(TrackValues::Quats(left), TrackValues::Quats(right)) => assert_eq!(
left.iter()
.flat_map(|value| value.to_array().map(f32::to_bits))
.collect::<Vec<_>>(),
right
.iter()
.flat_map(|value| value.to_array().map(f32::to_bits))
.collect::<Vec<_>>()
),
_ => panic!("candidate changed track value storage kind"),
}
}
}
fn assert_approx(left: f32, right: f32) {
assert!(
(left - right).abs() <= 2.0 * f32::EPSILON,
"{left} != {right}"
);
}
fn assert_error(
result: Result<Clip, FootCycleClipWarpError>,
expected: FootCycleClipWarpError,
) {
assert_eq!(result.unwrap_err(), expected);
}
fn assert_preflight_and_candidate_error(
source: &Clip,
plan: &FootCycleMemberPlanV1,
expected: FootCycleClipWarpError,
) {
assert_eq!(
preflight_time_warp_clip_v1(source, plan),
Err(expected.clone())
);
assert_error(time_warp_clip_v1(source, plan), expected);
}
#[test]
fn public_preflight_reports_exact_candidate_storage_and_bounded_work() {
let linear = vec_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![Vec3::ZERO, Vec3::ONE],
);
let mut cubic = vec_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
vec![Vec3::ZERO; 6],
);
cubic.bone = 8;
let step = Track {
bone: 9,
property: Property::Rotation,
interpolation: Interpolation::Step,
times: vec![0.0, 1.0],
values: TrackValues::Quats(vec![Quat::IDENTITY, Quat::from_rotation_y(0.5)]),
};
let source = clip(1.0, vec![linear, cubic, step]);
let plan = non_identity_plan(1.0);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.tracks(), 3);
assert_eq!(preflight.input_keys(), 6);
assert_eq!(preflight.input_values(), 10);
assert_eq!(preflight.candidate_keys(), 7);
assert_eq!(preflight.candidate_values(), 11);
assert_eq!(preflight.name_bytes(), source.name.len());
assert_eq!(
preflight.candidate_bytes(),
source.name.len()
+ 3 * size_of::<Track>()
+ 7 * size_of::<f32>()
+ 9 * size_of::<Vec3>()
+ 2 * size_of::<Quat>()
);
assert!(preflight.candidate_bytes() <= FOOT_CYCLE_CLIP_V1_MAX_CANDIDATE_BYTES);
assert_eq!(preflight.work(), 16);
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(
candidate
.tracks
.iter()
.map(|track| track.times.len())
.sum::<usize>(),
preflight.candidate_keys()
);
assert_eq!(
candidate
.tracks
.iter()
.map(|track| track.values.len())
.sum::<usize>(),
preflight.candidate_values()
);
}
#[test]
fn public_name_storage_bound_refuses_before_identity_or_nonidentity_candidate_allocation() {
let plans = [plan(1.0, &[(0.0, 0.0), (1.0, 1.0)]), non_identity_plan(1.0)];
for plan in &plans {
let mut exact = clip(
1.0,
vec![vec_track(Interpolation::Step, vec![0.0], vec![Vec3::ZERO])],
);
exact.name = "é".repeat(FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES / "é".len());
let preflight = preflight_time_warp_clip_v1(&exact, plan).unwrap();
assert_eq!(preflight.name_bytes(), FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES);
assert_eq!(time_warp_clip_v1(&exact, plan).unwrap().name, exact.name);
let mut first_excess = exact.clone();
first_excess.name.push('é');
let before = first_excess.clone();
let expected = FootCycleClipWarpError::LimitExceeded {
resource: FootCycleClipResourceV1::NameBytes,
observed: FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES + "é".len(),
maximum: FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES,
};
assert_preflight_and_candidate_error(&first_excess, plan, expected);
assert_clip_bits_equal(&first_excess, &before);
}
}
#[test]
fn maximum_step_tracks_and_map_knots_retain_only_the_preflighted_rows() {
let source = clip(
1.0,
(0..FOOT_CYCLE_CLIP_V1_MAX_TRACKS)
.map(|bone| Track {
bone,
property: Property::Translation,
interpolation: Interpolation::Step,
times: vec![0.5],
values: TrackValues::Vec3s(vec![Vec3::splat(bone as f32)]),
})
.collect(),
);
let plan = plan(
1.0,
&dense_points(crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS, false),
);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.candidate_keys(), FOOT_CYCLE_CLIP_V1_MAX_TRACKS);
assert_eq!(preflight.candidate_values(), FOOT_CYCLE_CLIP_V1_MAX_TRACKS);
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(candidate.tracks.len(), FOOT_CYCLE_CLIP_V1_MAX_TRACKS);
assert!(candidate.tracks.iter().all(|track| {
let value_capacity = match &track.values {
TrackValues::Vec3s(values) => values.capacity(),
TrackValues::Quats(values) => values.capacity(),
};
track.times.len() == 1
&& track.times.capacity() == 1
&& track.values.len() == 1
&& value_capacity == 1
}));
}
#[test]
fn nonunit_duration_scales_step_and_linear_inserted_knots() {
let duration = 1.1_f64;
let narrowed_duration = duration as f32;
let source = clip(
duration,
vec![
vec_track(
Interpolation::Step,
vec![0.0, narrowed_duration * 0.5, narrowed_duration],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
),
Track {
bone: 8,
property: Property::Scale,
interpolation: Interpolation::Linear,
times: vec![0.0, narrowed_duration * 0.5, narrowed_duration],
values: TrackValues::Vec3s(vec![
Vec3::ZERO,
Vec3::splat(10.0),
Vec3::splat(20.0),
]),
},
],
);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(duration)).unwrap();
let mapped_middle = 2.0 * narrowed_duration / 3.0;
assert_eq!(candidate.duration_s.to_bits(), duration.to_bits());
assert_eq!(candidate.tracks[0].times.len(), 3);
assert_eq!(candidate.tracks[1].times.len(), 4);
assert_eq!(candidate.tracks[0].times[0], 0.0);
assert_approx(candidate.tracks[0].times[1], mapped_middle);
assert_eq!(candidate.tracks[0].times[2], narrowed_duration);
assert_eq!(candidate.tracks[1].times[0], 0.0);
assert_approx(candidate.tracks[1].times[1], narrowed_duration * 0.5);
assert_approx(candidate.tracks[1].times[2], mapped_middle);
assert_eq!(candidate.tracks[1].times[3], narrowed_duration);
assert_eq!(
vec_values(&candidate.tracks[1]),
&[
Vec3::ZERO,
Vec3::splat(5.0),
Vec3::splat(10.0),
Vec3::splat(20.0),
]
);
}
#[test]
fn non_affine_linear_map_maps_authored_keys_and_samples_interior_knots() {
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, 0.5, 1.0],
vec![Vec3::ZERO, Vec3::splat(10.0), Vec3::splat(20.0)],
)],
);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(1.0)).unwrap();
assert_eq!(candidate.name, "walk_forward");
assert_eq!(candidate.duration_s, 1.0);
assert_eq!(candidate.tracks[0].bone, 7);
assert_eq!(candidate.tracks[0].property, Property::Translation);
assert_eq!(candidate.tracks[0].interpolation, Interpolation::Linear);
assert_eq!(candidate.tracks[0].times.len(), 4);
assert_eq!(candidate.tracks[0].times[0], 0.0);
assert_eq!(candidate.tracks[0].times[1], 0.5);
assert_approx(candidate.tracks[0].times[2], 2.0 / 3.0);
assert_eq!(candidate.tracks[0].times[3], 1.0);
assert_eq!(
vec_values(&candidate.tracks[0]),
&[
Vec3::ZERO,
Vec3::splat(5.0),
Vec3::splat(10.0),
Vec3::splat(20.0)
]
);
assert_eq!(
sample_track(&candidate.tracks[0], 0.5),
sample_track(&source.tracks[0], 0.25)
);
}
#[test]
fn nonidentity_multi_track_candidate_preserves_order_and_metadata() {
let zero_quat = Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
let source = clip(
1.0,
vec![
vec_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![Vec3::ZERO, Vec3::ONE],
),
Track {
bone: 8,
property: Property::Scale,
interpolation: Interpolation::Step,
times: vec![0.0, 1.0],
values: TrackValues::Vec3s(vec![Vec3::ONE, Vec3::splat(2.0)]),
},
quat_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
vec![
zero_quat,
Quat::IDENTITY,
zero_quat,
zero_quat,
Quat::IDENTITY,
zero_quat,
],
),
],
);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(1.0)).unwrap();
assert_eq!(
candidate
.tracks
.iter()
.map(|track| (track.bone, track.property, track.interpolation))
.collect::<Vec<_>>(),
vec![
(7, Property::Translation, Interpolation::Linear),
(8, Property::Scale, Interpolation::Step),
(9, Property::Rotation, Interpolation::CubicSpline),
]
);
}
#[test]
fn linear_knot_coincident_with_authored_key_is_deduplicated_deterministically() {
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, 0.25, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = non_identity_plan(1.0);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let first = time_warp_clip_v1(&source, &plan).unwrap();
let second = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(first.tracks[0].times, vec![0.0, 0.5, 1.0]);
assert_eq!(preflight.candidate_keys(), first.tracks[0].times.len());
assert_eq!(preflight.candidate_values(), first.tracks[0].values.len());
assert_eq!(first.tracks[0].times, second.tracks[0].times);
assert_eq!(vec_values(&first.tracks[0]), vec_values(&second.tracks[0]));
}
#[test]
fn linear_knot_that_narrows_to_authored_key_is_deduplicated() {
let duration = f64::from(17.0_f32 / 30.0);
let authored_time = 0.1_f32;
let control_phase = 3.0 / 17.0;
let reconstructed_time = control_phase * duration;
assert_ne!(reconstructed_time, f64::from(authored_time));
assert_eq!(reconstructed_time as f32, authored_time);
let source = clip(
duration,
vec![vec_track(
Interpolation::Linear,
vec![0.0, authored_time, duration as f32],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(
duration,
&[
(0.0, 0.0),
(control_phase, control_phase),
(0.5, 0.4),
(1.0, 1.0),
],
);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.candidate_keys(), 4);
assert_eq!(candidate.tracks[0].times.len(), preflight.candidate_keys());
assert_eq!(
candidate.tracks[0].values.len(),
preflight.candidate_values()
);
assert_eq!(candidate.tracks[0].times[1], authored_time);
assert_eq!(vec_values(&candidate.tracks[0])[1], Vec3::ONE);
assert_eq!(
candidate.tracks[0]
.times
.iter()
.filter(|&&time| time == authored_time)
.count(),
1
);
}
#[test]
fn interior_control_point_beside_a_source_endpoint_contributes_nothing() {
let above_start = f64::from(f32::from_bits(1));
let below_end = f64::from(f32::from_bits(1.0_f32.to_bits() - 1));
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, 0.5, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(
1.0,
&[(0.0, 0.0), (above_start, 0.2), (below_end, 0.8), (1.0, 1.0)],
);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.candidate_keys(), 3);
assert_eq!(candidate.tracks[0].times.len(), 3);
assert_eq!(
candidate.tracks[0].times[0], 0.0,
"the candidate still starts where the source does"
);
assert_eq!(
candidate.tracks[0].times[2], 1.0,
"the candidate still ends where the source does"
);
assert_eq!(
vec_values(&candidate.tracks[0]),
&[Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)]
);
}
#[test]
fn public_row_sequence_classifies_and_orders_the_emitted_keys() {
let duration = 2.0_f32;
let track = vec_track(
Interpolation::Linear,
vec![0.0, 0.5, 1.0, 1.5, 2.0],
vec![Vec3::ZERO; 5],
);
let operation = ContactTransformOperationV1::time_warp(
f64::from(duration),
[
(0.0, 0.0),
(1e-46, 0.05),
(0.25, 0.2),
(0.375, 0.5),
(1.0 - 1e-16, 0.95),
(1.0, 1.0),
]
.into_iter()
.map(|(input, output)| point(input, output))
.collect(),
);
let points = operation.control_points().unwrap();
assert_eq!((1e-46 * f64::from(duration)) as f32, 0.0);
assert_eq!(((1.0 - 1e-16) * f64::from(duration)) as f32, duration);
let rows = time_warp_rows_v1(points, duration, &track).collect::<Vec<_>>();
let classified = rows
.iter()
.map(|row| match row {
FootCycleClipWarpRowV1::Authored(index) => ("authored", *index, f32::NAN),
FootCycleClipWarpRowV1::Knot(knot) => ("knot", usize::MAX, knot.source_time()),
FootCycleClipWarpRowV1::Coalesced(index, knot) => {
("coalesced", *index, knot.source_time())
}
})
.collect::<Vec<_>>();
assert_eq!(
classified
.iter()
.map(|(kind, index, _)| (*kind, *index))
.collect::<Vec<_>>(),
vec![
("authored", 0),
("coalesced", 1),
("knot", usize::MAX),
("authored", 2),
("authored", 3),
("authored", 4),
],
"the two knots at the source endpoints are dropped"
);
assert_eq!(
classified[1].2, 0.5,
"the coalesced knot is the authored key"
);
assert_eq!(classified[2].2, 0.75);
let coalesced_knot = match rows[1] {
FootCycleClipWarpRowV1::Coalesced(_, knot) => knot,
_ => panic!("row 1 is coalesced"),
};
assert_eq!(coalesced_knot.control_point_index(), 2);
assert_eq!(coalesced_knot.output_time(), 0.4);
for interpolation in [Interpolation::Step, Interpolation::CubicSpline] {
let mut other = track.clone();
other.interpolation = interpolation;
assert_eq!(
time_warp_rows_v1(points, duration, &other).collect::<Vec<_>>(),
(0..other.times.len())
.map(FootCycleClipWarpRowV1::Authored)
.collect::<Vec<_>>(),
"{interpolation:?} tracks emit only authored rows"
);
}
}
#[test]
fn two_knots_straddling_one_authored_key_refuse() {
let interior = vec![0.0, 0.4, 0.6, 1.0];
let span_end = vec![0.25, 0.5, 0.75];
for (times, straddled, offsets) in [
(&interior, 0.4_f32, (-1_i32, 1_i32)),
(&span_end, 0.75, (-1, 1)),
(&interior, 0.4, (1, 2)),
(&interior, 0.4, (-2, -1)),
(&span_end, 0.75, (-2, -1)),
] {
let shifted = |offset: i32| {
f64::from(f32::from_bits(
straddled.to_bits().wrapping_add_signed(offset),
))
};
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
times.clone(),
vec![Vec3::ZERO; times.len()],
)],
);
let plan = plan(
1.0,
&[
(0.0, 0.0),
(shifted(offsets.0), 0.2),
(shifted(offsets.1), 0.5),
(1.0, 1.0),
],
);
let points = plan.operation().control_points().unwrap();
let knots = time_warp_rows_v1(points, 1.0, &source.tracks[0])
.filter(|row| !matches!(row, FootCycleClipWarpRowV1::Authored(_)))
.count();
assert_eq!(
knots, 2,
"{straddled} {offsets:?}: both knots must reach the sequence"
);
assert_preflight_and_candidate_error(
&source,
&plan,
FootCycleClipWarpError::SourceTimeCollision { track_index: 0 },
);
}
}
#[test]
fn two_control_points_that_are_one_instant_refuse_beside_a_key_or_not() {
let shared = 0.5_f32;
let values = vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)];
for beside_a_key in [true, false] {
let middle = if beside_a_key { shared } else { 0.25 };
for (label, second) in [
("the same instant", f64::from(shared)),
("adjacent instants", f64::from(shared.next_up())),
] {
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, middle, 1.0],
values.clone(),
)],
);
let plan = plan(
1.0,
&[
(0.0, 0.0),
(f64::from(shared), 0.4),
(second + f64::EPSILON, 0.6),
(1.0, 1.0),
],
);
let points = plan.operation().control_points().unwrap();
let knots = time_warp_rows_v1(points, 1.0, &source.tracks[0])
.filter_map(|row| match row {
FootCycleClipWarpRowV1::Authored(_) => None,
FootCycleClipWarpRowV1::Knot(knot)
| FootCycleClipWarpRowV1::Coalesced(_, knot) => Some(knot),
})
.collect::<Vec<_>>();
assert_eq!(knots.len(), 2, "{label}, beside a key: {beside_a_key}");
assert!(knots[1].coincides_with(knots[0].source_time()));
assert_preflight_and_candidate_error(
&source,
&plan,
FootCycleClipWarpError::SourceTimeCollision { track_index: 0 },
);
}
}
}
#[test]
fn a_knot_binds_to_the_authored_key_it_equals_not_the_one_beside_it() {
let first = 0.5_f32;
let second = first.next_up();
assert_ne!(first, second);
for bound_index in [1_usize, 2] {
let bound = if bound_index == 1 { first } else { second };
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, first, second, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0), Vec3::splat(3.0)],
)],
);
let plan = plan(1.0, &[(0.0, 0.0), (f64::from(bound), 0.25), (1.0, 1.0)]);
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
let times = &candidate.tracks[0].times;
assert_eq!(times.len(), 4, "both authored keys must be retained");
assert_eq!(
times[bound_index], 0.25,
"the key the knot equals takes the control point's output"
);
let other = if bound_index == 1 { 2 } else { 1 };
assert_ne!(
times[other], 0.25,
"the key beside it keeps its own mapped output"
);
assert_eq!(
vec_values(&candidate.tracks[0]),
&[Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0), Vec3::splat(3.0)]
);
}
}
#[test]
fn a_knot_beside_a_track_span_end_coalesces_from_either_side() {
for (key, emitted_index, before, after, outputs) in [
(
0.25_f32,
0_usize,
0.249_999_8,
0.250_000_2,
[0.07, 0.1, 0.13],
),
(0.75, 3, 0.749_999_8, 0.750_000_2, [0.87, 0.9, 0.93]),
] {
for offset in [-1_i32, 0, 1] {
let control_time = f32::from_bits(key.to_bits().wrapping_add_signed(offset));
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.25, 0.5, 0.75],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(
1.0,
&[
(0.0, 0.0),
(before, outputs[0]),
(f64::from(control_time), outputs[1]),
(after, outputs[2]),
(1.0, 1.0),
],
);
let points = plan.operation().control_points().unwrap();
if offset != 0 {
assert_ne!(
map_time(key, 1.0, points),
outputs[1] as f32,
"key {key} offset {offset}: the mapped output must differ"
);
}
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
let times = &candidate.tracks[0].times;
assert_eq!(times.len(), 4, "key {key} offset {offset}");
assert_eq!(
times[emitted_index], outputs[1] as f32,
"key {key} offset {offset}: the span end takes its control point's output"
);
assert!(times.windows(2).all(|pair| pair[0] < pair[1]));
}
}
}
#[test]
fn control_point_two_places_from_an_authored_key_keeps_its_own_key() {
let authored_time = 0.5_f32;
let two_below = f32::from_bits(authored_time.to_bits() - 2);
assert_ne!(f32::from_bits(authored_time.to_bits() - 1), two_below);
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, authored_time, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(1.0, &[(0.0, 0.0), (f64::from(two_below), 0.25), (1.0, 1.0)]);
let points = plan.operation().control_points().unwrap();
let knot = time_warp_rows_v1(points, 1.0, &source.tracks[0])
.find_map(|row| match row {
FootCycleClipWarpRowV1::Knot(knot) => Some(knot),
_ => None,
})
.expect("one interior knot of its own");
assert_eq!(knot.source_time(), two_below);
assert!(!knot.coincides_with(authored_time));
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
let times = &candidate.tracks[0].times;
assert_eq!(preflight.candidate_keys(), 4);
assert_eq!(times.len(), 4);
assert_eq!(times[1], 0.25, "the knot keeps its own key");
assert!(
times[2] > 0.25,
"the authored key keeps its own mapped output"
);
assert_eq!(vec_values(&candidate.tracks[0])[2], Vec3::ONE);
}
#[test]
fn stance_boundaries_coalesce_for_every_key_count_and_rate() {
for (rate, frames) in [24.0_f32, 30.0, 60.0]
.into_iter()
.flat_map(|rate| (18..=61_usize).map(move |frames| (rate, frames)))
{
let last = frames - 1;
let duration = f64::from(last as f32 / rate);
let source = clip(
duration,
vec![vec_track(
Interpolation::Linear,
(0..frames).map(|key| key as f32 / rate).collect(),
(0..frames).map(|key| Vec3::splat(key as f32)).collect(),
)],
);
let phase = |frame: usize| frame as f64 / last as f64;
let boundaries = [(4, 2), (6, 4), (last - 4, last - 6), (last - 2, last - 4)];
let mut points = vec![(0.0, 0.0)];
points.extend(
boundaries
.iter()
.map(|&(input, output)| (phase(input), phase(output))),
);
points.push((1.0, 1.0));
let plan = plan(duration, &points);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
let times = &candidate.tracks[0].times;
assert_eq!(
times.len(),
frames,
"{frames} keys at {rate} fps: coalescing must not add a key"
);
assert_eq!(preflight.candidate_keys(), frames);
assert!(times.windows(2).all(|pair| pair[0] < pair[1]));
for (input, output) in boundaries {
assert_eq!(
times[input],
(phase(output) * duration) as f32,
"{frames} keys at {rate} fps: key {input} must carry its control point output"
);
}
assert_eq!(
vec_values(&candidate.tracks[0]),
&(0..frames)
.map(|key| Vec3::splat(key as f32))
.collect::<Vec<_>>(),
"{frames} keys at {rate} fps: every emitted key keeps its authored value"
);
}
}
#[test]
fn adjacent_binary32_time_is_one_instant() {
let authored_time = 0.5_f32;
let adjacent_time = f32::from_bits(authored_time.to_bits() + 1);
assert_ne!(authored_time, adjacent_time);
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, authored_time, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(
1.0,
&[(0.0, 0.0), (f64::from(adjacent_time), 0.75), (1.0, 1.0)],
);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.candidate_keys(), 3);
assert_eq!(candidate.tracks[0].times.len(), preflight.candidate_keys());
assert_eq!(
candidate.tracks[0].values.len(),
preflight.candidate_values()
);
assert_eq!(candidate.tracks[0].times, vec![0.0, 0.75, 1.0]);
assert_eq!(vec_values(&candidate.tracks[0])[1], Vec3::ONE);
}
#[test]
fn linear_knot_that_rounds_down_to_authored_key_is_deduplicated() {
let authored_time = 0.5_f32;
let next_time = f32::from_bits(authored_time.to_bits() + 1);
let control_time =
f64::from(authored_time) + (f64::from(next_time) - f64::from(authored_time)) * 0.25;
assert!(control_time > f64::from(authored_time));
assert_eq!(control_time as f32, authored_time);
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, authored_time, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(
1.0,
&[
(0.0, 0.0),
(control_time, control_time),
(0.75, 0.7),
(1.0, 1.0),
],
);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(candidate.tracks[0].times.len(), preflight.candidate_keys());
assert_eq!(
candidate.tracks[0].values.len(),
preflight.candidate_values()
);
assert_eq!(candidate.tracks[0].times[1], authored_time);
assert_eq!(vec_values(&candidate.tracks[0])[1], Vec3::ONE);
}
#[test]
fn narrowed_authored_key_takes_the_control_point_output() {
let duration = f64::from(17.0_f32 / 30.0);
let authored_time = 0.1_f32;
let control_phase = 3.0 / 17.0;
assert_ne!(control_phase * duration, f64::from(authored_time));
assert_eq!((control_phase * duration) as f32, authored_time);
let source = clip(
duration,
vec![vec_track(
Interpolation::Linear,
vec![0.0, authored_time, duration as f32],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(
duration,
&[(0.0, 0.0), (control_phase, 4.0 / 17.0), (1.0, 1.0)],
);
let points = plan.operation().control_points().unwrap();
let output_time = (4.0 / 17.0 * duration) as f32;
assert_ne!(
map_time(authored_time, duration as f32, points),
output_time
);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.candidate_keys(), 3);
assert_eq!(candidate.tracks[0].times.len(), 3);
assert_eq!(candidate.tracks[0].times[1], output_time);
assert_eq!(vec_values(&candidate.tracks[0])[1], Vec3::ONE);
}
#[test]
fn coalesced_key_takes_the_control_point_output_not_the_mapped_one() {
let authored_time = 0.5_f32;
let next_time = f32::from_bits(authored_time.to_bits() + 1);
let control_time =
f64::from(authored_time) + (f64::from(next_time) - f64::from(authored_time)) * 0.49;
assert!(control_time > f64::from(authored_time));
assert_eq!(control_time as f32, authored_time);
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, authored_time, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(1.0, &[(0.0, 0.0), (control_time, 0.75), (1.0, 1.0)]);
let points = plan.operation().control_points().unwrap();
assert!(map_time(authored_time, 1.0, points) < 0.75);
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.candidate_keys(), 3);
assert_eq!(candidate.tracks[0].times, vec![0.0, 0.75, 1.0]);
assert_eq!(vec_values(&candidate.tracks[0])[1], Vec3::ONE);
}
#[test]
fn coincident_source_knot_coalesces_at_extreme_magnitudes() {
let duration = 12_794_115.0;
let source = clip(
duration,
vec![vec_track(
Interpolation::Linear,
vec![0.0, 5_496_923.0, 12_794_115.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let plan = plan(
duration,
&[
(0.0, 0.0),
(0.429_644_645_213_834_6, 7.523_163_845_262_64e-37),
(1.0, 1.0),
],
);
let output_time = (7.523_163_845_262_64e-37 * duration) as f32;
let preflight = preflight_time_warp_clip_v1(&source, &plan).unwrap();
let candidate = time_warp_clip_v1(&source, &plan).unwrap();
assert_eq!(preflight.candidate_keys(), 3);
assert_eq!(
candidate.tracks[0].times,
vec![0.0, output_time, duration as f32]
);
assert_eq!(vec_values(&candidate.tracks[0])[1], Vec3::ONE);
}
#[test]
fn step_maps_only_authored_breakpoints_and_preserves_hold() {
let source = clip(
1.0,
vec![vec_track(
Interpolation::Step,
vec![0.0, 0.5, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0)],
)],
);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(1.0)).unwrap();
assert_eq!(candidate.tracks[0].times.len(), 3);
assert_eq!(candidate.tracks[0].times[0], 0.0);
assert_approx(candidate.tracks[0].times[1], 2.0 / 3.0);
assert_eq!(candidate.tracks[0].times[2], 1.0);
assert_eq!(
sample_track(&candidate.tracks[0], 0.6),
TrackSample::Vec3(Vec3::ZERO)
);
}
#[test]
fn identity_map_is_structurally_identical_for_admissible_tracks() {
let mut cubic = vec_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
vec![
Vec3::ZERO,
Vec3::ONE,
Vec3::ZERO,
Vec3::ZERO,
Vec3::ONE,
Vec3::ZERO,
],
);
cubic.bone = 10;
let source = clip(
1.0,
vec![
vec_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![Vec3::ZERO, Vec3::ONE],
),
Track {
bone: 8,
property: Property::Scale,
interpolation: Interpolation::Step,
times: vec![0.2, 0.8],
values: TrackValues::Vec3s(vec![Vec3::ONE, Vec3::splat(2.0)]),
},
cubic,
],
);
let before = source.clone();
let candidate =
time_warp_clip_v1(&source, &plan(1.0, &[(0.0, 0.0), (0.4, 0.4), (1.0, 1.0)])).unwrap();
assert_clip_bits_equal(&candidate, &before);
assert_clip_bits_equal(&source, &before);
}
#[test]
fn non_exact_binary32_duration_is_permitted_and_preserved_as_binary64() {
let source = clip(
0.1,
vec![vec_track(
Interpolation::Step,
vec![0.0, 0.1_f32],
vec![Vec3::ZERO, Vec3::ONE],
)],
);
let candidate =
time_warp_clip_v1(&source, &plan(0.1, &[(0.0, 0.0), (0.5, 0.4), (1.0, 1.0)])).unwrap();
assert_eq!(candidate.duration_s.to_bits(), 0.1_f64.to_bits());
assert_eq!(candidate.tracks[0].times.last(), Some(&0.1_f32));
}
#[test]
fn constant_vec3_and_quaternion_cubic_tracks_are_retained_exactly() {
let zero_quat = Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
let vec_value = Vec3::new(1.0, 2.0, 3.0);
let quat_value = Quat::from_xyzw(0.0, 0.0, 0.5, 0.5);
let source = clip(
1.0,
vec![
vec_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
vec![
Vec3::ZERO,
vec_value,
Vec3::ZERO,
Vec3::ZERO,
vec_value,
Vec3::ZERO,
],
),
quat_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
vec![
zero_quat, quat_value, zero_quat, zero_quat, quat_value, zero_quat,
],
),
],
);
let before = format!("{:?}", source.tracks);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(1.0)).unwrap();
assert_eq!(format!("{:?}", candidate.tracks), before);
}
#[test]
fn one_key_cubic_track_is_retained_without_tangent_restrictions() {
let track = vec_track(
Interpolation::CubicSpline,
vec![0.5],
vec![Vec3::splat(2.0), Vec3::ONE, Vec3::splat(3.0)],
);
let source = clip(1.0, vec![track]);
let before = format!("{:?}", source.tracks[0]);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(1.0)).unwrap();
assert_eq!(format!("{:?}", candidate.tracks[0]), before);
}
#[test]
fn varying_or_nonzero_tangent_cubic_tracks_refuse_atomically() {
let cases = [
(
vec![
Vec3::ZERO,
Vec3::ONE,
Vec3::ZERO,
Vec3::ZERO,
Vec3::splat(2.0),
Vec3::ZERO,
],
FootCycleCubicSplineRefusalV1::DifferingValues,
),
(
vec![
Vec3::ZERO,
Vec3::ONE,
Vec3::ONE,
Vec3::ZERO,
Vec3::ONE,
Vec3::ZERO,
],
FootCycleCubicSplineRefusalV1::NonZeroTangent,
),
];
for (values, reason) in cases {
let source = clip(
1.0,
vec![vec_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
values,
)],
);
let before = format!("{source:?}");
assert_error(
time_warp_clip_v1(&source, &non_identity_plan(1.0)),
FootCycleClipWarpError::UnsupportedCubicSpline {
track_index: 0,
reason,
},
);
assert_eq!(format!("{source:?}"), before);
let identity = plan(1.0, &[(0.0, 0.0), (1.0, 1.0)]);
assert_error(
time_warp_clip_v1(&source, &identity),
FootCycleClipWarpError::UnsupportedCubicSpline {
track_index: 0,
reason,
},
);
}
}
#[test]
fn quaternion_cubic_value_and_both_tangent_directions_refuse_atomically() {
let zero = Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
let value = Quat::IDENTITY;
let different = Quat::from_rotation_y(0.5);
let cases = [
(
vec![zero, value, zero, zero, different, zero],
FootCycleCubicSplineRefusalV1::DifferingValues,
),
(
vec![Quat::IDENTITY, value, zero, zero, value, zero],
FootCycleCubicSplineRefusalV1::NonZeroTangent,
),
(
vec![zero, value, Quat::IDENTITY, zero, value, zero],
FootCycleCubicSplineRefusalV1::NonZeroTangent,
),
];
for (values, reason) in cases {
let source = clip(
1.0,
vec![quat_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
values,
)],
);
let before = format!("{source:?}");
assert_error(
time_warp_clip_v1(&source, &non_identity_plan(1.0)),
FootCycleClipWarpError::UnsupportedCubicSpline {
track_index: 0,
reason,
},
);
assert_eq!(format!("{source:?}"), before);
}
}
#[test]
fn source_and_output_binary32_collisions_refuse() {
let linear = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![Vec3::ZERO, Vec3::ONE],
)],
);
let source_collision = plan(
1.0,
&[(0.0, 0.0), (0.5, 0.4), (0.500_000_001, 0.6), (1.0, 1.0)],
);
assert_preflight_and_candidate_error(
&linear,
&source_collision,
FootCycleClipWarpError::SourceTimeCollision { track_index: 0 },
);
let mut second = vec_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![Vec3::ZERO, Vec3::ONE],
);
second.bone += 1;
let after_a_step_track = clip(
1.0,
vec![
vec_track(
Interpolation::Step,
vec![0.0, 1.0],
vec![Vec3::ZERO, Vec3::ONE],
),
second,
],
);
assert_preflight_and_candidate_error(
&after_a_step_track,
&source_collision,
FootCycleClipWarpError::SourceTimeCollision { track_index: 1 },
);
let step = clip(
1.0,
vec![vec_track(
Interpolation::Step,
vec![0.0, 0.25, 0.5, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0), Vec3::splat(3.0)],
)],
);
let output_collision = plan(
1.0,
&[(0.0, 0.0), (0.25, 0.5), (0.5, 0.500_000_001), (1.0, 1.0)],
);
assert_preflight_and_candidate_error(
&step,
&output_collision,
FootCycleClipWarpError::TimeCollision { track_index: 0 },
);
}
#[test]
fn preflight_refuses_linear_mapped_key_collision() {
let source = clip(
1.0,
vec![vec_track(
Interpolation::Linear,
vec![0.0, 0.25, 0.5, 1.0],
vec![Vec3::ZERO, Vec3::ONE, Vec3::splat(2.0), Vec3::splat(3.0)],
)],
);
assert_preflight_and_candidate_error(
&source,
&plan(
1.0,
&[(0.0, 0.0), (0.25, 0.5), (0.5, 0.500_000_001), (1.0, 1.0)],
),
FootCycleClipWarpError::TimeCollision { track_index: 0 },
);
}
#[test]
fn malformed_duplicate_and_out_of_range_tracks_refuse_without_mutation() {
let malformed = [
vec_track(Interpolation::Linear, vec![], vec![]),
vec_track(Interpolation::Linear, vec![f32::NAN], vec![Vec3::ZERO]),
vec_track(
Interpolation::Linear,
vec![0.5, 0.5],
vec![Vec3::ZERO, Vec3::ONE],
),
vec_track(Interpolation::Linear, vec![0.0, 1.0], vec![Vec3::ZERO]),
vec_track(
Interpolation::Linear,
vec![0.0],
vec![Vec3::splat(f32::INFINITY)],
),
];
let expected = [
TrackShapeViolation::EmptyTimes,
TrackShapeViolation::NonFiniteTime,
TrackShapeViolation::TimesNotStrictlyIncreasing,
TrackShapeViolation::ValueCountMismatch,
TrackShapeViolation::NonFiniteValue,
];
for (track, violation) in malformed.into_iter().zip(expected) {
let source = clip(1.0, vec![track]);
let before = format!("{source:?}");
assert_error(
time_warp_clip_v1(&source, &non_identity_plan(1.0)),
FootCycleClipWarpError::InvalidTrack {
track_index: 0,
source: DocumentShapeError::TrackShape {
clip_index: 0,
node: 7,
violation,
},
},
);
assert_eq!(format!("{source:?}"), before);
}
let track = vec_track(
Interpolation::Step,
vec![0.0, 1.1],
vec![Vec3::ZERO, Vec3::ONE],
);
let source = clip(1.0, vec![track]);
assert!(matches!(
time_warp_clip_v1(&source, &non_identity_plan(1.0)),
Err(FootCycleClipWarpError::TrackTimeOutOfRange { key_index: 1, .. })
));
let negative = clip(
1.0,
vec![vec_track(
Interpolation::Step,
vec![-f32::MIN_POSITIVE, 0.5],
vec![Vec3::ZERO, Vec3::ONE],
)],
);
assert!(matches!(
time_warp_clip_v1(&negative, &non_identity_plan(1.0)),
Err(FootCycleClipWarpError::TrackTimeOutOfRange { key_index: 0, .. })
));
let duplicate = vec_track(Interpolation::Step, vec![0.0], vec![Vec3::ZERO]);
let source = clip(1.0, vec![duplicate.clone(), duplicate]);
assert_error(
time_warp_clip_v1(&source, &non_identity_plan(1.0)),
FootCycleClipWarpError::DuplicateTrackTarget {
track_index: 1,
bone: 7,
property: Property::Translation,
},
);
}
#[test]
fn unsupported_operation_duration_and_map_structures_refuse() {
let source = clip(
1.0,
vec![vec_track(Interpolation::Step, vec![0.0], vec![Vec3::ZERO])],
);
let unsupported = clip_test_member_plan(ContactTransformOperationV1::trim(
ContactTransformIntervalV1::new(0.0, 1.0),
));
assert_error(
time_warp_clip_v1(&source, &unsupported),
FootCycleClipWarpError::UnsupportedOperation,
);
let distinct_duration = f64::from_bits(1.0_f64.to_bits() + 1);
assert_error(
time_warp_clip_v1(&source, &plan(distinct_duration, &[(0.0, 0.0), (1.0, 1.0)])),
FootCycleClipWarpError::DurationMismatch {
clip_duration_s: 1.0,
operation_duration_s: distinct_duration,
},
);
let bad_version = clip_test_member_plan(ContactTransformOperationV1::TimeWarp {
version: 2,
output_duration_s: 1.0,
control_points: vec![point(0.0, 0.0), point(1.0, 1.0)],
});
assert_error(
time_warp_clip_v1(&source, &bad_version),
FootCycleClipWarpError::UnsupportedVersion { version: 2 },
);
assert_error(
time_warp_clip_v1(&source, &plan(1.0, &[(0.1, 0.0), (1.0, 1.0)])),
FootCycleClipWarpError::InvalidMapEndpoints,
);
assert_error(
time_warp_clip_v1(&source, &plan(1.0, &[(0.0, 0.0), (0.9, 1.0)])),
FootCycleClipWarpError::InvalidMapEndpoints,
);
assert_error(
time_warp_clip_v1(
&source,
&plan(1.0, &[(0.0, 0.0), (0.5, 0.6), (0.4, 0.7), (1.0, 1.0)]),
),
FootCycleClipWarpError::NonMonotoneMap { index: 2 },
);
assert_error(
time_warp_clip_v1(
&source,
&plan(1.0, &[(0.0, 0.0), (0.4, 0.7), (0.5, 0.6), (1.0, 1.0)]),
),
FootCycleClipWarpError::NonMonotoneMap { index: 2 },
);
assert_error(
time_warp_clip_v1(
&source,
&plan(1.0, &[(0.0, 0.0), (f64::NAN, 0.5), (1.0, 1.0)]),
),
FootCycleClipWarpError::InvalidControlPoint { index: 1 },
);
assert_error(
time_warp_clip_v1(
&source,
&plan(1.0, &[(0.0, 0.0), (0.5, 1.0 + f64::EPSILON), (1.0, 1.0)]),
),
FootCycleClipWarpError::InvalidControlPoint { index: 1 },
);
}
#[test]
fn invalid_durations_control_point_counts_and_value_types_refuse() {
let source = clip(
1.0,
vec![vec_track(Interpolation::Step, vec![0.0], vec![Vec3::ZERO])],
);
for duration_s in [-1.0, 0.0, f64::NAN, f64::MAX] {
let invalid = clip(duration_s, source.tracks.clone());
assert!(matches!(
time_warp_clip_v1(&invalid, &plan(duration_s, &[(0.0, 0.0), (1.0, 1.0)])),
Err(FootCycleClipWarpError::InvalidClipDuration { .. })
));
}
assert_error(
time_warp_clip_v1(&source, &plan(1.0, &[(0.0, 0.0)])),
FootCycleClipWarpError::InvalidControlPointCount {
found: 1,
maximum: crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS,
},
);
let max_points = dense_points(crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS, true);
assert!(time_warp_clip_v1(&source, &plan(1.0, &max_points)).is_ok());
let too_many = dense_points(
crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS + 1,
true,
);
assert_error(
time_warp_clip_v1(&source, &plan(1.0, &too_many)),
FootCycleClipWarpError::InvalidControlPointCount {
found: crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS + 1,
maximum: crate::CONTACT_TRANSFORM_RESULT_V1_MAX_CONTROL_POINTS,
},
);
let wrong_type = clip(
1.0,
vec![Track {
bone: 0,
property: Property::Rotation,
interpolation: Interpolation::Linear,
times: vec![0.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO]),
}],
);
assert!(matches!(
time_warp_clip_v1(&wrong_type, &non_identity_plan(1.0)),
Err(FootCycleClipWarpError::InvalidTrack {
source: DocumentShapeError::TrackShape {
violation: TrackShapeViolation::ValueTypeMismatchesProperty,
..
},
..
})
));
}
#[test]
fn finite_zero_quaternion_refuses_before_identity_or_inserted_knot_sampling() {
let source = clip(
1.0,
vec![quat_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![
Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
Quat::from_xyzw(0.0, 0.0, 0.0, 0.0),
],
)],
);
let before = format!("{source:?}");
for candidate_plan in [plan(1.0, &[(0.0, 0.0), (1.0, 1.0)]), non_identity_plan(1.0)] {
assert_error(
time_warp_clip_v1(&source, &candidate_plan),
FootCycleClipWarpError::InvalidQuaternionKey {
track_index: 0,
key_index: 0,
},
);
}
assert_eq!(format!("{source:?}"), before);
let zero = Quat::from_xyzw(0.0, 0.0, 0.0, 0.0);
let cubic = clip(
1.0,
vec![quat_track(
Interpolation::CubicSpline,
vec![0.0, 1.0],
vec![zero, zero, zero, zero, zero, zero],
)],
);
assert_error(
time_warp_clip_v1(&cubic, &plan(1.0, &[(0.0, 0.0), (1.0, 1.0)])),
FootCycleClipWarpError::InvalidQuaternionKey {
track_index: 0,
key_index: 0,
},
);
}
#[test]
fn extreme_finite_linear_vec3_and_quaternion_samples_remain_finite() {
let magnitude = f32::MAX.sqrt() / 4.0;
let source = clip(
1.0,
vec![
vec_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![Vec3::splat(-f32::MAX), Vec3::splat(f32::MAX)],
),
Track {
bone: 8,
property: Property::Rotation,
interpolation: Interpolation::Linear,
times: vec![0.0, 1.0],
values: TrackValues::Quats(vec![
Quat::from_xyzw(magnitude, 0.0, 0.0, 0.0),
Quat::from_xyzw(0.0, magnitude, 0.0, 0.0),
]),
},
],
);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(1.0)).unwrap();
assert!(
vec_values(&candidate.tracks[0])
.iter()
.all(|value| value.is_finite())
);
assert!(
quat_values(&candidate.tracks[1])
.iter()
.all(|value| value.is_finite())
);
assert!(
quat_values(&candidate.tracks[1])[1]
.length_squared()
.is_finite()
);
}
#[test]
fn track_limit_is_enforced_at_exact_n_and_n_plus_one_through_public_api() {
let make_clip = |count: usize| {
clip(
1.0,
(0..count)
.map(|bone| Track {
bone,
property: Property::Translation,
interpolation: Interpolation::Step,
times: vec![0.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO]),
})
.collect(),
)
};
let identity = plan(1.0, &[(0.0, 0.0), (1.0, 1.0)]);
assert_eq!(
time_warp_clip_v1(&make_clip(FOOT_CYCLE_CLIP_V1_MAX_TRACKS), &identity)
.unwrap()
.tracks
.len(),
FOOT_CYCLE_CLIP_V1_MAX_TRACKS
);
assert_error(
time_warp_clip_v1(&make_clip(FOOT_CYCLE_CLIP_V1_MAX_TRACKS + 1), &identity),
FootCycleClipWarpError::LimitExceeded {
resource: FootCycleClipResourceV1::Tracks,
observed: FOOT_CYCLE_CLIP_V1_MAX_TRACKS + 1,
maximum: FOOT_CYCLE_CLIP_V1_MAX_TRACKS,
},
);
}
#[test]
fn every_aggregate_limit_has_exact_n_n_plus_one_and_overflow_coverage() {
let limits = [
(
FootCycleClipResourceV1::Tracks,
FOOT_CYCLE_CLIP_V1_MAX_TRACKS,
),
(
FootCycleClipResourceV1::InputKeys,
FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS,
),
(
FootCycleClipResourceV1::InputValues,
FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES,
),
(
FootCycleClipResourceV1::GeneratedKeys,
FOOT_CYCLE_CLIP_V1_MAX_GENERATED_KEYS,
),
(FootCycleClipResourceV1::Work, FOOT_CYCLE_CLIP_V1_MAX_WORK),
(
FootCycleClipResourceV1::NameBytes,
FOOT_CYCLE_CLIP_V1_MAX_NAME_BYTES,
),
];
for (resource, maximum) in limits {
assert_eq!(check_limit(resource, maximum, maximum), Ok(()));
assert_eq!(
check_limit(resource, maximum + 1, maximum),
Err(FootCycleClipWarpError::LimitExceeded {
resource,
observed: maximum + 1,
maximum,
})
);
assert_eq!(
checked_add(resource, usize::MAX, 1),
Err(FootCycleClipWarpError::CountOverflow { resource })
);
}
}
#[test]
fn input_value_limit_is_inclusive_through_public_api_before_shape_scan() {
assert_eq!(
FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES,
3 * FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS
);
let exact = clip(
1.0,
vec![Track {
bone: 0,
property: Property::Translation,
interpolation: Interpolation::Linear,
times: vec![0.0],
values: TrackValues::Vec3s(vec![Vec3::ZERO; FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES]),
}],
);
assert!(matches!(
time_warp_clip_v1(&exact, &plan(1.0, &[(0.0, 0.0), (1.0, 1.0)])),
Err(FootCycleClipWarpError::InvalidTrack { .. })
));
let first_excess = clip(
1.0,
vec![Track {
bone: 0,
property: Property::Translation,
interpolation: Interpolation::Linear,
times: vec![0.0],
values: TrackValues::Vec3s(vec![
Vec3::ZERO;
FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES + 1
]),
}],
);
assert_error(
time_warp_clip_v1(&first_excess, &plan(1.0, &[(0.0, 0.0), (1.0, 1.0)])),
FootCycleClipWarpError::LimitExceeded {
resource: FootCycleClipResourceV1::InputValues,
observed: FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES + 1,
maximum: FOOT_CYCLE_CLIP_V1_MAX_INPUT_VALUES,
},
);
}
#[test]
fn input_and_generated_key_bounds_are_observable_at_n_and_n_plus_one() {
let exact = clip(
1.0,
vec![dense_vec_track(
Interpolation::Linear,
FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS,
)],
);
let exact_candidate =
time_warp_clip_v1(&exact, &plan(1.0, &[(0.0, 0.0), (1.0, 1.0)])).unwrap();
assert_eq!(
exact_candidate.tracks[0].times.len(),
FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS
);
assert_eq!(
time_warp_clip_v1(&exact, &plan(1.0, &[(0.0, 0.0), (0.5, 0.5), (1.0, 1.0)]),)
.unwrap()
.tracks[0]
.times
.len(),
FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS
);
let generated_n_plus_one = plan(1.0, &[(0.0, 0.0), (0.5, 0.4), (1.0, 1.0)]);
assert_error(
time_warp_clip_v1(&exact, &generated_n_plus_one),
FootCycleClipWarpError::LimitExceeded {
resource: FootCycleClipResourceV1::GeneratedKeys,
observed: FOOT_CYCLE_CLIP_V1_MAX_GENERATED_KEYS + 1,
maximum: FOOT_CYCLE_CLIP_V1_MAX_GENERATED_KEYS,
},
);
let input_n_plus_one = clip(
1.0,
vec![dense_vec_track(
Interpolation::Step,
FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS + 1,
)],
);
assert_error(
time_warp_clip_v1(&input_n_plus_one, &plan(1.0, &[(0.0, 0.0), (1.0, 1.0)])),
FootCycleClipWarpError::LimitExceeded {
resource: FootCycleClipResourceV1::InputKeys,
observed: FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS + 1,
maximum: FOOT_CYCLE_CLIP_V1_MAX_INPUT_KEYS,
},
);
}
#[test]
fn work_bound_is_observable_at_exact_n_and_n_plus_one() {
let work_clip = |linear_tracks: usize, step_keys: usize| {
let mut tracks: Vec<Track> = (0..linear_tracks)
.map(|bone| Track {
bone,
property: Property::Translation,
interpolation: Interpolation::Linear,
times: vec![0.5],
values: TrackValues::Vec3s(vec![Vec3::ZERO]),
})
.collect();
tracks.push(Track {
bone: linear_tracks,
property: Property::Translation,
interpolation: Interpolation::Step,
times: (0..step_keys)
.map(|index| index as f32 / (step_keys - 1).max(1) as f32)
.collect(),
values: TrackValues::Vec3s(vec![Vec3::ZERO; step_keys]),
});
clip(1.0, tracks)
};
let exact = work_clip(2_047, 1);
let exact_points = dense_points(4_096, false);
assert_eq!(
time_warp_clip_v1(&exact, &plan(1.0, &exact_points))
.unwrap()
.tracks
.len(),
2_048
);
let n_plus_one = work_clip(2_047, 1_025);
let n_plus_one_points = dense_points(4_095, false);
assert_error(
time_warp_clip_v1(&n_plus_one, &plan(1.0, &n_plus_one_points)),
FootCycleClipWarpError::LimitExceeded {
resource: FootCycleClipResourceV1::Work,
observed: FOOT_CYCLE_CLIP_V1_MAX_WORK + 1,
maximum: FOOT_CYCLE_CLIP_V1_MAX_WORK,
},
);
}
#[test]
fn quaternion_linear_interior_knot_uses_existing_shortest_path_sampler() {
let source = clip(
1.0,
vec![quat_track(
Interpolation::Linear,
vec![0.0, 1.0],
vec![Quat::IDENTITY, -Quat::from_rotation_y(std::f32::consts::PI)],
)],
);
let candidate = time_warp_clip_v1(&source, &non_identity_plan(1.0)).unwrap();
assert_eq!(candidate.tracks[0].times, vec![0.0, 0.5, 1.0]);
let expected = match sample_track(&source.tracks[0], 0.25) {
TrackSample::Quat(value) => value,
TrackSample::Vec3(_) => unreachable!(),
};
assert!(quat_values(&candidate.tracks[0])[1].abs_diff_eq(expected, 1.0e-6));
}
}