turtle_graphics/
lib.rs

1use std::f32::consts::PI;
2use std::io::{self, Write};
3use std::ops::{Add, Neg};
4
5#[derive(Copy, Clone, Debug)]
6pub struct Position(f32, f32);
7
8impl Position {
9    pub fn origin() -> Position {
10        Position(0.0, 0.0)
11    }
12
13    pub fn min(&self, other: &Position) -> Position {
14        Position(self.0.min(other.0), self.1.min(other.1))
15    }
16
17    pub fn max(&self, other: &Position) -> Position {
18        Position(self.0.max(other.0), self.1.max(other.1))
19    }
20
21    pub fn min_max(&self, min_max: &(Position, Position)) -> (Position, Position) {
22        (self.min(&min_max.0), self.max(&min_max.1))
23    }
24}
25
26struct Bounds {
27    min_max: Option<(Position, Position)>,
28}
29
30impl Bounds {
31    fn new() -> Bounds {
32        Bounds { min_max: None }
33    }
34
35    fn add_position(&mut self, pos: Position) {
36        let mm = match self.min_max {
37            None => (pos, pos),
38            Some(ref a) => pos.min_max(a),
39        };
40
41        self.min_max = Some(mm);
42    }
43
44    #[allow(dead_code)]
45    fn is_bounded(&self) -> bool {
46        self.min_max.is_some()
47    }
48
49    fn width(&self) -> f32 {
50        let (min, max) = self.min_max.unwrap();
51        (max.0 - min.0).abs()
52    }
53
54    fn height(&self) -> f32 {
55        let (min, max) = self.min_max.unwrap();
56        (max.1 - min.1).abs()
57    }
58
59    fn min_x(&self) -> f32 {
60        let (min, _) = self.min_max.unwrap();
61        min.0
62    }
63    fn min_y(&self) -> f32 {
64        let (min, _) = self.min_max.unwrap();
65        min.1
66    }
67
68    fn max_x(&self) -> f32 {
69        let (_, max) = self.min_max.unwrap();
70        max.0
71    }
72    fn max_y(&self) -> f32 {
73        let (_, max) = self.min_max.unwrap();
74        max.1
75    }
76}
77
78impl Add<Position> for Position {
79    type Output = Position;
80    fn add(self, other: Position) -> Self::Output {
81        Position(self.0 + other.0, self.1 + other.1)
82    }
83}
84
85#[derive(Copy, Clone, Debug)]
86pub struct Degree(pub f32);
87
88#[derive(Copy, Clone, Debug)]
89pub struct Radiant(pub f32);
90
91impl Into<Degree> for Radiant {
92    fn into(self) -> Degree {
93        Degree(self.0 * 180.0 / PI)
94    }
95}
96
97impl Into<Degree> for f32 {
98    fn into(self) -> Degree {
99        Degree(self)
100    }
101}
102
103impl Into<Radiant> for Degree {
104    fn into(self) -> Radiant {
105        Radiant(self.0 * PI / 180.0)
106    }
107}
108
109#[derive(Copy, Clone, Debug)]
110pub struct Distance(f32);
111
112impl Into<Distance> for f32 {
113    fn into(self) -> Distance {
114        Distance(self)
115    }
116}
117
118impl Neg for Distance {
119    type Output = Distance;
120    fn neg(self) -> Self::Output {
121        Distance(-self.0)
122    }
123}
124
125impl Neg for Degree {
126    type Output = Degree;
127    fn neg(self) -> Self::Output {
128        Degree(-self.0)
129    }
130}
131
132pub trait Turtle {
133    /// Move turtle forward by specified `distance`.
134    fn forward<T: Into<Distance>>(&mut self, distance: T);
135
136    /// Move turtle backward by specified `distance`.
137    fn backward<T: Into<Distance>>(&mut self, distance: T) {
138        self.forward(-distance.into())
139    }
140
141    /// Move turtle forward by specified `distance` *without* drawing.
142    fn move_forward<T: Into<Distance>>(&mut self, distance: T);
143
144    /// Rotate around `angle`. If `angle` is positive,
145    /// the turtle is turned to the left, if negative,
146    /// to the right.
147    fn rotate<T: Into<Degree>>(&mut self, angle: T);
148
149    /// Turn turtle right by `angle` degree.
150    fn right<T: Into<Degree>>(&mut self, angle: T) {
151        self.rotate(-angle.into());
152    }
153
154    /// Turn turtle left by `angle` degree.
155    fn left<T: Into<Degree>>(&mut self, angle: T) {
156        self.rotate(angle.into());
157    }
158
159    /// Returns `true` if pen is down.
160    fn is_pen_down(&self) -> bool;
161
162    /// Returns `true` if pen is up.
163    fn is_pen_up(&self) -> bool {
164        !self.is_pen_down()
165    }
166
167    /// Put the pen down.
168    fn pen_down(&mut self);
169
170    /// Put the pen up.
171    fn pen_up(&mut self);
172
173    fn goto(&mut self, pos: Position);
174
175    fn home(&mut self) {
176        self.goto(Position::origin());
177    }
178
179    /// Push current turtle state on stack.
180    fn push(&mut self);
181
182    /// Restore previously saved turtle state.
183    fn pop(&mut self);
184}
185
186#[derive(Clone)]
187struct TurtleState {
188    pos: Position,
189    angle: Degree,
190    pendown: bool,
191}
192
193pub struct Canvas {
194    states: Vec<TurtleState>,
195    paths: Vec<Vec<Position>>,
196}
197
198impl Canvas {
199    pub fn new() -> Canvas {
200        let init_pos = Position::origin();
201        let init_state = TurtleState {
202            pos: init_pos,
203            // The coordinate system we use: x from left to right. y from bottom to top.
204            angle: Degree(0.0), // points upwards
205            pendown: true,      // start with pen down
206        };
207        Canvas {
208            states: vec![init_state],
209            paths: vec![vec![init_pos]],
210        }
211    }
212
213    #[inline]
214    fn current_state_mut(&mut self) -> &mut TurtleState {
215        self.states.last_mut().unwrap()
216    }
217
218    #[inline]
219    fn current_state(&self) -> &TurtleState {
220        self.states.last().unwrap()
221    }
222
223    #[inline]
224    fn direction(&self, distance: Distance) -> (f32, f32) {
225        let state = self.current_state();
226        let rad: Radiant = state.angle.into();
227        let (sin, cos) = rad.0.sin_cos();
228        let dx = -sin * distance.0;
229        let dy = cos * distance.0;
230        (dx, dy)
231    }
232
233    fn line_to(&mut self, dst: Position) {
234        self.paths.last_mut().unwrap().push(dst);
235    }
236
237    fn move_to(&mut self, dst: Position) {
238        if self.paths.is_empty() {
239            self.paths.push(vec![dst]);
240        } else {
241            let begin_new_path = self.paths.last().unwrap().len() > 1;
242            if begin_new_path {
243                self.paths.push(vec![dst]);
244            } else {
245                // Replace first path element with current position
246                self.paths.last_mut().unwrap()[0] = dst;
247            }
248        }
249    }
250
251    fn foreach_position<F: FnMut(Position)>(&self, mut f: F, scale_x: f32, scale_y: f32) {
252        for path in self.paths.iter() {
253            for pos in path.iter() {
254                f(Position(pos.0 * scale_x, pos.1 * scale_y));
255            }
256        }
257    }
258
259    /// Saves the turtle graphic as Embedded Postscript (EPS)
260    pub fn save_eps<W: Write>(&self, wr: &mut W) -> io::Result<()> {
261        // Determine extend of canvas
262        let mut bounds = Bounds::new();
263
264        // The EPS coordinates are from bottom to top, like turtle coordinates.
265        self.foreach_position(|pos| bounds.add_position(pos), 1.0, 1.0);
266
267        let (min_width, min_height) = (100.0, 100.0);
268        let width = bounds.width().max(min_width);
269        let height = bounds.height().max(min_height);
270        let border_percent = 0.1;
271
272        let scale = 1.0 + 2.0 * border_percent;
273
274        writeln!(
275            wr,
276            r#"%%!PS-Adobe-3.0 EPSF-3.0
277%%Creator: https://github.com/mneumann/turtle-graphics-rs
278%%DocumentData: Clean7Bit
279%%Origin: 0 0
280%%BoundingBox: {} {} {} {}
281%%LanguageLevel: 2
282%%Pages: 1
283%%Page: 1 1
284"#,
285            bounds.min_x() - border_percent * width,
286            bounds.min_y() - border_percent * height,
287            bounds.max_x() + border_percent * width,
288            bounds.max_y() + border_percent * height
289        )?;
290
291        // use a stroke width of 0.1% of the width or height of the canvas
292        let stroke_width = scale * width.max(height) / 1000.0;
293        writeln!(wr, r#"{} setlinewidth"#, stroke_width)?;
294
295        for path in self.paths.iter() {
296            if let Some((head, tail)) = path.split_first() {
297                writeln!(wr, "newpath")?;
298                writeln!(wr, "  {} {} moveto", head.0, head.1)?;
299                for pos in tail {
300                    writeln!(wr, r#"  {} {} lineto"#, pos.0, pos.1)?;
301                }
302                writeln!(wr, r#"stroke"#)?;
303            }
304        }
305        writeln!(wr, "%%EOF")
306    }
307
308    /// Saves the turtle graphic as Scalable Vector Graphic (SVG).
309    pub fn save_svg<W: Write>(&self, wr: &mut W) -> io::Result<()> {
310        // Determine extend of canvas
311        let mut bounds = Bounds::new();
312
313        // The SVG coordinates are from top to bottom, while turtle coordinates are
314        // bottom to
315        // top. We have to convert between the two. (multiply `y` by -1.0)
316        self.foreach_position(|pos| bounds.add_position(pos), 1.0, -1.0);
317
318        let (min_width, min_height) = (100.0, 100.0);
319        let width = bounds.width().max(min_width);
320        let height = bounds.height().max(min_height);
321        let border_percent = 0.1;
322
323        let top_left = Position(
324            bounds.min_x() - border_percent * width,
325            bounds.min_y() - border_percent * height,
326        );
327
328        let scale = 1.0 + 2.0 * border_percent;
329
330        writeln!(
331            wr,
332            r#"<?xml version="1.0" encoding="UTF-8"?>
333                <svg xmlns="http://www.w3.org/2000/svg"
334                version="1.1" baseProfile="full"
335                viewBox="{} {} {} {}">"#,
336            top_left.0,
337            top_left.1,
338            scale * width,
339            scale * height
340        )?;
341
342        // use a stroke width of 0.1% of the width or height of the canvas
343        let stroke_width = scale * width.max(height) / 1000.0;
344        writeln!(
345            wr,
346            r#"<g stroke="black" stroke-width="{}" fill="none">"#,
347            stroke_width
348        )?;
349
350        for path in self.paths.iter() {
351            if let Some((head, tail)) = path.split_first() {
352                // XXX
353                let head = Position(head.0, -1.0 * head.1);
354
355                write!(wr, r#"<path d="M{} {}"#, head.0, head.1)?;
356                for pos in tail {
357                    let pos = Position(pos.0, -1.0 * pos.1);
358                    write!(wr, r#" L{} {}"#, pos.0, pos.1)?;
359                }
360                writeln!(wr, r#"" />"#)?;
361            }
362        }
363        writeln!(wr, r#"</g>"#)?;
364
365        writeln!(wr, "</svg>")
366    }
367}
368
369impl Turtle for Canvas {
370    /// Move turtle forward by specified `distance`.
371    fn forward<T: Into<Distance>>(&mut self, distance: T) {
372        let (dx, dy) = self.direction(distance.into());
373        let src: Position = self.current_state().pos;
374        let dst = Position(src.0 + dx, src.1 + dy);
375        if self.is_pen_down() {
376            self.line_to(dst);
377        }
378        self.current_state_mut().pos = dst;
379    }
380
381    fn rotate<T: Into<Degree>>(&mut self, angle: T) {
382        let angle: Degree = angle.into();
383        self.current_state_mut().angle.0 += angle.0;
384    }
385
386    fn move_forward<T: Into<Distance>>(&mut self, distance: T) {
387        let (dx, dy) = self.direction(distance.into());
388        let src: Position = self.current_state().pos;
389        let dst = Position(src.0 + dx, src.1 + dy);
390        self.move_to(dst);
391        self.current_state_mut().pos = dst;
392    }
393
394    fn is_pen_down(&self) -> bool {
395        self.current_state().pendown
396    }
397
398    /// Put the pen down.
399    fn pen_down(&mut self) {
400        let pos = self.current_state().pos;
401        self.move_to(pos);
402        self.current_state_mut().pendown = true;
403    }
404
405    /// Put the pen up.
406    fn pen_up(&mut self) {
407        self.current_state_mut().pendown = false;
408    }
409
410    /// Positions the turtle exactly at `position`.
411    fn goto(&mut self, position: Position) {
412        self.current_state_mut().pos = position;
413        self.move_to(position);
414    }
415
416    /// Push current turtle state on stack.
417    fn push(&mut self) {
418        let state = self.current_state_mut().clone();
419        self.states.push(state);
420    }
421
422    /// Restore previously saved turtle state.
423    fn pop(&mut self) {
424        self.states.pop();
425        let pos = self.current_state().pos;
426        self.move_to(pos);
427    }
428}