Skip to main content

repame_input/
aim.rs

1//! Screen-anchored pointer staging: raw window-physical px in, live
2//! world point out.
3
4use glam::Vec2;
5
6/// Staged pointer click: world position (aim-at-click) plus canvas-dp
7/// position (camera-independent menu hit-testing).
8#[derive(Clone, Copy, Debug)]
9pub struct StagedClick {
10    pub world: Vec2,
11    pub dp: [f32; 2],
12}
13
14/// Raw window-physical px from two sources sharing one order clock:
15/// the root `on_pointer_move` (fires almost exclusively while a button
16/// is held.
17#[derive(Clone, Copy, Debug, Default)]
18pub struct AimTracker {
19    hover: Option<Vec2>,
20    hover_px: Option<Vec2>,
21    cursor_px: Option<Vec2>,
22    cursor_seq: u64,
23    hover_seq: u64,
24    hover_px_seq: u64,
25    input_seq: u64,
26}
27
28impl AimTracker {
29    /// Frames a staged px stays live without a refresh.
30    pub const STALE_AFTER: u64 = 30;
31
32    /// Stage the cursor's window-physical px position (root
33    /// `on_pointer_move`, y-down). Stored raw — unprojected through
34    /// the live camera each frame by the caller.
35    pub fn cursor_move(&mut self, phys_px: Vec2) {
36        self.input_seq += 1;
37        self.cursor_seq = self.input_seq;
38        self.cursor_px = Some(phys_px);
39    }
40
41    /// Stage one viewport hover: the baked world point AND the raw
42    /// window-physical `screen` px.
43    pub fn stage_hover(&mut self, world: Vec2, screen: [f32; 2]) {
44        self.input_seq += 1;
45        self.hover_seq = self.input_seq;
46        self.hover = Some(world);
47        self.hover_px_seq = self.input_seq;
48        self.hover_px = Some(Vec2::new(screen[0], screen[1]));
49    }
50
51    /// Fresh staged px in priority order (hover px, root px), or `None`
52    /// when every source is stale/absent and the caller must fall back
53    /// to the baked hover point.
54    pub fn live_px(&self) -> Option<Vec2> {
55        if self.hover_px_seq > 0
56            && self.input_seq.wrapping_sub(self.hover_px_seq) <= Self::STALE_AFTER
57        {
58            return self.hover_px;
59        }
60        if self.cursor_seq > 0 && self.input_seq.wrapping_sub(self.cursor_seq) <= Self::STALE_AFTER
61        {
62            return self.cursor_px;
63        }
64        None
65    }
66
67    /// Last-resort fallback: the baked hover world point (touch/pen
68    /// never stage any px).
69    pub fn baked(&self) -> Option<Vec2> {
70        self.hover
71    }
72}
73
74#[cfg(test)]
75mod tests {
76    use super::*;
77
78    #[test]
79    fn fresh_px_wins_stale_px_yields_none() {
80        let mut t = AimTracker::default();
81        assert_eq!(t.live_px(), None);
82        t.stage_hover(Vec2::new(10.0, 10.0), [0.0, 0.0]);
83        t.cursor_move(Vec2::new(100.0, 100.0));
84        assert_eq!(t.live_px(), Some(Vec2::new(0.0, 0.0)));
85        for _ in 0..40 {
86            t.stage_hover(Vec2::new(10.0, 10.0), [50.0, 50.0]);
87        }
88        assert_eq!(t.live_px(), Some(Vec2::new(50.0, 50.0)));
89        assert_eq!(t.baked(), Some(Vec2::new(10.0, 10.0)));
90    }
91}