use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Copy, Clone, Pod, Zeroable)]
pub struct TrailVertex {
pub position: [f32; 3],
pub color: [f32; 4],
}
pub struct TrailBuffer {
pub vertices: Vec<TrailVertex>,
pub head: usize,
pub count: usize,
pub capacity: usize,
pub color: [f32; 4],
}
impl TrailBuffer {
pub fn new(capacity: usize, color: [f32; 4]) -> Self {
let capacity = capacity.max(1);
Self {
vertices: vec![
TrailVertex {
position: [0.0; 3],
color: [0.0; 4]
};
capacity
],
head: 0,
count: 0,
capacity,
color,
}
}
pub fn push(&mut self, pos: [f32; 3]) {
self.vertices[self.head] = TrailVertex {
position: pos,
color: self.color,
};
self.head = (self.head + 1) % self.capacity;
if self.count < self.capacity {
self.count += 1;
}
}
pub fn clear(&mut self) {
self.count = 0;
self.head = 0;
}
pub fn ordered_vertices(&self) -> Vec<TrailVertex> {
let mut out = Vec::with_capacity(self.count);
for i in 0..self.count {
let age_frac = i as f32 / self.count.max(1) as f32; let alpha = age_frac * age_frac; let idx = if self.count == self.capacity {
(self.head + i) % self.capacity
} else {
i
};
let v = self.vertices[idx];
out.push(TrailVertex {
position: v.position,
color: [v.color[0], v.color[1], v.color[2], alpha],
});
}
out
}
}
#[cfg(test)]
mod tests {
use super::*;
fn red() -> [f32; 4] {
[1.0, 0.0, 0.0, 1.0]
}
#[test]
fn new_buffer_is_empty() {
let buf = TrailBuffer::new(10, red());
assert_eq!(buf.count, 0);
assert_eq!(buf.ordered_vertices().len(), 0);
}
#[test]
fn count_saturates_at_capacity() {
let cap = 5;
let mut buf = TrailBuffer::new(cap, red());
for i in 0..20 {
buf.push([i as f32, 0.0, 0.0]);
}
assert_eq!(buf.count, cap);
assert_eq!(buf.ordered_vertices().len(), cap);
}
#[test]
fn clear_resets_buffer() {
let mut buf = TrailBuffer::new(8, red());
for i in 0..8 {
buf.push([i as f32, 0.0, 0.0]);
}
buf.clear();
assert_eq!(buf.count, 0);
assert_eq!(buf.ordered_vertices().len(), 0);
}
#[test]
fn fade_increases_from_tail_to_head() {
let cap = 10;
let mut buf = TrailBuffer::new(cap, red());
for i in 0..cap {
buf.push([i as f32, 0.0, 0.0]);
}
let verts = buf.ordered_vertices();
let first_alpha = verts[0].color[3];
let last_alpha = verts[cap - 1].color[3];
assert!(
first_alpha < last_alpha,
"expected first_alpha < last_alpha, got {first_alpha} vs {last_alpha}"
);
}
#[test]
fn ring_wrap_preserves_order() {
let cap = 4;
let mut buf = TrailBuffer::new(cap, red());
for i in 0..6u32 {
buf.push([i as f32, 0.0, 0.0]);
}
let verts = buf.ordered_vertices();
assert_eq!(verts.len(), cap);
let positions: Vec<f32> = verts.iter().map(|v| v.position[0]).collect();
assert_eq!(
positions,
vec![2.0, 3.0, 4.0, 5.0],
"ring wrap order incorrect: {positions:?}"
);
}
#[test]
fn zero_capacity_is_clamped_to_one() {
let buf = TrailBuffer::new(0, red());
assert_eq!(buf.capacity, 1);
}
}