diff --git a/petri/src/lib.rs b/petri/src/lib.rs index 5370873..225926f 100644 --- a/petri/src/lib.rs +++ b/petri/src/lib.rs @@ -10,6 +10,7 @@ pub struct Cell(pub u16); pub struct Dish { pub chunk: Chunk, pub rules: Vec, + pub cell_groups: Vec>, } #[derive(Debug)] @@ -33,7 +34,31 @@ pub struct Rule { struct SubRule { width: usize, height: usize, - contents: Vec<(Option, Option)>, + contents: Vec<(RuleCellFrom, RuleCellTo)>, +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub enum RuleCellFrom { + /// matches anything + #[default] + Any, + /// matches one cell type + One(Cell), + /// matches anything defined in the group referenced by this index + Group(usize), +} + +#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)] +pub enum RuleCellTo { + /// don't modify this cell + #[default] + None, + /// set to this cell + One(Cell), + /// randomly choose from the group + GroupRandom(usize), + /// copy the cell from the corresponding input position + Copy(usize), } impl SubRule { @@ -41,36 +66,36 @@ impl SubRule { Self { width: 1, height: 1, - contents: vec![(None, None)], + contents: vec![Default::default()], } } - fn get(&self, x: usize, y: usize) -> (Option, Option) { + fn get(&self, x: usize, y: usize) -> (RuleCellFrom, RuleCellTo) { if x >= self.width || y >= self.height { - (None, None) + Default::default() } else { self.contents[x + self.width * y].clone() } } - fn get_mut(&mut self, x: usize, y: usize) -> &mut (Option, Option) { + fn get_mut(&mut self, x: usize, y: usize) -> &mut (RuleCellFrom, RuleCellTo) { assert!(x < self.width || y < self.height); &mut self.contents[x + self.width * y] } - fn set_both(&mut self, x: usize, y: usize, cells: (Option, Option)) { + fn set_both(&mut self, x: usize, y: usize, cells: (RuleCellFrom, RuleCellTo)) { if x < self.width && y < self.height { self.contents[x + self.width * y] = cells; } } - fn set_from(&mut self, x: usize, y: usize, cell: Option) { + fn set_from(&mut self, x: usize, y: usize, cell: RuleCellFrom) { if x < self.width && y < self.height { self.contents[x + self.width * y].0 = cell; } } - fn set_to(&mut self, x: usize, y: usize, cell: Option) { + fn set_to(&mut self, x: usize, y: usize, cell: RuleCellTo) { if x < self.width && y < self.height { self.contents[x + self.width * y].1 = cell; } @@ -99,20 +124,20 @@ impl Rule { } } - pub fn get(&self, x: usize, y: usize) -> (Option, Option) { + pub fn get(&self, x: usize, y: usize) -> (RuleCellFrom, RuleCellTo) { self.base.get(x, y) } - pub fn get_mut(&mut self, x: usize, y: usize) -> &mut (Option, Option) { + pub fn get_mut(&mut self, x: usize, y: usize) -> &mut (RuleCellFrom, RuleCellTo) { self.base.get_mut(x, y) } - pub fn set_from(&mut self, x: usize, y: usize, cell: Option) { + pub fn set_from(&mut self, x: usize, y: usize, cell: RuleCellFrom) { self.base.set_from(x, y, cell); self.generate_variants(); } - pub fn set_to(&mut self, x: usize, y: usize, cell: Option) { + pub fn set_to(&mut self, x: usize, y: usize, cell: RuleCellTo) { self.base.set_to(x, y, cell); self.generate_variants(); } @@ -133,7 +158,7 @@ impl Rule { if new_width < 1 || new_height < 1 { return; } - let mut new_contents = vec![(None, None); new_width * new_height]; + let mut new_contents = vec![Default::default(); new_width * new_height]; for nx in 0..new_width { let oldx = nx.wrapping_add_signed(dx); @@ -256,8 +281,8 @@ impl Dish { width: 1, height: 2, contents: vec![ - (Some(Cell(1)), Some(Cell(0))), - (Some(Cell(0)), Some(Cell(1))), + (RuleCellFrom::One(Cell(1)), RuleCellTo::One(Cell(0))), + (RuleCellFrom::One(Cell(0)), RuleCellTo::One(Cell(1))), ], }, ..Rule::new() @@ -268,10 +293,10 @@ impl Dish { width: 2, height: 2, contents: vec![ - (Some(Cell(1)), Some(Cell(0))), - (None, None), - (Some(Cell(1)), None), - (Some(Cell(0)), Some(Cell(1))), + (RuleCellFrom::One(Cell(1)), RuleCellTo::One(Cell(0))), + (RuleCellFrom::Any, RuleCellTo::None), + (RuleCellFrom::One(Cell(1)), RuleCellTo::None), + (RuleCellFrom::One(Cell(0)), RuleCellTo::One(Cell(1))), ], }, flip_h: true, @@ -286,6 +311,7 @@ impl Dish { Self { chunk: Chunk::new().fill_random(), rules: default_rules, + cell_groups: vec![vec![Cell(0), Cell(1)]], } } @@ -333,12 +359,32 @@ impl Dish { let width = variant.width; let height = variant.height; + let mut old_state = Vec::new(); + for dy in 0..height { + for dx in 0..width { + old_state.push(self.get_cell(dx, dy).unwrap()); + } + } + for dx in 0..width { for dy in 0..height { - let x = x + dx; - let y = y + dy; - if let Some(rule_cell) = variant.get(dx, dy).1 { - self.set_cell(x, y, rule_cell.clone()); + let px = x + dx; + let py = y + dy; + match variant.get(dx, dy).1 { + RuleCellTo::One(rule_cell) => { + self.set_cell(px, py, rule_cell.clone()); + } + RuleCellTo::GroupRandom(group_id) => { + let group = &self.cell_groups[group_id]; + let i = random::() % group.len(); + let cell = group[i]; + self.set_cell(px, py, cell); + } + RuleCellTo::Copy(index) => { + let cell = old_state[index]; + self.set_cell(px, py, cell); + } + RuleCellTo::None => (), } } } @@ -349,10 +395,21 @@ impl Dish { for dy in 0..subrule.height { let x = x + dx; let y = y + dy; - if let Some(rule_cell) = subrule.get(dx, dy).0 { - if self.get_cell(x, y) != Some(rule_cell) { - return false; + let Some(cell) = self.get_cell(x, y) else { + return false; + }; + match subrule.get(dx, dy).0 { + RuleCellFrom::One(rule_cell) => { + if cell != rule_cell { + return false; + } } + RuleCellFrom::Group(group_id) => { + if !self.cell_groups[group_id].contains(&cell) { + return false; + } + } + RuleCellFrom::Any => (), } } } diff --git a/uscope/src/main.rs b/uscope/src/main.rs index c40866d..6a75ab3 100644 --- a/uscope/src/main.rs +++ b/uscope/src/main.rs @@ -10,11 +10,15 @@ use eframe::{ epaint::Hsva, NativeOptions, }; +use egui::{ + menu::{SubMenu, SubMenuButton}, + popup, Layout, PointerButton, +}; use native_dialog::FileDialog; use rand::prelude::*; use serde::{Deserialize, Serialize}; -use petri::{Cell, Chunk, Dish, Rule, CHUNK_SIZE}; +use petri::{Cell, Chunk, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE}; use serde_json::{json, Value}; fn main() { @@ -45,7 +49,7 @@ impl UScope { fn new(_cc: &eframe::CreationContext<'_>) -> Self { Self { dish: Dish::new(), - speed: 250, + speed: 500, show_grid: false, brush: Cell(1), cell_types: vec![ @@ -134,12 +138,35 @@ impl eframe::App for UScope { } ui.separator(); + ui.heading("Groups"); + for group in &mut self.dish.cell_groups { + let (rect, _response) = ui.allocate_exact_size(Vec2::splat(CSIZE), Sense::click()); + draw_group(ui, rect, group, &self.cell_types); + ui.menu_button("edit", |ui| { + for (i, celldata) in self.cell_types.iter().enumerate() { + let mut included = group.contains(&Cell(i as u16)); + if ui.checkbox(&mut included, &celldata.name).changed() { + if included { + group.push(Cell(i as u16)); + } else { + group.retain(|c| c.0 != i as u16); + } + } + } + }); + // if response.clicked(){ + // } + } + if ui.button("add group").clicked() { + self.dish.cell_groups.push(Vec::new()); + } + ui.heading("Rules"); ScrollArea::vertical().show(ui, |ui| { let mut to_remove = None; for (i, rule) in self.dish.rules.iter_mut().enumerate() { ui.separator(); - rule_editor(ui, rule, &self.cell_types); + rule_editor(ui, rule, &self.cell_types, &self.dish.cell_groups); if ui.button("delete").clicked() { to_remove = Some(i); } @@ -189,7 +216,7 @@ fn paint_chunk(painter: Painter, chunk: &Chunk, cells: &[CellData], grid: bool) const CSIZE: f32 = 24.; const OUTLINE: (f32, Color32) = (2., Color32::GRAY); -fn rule_editor(ui: &mut Ui, rule: &mut Rule, cells: &[CellData]) { +fn rule_editor(ui: &mut Ui, rule: &mut Rule, cells: &[CellData], groups: &[Vec]) { ui.checkbox(&mut rule.enabled, "enable rule"); ui.horizontal(|ui| { ui.label("flip"); @@ -227,8 +254,10 @@ fn rule_editor(ui: &mut Ui, rule: &mut Rule, cells: &[CellData]) { for x in 0..cells_x { for y in 0..cells_y { let (left, right) = rule.get_mut(x, y); - let changed_left = rule_cell_edit(ui, from_cells_rect.min, left, x, y, cells); - let changed_right = rule_cell_edit(ui, to_cells_rect.min, right, x, y, cells); + let changed_left = + rule_cell_edit_from(ui, from_cells_rect.min, left, x, y, cells, groups); + let changed_right = + rule_cell_edit_to(ui, to_cells_rect.min, right, x, y, cells, groups); if changed_left || changed_right { rule.generate_variants(); } @@ -297,13 +326,14 @@ fn rule_editor(ui: &mut Ui, rule: &mut Rule, cells: &[CellData]) { } } -fn rule_cell_edit( +fn rule_cell_edit_from( ui: &mut Ui, origin: Pos2, - rule: &mut Option, + rule: &mut RuleCellFrom, x: usize, y: usize, cells: &[CellData], + groups: &[Vec], ) -> bool { let mut changed = false; let rect = Rect::from_min_size( @@ -311,24 +341,134 @@ fn rule_cell_edit( Vec2::splat(CSIZE), ); let aabb = ui.allocate_rect(rect, Sense::click()); - if let Some(cell) = rule { - let color = cells[cell.id()].color; - ui.painter() - .rect(rect.shrink(OUTLINE.0 / 2.), 0., color, OUTLINE); - if aabb.clicked() { - changed = true; - cell.0 += 1; - if cell.0 as usize == cells.len() { - *rule = None; + let cycle_colors = aabb.clicked_by(PointerButton::Primary); + let switch_type = aabb.clicked_by(PointerButton::Secondary); + + // draw + match rule { + RuleCellFrom::Any => (), + RuleCellFrom::One(cell) => { + let color = cells[cell.id()].color; + ui.painter() + .rect(rect.shrink(OUTLINE.0 / 2.), 0., color, OUTLINE); + } + RuleCellFrom::Group(group_id) => { + let group = &groups[*group_id]; + draw_group(ui, rect, group, cells); + } + } + // update + if cycle_colors { + match rule { + RuleCellFrom::Any => (), + RuleCellFrom::One(cell) => { + cell.0 += 1; + cell.0 %= cells.len() as u16; + changed = true; + } + RuleCellFrom::Group(group_id) => { + *group_id += 1; + *group_id %= groups.len(); + changed = true; } } - } else if aabb.clicked() { - *rule = Some(Cell(0)); + } + if switch_type { changed = true; + match rule { + RuleCellFrom::Any => { + *rule = RuleCellFrom::One(Cell(0)); + } + RuleCellFrom::One(_) => { + *rule = RuleCellFrom::Group(0); + } + RuleCellFrom::Group(_) => { + *rule = RuleCellFrom::Any; + } + } } changed } +fn rule_cell_edit_to( + ui: &mut Ui, + origin: Pos2, + rule: &mut RuleCellTo, + x: usize, + y: usize, + cells: &[CellData], + groups: &[Vec], +) -> bool { + let mut changed = false; + let rect = Rect::from_min_size( + origin + Vec2::from((x as f32, y as f32)) * CSIZE, + Vec2::splat(CSIZE), + ); + let aabb = ui.allocate_rect(rect, Sense::click()); + let cycle_colors = aabb.clicked_by(PointerButton::Primary); + let switch_type = aabb.clicked_by(PointerButton::Secondary); + + // draw + match rule { + RuleCellTo::None => (), + RuleCellTo::One(cell) => { + let color = cells[cell.id()].color; + ui.painter() + .rect(rect.shrink(OUTLINE.0 / 2.), 0., color, OUTLINE); + } + RuleCellTo::GroupRandom(group_id) => { + let group = &groups[*group_id]; + draw_group(ui, rect, group, cells); + } + RuleCellTo::Copy(_) => todo!(), + } + + if cycle_colors { + match rule { + RuleCellTo::None => (), + RuleCellTo::One(cell) => { + cell.0 += 1; + cell.0 %= cells.len() as u16; + changed = true; + } + RuleCellTo::GroupRandom(group_id) => { + *group_id += 1; + *group_id %= groups.len(); + changed = true; + } + RuleCellTo::Copy(_) => todo!(), + } + } + + if switch_type { + changed = true; + match rule { + RuleCellTo::None => { + *rule = RuleCellTo::One(Cell(0)); + } + RuleCellTo::One(_) => { + *rule = RuleCellTo::GroupRandom(0); + } + RuleCellTo::GroupRandom(_) => { + *rule = RuleCellTo::None; + } + RuleCellTo::Copy(_) => todo!(), + } + } + changed +} + +fn draw_group(ui: &mut Ui, rect: Rect, group: &[Cell], cells: &[CellData]) { + let group_size = group.len(); + let radius_per_color = (CSIZE * 0.7) / (group_size as f32); + for (i, cell) in group.iter().enumerate() { + let color = cells[cell.id()].color; + let radius = radius_per_color * ((group_size - i) as f32); + ui.painter_at(rect) + .circle_filled(rect.center(), radius, color); + } +} + impl CellData { fn new(name: &str, r: u8, g: u8, b: u8) -> Self { Self {