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
61            && self.input_seq.wrapping_sub(self.cursor_seq) <= Self::STALE_AFTER
62        {
63            return self.cursor_px;
64        }
65        None
66    }
67
68    /// Last-resort fallback: the baked hover world point (touch/pen
69    /// never stage any px).
70    pub fn baked(&self) -> Option<Vec2> {
71        self.hover
72    }
73}
74
75#[cfg(test)]
76mod tests {
77    use super::*;
78
79    #[test]
80    fn fresh_px_wins_stale_px_yields_none() {
81        let mut t = AimTracker::default();
82        assert_eq!(t.live_px(), None);
83        t.stage_hover(Vec2::new(10.0, 10.0), [0.0, 0.0]);
84        t.cursor_move(Vec2::new(100.0, 100.0));
85        assert_eq!(t.live_px(), Some(Vec2::new(0.0, 0.0)));
86        for _ in 0..40 {
87            t.stage_hover(Vec2::new(10.0, 10.0), [50.0, 50.0]);
88        }
89        assert_eq!(t.live_px(), Some(Vec2::new(50.0, 50.0)));
90        assert_eq!(t.baked(), Some(Vec2::new(10.0, 10.0)));
91    }
92}