diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/app.rs | 139 | ||||
-rw-r--r-- | src/bitmap.rs | 37 |
2 files changed, 118 insertions, 58 deletions
@@ -1,9 +1,10 @@ | |||
1 | use crate::{ | 1 | use crate::{ |
2 | bitmap::{MapPoint, Pixmap}, | 2 | bitmap::{MapPoint, Pixmap}, |
3 | brush::Brush, | ||
3 | command::CommandBox, | 4 | command::CommandBox, |
4 | consts::{colors::*, FONT_PATH}, | 5 | consts::{colors::*, FONT_PATH}, |
5 | dither, | 6 | dither, |
6 | lisp::{eval, lex::Lexer, parse::Parser, prelude, EnvList, Environment}, | 7 | lisp::{eval, lex::Lexer, parse::Parser, prelude, EnvList}, |
7 | message::Message, | 8 | message::Message, |
8 | rect, | 9 | rect, |
9 | symmetry::Symmetry, | 10 | symmetry::Symmetry, |
@@ -11,12 +12,7 @@ use crate::{ | |||
11 | utils::{draw_text, is_copy_event, is_paste_event}, | 12 | utils::{draw_text, is_copy_event, is_paste_event}, |
12 | }; | 13 | }; |
13 | 14 | ||
14 | use std::{ | 15 | use std::{convert::From, fs::File, io::prelude::*, path::Path}; |
15 | convert::From, | ||
16 | fs::File, | ||
17 | io::prelude::*, | ||
18 | path::{Path, PathBuf}, | ||
19 | }; | ||
20 | 16 | ||
21 | use obi::Image; | 17 | use obi::Image; |
22 | use sdl2::{ | 18 | use sdl2::{ |
@@ -40,6 +36,7 @@ pub enum Mode { | |||
40 | pub struct AppState<'ctx, 'file> { | 36 | pub struct AppState<'ctx, 'file> { |
41 | pub active_color: bool, | 37 | pub active_color: bool, |
42 | pub brush_size: u8, | 38 | pub brush_size: u8, |
39 | pub brush: Brush, | ||
43 | pub canvas: Canvas<Window>, | 40 | pub canvas: Canvas<Window>, |
44 | pub command_box: CommandBox, | 41 | pub command_box: CommandBox, |
45 | pub context: &'ctx Sdl, | 42 | pub context: &'ctx Sdl, |
@@ -204,7 +201,7 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
204 | val, | 201 | val, |
205 | } in op.into_iter() | 202 | } in op.into_iter() |
206 | { | 203 | { |
207 | if self.pixmap.is_inside(point) { | 204 | if self.pixmap.contains(point) { |
208 | match op_kind { | 205 | match op_kind { |
209 | OpKind::Undo => self.pixmap.set(point, old_val), | 206 | OpKind::Undo => self.pixmap.set(point, old_val), |
210 | OpKind::Redo => self.pixmap.set(point, val), | 207 | OpKind::Redo => self.pixmap.set(point, val), |
@@ -346,12 +343,13 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
346 | format!("---, ---") | 343 | format!("---, ---") |
347 | }; | 344 | }; |
348 | let status_text = format!( | 345 | let status_text = format!( |
349 | "[DITHER {}][BRUSH {}][SYM {}][PT {}][ACTIVE {}]", | 346 | "[DITHER {}][BRUSH {}][SYM {}][PT {}][ACTIVE {}][KIND {}]", |
350 | self.dither_level, | 347 | self.dither_level, |
351 | self.brush_size + 1, | 348 | self.brush_size + 1, |
352 | self.symmetry, | 349 | self.symmetry, |
353 | mouse_coords, | 350 | mouse_coords, |
354 | if self.active_color { "WHT" } else { "BLK" } | 351 | if self.active_color { "WHT" } else { "BLK" }, |
352 | self.brush | ||
355 | ); | 353 | ); |
356 | draw_text( | 354 | draw_text( |
357 | &mut self.canvas, | 355 | &mut self.canvas, |
@@ -426,13 +424,31 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
426 | } | 424 | } |
427 | } | 425 | } |
428 | 426 | ||
427 | fn draw_symmetry(&mut self) { | ||
428 | let (winsize_x, winsize_y) = self.canvas.window().size(); | ||
429 | let cs = self.zoom as u32; | ||
430 | let Symmetry { x: sym_x, y: sym_y } = self.symmetry; | ||
431 | if let Some(line) = sym_x { | ||
432 | self.canvas.set_draw_color(CYAN); | ||
433 | let line_coord = (line * cs) as i32 + self.start.y() + (cs / 2) as i32; | ||
434 | self.canvas | ||
435 | .draw_line((0, line_coord), (winsize_x as i32, line_coord)) | ||
436 | .unwrap(); | ||
437 | } | ||
438 | if let Some(line) = sym_y { | ||
439 | self.canvas.set_draw_color(CYAN); | ||
440 | let line_coord = (line * cs) as i32 + self.start.x() + (cs / 2) as i32; | ||
441 | self.canvas | ||
442 | .draw_line((line_coord, 0), (line_coord, winsize_y as i32)) | ||
443 | .unwrap(); | ||
444 | } | ||
445 | } | ||
446 | |||
429 | fn draw(&mut self) { | 447 | fn draw(&mut self) { |
430 | let cs = self.zoom as u32; | 448 | let cs = self.zoom as u32; |
431 | let (width, height) = (self.width(), self.height()); | 449 | let (width, height) = (self.width(), self.height()); |
432 | let start = self.start; | 450 | let start = self.start; |
433 | let grid_enabled = self.grid.enabled; | 451 | let grid_enabled = self.grid.enabled; |
434 | let (winsize_x, winsize_y) = self.canvas.window().size(); | ||
435 | let Symmetry { x: sym_x, y: sym_y } = self.symmetry; | ||
436 | for (idx, val) in self.pixmap.data.iter().enumerate() { | 452 | for (idx, val) in self.pixmap.data.iter().enumerate() { |
437 | if *val { | 453 | if *val { |
438 | let idx = idx as i32; | 454 | let idx = idx as i32; |
@@ -451,20 +467,7 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
451 | if grid_enabled { | 467 | if grid_enabled { |
452 | self.draw_grid(); | 468 | self.draw_grid(); |
453 | } | 469 | } |
454 | if let Some(line) = sym_x { | 470 | self.draw_symmetry(); |
455 | self.canvas.set_draw_color(CYAN); | ||
456 | let line_coord = (line * cs) as i32 + self.start.y() + (cs / 2) as i32; | ||
457 | self.canvas | ||
458 | .draw_line((0, line_coord), (winsize_x as i32, line_coord)) | ||
459 | .unwrap(); | ||
460 | } | ||
461 | if let Some(line) = sym_y { | ||
462 | self.canvas.set_draw_color(CYAN); | ||
463 | let line_coord = (line * cs) as i32 + self.start.x() + (cs / 2) as i32; | ||
464 | self.canvas | ||
465 | .draw_line((line_coord, 0), (line_coord, winsize_y as i32)) | ||
466 | .unwrap(); | ||
467 | } | ||
468 | self.draw_statusline(); | 471 | self.draw_statusline(); |
469 | self.draw_command_box(); | 472 | self.draw_command_box(); |
470 | self.draw_mouse(); | 473 | self.draw_mouse(); |
@@ -515,6 +518,7 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
515 | Self { | 518 | Self { |
516 | active_color: true, | 519 | active_color: true, |
517 | brush_size: 0, | 520 | brush_size: 0, |
521 | brush: Brush::new(), | ||
518 | canvas, | 522 | canvas, |
519 | command_box: CommandBox::new(), | 523 | command_box: CommandBox::new(), |
520 | context, | 524 | context, |
@@ -602,22 +606,14 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
602 | Keycode::I => self.pixmap.invert(), | 606 | Keycode::I => self.pixmap.invert(), |
603 | // line drawing | 607 | // line drawing |
604 | Keycode::F => { | 608 | Keycode::F => { |
605 | let end: Point = (mouse.x(), mouse.y()).into(); | 609 | if matches!(self.brush, Brush::Line { extend: false, .. }) { |
606 | if let Some(start) = self.last_point { | 610 | self.brush = Brush::line(0, true); |
607 | if let Ok(o) = | 611 | } else { |
608 | self.paint_line(start, end, self.active_color) | 612 | self.brush = Brush::line(0, false); |
609 | { | ||
610 | self.commit_operation(); | ||
611 | self.current_operation = o | ||
612 | .into_iter() | ||
613 | .filter(|v| !v.old_val == v.val) | ||
614 | .collect(); | ||
615 | self.commit_operation(); | ||
616 | self.last_point = | ||
617 | Some(self.idx_at_coord(end).unwrap().into()); | ||
618 | } | ||
619 | } | 613 | } |
620 | } | 614 | } |
615 | // bucket fill tool | ||
616 | Keycode::B => self.brush = Brush::Fill, | ||
621 | Keycode::V => self.cycle_symmetry(), | 617 | Keycode::V => self.cycle_symmetry(), |
622 | // undo & redo | 618 | // undo & redo |
623 | Keycode::U => { | 619 | Keycode::U => { |
@@ -646,13 +642,63 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
646 | x, y, mouse_btn, .. | 642 | x, y, mouse_btn, .. |
647 | } => { | 643 | } => { |
648 | let pt = (x, y); | 644 | let pt = (x, y); |
645 | let contact = self.idx_at_coord(pt).map(MapPoint::from); | ||
649 | self.last_point = self.idx_at_coord(pt).map(MapPoint::from); | 646 | self.last_point = self.idx_at_coord(pt).map(MapPoint::from); |
650 | let val = match mouse_btn { | 647 | let val = match mouse_btn { |
651 | MouseButton::Right => !self.active_color, | 648 | MouseButton::Right => !self.active_color, |
652 | _ => self.active_color, | 649 | _ => self.active_color, |
653 | }; | 650 | }; |
654 | if let Ok(o) = self.paint_point(pt, val) { | 651 | match self.brush { |
655 | self.current_operation.extend(o); | 652 | Brush::Circle { size } => { |
653 | if let Ok(o) = self.paint_point(pt, val) { | ||
654 | self.current_operation.extend(o); | ||
655 | } | ||
656 | } | ||
657 | Brush::Line { | ||
658 | size, | ||
659 | start, | ||
660 | extend, | ||
661 | } => { | ||
662 | if start.is_none() { | ||
663 | self.brush = Brush::Line { | ||
664 | size, | ||
665 | start: contact, | ||
666 | extend, | ||
667 | }; | ||
668 | } else if let Ok(o) = | ||
669 | self.paint_line(start.unwrap(), pt, val) | ||
670 | { | ||
671 | self.current_operation.extend(o); | ||
672 | self.brush = Brush::Line { | ||
673 | size, | ||
674 | start: if extend { contact } else { None }, | ||
675 | extend, | ||
676 | }; | ||
677 | } | ||
678 | } | ||
679 | Brush::Fill => { | ||
680 | if let Some(c) = contact { | ||
681 | let target = self.pixmap.get(c); | ||
682 | let mut operation = vec![]; | ||
683 | self.pixmap.flood_fill( | ||
684 | c, | ||
685 | target, | ||
686 | self.active_color, | ||
687 | &mut operation, | ||
688 | ); | ||
689 | self.current_operation.extend( | ||
690 | operation | ||
691 | .into_iter() | ||
692 | .map(|point| ModifyRecord { | ||
693 | point, | ||
694 | old_val: target, | ||
695 | val: self.active_color, | ||
696 | }) | ||
697 | .collect::<Vec<ModifyRecord>>(), | ||
698 | ) | ||
699 | } | ||
700 | } | ||
701 | _ => {} | ||
656 | } | 702 | } |
657 | } | 703 | } |
658 | // click and drag | 704 | // click and drag |
@@ -674,14 +720,7 @@ impl<'ctx, 'file> AppState<'ctx, 'file> { | |||
674 | } | 720 | } |
675 | } | 721 | } |
676 | // end of operation | 722 | // end of operation |
677 | Event::MouseButtonUp { .. } => { | 723 | Event::MouseButtonUp { .. } => self.commit_operation(), |
678 | let op = self | ||
679 | .current_operation | ||
680 | .drain(..) | ||
681 | .filter(|v| !v.old_val == v.val) | ||
682 | .collect::<Vec<_>>(); | ||
683 | self.undo_stack.push(op); | ||
684 | } | ||
685 | Event::Quit { .. } => { | 724 | Event::Quit { .. } => { |
686 | break 'running; | 725 | break 'running; |
687 | } | 726 | } |
diff --git a/src/bitmap.rs b/src/bitmap.rs index c7e8fd4..41af745 100644 --- a/src/bitmap.rs +++ b/src/bitmap.rs | |||
@@ -54,8 +54,8 @@ impl Add for MapPoint { | |||
54 | type Output = Self; | 54 | type Output = Self; |
55 | fn add(self, rhs: Self) -> Self::Output { | 55 | fn add(self, rhs: Self) -> Self::Output { |
56 | MapPoint { | 56 | MapPoint { |
57 | x: self.x + rhs.x, | 57 | x: self.x.saturating_add(rhs.x), |
58 | y: self.y + rhs.y, | 58 | y: self.y.saturating_add(rhs.y), |
59 | } | 59 | } |
60 | } | 60 | } |
61 | } | 61 | } |
@@ -64,8 +64,8 @@ impl Sub for MapPoint { | |||
64 | type Output = Self; | 64 | type Output = Self; |
65 | fn sub(self, rhs: Self) -> Self::Output { | 65 | fn sub(self, rhs: Self) -> Self::Output { |
66 | MapPoint { | 66 | MapPoint { |
67 | x: self.x - rhs.x, | 67 | x: self.x.saturating_sub(rhs.x), |
68 | y: self.y - rhs.y, | 68 | y: self.y.saturating_sub(rhs.y), |
69 | } | 69 | } |
70 | } | 70 | } |
71 | } | 71 | } |
@@ -102,7 +102,7 @@ impl MapPoint { | |||
102 | 102 | ||
103 | impl<T> Pixmap<T> | 103 | impl<T> Pixmap<T> |
104 | where | 104 | where |
105 | T: Copy + Clone + Default, | 105 | T: Copy + Clone + Default + PartialEq, |
106 | { | 106 | { |
107 | pub fn new(width: u32, height: u32) -> Self { | 107 | pub fn new(width: u32, height: u32) -> Self { |
108 | let data = vec![Default::default(); (width * height) as usize]; | 108 | let data = vec![Default::default(); (width * height) as usize]; |
@@ -121,7 +121,7 @@ where | |||
121 | } | 121 | } |
122 | } | 122 | } |
123 | 123 | ||
124 | pub fn is_inside<P: Into<MapPoint>>(&self, pt: P) -> bool { | 124 | pub fn contains<P: Into<MapPoint>>(&self, pt: P) -> bool { |
125 | let MapPoint { x, y } = pt.into(); | 125 | let MapPoint { x, y } = pt.into(); |
126 | x < self.width && y < self.height | 126 | x < self.width && y < self.height |
127 | } | 127 | } |
@@ -181,7 +181,7 @@ where | |||
181 | circle | 181 | circle |
182 | .into_iter() | 182 | .into_iter() |
183 | .flat_map(|pt| MapPoint::try_from(pt)) | 183 | .flat_map(|pt| MapPoint::try_from(pt)) |
184 | .filter(|&pt| self.is_inside(pt)) | 184 | .filter(|&pt| self.contains(pt)) |
185 | .collect() | 185 | .collect() |
186 | } | 186 | } |
187 | 187 | ||
@@ -226,9 +226,30 @@ where | |||
226 | coordinates | 226 | coordinates |
227 | .into_iter() | 227 | .into_iter() |
228 | .flat_map(|pt| MapPoint::try_from(pt)) | 228 | .flat_map(|pt| MapPoint::try_from(pt)) |
229 | .filter(|&pt| self.is_inside(pt)) | 229 | .filter(|&pt| self.contains(pt)) |
230 | .collect() | 230 | .collect() |
231 | } | 231 | } |
232 | |||
233 | pub fn flood_fill( | ||
234 | &mut self, | ||
235 | start: MapPoint, | ||
236 | target: T, | ||
237 | replacement: T, | ||
238 | pts: &mut Vec<MapPoint>, | ||
239 | ) { | ||
240 | if !self.contains(start) || self.get(start) != target || self.get(start) == replacement { | ||
241 | return; | ||
242 | } else { | ||
243 | pts.push(start); | ||
244 | self.set(start, replacement); | ||
245 | for (x, y) in [(1, 0), (0, 1), (-1, 0), (0, -1)].iter() { | ||
246 | let dir = MapPoint::try_from((start.x as i64 - x, start.y as i64 + y)); | ||
247 | if let Ok(pt) = dir { | ||
248 | self.flood_fill(pt, target, replacement, pts); | ||
249 | } | ||
250 | } | ||
251 | } | ||
252 | } | ||
232 | } | 253 | } |
233 | 254 | ||
234 | impl Pixmap<bool> { | 255 | impl Pixmap<bool> { |