Skip to main content

sightloom_core/zone/
line.rs

1//! Fixed-capacity finite-line crossing monitor.
2
3use crate::{
4    CoreError, Direction, LineSegment, LineSide, Point, TrackId, VisionEvent, ZoneId,
5    crosses_segment, line_side,
6};
7
8use super::slots::TrackSlot;
9
10#[derive(Clone, Copy)]
11struct LineTrackState {
12    previous: Point,
13    last_non_on: Option<LineSide>,
14}
15
16/// Tracks crossings of a finite directed line segment for at most `N` tracks.
17pub struct LineZoneMonitor<const N: usize> {
18    zone_id: ZoneId,
19    segment: LineSegment,
20    slots: [TrackSlot<LineTrackState>; N],
21}
22
23impl<const N: usize> LineZoneMonitor<N> {
24    /// Creates a monitor for `segment` using caller-selected fixed track capacity.
25    #[must_use]
26    pub const fn new(zone_id: ZoneId, segment: LineSegment) -> Self {
27        Self {
28            zone_id,
29            segment,
30            slots: [TrackSlot::Empty; N],
31        }
32    }
33
34    /// Records a point sample and writes its crossing event to `output[0]` when needed.
35    ///
36    /// # Errors
37    ///
38    /// Returns [`CoreError::InsufficientCapacity`] when no track slot or required
39    /// event-output slot is available. On error, the monitor state is unchanged.
40    pub fn update(
41        &mut self,
42        track_id: TrackId,
43        point: Point,
44        output: &mut [VisionEvent],
45    ) -> Result<usize, CoreError> {
46        let (existing, free) = TrackSlot::find_slot(&self.slots, track_id);
47        let Some(index) = existing.or(free) else {
48            return Err(CoreError::InsufficientCapacity);
49        };
50        let side = line_side(self.segment, point);
51        let (next, event) = match self.slots[index] {
52            TrackSlot::Empty => (
53                TrackSlot::Occupied {
54                    track_id,
55                    state: LineTrackState {
56                        previous: point,
57                        last_non_on: non_on_side(side),
58                    },
59                },
60                None,
61            ),
62            TrackSlot::Occupied {
63                state:
64                    LineTrackState {
65                        previous,
66                        last_non_on,
67                    },
68                ..
69            } => {
70                let event = crossing_event(
71                    self.zone_id,
72                    self.segment,
73                    track_id,
74                    previous,
75                    last_non_on,
76                    point,
77                    side,
78                );
79                (
80                    TrackSlot::Occupied {
81                        track_id,
82                        state: LineTrackState {
83                            previous: point,
84                            last_non_on: non_on_side(side).or(last_non_on),
85                        },
86                    },
87                    event,
88                )
89            }
90        };
91
92        if event.is_some() && output.is_empty() {
93            return Err(CoreError::InsufficientCapacity);
94        }
95
96        self.slots[index] = next;
97        if let Some(event) = event {
98            output[0] = event;
99            Ok(1)
100        } else {
101            Ok(0)
102        }
103    }
104
105    /// Removes a track's stored crossing state without emitting an event.
106    pub fn forget_track(&mut self, track_id: TrackId) -> bool {
107        TrackSlot::forget_track(&mut self.slots, track_id)
108    }
109}
110
111fn non_on_side(side: LineSide) -> Option<LineSide> {
112    (side != LineSide::On).then_some(side)
113}
114
115#[allow(clippy::too_many_arguments)]
116fn crossing_event(
117    zone_id: ZoneId,
118    segment: LineSegment,
119    track_id: TrackId,
120    previous: Point,
121    last_non_on: Option<LineSide>,
122    point: Point,
123    side: LineSide,
124) -> Option<VisionEvent> {
125    let previous_side = last_non_on?;
126    if side == LineSide::On || side == previous_side || previous == point {
127        return None;
128    }
129
130    let motion = LineSegment::new(previous, point).ok()?;
131    if !crosses_segment(segment, motion) {
132        return None;
133    }
134
135    let direction = match (previous_side, side) {
136        (LineSide::Left, LineSide::Right) => Direction::LeftToRight,
137        (LineSide::Right, LineSide::Left) => Direction::RightToLeft,
138        _ => return None,
139    };
140    Some(VisionEvent::Crossed {
141        track_id,
142        zone_id,
143        direction,
144    })
145}