Skip to main content

oxigdal_noalloc/
linestring.rs

1//! Fixed-capacity polyline type for `no_std`, `no_alloc` environments.
2//!
3//! [`FixedLineString`] stores up to `N` points inline, representing an
4//! open polyline (sequence of connected line segments).
5
6use crate::{BBox2D, NoAllocError, Point2D, f64_max, f64_min};
7
8/// A fixed-capacity polyline (open line string) backed by an inline array.
9///
10/// Stores up to `N` [`Point2D`] vertices with no heap allocation.
11pub struct FixedLineString<const N: usize> {
12    points: [Point2D; N],
13    len: usize,
14}
15
16impl<const N: usize> FixedLineString<N> {
17    /// Creates an empty `FixedLineString`.
18    #[must_use]
19    #[inline]
20    pub fn new() -> Self {
21        Self {
22            points: [Point2D::new(0.0, 0.0); N],
23            len: 0,
24        }
25    }
26
27    /// Attempts to append a point. Returns `Ok(())` on success or
28    /// `Err(NoAllocError::CapacityExceeded)` if full.
29    #[inline]
30    pub fn push(&mut self, p: Point2D) -> Result<(), NoAllocError> {
31        if self.len >= N {
32            return Err(NoAllocError::CapacityExceeded);
33        }
34        self.points[self.len] = p;
35        self.len += 1;
36        Ok(())
37    }
38
39    /// Returns the number of points in the line string.
40    #[must_use]
41    #[inline]
42    pub fn len(&self) -> usize {
43        self.len
44    }
45
46    /// Returns `true` if the line string contains no points.
47    #[must_use]
48    #[inline]
49    pub fn is_empty(&self) -> bool {
50        self.len == 0
51    }
52
53    /// Returns the point at the given index, or `None` if out of bounds.
54    #[must_use]
55    #[inline]
56    pub fn get(&self, index: usize) -> Option<&Point2D> {
57        if index < self.len {
58            Some(&self.points[index])
59        } else {
60            None
61        }
62    }
63
64    /// Returns a slice of the current points.
65    #[must_use]
66    #[inline]
67    pub fn as_slice(&self) -> &[Point2D] {
68        &self.points[..self.len]
69    }
70
71    /// Computes the total length of the polyline (sum of segment lengths).
72    ///
73    /// Returns `0.0` for line strings with fewer than 2 points.
74    #[must_use]
75    pub fn total_length(&self) -> f64 {
76        if self.len < 2 {
77            return 0.0;
78        }
79        let pts = self.as_slice();
80        let mut total = 0.0_f64;
81        let mut i = 0;
82        while i + 1 < self.len {
83            total += pts[i].distance_to(&pts[i + 1]);
84            i += 1;
85        }
86        total
87    }
88
89    /// Returns the axis-aligned bounding box, or `None` if empty.
90    #[must_use]
91    pub fn bbox(&self) -> Option<BBox2D> {
92        if self.is_empty() {
93            return None;
94        }
95        let pts = self.as_slice();
96        let mut min_x = pts[0].x;
97        let mut min_y = pts[0].y;
98        let mut max_x = pts[0].x;
99        let mut max_y = pts[0].y;
100        let mut i = 1;
101        while i < self.len {
102            min_x = f64_min(min_x, pts[i].x);
103            min_y = f64_min(min_y, pts[i].y);
104            max_x = f64_max(max_x, pts[i].x);
105            max_y = f64_max(max_y, pts[i].y);
106            i += 1;
107        }
108        Some(BBox2D::new(min_x, min_y, max_x, max_y))
109    }
110
111    /// Returns an iterator over the points.
112    #[inline]
113    pub fn iter(&self) -> core::slice::Iter<'_, Point2D> {
114        self.as_slice().iter()
115    }
116
117    /// Returns the capacity of this line string.
118    #[must_use]
119    #[inline]
120    pub const fn capacity(&self) -> usize {
121        N
122    }
123
124    /// Clears all points, resetting the length to zero.
125    #[inline]
126    pub fn clear(&mut self) {
127        self.len = 0;
128    }
129
130    /// Returns the first point, or `None` if empty.
131    #[must_use]
132    #[inline]
133    pub fn first(&self) -> Option<&Point2D> {
134        if self.is_empty() {
135            None
136        } else {
137            Some(&self.points[0])
138        }
139    }
140
141    /// Returns the last point, or `None` if empty.
142    #[must_use]
143    #[inline]
144    pub fn last(&self) -> Option<&Point2D> {
145        if self.is_empty() {
146            None
147        } else {
148            Some(&self.points[self.len - 1])
149        }
150    }
151}
152
153impl<const N: usize> Default for FixedLineString<N> {
154    fn default() -> Self {
155        Self::new()
156    }
157}
158
159#[cfg(test)]
160mod tests {
161    use super::*;
162
163    #[test]
164    fn test_new_empty() {
165        let ls: FixedLineString<10> = FixedLineString::new();
166        assert!(ls.is_empty());
167        assert_eq!(ls.len(), 0);
168        assert_eq!(ls.capacity(), 10);
169    }
170
171    #[test]
172    fn test_push_and_get() {
173        let mut ls: FixedLineString<4> = FixedLineString::new();
174        assert!(ls.push(Point2D::new(1.0, 2.0)).is_ok());
175        assert!(ls.push(Point2D::new(3.0, 4.0)).is_ok());
176        assert_eq!(ls.len(), 2);
177        assert!(!ls.is_empty());
178
179        let p = ls.get(0);
180        assert!(p.is_some());
181        let p = p.expect("tested above");
182        assert!((p.x - 1.0).abs() < f64::EPSILON);
183        assert!((p.y - 2.0).abs() < f64::EPSILON);
184
185        assert!(ls.get(2).is_none());
186    }
187
188    #[test]
189    fn test_capacity_exceeded() {
190        let mut ls: FixedLineString<2> = FixedLineString::new();
191        assert!(ls.push(Point2D::new(0.0, 0.0)).is_ok());
192        assert!(ls.push(Point2D::new(1.0, 1.0)).is_ok());
193        let result = ls.push(Point2D::new(2.0, 2.0));
194        assert_eq!(result, Err(NoAllocError::CapacityExceeded));
195    }
196
197    #[test]
198    fn test_total_length_empty() {
199        let ls: FixedLineString<10> = FixedLineString::new();
200        assert!((ls.total_length() - 0.0).abs() < f64::EPSILON);
201    }
202
203    #[test]
204    fn test_total_length_single_point() {
205        let mut ls: FixedLineString<10> = FixedLineString::new();
206        let _ = ls.push(Point2D::new(5.0, 5.0));
207        assert!((ls.total_length() - 0.0).abs() < f64::EPSILON);
208    }
209
210    #[test]
211    fn test_total_length_two_points() {
212        let mut ls: FixedLineString<10> = FixedLineString::new();
213        let _ = ls.push(Point2D::new(0.0, 0.0));
214        let _ = ls.push(Point2D::new(3.0, 4.0));
215        assert!((ls.total_length() - 5.0).abs() < 1e-10);
216    }
217
218    #[test]
219    fn test_total_length_three_points() {
220        let mut ls: FixedLineString<10> = FixedLineString::new();
221        let _ = ls.push(Point2D::new(0.0, 0.0));
222        let _ = ls.push(Point2D::new(3.0, 0.0));
223        let _ = ls.push(Point2D::new(3.0, 4.0));
224        assert!((ls.total_length() - 7.0).abs() < 1e-10);
225    }
226
227    #[test]
228    fn test_bbox_empty() {
229        let ls: FixedLineString<10> = FixedLineString::new();
230        assert!(ls.bbox().is_none());
231    }
232
233    #[test]
234    fn test_bbox_single_point() {
235        let mut ls: FixedLineString<10> = FixedLineString::new();
236        let _ = ls.push(Point2D::new(5.0, 10.0));
237        let bb = ls.bbox();
238        assert!(bb.is_some());
239        let bb = bb.expect("tested above");
240        assert!((bb.min_x - 5.0).abs() < f64::EPSILON);
241        assert!((bb.max_x - 5.0).abs() < f64::EPSILON);
242    }
243
244    #[test]
245    fn test_bbox_multiple_points() {
246        let mut ls: FixedLineString<10> = FixedLineString::new();
247        let _ = ls.push(Point2D::new(1.0, 5.0));
248        let _ = ls.push(Point2D::new(3.0, 2.0));
249        let _ = ls.push(Point2D::new(-1.0, 8.0));
250        let bb = ls.bbox().expect("non-empty line string");
251        assert!((bb.min_x - (-1.0)).abs() < f64::EPSILON);
252        assert!((bb.min_y - 2.0).abs() < f64::EPSILON);
253        assert!((bb.max_x - 3.0).abs() < f64::EPSILON);
254        assert!((bb.max_y - 8.0).abs() < f64::EPSILON);
255    }
256
257    #[test]
258    fn test_iter() {
259        let mut ls: FixedLineString<10> = FixedLineString::new();
260        let _ = ls.push(Point2D::new(1.0, 2.0));
261        let _ = ls.push(Point2D::new(3.0, 4.0));
262        let collected: &[Point2D] = ls.as_slice();
263        assert_eq!(collected.len(), 2);
264    }
265
266    #[test]
267    fn test_first_last() {
268        let mut ls: FixedLineString<10> = FixedLineString::new();
269        assert!(ls.first().is_none());
270        assert!(ls.last().is_none());
271
272        let _ = ls.push(Point2D::new(1.0, 2.0));
273        let _ = ls.push(Point2D::new(5.0, 6.0));
274        let first = ls.first().expect("non-empty");
275        let last = ls.last().expect("non-empty");
276        assert!((first.x - 1.0).abs() < f64::EPSILON);
277        assert!((last.x - 5.0).abs() < f64::EPSILON);
278    }
279
280    #[test]
281    fn test_clear() {
282        let mut ls: FixedLineString<10> = FixedLineString::new();
283        let _ = ls.push(Point2D::new(1.0, 2.0));
284        ls.clear();
285        assert!(ls.is_empty());
286        assert_eq!(ls.len(), 0);
287    }
288
289    #[test]
290    fn test_default() {
291        let ls: FixedLineString<5> = FixedLineString::default();
292        assert!(ls.is_empty());
293    }
294}