Skip to main content

sightloom_core/zone/
polygon.rs

1//! Fixed-capacity polygon membership monitor.
2
3use crate::{CoreError, Point, Polygon, TrackId, VisionEvent, ZoneId};
4
5use super::slots::TrackSlot;
6
7/// Tracks polygon membership transitions for at most `N` tracks.
8pub struct PolygonZoneMonitor<'a, const N: usize> {
9    zone_id: ZoneId,
10    polygon: Polygon<'a>,
11    slots: [TrackSlot<bool>; N],
12}
13
14impl<'a, const N: usize> PolygonZoneMonitor<'a, N> {
15    /// Creates a monitor for `polygon` using caller-selected fixed track capacity.
16    #[must_use]
17    pub const fn new(zone_id: ZoneId, polygon: Polygon<'a>) -> Self {
18        Self {
19            zone_id,
20            polygon,
21            slots: [TrackSlot::Empty; N],
22        }
23    }
24
25    /// Records a point sample and writes its membership event to `output[0]` when needed.
26    ///
27    /// # Errors
28    ///
29    /// Returns [`CoreError::InsufficientCapacity`] when no track slot or required
30    /// event-output slot is available. On error, the monitor state is unchanged.
31    pub fn update(
32        &mut self,
33        track_id: TrackId,
34        point: Point,
35        output: &mut [VisionEvent],
36    ) -> Result<usize, CoreError> {
37        let (existing, free) = TrackSlot::find_slot(&self.slots, track_id);
38        let Some(index) = existing.or(free) else {
39            return Err(CoreError::InsufficientCapacity);
40        };
41        let inside = self.polygon.contains(point);
42        let (next, event) = match self.slots[index] {
43            TrackSlot::Empty => (
44                TrackSlot::Occupied {
45                    track_id,
46                    state: inside,
47                },
48                membership_event(track_id, self.zone_id, false, inside),
49            ),
50            TrackSlot::Occupied {
51                state: previous_inside,
52                ..
53            } => (
54                TrackSlot::Occupied {
55                    track_id,
56                    state: inside,
57                },
58                membership_event(track_id, self.zone_id, previous_inside, inside),
59            ),
60        };
61
62        if event.is_some() && output.is_empty() {
63            return Err(CoreError::InsufficientCapacity);
64        }
65
66        self.slots[index] = next;
67        if let Some(event) = event {
68            output[0] = event;
69            Ok(1)
70        } else {
71            Ok(0)
72        }
73    }
74
75    /// Removes a track's stored membership state without emitting an event.
76    pub fn forget_track(&mut self, track_id: TrackId) -> bool {
77        TrackSlot::forget_track(&mut self.slots, track_id)
78    }
79}
80
81fn membership_event(
82    track_id: TrackId,
83    zone_id: ZoneId,
84    was_inside: bool,
85    is_inside: bool,
86) -> Option<VisionEvent> {
87    match (was_inside, is_inside) {
88        (false, true) => Some(VisionEvent::Entered { track_id, zone_id }),
89        (true, false) => Some(VisionEvent::Exited { track_id, zone_id }),
90        (false, false) | (true, true) => None,
91    }
92}