summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKai Stevenson <kai@kaistevenson.com>2026-08-11 00:46:56 -0700
committerKai Stevenson <kai@kaistevenson.com>2026-08-11 00:46:56 -0700
commit6f67586b8fc6efdb86d0c9a9744c546da97c4dab (patch)
tree2c0b328f7427efc5647cde810a361405d914a37c
parent167e31655b63aa4d4548532cf0410cea9fd145ca (diff)
physics for sand and water
-rw-r--r--src/camera.rs112
-rw-r--r--src/config.rs1
-rw-r--r--src/main.rs184
-rw-r--r--src/sim/board.rs24
-rw-r--r--src/sim/materials.rs25
-rw-r--r--src/sim/overlay.rs19
-rw-r--r--src/sim/sim.rs135
-rw-r--r--src/ui.rs65
8 files changed, 416 insertions, 149 deletions
diff --git a/src/camera.rs b/src/camera.rs
index 695baf5..d10c92e 100644
--- a/src/camera.rs
+++ b/src/camera.rs
@@ -1,9 +1,10 @@
use crate::{
- config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH},
+ Input,
+ config::{CAMERA_MOVEMENT_SPEED, PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH},
sim::{board::Board, materials::MATERIALS},
};
-pub struct CameraState {
+pub struct Camera {
// centre coords
pub x: f64,
pub y: f64,
@@ -11,48 +12,79 @@ pub struct CameraState {
pub zoom: f64,
}
-pub fn screen_position_to_board(
- board: &Board,
- camera: &CameraState,
- screen_x: f64,
- screen_y: f64,
-) -> (f64, f64) {
- let camera_width = camera.zoom * f64::from(board.size_x);
- let camera_height = camera.zoom * f64::from(board.size_y);
- let camera_start_x = camera.x - camera_width / 2.0;
- let camera_start_y = camera.y - camera_height / 2.0;
- (
- (screen_x / PIXEL_BUFFER_WIDTH as f64 * camera_width) + camera_start_x,
- (screen_y / PIXEL_BUFFER_HEIGHT as f64 * camera_height) + camera_start_y,
- )
-}
+impl Camera {
+ pub fn handle_camera_input(&mut self, input: &Input, delta_time: f32) {
+ // wasd movement
+ let x = if input.is_left_pressed {
+ -CAMERA_MOVEMENT_SPEED
+ } else if input.is_right_pressed {
+ CAMERA_MOVEMENT_SPEED
+ } else {
+ 0.0
+ };
+ let y = if input.is_down_pressed {
+ CAMERA_MOVEMENT_SPEED
+ } else if input.is_up_pressed {
+ -CAMERA_MOVEMENT_SPEED
+ } else {
+ 0.0
+ };
+
+ if x == 0.0 && y == 0.0 {
+ return;
+ }
+
+ let magnitude = (x.powi(2) + y.powi(2)).sqrt();
+ let adjusted_x = x / magnitude * self.zoom as f32 * CAMERA_MOVEMENT_SPEED * delta_time;
+ let adjusted_y = y / magnitude * self.zoom as f32 * CAMERA_MOVEMENT_SPEED * delta_time;
+
+ self.x += adjusted_x as f64;
+ self.y += adjusted_y as f64;
+ }
+
+ pub fn screen_position_to_board(
+ &self,
+ board: &Board,
+ screen_x: f64,
+ screen_y: f64,
+ ) -> (f64, f64) {
+ let camera_width = self.zoom * f64::from(board.size_x);
+ let camera_height = self.zoom * f64::from(board.size_y);
+ let camera_start_x = self.x - camera_width / 2.0;
+ let camera_start_y = self.y - camera_height / 2.0;
+ (
+ (screen_x / PIXEL_BUFFER_WIDTH as f64 * camera_width) + camera_start_x,
+ (screen_y / PIXEL_BUFFER_HEIGHT as f64 * camera_height) + camera_start_y,
+ )
+ }
-pub fn write_frame_view(
- frame: &mut [u8],
- board: &Board,
- camera: &CameraState,
- get_overlay: impl Fn(i32, i32) -> (u8, u8, u8, u8),
-) {
- for frame_y in 0..PIXEL_BUFFER_HEIGHT {
- for frame_x in 0..PIXEL_BUFFER_WIDTH {
- let (x_coord, y_coord) =
- screen_position_to_board(board, camera, frame_x as f64, frame_y as f64);
+ pub fn write_frame_view(
+ &self,
+ frame: &mut [u8],
+ board: &Board,
+ get_overlay: impl Fn(i32, i32) -> (u8, u8, u8, u8),
+ ) {
+ for frame_y in 0..PIXEL_BUFFER_HEIGHT {
+ for frame_x in 0..PIXEL_BUFFER_WIDTH {
+ let (x_coord, y_coord) =
+ self.screen_position_to_board(board, frame_x as f64, frame_y as f64);
- let cell = board.cell_at_position(x_coord as i32, y_coord as i32);
- let cell_color: Option<(u8, u8, u8, u8)> = cell.map(|c| {
- let m = MATERIALS[c.material as usize];
- (m.r, m.g, m.b, 0xFF)
- });
- let off_grid_color: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0xFF);
+ let cell = board.cell_at_position(x_coord as i32, y_coord as i32);
+ let cell_color: Option<(u8, u8, u8, u8)> = cell.map(|c| {
+ let m = MATERIALS[c.material as usize];
+ (m.r, m.g, m.b, 0xFF)
+ });
+ let off_grid_color: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0xFF);
- let target_color: (u8, u8, u8, u8) = cell_color.unwrap_or(off_grid_color);
- let overlay = get_overlay(x_coord as i32, y_coord as i32);
+ let target_color: (u8, u8, u8, u8) = cell_color.unwrap_or(off_grid_color);
+ let overlay = get_overlay(x_coord as i32, y_coord as i32);
- let frame_idx = ((frame_y * PIXEL_BUFFER_WIDTH + frame_x) * 4) as usize;
- frame[frame_idx] = target_color.0.saturating_add(overlay.0);
- frame[frame_idx + 1] = target_color.1.saturating_add(overlay.1);
- frame[frame_idx + 2] = target_color.2.saturating_add(overlay.2);
- frame[frame_idx + 3] = target_color.3.saturating_add(overlay.3);
+ let frame_idx = ((frame_y * PIXEL_BUFFER_WIDTH + frame_x) * 4) as usize;
+ frame[frame_idx] = target_color.0.saturating_add(overlay.0);
+ frame[frame_idx + 1] = target_color.1.saturating_add(overlay.1);
+ frame[frame_idx + 2] = target_color.2.saturating_add(overlay.2);
+ frame[frame_idx + 3] = target_color.3.saturating_add(overlay.3);
+ }
}
}
}
diff --git a/src/config.rs b/src/config.rs
index 75cb581..74a98e4 100644
--- a/src/config.rs
+++ b/src/config.rs
@@ -1,3 +1,4 @@
pub const WINDOW_TITLE: &str = "pxs";
pub const PIXEL_BUFFER_WIDTH: u32 = 320;
pub const PIXEL_BUFFER_HEIGHT: u32 = 240;
+pub const CAMERA_MOVEMENT_SPEED: f32 = 10.0;
diff --git a/src/main.rs b/src/main.rs
index 64e57df..02920d6 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -7,19 +7,28 @@ use egui::Id;
use egui_wgpu::{RendererOptions, ScreenDescriptor};
use egui_winit::egui::{self, Context};
use pixels::{Pixels, ScalingMode, SurfaceTexture};
-use std::time::{Duration, Instant};
+use std::{
+ cmp::{max, min},
+ time::{Duration, Instant},
+};
use winit::{
application::ApplicationHandler,
- event::{ElementState, MouseButton, WindowEvent},
+ event::{
+ ElementState, KeyEvent, MouseButton,
+ WindowEvent::{self},
+ },
event_loop::{ActiveEventLoop, ControlFlow, EventLoop},
- keyboard::Key::{self},
+ keyboard::{KeyCode, PhysicalKey},
window::Window,
};
use crate::{
- camera::{CameraState, screen_position_to_board, write_frame_view},
+ camera::Camera,
config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH, WINDOW_TITLE},
- sim::{board::Board, overlay::create_compute_combined_overlay_offset},
+ sim::{
+ board::Board, materials::MaterialId, overlay::create_compute_combined_overlay_offset,
+ sim::sim_tick,
+ },
ui::draw_egui,
};
@@ -27,21 +36,22 @@ pub type Error = Box<dyn std::error::Error>;
pub type Result<T> = std::result::Result<T, Error>;
struct Config {
+ show_ticks: bool,
fps: u16,
- brush_size: u8,
+ brush_radius: u8,
+ brush_material: MaterialId,
}
struct Input {
- last_mouse_pos_on_screen: (f64, f64),
- last_mouse_pos_on_board: (i32, i32),
- is_lmb_down: bool,
-}
+ last_mouse_pos_on_screen: Option<(f64, f64)>,
+ last_mouse_pos_on_board: Option<(i32, i32)>,
+ is_lmb_pressed: bool,
-struct State {
- // debug
- red_level: u8,
- green_level: u8,
- blue_level: u8,
+ // keybindings
+ is_up_pressed: bool,
+ is_left_pressed: bool,
+ is_down_pressed: bool,
+ is_right_pressed: bool,
}
struct Diagnostics {
@@ -56,22 +66,24 @@ struct App {
window: Option<&'static Window>,
pixels: Option<Pixels<'static>>,
- // input state
input: Input,
- // camera state
- camera: Option<CameraState>,
+ camera: Option<Camera>,
- // game state
board: Option<Board>,
+ // sim state
+ // the last/current (not yet completed) seqno
+ sim_seqno: u64,
+ sim_paused: bool,
+ ignore_pause_next_tick: bool,
+
// used to wait for drawing
last_frame_requested: Instant,
// used to compute delta_time
last_frame_real: Instant,
config: Config,
- state: State,
diagnostics: Diagnostics,
}
@@ -85,12 +97,17 @@ impl Default for App {
pixels: None,
input: Input {
- last_mouse_pos_on_screen: (0.0, 0.0),
- last_mouse_pos_on_board: (0, 0),
- is_lmb_down: false,
+ last_mouse_pos_on_screen: None,
+ last_mouse_pos_on_board: None,
+
+ is_lmb_pressed: false,
+ is_up_pressed: false,
+ is_left_pressed: false,
+ is_down_pressed: false,
+ is_right_pressed: false,
},
- camera: Some(CameraState {
+ camera: Some(Camera {
x: 0.0,
y: 0.0,
zoom: 0.5,
@@ -98,17 +115,18 @@ impl Default for App {
board: Some(Board::empty()),
+ sim_seqno: 0,
+ sim_paused: false,
+ ignore_pause_next_tick: false,
+
last_frame_requested: Instant::now(),
last_frame_real: Instant::now(),
config: Config {
fps: 120,
- brush_size: 10,
- },
- state: State {
- red_level: 0xFF,
- green_level: 0xFF,
- blue_level: 0xFF,
+ show_ticks: false,
+ brush_radius: 10,
+ brush_material: MaterialId::Sand,
},
diagnostics: Diagnostics { fps: 0.0 },
}
@@ -174,25 +192,46 @@ impl ApplicationHandler for App {
}
match event {
- WindowEvent::KeyboardInput { event, .. } => match event.logical_key {
- Key::Character(char) => {
- if char == "z" {
- camera.zoom += 0.1;
+ WindowEvent::KeyboardInput {
+ event:
+ KeyEvent {
+ physical_key: PhysicalKey::Code(code),
+ state,
+ ..
+ },
+ ..
+ } => {
+ let pressed = state.is_pressed();
+ match code {
+ KeyCode::KeyW => self.input.is_up_pressed = pressed,
+ KeyCode::KeyA => self.input.is_left_pressed = pressed,
+ KeyCode::KeyS => self.input.is_down_pressed = pressed,
+ KeyCode::KeyD => self.input.is_right_pressed = pressed,
+ KeyCode::KeyC => self.board = Some(Board::empty()),
+ KeyCode::Space => {
+ if pressed {
+ self.sim_paused = !self.sim_paused
+ }
+ }
+ KeyCode::KeyX => {
+ if pressed {
+ self.ignore_pause_next_tick = true
+ }
}
+ _ => {}
}
- _ => {}
- },
+ }
WindowEvent::CursorMoved { position, .. } => {
- self.input.last_mouse_pos_on_screen = (position.x, position.y);
+ self.input.last_mouse_pos_on_screen = Some((position.x, position.y));
self.input.last_mouse_pos_on_board = pixels
.window_pos_to_pixel((position.x as f32, position.y as f32))
- .map(|v| screen_position_to_board(board, camera, v.0 as f64, v.1 as f64))
+ .map(|v| camera.screen_position_to_board(board, v.0 as f64, v.1 as f64))
.map(|v| (v.0 as i32, v.1 as i32))
- .unwrap();
+ .ok()
}
WindowEvent::MouseInput { state, button, .. } => {
if button == MouseButton::Left {
- self.input.is_lmb_down = state == ElementState::Pressed
+ self.input.is_lmb_pressed = state == ElementState::Pressed
}
}
WindowEvent::Resized(size) => {
@@ -212,54 +251,68 @@ impl ApplicationHandler for App {
// TODO: can smooth and round this
self.diagnostics.fps = instantaneous_fps;
+ // apply inputs
+ camera.handle_camera_input(&self.input, delta_time);
+
// pixels/camera logic
let frame = pixels.frame_mut();
frame.fill(0);
// --TEST DRAWING--
- if self.input.is_lmb_down {
+ if self.input.is_lmb_pressed && self.input.last_mouse_pos_on_board.is_some() {
// start with the bounding box of the drawing brush circle + some margin
- let bb_xl = self.input.last_mouse_pos_on_board.0
- - self.config.brush_size as i32 / 2
- - 5;
- let bb_xu = self.input.last_mouse_pos_on_board.0
- + self.config.brush_size as i32 / 2
- + 5;
- let bb_yl = self.input.last_mouse_pos_on_board.1
- - self.config.brush_size as i32 / 2
- - 5;
- let bb_yu = self.input.last_mouse_pos_on_board.1
- + self.config.brush_size as i32 / 2
- + 5;
+ // clamp the bounding box to the board sie
+ // TODO better way to do this without unwrap?
+ let bb_xl = max(
+ self.input.last_mouse_pos_on_board.unwrap().0
+ - self.config.brush_radius as i32,
+ -((board.size_x / 2) as i32),
+ );
+ let bb_xu = min(
+ self.input.last_mouse_pos_on_board.unwrap().0
+ + self.config.brush_radius as i32,
+ (board.size_x / 2) as i32,
+ );
+ let bb_yl = max(
+ self.input.last_mouse_pos_on_board.unwrap().1
+ - self.config.brush_radius as i32,
+ -((board.size_y / 2) as i32),
+ );
+ let bb_yu = min(
+ self.input.last_mouse_pos_on_board.unwrap().1
+ + self.config.brush_radius as i32,
+ (board.size_y / 2) as i32,
+ );
// for each point, check if the distance is less than the brush size and write the pixel
for x in bb_xl..bb_xu {
for y in bb_yl..bb_yu {
// brush/selection
- if (((x - self.input.last_mouse_pos_on_board.0).pow(2)
- + (y - self.input.last_mouse_pos_on_board.1).pow(2))
- as f32)
- .sqrt()
- < self.config.brush_size as f32
+ if ((x - self.input.last_mouse_pos_on_board.unwrap().0).pow(2)
+ + (y - self.input.last_mouse_pos_on_board.unwrap().1).pow(2))
+ < (self.config.brush_radius as i32).pow(2)
{
board.set_cell_at_position(
x,
y,
- sim::board::Cell {
- material: 1,
- velocity_x: 0,
- velocity_y: 0,
- flags: 0,
- },
+ sim::board::Cell::from_material(self.config.brush_material),
);
}
}
}
}
+ // TODO check if we need to run another sim tick given the sim speed
+ // SIM logic
+ if !self.sim_paused || self.ignore_pause_next_tick {
+ sim_tick(board, self.sim_seqno, delta_time);
+ self.sim_seqno += 1;
+ self.ignore_pause_next_tick = false;
+ }
+
let get_overlay =
create_compute_combined_overlay_offset(board, &self.config, &self.input);
- write_frame_view(frame, board, camera, get_overlay);
+ camera.write_frame_view(frame, board, get_overlay);
// egui logic
let raw_input = egui_state.take_egui_input(window);
@@ -272,7 +325,6 @@ impl ApplicationHandler for App {
draw_egui(
panel_ui,
&mut self.config,
- &mut self.state,
camera,
&self.diagnostics,
&self.input,
diff --git a/src/sim/board.rs b/src/sim/board.rs
index 4cc5756..7af19be 100644
--- a/src/sim/board.rs
+++ b/src/sim/board.rs
@@ -1,24 +1,24 @@
-use crate::config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH};
-
-type MaterialId = u16;
+use crate::{
+ config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH},
+ sim::materials::MaterialId,
+};
#[derive(Clone, Copy)]
pub struct Cell {
pub material: MaterialId,
- pub velocity_x: i8,
- pub velocity_y: i8,
pub flags: u8,
}
impl Cell {
- fn empty() -> Cell {
+ pub fn void() -> Cell {
Cell {
- material: 0,
- velocity_x: 0,
- velocity_y: 0,
+ material: MaterialId::Void,
flags: 0,
}
}
+ pub fn from_material(material: MaterialId) -> Cell {
+ Cell { material, flags: 0 }
+ }
}
pub struct Board {
@@ -40,8 +40,8 @@ impl Board {
let idx = self.position_to_index(x, y).unwrap();
self.cells[idx] = c;
}
- pub fn cell_at_position(&self, x: i32, y: i32) -> Option<&Cell> {
- Some(&self.cells[self.position_to_index(x, y)?])
+ pub fn cell_at_position(&self, x: i32, y: i32) -> Option<Cell> {
+ Some(self.cells[self.position_to_index(x, y)?])
}
pub fn position_to_index(&self, x: i32, y: i32) -> Option<usize> {
let board_x = x + self.size_x as i32 / 2;
@@ -61,7 +61,7 @@ impl Board {
pub fn empty() -> Board {
let size_x = PIXEL_BUFFER_WIDTH * 2;
let size_y = PIXEL_BUFFER_HEIGHT * 2;
- let cells: Vec<Cell> = vec![Cell::empty(); (size_x * size_y) as usize];
+ let cells: Vec<Cell> = vec![Cell::void(); (size_x * size_y) as usize];
Board {
size_x,
diff --git a/src/sim/materials.rs b/src/sim/materials.rs
index 13a45f3..8b21618 100644
--- a/src/sim/materials.rs
+++ b/src/sim/materials.rs
@@ -4,19 +4,38 @@ pub struct Material<'a> {
pub r: u8,
pub g: u8,
pub b: u8,
+
+ pub density: u8,
+}
+
+#[repr(u8)]
+#[derive(Clone, Copy, PartialEq, Eq)]
+pub enum MaterialId {
+ Void,
+ Sand,
+ Water,
}
-pub static MATERIALS: [Material; 2] = [
+pub static MATERIALS: [Material; 3] = [
Material {
name: "Void",
r: 0x00,
g: 0x00,
b: 0x00,
+ density: 0,
},
Material {
name: "Sand",
- r: 0xFF,
- g: 0x00,
+ r: 0xDE,
+ g: 0xCB,
+ b: 0x85,
+ density: 50,
+ },
+ Material {
+ name: "Water",
+ r: 0x38,
+ g: 0xA9,
b: 0xFF,
+ density: 40,
},
];
diff --git a/src/sim/overlay.rs b/src/sim/overlay.rs
index 36a810b..7c5d5a4 100644
--- a/src/sim/overlay.rs
+++ b/src/sim/overlay.rs
@@ -1,3 +1,5 @@
+use core::range::Range;
+
use crate::{Config, Input, sim::board::Board};
pub fn create_compute_combined_overlay_offset(
@@ -5,7 +7,8 @@ pub fn create_compute_combined_overlay_offset(
config: &Config,
input: &Input,
) -> impl Fn(i32, i32) -> (u8, u8, u8, u8) {
- |x: i32, y: i32| {
+ // TODO fix this move?
+ move |x: i32, y: i32| {
// could allow negative offsets too
let mut offset: (u8, u8, u8, u8) = (0x00, 0x00, 0x00, 0x00);
@@ -35,14 +38,12 @@ pub fn create_compute_combined_overlay_offset(
}
// brush/selection
- if (((x - input.last_mouse_pos_on_board.0).pow(2)
- + (y - input.last_mouse_pos_on_board.1).pow(2)) as f32)
- .sqrt()
- < config.brush_size as f32
- {
- offset.0 = offset.0.saturating_add(0xAA);
- offset.1 = offset.1.saturating_add(0x00);
- offset.2 = offset.2.saturating_add(0xAA);
+ if input.last_mouse_pos_on_board.is_some_and(|p| {
+ ((x - p.0).pow(2) + (y - p.1).pow(2)) < (config.brush_radius as i32).pow(2)
+ }) {
+ offset.0 = offset.0.saturating_add(0x82);
+ offset.1 = offset.1.saturating_add(0xA1);
+ offset.2 = offset.2.saturating_add(0xAD);
}
return offset;
diff --git a/src/sim/sim.rs b/src/sim/sim.rs
index bb68306..186483d 100644
--- a/src/sim/sim.rs
+++ b/src/sim/sim.rs
@@ -1,3 +1,134 @@
-use crate::Board;
+use core::range::Range;
-fn sim_tick(board: Board, seqno: u64) {}
+use crate::{
+ Board,
+ sim::materials::{MATERIALS, MaterialId},
+};
+
+// TODO: chunks
+pub fn sim_tick(board: &mut Board, seqno: u64, delta_time: f32) {
+ // scan bottom to top to enable contiguous falling
+ let seqno_parity = (seqno as u8) & 0b1;
+
+ let bx = (board.size_x / 2) as i32;
+ let by = (board.size_y / 2) as i32;
+ for y in (-by..by + 1).rev() {
+ // invert scan order on every other frame
+ for col in -bx..bx + 1 {
+ let x = if seqno_parity == 0 { col } else { -col };
+
+ let cell = board.cell_at_position(x, y);
+ if let Some(cell) = cell
+ && cell.flags & 0b1 == seqno_parity
+ {
+ let mut cur = cell.clone();
+ // flip the parity bit
+ cur.flags = cur.flags ^ 0b1;
+
+ let material = &MATERIALS[cur.material as usize];
+
+ match cur.material {
+ MaterialId::Void => {}
+ // TODO abstract density based movement
+ MaterialId::Sand => {
+ for candidate in [
+ (x, y + 1),
+ (x - 1 + 2 * seqno_parity as i32, y + 1),
+ (x + 1 - 2 * seqno_parity as i32, y + 1),
+ ] {
+ let target = board.cell_at_position(candidate.0, candidate.1);
+ if let Some(target) = target
+ && MATERIALS[target.material as usize].density < material.density
+ {
+ // swap the cells
+ board.set_cell_at_position(x, y, target);
+ board.set_cell_at_position(candidate.0, candidate.1, cur);
+ break;
+ }
+ }
+ }
+ MaterialId::Water => 'water: {
+ // if the water can fall, do so
+ for candidate in [
+ (x, y + 1),
+ (x - 1 + 2 * seqno_parity as i32, y + 1),
+ (x + 1 - 2 * seqno_parity as i32, y + 1),
+ ] {
+ let target = board.cell_at_position(candidate.0, candidate.1);
+ if let Some(target) = target
+ && MATERIALS[target.material as usize].density < material.density
+ {
+ // swap the cells
+ board.set_cell_at_position(x, y, target);
+ board.set_cell_at_position(candidate.0, candidate.1, cur);
+ break 'water;
+ }
+ }
+ // if the water can't fall, check if we can move left or right
+ // these are inverted on parity so that we don't preference a direction
+ let left_target = board.cell_at_position(x - 1, y);
+ let can_move_left = left_target.is_some_and(|c| {
+ MATERIALS[c.material as usize].density < material.density
+ });
+ let right_target = board.cell_at_position(x + 1, y);
+ let can_move_right = right_target.is_some_and(|c| {
+ MATERIALS[c.material as usize].density < material.density
+ });
+
+ // we can't move down or to other side, so we're stuck
+ if !can_move_left && !can_move_right {
+ break 'water;
+ }
+
+ // find the closest hole within 20 pixels (TODO optimize)
+ // a hole is any space below us with a lesser density
+ // prevents equidistance stuck state
+ let starting_side = if seqno_parity == 0 { 1 } else { -1 };
+ for i in 0..20 {
+ let side = if i % 2 == 0 {
+ starting_side
+ } else {
+ -starting_side
+ };
+ if (side == 1 && !can_move_right) || (side == -1 && !can_move_left) {
+ continue;
+ }
+
+ let offset = side * (1 + i / 2);
+
+ let target = board.cell_at_position(x + offset, y + 1);
+ if let Some(target) = target
+ && MATERIALS[target.material as usize].density < material.density
+ {
+ // we identified a hole and we know that the space on this side is open
+ // move toward the hole
+ let mut new_target =
+ if side == 1 { right_target } else { left_target }.clone();
+ // new_target.flags = new_target.flags ^ 0b1;
+ // safe to unwrap
+ board.set_cell_at_position(x, y, new_target.unwrap());
+ board.set_cell_at_position(x + side, y, cur);
+ break 'water;
+ }
+ }
+
+ // we didn't find a hole, so just move "randomly" on the same surface
+ // TODO when to settle?
+ let (target, target_x) = if !can_move_left {
+ (right_target, 1)
+ } else if !can_move_right {
+ (left_target, -1)
+ } else if seqno_parity % 2 == 1 {
+ (right_target, 1)
+ } else {
+ (left_target, -1)
+ };
+
+ board.set_cell_at_position(x, y, target.unwrap());
+ board.set_cell_at_position(x + target_x, y, cur);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/ui.rs b/src/ui.rs
index d55c5fb..dbdfa69 100644
--- a/src/ui.rs
+++ b/src/ui.rs
@@ -1,33 +1,64 @@
-use egui::Ui;
+use egui::{Color32, Stroke, Ui, accesskit::ListStyle::Circle, epaint::CircleShape};
-use crate::{Config, Diagnostics, Input, State, camera::CameraState};
+use crate::{
+ Config, Diagnostics, Input,
+ camera::Camera,
+ sim::materials::{MATERIALS, MaterialId},
+};
-pub fn draw_egui(
+pub fn draw_egui<'a>(
ui: &mut Ui,
config: &mut Config,
- state: &mut State,
- camera: &mut CameraState,
+ camera: &mut Camera,
diagnostics: &Diagnostics,
input: &Input,
) {
ui.heading("Config");
- ui.add(egui::Slider::new(&mut config.brush_size, 1..=100).text("Brush size"));
+ ui.add(egui::Slider::new(&mut config.brush_radius, 1..=100).text("Brush radius"));
+
+ let material = MATERIALS[config.brush_material as usize];
+
+ // material combobox
+ egui::ComboBox::from_label("Select a material")
+ .selected_text(format!(
+ "Material: [{}, d={}]",
+ material.name, material.density,
+ ))
+ .icon(move |ui, rect, _, _| {
+ ui.painter().add(egui::Shape::Circle(
+ (CircleShape {
+ center: rect.center(),
+ radius: rect.width() / 2.5,
+ stroke: Stroke::NONE,
+ fill: Color32::from_rgb(material.r, material.g, material.b),
+ }),
+ ));
+ })
+ .show_ui(ui, |ui| {
+ ui.selectable_value(&mut config.brush_material, MaterialId::Void, "Void");
+ ui.selectable_value(&mut config.brush_material, MaterialId::Sand, "Sand");
+ ui.selectable_value(&mut config.brush_material, MaterialId::Water, "Water");
+ });
+
ui.add(egui::Slider::new(&mut config.fps, 1..=1000).text("Max FPS"));
+ ui.checkbox(&mut config.show_ticks, "Visualize ticks");
ui.label(format!("Real FPS: {}", diagnostics.fps));
ui.heading("Camera");
ui.add(egui::Slider::new(&mut camera.zoom, 0.0..=10.0).text("Zoom"));
ui.add(egui::Slider::new(&mut camera.x, -1000.0..=1000.0).text("X"));
ui.add(egui::Slider::new(&mut camera.y, -1000.0..=1000.0).text("Y"));
ui.heading("Input");
- ui.label(format!(
- "Mouse: x,y=({x}, {y}), lmb_pressed={lmb}",
- x = input.last_mouse_pos_on_screen.0,
- y = input.last_mouse_pos_on_screen.1,
- lmb = input.is_lmb_down
- ));
- ui.label(format!(
- "Mouse (board): x,y=({x}, {y})",
- x = input.last_mouse_pos_on_board.0,
- y = input.last_mouse_pos_on_board.1,
- ));
+
+ input.last_mouse_pos_on_screen.map(|p| {
+ ui.label(format!(
+ "Mouse: x,y=({x}, {y}), lmb_pressed={lmb}",
+ x = p.0,
+ y = p.1,
+ lmb = input.is_lmb_pressed
+ ))
+ });
+
+ input
+ .last_mouse_pos_on_board
+ .map(|p| ui.label(format!("Mouse (board): x,y=({x}, {y})", x = p.0, y = p.1,)));
}