use egui::Pos2;
pub fn stable_snap(p: Pos2, subpixel: f32) -> Pos2 {
let q = if subpixel <= 0.0 { 1.0 } else { 1.0 / subpixel };
Pos2::new((p.x * q).round() / q, (p.y * q).round() / q)
}
pub fn smooth_anchor(prev: Pos2, target: Pos2, alpha: f32) -> Pos2 {
let a = alpha.clamp(0.0, 1.0);
Pos2::new(prev.x + (target.x - prev.x) * a, prev.y + (target.y - prev.y) * a)
}
pub fn billboard_placement(projected: Pos2, prev: Option<Pos2>, smoothing: f32, subpixel: f32) -> Pos2 {
let smoothed = match prev {
Some(p) => smooth_anchor(p, projected, smoothing),
None => projected,
};
stable_snap(smoothed, subpixel)
}
#[cfg(test)]
mod tests {
use egui::pos2;
use super::*;
#[test]
fn stable_snap_quantises_to_subpixel_grid() {
let a = stable_snap(pos2(100.04, 50.02), 0.5);
let b = stable_snap(pos2(100.06, 49.98), 0.5);
assert_eq!(a, b, "sub-half-pixel wobble snaps to the same position (no jitter)");
assert_eq!(a, pos2(100.0, 50.0));
}
#[test]
fn smoothing_follows_but_damps_a_jump() {
let prev = pos2(0.0, 0.0);
let target = pos2(10.0, 0.0);
let mid = smooth_anchor(prev, target, 0.5);
assert_eq!(mid, pos2(5.0, 0.0), "half-way toward target at alpha 0.5");
assert_eq!(smooth_anchor(prev, target, 1.0), target);
assert_eq!(smooth_anchor(prev, target, 0.0), prev);
}
#[test]
fn rotating_anchor_does_not_bob_after_stabilization() {
let mut prev = pos2(200.0, 100.0);
let mut ys = Vec::new();
for i in 0..20 {
let wobble = ((i as f32) * 0.7).sin() * 0.3;
let projected = pos2(200.0, 100.0 + wobble);
let placed = billboard_placement(projected, Some(prev), 0.6, 0.5);
prev = placed;
ys.push(placed.y);
}
assert!(ys.iter().all(|&y| (y - ys[0]).abs() < 1e-6), "stabilized label must not bob: {ys:?}");
assert_eq!(ys[0], 100.0);
}
#[test]
fn placement_with_no_prior_is_just_the_snapped_projection() {
let placed = billboard_placement(pos2(50.4, 25.6), None, 0.6, 0.5);
assert_eq!(placed, stable_snap(pos2(50.4, 25.6), 0.5));
}
}