Skip to main content

pizarra/shape/builder/
free_grid.rs

1use crate::shape::{ShapeBuilder, ShapeFinished};
2use crate::draw_commands::{DrawCommand, circle_helper, cancel_helper};
3use crate::path_command::PathCommand::*;
4use crate::point::{Vec2D, WorldUnit, ScreenUnit};
5use crate::transform::Transform;
6use crate::style::Style;
7use crate::geom::{rombus_to_rectangle, project};
8use crate::shape::builder::grid::grid_from_cell_and_point;
9use crate::shape::ShapeStored;
10use crate::shape::stored::path::Path;
11
12#[derive(Debug, Copy, Clone)]
13enum State {
14    Initial(Vec2D<WorldUnit>),
15
16    OneDimension {
17        start: Vec2D<WorldUnit>,
18        free: Vec2D<WorldUnit>
19    },
20
21    TwoDimension {
22        start: Vec2D<WorldUnit>,
23        p1: Vec2D<WorldUnit>,
24        free: Vec2D<WorldUnit>,
25    },
26
27    DefineGrid {
28        start: Vec2D<WorldUnit>,
29        p1: Vec2D<WorldUnit>,
30        p2: Vec2D<WorldUnit>,
31        free: Vec2D<WorldUnit>,
32
33        /// A transform matrix that turns the parallelogram defined by start, p1
34        /// and p2 into a square of size 1 in the first quadrant
35        t: Transform,
36    },
37}
38
39/// This tools builds a grid through a 5-step process. It might seem complicated
40/// at first but it is flexible and easy to learn.
41///
42/// 1. First place a point, this will be a corner of the grid.
43#[derive(Debug, Copy, Clone)]
44pub struct FreeGrid {
45    style: Style<WorldUnit>,
46    state: State,
47}
48
49impl FreeGrid {
50    pub fn start(initial: Vec2D<WorldUnit>, style: Style<WorldUnit>) -> FreeGrid {
51        FreeGrid {
52            style,
53            state: State::Initial(initial),
54        }
55    }
56}
57
58impl ShapeBuilder for FreeGrid {
59    fn handle_mouse_moved(&mut self, pos: Vec2D<ScreenUnit>, t: Transform, _snap: ScreenUnit) {
60        let wpos = t.to_world_coordinates(pos);
61
62        match self.state {
63            State::Initial(_) => {
64                self.state = State::Initial(wpos);
65            }
66
67            State::OneDimension { start, .. } => {
68                self.state = State::OneDimension { start, free: wpos };
69            }
70
71            State::TwoDimension { start, p1, .. } => {
72                self.state = State::TwoDimension { start, p1, free: wpos };
73            }
74
75            State::DefineGrid { start, p1, p2, t, .. } => {
76                self.state = State::DefineGrid { start, p1, p2, t, free: wpos };
77            }
78        }
79    }
80
81    fn handle_button_pressed(&mut self, _pos: Vec2D<ScreenUnit>, _t: Transform, _snap: ScreenUnit) { }
82
83    fn handle_button_released(&mut self, pos: Vec2D<ScreenUnit>, t: Transform, snap: ScreenUnit) -> ShapeFinished {
84        let wpos = t.to_world_coordinates(pos);
85
86        match self.state {
87            State::Initial(p) => {
88                self.state = State::OneDimension { start: p, free: wpos };
89
90                ShapeFinished::No
91            }
92
93            State::OneDimension { start, free } => {
94                self.state = State::TwoDimension { start, p1: free, free: wpos };
95
96                if t.to_screen_coordinates(start).distance(pos) < snap {
97                    ShapeFinished::Cancelled
98                } else {
99                    ShapeFinished::No
100                }
101            }
102
103            State::TwoDimension { start, p1, free } => {
104                let p2 = free;
105
106                self.state = State::DefineGrid {
107                    start, p1, p2,
108                    t: rombus_to_rectangle(start.to_vec2d(), p1.to_vec2d(), p2.to_vec2d()),
109                    free: wpos,
110                };
111
112                let sstart = t.to_screen_coordinates(start);
113                let sp1 = t.to_screen_coordinates(p1);
114                let proj = t.to_screen_coordinates(project(free, [start, p1]));
115                let proj2 = t.to_screen_coordinates(project(start, [p1, free]));
116
117                if sstart.distance(pos) < snap || sp1.distance(pos) < snap || proj.distance(pos) < snap || proj2.distance(sstart) < snap {
118                    ShapeFinished::Cancelled
119                } else {
120                    ShapeFinished::No
121                }
122            }
123
124            State::DefineGrid { start, p2, free, t, ..} => {
125                let cell = (
126                    t.to_screen_coordinates(start),
127                    t.to_screen_coordinates(p2),
128                );
129                let free = t.to_screen_coordinates(free);
130                let grid = grid_from_cell_and_point(cell, free);
131
132                let mut shapes: Vec<Box<dyn ShapeStored>> = Vec::with_capacity(
133                    grid.row_count() * (grid.col_count() - 1) +
134                    grid.col_count() * (grid.row_count() - 1)
135                );
136
137                let rows = grid.rows();
138                let cols = grid.cols();
139
140                for row in rows.iter() {
141                    for (&a, &b) in row.iter().zip(row.iter().skip(1)) {
142                        shapes.push(Box::new(Path::from_parts(
143                            vec![
144                                MoveTo(t.to_world_coordinates(a)),
145                                LineTo(t.to_world_coordinates(b)),
146                            ],
147                            self.style,
148                        )));
149                    }
150                }
151
152                for col in cols.iter() {
153                    for (&a, &b) in col.iter().zip(col.iter().skip(1)) {
154                        shapes.push(Box::new(Path::from_parts(
155                            vec![
156                                MoveTo(t.to_world_coordinates(a)),
157                                LineTo(t.to_world_coordinates(b)),
158                            ],
159                            self.style,
160                        )));
161                    }
162                }
163
164                ShapeFinished::Yes(shapes)
165            }
166        }
167    }
168
169    fn draw_commands(&self, t: Transform, snap: ScreenUnit) -> Vec<DrawCommand> {
170        match self.state {
171            State::Initial(p) => vec![
172                circle_helper(t.to_screen_coordinates(p), snap),
173            ],
174
175            State::OneDimension { start, free } => vec![
176                DrawCommand::Path {
177                    commands: vec![
178                        MoveTo(start),
179                        LineTo(free),
180                    ],
181                    style: self.style,
182                },
183                cancel_helper(start, free, t, snap),
184            ],
185
186            State::TwoDimension { start, p1, free } => vec![
187                DrawCommand::Path {
188                    commands: vec![
189                        MoveTo(start),
190                        LineTo(p1),
191                        LineTo(free),
192                        LineTo(start + (free - p1)),
193                        LineTo(start),
194                    ],
195                    style: self.style,
196                },
197
198                cancel_helper(start, free, t, snap),
199                cancel_helper(p1, free, t, snap),
200                // the projection of the mouse on the line defined by the first
201                // two points
202                cancel_helper(project(free, [start, p1]), free, t, snap),
203                // the projection of the first point on the line defined by the
204                // last two
205                cancel_helper(project(start, [p1, free]), start, t, snap),
206            ],
207
208            State::DefineGrid { start, p1: _, p2, t, free } => {
209                let cell = (
210                    t.apply(start.to_vec2d()),
211                    t.apply(p2.to_vec2d()),
212                );
213                let free = t.apply(free.to_vec2d());
214                let inverse = t.invert();
215                let grid = grid_from_cell_and_point(cell, free);
216
217                let mut commands = Vec::with_capacity(grid.row_count() + grid.col_count());
218
219                for row in grid.rows() {
220                    commands.push(DrawCommand::Path {
221                        commands: vec![
222                            MoveTo(inverse.apply(row[0]).into()),
223                            LineTo(inverse.apply(*row.last().unwrap()).into()),
224                        ],
225                        style: self.style,
226                    });
227                }
228
229                for col in grid.cols() {
230                    commands.push(DrawCommand::Path {
231                        commands: vec![
232                            MoveTo(inverse.apply(col[0]).into()),
233                            LineTo(inverse.apply(*col.last().unwrap()).into()),
234                        ],
235                        style: self.style,
236                    });
237                }
238
239                commands
240            }
241        }
242    }
243}
244
245#[cfg(test)]
246mod tests  {
247    use pretty_assertions::assert_eq;
248
249    use super::*;
250
251    const SNAP: ScreenUnit = ScreenUnit::from_float(10.0);
252
253    #[test]
254    fn figure_is_cancelled() {
255        let t = Default::default();
256        let mut grid = FreeGrid::start(Vec2D::new_world(0.0, 0.0), Default::default());
257
258        grid.handle_mouse_moved((1.0, 1.0).into(), t, SNAP);
259
260        // a circle goes around the cursor
261        assert_eq!(grid.draw_commands(t, SNAP), vec![
262            DrawCommand::ScreenCircle {
263                center: Vec2D::new_screen(1.0, 1.0),
264                radius: 10.0.into(),
265                style: Style::circle_helper(),
266            }
267        ]);
268
269        // start the grid at the origin
270        grid.handle_mouse_moved((0.0, 0.0).into(), t, SNAP);
271        grid.handle_button_released((0.0, 0.0).into(), t, SNAP);
272
273        // vove a little bit and observe a circle around the first point
274        grid.handle_mouse_moved((1.0, 1.0).into(), t, SNAP);
275
276        assert_eq!(grid.draw_commands(t, SNAP), vec![
277            DrawCommand::Path {
278                commands: vec![
279                    MoveTo((0.0, 0.0).into()),
280                    LineTo((1.0, 1.0).into()),
281                ],
282                style: Default::default(),
283            },
284
285            DrawCommand::ScreenCircle {
286                center: Vec2D::new_screen(0.0, 0.0),
287                radius: SNAP,
288                style: Style::red_circle_helper(),
289            },
290        ]);
291
292        Some(grid).map(|mut grid| {
293            // releasing here would cause a cancel
294            grid.handle_button_pressed((1.0, 1.0).into(), t, SNAP);
295            match grid.handle_button_released((1.0, 1.0).into(), t, SNAP) {
296                ShapeFinished::Cancelled => {}
297                _ => panic!(),
298            }
299        }).unwrap();
300
301        // move to the second point and use it to define the grid
302        grid.handle_mouse_moved((20.0, 0.0).into(), t, SNAP);
303        grid.handle_button_pressed((20.0, 0.0).into(), t, SNAP);
304        grid.handle_button_released((20.0, 0.0).into(), t, SNAP);
305
306        // moving around the two original points would threaten to cancel the
307        // shape
308        grid.handle_mouse_moved((1.0, 1.0).into(), t, SNAP);
309
310        assert_eq!(grid.draw_commands(t, SNAP), vec![
311            DrawCommand::Path {
312                commands: vec![
313                    MoveTo((0.0, 0.0).into()),
314                    LineTo((20.0, 0.0).into()),
315                    LineTo((1.0, 1.0).into()),
316                    LineTo((-19.0, 1.0).into()),
317                    LineTo((0.0, 0.0).into()),
318                ],
319                style: Default::default(),
320            },
321
322            // first circle red
323            DrawCommand::ScreenCircle {
324                center: (0.0, 0.0).into(),
325                radius: SNAP,
326                style: Style::red_circle_helper(),
327            },
328
329            // second circle gray
330            DrawCommand::ScreenCircle {
331                center: (20.0, 0.0).into(),
332                radius: SNAP,
333                style: Style::circle_helper(),
334            },
335
336            // third circle
337            DrawCommand::ScreenCircle {
338                center: (1.0, 0.0).into(),
339                radius: SNAP,
340                style: Style::red_circle_helper(),
341            },
342
343            // fourth circle
344            DrawCommand::ScreenCircle {
345                center: (0.0552486187845318, 1.0497237569060773).into(),
346                radius: SNAP,
347                style: Style::red_circle_helper(),
348            },
349        ]);
350
351        // releasing here would cancel
352        Some(grid).map(|mut grid| {
353            grid.handle_button_pressed((1.0, 1.0).into(), t, SNAP);
354            match grid.handle_button_released((1.0, 1.0).into(), t, SNAP) {
355                ShapeFinished::Cancelled => {}
356                _ => panic!()
357            }
358        }).unwrap();
359
360        grid.handle_mouse_moved((21.0, 1.0).into(), t, SNAP);
361
362        assert_eq!(grid.draw_commands(t, SNAP), vec![
363            DrawCommand::Path {
364                commands: vec![
365                    MoveTo((0.0, 0.0).into()),
366                    LineTo((20.0, 0.0).into()),
367                    LineTo((21.0, 1.0).into()),
368                    LineTo((1.0, 1.0).into()),
369                    LineTo((0.0, 0.0).into()),
370                ],
371                style: Default::default(),
372            },
373
374            // first circle red
375            DrawCommand::ScreenCircle {
376                center: (0.0, 0.0).into(),
377                radius: SNAP,
378                style: Style::circle_helper(),
379            },
380
381            // second circle gray
382            DrawCommand::ScreenCircle {
383                center: (20.0, 0.0).into(),
384                radius: SNAP,
385                style: Style::red_circle_helper(),
386            },
387
388            DrawCommand::ScreenCircle {
389                center: (21.0, 0.0).into(),
390                radius: SNAP,
391                style: Style::red_circle_helper(),
392            },
393
394            DrawCommand::ScreenCircle {
395                center: (10.0, -10.0).into(),
396                radius: SNAP,
397                style: Style::circle_helper(),
398            },
399        ]);
400
401        // releasing here would cancel
402        Some(grid).map(|mut grid| {
403            grid.handle_button_pressed((21.0, 1.0).into(), t, SNAP);
404            match grid.handle_button_released((21.0, 1.0).into(), t, SNAP) {
405                ShapeFinished::Cancelled => {}
406                _ => panic!()
407            }
408        }).unwrap();
409    }
410}