1use geometry::span::Span;
4use num::integer::{div_ceil, div_floor};
5use serde::{Deserialize, Serialize};
6
7#[derive(Debug, Copy, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
15pub struct UniformTracks {
16 line: i64,
18 space: i64,
20 offset: i64,
22}
23
24impl UniformTracks {
25 pub fn new(line: i64, space: i64) -> Self {
27 Self::with_offset(line, space, 0)
28 }
29
30 pub fn with_offset(line: i64, space: i64, offset: i64) -> Self {
32 assert_eq!(line & 1, 0, "track width must be even");
33 assert_eq!(space & 1, 0, "track spacing must be even");
34 assert!(line > 0);
35 assert!(space > 0);
36 Self {
37 line,
38 space,
39 offset,
40 }
41 }
42
43 pub fn get(&self, idx: i64) -> Span {
45 let start = self.offset + idx * self.pitch() - self.line / 2;
46 Span::new(start, start + self.line)
47 }
48
49 #[inline]
51 pub fn pitch(&self) -> i64 {
52 self.line + self.space
53 }
54
55 pub fn get_tracks(
57 &self,
58 range: impl Into<std::ops::Range<i64>>,
59 ) -> impl Iterator<Item = Span> + '_ {
60 range.into().map(|i| self.get(i))
61 }
62
63 pub fn enumerate(&self, range: impl Into<std::ops::Range<i64>>) -> EnumeratedTracks {
67 self.get_tracks(range).collect()
68 }
69
70 pub fn to_track_idx(&self, coord: i64, mode: RoundingMode) -> i64 {
72 match mode {
73 RoundingMode::Down => div_floor(coord - self.offset + self.line / 2, self.pitch()),
74 RoundingMode::Up => div_ceil(coord - self.offset - self.line / 2, self.pitch()),
75 RoundingMode::Nearest => div_floor(
76 coord - self.offset + self.pitch() / 2 + self.line / 2,
77 self.pitch(),
78 ),
79 }
80 }
81}
82
83#[derive(Copy, Clone, Eq, PartialEq, Default, Debug, Serialize, Deserialize)]
85pub enum RoundingMode {
86 #[default]
88 Nearest,
89 Down,
91 Up,
93}
94
95#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
97pub struct EnumeratedTracks {
98 tracks: Vec<Span>,
99}
100
101impl EnumeratedTracks {
102 pub fn tracks(&self) -> impl Iterator<Item = Span> + '_ {
104 self.tracks.iter().copied()
105 }
106
107 pub fn new(iter: impl IntoIterator<Item = Span>) -> Self {
113 iter.into_iter().collect()
114 }
115
116 pub fn len(&self) -> usize {
118 self.tracks.len()
119 }
120
121 #[inline]
123 pub fn is_empty(&self) -> bool {
124 self.tracks.is_empty()
125 }
126}
127
128impl FromIterator<Span> for EnumeratedTracks {
129 fn from_iter<T: IntoIterator<Item = Span>>(iter: T) -> Self {
130 let tracks: Vec<Span> = iter.into_iter().collect();
131 for (track, next) in tracks.iter().zip(tracks.iter().skip(1)) {
133 assert!(next.start() > track.stop());
134 }
135 Self { tracks }
136 }
137}
138
139impl IntoIterator for EnumeratedTracks {
140 type Item = Span;
141 type IntoIter = std::vec::IntoIter<Span>;
142 fn into_iter(self) -> Self::IntoIter {
143 self.tracks.into_iter()
144 }
145}
146
147pub trait Tracks {
149 fn try_track(&self, idx: i64) -> Option<Span>;
151
152 fn try_range(&self) -> (Option<i64>, Option<i64>);
156
157 #[inline]
159 fn track(&self, idx: i64) -> Span {
160 self.try_track(idx).expect("track index out of bounds")
161 }
162}
163
164pub trait FiniteTracks {
166 fn range(&self) -> (i64, i64);
171}
172
173impl Tracks for EnumeratedTracks {
174 fn try_track(&self, idx: i64) -> Option<Span> {
175 let idx = usize::try_from(idx).ok()?;
176 self.tracks.get(idx).copied()
177 }
178
179 fn try_range(&self) -> (Option<i64>, Option<i64>) {
180 let range = <Self as FiniteTracks>::range(self);
181 (Some(range.0), Some(range.1))
182 }
183}
184
185impl FiniteTracks for EnumeratedTracks {
186 fn range(&self) -> (i64, i64) {
187 let max = i64::try_from(self.tracks.len()).expect("track list length is too long");
188 (0, max)
189 }
190}
191
192impl Tracks for UniformTracks {
193 fn try_track(&self, idx: i64) -> Option<Span> {
194 Some(self.get(idx))
195 }
196
197 fn try_range(&self) -> (Option<i64>, Option<i64>) {
198 (None, None)
199 }
200}
201
202#[cfg(test)]
203mod tests {
204 use geometry::span::Span;
205
206 use super::*;
207
208 #[test]
209 fn enumerated_tracks() {
210 let tracks: EnumeratedTracks = [Span::new(10, 20), Span::new(30, 40), Span::new(80, 100)]
211 .into_iter()
212 .collect();
213
214 assert_eq!(tracks.track(0), Span::new(10, 20));
215 assert_eq!(tracks.track(1), Span::new(30, 40));
216 assert_eq!(tracks.track(2), Span::new(80, 100));
217 assert_eq!(tracks.range(), (0, 3));
218 assert_eq!(tracks.try_track(-1), None);
219 assert_eq!(tracks.try_track(3), None);
220 }
221
222 #[test]
223 #[should_panic]
224 fn enumerated_tracks_panics_when_tracks_are_out_of_order() {
225 let _: EnumeratedTracks = [Span::new(10, 20), Span::new(15, 30), Span::new(80, 100)]
226 .into_iter()
227 .collect();
228 }
229
230 #[test]
231 fn uniform_tracks() {
232 let tracks = UniformTracks::new(20, 40);
233
234 assert_eq!(tracks.track(-2), Span::new(-130, -110));
235 assert_eq!(tracks.track(-1), Span::new(-70, -50));
236 assert_eq!(tracks.track(0), Span::new(-10, 10));
237 assert_eq!(tracks.track(1), Span::new(50, 70));
238 assert_eq!(tracks.track(2), Span::new(110, 130));
239 assert_eq!(tracks.try_range(), (None, None));
240 }
241
242 #[test]
243 fn uniform_tracks_with_offset() {
244 let tracks = UniformTracks::with_offset(20, 40, 15);
245
246 assert_eq!(tracks.track(-2), Span::new(-115, -95));
247 assert_eq!(tracks.track(-1), Span::new(-55, -35));
248 assert_eq!(tracks.track(0), Span::new(5, 25));
249 assert_eq!(tracks.track(1), Span::new(65, 85));
250 assert_eq!(tracks.track(2), Span::new(125, 145));
251 assert_eq!(tracks.try_range(), (None, None));
252 }
253
254 #[test]
255 #[should_panic]
256 fn uniform_tracks_requires_even_line_width() {
257 UniformTracks::new(5, 40);
258 }
259
260 #[test]
261 #[should_panic]
262 fn uniform_tracks_requires_even_spacing() {
263 UniformTracks::new(320, 645);
264 }
265
266 #[test]
267 fn uniform_tracks_to_track_idx() {
268 let tracks = UniformTracks::with_offset(260, 140, 130);
269 assert_eq!(tracks.to_track_idx(-20, RoundingMode::Down), -1);
270 assert_eq!(tracks.to_track_idx(-550, RoundingMode::Down), -2);
271 assert_eq!(tracks.to_track_idx(-200, RoundingMode::Down), -1);
272 assert_eq!(tracks.to_track_idx(-20, RoundingMode::Up), 0);
273 assert_eq!(tracks.to_track_idx(-530, RoundingMode::Up), -1);
274 assert_eq!(tracks.to_track_idx(-550, RoundingMode::Up), -2);
275 }
276}