use std::time::{Duration, Instant};
use elara_core::{PerceptualTime, StateTime};
pub struct PerceptualClock {
value: PerceptualTime,
last_update: Instant,
}
const MAX_PERCEPTUAL_TICK: Duration = Duration::from_millis(100);
impl PerceptualClock {
pub fn new() -> Self {
let now = Instant::now();
PerceptualClock {
value: PerceptualTime::ZERO,
last_update: now,
}
}
pub fn tick(&mut self) -> PerceptualTime {
let now = Instant::now();
let elapsed = now.duration_since(self.last_update);
let clamped = if elapsed > MAX_PERCEPTUAL_TICK {
MAX_PERCEPTUAL_TICK
} else {
elapsed
};
self.value = self.value.saturating_add(clamped);
self.last_update = now;
self.value
}
pub fn now(&self) -> PerceptualTime {
self.value
}
pub fn is_advancing(&self) -> bool {
true
}
}
impl Default for PerceptualClock {
fn default() -> Self {
Self::new()
}
}
pub struct StateClock {
value: StateTime,
rate: f64,
convergence_target: Option<StateTime>,
max_correction_per_tick: Duration,
}
impl StateClock {
pub fn new() -> Self {
StateClock {
value: StateTime::ZERO,
rate: 1.0,
convergence_target: None,
max_correction_per_tick: Duration::from_millis(10),
}
}
pub fn advance(&mut self, dt: Duration) -> StateTime {
let base_advance_us = (dt.as_micros() as f64 * self.rate) as i64;
let correction = if let Some(target) = self.convergence_target {
let error = target.as_micros() - self.value.as_micros();
let max_correction = self.max_correction_per_tick.as_micros() as i64;
let correction = (error as f64 * 0.1) as i64;
correction.clamp(-max_correction, max_correction)
} else {
0
};
self.value = StateTime::from_micros(self.value.as_micros() + base_advance_us + correction);
self.value
}
pub fn now(&self) -> StateTime {
self.value
}
pub fn set_convergence_target(&mut self, target: StateTime) {
self.convergence_target = Some(target);
}
pub fn clear_convergence_target(&mut self) {
self.convergence_target = None;
}
pub fn set_rate(&mut self, rate: f64) {
self.rate = rate.clamp(0.5, 2.0);
}
pub fn rate(&self) -> f64 {
self.rate
}
pub fn sync_to(&mut self, target: StateTime) {
if target > self.value {
self.value = target;
}
}
}
impl Default for StateClock {
fn default() -> Self {
Self::new()
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_perceptual_clock_monotonic() {
let mut clock = PerceptualClock::new();
let t1 = clock.tick();
std::thread::sleep(Duration::from_millis(10));
let t2 = clock.tick();
assert!(t2 > t1);
}
#[test]
fn test_state_clock_advance() {
let mut clock = StateClock::new();
let t1 = clock.now();
clock.advance(Duration::from_millis(100));
let t2 = clock.now();
assert!(t2 > t1);
let diff = t2.as_micros() - t1.as_micros();
assert!((99_000..=101_000).contains(&diff));
}
#[test]
fn test_state_clock_rate() {
let mut clock = StateClock::new();
clock.set_rate(2.0);
clock.advance(Duration::from_millis(100));
let value = clock.now().as_micros();
assert!((190_000..=210_000).contains(&value));
}
#[test]
fn test_state_clock_convergence() {
let mut clock = StateClock::new();
clock.set_convergence_target(StateTime::from_millis(1000));
for _ in 0..100 {
clock.advance(Duration::from_millis(10));
}
let value = clock.now().as_millis();
assert!(value > 1000); }
}