diff options
Diffstat (limited to 'src/main.rs')
| -rw-r--r-- | src/main.rs | 280 |
1 files changed, 179 insertions, 101 deletions
diff --git a/src/main.rs b/src/main.rs index d818b04..64e57df 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,26 +1,51 @@ +mod camera; +mod config; +mod sim; +mod ui; + +use egui::Id; use egui_wgpu::{RendererOptions, ScreenDescriptor}; use egui_winit::egui::{self, Context}; -use pixels::{Pixels, SurfaceTexture}; +use pixels::{Pixels, ScalingMode, SurfaceTexture}; use std::time::{Duration, Instant}; use winit::{ application::ApplicationHandler, event::{ElementState, MouseButton, WindowEvent}, event_loop::{ActiveEventLoop, ControlFlow, EventLoop}, + keyboard::Key::{self}, window::Window, }; -const WIDTH: u32 = 320; -const HEIGHT: u32 = 240; -const FPS: u64 = 60; -const FRAME_DURATION: Duration = Duration::from_micros(1_000_000 / FPS); +use crate::{ + camera::{CameraState, screen_position_to_board, write_frame_view}, + config::{PIXEL_BUFFER_HEIGHT, PIXEL_BUFFER_WIDTH, WINDOW_TITLE}, + sim::{board::Board, overlay::create_compute_combined_overlay_offset}, + ui::draw_egui, +}; pub type Error = Box<dyn std::error::Error>; pub type Result<T> = std::result::Result<T, Error>; -struct Clicked { - x: usize, - y: usize, - s: f64, +struct Config { + fps: u16, + brush_size: u8, +} + +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, +} + +struct Diagnostics { + fps: f32, } struct App { @@ -30,47 +55,62 @@ struct App { egui_context: Option<egui::Context>, window: Option<&'static Window>, pixels: Option<Pixels<'static>>, - last_frame: Instant, - // input - last_mouse_pos: Option<(f64, f64)>, + // input state + input: Input, - // experiemnts - clicked: Vec<Clicked>, + // camera state + camera: Option<CameraState>, - // debug - red_level: u8, - green_level: u8, - blue_level: u8, -} + // game state + board: Option<Board>, -impl App { - fn set_px(f: &mut [u8], x: u32, y: u32, r: u8, g: u8, b: u8, a: u8) { - let idx = ((y * WIDTH + x) * 4) as usize; - f[idx] = r; - f[idx + 1] = g; - f[idx + 2] = b; - f[idx + 3] = a; - } + // used to wait for drawing + last_frame_requested: Instant, + // used to compute delta_time + last_frame_real: Instant, + + config: Config, + state: State, + diagnostics: Diagnostics, } impl Default for App { fn default() -> Self { Self { - egui_context: None, - egui_state: None, egui_renderer: None, + egui_state: None, + egui_context: None, window: None, pixels: None, - last_frame: Instant::now(), - last_mouse_pos: Some((0.0, 0.0)), + input: Input { + last_mouse_pos_on_screen: (0.0, 0.0), + last_mouse_pos_on_board: (0, 0), + is_lmb_down: false, + }, - clicked: Vec::new(), + camera: Some(CameraState { + x: 0.0, + y: 0.0, + zoom: 0.5, + }), - red_level: 0xFF, - green_level: 0xFF, - blue_level: 0xFF, + board: Some(Board::empty()), + + 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, + }, + diagnostics: Diagnostics { fps: 0.0 }, } } } @@ -78,14 +118,16 @@ impl Default for App { impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { let window = event_loop - .create_window(Window::default_attributes().with_title("pxs")) + .create_window(Window::default_attributes().with_title(WINDOW_TITLE)) .unwrap(); let size = window.inner_size(); let window_ref: &'static Window = Box::leak(Box::new(window)); let surface = SurfaceTexture::new(size.width, size.height, window_ref); - let pixels = Pixels::new(WIDTH, HEIGHT, surface).unwrap(); + let mut pixels = Pixels::new(PIXEL_BUFFER_WIDTH, PIXEL_BUFFER_HEIGHT, surface).unwrap(); + + pixels.set_scaling_mode(ScalingMode::Fill); let egui_context = Context::default(); let egui_state = egui_winit::State::new( @@ -122,27 +164,35 @@ impl ApplicationHandler for App { && let Some(egui_context) = &self.egui_context && let Some(egui_state) = &mut self.egui_state && let Some(egui_renderer) = &mut self.egui_renderer + && let Some(board) = &mut self.board + && let Some(camera) = &mut self.camera { - // TODO how to use the response? - let _ = egui_state.on_window_event(window, &event); + let egui_response = egui_state.on_window_event(window, &event); + // if egui consumed the event, it means we shouldn't treat any e.g., mouse clicks + if egui_response.consumed { + return; + } match event { + WindowEvent::KeyboardInput { event, .. } => match event.logical_key { + Key::Character(char) => { + if char == "z" { + camera.zoom += 0.1; + } + } + _ => {} + }, WindowEvent::CursorMoved { position, .. } => { - self.last_mouse_pos = Some((position.x, position.y)); + self.input.last_mouse_pos_on_screen = (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| (v.0 as i32, v.1 as i32)) + .unwrap(); } WindowEvent::MouseInput { state, button, .. } => { - if state == ElementState::Pressed - && button == MouseButton::Left - && let Some(last_mouse_pos) = self.last_mouse_pos - { - let coords = pixels - .window_pos_to_pixel((last_mouse_pos.0 as f32, last_mouse_pos.1 as f32)) - .unwrap(); - self.clicked.push(Clicked { - x: coords.0, - y: coords.1, - s: 25.0, - }); + if button == MouseButton::Left { + self.input.is_lmb_down = state == ElementState::Pressed } } WindowEvent::Resized(size) => { @@ -153,55 +203,81 @@ impl ApplicationHandler for App { event_loop.exit(); } WindowEvent::RedrawRequested => { - // pixels logic + // compute frame delta + let now = Instant::now(); + let secs_since_last_frame = (now - self.last_frame_real).as_secs_f32(); + let delta_time = secs_since_last_frame / (1.0 / 60.0); + self.last_frame_real = now; + let instantaneous_fps = 1.0 / secs_since_last_frame; + // TODO: can smooth and round this + self.diagnostics.fps = instantaneous_fps; + + // pixels/camera logic let frame = pixels.frame_mut(); frame.fill(0); - for y in 0..HEIGHT { - for x in 0..WIDTH { - let active = x % 2 == 0 - && y % 2 == 0 - && self.clicked.iter().all(|c| { - (((x as i64) - (c.x as i64)).pow(2) - + ((y as i64) - (c.y as i64)).pow(2)) - .isqrt() as f64 - > c.s - }); + // --TEST DRAWING-- + if self.input.is_lmb_down { + // 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; - Self::set_px( - frame, - x, - y, - if active { self.red_level } else { 0x00 }, - if active { self.green_level } else { 0x00 }, - if active { self.blue_level } else { 0x00 }, - 0xFF, - ); + // 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 + { + board.set_cell_at_position( + x, + y, + sim::board::Cell { + material: 1, + velocity_x: 0, + velocity_y: 0, + flags: 0, + }, + ); + } + } } } - for i in 0..self.clicked.len() { - self.clicked[i].s -= 0.3; - if self.clicked[i].s <= 0.0 { - self.clicked.remove(i); - break; - } - } + let get_overlay = + create_compute_combined_overlay_offset(board, &self.config, &self.input); + write_frame_view(frame, board, camera, get_overlay); // egui logic let raw_input = egui_state.take_egui_input(window); let full_output = egui_context.run_ui(raw_input, |ui| { - ui.heading("Debug"); - ui.label( - self.clicked - .iter() - .map(|c| format!("x={x},y={y},s={s}", x = c.x, y = c.y, s = c.s)) - .collect::<Vec<String>>() - .join("\n"), - ); - ui.add(egui::Slider::new(&mut self.red_level, 0..=120).text("Red")); - ui.add(egui::Slider::new(&mut self.green_level, 0..=120).text("Green")); - ui.add(egui::Slider::new(&mut self.blue_level, 0..=120).text("Blue")); + let right_panel = egui::Panel::right(Id::new("right_panel")); + right_panel + .resizable(false) + // TODO collapse button + .show_collapsible(ui, &mut true, |panel_ui| { + draw_egui( + panel_ui, + &mut self.config, + &mut self.state, + camera, + &self.diagnostics, + &self.input, + ) + }); }); egui_state.handle_platform_output(window, full_output.platform_output); @@ -225,10 +301,8 @@ impl ApplicationHandler for App { } let _ = pixels.render_with(|encoder, render_target, context| { - // 1. pixels' own blit of the framebuffer to the surface context.scaling_renderer.render(encoder, render_target); - // 2. prep egui's GPU-side buffers for this frame's draws egui_renderer.update_buffers( device, &queue, @@ -237,15 +311,14 @@ impl ApplicationHandler for App { &screen_descriptor, ); - // 3. open a pass on the same target, preserving the blit - let mut pass = encoder + let mut egui_pass = encoder .begin_render_pass(&wgpu::RenderPassDescriptor { label: Some("egui pass"), color_attachments: &[Some(wgpu::RenderPassColorAttachment { view: render_target, resolve_target: None, ops: wgpu::Operations { - load: wgpu::LoadOp::Load, // keep the blit, don't clear + load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store, }, depth_slice: None, @@ -255,10 +328,14 @@ impl ApplicationHandler for App { occlusion_query_set: None, multiview_mask: None, }) - .forget_lifetime(); // bridge wgpu 30's pass-lifetime requirement + .forget_lifetime(); - egui_renderer.render(&mut pass, &clipped_primitives, &screen_descriptor); - drop(pass); + egui_renderer.render( + &mut egui_pass, + &clipped_primitives, + &screen_descriptor, + ); + drop(egui_pass); Ok(()) }); @@ -270,9 +347,10 @@ impl ApplicationHandler for App { fn about_to_wait(&mut self, _: &ActiveEventLoop) { let now = Instant::now(); - // Limit to 60 FPS - if now - self.last_frame >= FRAME_DURATION { - self.last_frame = now; + let frame_duration: Duration = Duration::from_micros(1_000_000 / self.config.fps as u64); + // limit our internal redraw requests to (fps) + if now - self.last_frame_requested >= frame_duration { + self.last_frame_requested = now; self.window .expect("Bug - Window should exist") .request_redraw(); |
