use glam::{Quat, Vec2, Vec3};
pub trait Interpolate: Copy {
fn interpolate(a: &Self, b: &Self, span: f32, t: f32) -> Self;
fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self;
}
impl Interpolate for f32 {
fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
a + (b - a) * t
}
fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
if span > 0.0 {
last + (last - prev) / span * ahead
} else {
*last
}
}
}
impl Interpolate for Vec2 {
fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
a.lerp(*b, t)
}
fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
if span > 0.0 {
*last + (*last - *prev) / span * ahead
} else {
*last
}
}
}
impl Interpolate for Vec3 {
fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
a.lerp(*b, t)
}
fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
if span > 0.0 {
*last + (*last - *prev) / span * ahead
} else {
*last
}
}
}
impl Interpolate for Quat {
fn interpolate(a: &Self, b: &Self, _span: f32, t: f32) -> Self {
a.slerp(*b, t).normalize()
}
fn extrapolate(_prev: &Self, last: &Self, _span: f32, _ahead: f32) -> Self {
*last
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct Pose {
pub position: Vec3,
pub rotation: Quat,
pub velocity: Option<Vec3>,
}
impl Default for Pose {
fn default() -> Self {
Self::at(Vec3::ZERO)
}
}
impl Pose {
pub const fn at(position: Vec3) -> Self {
Self {
position,
rotation: Quat::IDENTITY,
velocity: None,
}
}
#[must_use]
pub const fn facing(self, rotation: Quat) -> Self {
Self { rotation, ..self }
}
#[must_use]
pub const fn moving(self, velocity: Vec3) -> Self {
Self {
velocity: Some(velocity),
..self
}
}
}
impl Interpolate for Pose {
fn interpolate(a: &Self, b: &Self, span: f32, t: f32) -> Self {
let position = match (a.velocity, b.velocity) {
(Some(va), Some(vb)) if span > 0.0 => {
let (t2, t3) = (t * t, t * t * t);
let h00 = 2.0 * t3 - 3.0 * t2 + 1.0;
let h10 = t3 - 2.0 * t2 + t;
let h01 = -2.0 * t3 + 3.0 * t2;
let h11 = t3 - t2;
a.position * h00 + va * (span * h10) + b.position * h01 + vb * (span * h11)
}
_ => a.position.lerp(b.position, t),
};
Self {
position,
rotation: a.rotation.slerp(b.rotation, t).normalize(),
velocity: match (a.velocity, b.velocity) {
(Some(va), Some(vb)) => Some(va.lerp(vb, t)),
_ => None,
},
}
}
fn extrapolate(prev: &Self, last: &Self, span: f32, ahead: f32) -> Self {
let velocity = last.velocity.or_else(|| {
(span > 0.0).then(|| (last.position - prev.position) / span)
});
Self {
position: last.position + velocity.unwrap_or(Vec3::ZERO) * ahead,
..*last
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SnapshotState {
Interpolated,
Extrapolated,
Held,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SnapshotSample<T> {
pub value: T,
pub state: SnapshotState,
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SnapshotConfig {
pub delay: f64,
pub max_extrapolation: f64,
pub capacity: usize,
}
impl Default for SnapshotConfig {
fn default() -> Self {
Self {
delay: 0.1,
max_extrapolation: 0.25,
capacity: 64,
}
}
}
impl SnapshotConfig {
pub fn validate(&self) -> Result<(), &'static str> {
if self.capacity < 2 {
return Err("SnapshotConfig: capacity must be at least 2");
}
if !(self.delay.is_finite() && self.delay >= 0.0) {
return Err("SnapshotConfig: delay must be a finite number of seconds, 0 or more");
}
if !(self.max_extrapolation.is_finite() && self.max_extrapolation >= 0.0) {
return Err("SnapshotConfig: max_extrapolation must be a finite number of seconds, 0 or more");
}
Ok(())
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum SnapshotPush {
Added,
Duplicate,
TooOld,
Invalid,
}
#[derive(Clone, Debug)]
pub struct SnapshotBuffer<T: Interpolate> {
config: SnapshotConfig,
samples: Vec<(f64, T)>,
}
impl<T: Interpolate> Default for SnapshotBuffer<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Interpolate> SnapshotBuffer<T> {
pub fn new() -> Self {
Self::with_config(SnapshotConfig::default())
}
pub fn with_config(config: SnapshotConfig) -> Self {
if let Err(message) = config.validate() {
panic!("{message}");
}
Self {
config,
samples: Vec::with_capacity(config.capacity),
}
}
pub fn config(&self) -> SnapshotConfig {
self.config
}
pub fn push(&mut self, time: f64, value: T) -> SnapshotPush {
if !time.is_finite() {
return SnapshotPush::Invalid;
}
let at = self.samples.partition_point(|s| s.0 < time);
if self.samples.get(at).is_some_and(|s| s.0 == time) {
return SnapshotPush::Duplicate;
}
if self.samples.len() == self.config.capacity {
if at == 0 {
return SnapshotPush::TooOld;
}
self.samples.remove(0);
self.samples.insert(at - 1, (time, value));
} else {
self.samples.insert(at, (time, value));
}
SnapshotPush::Added
}
pub fn sample_at(&self, time: f64) -> Option<SnapshotSample<T>> {
let first = self.samples.first()?;
let last = self.samples.last()?;
if time <= first.0 {
return Some(SnapshotSample {
value: first.1,
state: SnapshotState::Held,
});
}
if time >= last.0 {
let (prev, span) = match self.samples.len().checked_sub(2).map(|i| &self.samples[i]) {
Some(p) => (&p.1, (last.0 - p.0) as f32),
None => (&last.1, 0.0),
};
let ahead = time - last.0;
return Some(if ahead == 0.0 {
SnapshotSample {
value: last.1,
state: SnapshotState::Interpolated,
}
} else if ahead <= self.config.max_extrapolation {
SnapshotSample {
value: T::extrapolate(prev, &last.1, span, ahead as f32),
state: SnapshotState::Extrapolated,
}
} else {
SnapshotSample {
value: T::extrapolate(prev, &last.1, span, self.config.max_extrapolation as f32),
state: SnapshotState::Held,
}
});
}
let i = self.samples.partition_point(|s| s.0 <= time) - 1;
let (a, b) = (&self.samples[i], &self.samples[i + 1]);
let span = b.0 - a.0;
Some(SnapshotSample {
value: T::interpolate(&a.1, &b.1, span as f32, ((time - a.0) / span) as f32),
state: SnapshotState::Interpolated,
})
}
pub fn sample(&self, now: f64) -> Option<SnapshotSample<T>> {
self.sample_at(now - self.config.delay)
}
pub fn latest_time(&self) -> Option<f64> {
self.samples.last().map(|s| s.0)
}
pub fn oldest_time(&self) -> Option<f64> {
self.samples.first().map(|s| s.0)
}
pub fn len(&self) -> usize {
self.samples.len()
}
pub fn is_empty(&self) -> bool {
self.samples.is_empty()
}
pub fn clear(&mut self) {
self.samples.clear();
}
pub fn trim_before(&mut self, time: f64) {
let keep_from = self.samples.partition_point(|s| s.0 <= time).saturating_sub(1);
self.samples.drain(..keep_from);
}
}
const CLOCK_WINDOW: usize = 32;
const CLOCK_JUMP_CONFIRM: u32 = 8;
#[derive(Clone, Debug)]
pub struct ClockSync {
window: [f64; CLOCK_WINDOW],
filled: usize,
next: usize,
offset: Option<f64>,
off_streak: u32,
rise: f64,
fall: f64,
jump: f64,
}
impl Default for ClockSync {
fn default() -> Self {
Self::new()
}
}
impl ClockSync {
pub fn new() -> Self {
Self {
window: [0.0; CLOCK_WINDOW],
filled: 0,
next: 0,
offset: None,
off_streak: 0,
rise: 0.05,
fall: 0.25,
jump: 1.0,
}
}
pub fn observe(&mut self, remote_time: f64, local_now: f64) {
let sample = local_now - remote_time;
if !sample.is_finite() {
return;
}
if let Some(estimate) = self.offset {
if (sample - estimate).abs() > self.jump {
self.off_streak += 1;
if self.off_streak >= CLOCK_JUMP_CONFIRM {
self.filled = 0;
self.next = 0;
self.offset = None;
self.off_streak = 0;
}
} else {
self.off_streak = 0;
}
}
self.window[self.next] = sample;
self.next = (self.next + 1) % CLOCK_WINDOW;
self.filled = (self.filled + 1).min(CLOCK_WINDOW);
let mut sorted = self.window;
let used = &mut sorted[..self.filled];
used.sort_unstable_by(f64::total_cmp);
let target = used[(self.filled - 1) / 8];
self.offset = Some(match self.offset {
None => target,
Some(o) => {
let rate = if target < o { self.fall } else { self.rise };
o + (target - o) * rate
}
});
}
pub fn offset(&self) -> Option<f64> {
self.offset
}
pub fn to_local(&self, remote_time: f64) -> Option<f64> {
self.offset.map(|o| remote_time + o)
}
pub fn to_remote(&self, local_time: f64) -> Option<f64> {
self.offset.map(|o| local_time - o)
}
pub fn samples(&self) -> usize {
self.filled
}
pub fn reset(&mut self) {
*self = Self::new();
}
}
#[derive(Clone, Debug)]
pub struct SnapshotStream<T: Interpolate> {
clock: ClockSync,
buffer: SnapshotBuffer<T>,
}
impl<T: Interpolate> Default for SnapshotStream<T> {
fn default() -> Self {
Self::new()
}
}
impl<T: Interpolate> SnapshotStream<T> {
pub fn new() -> Self {
Self::with_config(SnapshotConfig::default())
}
pub fn with_config(config: SnapshotConfig) -> Self {
Self {
clock: ClockSync::new(),
buffer: SnapshotBuffer::with_config(config),
}
}
pub fn push(&mut self, remote_time: f64, local_now: f64, value: T) -> SnapshotPush {
let result = self.buffer.push(remote_time, value);
if matches!(result, SnapshotPush::Added | SnapshotPush::TooOld) {
self.clock.observe(remote_time, local_now);
}
result
}
pub fn sample(&self, now: f64) -> Option<SnapshotSample<T>> {
let remote = self.clock.to_remote(now - self.buffer.config.delay)?;
self.buffer.sample_at(remote)
}
pub fn set_delay(&mut self, delay: f64) {
assert!(
delay.is_finite() && delay >= 0.0,
"SnapshotStream::set_delay: delay must be a finite number of seconds, 0 or more"
);
self.buffer.config.delay = delay;
}
pub fn clock(&self) -> &ClockSync {
&self.clock
}
pub fn buffer(&self) -> &SnapshotBuffer<T> {
&self.buffer
}
pub fn latest_local_time(&self) -> Option<f64> {
self.clock.to_local(self.buffer.latest_time()?)
}
pub fn clear(&mut self) {
self.buffer.clear();
}
pub fn reset(&mut self) {
self.buffer.clear();
self.clock.reset();
}
}
#[cfg(feature = "3d")]
#[derive(Clone, Debug, Default)]
pub struct NetPose {
pub stream: SnapshotStream<Pose>,
}
#[cfg(feature = "3d")]
pub fn apply_net_poses(ctx: &mut crate::Ctx) {
let now = ctx.time();
ctx.world
.query::<(&NetPose, &mut crate::Transform3D)>()
.for_each(|(pose, transform)| {
if let Some(sample) = pose.stream.sample(now) {
transform.position = sample.value.position;
transform.rotation = sample.value.rotation;
}
});
}
#[cfg(test)]
mod tests {
use super::*;
use glam::vec3;
fn x(sample: Option<SnapshotSample<Vec3>>) -> f32 {
sample.unwrap().value.x
}
fn walk(t: f64) -> Vec3 {
vec3(t as f32 * 10.0, 0.0, 0.0)
}
#[test]
fn interpolates_linearly_between_snapshots() {
let mut b = SnapshotBuffer::new();
b.push(0.0, walk(0.0));
b.push(0.1, walk(0.1));
let s = b.sample_at(0.025).unwrap();
assert!((s.value.x - 0.25).abs() < 1e-5);
assert_eq!(s.state, SnapshotState::Interpolated);
}
#[test]
fn hermite_follows_a_curve_better_than_a_straight_line() {
let a = Pose::at(vec3(10.0, 0.0, 0.0)).moving(vec3(0.0, 10.0, 0.0));
let b = Pose::at(vec3(0.0, 10.0, 0.0)).moving(vec3(-10.0, 0.0, 0.0));
let span = std::f32::consts::FRAC_PI_2; let hermite = Pose::interpolate(&a, &b, span * 1.0, 0.5).position;
let linear = Pose::interpolate(&Pose::at(a.position), &Pose::at(b.position), span, 0.5).position;
let truth = vec3(7.071, 7.071, 0.0);
assert!((hermite - truth).length() < (linear - truth).length() * 0.5);
assert!((Pose::interpolate(&a, &b, span, 0.0).position - a.position).length() < 1e-5);
assert!((Pose::interpolate(&a, &b, span, 1.0).position - b.position).length() < 1e-5);
}
#[test]
fn hermite_needs_both_velocities() {
let a = Pose::at(vec3(0.0, 0.0, 0.0)).moving(vec3(100.0, 0.0, 0.0));
let b = Pose::at(vec3(10.0, 0.0, 0.0));
assert_eq!(Pose::interpolate(&a, &b, 1.0, 0.5).position, vec3(5.0, 0.0, 0.0));
assert_eq!(Pose::interpolate(&a, &b, 1.0, 0.5).velocity, None);
}
#[test]
fn rotation_takes_the_short_way_round() {
let a = Pose::at(Vec3::ZERO).facing(Quat::from_rotation_y(0.1));
let b = Pose::at(Vec3::ZERO).facing(Quat::from_rotation_y(std::f32::consts::TAU - 0.1));
let mid = Pose::interpolate(&a, &b, 1.0, 0.5).rotation;
assert!(mid.angle_between(Quat::IDENTITY) < 1e-3);
assert!((mid.length() - 1.0).abs() < 1e-5);
}
#[test]
fn out_of_order_and_duplicate_snapshots_end_up_sorted() {
let mut b = SnapshotBuffer::new();
let order = [3, 1, 4, 0, 2, 3, 1];
let results: Vec<_> = order
.iter()
.map(|&i| b.push(i as f64 * 0.1, walk(i as f64 * 0.1)))
.collect();
assert_eq!(
results,
[
SnapshotPush::Added,
SnapshotPush::Added,
SnapshotPush::Added,
SnapshotPush::Added,
SnapshotPush::Added,
SnapshotPush::Duplicate,
SnapshotPush::Duplicate,
]
);
assert_eq!(b.len(), 5);
for i in 0..40 {
let t = i as f64 * 0.01;
assert!((x(b.sample_at(t)) - walk(t).x).abs() < 1e-4, "t={t}");
}
}
#[test]
fn a_duplicate_does_not_overwrite_the_first_copy() {
let mut b = SnapshotBuffer::new();
b.push(1.0, 1.0f32);
b.push(1.0, 99.0);
assert_eq!(b.sample_at(1.0).unwrap().value, 1.0);
}
#[test]
fn extrapolates_for_a_bounded_time_then_holds_where_it_ended() {
let mut b = SnapshotBuffer::new();
b.push(0.0, walk(0.0));
b.push(0.1, walk(0.1));
let s = b.sample_at(0.2).unwrap(); assert_eq!(s.state, SnapshotState::Extrapolated);
assert!((s.value.x - 2.0).abs() < 1e-4, "{}", s.value.x);
let cap = b.sample_at(0.349).unwrap();
assert_eq!(cap.state, SnapshotState::Extrapolated);
assert!((cap.value.x - 3.49).abs() < 1e-4);
for t in [0.36, 1.0, 100.0] {
let h = b.sample_at(t).unwrap();
assert_eq!(h.state, SnapshotState::Held, "t={t}");
assert!((h.value.x - 3.5).abs() < 1e-4, "t={t}");
}
}
#[test]
fn velocity_drives_extrapolation_when_given() {
let mut b = SnapshotBuffer::new();
b.push(0.0, Pose::at(vec3(1.0, 0.0, 0.0)).moving(vec3(0.0, 4.0, 0.0)));
let s = b.sample_at(0.1).unwrap();
assert_eq!(s.state, SnapshotState::Extrapolated);
assert!((s.value.position - vec3(1.0, 0.4, 0.0)).length() < 1e-5);
let mut b = SnapshotBuffer::new();
b.push(0.0, Pose::at(vec3(1.0, 0.0, 0.0)));
assert_eq!(b.sample_at(0.2).unwrap().value.position, vec3(1.0, 0.0, 0.0));
}
#[test]
fn zero_extrapolation_holds_at_the_last_snapshot() {
let mut b = SnapshotBuffer::with_config(SnapshotConfig {
max_extrapolation: 0.0,
..Default::default()
});
b.push(0.0, walk(0.0));
b.push(0.1, walk(0.1));
let s = b.sample_at(0.15).unwrap();
assert_eq!(s.state, SnapshotState::Held);
assert!((s.value.x - 1.0).abs() < 1e-5);
}
#[test]
fn before_the_first_snapshot_the_first_is_held() {
let mut b = SnapshotBuffer::new();
b.push(5.0, walk(5.0));
b.push(5.1, walk(5.1));
let s = b.sample_at(1.0).unwrap();
assert_eq!((s.value, s.state), (walk(5.0), SnapshotState::Held));
}
#[test]
fn full_buffer_drops_the_oldest_and_refuses_older_ones() {
let mut b = SnapshotBuffer::with_config(SnapshotConfig {
capacity: 4,
..Default::default()
});
for i in 1..=4 {
b.push(i as f64, i as f32);
}
assert_eq!(b.push(0.5, 0.0), SnapshotPush::TooOld);
assert_eq!(b.push(5.0, 5.0), SnapshotPush::Added);
assert_eq!(b.oldest_time(), Some(2.0));
assert_eq!(b.push(2.5, 2.5), SnapshotPush::Added);
assert_eq!((b.oldest_time(), b.len()), (Some(2.5), 4));
assert_eq!(b.sample_at(3.5).unwrap().value, 3.5);
}
#[test]
fn rejects_non_finite_times() {
let mut b = SnapshotBuffer::new();
assert_eq!(b.push(f64::INFINITY, 0.0f32), SnapshotPush::Invalid);
assert_eq!(b.push(f64::NAN, 0.0f32), SnapshotPush::Invalid);
assert!(b.is_empty());
}
#[test]
fn trim_keeps_one_snapshot_before_the_time() {
let mut b = SnapshotBuffer::new();
for i in 0..10 {
b.push(i as f64, i as f32);
}
b.trim_before(4.5);
assert_eq!(b.oldest_time(), Some(4.0));
assert_eq!(b.sample_at(4.5).unwrap().value, 4.5);
b.trim_before(-3.0);
assert_eq!(b.len(), 6);
b.trim_before(100.0);
assert_eq!(b.len(), 1);
}
#[test]
fn clock_sync_ignores_jitter_and_late_packets() {
let mut clock = ClockSync::new();
let mut worst: f64 = 0.0;
for i in 0..400u32 {
let sent = 7.0 + i as f64 * 0.02;
let mut trip = 0.02 + ((i * 37) % 11) as f64 * 0.01;
if i % 53 == 0 {
trip += 1.0;
}
clock.observe(sent, sent + 100.0 + trip);
if i > 60 {
worst = worst.max((clock.offset().unwrap() - 100.02).abs());
}
}
assert!(worst < 0.025, "estimate strayed {worst} s from the fastest trip");
}
#[test]
fn clock_sync_is_not_dragged_by_one_freakishly_fast_packet() {
let mut clock = ClockSync::new();
for i in 0..40 {
clock.observe(i as f64, i as f64 + 0.05);
}
clock.observe(40.0, 40.0 + 0.005); assert!((clock.offset().unwrap() - 0.05).abs() < 1e-9);
}
#[test]
fn clock_sync_follows_slow_drift() {
let mut clock = ClockSync::new();
for i in 0..6000 {
let sent = i as f64 * 0.05;
let local = sent * (1.0 - 1e-4) + 0.04 + (i % 4) as f64 * 0.01;
clock.observe(sent, local);
}
let truth = 0.04 - 299.95e-4; assert!((clock.offset().unwrap() - truth).abs() < 0.004, "{:?}", clock.offset());
}
#[test]
fn clock_sync_restarts_after_a_clock_jump() {
let mut clock = ClockSync::new();
for i in 0..50 {
clock.observe(i as f64, i as f64 + 0.05);
}
for i in 0..40 {
clock.observe(i as f64, 5000.0 + i as f64 + 0.05);
}
assert!((clock.offset().unwrap() - 5000.05).abs() < 0.01, "{:?}", clock.offset());
let before = clock.offset().unwrap();
clock.observe(41.0, 5041.0 + 20.0);
assert!((clock.offset().unwrap() - before).abs() < 1e-6);
}
#[test]
fn clock_sync_converts_both_ways() {
let mut clock = ClockSync::new();
assert_eq!(clock.to_local(1.0), None);
clock.observe(10.0, 3.0);
assert_eq!(clock.to_local(12.0), Some(5.0));
assert_eq!(clock.to_remote(5.0), Some(12.0));
clock.observe(f64::NAN, 1.0);
assert_eq!(clock.samples(), 1);
}
fn run_link(loss_every: u32, jitter_ms: u32, reorder: bool) -> (f32, usize, usize) {
let mut stream = SnapshotStream::new();
let mut inflight: Vec<(f64, f64, Vec3)> = Vec::new(); let (mut worst, mut extrapolated, mut held) = (0.0f32, 0, 0);
for frame in 0..600u32 {
let now = frame as f64 / 60.0;
if frame % 3 == 0 {
let sent = 900.0 + now;
let n = frame / 3;
if loss_every == 0 || n % loss_every != 0 {
let jitter = ((n * 7919) % (jitter_ms + 1)) as f64 / 1000.0;
inflight.push((now + 0.03 + jitter, sent, walk(now)));
if reorder && n % 5 == 0 {
inflight.push((now + 0.03 + jitter + 0.02, sent, walk(now))); }
}
}
inflight.sort_by(|a, b| a.0.total_cmp(&b.0));
while inflight.first().is_some_and(|p| p.0 <= now) {
let (_, sent, value) = inflight.remove(0);
stream.push(sent, now, value);
}
if now > 1.0 {
let s = stream.sample(now).unwrap();
let truth = walk(now - 0.1 - 0.03).x;
worst = worst.max((s.value.x - truth).abs());
match s.state {
SnapshotState::Extrapolated => extrapolated += 1,
SnapshotState::Held => held += 1,
SnapshotState::Interpolated => {}
}
}
}
(worst, extrapolated, held)
}
#[test]
fn stream_is_smooth_on_a_clean_link() {
let (worst, extrapolated, held) = run_link(0, 0, false);
assert!(worst < 0.1, "{worst}");
assert_eq!((extrapolated, held), (0, 0));
}
#[test]
fn stream_hides_jitter_within_the_delay() {
let (worst, extrapolated, held) = run_link(0, 60, true);
assert!(worst < 0.5, "{worst}");
assert_eq!(held, 0);
assert!(extrapolated < 60, "{extrapolated}");
}
#[test]
fn stream_rides_out_packet_loss_on_a_straight_walk() {
let (worst, extrapolated, held) = run_link(4, 0, false);
assert!(worst < 0.05, "{worst}");
assert_eq!((extrapolated, held), (0, 0));
}
#[test]
fn stream_extrapolates_then_holds_through_a_blackout() {
let mut stream = SnapshotStream::new();
for i in 0..20 {
let t = i as f64 * 0.05;
stream.push(t, t, walk(t));
}
let states: Vec<_> = [1.0, 1.1, 1.29, 1.5, 3.0]
.iter()
.map(|&now| stream.sample(now).unwrap())
.collect();
assert_eq!(states[0].state, SnapshotState::Interpolated); assert_eq!(states[1].state, SnapshotState::Extrapolated); assert_eq!(states[2].state, SnapshotState::Extrapolated); assert_eq!(states[3].state, SnapshotState::Held);
assert_eq!(states[4].state, SnapshotState::Held);
assert!((states[4].value.x - walk(0.95 + 0.25).x).abs() < 1e-3);
assert!(states[1].value.x > states[0].value.x);
}
#[test]
fn stream_is_empty_until_the_first_snapshot_and_after_clear() {
let mut s = SnapshotStream::<Vec3>::new();
assert!(s.sample(1.0).is_none());
s.push(0.0, 0.0, Vec3::ONE);
assert!(s.sample(1.0).is_some());
s.clear();
assert!(s.sample(1.0).is_none());
s.push(0.0, 0.0, Vec3::ONE);
s.reset();
assert_eq!(s.clock().samples(), 0);
}
#[test]
fn late_duplicate_does_not_disturb_the_clock() {
let mut s = SnapshotStream::new();
s.push(10.0, 0.05, 0.0f32);
let before = s.clock().offset();
assert_eq!(s.push(10.0, 3.0, 0.0f32), SnapshotPush::Duplicate);
assert_eq!(s.clock().offset(), before);
}
#[test]
fn delay_can_be_changed_at_runtime() {
let mut s = SnapshotStream::new();
s.push(0.0, 0.0, 0.0f32);
s.push(1.0, 1.0, 10.0);
assert_eq!(s.sample(0.6).unwrap().value, 5.0);
s.set_delay(0.0);
assert_eq!(s.sample(0.6).unwrap().value, 6.0);
}
#[test]
#[should_panic(expected = "capacity must be at least 2")]
fn tiny_capacity_is_refused() {
let _ = SnapshotBuffer::<f32>::with_config(SnapshotConfig {
capacity: 1,
..Default::default()
});
}
#[test]
#[should_panic(expected = "delay must be a finite number")]
fn negative_delay_is_refused() {
SnapshotStream::<f32>::new().set_delay(-1.0);
}
}