veloren_common/
spiral.rs

1use vek::*;
2
3/// An iterator of coordinates that create a rectangular spiral out from the
4/// origin
5#[derive(Clone)]
6pub struct Spiral2d {
7    layer: i32,
8    i: i32,
9}
10
11impl Spiral2d {
12    #[expect(clippy::new_without_default)]
13    /// Creates a new spiral starting at the origin
14    pub fn new() -> Self { Self { layer: 0, i: 0 } }
15
16    /// Creates an iterator over points in a spiral starting at the origin and
17    /// going out to some radius
18    pub fn with_radius(radius: i32) -> impl Iterator<Item = Vec2<i32>> {
19        Self::new()
20            .take((radius * 2 + 1).pow(2) as usize)
21            .filter(move |pos| pos.magnitude_squared() < (radius + 1).pow(2))
22    }
23
24    /// Creates an iterator over points in the edge of a circle of some radius
25    pub fn with_edge_radius(radius: i32) -> impl Iterator<Item = Vec2<i32>> {
26        Self::new()
27            .take((radius * 2 + 1).pow(2) as usize)
28            .filter(move |pos| pos.magnitude_squared() < (radius + 1).pow(2))
29            .filter(move |pos| pos.magnitude_squared() >= radius.pow(2))
30    }
31
32    /// Creates an iterator over points in the margin between two squares,
33    /// inclusive of the inner_radius and exclusive of the outer_radius
34    /// where outer_radius = inner_radius + margin
35    /**
36        Spiral2d iterates over the points in a square spiral pattern starting at the bottom left.
37        In the ring spiral, the iteration starts at the bottom left of the inner square and
38        does not include the outer square (if you think of the outer square as inner_radius + margin).
39        +-----------------------+
40        |        Margin         |
41        |     +-----------+     |
42        |     |           |     |
43        |     |    Not    |     |
44        |     | Included  |     |
45        |     |           |     |
46        |     +-----------+     |
47        |                       |
48        +-----------------------+
49        For example, Spiral2d::with_ring(1, 2) yields the following output:
50            Vec2 { x: -1, y: -1 }
51            Vec2 { x: 0, y: -1 }
52            Vec2 { x: 1, y: -1 }
53            Vec2 { x: 1, y: 0 }
54            Vec2 { x: 1, y: 1 }
55            Vec2 { x: 0, y: 1 }
56            Vec2 { x: -1, y: 1 }
57            Vec2 { x: -1, y: 0 }
58            Vec2 { x: -2, y: -2 }
59            Vec2 { x: -1, y: -2 }
60            Vec2 { x: 0, y: -2 }
61            Vec2 { x: 1, y: -2 }
62            Vec2 { x: 2, y: -2 }
63            Vec2 { x: 2, y: -1 }
64            Vec2 { x: 2, y: 0 }
65            Vec2 { x: 2, y: 1 }
66            Vec2 { x: 2, y: 2 }
67            Vec2 { x: 1, y: 2 }
68            Vec2 { x: 0, y: 2 }
69            Vec2 { x: -1, y: 2 }
70            Vec2 { x: -2, y: 2 }
71            Vec2 { x: -2, y: 1 }
72            Vec2 { x: -2, y: 0 }
73            Vec2 { x: -2, y: -1 }
74        Run the first test below to see this output.
75    **/
76    pub fn with_ring(inner_radius: u32, margin: u32) -> impl Iterator<Item = Vec2<i32>> {
77        let outer_radius: u32 = inner_radius + margin - 1;
78        let adjusted_inner_radius = if inner_radius > 0 {
79            inner_radius - 1
80        } else {
81            0
82        };
83        Spiral2d {
84            layer: inner_radius as i32,
85            i: 0,
86        }
87        .take(
88            (outer_radius * 2 + 1).pow(2) as usize
89                - (adjusted_inner_radius * 2 + 1).pow(2) as usize,
90        )
91    }
92}
93
94impl Iterator for Spiral2d {
95    type Item = Vec2<i32>;
96
97    #[expect(clippy::erasing_op, clippy::identity_op)]
98    fn next(&mut self) -> Option<Self::Item> {
99        let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1);
100        if self.i >= layer_size {
101            self.layer += 1;
102            self.i = 0;
103        }
104        let layer_size = (self.layer * 8 + 4 * self.layer.min(1) - 4).max(1);
105
106        let pos = Vec2::new(
107            -self.layer + (self.i - (layer_size / 4) * 0).clamp(0, self.layer * 2)
108                - (self.i - (layer_size / 4) * 2).clamp(0, self.layer * 2),
109            -self.layer + (self.i - (layer_size / 4) * 1).clamp(0, self.layer * 2)
110                - (self.i - (layer_size / 4) * 3).clamp(0, self.layer * 2),
111        );
112
113        self.i += 1;
114
115        Some(pos)
116    }
117}
118
119#[cfg(test)]
120mod tests {
121    use super::*;
122
123    #[test]
124    fn print_spiral_ring() {
125        let spiral = Spiral2d::with_ring(1, 2);
126        for pos in spiral {
127            println!("{:?}", pos);
128        }
129    }
130
131    #[test]
132    fn empty_spiral_ring() {
133        assert_eq!(Spiral2d::with_ring(0, 1).count(), 0);
134        assert_eq!(Spiral2d::with_ring(0, 2).count(), 8);
135    }
136
137    #[test]
138    fn minimum_spiral_ring() {
139        let min_spiral_ring: Vec<Vec2<i32>> = vec![
140            Vec2::new(-1, -1),
141            Vec2::new(0, -1),
142            Vec2::new(1, -1),
143            Vec2::new(1, 0),
144            Vec2::new(1, 1),
145            Vec2::new(0, 1),
146            Vec2::new(-1, 1),
147            Vec2::new(-1, 0),
148        ];
149        let result: Vec<Vec2<i32>> = Spiral2d::with_ring(1, 1).collect();
150        assert_eq!(result, min_spiral_ring);
151    }
152}