sightloom_core/zone/
line.rs1use 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
16pub 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 #[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 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 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}