~comcloudway/little_town

ebcefd440ca1372c5e9845226340cb8913a58855 — Jakob Meier 1 year, 8 months ago d27cbce
Basic face detection and block placement
4 files changed, 393 insertions(+), 25 deletions(-)

M src/blocks.rs
A src/screens/#
M src/screens/build.rs
M src/textures.rs
M src/blocks.rs => src/blocks.rs +4 -2
@@ 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)
        }
    }
}

A src/screens/# => src/screens/# +238 -0
@@ 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<Pos3, Block>,
    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<Self> {

        // 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
    }
}

M src/screens/build.rs => src/screens/build.rs +147 -21
@@ 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<Pos3, Block>,


@@ 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<Self> {

        // 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

M src/textures.rs => src/textures.rs +4 -2
@@ 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
            }
        }
    }