Compare commits
3 commits
d1d033565e
...
438efffaf3
Author | SHA1 | Date | |
---|---|---|---|
438efffaf3 | |||
3cadbb0984 | |||
0611be2837 |
2 changed files with 355 additions and 57 deletions
358
petri/src/lib.rs
358
petri/src/lib.rs
|
@ -1,3 +1,5 @@
|
|||
use std::ops::Not;
|
||||
|
||||
use rand::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
|
@ -9,10 +11,25 @@ pub struct Cell(pub u16);
|
|||
#[derive(Debug, Serialize, Deserialize)]
|
||||
pub struct Dish {
|
||||
#[serde(skip)]
|
||||
pub chunk: Chunk,
|
||||
pub rules: Vec<Rule>,
|
||||
world: World,
|
||||
pub rules: Vec<Rule>, // todo make read-only to ensure cache is updated
|
||||
pub types: Vec<CellData>,
|
||||
pub groups: Vec<CellGroup>,
|
||||
pub groups: Vec<CellGroup>, // todo make read-only to ensure cache is updated
|
||||
#[serde(skip)]
|
||||
cache: Vec<RuleCache>,
|
||||
#[serde(skip)]
|
||||
match_cache: Vec<usize>,
|
||||
#[serde(skip)]
|
||||
max_rule_width: usize,
|
||||
#[serde(skip)]
|
||||
max_rule_height: usize,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
struct RuleCache {
|
||||
rule: usize,
|
||||
variant: usize,
|
||||
matches: Vec<(isize, isize)>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||
|
@ -29,10 +46,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)]
|
||||
|
@ -170,6 +192,22 @@ impl Rule {
|
|||
self.base.width
|
||||
}
|
||||
|
||||
fn max_width(&self) -> usize {
|
||||
self.variants
|
||||
.iter()
|
||||
.map(|r| r.width)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn max_height(&self) -> usize {
|
||||
self.variants
|
||||
.iter()
|
||||
.map(|r| r.height)
|
||||
.max()
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
pub fn resize(&mut self, params: ResizeParam) {
|
||||
let (dw, dh, dx, dy) = params;
|
||||
|
||||
|
@ -289,7 +327,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::<u8>() % 4 == 0 {
|
||||
|
@ -300,7 +342,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]
|
||||
}
|
||||
|
||||
|
@ -347,8 +389,10 @@ impl Dish {
|
|||
rule.generate_variants()
|
||||
}
|
||||
|
||||
Self {
|
||||
chunk: Chunk::new().fill_random(),
|
||||
let mut new = Self {
|
||||
world: World {
|
||||
chunk: Chunk::new().with_random_ones(),
|
||||
},
|
||||
rules: default_rules,
|
||||
types: vec![
|
||||
CellData::new("air", 0, 0, 0),
|
||||
|
@ -359,13 +403,233 @@ impl Dish {
|
|||
void: true,
|
||||
cells: vec![Cell(0)],
|
||||
}],
|
||||
cache: Vec::new(),
|
||||
match_cache: Vec::new(),
|
||||
max_rule_height: 1,
|
||||
max_rule_width: 1,
|
||||
};
|
||||
new.update_all_rules();
|
||||
new
|
||||
}
|
||||
|
||||
pub fn fill(&mut self, cell: Cell) {
|
||||
self.world.fill(cell);
|
||||
self.rebuild_cache();
|
||||
}
|
||||
|
||||
pub fn update_all_rules(&mut self) {
|
||||
self.max_rule_height = 1;
|
||||
self.max_rule_width = 1;
|
||||
for rule in &mut self.rules {
|
||||
rule.generate_variants();
|
||||
self.max_rule_height = self.max_rule_height.max(rule.max_height());
|
||||
self.max_rule_width = self.max_rule_width.max(rule.max_width());
|
||||
}
|
||||
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);
|
||||
self.update_match_cache();
|
||||
}
|
||||
|
||||
/// 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));
|
||||
}
|
||||
}
|
||||
}
|
||||
self.cache.push(RuleCache {
|
||||
rule: rule_index,
|
||||
variant: variant_index,
|
||||
matches,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
pub fn update_rules(&mut self) {
|
||||
for rule in &mut self.rules {
|
||||
rule.generate_variants();
|
||||
pub fn rebuild_cache(&mut self) {
|
||||
println!("rebuilding cache");
|
||||
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();
|
||||
}
|
||||
|
||||
/// picks a random match from any rule with at least one match
|
||||
pub fn apply_one_match(&mut self) {
|
||||
if self.match_cache.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = random::<usize>() % self.match_cache.len();
|
||||
let i = self.match_cache[i];
|
||||
let rule_cache = &self.cache[i];
|
||||
let match_pos_index = random::<usize>() % 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);
|
||||
}
|
||||
|
||||
/// Picks a random point and applies a random match at that position, if any exist.
|
||||
/// The random point can be outside the world bounds, to catch cases where the root (top-left) of a match is outside the bounds.
|
||||
/// TODO make sure max_rule_[width/height] is up to date after each rule.generate_variants
|
||||
pub fn fire_blindly_cached(&mut self) {
|
||||
let border_x = self.max_rule_width - 1;
|
||||
let border_y = self.max_rule_height - 1;
|
||||
let x = ((random::<usize>() % (CHUNK_SIZE + border_x)) as isize)
|
||||
.wrapping_sub_unsigned(border_x);
|
||||
let y = ((random::<usize>() % (CHUNK_SIZE + border_y)) as isize)
|
||||
.wrapping_sub_unsigned(border_y);
|
||||
|
||||
let matches = self.get_matches_at_point(x, y);
|
||||
if matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = random::<usize>() % matches.len();
|
||||
let (rule_index, variant_index) = matches[i];
|
||||
self.apply_rule(x, y, rule_index, variant_index);
|
||||
let rule = &self.rules[rule_index].variants[variant_index];
|
||||
let width = rule.width;
|
||||
let height = rule.height;
|
||||
self.update_cache(x, y, width, height);
|
||||
}
|
||||
|
||||
/// Picks a random point and applies a random match that overlaps with it, if any exist.
|
||||
/// TODO benchmark and only keep one of try_one_position_overlapped and fire_blindly_cached
|
||||
pub fn try_one_position_overlapped(&mut self) {
|
||||
let x = (random::<usize>() % CHUNK_SIZE) as isize;
|
||||
let y = (random::<usize>() % CHUNK_SIZE) as isize;
|
||||
|
||||
let matches = self.get_matches_containing_point(x, y);
|
||||
if matches.is_empty() {
|
||||
return;
|
||||
}
|
||||
let i = random::<usize>() % matches.len();
|
||||
let (x, y, rule_index, variant_index) = matches[i];
|
||||
self.apply_rule(x, y, rule_index, variant_index);
|
||||
let rule = &self.rules[rule_index].variants[variant_index];
|
||||
let width = rule.width;
|
||||
let height = rule.height;
|
||||
self.update_cache(x, y, width, height);
|
||||
}
|
||||
|
||||
fn get_matches_at_point(&self, x: isize, y: isize) -> Vec<(usize, usize)> {
|
||||
self.cache
|
||||
.iter()
|
||||
.flat_map(|rule| {
|
||||
rule.matches.iter().filter_map(|&(mx, my)| {
|
||||
(mx == x && my == y).then_some((rule.rule, rule.variant))
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn get_matches_containing_point(
|
||||
&self,
|
||||
x: isize,
|
||||
y: isize,
|
||||
) -> Vec<(isize, isize, usize, usize)> {
|
||||
fn contains((x, y, w, h): (isize, isize, usize, usize), (px, py): (isize, isize)) -> bool {
|
||||
px >= x
|
||||
&& py >= y && px < x.saturating_add_unsigned(w)
|
||||
&& py < y.saturating_add_unsigned(h)
|
||||
}
|
||||
self.cache
|
||||
.iter()
|
||||
.flat_map(|rule| {
|
||||
let variant = &self.rules[rule.rule].variants[rule.variant];
|
||||
let (w, h) = (variant.width, variant.height);
|
||||
rule.matches.iter().filter_map(move |&(mx, my)| {
|
||||
if contains((mx, my, w, h), (x, y)) {
|
||||
Some((mx, my, rule.rule, rule.variant))
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub fn fire_blindly(&mut self) {
|
||||
|
@ -381,12 +645,8 @@ impl Dish {
|
|||
if enabled_rules.is_empty() {
|
||||
return;
|
||||
}
|
||||
let rule = random::<usize>() % 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::<usize>() % enabled_rules.len();
|
||||
let rule_index = enabled_rules[enabled_rule_index];
|
||||
let rule = &self.rules[rule_index];
|
||||
let variant_index = random::<usize>() % rule.variants.len();
|
||||
let variant = &rule.variants[variant_index].clone();
|
||||
|
@ -397,17 +657,23 @@ impl Dish {
|
|||
let y = ((random::<usize>() % (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 != 0 && rule.failrate > random() {
|
||||
// TODO don't update cache after this
|
||||
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 +702,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 +718,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<Cell> {
|
||||
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<Cell> {
|
||||
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 +759,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 +774,6 @@ impl Dish {
|
|||
}
|
||||
true
|
||||
}
|
||||
|
||||
//todo isize
|
||||
pub fn get_cell(&self, x: usize, y: usize) -> Option<Cell> {
|
||||
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 {
|
||||
|
|
|
@ -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(
|
||||
|
@ -29,7 +29,7 @@ fn main() {
|
|||
struct UScope {
|
||||
dish: Dish,
|
||||
brush: Cell,
|
||||
speed: usize,
|
||||
speed: u32,
|
||||
show_grid: bool,
|
||||
}
|
||||
|
||||
|
@ -37,7 +37,7 @@ impl UScope {
|
|||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
Self {
|
||||
dish: Dish::new(),
|
||||
speed: 500,
|
||||
speed: 50,
|
||||
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.try_one_position_overlapped();
|
||||
}
|
||||
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..=500).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<usize>,
|
||||
to_clone: &mut Option<usize>,
|
||||
) {
|
||||
) -> 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(
|
||||
|
|
Loading…
Reference in a new issue