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 rand::prelude::*;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
@ -9,10 +11,25 @@ pub struct Cell(pub u16);
|
||||||
#[derive(Debug, Serialize, Deserialize)]
|
#[derive(Debug, Serialize, Deserialize)]
|
||||||
pub struct Dish {
|
pub struct Dish {
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
pub chunk: Chunk,
|
world: World,
|
||||||
pub rules: Vec<Rule>,
|
pub rules: Vec<Rule>, // todo make read-only to ensure cache is updated
|
||||||
pub types: Vec<CellData>,
|
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)]
|
#[derive(Debug, Default, Serialize, Deserialize)]
|
||||||
|
@ -29,10 +46,15 @@ pub struct CellData {
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Debug, Default)]
|
#[derive(Debug, Default)]
|
||||||
pub struct Chunk {
|
struct Chunk {
|
||||||
pub contents: Box<[[Cell; CHUNK_SIZE]; CHUNK_SIZE]>,
|
pub contents: Box<[[Cell; CHUNK_SIZE]; CHUNK_SIZE]>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Default)]
|
||||||
|
struct World {
|
||||||
|
chunk: Chunk,
|
||||||
|
}
|
||||||
|
|
||||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||||
pub struct Rule {
|
pub struct Rule {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
|
@ -170,6 +192,22 @@ impl Rule {
|
||||||
self.base.width
|
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) {
|
pub fn resize(&mut self, params: ResizeParam) {
|
||||||
let (dw, dh, dx, dy) = params;
|
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 col in self.contents.iter_mut() {
|
||||||
for cell in col.iter_mut() {
|
for cell in col.iter_mut() {
|
||||||
if random::<u8>() % 4 == 0 {
|
if random::<u8>() % 4 == 0 {
|
||||||
|
@ -300,7 +342,7 @@ impl Chunk {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn get_cell(&self, x: usize, y: usize) -> Cell {
|
fn get_cell(&self, x: usize, y: usize) -> Cell {
|
||||||
self.contents[x][y]
|
self.contents[x][y]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -347,8 +389,10 @@ impl Dish {
|
||||||
rule.generate_variants()
|
rule.generate_variants()
|
||||||
}
|
}
|
||||||
|
|
||||||
Self {
|
let mut new = Self {
|
||||||
chunk: Chunk::new().fill_random(),
|
world: World {
|
||||||
|
chunk: Chunk::new().with_random_ones(),
|
||||||
|
},
|
||||||
rules: default_rules,
|
rules: default_rules,
|
||||||
types: vec![
|
types: vec![
|
||||||
CellData::new("air", 0, 0, 0),
|
CellData::new("air", 0, 0, 0),
|
||||||
|
@ -359,13 +403,233 @@ impl Dish {
|
||||||
void: true,
|
void: true,
|
||||||
cells: vec![Cell(0)],
|
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) {
|
pub fn rebuild_cache(&mut self) {
|
||||||
for rule in &mut self.rules {
|
println!("rebuilding cache");
|
||||||
rule.generate_variants();
|
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) {
|
pub fn fire_blindly(&mut self) {
|
||||||
|
@ -381,12 +645,8 @@ impl Dish {
|
||||||
if enabled_rules.is_empty() {
|
if enabled_rules.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let rule = random::<usize>() % enabled_rules.len();
|
let enabled_rule_index = random::<usize>() % enabled_rules.len();
|
||||||
let rule = enabled_rules[rule];
|
let rule_index = enabled_rules[enabled_rule_index];
|
||||||
self.fire_rule(rule);
|
|
||||||
}
|
|
||||||
|
|
||||||
fn fire_rule(&mut self, rule_index: usize) {
|
|
||||||
let rule = &self.rules[rule_index];
|
let rule = &self.rules[rule_index];
|
||||||
let variant_index = random::<usize>() % rule.variants.len();
|
let variant_index = random::<usize>() % rule.variants.len();
|
||||||
let variant = &rule.variants[variant_index].clone();
|
let variant = &rule.variants[variant_index].clone();
|
||||||
|
@ -397,17 +657,23 @@ impl Dish {
|
||||||
let y = ((random::<usize>() % (CHUNK_SIZE + border_y)) as isize)
|
let y = ((random::<usize>() % (CHUNK_SIZE + border_y)) as isize)
|
||||||
.wrapping_sub_unsigned(border_y);
|
.wrapping_sub_unsigned(border_y);
|
||||||
|
|
||||||
if !self.subrule_matches(x, y, variant) {
|
if self.world.subrule_matches(x, y, variant, &self.groups) {
|
||||||
return;
|
self.apply_rule(x, y, rule_index, variant_index);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let fail: u8 = random();
|
fn apply_rule(&mut self, x: isize, y: isize, rule_index: usize, variant_index: usize) {
|
||||||
if rule.failrate > fail {
|
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;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let width = variant.width;
|
let width = variant.width;
|
||||||
let height = variant.height;
|
let height = variant.height;
|
||||||
|
|
||||||
let mut old_state = Vec::new();
|
let mut old_state = Vec::new();
|
||||||
for dy in 0..height {
|
for dy in 0..height {
|
||||||
for dx in 0..width {
|
for dx in 0..width {
|
||||||
|
@ -436,6 +702,7 @@ impl Dish {
|
||||||
RuleCellTo::Copy(x, y) => {
|
RuleCellTo::Copy(x, y) => {
|
||||||
let index = x + y * variant.width;
|
let index = x + y * variant.width;
|
||||||
if index >= old_state.len() {
|
if index >= old_state.len() {
|
||||||
|
// TODO sanitize the rules somewhere else and remove this bounds check
|
||||||
// the copy source is outside the rule bounds
|
// the copy source is outside the rule bounds
|
||||||
continue;
|
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 dx in 0..subrule.width {
|
||||||
for dy in 0..subrule.height {
|
for dy in 0..subrule.height {
|
||||||
let x = x.wrapping_add_unsigned(dx) as usize;
|
let x = x.wrapping_add_unsigned(dx) as usize;
|
||||||
|
@ -464,7 +759,7 @@ impl Dish {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
RuleCellFrom::Group(group_id) => {
|
RuleCellFrom::Group(group_id) => {
|
||||||
let group = &self.groups[group_id];
|
let group = &groups[group_id];
|
||||||
if let Some(cell) = cell {
|
if let Some(cell) = cell {
|
||||||
if !group.cells.contains(&cell) {
|
if !group.cells.contains(&cell) {
|
||||||
return false;
|
return false;
|
||||||
|
@ -479,23 +774,6 @@ impl Dish {
|
||||||
}
|
}
|
||||||
true
|
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 {
|
impl Cell {
|
||||||
|
|
|
@ -14,7 +14,7 @@ use egui::{collapsing_header::CollapsingState, DragValue, PointerButton};
|
||||||
use native_dialog::FileDialog;
|
use native_dialog::FileDialog;
|
||||||
use rand::prelude::*;
|
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() {
|
fn main() {
|
||||||
eframe::run_native(
|
eframe::run_native(
|
||||||
|
@ -29,7 +29,7 @@ fn main() {
|
||||||
struct UScope {
|
struct UScope {
|
||||||
dish: Dish,
|
dish: Dish,
|
||||||
brush: Cell,
|
brush: Cell,
|
||||||
speed: usize,
|
speed: u32,
|
||||||
show_grid: bool,
|
show_grid: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,7 +37,7 @@ impl UScope {
|
||||||
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||||
Self {
|
Self {
|
||||||
dish: Dish::new(),
|
dish: Dish::new(),
|
||||||
speed: 500,
|
speed: 50,
|
||||||
show_grid: false,
|
show_grid: false,
|
||||||
brush: Cell(1),
|
brush: Cell(1),
|
||||||
}
|
}
|
||||||
|
@ -65,7 +65,7 @@ impl UScope {
|
||||||
// TODO: show errors to user
|
// TODO: show errors to user
|
||||||
let s = fs::read_to_string(path).unwrap();
|
let s = fs::read_to_string(path).unwrap();
|
||||||
self.dish = serde_json::from_str(&s).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) {
|
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
|
||||||
ctx.request_repaint();
|
ctx.request_repaint();
|
||||||
for _ in 0..self.speed {
|
for _ in 0..self.speed {
|
||||||
self.dish.fire_blindly();
|
self.dish.try_one_position_overlapped();
|
||||||
}
|
}
|
||||||
SidePanel::left("left_panel")
|
SidePanel::left("left_panel")
|
||||||
.min_width(100.)
|
.min_width(100.)
|
||||||
.show(ctx, |ui| {
|
.show(ctx, |ui| {
|
||||||
ui.heading("Simulation");
|
ui.heading("Simulation");
|
||||||
ui.label("speed");
|
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");
|
ui.checkbox(&mut self.show_grid, "show grid");
|
||||||
|
if ui.button("regenerate rules and cache").clicked() {
|
||||||
|
self.dish.update_all_rules();
|
||||||
|
}
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.button("Save").clicked() {
|
if ui.button("Save").clicked() {
|
||||||
self.save_universe();
|
self.save_universe();
|
||||||
|
@ -113,7 +116,7 @@ impl eframe::App for UScope {
|
||||||
self.dish.types.push(CellData { name, color })
|
self.dish.types.push(CellData { name, color })
|
||||||
}
|
}
|
||||||
if ui.button("fill").clicked() {
|
if ui.button("fill").clicked() {
|
||||||
self.dish.chunk.contents.fill([self.brush; CHUNK_SIZE]);
|
self.dish.fill(self.brush);
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
|
|
||||||
|
@ -147,8 +150,9 @@ impl eframe::App for UScope {
|
||||||
|
|
||||||
let mut to_remove = None;
|
let mut to_remove = None;
|
||||||
let mut to_clone = None;
|
let mut to_clone = None;
|
||||||
|
let mut to_update = None;
|
||||||
for (i, rule) in self.dish.rules.iter_mut().enumerate() {
|
for (i, rule) in self.dish.rules.iter_mut().enumerate() {
|
||||||
rule_editor(
|
let changed = rule_editor(
|
||||||
ui,
|
ui,
|
||||||
rule,
|
rule,
|
||||||
i,
|
i,
|
||||||
|
@ -157,25 +161,35 @@ impl eframe::App for UScope {
|
||||||
&mut to_remove,
|
&mut to_remove,
|
||||||
&mut to_clone,
|
&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 {
|
if let Some(i) = to_remove {
|
||||||
self.dish.rules.remove(i);
|
self.dish.rules.remove(i);
|
||||||
|
self.dish.rebuild_cache();
|
||||||
}
|
}
|
||||||
if let Some(i) = to_clone {
|
if let Some(i) = to_clone {
|
||||||
let mut new_rule = self.dish.rules[i].clone();
|
let mut new_rule = self.dish.rules[i].clone();
|
||||||
new_rule.enabled = false;
|
new_rule.enabled = false;
|
||||||
self.dish.rules.push(new_rule);
|
self.dish.rules.push(new_rule);
|
||||||
|
self.dish.cache_last_added_rule();
|
||||||
}
|
}
|
||||||
ui.separator();
|
ui.separator();
|
||||||
if ui.button("add rule").clicked() {
|
if ui.button("add rule").clicked() {
|
||||||
self.dish.rules.push(Rule::new());
|
self.dish.rules.push(Rule::new());
|
||||||
|
self.dish.cache_last_added_rule()
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
CentralPanel::default().show(ctx, |ui| {
|
CentralPanel::default().show(ctx, |ui| {
|
||||||
let bounds = ui.available_rect_before_wrap();
|
let bounds = ui.available_rect_before_wrap();
|
||||||
let painter = ui.painter_at(bounds);
|
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());
|
let rect = ui.allocate_rect(bounds, Sense::click_and_drag());
|
||||||
if let Some(pos) = rect.interact_pointer_pos() {
|
if let Some(pos) = rect.interact_pointer_pos() {
|
||||||
|
@ -189,6 +203,7 @@ impl eframe::App for UScope {
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
self.dish.set_cell(x, y, self.brush);
|
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.;
|
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();
|
let bounds = painter.clip_rect();
|
||||||
for x in 0..CHUNK_SIZE {
|
for x in 0..CHUNK_SIZE {
|
||||||
for y 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 corner = bounds.min + (Vec2::from((x as f32, y as f32)) * GRID_SIZE);
|
||||||
let rect = Rect::from_min_size(corner, Vec2::splat(GRID_SIZE));
|
let rect = Rect::from_min_size(corner, Vec2::splat(GRID_SIZE));
|
||||||
if cell.id() >= cells.len() {
|
if cell.id() >= cells.len() {
|
||||||
|
@ -229,11 +245,14 @@ fn rule_editor(
|
||||||
groups: &[CellGroup],
|
groups: &[CellGroup],
|
||||||
to_remove: &mut Option<usize>,
|
to_remove: &mut Option<usize>,
|
||||||
to_clone: &mut Option<usize>,
|
to_clone: &mut Option<usize>,
|
||||||
) {
|
) -> bool {
|
||||||
|
let mut changed = false;
|
||||||
let id = ui.make_persistent_id(format!("rule {index}"));
|
let id = ui.make_persistent_id(format!("rule {index}"));
|
||||||
CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
CollapsingState::load_with_default_open(ui.ctx(), id, true)
|
||||||
.show_header(ui, |ui| {
|
.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() {
|
if ui.button("delete").clicked() {
|
||||||
*to_remove = Some(index);
|
*to_remove = Some(index);
|
||||||
}
|
}
|
||||||
|
@ -245,13 +264,13 @@ fn rule_editor(
|
||||||
ui.text_edit_singleline(&mut rule.name);
|
ui.text_edit_singleline(&mut rule.name);
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
if ui.checkbox(&mut rule.flip_x, "flip X").changed() {
|
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() {
|
if ui.checkbox(&mut rule.flip_y, "flip Y").changed() {
|
||||||
rule.generate_variants();
|
changed = true;
|
||||||
}
|
}
|
||||||
if ui.checkbox(&mut rule.rotate, "rotate").changed() {
|
if ui.checkbox(&mut rule.rotate, "rotate").changed() {
|
||||||
rule.generate_variants();
|
changed = true;
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
ui.horizontal(|ui| {
|
ui.horizontal(|ui| {
|
||||||
|
@ -297,7 +316,7 @@ fn rule_editor(
|
||||||
&mut overlay_lines,
|
&mut overlay_lines,
|
||||||
);
|
);
|
||||||
if changed_left || changed_right {
|
if changed_left || changed_right {
|
||||||
rule.generate_variants();
|
changed = true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -382,6 +401,7 @@ fn rule_editor(
|
||||||
ui.painter().line_segment([a, b], stroke);
|
ui.painter().line_segment([a, b], stroke);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
changed
|
||||||
}
|
}
|
||||||
|
|
||||||
fn rule_cell_edit_from(
|
fn rule_cell_edit_from(
|
||||||
|
|
Loading…
Reference in a new issue