diff --git a/petri/src/lib.rs b/petri/src/lib.rs index fe9ede4..98e7641 100644 --- a/petri/src/lib.rs +++ b/petri/src/lib.rs @@ -1,3 +1,5 @@ +use std::ops::Not; + use rand::prelude::*; use serde::{Deserialize, Serialize}; @@ -9,10 +11,21 @@ pub struct Cell(pub u16); #[derive(Debug, Serialize, Deserialize)] pub struct Dish { #[serde(skip)] - pub chunk: Chunk, - pub rules: Vec, + world: World, + pub rules: Vec, // todo make read-only to ensure cache is updated pub types: Vec, - pub groups: Vec, + pub groups: Vec, // todo make read-only to ensure cache is updated + #[serde(skip)] + cache: Vec, + #[serde(skip)] + match_cache: Vec, +} + +#[derive(Debug)] +struct RuleCache { + rule: usize, + variant: usize, + matches: Vec<(isize, isize)>, } #[derive(Debug, Default, Serialize, Deserialize)] @@ -29,10 +42,15 @@ pub struct CellData { } #[derive(Debug, Default)] -pub struct Chunk { +struct Chunk { pub contents: Box<[[Cell; CHUNK_SIZE]; CHUNK_SIZE]>, } +#[derive(Debug, Default)] +struct World { + chunk: Chunk, +} + #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Rule { #[serde(default)] @@ -289,7 +307,11 @@ impl Chunk { } } - fn fill_random(mut self) -> Self { + fn fill(&mut self, cell: Cell) { + self.contents.fill([cell; CHUNK_SIZE]); + } + + fn with_random_ones(mut self) -> Self { for col in self.contents.iter_mut() { for cell in col.iter_mut() { if random::() % 4 == 0 { @@ -300,7 +322,7 @@ impl Chunk { self } - pub fn get_cell(&self, x: usize, y: usize) -> Cell { + fn get_cell(&self, x: usize, y: usize) -> Cell { self.contents[x][y] } @@ -348,7 +370,9 @@ impl Dish { } Self { - chunk: Chunk::new().fill_random(), + world: World { + chunk: Chunk::new().with_random_ones(), + }, rules: default_rules, types: vec![ CellData::new("air", 0, 0, 0), @@ -359,13 +383,144 @@ impl Dish { void: true, cells: vec![Cell(0)], }], + cache: Vec::new(), + match_cache: Vec::new(), } } - pub fn update_rules(&mut self) { + pub fn fill(&mut self, cell: Cell) { + self.world.fill(cell); + } + + pub fn update_all_rules(&mut self) { for rule in &mut self.rules { rule.generate_variants(); } + self.rebuild_cache(); + } + + /// run after any rule modification + pub fn update_cache_single_rule(&mut self, rule_index: usize) { + // remove old cache for this rule, since the variants may have changed + self.cache.retain(|c| c.rule != rule_index); + self.add_cache_single_rule(rule_index); + } + + /// run after adding a rule + pub fn cache_last_added_rule(&mut self) { + if self.rules.is_empty() { + return; + } + let index = self.rules.len() - 1; + self.update_cache_single_rule(index); + } + + fn add_cache_single_rule(&mut self, rule_index: usize) { + let rule = &self.rules[rule_index]; + if !rule.enabled { + return; + } + for variant_index in 0..rule.variants.len() { + let mut matches = Vec::new(); + + let rule = &rule.variants[variant_index]; + let border_x = rule.width as isize - 1; + let border_y = rule.height as isize - 1; + + for px in -border_x..(CHUNK_SIZE as isize) { + for py in -border_y..(CHUNK_SIZE as isize) { + if self.world.subrule_matches(px, py, &rule, &self.groups) { + matches.push((px, py)); + } + } + } + if !matches.is_empty() { + self.match_cache.push(self.cache.len()); + } + self.cache.push(RuleCache { + rule: rule_index, + variant: variant_index, + matches, + }); + } + } + + pub fn rebuild_cache(&mut self) { + self.cache.clear(); + for rule_index in 0..self.rules.len() { + self.add_cache_single_rule(rule_index); + } + self.update_match_cache(); + } + + pub fn update_cache(&mut self, x: isize, y: isize, width: usize, height: usize) { + fn overlap( + (x1, y1, w1, h1): (isize, isize, usize, usize), + (x2, y2, w2, h2): (isize, isize, usize, usize), + ) -> bool { + x2 < x1.saturating_add_unsigned(w1) + && x1 < x2.saturating_add_unsigned(w2) + && y2 < y1.saturating_add_unsigned(h1) + && y1 < y2.saturating_add_unsigned(h2) + } + let edited_rect = (x, y, width, height); + + for cache in &mut self.cache { + let rule = &self.rules[cache.rule].variants[cache.variant]; + let rule_width = rule.width; + let rule_height = rule.height; + + // discard all overlapping matches + let mut i = 0; + while i < cache.matches.len() { + let match_pos = cache.matches[i]; + let match_rect = (match_pos.0, match_pos.1, rule_width, rule_height); + if overlap(edited_rect, match_rect) { + cache.matches.swap_remove(i); + } else { + i += 1; + } + } + // check entire changed area and add matches + let border_x = rule_width - 1; + let border_y = rule_height - 1; + + for px in (x.wrapping_sub_unsigned(border_x))..(x.wrapping_add_unsigned(width)) { + for py in (y.wrapping_sub_unsigned(border_y))..(y.wrapping_add_unsigned(height)) { + if self.world.subrule_matches(px, py, &rule, &self.groups) { + cache.matches.push((px, py)); + } + } + } + } + self.update_match_cache(); + } + + fn update_match_cache(&mut self) { + self.match_cache = self + .cache + .iter() + .enumerate() + .filter_map(|(i, c)| c.matches.is_empty().not().then_some(i)) + .collect(); + } + + pub fn fire_once(&mut self) { + if self.match_cache.is_empty() { + return; + } + let i = random::() % self.match_cache.len(); + let i = self.match_cache[i]; + let rule_cache = &self.cache[i]; + let match_pos_index = random::() % rule_cache.matches.len(); + let (x, y) = rule_cache.matches[match_pos_index]; + + let rule = &self.rules[rule_cache.rule].variants[rule_cache.variant]; + let width = rule.width; + let height = rule.height; + + self.apply_rule(x, y, rule_cache.rule, rule_cache.variant); + self.update_cache(x, y, width, height); } pub fn fire_blindly(&mut self) { @@ -381,12 +536,8 @@ impl Dish { if enabled_rules.is_empty() { return; } - let rule = random::() % enabled_rules.len(); - let rule = enabled_rules[rule]; - self.fire_rule(rule); - } - - fn fire_rule(&mut self, rule_index: usize) { + let enabled_rule_index = random::() % enabled_rules.len(); + let rule_index = enabled_rules[enabled_rule_index]; let rule = &self.rules[rule_index]; let variant_index = random::() % rule.variants.len(); let variant = &rule.variants[variant_index].clone(); @@ -397,17 +548,22 @@ impl Dish { let y = ((random::() % (CHUNK_SIZE + border_y)) as isize) .wrapping_sub_unsigned(border_y); - if !self.subrule_matches(x, y, variant) { - return; + if self.world.subrule_matches(x, y, variant, &self.groups) { + self.apply_rule(x, y, rule_index, variant_index); } + } - let fail: u8 = random(); - if rule.failrate > fail { + fn apply_rule(&mut self, x: isize, y: isize, rule_index: usize, variant_index: usize) { + let rule = &self.rules[rule_index]; + let variant = &rule.variants[variant_index].clone(); + + if rule.failrate > random() { return; } let width = variant.width; let height = variant.height; + let mut old_state = Vec::new(); for dy in 0..height { for dx in 0..width { @@ -436,6 +592,7 @@ impl Dish { RuleCellTo::Copy(x, y) => { let index = x + y * variant.width; if index >= old_state.len() { + // TODO sanitize the rules somewhere else and remove this bounds check // the copy source is outside the rule bounds continue; } @@ -451,7 +608,35 @@ impl Dish { } } - fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule) -> bool { + //todo isize + pub fn get_cell(&self, x: usize, y: usize) -> Option { + self.world.get_cell(x, y) + } + + //todo isize + pub fn set_cell(&mut self, x: usize, y: usize, cell: Cell) { + if x >= CHUNK_SIZE || y >= CHUNK_SIZE { + return; + } + self.world.chunk.set_cell(x, y, cell); + } +} + +impl World { + fn fill(&mut self, cell: Cell) { + self.chunk.fill(cell); + } + + //todo isize + fn get_cell(&self, x: usize, y: usize) -> Option { + if x >= CHUNK_SIZE || y >= CHUNK_SIZE { + None + } else { + Some(self.chunk.get_cell(x, y)) + } + } + + fn subrule_matches(&self, x: isize, y: isize, subrule: &SubRule, groups: &[CellGroup]) -> bool { for dx in 0..subrule.width { for dy in 0..subrule.height { let x = x.wrapping_add_unsigned(dx) as usize; @@ -464,7 +649,7 @@ impl Dish { } } RuleCellFrom::Group(group_id) => { - let group = &self.groups[group_id]; + let group = &groups[group_id]; if let Some(cell) = cell { if !group.cells.contains(&cell) { return false; @@ -479,23 +664,6 @@ impl Dish { } true } - - //todo isize - pub fn get_cell(&self, x: usize, y: usize) -> Option { - if x >= CHUNK_SIZE || y >= CHUNK_SIZE { - None - } else { - Some(self.chunk.get_cell(x, y)) - } - } - - //todo isize - pub fn set_cell(&mut self, x: usize, y: usize, cell: Cell) { - if x >= CHUNK_SIZE || y >= CHUNK_SIZE { - return; - } - self.chunk.set_cell(x, y, cell) - } } impl Cell { diff --git a/uscope/src/main.rs b/uscope/src/main.rs index 6c51aa0..9e9d57a 100644 --- a/uscope/src/main.rs +++ b/uscope/src/main.rs @@ -14,7 +14,7 @@ use egui::{collapsing_header::CollapsingState, DragValue, PointerButton}; use native_dialog::FileDialog; use rand::prelude::*; -use petri::{Cell, CellData, CellGroup, Chunk, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE}; +use petri::{Cell, CellData, CellGroup, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE}; fn main() { eframe::run_native( @@ -37,7 +37,7 @@ impl UScope { fn new(_cc: &eframe::CreationContext<'_>) -> Self { Self { dish: Dish::new(), - speed: 500, + speed: 1, show_grid: false, brush: Cell(1), } @@ -65,7 +65,7 @@ impl UScope { // TODO: show errors to user let s = fs::read_to_string(path).unwrap(); self.dish = serde_json::from_str(&s).unwrap(); - self.dish.update_rules(); + self.dish.update_all_rules(); } } } @@ -74,15 +74,18 @@ impl eframe::App for UScope { fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) { ctx.request_repaint(); for _ in 0..self.speed { - self.dish.fire_blindly(); + self.dish.fire_once(); } SidePanel::left("left_panel") .min_width(100.) .show(ctx, |ui| { ui.heading("Simulation"); ui.label("speed"); - ui.add(Slider::new(&mut self.speed, 0..=5000).clamp_to_range(false)); + ui.add(Slider::new(&mut self.speed, 0..=15).clamp_to_range(false)); ui.checkbox(&mut self.show_grid, "show grid"); + if ui.button("regenerate rules and cache").clicked() { + self.dish.update_all_rules(); + } ui.horizontal(|ui| { if ui.button("Save").clicked() { self.save_universe(); @@ -113,7 +116,7 @@ impl eframe::App for UScope { self.dish.types.push(CellData { name, color }) } if ui.button("fill").clicked() { - self.dish.chunk.contents.fill([self.brush; CHUNK_SIZE]); + self.dish.fill(self.brush); } ui.separator(); @@ -147,8 +150,9 @@ impl eframe::App for UScope { let mut to_remove = None; let mut to_clone = None; + let mut to_update = None; for (i, rule) in self.dish.rules.iter_mut().enumerate() { - rule_editor( + let changed = rule_editor( ui, rule, i, @@ -157,25 +161,35 @@ impl eframe::App for UScope { &mut to_remove, &mut to_clone, ); + if changed { + rule.generate_variants(); + to_update = Some(i); + } + } + if let Some(i) = to_update { + self.dish.update_cache_single_rule(i); } if let Some(i) = to_remove { self.dish.rules.remove(i); + self.dish.rebuild_cache(); } if let Some(i) = to_clone { let mut new_rule = self.dish.rules[i].clone(); new_rule.enabled = false; self.dish.rules.push(new_rule); + self.dish.cache_last_added_rule(); } ui.separator(); if ui.button("add rule").clicked() { self.dish.rules.push(Rule::new()); + self.dish.cache_last_added_rule() } }); }); CentralPanel::default().show(ctx, |ui| { let bounds = ui.available_rect_before_wrap(); let painter = ui.painter_at(bounds); - paint_chunk(painter, &self.dish.chunk, &self.dish.types, self.show_grid); + paint_world(painter, &self.dish, self.show_grid); let rect = ui.allocate_rect(bounds, Sense::click_and_drag()); if let Some(pos) = rect.interact_pointer_pos() { @@ -189,6 +203,7 @@ impl eframe::App for UScope { } } else { self.dish.set_cell(x, y, self.brush); + self.dish.update_cache(x as isize, y as isize, 1, 1); } } }); @@ -196,11 +211,12 @@ impl eframe::App for UScope { } const GRID_SIZE: f32 = 16.; -fn paint_chunk(painter: Painter, chunk: &Chunk, cells: &[CellData], grid: bool) { +fn paint_world(painter: Painter, world: &Dish, grid: bool) { + let cells = &world.types; let bounds = painter.clip_rect(); for x in 0..CHUNK_SIZE { for y in 0..CHUNK_SIZE { - let cell = &chunk.get_cell(x, y); + let cell = &world.get_cell(x, y).unwrap(); let corner = bounds.min + (Vec2::from((x as f32, y as f32)) * GRID_SIZE); let rect = Rect::from_min_size(corner, Vec2::splat(GRID_SIZE)); if cell.id() >= cells.len() { @@ -229,11 +245,14 @@ fn rule_editor( groups: &[CellGroup], to_remove: &mut Option, to_clone: &mut Option, -) { +) -> bool { + let mut changed = false; let id = ui.make_persistent_id(format!("rule {index}")); CollapsingState::load_with_default_open(ui.ctx(), id, true) .show_header(ui, |ui| { - ui.checkbox(&mut rule.enabled, &rule.name); + if ui.checkbox(&mut rule.enabled, &rule.name).changed() { + changed = true; + } if ui.button("delete").clicked() { *to_remove = Some(index); } @@ -245,13 +264,13 @@ fn rule_editor( ui.text_edit_singleline(&mut rule.name); ui.horizontal(|ui| { if ui.checkbox(&mut rule.flip_x, "flip X").changed() { - rule.generate_variants(); + changed = true; } if ui.checkbox(&mut rule.flip_y, "flip Y").changed() { - rule.generate_variants(); + changed = true; } if ui.checkbox(&mut rule.rotate, "rotate").changed() { - rule.generate_variants(); + changed = true; } }); ui.horizontal(|ui| { @@ -297,7 +316,7 @@ fn rule_editor( &mut overlay_lines, ); if changed_left || changed_right { - rule.generate_variants(); + changed = true; } } } @@ -382,6 +401,7 @@ fn rule_editor( ui.painter().line_segment([a, b], stroke); } }); + changed } fn rule_cell_edit_from(