CrispyPin
4793bc1b8f
This also avoids the bias toward larger rules caused by checking for overlapping matches at the chosen position.
603 lines
15 KiB
Rust
603 lines
15 KiB
Rust
use std::{
|
|
fs::{self, File},
|
|
io::Write,
|
|
time::{Duration, Instant},
|
|
};
|
|
|
|
use eframe::{
|
|
egui::{
|
|
CentralPanel, Color32, Painter, Pos2, Rect, ScrollArea, Sense, SidePanel, Slider, Ui, Vec2,
|
|
},
|
|
epaint::Hsva,
|
|
NativeOptions,
|
|
};
|
|
use egui::{collapsing_header::CollapsingState, DragValue, PointerButton};
|
|
use native_dialog::FileDialog;
|
|
use rand::prelude::*;
|
|
|
|
use petri::{Cell, CellData, CellGroup, Dish, Rule, RuleCellFrom, RuleCellTo, CHUNK_SIZE};
|
|
|
|
fn main() {
|
|
eframe::run_native(
|
|
"µscope",
|
|
NativeOptions::default(),
|
|
Box::new(|_cc| Box::new(UScope::new(_cc))),
|
|
)
|
|
.unwrap();
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
struct UScope {
|
|
dish: Dish,
|
|
brush: Cell,
|
|
speed: u32,
|
|
show_grid: bool,
|
|
sim_times: Vec<Duration>,
|
|
}
|
|
|
|
impl UScope {
|
|
fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
|
Self {
|
|
dish: Dish::new(),
|
|
speed: 50,
|
|
show_grid: false,
|
|
brush: Cell(1),
|
|
// sim_times: vec![0],
|
|
sim_times: vec![Duration::from_micros(1)],
|
|
}
|
|
}
|
|
|
|
fn save_universe(&self) -> Option<()> {
|
|
if let Ok(Some(path)) = FileDialog::new()
|
|
.set_filename("universe_1.json")
|
|
.add_filter("JSON", &["json"])
|
|
.show_save_single_file()
|
|
{
|
|
let mut file = File::create(path).ok()?;
|
|
let out = serde_json::to_string(&self.dish).ok()?;
|
|
file.write_all(out.as_bytes()).ok()?;
|
|
}
|
|
Some(())
|
|
}
|
|
|
|
fn open_universe(&mut self) {
|
|
if let Ok(Some(path)) = FileDialog::new()
|
|
.set_filename("universe_1.json")
|
|
.add_filter("JSON", &["json"])
|
|
.show_open_single_file()
|
|
{
|
|
// TODO: show errors to user
|
|
let s = fs::read_to_string(path).unwrap();
|
|
self.dish = serde_json::from_str(&s).unwrap();
|
|
self.dish.update_all_rules();
|
|
}
|
|
}
|
|
}
|
|
|
|
impl eframe::App for UScope {
|
|
fn update(&mut self, ctx: &eframe::egui::Context, _frame: &mut eframe::Frame) {
|
|
ctx.request_repaint();
|
|
let sim_frame = Instant::now();
|
|
for _ in 0..self.speed {
|
|
self.dish.try_one_location();
|
|
// self.dish.apply_one_match();
|
|
}
|
|
let sim_time = sim_frame.elapsed();
|
|
// self.sim_times.push(sim_time.as_micros());
|
|
self.sim_times.push(sim_time);
|
|
if self.sim_times.len() > 60 {
|
|
self.sim_times.remove(0);
|
|
}
|
|
SidePanel::left("left_panel")
|
|
.min_width(100.)
|
|
.show(ctx, |ui| {
|
|
ui.heading("Simulation");
|
|
ui.label("speed");
|
|
ui.add(Slider::new(&mut self.speed, 0..=500).clamp_to_range(false));
|
|
ui.label(format!("sim time: {sim_time:?}"));
|
|
let avg_sim_time =
|
|
self.sim_times.iter().sum::<Duration>() / self.sim_times.len() as u32;
|
|
ui.label(format!("average sim time: {avg_sim_time:?}"));
|
|
|
|
ui.checkbox(&mut self.show_grid, "show grid");
|
|
if ui.button("regenerate rules and cache").clicked() {
|
|
self.dish.update_all_rules();
|
|
}
|
|
if ui.button("debug cache").clicked() {
|
|
self.dish.dbg_cache();
|
|
}
|
|
ui.horizontal(|ui| {
|
|
if ui.button("Save").clicked() {
|
|
self.save_universe();
|
|
}
|
|
if ui.button("Open").clicked() {
|
|
self.open_universe();
|
|
}
|
|
});
|
|
ui.separator();
|
|
|
|
ScrollArea::vertical().show(ui, |ui| {
|
|
ui.heading("Cells");
|
|
for (i, cell) in self.dish.types.iter_mut().enumerate() {
|
|
ui.horizontal(|ui| {
|
|
ui.set_width(120.);
|
|
ui.radio_value(&mut self.brush.0, i as u16, "");
|
|
ui.text_edit_singleline(&mut cell.name);
|
|
ui.color_edit_button_srgb(&mut cell.color);
|
|
});
|
|
}
|
|
|
|
if ui.button("add cell").clicked() {
|
|
let h = random::<f32>();
|
|
let s = random::<f32>() * 0.5 + 0.5;
|
|
let v = random::<f32>() * 0.5 + 0.5;
|
|
let color = Hsva::new(h, s, v, 1.).to_srgb();
|
|
let name = format!("cell #{}", self.dish.types.len());
|
|
self.dish.types.push(CellData { name, color })
|
|
}
|
|
if ui.button("fill").clicked() {
|
|
self.dish.fill(self.brush);
|
|
}
|
|
ui.separator();
|
|
|
|
ui.heading("Groups");
|
|
for group in &mut self.dish.groups {
|
|
let (rect, _response) =
|
|
ui.allocate_exact_size(Vec2::splat(CSIZE), Sense::click());
|
|
draw_group(ui, rect, group, &self.dish.types);
|
|
ui.horizontal(|ui| {
|
|
ui.menu_button("edit", |ui| {
|
|
ui.checkbox(&mut group.void, "void");
|
|
for (i, celldata) in self.dish.types.iter().enumerate() {
|
|
let mut included = group.cells.contains(&Cell(i as u16));
|
|
if ui.checkbox(&mut included, &celldata.name).changed() {
|
|
if included {
|
|
group.cells.push(Cell(i as u16));
|
|
} else {
|
|
group.cells.retain(|c| c != &Cell(i as u16));
|
|
}
|
|
}
|
|
}
|
|
});
|
|
ui.text_edit_singleline(&mut group.name);
|
|
});
|
|
}
|
|
if ui.button("add group").clicked() {
|
|
self.dish.groups.push(CellGroup::default());
|
|
}
|
|
|
|
ui.heading("Rules");
|
|
|
|
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() {
|
|
let changed = rule_editor(
|
|
ui,
|
|
rule,
|
|
i,
|
|
&self.dish.types,
|
|
&self.dish.groups,
|
|
&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_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() {
|
|
let p = ((pos - bounds.min) / GRID_SIZE).floor();
|
|
let x = p.x as usize;
|
|
let y = p.y as usize;
|
|
let pick = ui.input(|i| i.modifiers.shift);
|
|
if pick {
|
|
if let Some(clicked_cell) = self.dish.get_cell(x, y) {
|
|
self.brush = clicked_cell;
|
|
}
|
|
} else {
|
|
let old = self.dish.get_cell(x, y);
|
|
if Some(self.brush) != old {
|
|
self.dish.set_cell(x, y, self.brush);
|
|
self.dish.update_cache(x as isize, y as isize, 1, 1);
|
|
}
|
|
}
|
|
}
|
|
});
|
|
}
|
|
}
|
|
|
|
const GRID_SIZE: f32 = 16.;
|
|
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 = &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() {
|
|
continue;
|
|
}
|
|
let color = cells[cell.id()].color;
|
|
let color = Color32::from_rgb(color[0], color[1], color[2]);
|
|
if grid {
|
|
painter.rect(rect, 0., color, (1., Color32::GRAY));
|
|
} else {
|
|
painter.rect_filled(rect, 0., color);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
const CSIZE: f32 = 24.;
|
|
const RESIZE_BUTTON_WIDTH: f32 = 8.;
|
|
|
|
const OUTLINE: (f32, Color32) = (2., Color32::GRAY);
|
|
fn rule_editor(
|
|
ui: &mut Ui,
|
|
rule: &mut Rule,
|
|
index: usize,
|
|
cells: &[CellData],
|
|
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| {
|
|
if ui.checkbox(&mut rule.enabled, &rule.name).changed() {
|
|
changed = true;
|
|
}
|
|
if ui.button("delete").clicked() {
|
|
*to_remove = Some(index);
|
|
}
|
|
if ui.button("copy").clicked() {
|
|
*to_clone = Some(index);
|
|
}
|
|
})
|
|
.body(|ui| {
|
|
ui.text_edit_singleline(&mut rule.name);
|
|
ui.horizontal(|ui| {
|
|
if ui.checkbox(&mut rule.flip_x, "flip X").changed() {
|
|
changed = true;
|
|
}
|
|
if ui.checkbox(&mut rule.flip_y, "flip Y").changed() {
|
|
changed = true;
|
|
}
|
|
if ui.checkbox(&mut rule.rotate, "rotate").changed() {
|
|
changed = true;
|
|
}
|
|
});
|
|
ui.horizontal(|ui| {
|
|
ui.label("fail rate:");
|
|
ui.add(DragValue::new(&mut rule.failrate));
|
|
ui.label(format!("variants: {}", rule.variant_count()));
|
|
if ui.button("debug").clicked() {
|
|
rule.dbg_variants();
|
|
}
|
|
});
|
|
let cells_y = rule.height();
|
|
let cells_x = rule.width();
|
|
let patt_width = CSIZE * cells_x as f32;
|
|
let patt_height = CSIZE * cells_y as f32;
|
|
|
|
let (_, bounds) = ui.allocate_space(Vec2::new(
|
|
patt_width * 2. + RESIZE_BUTTON_WIDTH * 4. + CSIZE,
|
|
patt_height + RESIZE_BUTTON_WIDTH * 2.,
|
|
));
|
|
|
|
let from_cells_rect = Rect::from_min_size(
|
|
bounds.min + Vec2::splat(RESIZE_BUTTON_WIDTH),
|
|
Vec2::new(patt_width, patt_height),
|
|
);
|
|
let to_cells_rect = Rect::from_min_size(
|
|
bounds.min
|
|
+ Vec2::splat(RESIZE_BUTTON_WIDTH)
|
|
+ Vec2::X * (patt_width + RESIZE_BUTTON_WIDTH * 2. + CSIZE),
|
|
Vec2::new(patt_width, patt_height),
|
|
);
|
|
|
|
let mut overlay_lines = Vec::new();
|
|
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_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,
|
|
(cells_x, cells_y),
|
|
&mut overlay_lines,
|
|
);
|
|
if changed_left || changed_right {
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
let delete_mode = ui.input(|i| i.modifiers.shift);
|
|
|
|
let mut resize_box = |x, y, w, h| {
|
|
let rect_a = Rect::from_min_size(Pos2::new(x, y), Vec2::new(w, h));
|
|
let a = ui.allocate_rect(rect_a, Sense::click());
|
|
let rect_b = rect_a.translate(to_cells_rect.min - from_cells_rect.min);
|
|
let b = ui.allocate_rect(rect_b, Sense::click());
|
|
let result = a.union(b);
|
|
let color = if result.hovered() {
|
|
if delete_mode {
|
|
Color32::RED
|
|
} else {
|
|
Color32::GRAY
|
|
}
|
|
} else {
|
|
Color32::DARK_GRAY
|
|
};
|
|
ui.painter_at(bounds).rect_filled(rect_a, 0., color);
|
|
ui.painter_at(bounds).rect_filled(rect_b, 0., color);
|
|
|
|
result.clicked()
|
|
};
|
|
if resize_box(
|
|
bounds.min.x,
|
|
bounds.min.y + RESIZE_BUTTON_WIDTH,
|
|
RESIZE_BUTTON_WIDTH,
|
|
patt_height,
|
|
) {
|
|
if delete_mode {
|
|
rule.resize(Rule::SHRINK_LEFT);
|
|
} else {
|
|
rule.resize(Rule::EXTEND_LEFT);
|
|
}
|
|
}
|
|
if resize_box(
|
|
from_cells_rect.max.x,
|
|
bounds.min.y + RESIZE_BUTTON_WIDTH,
|
|
RESIZE_BUTTON_WIDTH,
|
|
patt_height,
|
|
) {
|
|
if delete_mode {
|
|
rule.resize(Rule::SHRINK_RIGHT);
|
|
} else {
|
|
rule.resize(Rule::EXTEND_RIGHT);
|
|
}
|
|
}
|
|
if resize_box(
|
|
bounds.min.x + RESIZE_BUTTON_WIDTH,
|
|
bounds.min.y,
|
|
patt_width,
|
|
RESIZE_BUTTON_WIDTH,
|
|
) {
|
|
if delete_mode {
|
|
rule.resize(Rule::SHRINK_UP);
|
|
} else {
|
|
rule.resize(Rule::EXTEND_UP);
|
|
}
|
|
}
|
|
if resize_box(
|
|
bounds.min.x + RESIZE_BUTTON_WIDTH,
|
|
bounds.max.y - RESIZE_BUTTON_WIDTH,
|
|
patt_width,
|
|
RESIZE_BUTTON_WIDTH,
|
|
) {
|
|
if delete_mode {
|
|
rule.resize(Rule::SHRINK_DOWN);
|
|
} else {
|
|
rule.resize(Rule::EXTEND_DOWN);
|
|
}
|
|
}
|
|
|
|
for (a, b, marked) in overlay_lines {
|
|
let stroke = if marked {
|
|
(6., Color32::RED)
|
|
} else {
|
|
(2., Color32::WHITE)
|
|
};
|
|
ui.painter().line_segment([a, b], stroke);
|
|
}
|
|
});
|
|
changed
|
|
}
|
|
|
|
fn rule_cell_edit_from(
|
|
ui: &mut Ui,
|
|
origin: Pos2,
|
|
rule: &mut RuleCellFrom,
|
|
x: usize,
|
|
y: usize,
|
|
cells: &[CellData],
|
|
groups: &[CellGroup],
|
|
) -> 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 {
|
|
RuleCellFrom::Any => (),
|
|
RuleCellFrom::One(cell) => {
|
|
let color = cells[cell.id()].color;
|
|
let color = Color32::from_rgb(color[0], color[1], color[2]);
|
|
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;
|
|
}
|
|
}
|
|
}
|
|
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, y): (usize, usize),
|
|
cells: &[CellData],
|
|
groups: &[CellGroup],
|
|
(rule_width, rule_height): (usize, usize),
|
|
overlay_lines: &mut Vec<(Pos2, Pos2, bool)>,
|
|
) -> 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);
|
|
let hovered = aabb.hovered();
|
|
|
|
// draw
|
|
match rule {
|
|
RuleCellTo::None => (),
|
|
RuleCellTo::One(cell) => {
|
|
let color = cells[cell.id()].color;
|
|
let color = Color32::from_rgb(color[0], color[1], color[2]);
|
|
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(x, y) => {
|
|
let this = rect.center();
|
|
let target = origin + Vec2::from((*x as f32, *y as f32)) * CSIZE
|
|
- Vec2::X * (CSIZE * (rule_width as f32 + 1.) + RESIZE_BUTTON_WIDTH * 2.)
|
|
+ Vec2::splat(CSIZE) * 0.5;
|
|
overlay_lines.push((this, target, hovered));
|
|
}
|
|
}
|
|
|
|
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(x, y) => {
|
|
*x += 1;
|
|
if *x >= rule_width {
|
|
*x = 0;
|
|
*y += 1;
|
|
if *y >= rule_height {
|
|
*y = 0;
|
|
}
|
|
}
|
|
changed = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if switch_type {
|
|
changed = true;
|
|
match rule {
|
|
RuleCellTo::None => {
|
|
*rule = RuleCellTo::One(Cell(0));
|
|
}
|
|
RuleCellTo::One(_) => {
|
|
*rule = RuleCellTo::GroupRandom(0);
|
|
}
|
|
RuleCellTo::GroupRandom(_) => {
|
|
*rule = RuleCellTo::Copy(0, 0);
|
|
}
|
|
RuleCellTo::Copy(_, _) => {
|
|
*rule = RuleCellTo::None;
|
|
}
|
|
}
|
|
}
|
|
changed
|
|
}
|
|
|
|
fn draw_group(ui: &mut Ui, rect: Rect, group: &CellGroup, cells: &[CellData]) {
|
|
let group_size = group.cells.len();
|
|
let radius_per_color = (CSIZE * 0.7) / (group_size as f32);
|
|
for (i, cell) in group.cells.iter().enumerate() {
|
|
let color = cells[cell.id()].color;
|
|
let color = Color32::from_rgb(color[0], color[1], color[2]);
|
|
let radius = radius_per_color * ((group_size - i) as f32);
|
|
ui.painter_at(rect)
|
|
.circle_filled(rect.center(), radius, color);
|
|
}
|
|
if group.void {
|
|
ui.painter_at(rect)
|
|
.line_segment([rect.min, rect.max], (1., Color32::WHITE));
|
|
}
|
|
ui.allocate_rect(rect, Sense::hover())
|
|
.on_hover_text(&group.name);
|
|
}
|