From 6f67586b8fc6efdb86d0c9a9744c546da97c4dab Mon Sep 17 00:00:00 2001 From: Kai Stevenson Date: Tue, 11 Aug 2026 00:46:56 -0700 Subject: physics for sand and water --- src/camera.rs | 118 ++++++++++++++++++++------------ src/config.rs | 1 + src/main.rs | 186 ++++++++++++++++++++++++++++++++------------------- src/sim/board.rs | 24 +++---- src/sim/materials.rs | 25 ++++++- src/sim/overlay.rs | 19 +++--- src/sim/sim.rs | 135 ++++++++++++++++++++++++++++++++++++- src/ui.rs | 65 +++++++++++++----- 8 files changed, 420 insertions(+), 153 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( + &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 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); -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); - - 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 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; pub type Result = std::result::Result; 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, -} - -struct State { - // debug - red_level: u8, - green_level: u8, - blue_level: u8, + last_mouse_pos_on_screen: Option<(f64, f64)>, + last_mouse_pos_on_board: Option<(i32, i32)>, + is_lmb_pressed: bool, + + // 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>, - // input state input: Input, - // camera state - camera: Option, + camera: Option, - // game state board: Option, + // 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 { + Some(self.cells[self.position_to_index(x, y)?]) } pub fn position_to_index(&self, x: i32, y: i32) -> Option { 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 = vec![Cell::empty(); (size_x * size_y) as usize]; + let cells: Vec = 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,))); } -- cgit v1.3.1