From: Huck Boles Date: Sat, 17 Jun 2023 22:16:50 +0000 (-0500) Subject: added: mask canvas X-Git-Url: https://git.huck.website/?a=commitdiff_plain;h=972804a939da8357e0b3fe4d09353639abae5f4b;p=cellseq.git added: mask canvas --- diff --git a/src/lib.rs b/src/lib.rs index b1cd57e..2be8d64 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,13 +2,50 @@ use iced::executor; use iced::theme::{self, Theme}; use iced::time; use iced::widget::{button, column, container, row, slider, text}; -use iced::{Alignment, Application, Command, Element, Length, Subscription}; +use iced::{Alignment, Application, Command, Element, Length, Point, Subscription}; use std::time::{Duration, Instant}; +mod life; mod map; +mod mask; +mod state; +use life::*; use map::*; +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub struct Cell { + i: isize, + j: isize, +} + +impl Cell { + const SIZE: usize = 16; + + fn at(position: Point) -> Cell { + let i = (position.y / Cell::SIZE as f32).ceil() as isize; + let j = (position.x / Cell::SIZE as f32).ceil() as isize; + + Cell { + i: i.saturating_sub(1), + j: j.saturating_sub(1), + } + } + + fn cluster(cell: Cell) -> impl Iterator { + use itertools::Itertools; + + let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); + let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); + + rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) + } + + fn neighbors(cell: Cell) -> impl Iterator { + Cell::cluster(cell).filter(move |candidate| *candidate != cell) + } +} + #[derive(Default)] pub struct CellSeq { grid: Map, diff --git a/src/life.rs b/src/life.rs new file mode 100644 index 0000000..1010819 --- /dev/null +++ b/src/life.rs @@ -0,0 +1,72 @@ +use super::*; + +use rustc_hash::{FxHashMap, FxHashSet}; + +#[derive(Clone, Default)] +pub struct Life { + cells: FxHashSet, +} + +impl Life { + pub fn len(&self) -> usize { + self.cells.len() + } + + pub fn contains(&self, cell: &Cell) -> bool { + self.cells.contains(cell) + } + + pub fn populate(&mut self, cell: Cell) { + self.cells.insert(cell); + } + + pub fn unpopulate(&mut self, cell: &Cell) { + let _ = self.cells.remove(cell); + } + + pub fn tick(&mut self) { + let mut adjacent_life = FxHashMap::default(); + + for cell in &self.cells { + let _ = adjacent_life.entry(*cell).or_insert(0); + + for neighbor in Cell::neighbors(*cell) { + let amount = adjacent_life.entry(neighbor).or_insert(0); + + *amount += 1; + } + } + + for (cell, amount) in adjacent_life.iter() { + match amount { + 2 => {} + 3 => { + let _ = self.cells.insert(*cell); + } + _ => { + let _ = self.cells.remove(cell); + } + } + } + } + + pub fn iter(&self) -> impl Iterator { + self.cells.iter() + } +} + +impl std::iter::FromIterator for Life { + fn from_iter>(iter: I) -> Self { + Life { + cells: iter.into_iter().collect(), + } + } +} + +impl std::fmt::Debug for Life { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("Life") + .field("cells", &self.cells.len()) + .finish() + } +} diff --git a/src/map.rs b/src/map.rs index 52b48cc..5d87515 100644 --- a/src/map.rs +++ b/src/map.rs @@ -1,20 +1,20 @@ use iced::alignment; use iced::mouse; -use iced::touch; use iced::widget::canvas; use iced::widget::canvas::event::{self, Event}; use iced::widget::canvas::{Cache, Canvas, Cursor, Frame, Geometry, Path, Text}; use iced::{Color, Element, Length, Point, Rectangle, Size, Theme, Vector}; -use rustc_hash::{FxHashMap, FxHashSet}; use std::future::Future; use std::ops::RangeInclusive; use std::time::{Duration, Instant}; +use crate::{mask::Note, state::State, Cell, Life}; + pub struct Map { state: State, life_cache: Cache, + mask_cache: Cache, translation: Vector, - show_lines: bool, last_tick_duration: Duration, last_queued_ticks: usize, } @@ -23,6 +23,8 @@ pub struct Map { pub enum Message { Populate(Cell), Unpopulate(Cell), + Check(Cell), + Uncheck(Cell), Ticked { result: Result, tick_duration: Duration, @@ -39,8 +41,8 @@ impl Default for Map { Self { state: State::with_life(Life::default()), life_cache: Cache::default(), + mask_cache: Cache::default(), translation: Vector::default(), - show_lines: true, last_tick_duration: Duration::default(), last_queued_ticks: 0, } @@ -67,6 +69,14 @@ impl Map { pub fn update(&mut self, message: Message) { match message { + Message::Check(cell) => { + self.state.check(cell); + self.mask_cache.clear(); + } + Message::Uncheck(cell) => { + self.state.uncheck(cell); + self.mask_cache.clear(); + } Message::Populate(cell) => { self.state.populate(cell); self.life_cache.clear(); @@ -104,14 +114,6 @@ impl Map { self.life_cache.clear(); } - pub fn toggle_lines(&mut self, enabled: bool) { - self.show_lines = enabled; - } - - pub fn are_lines_visible(&self) -> bool { - self.show_lines - } - fn visible_region(&self, size: Size) -> Region { let width = size.width; let height = size.height; @@ -124,10 +126,24 @@ impl Map { } } - fn project(&self, position: Point, size: Size) -> Point { + fn project(&self, position: Point, size: Size, in_life: bool) -> Point { let region = self.visible_region(size); - Point::new(position.x / 1.0 + region.x, position.y / 1.0 + region.y) + let center = Point { + x: size.width / 2.0, + y: size.height / 2.0, + }; + + let translation = Vector { + x: 0.0, + y: if in_life { + center.y - center.y / 2.0 + } else { + center.y + center.y / 2.0 + }, + }; + + Point::new(position.x + region.x, position.y + region.y) + translation } } @@ -141,6 +157,8 @@ impl canvas::Program for Map { bounds: Rectangle, cursor: Cursor, ) -> (event::Status, Option) { + let center = bounds.center(); + if let Event::Mouse(mouse::Event::ButtonReleased(_)) = event { *interaction = Interaction::None; } @@ -151,82 +169,68 @@ impl canvas::Program for Map { return (event::Status::Ignored, None); }; - let cell = Cell::at(self.project(cursor_position, bounds.size())); - let is_populated = self.state.contains(&cell); + let cell: Cell; + let action: Option; + let is_populated: bool; - let (populate, unpopulate) = if is_populated { - (None, Some(Message::Unpopulate(cell))) + if cursor_position.y < center.y { + cell = Cell::at(self.project(cursor_position, bounds.size(), true)); + is_populated = self.state.contains(&cell); + + action = if is_populated { + Some(Message::Unpopulate(cell)) + } else { + Some(Message::Populate(cell)) + }; } else { - (Some(Message::Populate(cell)), None) - }; + cell = Cell::at(self.project(cursor_position, bounds.size(), false)); + is_populated = self.state.mask_contains(&cell); + + action = if is_populated { + Some(Message::Uncheck(cell)) + } else { + Some(Message::Check(cell)) + }; + } match event { - Event::Touch(touch::Event::FingerMoved { .. }) => { - let message = { - *interaction = if is_populated { - Interaction::Erasing - } else { - Interaction::Drawing - }; - - populate.or(unpopulate) + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + *interaction = if is_populated { + Interaction::Erasing + } else { + Interaction::Drawing }; - - (event::Status::Captured, message) + (event::Status::Captured, action) } - Event::Mouse(mouse_event) => match mouse_event { - mouse::Event::ButtonPressed(button) => { - let message = match button { - mouse::Button::Left => { - *interaction = if is_populated { - Interaction::Erasing - } else { - Interaction::Drawing - }; - - populate.or(unpopulate) - } - _ => None, - }; - - (event::Status::Captured, message) - } - mouse::Event::CursorMoved { .. } => { - let message = match *interaction { - Interaction::Drawing => populate, - Interaction::Erasing => unpopulate, - _ => None, - }; - - let event_status = match interaction { - Interaction::None => event::Status::Ignored, - _ => event::Status::Captured, - }; - - (event_status, message) - } - _ => (event::Status::Ignored, None), - }, _ => (event::Status::Ignored, None), } } + fn draw( &self, _interaction: &Interaction, _theme: &Theme, bounds: Rectangle, - cursor: Cursor, + _cursor: Cursor, ) -> Vec { let center = Vector::new(bounds.width / 2.0, bounds.height / 2.0); - let life = self.life_cache.draw(bounds.size(), |frame| { + let split_half = Size::new(bounds.width, bounds.height / 2.0); + + let life_canvas = Rectangle::new(bounds.position(), split_half); + let mask_canvas = + Rectangle::new(bounds.position() + Vector::new(0.0, center.y), split_half); + + let life = self.life_cache.draw(life_canvas.size(), |frame| { + let life_center = Vector { + x: bounds.x + life_canvas.center().x, + y: bounds.y + life_canvas.center().y, + }; let background = Path::rectangle(Point::ORIGIN, frame.size()); - frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0x4B)); + frame.fill(&background, Color::from_rgb8(0xA0, 0x44, 0x4B)); frame.with_save(|frame| { - frame.translate(center); - frame.scale(1.0); - frame.translate(self.translation); + frame.translate(life_center); frame.scale(Cell::SIZE as f32); let region = self.visible_region(frame.size()); @@ -241,30 +245,32 @@ impl canvas::Program for Map { }); }); - let overlay = { - let mut frame = Frame::new(bounds.size()); + let mask = self.mask_cache.draw(mask_canvas.size(), |frame| { + let mask_center = Vector { + x: bounds.x + life_canvas.center().x, + y: bounds.y + life_canvas.center().y, + }; + let background = Path::rectangle(Point::ORIGIN, frame.size()); + frame.fill(&background, Color::from_rgb8(0x40, 0x44, 0xA0)); - let hovered_cell = cursor - .position_in(&bounds) - .map(|position| Cell::at(self.project(position, frame.size()))); + frame.with_save(|frame| { + frame.translate(mask_center); + frame.scale(Cell::SIZE as f32); - if let Some(cell) = hovered_cell { - frame.with_save(|frame| { - frame.translate(center); - frame.scale(1.0); - frame.translate(self.translation); - frame.scale(Cell::SIZE as f32); + let region = self.visible_region(frame.size()); + for (cell, _) in region.cull_mask(self.state.mask()) { frame.fill_rectangle( Point::new(cell.j as f32, cell.i as f32), Size::UNIT, - Color { - a: 0.5, - ..Color::BLACK - }, + Color::WHITE, ); - }); - } + } + }); + }); + + let overlay = { + let mut frame = Frame::new(bounds.size()); let text = Text { color: Color::WHITE, @@ -275,14 +281,6 @@ impl canvas::Program for Map { ..Text::default() }; - if let Some(cell) = hovered_cell { - frame.fill_text(Text { - content: format!("({}, {})", cell.j, cell.i), - position: text.position - Vector::new(0.0, 16.0), - ..text - }); - } - let cell_count = self.state.cell_count(); frame.fill_text(Text { @@ -299,7 +297,7 @@ impl canvas::Program for Map { frame.into_geometry() }; - vec![life, overlay] + vec![overlay, mask, life] } fn mouse_interaction( @@ -316,182 +314,6 @@ impl canvas::Program for Map { } } } - -#[derive(Default)] -struct State { - life: Life, - births: FxHashSet, - is_ticking: bool, -} - -impl State { - pub fn with_life(life: Life) -> Self { - Self { - life, - ..Self::default() - } - } - - fn cell_count(&self) -> usize { - self.life.len() + self.births.len() - } - - fn contains(&self, cell: &Cell) -> bool { - self.life.contains(cell) || self.births.contains(cell) - } - - fn cells(&self) -> impl Iterator { - self.life.iter().chain(self.births.iter()) - } - - fn populate(&mut self, cell: Cell) { - if self.is_ticking { - self.births.insert(cell); - } else { - self.life.populate(cell); - } - } - - fn unpopulate(&mut self, cell: &Cell) { - if self.is_ticking { - let _ = self.births.remove(cell); - } else { - self.life.unpopulate(cell); - } - } - - fn update(&mut self, mut life: Life) { - self.births.drain().for_each(|cell| life.populate(cell)); - - self.life = life; - self.is_ticking = false; - } - - fn tick(&mut self, amount: usize) -> Option>> { - if self.is_ticking { - return None; - } - - self.is_ticking = true; - - let mut life = self.life.clone(); - - Some(async move { - tokio::task::spawn_blocking(move || { - for _ in 0..amount { - life.tick(); - } - - life - }) - .await - .map_err(|_| TickError::JoinFailed) - }) - } -} - -#[derive(Clone, Default)] -pub struct Life { - cells: FxHashSet, -} - -impl Life { - fn len(&self) -> usize { - self.cells.len() - } - - fn contains(&self, cell: &Cell) -> bool { - self.cells.contains(cell) - } - - fn populate(&mut self, cell: Cell) { - self.cells.insert(cell); - } - - fn unpopulate(&mut self, cell: &Cell) { - let _ = self.cells.remove(cell); - } - - fn tick(&mut self) { - let mut adjacent_life = FxHashMap::default(); - - for cell in &self.cells { - let _ = adjacent_life.entry(*cell).or_insert(0); - - for neighbor in Cell::neighbors(*cell) { - let amount = adjacent_life.entry(neighbor).or_insert(0); - - *amount += 1; - } - } - - for (cell, amount) in adjacent_life.iter() { - match amount { - 2 => {} - 3 => { - let _ = self.cells.insert(*cell); - } - _ => { - let _ = self.cells.remove(cell); - } - } - } - } - - pub fn iter(&self) -> impl Iterator { - self.cells.iter() - } -} - -impl std::iter::FromIterator for Life { - fn from_iter>(iter: I) -> Self { - Life { - cells: iter.into_iter().collect(), - } - } -} - -impl std::fmt::Debug for Life { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - f.debug_struct("Life") - .field("cells", &self.cells.len()) - .finish() - } -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] -pub struct Cell { - i: isize, - j: isize, -} - -impl Cell { - const SIZE: usize = 16; - - fn at(position: Point) -> Cell { - let i = (position.y / Cell::SIZE as f32).ceil() as isize; - let j = (position.x / Cell::SIZE as f32).ceil() as isize; - - Cell { - i: i.saturating_sub(1), - j: j.saturating_sub(1), - } - } - - fn cluster(cell: Cell) -> impl Iterator { - use itertools::Itertools; - - let rows = cell.i.saturating_sub(1)..=cell.i.saturating_add(1); - let columns = cell.j.saturating_sub(1)..=cell.j.saturating_add(1); - - rows.cartesian_product(columns).map(|(i, j)| Cell { i, j }) - } - - fn neighbors(cell: Cell) -> impl Iterator { - Cell::cluster(cell).filter(move |candidate| *candidate != cell) - } -} - pub struct Region { x: f32, y: f32, @@ -522,6 +344,16 @@ impl Region { cells.filter(move |cell| rows.contains(&cell.i) && columns.contains(&cell.j)) } + + fn cull_mask<'a>( + &self, + cells: impl Iterator, + ) -> impl Iterator { + let rows = self.rows(); + let columns = self.columns(); + + cells.filter(move |(cell, _)| rows.contains(&cell.i) && columns.contains(&cell.j)) + } } pub enum Interaction { diff --git a/src/mask.rs b/src/mask.rs new file mode 100644 index 0000000..5fcd1ff --- /dev/null +++ b/src/mask.rs @@ -0,0 +1,28 @@ +use super::*; + +use rustc_hash::FxHashMap; + +pub type Note = usize; + +#[derive(Clone, Default)] +pub struct Mask { + cells: FxHashMap, +} + +impl Mask { + pub fn contains(&self, cell: &Cell) -> bool { + self.cells.contains_key(cell) + } + + pub fn check(&mut self, cell: Cell) { + self.cells.insert(cell, Note::default()); + } + + pub fn uncheck(&mut self, cell: Cell) { + let _ = self.cells.remove(&cell); + } + + pub fn iter(&self) -> impl Iterator { + self.cells.iter() + } +} diff --git a/src/state.rs b/src/state.rs new file mode 100644 index 0000000..850b499 --- /dev/null +++ b/src/state.rs @@ -0,0 +1,98 @@ +use crate::{ + life::Life, + mask::{Mask, Note}, + Cell, TickError, +}; + +use rustc_hash::FxHashSet; +use std::future::Future; + +#[derive(Default)] +pub struct State { + life: Life, + mask: Mask, + births: FxHashSet, + is_ticking: bool, +} + +impl State { + pub fn with_life(life: Life) -> Self { + Self { + life, + ..Self::default() + } + } + + pub fn cell_count(&self) -> usize { + self.life.len() + self.births.len() + } + + pub fn contains(&self, cell: &Cell) -> bool { + self.life.contains(cell) || self.births.contains(cell) + } + + pub fn mask_contains(&self, cell: &Cell) -> bool { + self.mask.contains(cell) + } + + pub fn cells(&self) -> impl Iterator { + self.life.iter().chain(self.births.iter()) + } + + pub fn mask(&self) -> impl Iterator { + self.mask.iter() + } + + pub fn check(&mut self, cell: Cell) { + self.mask.check(cell); + } + + pub fn uncheck(&mut self, cell: Cell) { + self.mask.uncheck(cell); + } + + pub fn populate(&mut self, cell: Cell) { + if self.is_ticking { + self.births.insert(cell); + } else { + self.life.populate(cell); + } + } + + pub fn unpopulate(&mut self, cell: &Cell) { + if self.is_ticking { + let _ = self.births.remove(cell); + } else { + self.life.unpopulate(cell); + } + } + + pub fn update(&mut self, mut life: Life) { + self.births.drain().for_each(|cell| life.populate(cell)); + + self.life = life; + self.is_ticking = false; + } + + pub fn tick(&mut self, amount: usize) -> Option>> { + if self.is_ticking { + return None; + } + + self.is_ticking = true; + + let mut life = self.life.clone(); + + Some(async move { + tokio::task::spawn_blocking(move || { + for _ in 0..amount { + life.tick(); + } + + life + }) + .await + .map_err(|_| TickError::JoinFailed) + }) + } +}