aoer_plotty_rs/turtle/
mod.rs1use geo_types::{LineString, MultiLineString, Point, Polygon};
2
3#[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
17pub fn degrees(deg: f64) -> f64 {
19 std::f64::consts::PI * (deg / 180.0)
20}
21
22pub trait TurtleTrait {
47 fn new() -> Turtle;
48
49 fn fwd(self, distance: f64) -> Self;
53
54 fn left(self, angle: f64) -> Self;
58
59 fn right(self, angle: f64) -> Self;
63
64 fn pen_up(self) -> Self;
68
69 fn pen_down(self) -> Self;
74
75 fn close(self) -> Self;
79
80 fn push(self) -> Self;
84
85 fn pop(self) -> Self;
90
91 fn walk_lpath(self, lpath: &String, angle: f64, distance: f64) -> Self;
97
98 fn to_multiline(&mut self) -> MultiLineString<f64>;
102
103 fn to_polygon(&mut self) -> Result<Polygon<f64>, geo_types::Error>;
108 }
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 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 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}