aoer_plotty_rs/turtle/
mod.rs

1use geo_types::{LineString, MultiLineString, Point, Polygon};
2
3/// # Turtle Module
4///
5/// This provides logo-style turtle features, which are useful for things like
6/// lindemeyer systems, etc.
7#[derive(Clone)]
8pub struct Turtle {
9    stack: Vec<Turtle>,
10    lines: Vec<Vec<Point<f64>>>,
11    position: Point<f64>,
12    start: Option<Point<f64>>,
13    heading: f64,
14    pen: bool,
15}
16
17/// Helper function to convert degrees to radians
18pub fn degrees(deg: f64) -> f64 {
19    std::f64::consts::PI * (deg / 180.0)
20}
21
22/// TurtleTrait provides turtle related functions for the Turtle struct.
23///
24/// Provides 2D turtle actions, and a stack-based history for drawing
25/// 2D graphics.
26///
27/// # Example
28///
29/// ```
30/// use geo_types::MultiLineString;
31/// use aoer_plotty_rs::turtle::{Turtle, TurtleTrait, degrees};
32/// let mline_string: MultiLineString<f64> = Turtle::new()
33///     .pen_down()
34///     .fwd(100.0)
35///     .right(degrees(90.0))
36///     .fwd(100.0)
37///     .right(degrees(90.0))
38///     .fwd(100.0)
39///     .right(degrees(90.0))
40///     .fwd(100.0)
41///     .right(degrees(90.0))
42///     .to_multiline();
43/// ```
44
45
46pub trait TurtleTrait {
47    fn new() -> Turtle;
48
49    /// #fwd
50    ///
51    /// Move forward @distance units (negative values are allowed)
52    fn fwd(self, distance: f64) -> Self;
53
54    /// #left
55    ///
56    /// Turn left @angle radians
57    fn left(self, angle: f64) -> Self;
58
59    /// #right
60    ///
61    /// Turn right @angle radians
62    fn right(self, angle: f64) -> Self;
63
64    /// #pen_up
65    ///
66    /// Lift the pen and discard the closing state
67    fn pen_up(self) -> Self;
68
69    /// #pen_down
70    ///
71    /// Put the pen down so that we start drawing, and store the "start position" state
72    /// for later closing the drawing.
73    fn pen_down(self) -> Self;
74
75    /// #close
76    ///
77    /// Automatically close the current line. Great for automagically closing Polygons.
78    fn close(self) -> Self;
79
80    /// #push
81    ///
82    /// Pushes the current state onto the stack, including heading, position.
83    fn push(self) -> Self;
84
85    /// #pop
86    ///
87    /// Pops the state back to the previously [`crate::turtle::TurtleTrait::push`]'d state, but retains the line state
88    /// so that we can backtrack and continue drawing.
89    fn pop(self) -> Self;
90
91    /// # walk_lpath (Walk L-system Path)
92    ///
93    /// Used to take an existing expanded l-system path (see [`crate::l_system::LSystem`]) for more
94    /// information on the expansion syntax. Walks the L-system, performing movements, stack
95    /// push/pop, and turns.
96    fn walk_lpath(self, lpath: &String, angle: f64, distance: f64) -> Self;
97
98    /// # to_multiline
99    ///
100    /// Takes the lines recorded in the Turtle state and returns a [`geo_types::MultiLineString`]
101    fn to_multiline(&mut self) -> MultiLineString<f64>;
102
103    /// # to_polygon
104    ///
105    /// Returns a [`geo_types::Polygon`] from the Turtle state. May return an error for turtles
106    /// which have self-intersecting lines, or zero-volume polygons.
107    fn to_polygon(&mut self) -> Result<Polygon<f64>, geo_types::Error>;
108    // fn to_multipolygon(self) -> Result<MultiPolygon<f64>, geo_types::Error>;
109}
110
111
112impl TurtleTrait for Turtle {
113    fn new() -> Self {
114        Turtle {
115            stack: vec![],
116            lines: vec![],
117            position: Point::new(0.0f64, 0.0f64),
118            start: None,
119            heading: 0.0,
120            pen: false,
121        }
122    }
123
124    fn fwd(mut self, distance: f64) -> Self {
125        let pos = self.position + Point::new(distance * self.heading.cos(),
126                                             distance * self.heading.sin());
127        if self.pen {
128            self.lines.last_mut()
129                .expect("Turtle closing without an active line!")
130                .push(pos)
131        }
132
133        self.position = pos;
134        self
135    }
136
137    fn left(mut self, angle: f64) -> Self {
138        self.heading = self.heading + angle;
139        self
140    }
141
142    fn right(mut self, angle: f64) -> Self {
143        self.heading = self.heading - angle;
144        self
145    }
146
147    fn pen_up(mut self) -> Self {
148        self.pen = false;
149        self.start = None;
150        self
151    }
152
153    fn pen_down(mut self) -> Self {
154        if self.pen { self } else {
155            self.pen = true;
156            self.start = Some(self.position.clone());
157            self.lines.push(vec![self.position.clone()]);
158            self
159        }
160    }
161
162    fn close(mut self) -> Self {
163        match self.start {
164            Some(start) => {
165                if self.pen {
166                    self.lines.last_mut()
167                        .expect("Turtle closing without an active line!")
168                        .push(self.start.expect("Turtle closing without a start point!").clone())
169                }
170                self.position = start.clone();
171                self
172            }
173            None => self
174        }
175    }
176
177    fn push(mut self) -> Self {
178        self.stack.push(self.clone());
179        self
180    }
181
182    fn pop(mut self) -> Self {
183        match self.stack.pop() {
184            Some(t) => Turtle {
185                lines: self.lines,
186                ..t
187            },
188            None => self
189        }
190    }
191
192    fn to_multiline(&mut self) -> MultiLineString<f64> {
193        // MultiLineString::new(vec![])
194        self.lines.iter().map(|line| {
195            LineString::from(line.clone())
196        }).collect()
197    }
198
199    fn to_polygon(&mut self) -> Result<Polygon<f64>, geo_types::Error> {
200        match self.lines.len() {
201            1 => Ok(Polygon::new(LineString::from(self.lines[0].clone()), vec![])),
202            _ => Err(geo_types::Error::MismatchedGeometry {
203                expected: "Single linestring",
204                found: "Multiple or zero linestrings",
205            })
206        }
207    }
208
209    // fn to_multipolygon(self) -> Result<MultiPolygon<f64>, geo_types::Error> {
210    //
211    // }
212    fn walk_lpath(mut self, lpath: &String, angle: f64, distance: f64) -> Self {
213        for c in lpath.chars() {
214            self = match c {
215                '[' => self.push(),
216                ']' => self.pop(),
217                '-' => self.left(angle),
218                '+' => self.right(angle),
219                _ => self.fwd(distance)
220            }
221        }
222        self
223    }
224}
225
226#[cfg(test)]
227mod tests {
228    use std::collections::HashMap;
229
230    use geo_types::Point;
231
232    use crate::geo_types::PointDistance;
233    use crate::l_system::LSystem;
234
235    use super::{degrees, Turtle, TurtleTrait};
236
237    #[test]
238    fn test_walk_lsystem() {
239        let t = Turtle::new().pen_down();
240        let system = LSystem {
241            axiom: "A".to_string(),
242            rules: HashMap::from([
243                ('A', "A-B".to_string()),
244                ('B', "A".to_string())]),
245        };
246        let expanded = system.expand(2);
247        let t = t.walk_lpath(&expanded, degrees(90.0), 10.0);
248        let last_point = t.lines.last().unwrap().last().unwrap();
249        assert!(last_point.x().abs() <= 0.0001f64);
250        assert!((&last_point.y() - 10.0f64).abs() <= 0.0001);
251    }
252
253    #[test]
254    fn test_stack() {
255        let t = Turtle::new();
256        let result = t.push()
257            .fwd(100.0)
258            .right(degrees(90.0))
259            .fwd(100.0)
260            .pop();
261        assert!(result.position == Point::new(0.0f64, 0.0f64));
262    }
263
264    #[test]
265    fn test_pendown() {
266        let t = Turtle::new()
267            .pen_down();
268        assert_eq!(t.pen, true);
269        let t = Turtle::new();
270        assert_eq!(t.pen, false);
271    }
272
273    #[test]
274    fn test_simple_box() {
275        let t = Turtle::new()
276            .pen_down()
277            .fwd(100.0)
278            .right(degrees(90.0))
279            .fwd(100.0)
280            .right(degrees(90.0))
281            .fwd(100.0)
282            .right(degrees(90.0))
283            .close();
284        assert!(t.lines[0][0]
285            .distance(&Point::new(0.0f64, 0.0f64)) < 0.0001f64);
286        assert!(t.lines[0][1]
287            .distance(&Point::new(100.0f64, 0.0f64)) < 0.0001f64);
288        assert!(t.lines[0][2]
289            .distance(&Point::new(100.0f64, -100.0f64)) < 0.0001f64);
290        assert!(t.lines[0][3]
291            .distance(&Point::new(0.0f64, -100.0f64)) < 0.0001f64);
292        assert!(t.lines[0][4]
293            .distance(&Point::new(0.0f64, 0.0f64)) < 0.0001f64);
294    }
295}