From ebcefd440ca1372c5e9845226340cb8913a58855 Mon Sep 17 00:00:00 2001 From: Jakob Meier Date: Fri, 24 Feb 2023 20:43:41 +0100 Subject: [PATCH] Basic face detection and block placement --- src/blocks.rs | 6 +- src/screens/# | 238 +++++++++++++++++++++++++++++++++++++++++++ src/screens/build.rs | 168 ++++++++++++++++++++++++++---- src/textures.rs | 6 +- 4 files changed, 393 insertions(+), 25 deletions(-) create mode 100644 src/screens/# diff --git a/src/blocks.rs b/src/blocks.rs index 71623a9..4af3a95 100644 --- a/src/blocks.rs +++ b/src/blocks.rs @@ -3,12 +3,14 @@ use crate::textures::AssetStore; use crate::types::Direction; pub enum Block { - Dirt(Direction) + Dirt(Direction), + GrassCenter(Direction) } impl Block { pub fn get_texture(&self, assets: &AssetStore) -> Texture2D { match self { - Block::Dirt(d) => assets.blocks.dirt.get_dir(d) + Block::Dirt(d) => assets.blocks.dirt.get_dir(d), + Block::GrassCenter(d) => assets.blocks.grass_center.get_dir(d) } } } diff --git a/src/screens/# b/src/screens/# new file mode 100644 index 0000000..6c7b2c6 --- /dev/null +++ b/src/screens/# @@ -0,0 +1,238 @@ +use macroquad::prelude::*; +use std::collections::HashMap; +use crate::textures::AssetStore; +use crate::types::{ + GameComponent, + GameEvent, + Pos3 +}; +use crate::blocks::Block; + +struct Camera { + center: Vec2, + scale: f32 +} +impl Camera { + fn new() -> Self { + Self { + center: Vec2::new(0.0, 0.0), + scale: 1.0 + } + } +} + +/// image width +const TEXTURE_WIDTH: f32 = 256.0; +/// actual width inside ob cube +const TEXTURE_INNER_WIDTH: f32 = 223.0; // 216 +/// height of image +const TEXTURE_HEIGHT: f32 = 352.0; +/// empty space above texture +const TEXTURE_Y_WHITESPACE: f32 = 130.0; +/// actual height of cube +const TEXTURE_INNER_HEIGHT: f32 = 108.0; // 110 +/// height of top surface +const TEXTURE_DEPTH: f32 = 100.0; + +pub struct BuildScreen { + grid: HashMap, + cam: Camera, + mouse_down: bool +} +impl BuildScreen { + pub fn new() -> Self { + let mut this = Self { + grid: HashMap::new(), + cam: Camera::new(), + mouse_down: false + }; + + this.grid.insert(Pos3::new(0, 0, 0), Block::Dirt(crate::types::Direction::North)); + this.grid.insert(Pos3::new(1, 0, 0), Block::Dirt(crate::types::Direction::North)); + this.grid.insert(Pos3::new(2, 0, 0), Block::Dirt(crate::types::Direction::North)); + this.grid.insert(Pos3::new(2, 1, 0), Block::Dirt(crate::types::Direction::North)); + this.grid.insert(Pos3::new(2, 1, 1), Block::Dirt(crate::types::Direction::North)); + + this + } +} + +fn get_screen_coords(pos: &Pos3, scale: f32, center: Vec2) -> (f32, f32) { + let w_i = TEXTURE_INNER_WIDTH * scale; + let width = TEXTURE_WIDTH * scale; + let height = TEXTURE_HEIGHT * scale; + let h_i = TEXTURE_INNER_HEIGHT * scale; + + let dx = pos.y - pos.x; + let dy = pos.y + pos.x; + + let x = screen_width() / 2.0 + + dx as f32 * w_i / 2.0 + + center.x + - width / 2.0; + let y = screen_height() / 2.0 + + dy as f32 * h_i / 2.0 + - pos.z as f32 * h_i + + center.y + - height / 2.0; + + return (x,y); +} + +/// A block's face +/// /'\ +/// / A \ +/// |\ /| +/// | \./ | +/// \ B|C | +/// \.|./ +/// A - Top +/// B- Left +/// C -Right +enum Face { + Top, + Left, + Right +} +impl Face { + /// gets the face (if any) that was clicked + /// coordinates have to be normalized, + /// the top left should be (0,0) + fn from_xy(x: f32, y: f32) -> Option { + + // test B face + { + let ox = (TEXTURE_WIDTH / 2.0 - x).abs(); + let oy = (TEXTURE_Y_WHITESPACE + TEXTURE_DEPTH / 2.0 - y).abs(); + + let y_max = TEXTURE_DEPTH / 2.0; + let x_max = TEXTURE_WIDTH / 2.0; + + let grow = (0.0 - y_max)/(x_max - 0.0); + let cy = grow * ox + y_max; + + if cy <= oy { + return Some(Face::Top); + } + + } + + None + } +} + +impl GameComponent for BuildScreen { + fn draw(&self, assets: &AssetStore) { + // order: x (inc) -> y (inc) -> z (inc) + let mut render_order: Vec<(&Pos3, &Block)> = self.grid.iter().collect(); + render_order.sort_by_key(|(pos, _)| pos.x + pos.y*2 + pos.z*3 ); + + for (pos, block) in render_order.iter() { + let texture = block.get_texture(&assets); + let width = TEXTURE_WIDTH * self.cam.scale; + let height = TEXTURE_HEIGHT * self.cam.scale; + + let (x,y) = get_screen_coords(pos, self.cam.scale, self.cam.center); + + if x >= -width && x <= screen_width() + && y >= -width && y <= screen_height() { + // render block + draw_texture_ex( + texture, + x, + y, + WHITE, + DrawTextureParams { + dest_size: Some(Vec2::new(width, height)), + source: None, + rotation: 0.0, + flip_x: false, + flip_y: false, + pivot: Some(Vec2::new(width / 2.0, height / 2.0)) + }); + } + } + } + fn ev_loop(&mut self) -> GameEvent { + // mouse input + if is_mouse_button_down(MouseButton::Left) { + // currently holding button down + self.mouse_down = true; + } else if self.mouse_down { + // button was released + self.mouse_down = false; + // determine which block / side was clicked + let (mx, my) = mouse_position(); + + // virtual render cycle + let render_order: Vec<(&Pos3, &Block)> = self.grid.iter().collect(); + // no need to sort the first time + //render_order.sort_by_key(|(pos, _)| pos.x + pos.y*2 + pos.z*3 ); + + // list of positions in render que for given pixel + // (mx, my) + let mut in_path:Vec<&Pos3> = Vec::new(); + + for (pos, _) in render_order.iter() { + let (x,y) = get_screen_coords(pos, self.cam.scale, self.cam.center); + + if mx >= x + && mx <= x + TEXTURE_WIDTH + && my >= y + TEXTURE_Y_WHITESPACE + && my <= y + TEXTURE_HEIGHT { + + // check if mouse is above transparent area + // and skip block if necessary + if Face::from_xy(mx-x, my-y).is_none() { + println!("skipping"); + continue; + } + + // block in mouse path + in_path.push(pos); + } + } + in_path.sort_by_key(|pos| pos.x + pos.y*2 + pos.z*3 ); + if let Some(pos) = in_path.last() { + // position of clicked block + // because it is the last block in de render queue + // for a given pixel + let pos = Pos3::new(pos.x, pos.y, pos.z); + let (x,y) = get_screen_coords(&pos, self.cam.scale, self.cam.center); + let face = Face::from_xy(mx-x, my-y); + println!("{:?}", face); + + // TODO: determine side + self.grid.insert(pos, Block::GrassCenter(crate::types::Direction::West)); + } + } + + // zoom with Ctrl-MouseWheel + if is_key_down(KeyCode::LeftControl) { + let mut scale = self.cam.scale; + let (_, wy) = mouse_wheel(); + + scale += wy as f32 * 1.0/10.0; + + if scale > 0.05 && scale < 6.0 { + self.cam.scale = scale; + } + } + + // keyboard control + if is_key_down(KeyCode::Down) { + self.cam.center.y += 1.0 * (1.0/self.cam.scale); + } + if is_key_down(KeyCode::Up) { + self.cam.center.y -= 1.0 * (1.0/self.cam.scale); + } + if is_key_down(KeyCode::Left) { + self.cam.center.x -= 1.0 * (1.0/self.cam.scale); + } + if is_key_down(KeyCode::Right) { + self.cam.center.x += 1.0 * (1.0/self.cam.scale); + } + + GameEvent::None + } +} diff --git a/src/screens/build.rs b/src/screens/build.rs index 02fa2b0..bd36891 100644 --- a/src/screens/build.rs +++ b/src/screens/build.rs @@ -21,16 +21,18 @@ impl Camera { } } -const TARGET_RENDER_SIZE:f32 = 128.0; - /// image width const TEXTURE_WIDTH: f32 = 256.0; /// actual width inside ob cube const TEXTURE_INNER_WIDTH: f32 = 223.0; // 216 /// height of image const TEXTURE_HEIGHT: f32 = 352.0; +/// empty space above texture +const TEXTURE_Y_WHITESPACE: f32 = 130.0; /// actual height of cube const TEXTURE_INNER_HEIGHT: f32 = 108.0; // 110 +/// height of top surface +const TEXTURE_DEPTH: f32 = 100.0; pub struct BuildScreen { grid: HashMap, @@ -51,33 +53,109 @@ impl BuildScreen { } } -impl GameComponent for BuildScreen { - fn draw(&self, assets: &AssetStore) { - // order: x (inc) -> y (inc) -> z (inc) - let mut render_order: Vec<(&Pos3, &Block)> = self.grid.iter().collect(); - render_order.sort_by_key(|(pos, _)| pos.x + pos.y*2 + pos.z*3 ); - - for (pos, block) in render_order.iter() { - let texture = block.get_texture(&assets); - - let w_i = TEXTURE_INNER_WIDTH * self.cam.scale; - let width = TEXTURE_WIDTH * self.cam.scale; - let height = TEXTURE_HEIGHT * self.cam.scale; - let h_i = TEXTURE_INNER_HEIGHT * self.cam.scale; +fn get_screen_coords(pos: &Pos3, scale: f32, center: Vec2) -> (f32, f32) { + let w_i = TEXTURE_INNER_WIDTH * scale; + let width = TEXTURE_WIDTH * scale; + let height = TEXTURE_HEIGHT * scale; + let h_i = TEXTURE_INNER_HEIGHT * scale; let dx = pos.y - pos.x; let dy = pos.y + pos.x; let x = screen_width() / 2.0 + dx as f32 * w_i / 2.0 - + self.cam.center.x + + center.x - width / 2.0; let y = screen_height() / 2.0 + dy as f32 * h_i / 2.0 - pos.z as f32 * h_i - + self.cam.center.y + + center.y - height / 2.0; + return (x,y); +} + +/// A block's face +/// /'\ +/// / A \ +/// |\ /| +/// | \./ | +/// \ B|C | +/// \.|./ +/// A - Top +/// B- Left +/// C -Right +#[derive(Debug)] +enum Face { + Top, + Left, + Right +} +impl Face { + /// gets the face (if any) that was clicked + /// coordinates have to be normalized, + /// the top left should be (0,0) + fn from_xy(x: f32, y: f32, scale: f32) -> Option { + + // test A face + { + let ox = (TEXTURE_WIDTH * scale / 2.0 - x).abs(); + let oy = (TEXTURE_DEPTH * scale / 2.0 + TEXTURE_Y_WHITESPACE * scale - y).abs(); + + let y_max = TEXTURE_DEPTH * scale / 2.0; + let x_max = TEXTURE_WIDTH * scale / 2.0; + + let grow = (0.0 - y_max)/(x_max - 0.0); + let cy = grow * ox + y_max; + + if cy >= oy { + return Some(Face::Top); + } + } + + // test B and C faces + { + let ox = TEXTURE_WIDTH * scale / 2.0 - x; + let oy = y - (TEXTURE_DEPTH * scale / 2.0 + TEXTURE_Y_WHITESPACE * scale); + + let ox_abs = ox.abs(); + + let y_max = TEXTURE_DEPTH * scale / 2.0; + let x_max = TEXTURE_WIDTH * scale / 2.0; + + let grow = (0.0 - y_max)/(x_max - 0.0); + + let y_top = grow * ox_abs + y_max; + let y_bottom = grow * ox_abs + y_max + TEXTURE_INNER_HEIGHT * scale; + + if oy >= y_top && oy <= y_bottom { + if x > TEXTURE_WIDTH * scale / 2.0 { + // C face + return Some(Face::Right); + } else { + // B face + return Some(Face::Left); + } + } + } + + None + } +} + +impl GameComponent for BuildScreen { + fn draw(&self, assets: &AssetStore) { + // order: x (inc) -> y (inc) -> z (inc) + let mut render_order: Vec<(&Pos3, &Block)> = self.grid.iter().collect(); + render_order.sort_by_key(|(pos, _)| pos.x + pos.y*2 + pos.z*3 ); + + for (pos, block) in render_order.iter() { + let texture = block.get_texture(&assets); + let width = TEXTURE_WIDTH * self.cam.scale; + let height = TEXTURE_HEIGHT * self.cam.scale; + + let (x,y) = get_screen_coords(pos, self.cam.scale, self.cam.center); + if x >= -width && x <= screen_width() && y >= -width && y <= screen_height() { // render block @@ -92,9 +170,8 @@ impl GameComponent for BuildScreen { rotation: 0.0, flip_x: false, flip_y: false, - pivot: None + pivot: Some(Vec2::new(width / 2.0, height / 2.0)) }); - } } } @@ -106,10 +183,59 @@ impl GameComponent for BuildScreen { } else if self.mouse_down { // button was released self.mouse_down = false; - // TODO: determine which block / side was clicked + // determine which block / side was clicked let (mx, my) = mouse_position(); - let size = TARGET_RENDER_SIZE * self.cam.scale; + // virtual render cycle + let render_order: Vec<(&Pos3, &Block)> = self.grid.iter().collect(); + + // list of positions in render que for given pixel + // (mx, my) + let mut in_path:Vec<&Pos3> = Vec::new(); + + for (pos, _) in render_order.iter() { + let (x,y) = get_screen_coords(pos, self.cam.scale, self.cam.center); + + if mx >= x + && mx <= x + TEXTURE_WIDTH * self.cam.scale + && my >= y + TEXTURE_Y_WHITESPACE * self.cam.scale + && my <= y + TEXTURE_HEIGHT * self.cam.scale { + + // check if mouse is above transparent area + // and skip block if necessary + if Face::from_xy(mx-x, my-y, self.cam.scale).is_none() { + println!("skipping"); + continue; + } + + // block in mouse path + in_path.push(pos); + } + } + // weight axis + in_path.sort_by_key(|pos| pos.x + pos.y*2 + pos.z*3 ); + if let Some(pos) = in_path.last() { + // position of clicked block + // because it is the last block in de render queue + // for a given pixel + let mut pos = Pos3::new(pos.x, pos.y, pos.z); + let (x,y) = get_screen_coords(&pos, self.cam.scale, self.cam.center); + let face = Face::from_xy(mx-x, my-y, self.cam.scale); + + match face.unwrap() { + Face::Top => { + pos.z+=1; + }, + Face::Left => { + pos.x+=1; + }, + Face::Right => { + pos.y+=1; + } + } + + self.grid.insert(pos, Block::GrassCenter(crate::types::Direction::West)); + } } // zoom with Ctrl-MouseWheel diff --git a/src/textures.rs b/src/textures.rs index f5af25f..06fc8ad 100644 --- a/src/textures.rs +++ b/src/textures.rs @@ -63,7 +63,8 @@ impl DirectionalTexture { } } pub struct BlockAssetCollection { - pub dirt: DirectionalTexture + pub dirt: DirectionalTexture, + pub grass_center: DirectionalTexture } impl AssetStore { @@ -80,7 +81,8 @@ impl AssetStore { &in_ui_folder("PNG/buttonLong_brown_pressed.png")) .await.expect("Unable to load texture")), blocks: BlockAssetCollection { - dirt: DirectionalTexture::from_auto_png("Tiles/dirt_center").await + dirt: DirectionalTexture::from_auto_png("Tiles/dirt_center").await, + grass_center: DirectionalTexture::from_auto_png("Tiles/grass_center").await } } } -- 2.38.5