diff options
| author | Arne Dußin | 2021-01-27 14:01:50 +0100 |
|---|---|---|
| committer | Arne Dußin | 2021-02-02 22:16:15 +0100 |
| commit | f92e9f6f07b1e3834c2ca58ce3510734819d08e4 (patch) | |
| tree | 20e3d3afce342a56ae98f6c20491482ccd2b5c6b /src/client | |
| parent | c60a6d07efb120724b308e29e8e70f27c87c952d (diff) | |
| download | graf_karto-f92e9f6f07b1e3834c2ca58ce3510734819d08e4.tar.gz graf_karto-f92e9f6f07b1e3834c2ca58ce3510734819d08e4.zip | |
Rework graf karto to fit the client/server structure
Diffstat (limited to 'src/client')
35 files changed, 3973 insertions, 0 deletions
diff --git a/src/client/cli/cmd/edit.rs b/src/client/cli/cmd/edit.rs new file mode 100644 index 0000000..7a02959 --- /dev/null +++ b/src/client/cli/cmd/edit.rs @@ -0,0 +1,41 @@ +//! Replace the contents of the currently edited map with contents from a file. + +use super::Command; +use super::{CmdParseError, FromArgs}; +use crate::client::map::MapData; +use crate::client::Editor; +use std::path::PathBuf; + +/// Command to load a file from the disk and replace the current editor contents with it's info. +pub struct Edit { + file: PathBuf, +} + +impl FromArgs for Edit { + fn from_args(args: &[&str]) -> Result<Self, CmdParseError> { + if args.len() != 1 { + return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1)); + } + + Ok(Self { + file: PathBuf::from(args[0]), + }) + } +} + +impl Command for Edit { + fn process(&self, editor: &mut Editor) -> Result<String, String> { + let data = match MapData::load_from_file(&self.file) { + Ok(data) => data, + Err(err) => { + return Err(format!( + "Unable to read file: {:?}, reason: {:?}", + &self.file, err + )) + } + }; + + editor.map_mut().set_data(data); + Ok(format!("Map data from {:?} loaded.", &self.file)) + } +} diff --git a/src/client/cli/cmd/mod.rs b/src/client/cli/cmd/mod.rs new file mode 100644 index 0000000..b403772 --- /dev/null +++ b/src/client/cli/cmd/mod.rs @@ -0,0 +1,59 @@ +//! The commands that can be performed in the CLI + +pub mod edit; +pub mod read; +pub mod write; + +pub use edit::*; +pub use read::*; +pub use write::*; + +use crate::client::Editor; +use std::ops::RangeInclusive; + +/// Errors that can occur when parsing a command. This is for syntax checking, the +/// semantics are checked when trying to execute the command. +#[allow(missing_docs)] +#[derive(thiserror::Error, Debug)] +pub enum CmdParseError { + #[error("no command specified")] + StringEmpty, + #[error("the command {0} is unknown")] + NoSuchCmd(String), + #[error("wrong number of arguments. Expected in range {1:?}, but received {0}")] + WrongNumberOfArgs(usize, RangeInclusive<usize>), + #[error("{0} cannot be converted into a {1}, which is required")] + InvalidArgType(String, &'static str), +} + +/// Attempts to parse a command from the given string. If it is unsuccessful, it returns a +/// [CmdParseError]. +pub fn parse_command(string: &str) -> Result<Box<dyn Command>, CmdParseError> { + if string.is_empty() { + return Err(CmdParseError::StringEmpty); + } + + let parts: Vec<&str> = string.split_whitespace().collect(); + match parts[0] { + "w" => Ok(Box::new(Write::from_args(&parts[1..])?)), + "e" => Ok(Box::new(Edit::from_args(&parts[1..])?)), + "r" => Ok(Box::new(Read::from_args(&parts[1..])?)), + other => Err(CmdParseError::NoSuchCmd(other.to_owned())), + } +} + +/// Indicates that this entity (command) can be created from arguments. Make sure to check what is +/// expected, to pass the arguments to the correct command. +pub trait FromArgs: Sized { + /// Creates a new instance from the arguments provided. If for whatever reason the syntax of the + /// given arguments is correct an [ArgParseError] is returned. + fn from_args(args: &[&str]) -> Result<Self, CmdParseError>; +} + +/// A common trait for all commands. +pub trait Command { + /// Process this command on the provided context. Returns either a string with the output of the + /// command when everything went right with it, or an error string explaining what went wrong, + /// which can be displayed to the user. + fn process(&self, editor: &mut Editor) -> Result<String, String>; +} diff --git a/src/client/cli/cmd/read.rs b/src/client/cli/cmd/read.rs new file mode 100644 index 0000000..313530a --- /dev/null +++ b/src/client/cli/cmd/read.rs @@ -0,0 +1,44 @@ +//! Read the contents of a file and add it to the currently edited map. + +use super::Command; +use super::{CmdParseError, FromArgs}; +use crate::client::Editor; +use crate::map::MapData; +use std::path::PathBuf; + +/// Command to read a file from the system +pub struct Read { + file: PathBuf, +} + +impl FromArgs for Read { + fn from_args(args: &[&str]) -> Result<Self, CmdParseError> { + if args.len() != 1 { + return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1)); + } + + Ok(Self { + file: PathBuf::from(args[0]), + }) + } +} + +impl Command for Read { + fn process(&self, editor: &mut Editor) -> Result<String, String> { + let data = match MapData::load_from_file(&self.file) { + Ok(data) => data, + Err(err) => { + return Err(format!( + "Unable to read file: {:?}, reason: {:?}", + &self.file, err + )) + } + }; + + editor.map_mut().add_data(data); + Ok(format!( + "Map data from {:?} read and added to the current buffer.", + &self.file + )) + } +} diff --git a/src/client/cli/cmd/write.rs b/src/client/cli/cmd/write.rs new file mode 100644 index 0000000..3114f63 --- /dev/null +++ b/src/client/cli/cmd/write.rs @@ -0,0 +1,41 @@ +//! Save the contents of the map to disk + +use super::Command; +use super::{CmdParseError, FromArgs}; +use crate::client::Editor; +use std::path::PathBuf; + +/// The save command can take any destination in the filesystem the user can write to. Processing +/// will then save the map contents to that destination, overwriting anything that may be there. +pub struct Write { + destination: PathBuf, +} + +impl FromArgs for Write { + fn from_args(args: &[&str]) -> Result<Self, CmdParseError> { + if args.len() != 1 { + return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1)); + } + + Ok(Self { + destination: PathBuf::from(args[0]), + }) + } +} + +impl Command for Write { + fn process(&self, editor: &mut Editor) -> Result<String, String> { + let data = MapData::extract_data(editor.map()); + + match data.write_to_file(&self.destination) { + Ok(_) => Ok(format!( + "Successfully wrote contents to `{:?}`", + &self.destination + )), + Err(e) => Err(format!( + "Unable to write to `{:?}`. Error: {:?}", + &self.destination, e + )), + } + } +} diff --git a/src/client/cli/mod.rs b/src/client/cli/mod.rs new file mode 100644 index 0000000..ed2a1bf --- /dev/null +++ b/src/client/cli/mod.rs @@ -0,0 +1,128 @@ +//! In-window Command line interface. Used for operations that are just easier than with GUI. +//! +//! Sometimes it is nice to have a GUI, for instance when a selection has to be made, things have to +//! be moved etc., however for operations like saving/loading and exporting, no such thing has to be +//! done and the GUI is really just slowing you down (at least in my opinion). For these operations, +//! it is much better to simply have a command do that specific thing. It is also much easier to +//! implement a new command, so features can be tested more quickly. For some things, there should +//! still be a GUI option. With the example of saving/loading, it is much easier to find some hidden +//! folder in a GUI, so that is definitely a consideration for the future. + +pub mod cmd; +pub use self::cmd::*; + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::input::{Button, Input}; +use crate::client::Editor; +use crate::math::Vec2; +use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; +use std::sync::mpsc::Receiver; + +/// The command line interface. Should be created only once per program instance. +pub struct CLI { + text: String, + char_pipe: Option<Receiver<char>>, +} + +impl CLI { + /// Create a CLI for this instance + pub fn new(input: &mut Input) -> Self { + input.add_global(Button::Text(':').into()); + + Self { + text: String::new(), + char_pipe: None, + } + } + + /// Activates the CLI, which will now capture keyboard input and execute commands accordingly. + pub fn try_activate(&mut self, input: &mut Input) { + if !self.active() { + self.char_pipe = input.try_capture_keyboard(); + + if self.char_pipe.is_some() { + self.text = ":".to_owned(); + } + } + } + + /// Deactivate the command line. + pub fn deactivate(&mut self) { + // Hang up, the input handler will get the message. + self.char_pipe = None; + } + + /// Checks if the CLI is currently active. This means input to other things should be ignored. + pub fn active(&self) -> bool { + self.char_pipe.is_some() + } + + fn perform_command(&mut self, editor: &mut Editor) { + match cmd::parse_command(&self.text[1..]) { + Ok(cmd) => match cmd.process(editor) { + Ok(res) => self.text = format!("SUCCESS: {}", res), + Err(err) => self.text = format!("ERROR: {}", err), + }, + Err(err) => self.text = format!("SYNTAX ERROR: {}", err), + } + } + + /// Handle input for the command line and perform any commands the user may want to run. + pub fn update(&mut self, editor: &mut Editor, input: &mut Input) { + /* Check if the CLI is currently active. If not and it should not be activated according to + * keyboard input, there is nothing to do. + */ + if !self.active() { + if input.poll_global(&Button::Text(':').into()) { + self.try_activate(input); + if !self.active() { + return; + } + } else { + return; + } + } + + let rx = self + .char_pipe + .as_mut() + .expect("No character pipe eventhough CLI should be active"); + + if let Ok(c) = rx.try_recv() { + match c { + '\x1B' => self.text.clear(), // Escape + '\x7f' => { + self.text.pop(); + } // Backspace + '\n' => { + self.perform_command(editor); + self.deactivate(); + } + c => self.text.push(c), + } + } + + // When the text is empty, there is also no command marker, so set as inactive and leave. + if self.text.is_empty() { + self.deactivate() + } + } + + /// Draw the command line at the bottom of the window. + pub fn draw(&self, rld: &mut RaylibDrawHandle) { + let pos = Vec2::new(150., rld.get_screen_height() as f32 - 25.); + + rld.draw_rectangle_v( + pos, + Vec2::new(rld.get_screen_width() as f32 - pos.x, 25.), + DEFAULT_COLOURS.cli_background, + ); + rld.draw_text( + &self.text, + 155, + rld.get_screen_height() - 22, + 20, + DEFAULT_COLOURS.cli_foreground, + ); + } +} diff --git a/src/client/colours.rs b/src/client/colours.rs new file mode 100644 index 0000000..d7c728c --- /dev/null +++ b/src/client/colours.rs @@ -0,0 +1,158 @@ +//! The colour definitions used for items drawn in graf karto. + +use raylib::ffi::Color; + +/// Contains the default colours used throughout, if nothing else is set. +pub const DEFAULT_COLOURS: Colours = Colours::default(); + +/// All the different colours that may be used for different elements of the program. Contains one +/// entry for each colourable component. +pub struct Colours { + /// Colour the rectangle used for the deletion tool is filled with. + pub deletion_rect: Color, + /// The colour of the outline of the deletion tool rectangle. + pub deletion_rect_outline: Color, + /// The colour that is used for filling the selection tool's rectangle. + pub selection_rect: Color, + /// Colour of the selection tool rectangle outline. + pub selection_rect_outline: Color, + /// Colour of the rooms that are currently not selected. + pub room_normal: Color, + /// The Colour the rooms should be tinted in when they have been selected. + pub room_selected: Color, + /// Colour of the walls when they are not selected. + pub wall_normal: Color, + /// Colour of the walls when they have been selected. + pub wall_selected: Color, + /// Colour of the icons when they are not selected. + pub icon_normal: Color, + /// Colour of the icons when they are selected. + pub icon_selected: Color, + /// Colour used to draw the rulers (the ruling lines) of the dimension indicator. + pub dimension_indicators: Color, + /// Colour of the text used to display the size of the dimension indicators dimensions. + pub dimension_text: Color, + /// Colour the point to show where something is will be drawn in. + pub position_indicator: Color, + /// Colour that is used for the text stating the position of the position indicator in meters. + pub position_text: Color, + /// The colour used for drawing the lines of the grid which divides the map into chunks of evenly + /// spaced cells. + pub grid_lines: Color, + /// Color used to draw the background of the Command Line Interface + pub cli_background: Color, + /// Color used to draw the normal text of the Command Line Interface + pub cli_foreground: Color, +} + +impl Colours { + // NOTE: Unfortunately the default function cannot be made const, since Default is a trait. This + // feature is, as far as I can tell, planned in Rust, but not yet implemented. Once it is, Colours + // should implement Default instead. + const fn default() -> Self { + Self { + deletion_rect: Color { + r: 200, + g: 150, + b: 150, + a: 50, + }, + deletion_rect_outline: Color { + r: 200, + g: 150, + b: 150, + a: 150, + }, + selection_rect: Color { + r: 255, + g: 129, + b: 0, + a: 50, + }, + selection_rect_outline: Color { + r: 255, + g: 129, + b: 0, + a: 150, + }, + room_normal: Color { + r: 180, + g: 180, + b: 180, + a: 255, + }, + room_selected: Color { + r: 150, + g: 200, + b: 150, + a: 255, + }, + wall_normal: Color { + r: 200, + g: 120, + b: 120, + a: 255, + }, + wall_selected: Color { + r: 150, + g: 200, + b: 150, + a: 255, + }, + icon_normal: Color { + r: 255, + g: 255, + b: 255, + a: 255, + }, + icon_selected: Color { + r: 150, + g: 200, + b: 150, + a: 255, + }, + dimension_indicators: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + dimension_text: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + position_indicator: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + position_text: Color { + r: 200, + g: 200, + b: 200, + a: 255, + }, + grid_lines: Color { + r: 255, + g: 255, + b: 255, + a: 75, + }, + cli_background: Color { + r: 100, + g: 100, + b: 100, + a: 150, + }, + cli_foreground: Color { + r: 255, + g: 255, + b: 255, + a: 200, + }, + } + } +} diff --git a/src/client/config.rs b/src/client/config.rs new file mode 100644 index 0000000..96ff3f5 --- /dev/null +++ b/src/client/config.rs @@ -0,0 +1,166 @@ +//! Home of the user configuratable content of graf karto, like keybindings and (TODO) colours etc. + +use crate::client::input::{Binding, Button, Input, MouseButton, Scancode}; +use ron::de::from_reader; +use ron::ser::{to_string_pretty, PrettyConfig}; +use serde::{Deserialize, Serialize}; +use std::fs::File; +use std::io::{self, Write}; +use std::path::Path; + +/// All configuration parameters the user can set are contained in this struct. +#[allow(missing_docs)] +#[derive(Deserialize, Serialize)] +pub struct Config { + pub tool_activation_binds: ToolActivationBinds, + pub tool_general_binds: ToolGeneralBinds, + pub icon_binds: IconToolBinds, +} + +#[allow(missing_docs)] +#[derive(Deserialize, Serialize)] +/// The keys used to activate the individual tools. These keystrokes will not be sent to the tools, +/// but instead will be handled by the editor where the tools are registered. +pub struct ToolActivationBinds { + pub deletion: Binding, + pub icon: Binding, + pub polygon_room: Binding, + pub rect_room: Binding, + pub selection: Binding, + pub wall: Binding, +} + +#[derive(Deserialize, Serialize)] +/// Keys that are useful to most tools. These are packaged so that not every tool has the same n keys +/// and then some more. +pub struct ToolGeneralBinds { + /// Keybinding to, where applicable, place a single node (usually a vertex) for the tool in + /// question. + pub place_single: Binding, + /// Finish up whatever one is doing with the current tool, without removing information. + pub finish: Binding, + /// Abort whatever one is doing with the current tool which means the last atomic action will not + /// be pushed into the map items. + pub abort: Binding, +} + +#[derive(Clone, Serialize, Deserialize)] +/// Key bindings that are individually interesting to the icon tool. +pub struct IconToolBinds { + /// Key to change to the next icon of the icon list. + pub next: Binding, + /// Key to change to the previous icon of the icon list. + pub previous: Binding, + /// Rotate the working icon clockwise by a certain amount (currently 45 degrees) + pub rotate_clockwise: Binding, + /// Rotate the working icon counterclockwise by a certain amount (currently 45 degrees) + pub rotate_counterclockwise: Binding, +} + +impl Config { + /// Try to parse a configuration from the file located at path. + /// + /// # Errors + /// If the file is not found or can not be read or parsed for a different reason, an IO-Error is + /// returned. + pub fn from_file<P: AsRef<Path>>(path: P) -> io::Result<Config> { + let file = File::open(&path)?; + match from_reader(file) { + Ok(data) => Ok(data), + Err(err) => Err(io::Error::new(io::ErrorKind::InvalidData, err)), + } + } + + /// Try to write the configuration to the file at path. If the file exists, it will be overwritten. + /// + /// # Errors + /// If the file can not be written, for example for lack of permissions, an IO-Error is returned. + pub fn write_file<P: AsRef<Path>>(&self, path: P) -> io::Result<()> { + let mut file = File::create(&path)?; + + let pretty_conf = PrettyConfig::new() + .with_depth_limit(4) + .with_decimal_floats(true) + .with_separate_tuple_members(true) + .with_indentor("\t".to_owned()); + let string = match to_string_pretty(&self, pretty_conf) { + Ok(string) => string, + Err(err) => { + return Err(io::Error::new(io::ErrorKind::InvalidInput, err)); + } + }; + + file.write_all(&string.as_bytes()) + } +} + +/// Registers all bindings from the given configuration into the input handler. Should the +/// configuration change at runtime, the global bindings of the input handler need to be cleared and +/// this function must be called again. +pub fn register_bindings(config: &Config, input: &mut Input) { + if !input.add_global(config.tool_activation_binds.deletion.clone()) { + warn!("Tried to add deletion binding twice."); + } + if !input.add_global(config.tool_activation_binds.icon.clone()) { + warn!("Tried to add icon binding twice."); + } + if !input.add_global(config.tool_activation_binds.polygon_room.clone()) { + warn!("Tried to add polygon room binding twice."); + } + if !input.add_global(config.tool_activation_binds.rect_room.clone()) { + warn!("Tried to add rect room binding twice."); + } + if !input.add_global(config.tool_activation_binds.selection.clone()) { + warn!("Tried to add selection binding twice."); + } + if !input.add_global(config.tool_activation_binds.wall.clone()) { + warn!("Tried to add wall binding twice."); + } + if !input.add_global(config.tool_general_binds.place_single.clone()) { + warn!("Tried to add place single binding twice."); + } + if !input.add_global(config.tool_general_binds.finish.clone()) { + warn!("Tried to add finish binding twice."); + } + if !input.add_global(config.tool_general_binds.abort.clone()) { + warn!("Tried to add abort binding twice."); + } + if !input.add_global(config.icon_binds.next.clone()) { + warn!("Tried to add next binding twice."); + } + if !input.add_global(config.icon_binds.previous.clone()) { + warn!("Tried to add previous binding twice."); + } + if !input.add_global(config.icon_binds.rotate_clockwise.clone()) { + warn!("Tried to add rotate clockwise binding twice."); + } + if !input.add_global(config.icon_binds.rotate_counterclockwise.clone()) { + warn!("Tried to add rotate counterclockwise binding twice."); + } +} + +impl Default for Config { + fn default() -> Self { + Config { + tool_activation_binds: ToolActivationBinds { + deletion: Button::Text('d').into(), + icon: Button::Text('i').into(), + polygon_room: Button::Text('p').into(), + rect_room: Button::Text('r').into(), + selection: Button::Text('s').into(), + wall: Button::Text('w').into(), + }, + tool_general_binds: ToolGeneralBinds { + place_single: Button::Mouse(MouseButton::Left).into(), + finish: Button::Scancode(Scancode::Enter).into(), + abort: Button::Mouse(MouseButton::Right).into(), + }, + icon_binds: IconToolBinds { + next: Button::Text('j').into(), + previous: Button::Text('k').into(), + rotate_clockwise: Button::Text('+').into(), + rotate_counterclockwise: Button::Text('-').into(), + }, + } + } +} diff --git a/src/client/editor.rs b/src/client/editor.rs new file mode 100644 index 0000000..0fb5794 --- /dev/null +++ b/src/client/editor.rs @@ -0,0 +1,189 @@ +//! Element creation base +//! +//! The actual editor environment sits here. This especially means all tools that require low-level +//! access to the data of items currently being created. While this may be subject to change, there is +//! currently a difference between things that are being created (inside the editor) and things that +//! are part of the environment (the map). + +use crate::client::config::Config; +use crate::client::input::{Binding, Input}; +use crate::client::map::Map; +use crate::client::snapping::Snapper; +use crate::client::tool::*; +use crate::client::transform::Transform; +use crate::net::{Cargo, Connection}; +use raylib::core::drawing::RaylibDrawHandle; +use raylib::{RaylibHandle, RaylibThread}; +use std::collections::HashMap; + +/// The editor containing all tools and currently the map of the stuff that has been created. +pub struct Editor { + map: Map, + /// HashMap that matches the ToolType with its proper activation key and of course the tool + /// itself. + tools: HashMap<ToolType, (Box<dyn Tool>, Binding)>, + active: ToolType, + config: Config, + server: Connection<Cargo>, +} + +impl Editor { + /// Create a new editor with all tools necessary. There should be only one editor per program + /// instance. + pub fn new( + rl: &mut RaylibHandle, + rlt: &RaylibThread, + config: Config, + server: Connection<Cargo>, + ) -> Self { + let map = Map::new(rl, rlt); + + let mut tools: HashMap<ToolType, (Box<dyn Tool>, Binding)> = + HashMap::with_capacity(ToolType::NumTools as usize); + + tools.insert( + ToolType::RectRoomTool, + ( + Box::new(RectRoomTool::new()), + config.tool_activation_binds.rect_room.clone(), + ), + ); + tools.insert( + ToolType::PolygonRoomTool, + ( + Box::new(PolygonRoomTool::new()), + config.tool_activation_binds.polygon_room.clone(), + ), + ); + tools.insert( + ToolType::WallTool, + ( + Box::new(WallTool::new()), + config.tool_activation_binds.wall.clone(), + ), + ); + tools.insert( + ToolType::IconTool, + ( + Box::new(IconTool::new( + config.icon_binds.clone(), + map.icon_renderer(), + )), + config.tool_activation_binds.icon.clone(), + ), + ); + tools.insert( + ToolType::DeletionTool, + ( + Box::new(DeletionTool::new()), + config.tool_activation_binds.deletion.clone(), + ), + ); + tools.insert( + ToolType::SelectionTool, + ( + Box::new(SelectionTool::new()), + config.tool_activation_binds.selection.clone(), + ), + ); + + assert_eq!(ToolType::NumTools as usize, tools.len()); + + Self { + map, + tools, + active: ToolType::RectRoomTool, + config, + server, + } + } + + /// Get the currently active tool. Since the every tool exists only once, it is entirely indexable + /// by its type, which is what is actually returned. + pub fn active(&self) -> ToolType { + self.active + } + + /// Set the currently active tool. Any process currently going on in a different tool will be + /// aborted. + pub fn set_active(&mut self, tool: ToolType) { + if tool != self.active { + self.tools.get_mut(&self.active).unwrap().0.deactivate(); + self.active = tool; + self.tools + .get_mut(&self.active) + .expect("{:?} is not a Tool in the Editor. Maybe you forgot to register it?") + .0 + .activate(); + } + } + + /// Update the internal editor data where necessary and handle selecting different tools, aswell + /// as updating the currently active tool. Should be called once every frame. + pub fn update( + &mut self, + rl: &mut RaylibHandle, + transform: &Transform, + snapper: &Snapper, + input: &mut Input, + ) { + // Handle keybindings for tool change + for (&tool_type, (_, activation_bind)) in self.tools.iter() { + if input.poll_global(&activation_bind) { + // Don't do anything if the tool does not change. + if tool_type == self.active { + break; + } + + // Activate the tool of which the key binding has been pressed. + self.set_active(tool_type); + break; + } + } + + let mouse_pos_m = transform.point_px_to_m(&rl.get_mouse_position().into()); + let snapped_mouse_pos = snapper.snap(mouse_pos_m); + + // Update the currently active tool + let active_tool = &mut self.tools.get_mut(&self.active).unwrap().0; + active_tool.update(&self.map, &snapped_mouse_pos); + + // Handle common keybindings many of the tools have. + if input.poll_global(&self.config.tool_general_binds.place_single) { + active_tool.place_single(&mut self.map, &self.server, &snapped_mouse_pos); + } + if input.poll_global(&self.config.tool_general_binds.finish) { + active_tool.finish(&mut self.map, &self.server); + } + if input.poll_global(&self.config.tool_general_binds.abort) { + active_tool.abort(); + } + + // Handle custom keybindings in case the tool has any. + active_tool.handle_custom_bindings(&mut self.map, &self.server, input); + } + + /// Draw all tools and in case of the active tool also what is currently being edited by it, if + /// that exists. + pub fn draw_tools(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + for (tool, _) in self.tools.values() { + tool.draw(rld, transform); + } + } + + /// Get the world containing all finished elements. + pub fn map(&self) -> &Map { + &self.map + } + + /// Get the server this editor is connected to. Even if the program is executed locally, this will + /// return a server, since one must have been started locally. + pub fn server(&self) -> &Connection<Cargo> { + &self.server + } + + /// Get the server this editor is connected to mutably. + pub fn server_mut(&mut self) -> &mut Connection<Cargo> { + &mut self.server + } +} diff --git a/src/client/grid.rs b/src/client/grid.rs new file mode 100644 index 0000000..17d537d --- /dev/null +++ b/src/client/grid.rs @@ -0,0 +1,56 @@ +//! The grid used to divide the map into evenly sized chunks. + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::transform::Transform; +use crate::math; +use raylib::drawing::RaylibDraw; + +/// Draw an infinite grid that can be moved around on the screen and zoomed in and out of. +pub fn draw_grid<D>(rld: &mut D, screen_width: i32, screen_height: i32, transform: &Transform) +where + D: RaylibDraw, +{ + /* Calculate the first whole meter that can be seen on the grid. This is the first meter that + * will be seen on screen. + */ + let mut first_cell = *transform.translation_px() / -transform.pixels_per_m(); + first_cell.x = first_cell.x.floor(); + first_cell.y = first_cell.y.floor(); + + let mut cell = first_cell; + let mut draw_y = transform.point_m_to_px(&cell).y; + loop { + draw_y = math::round(draw_y, 1.); + rld.draw_line( + 0, + draw_y as i32, + screen_width, + draw_y as i32, + DEFAULT_COLOURS.grid_lines, + ); + cell.y += 1.; + draw_y = transform.point_m_to_px(&cell).y; + + if draw_y as i32 > screen_height { + break; + } + } + + let mut draw_x = transform.point_m_to_px(&cell).x; + loop { + draw_x = math::round(draw_x, 1.); + rld.draw_line( + draw_x as i32, + 0, + draw_x as i32, + screen_height, + DEFAULT_COLOURS.grid_lines, + ); + cell.x += 1.; + draw_x = transform.point_m_to_px(&cell).x; + + if draw_x as i32 > screen_width { + break; + } + } +} diff --git a/src/client/gui/decimal_num_box.rs b/src/client/gui/decimal_num_box.rs new file mode 100644 index 0000000..e9395f7 --- /dev/null +++ b/src/client/gui/decimal_num_box.rs @@ -0,0 +1,173 @@ +//! Functions similarly to a text-bux, but only accepts floating point (decimal) numbers +//! +//! Since a lot of functions require the user to input measurements in meters, it is useful to have a +//! singular entity that reads these in an intuitive way. Inputting of such numbers is handled in +//! this module. + +use crate::math::{self, Vec2}; +use nalgebra::RealField; +use num_traits::Pow; +use raylib::drawing::RaylibDraw; +use raylib::ffi::{Color, KeyboardKey}; +use raylib::text; +use raylib::RaylibHandle; +use std::str::FromStr; + +/// The number of decimal places that can be edited and will be shown by a decimal text field. +pub const DECIMAL_PLACES: u16 = 4; + +/// The decimal num box can handle any decimal number, like f32 or f64. Currently has a hard limit +/// of four decimal places, but that may change in the future. +pub struct DecimalNumBox<F: RealField + Pow<u16, Output = F> + FromStr> { + input: String, + last_value: F, + active: bool, +} + +impl<F: RealField + Pow<u16, Output = F> + FromStr> DecimalNumBox<F> { + /// Create a new Number box showing the value specified. Should the value have more then the + /// maximum number of decimal places, it will be rounded. + pub fn new(value: F) -> Self { + let value = math::round_nth_decimal(value, DECIMAL_PLACES); + let input = format!("{:.4}", value); + + Self { + input, + last_value: value, + active: false, + } + } + + /// Get the value entered by the user. If the user has something that cannot be parsed into a + /// decimal value, this differs from the string that is shown and is instead the last value + /// entered by the user that is still a valid decimal number. + pub fn value(&self) -> F { + self.last_value + } + + /// Set the value directly. This may only be done, if the box is currently not active, to protect + /// user input. Returns true if the value could be set, otherwise false. + pub fn set_value(&mut self, value: F) -> bool { + if !self.active { + self.last_value = math::round_nth_decimal(value, DECIMAL_PLACES); + // XXX: Don't use the magical 4 + self.input = format!("{:.4}", self.last_value); + true + } else { + false + } + } + + /// Check if this number box is currently active. Active means, it's capturing keyboard input. + /// If it's not active, it does not attempt to capture any keystrokes. + pub fn active(&self) -> bool { + self.active + } + + /// Set if the box is active (capturing keyboard input and adjusting it's value accordingly) or + /// not. + pub fn set_active(&mut self, active: bool) { + self.active = active + } + + /// Update this decimal box. If it is inactive, this doesn't do anything, but if it is active, it + /// captures the keyboard input, if available. Returns `true`, if the value changed, otherwise + /// `false`. Note that the string that is displayed may change, but the value does not have to. + /// This happens, if the user types something invalid. In this case, `false` is returned as well. + pub fn update(&mut self, rl: &mut RaylibHandle) -> bool { + /* If the box is currently inactive, nothing must be changed, and this function will do + * nothing. + */ + if !self.active { + return false; + } + + // TODO: Check for movement keys. + + // Delete the last character when pressing backspace. + let string_changed = if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { + self.input.pop().is_some() + } + // Check the entered numbers or decimal point. + else if let Some(key) = rl.get_key_pressed() { + match key { + // Add (at most one) decimal point to the input when entering a dot. + KeyboardKey::KEY_PERIOD => { + if !self.input.contains('.') { + self.input.push('.'); + true + } else { + false + } + } + _ => { + if key as u16 >= KeyboardKey::KEY_ZERO as u16 + && key as u16 <= KeyboardKey::KEY_NINE as u16 + { + self.input.push(key as u8 as char); + true + } else { + false + } + } + } + } else { + false + }; + + if string_changed { + // Try to parse the new string. If it doesn't work, keep the old one. + match self.input.parse::<F>() { + Ok(value) => { + let value = math::round_nth_decimal(value, DECIMAL_PLACES); + if value != self.last_value { + self.last_value = value; + true + } else { + false + } + } + Err(_) => false, + } + } else { + false + } + } + + /// Draw the number box at the given position. the `unit` parameter is used to append this text, + /// let's say for instance 'm' for meters to the text drawn to screen. Most of the time, a unit + /// makes sense to show on this number box, otherwise it can be left as an empty string. The unit + /// has no relevance to internal processes and cannot be edited by the user. + pub fn draw(&self, rld: &mut impl RaylibDraw, unit: &str, pos: &Vec2<f64>) { + let text = format!("{}{}", self.input, unit); + let width = text::measure_text(&text, 20); + + // Draw background to highlight this box if it's active. + if self.active { + rld.draw_rectangle_v( + *pos - Vec2::new(5., 5.), + Vec2::new(width as f32 + 10., 20. + 10.), + Color { + r: 120, + g: 120, + b: 120, + a: 180, + }, + ); + } + + // Draw the text of the box. + rld.draw_text( + &text, + pos.x as i32, + pos.y as i32, + 20, + Color { + r: 255, + g: 255, + b: 255, + a: 255, + }, + ) + } +} diff --git a/src/client/gui/dimension_indicator.rs b/src/client/gui/dimension_indicator.rs new file mode 100644 index 0000000..fb6394a --- /dev/null +++ b/src/client/gui/dimension_indicator.rs @@ -0,0 +1,308 @@ +//! An interface element that shows the size of the selected map items and provides a means to +//! manually change the size of them in a precise manner should need be. + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::Editor; +use crate::math::{self, Rect, Vec2}; +use crate::net::Cargo; +use nalgebra::{Matrix3, Vector2}; +use raylib::drawing::RaylibDraw; +use raylib::ffi::KeyboardKey; +use raylib::RaylibHandle; + +/// A state the [DimensionIndicator] is currently in. This determines the behaviour of it and what +/// inputs it might be waiting for. +enum State { + /// In this state, the indicator is not trying to read any keyboard input, but will instead watch + /// for any changes to the dimensions from a different source, updating its display. + Watching, + /// In this state, the indicator will capture keyboard input and attempt to set the dimensions + /// according to whatever was entered. If the dimensions cannot be set, the indicator will use + /// the last valid dimensions. + Ruling { + dim_x: String, + dim_y: String, + editing_x: bool, + }, +} + +/// Used to render the horizontal and vertical dimensions of whatever is selected on the map and, if +/// the user so desires edit them directly by entering values into it. +pub struct DimensionIndicator { + /// The [State] the dimension indicator is currently in. + state: State, + /// The last dimensions that were valid. + bounds: Rect<f64>, +} + +impl Default for State { + fn default() -> Self { + Self::Watching + } +} + +impl Default for DimensionIndicator { + fn default() -> Self { + Self { + state: State::default(), + bounds: Rect::new(0., 0., 0., 0.), + } + } +} + +impl DimensionIndicator { + /// Create a new dimension indicator. While it is possible to have multiple instances, this is + /// not generally recommended, since they will need to be managed carefully or otherwise steal + /// keystrokes from each other. + pub fn new() -> Self { + Self::default() + } + + /// Update whatever is selected on the map according to the dimension indicator rules and rulers. + pub fn update(&mut self, editor: &Editor, rl: &mut RaylibHandle) { + match self.state { + State::Watching => self.update_watching(editor.map(), rl), + State::Ruling { .. } => self.update_ruling(editor, rl), + }; + } + + fn update_watching(&mut self, map: &Map, rl: &RaylibHandle) { + let mut min: Vec2<f64> = Vec2::default(); + let mut max: Vec2<f64> = Vec2::default(); + + /* Try to find selected items. If no items exist, the dimension indicator is set to its + * default, otherwise it is adjusted to the size of the combined selection. + */ + let mut selection_exists = false; + for (_id, e) in map.elements() { + if e.selected() { + let element_bounds = e.as_component().bounding_rect(); + if selection_exists { + // Adjust the currently detected selection size. + min.x = math::partial_min(min.x, element_bounds.x); + min.y = math::partial_min(min.y, element_bounds.y); + max.x = math::partial_max(max.x, element_bounds.x + element_bounds.w); + max.y = math::partial_max(max.y, element_bounds.y + element_bounds.h); + } else { + // No selection size detected yet. Set now. + min.x = element_bounds.x; + min.y = element_bounds.y; + max.x = element_bounds.x + element_bounds.w; + max.y = element_bounds.y + element_bounds.h; + } + selection_exists = true; + } + } + + // Set the current selection limits, if any. + self.bounds = if selection_exists { + Rect::bounding_rect(min, max) + } else { + Rect::new(0., 0., 0., 0.) + }; + + // Check if the user wants to change into editing mode, which the user can only do if there + // is a selection to begin with. + if selection_exists && rl.is_key_pressed(KeyboardKey::KEY_TAB) { + self.state = State::Ruling { + dim_x: self.bounds.w.to_string(), + dim_y: self.bounds.h.to_string(), + editing_x: true, + }; + } + } + + fn update_ruling(&mut self, editor: &Editor, rl: &mut RaylibHandle) { + // Get the currently edited dimension for processing. + let (edited_dim, editing_x) = match &mut self.state { + State::Watching => panic!("Called ruler update when in watching state"), + State::Ruling { + dim_x, + dim_y, + editing_x, + } => { + if *editing_x { + (dim_x, editing_x) + } else { + (dim_y, editing_x) + } + } + }; + + // Switch the currently edited dimension when pressing tab. + if rl.is_key_pressed(KeyboardKey::KEY_TAB) { + *editing_x = !*editing_x; + return; + } + // Finish editing mode on enter. + if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { + self.state = State::Watching; + return; + } + + // Marker to see if the dimensions will have to be checked for an update. + let mut dimension_changed = false; + // Delete the last character of the dimension on backspace. + if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { + edited_dim.pop(); + dimension_changed = true; + } + /* Capture the current key and try to add it to the string of the current dimension, + * if possible. + */ + else if let Some(key) = rl.get_key_pressed() { + match key { + // Add a decimal point to the dimension if possible. + KeyboardKey::KEY_PERIOD => { + if !edited_dim.contains('.') { + edited_dim.push('.'); + } + // Nothing changed here, since there is an implicit .0 at the end. + } + // Handle the entered key if it is a number to append it to the currently edited dimension. + _ => { + if key as u16 >= KeyboardKey::KEY_ZERO as u16 + && key as u16 <= KeyboardKey::KEY_NINE as u16 + { + edited_dim.push(key as u8 as char); + dimension_changed = true; + } + } + }; + } + + if dimension_changed { + /* Try to parse the dimension from the currently edited string. If it + * is valid, change the dimensions of the currently selected items. If + * not, ignore the change and wait for a valid dimension. + */ + if let Ok(dim) = edited_dim.parse::<f64>() { + let new_bounds = if *editing_x { + Rect::new(self.bounds.x, self.bounds.y, dim, self.bounds.h) + } else { + Rect::new(self.bounds.x, self.bounds.y, self.bounds.h, dim) + }; + + self.set_bounds(editor, new_bounds); + } + } + } + + /// Set the selection boundaries to the given bounds. Tries to transform the + /// currently selected items in the map so they fit inside of the new bounding box. + /// + /// # Panics + /// If the `bounds` have a negative value for width or height, the dimensions + /// cannot be set and the function will panic. + pub fn set_bounds(&mut self, editor: &Editor, bounds: Rect<f64>) { + if bounds.w <= 0. || bounds.h <= 0. { + panic!("Cannot set dimensions of elements to zero."); + } + + // If the bounds are the same as before, there is nothing to do. + if self.bounds == bounds { + return; + } + + /* Create a matrix to transform from the current rectangle bounds into the + * new bounds. Internally, this is a three-step process. First, we + * translate the points currently in the bounding box to the origin + * (0, 0) origin vector of the map, then scale and finally move it to the + * origin of the new rectangle. This needs to be applied to all vertices + * of all elements that can be scaled. + */ + let scale = Vector2::new(bounds.w / self.bounds.w, bounds.h / self.bounds.h); + let transform = Matrix3::new_translation(&Vector2::new(-self.bounds.x, -self.bounds.y)) + .append_nonuniform_scaling(&scale) + .append_translation(&Vector2::new(bounds.x, bounds.y)); + + for (id, element) in editor.map().elements() { + if element.selected() { + if element.as_component().as_non_rigid().is_some() { + editor + .server() + .send(Cargo::ApplyMatrix((id, transform.clone()))); + } + } + } + + self.bounds = bounds; + } + + /// Draw the dimensions detected on the current selection. + pub fn draw(&self, rld: &mut impl RaylibDraw, transform: &Transform) { + /* Ignore a selection that has no non-null dimensions, since this usually + * indicates that there is nothing to be scaled. + */ + if self.bounds.w == 0. && self.bounds.h == 0. { + return; + } + + let (dim_str_width, dim_str_height) = match &self.state { + State::Watching => (self.bounds.w.to_string(), self.bounds.h.to_string()), + State::Ruling { dim_x, dim_y, .. } => (dim_x.clone(), dim_y.clone()), + }; + + // Draw the horizontal dimension at the bottom and the vertical dimension to the right. + // Use the valid dimensions, but show the edited dimensions in the String (should they differ) + let top_right = Vec2::new(self.bounds.x + self.bounds.w, self.bounds.y); + let bot_left = Vec2::new(self.bounds.x, self.bounds.y + self.bounds.h); + let bot_right = Vec2::new(self.bounds.x + self.bounds.w, self.bounds.y + self.bounds.h); + let dimensions = [ + (bot_left, bot_right, dim_str_width), + (bot_right, top_right, dim_str_height), + ]; + for (start, end, dim_str) in &dimensions { + // Don't draw anything if the length is zero. + if start == end { + continue; + } + + /* Get the vector that is perpendicular and points right/down from the line, assuming + * the lines prefer left as start over right and bottom over top. + */ + let line_normal = { + // Start with the direction of the line vector. + let dir = *start - *end; + // Calculate perpendicular vec and normalise. + dir.rotated_90_clockwise() / dir.length() + }; + + // To not have the line directly in the rect, move start and end outside a bit. + let start_px = transform.point_m_to_px(start) + line_normal * 10.; + let end_px = transform.point_m_to_px(end) + line_normal * 10.; + + /* Draw the indicator line, with stubs at both ends. */ + // First the two stubs. + rld.draw_line_ex( + start_px - line_normal * 5., + start_px + line_normal * 5., + 2., + DEFAULT_COLOURS.dimension_indicators, + ); + rld.draw_line_ex( + end_px - line_normal * 5., + end_px + line_normal * 5., + 2., + DEFAULT_COLOURS.dimension_indicators, + ); + // Then the actual indicator line. + rld.draw_line_ex(start_px, end_px, 2., DEFAULT_COLOURS.dimension_indicators); + + /* Draw the indicator text showing how long this line is in meters. + * It should be placed in the middle of the line, but not into the line directly, so it + * will be moved out by the normal. + */ + let text_pos = transform.point_m_to_px(&((*end + *start) / 2.)) + line_normal * 20.; + rld.draw_text( + &format!("{}m", dim_str), + text_pos.x as i32, + text_pos.y as i32, + 20, + DEFAULT_COLOURS.dimension_text, + ); + } + } +} diff --git a/src/client/gui/mod.rs b/src/client/gui/mod.rs new file mode 100644 index 0000000..62173ec --- /dev/null +++ b/src/client/gui/mod.rs @@ -0,0 +1,18 @@ +//! General graphical user interfaces +//! +//! This mod does not contain all graphical content on screen, but all user interfaces that is drawn +//! that is not contained in a different category. This means all interface elements where it does not +//! make sense to bind it to any other part of the program, for instance a tool or type of element. +//! It also does *not* contain anything that does anything that is not triggered by the user, which +//! means everything is called top-down from this module. A function in this module should not be +//! called from any point in the program except the main loop, where the user input is polled. + +pub mod decimal_num_box; +pub mod dimension_indicator; +pub mod position_indicator; +pub mod tool_sidebar; + +pub use self::decimal_num_box::*; +pub use self::dimension_indicator::*; +pub use self::position_indicator::*; +pub use self::tool_sidebar::*; diff --git a/src/client/gui/position_indicator.rs b/src/client/gui/position_indicator.rs new file mode 100644 index 0000000..4d68b86 --- /dev/null +++ b/src/client/gui/position_indicator.rs @@ -0,0 +1,37 @@ +//! The position indicator shows the mouse position on the map +//! +//! The exact position the mouse is currently on is shown unless hidden by the user (TODO). This +//! helps to place things exactly where they should be on the map and let the user know where they +//! are looking and where relative to them other things should be easily at all times. Currently, this +//! is a simple HUD so it doesn't interact with anything in the world, but that may change in the +//! future. + +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::snapping::Snapper; +use crate::client::transform::Transform; +use crate::math::Vec2; +use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; + +/// Function to draw a dot at the mouse position and the coordinates associated with it. +// TODO: Snap this, when the user wants to snap, don't if they don't want to. +pub fn position_indicator_draw( + rld: &mut RaylibDrawHandle, + mouse_pos_px: Vec2<f64>, + transform: &Transform, + snapper: &Snapper, +) { + let mouse_pos_snapped_m = snapper.snap(transform.point_px_to_m(&mouse_pos_px)); + let mouse_pos_snapped_px = transform.point_m_to_px(&mouse_pos_snapped_m); + + rld.draw_circle_v(mouse_pos_snapped_px, 2., DEFAULT_COLOURS.position_indicator); + rld.draw_text( + &format!( + "({:.3}m, {:.3}m)", + mouse_pos_snapped_m.x, mouse_pos_snapped_m.y + ), + mouse_pos_snapped_px.x as i32 - 30, + mouse_pos_snapped_px.y as i32 - 30, + 20, + DEFAULT_COLOURS.position_text, + ); +} diff --git a/src/client/gui/tool_sidebar.rs b/src/client/gui/tool_sidebar.rs new file mode 100644 index 0000000..3147bf8 --- /dev/null +++ b/src/client/gui/tool_sidebar.rs @@ -0,0 +1,91 @@ +//! The sidebar showing all tools available to the user. This toolbar handles changing the active tool +//! based on the mouse input and (TODO!) keyboard inputs. +// TODO: Currently, the keyboard shortcuts for tools are handled by the editor, but a lot speaks for +// them being handled by the ToolSidebar instead. + +use crate::client::input::Input; +use crate::client::tool::ToolType; +use crate::client::Editor; +use crate::math::Rect; +use raylib::core::texture::Texture2D; +use raylib::rgui::RaylibDrawGui; +use raylib::{RaylibHandle, RaylibThread}; +use std::mem; + +/// The file containing textures for all buttons describing the tools. +pub const BUTTON_FILE: &str = "assets/button/tool_buttons.png"; + +/// Sidebar that renders and handles input for the tool activation buttons. +pub struct ToolSidebar { + button_texture: Texture2D, + bindings_id: usize, + panel_rect: Rect<u16>, +} + +impl ToolSidebar { + /// Create a new tool sidebar. There should be only one sidebar per program instance. + pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread, input: &mut Input) -> Self { + let button_texture = rl + .load_texture(rlt, BUTTON_FILE) + .expect("Could not read file containing tool icons."); + + let panel_rect = Self::panel_rect(rl.get_screen_height() as u16); + let bindings_id = input.add_local_handler(panel_rect.clone()); + + Self { + button_texture, + bindings_id, + panel_rect, + } + } + + fn panel_rect(screen_height: u16) -> Rect<u16> { + /* The width is currently hardcoded as 104, which is + * 64 (button-size) + 20 left gap + 20 right gap + */ + Rect::new(0, 0, 104, screen_height) + } + + /// Update the state of the tool sidebar. Due to raylib limitations, this is not where the tools + /// are selected for the editor, which happens in draw. + pub fn update(&mut self, screen_height: u16, input: &mut Input) { + let new_panel_rect = Self::panel_rect(screen_height); + if new_panel_rect != self.panel_rect { + self.panel_rect = new_panel_rect; + input.set_binding_rect(self.bindings_id, self.panel_rect); + } + } + + /// Draw the tool buttons and encasing panel. Because of the way raylib works, this also handles + /// clicking on tool buttons, which may be changed in the future, should a different gui be + /// chosen. + pub fn draw(&self, rld: &mut impl RaylibDrawGui, editor: &mut Editor) { + rld.gui_panel(Rect::new( + self.panel_rect.x as f32, + self.panel_rect.y as f32, + self.panel_rect.w as f32, + self.panel_rect.h as f32, + )); + + // TODO: Update to new input system. Create buttons that integrate. + let mut active = editor.active(); + for i in 0..ToolType::NumTools as usize { + let is_current_active = active as usize == i; + if rld.gui_image_button_ex( + Rect::new(20., i as f32 * 100. + 20., 64., 64.), + None, + &self.button_texture, + Rect::new( + is_current_active as u8 as f32 * 64., + i as f32 * 64., + 64., + 64., + ), + ) { + active = unsafe { mem::transmute(i as u8) }; + } + } + + editor.set_active(active); + } +} diff --git a/src/client/input/binding.rs b/src/client/input/binding.rs new file mode 100644 index 0000000..386fb66 --- /dev/null +++ b/src/client/input/binding.rs @@ -0,0 +1,123 @@ +//! Bindings module, which is a key combination that does something when pressed. + +use super::Button; +use raylib::RaylibHandle; +use serde::{Deserialize, Serialize}; + +/// Binding struct, which holds any number of buttons (keyboard and mouse may be mixed, if desired) +#[derive(Clone, PartialEq, Eq, Hash, Serialize, Deserialize)] +pub struct Binding { + buttons: Vec<Button>, +} + +impl Binding { + /// Create a new binding from a range of buttons. The button order does not matter, but at least + /// one button must be supplied. + pub fn new(buttons: Vec<Button>) -> Self { + if buttons.is_empty() { + panic!("Tried to create a binding without any keys."); + } + + Self { buttons } + } + + /// Returns `true` if only mouse buttons are present in this binding, otherwise false. + pub fn mouse_only(&self) -> bool { + for button in &self.buttons { + match button { + Button::Mouse(_) => continue, + _ => return false, + } + } + + true + } + + /// Returns `true` if only keyboard buttons are present in this binding, otherwise false. + pub fn keyboard_only(&self) -> bool { + for button in &self.buttons { + match button { + Button::Scancode(_) | Button::Text(_) => continue, + _ => return false, + } + } + + true + } + + /// Returns `true` if at least one mouse button is required for this binding to work. + pub fn has_mouse_component(&self) -> bool { + self.buttons.iter().any(|b| { + if let Button::Mouse(_) = b { + true + } else { + false + } + }) + } + + /// Returns `true` if at least one keyboard button is required for this binding to work. + pub fn has_keyboard_component(&self) -> bool { + self.buttons.iter().any(|b| match b { + Button::Scancode(_) | Button::Text(_) => true, + _ => false, + }) + } + + /// Checks if this binding was pressed this frame. Heavily dependent on input struct working + /// correctly. + pub(super) fn is_pressed( + &self, + allow_mouse: bool, + allow_keyboard: bool, + text: &str, + rl: &RaylibHandle, + ) -> bool { + let mut distinct_press = false; + for button in &self.buttons { + match *button { + Button::Mouse(mouse_button) => { + if !allow_mouse || !rl.is_mouse_button_down(mouse_button.into()) { + return false; + } + + /* Check if the button has been pressed in this frame exactly. + * This prevents activating the same keybinding every frame + * while the buttons are being held down. + */ + if rl.is_mouse_button_pressed(mouse_button.into()) { + distinct_press = true; + } + } + Button::Scancode(code) => { + if !allow_keyboard || !rl.is_key_down(code.into()) { + return false; + } + + // Check the same as with the mouse button. + if rl.is_key_pressed(code.into()) { + distinct_press = true; + } + } + Button::Text(c) => { + if !allow_keyboard || !text.contains(c) { + return false; + } + + // Always distinct, since on triggering, the text is cleared. + distinct_press = true; + } + } + } + + distinct_press + } +} + +impl From<Button> for Binding { + fn from(button: Button) -> Self { + Self { + buttons: vec![button], + } + } +} diff --git a/src/client/input/button.rs b/src/client/input/button.rs new file mode 100644 index 0000000..e9ef45e --- /dev/null +++ b/src/client/input/button.rs @@ -0,0 +1,177 @@ +//! Reimplementation crate of the KeyboardKey and MouseButton structs of raylib, because they do +//! not implement `serde::Serialize` and `serde::Deserialize`. If you have a better idea on how to +//! handle it, feel free. + +use raylib::ffi::{KeyboardKey as rlKeyboardKey, MouseButton as rlMouseButton}; +use serde::{Deserialize, Serialize}; +use std::mem; + +/// Abstraction over different key-types. A binding can be constructed from this button-type or +/// multiple button-presses can be chained together to create a binding. Just be careful to not +/// have bindings where one binding is included in another. This includes bindings where on your +/// keyboard you have Scancode(Shift) + Scancode(A) and another binding of Text(A) (Text(a) would +/// be okay) +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Button { + /// A button on the mouse (raylib supports just three :/ ) + Mouse(MouseButton), + /// Scancode that is sent by the OS. This can change between OSes, but stays the same between + /// runs and layout changes in the keyboard. + Scancode(Scancode), + /// The text input read from the operating system. This means even characters composed or + /// non-ASCII characters can be used. I mean, who doesn't want to bind the wall tool to 壁? + Text(char), +} + +#[allow(missing_docs)] +#[repr(u32)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum MouseButton { + Left = 0, + Right = 1, + Middle = 2, +} + +#[allow(missing_docs)] +#[repr(u32)] +#[derive(Serialize, Deserialize, Debug, Copy, Clone, PartialEq, Eq, Hash)] +pub enum Scancode { + Apostrophe = 39, + Comma = 44, + Minus = 45, + Period = 46, + Slash = 47, + Zero = 48, + One = 49, + Two = 50, + Three = 51, + Four = 52, + Five = 53, + Six = 54, + Seven = 55, + Eight = 56, + Nine = 57, + Semicolon = 59, + Equal = 61, + A = 65, + B = 66, + C = 67, + D = 68, + E = 69, + F = 70, + G = 71, + H = 72, + I = 73, + J = 74, + K = 75, + L = 76, + M = 77, + N = 78, + O = 79, + P = 80, + Q = 81, + R = 82, + S = 83, + T = 84, + U = 85, + V = 86, + W = 87, + X = 88, + Y = 89, + Z = 90, + Space = 32, + Escape = 256, + Enter = 257, + Tab = 258, + Backspace = 259, + Insert = 260, + Delete = 261, + Right = 262, + Left = 263, + Down = 264, + Up = 265, + PageUp = 266, + PageDown = 267, + Home = 268, + End = 269, + CapsLock = 280, + ScrollLock = 281, + NumLock = 282, + PrintScreen = 283, + Pause = 284, + F1 = 290, + F2 = 291, + F3 = 292, + F4 = 293, + F5 = 294, + F6 = 295, + F7 = 296, + F8 = 297, + F9 = 298, + F10 = 299, + F11 = 300, + F12 = 301, + LeftShift = 340, + LeftControl = 341, + LeftAlt = 342, + LeftSuper = 343, + RightShift = 344, + RightControl = 345, + RightAlt = 346, + RightSuper = 347, + Menu = 348, + LeftBracket = 91, + Backslash = 92, + RightBracket = 93, + Grave = 96, + Keypad0 = 320, + Keypad1 = 321, + Keypad2 = 322, + Keypad3 = 323, + Keypad4 = 324, + Keypad5 = 325, + Keypad6 = 326, + Keypad7 = 327, + Keypad8 = 328, + Keypad9 = 329, + KeypadDecimal = 330, + KeypadDivide = 331, + KeypadMultiply = 332, + KeypadSubtract = 333, + KeypadAdd = 334, + KeypadEnter = 335, + KeypadEqual = 336, +} + +impl From<rlMouseButton> for Button { + fn from(button: rlMouseButton) -> Self { + Self::Mouse(MouseButton::from(button)) + } +} +impl From<rlKeyboardKey> for Button { + fn from(key: rlKeyboardKey) -> Self { + Self::Scancode(Scancode::from(key)) + } +} + +impl From<rlMouseButton> for MouseButton { + fn from(button: rlMouseButton) -> Self { + unsafe { mem::transmute(button as u32) } + } +} +impl Into<rlMouseButton> for MouseButton { + fn into(self) -> rlMouseButton { + unsafe { mem::transmute(self as u32) } + } +} + +impl From<rlKeyboardKey> for Scancode { + fn from(key: rlKeyboardKey) -> Self { + unsafe { mem::transmute(key as u32) } + } +} +impl Into<rlKeyboardKey> for Scancode { + fn into(self) -> rlKeyboardKey { + unsafe { mem::transmute(self as u32) } + } +} diff --git a/src/client/input/mod.rs b/src/client/input/mod.rs new file mode 100644 index 0000000..e8b1821 --- /dev/null +++ b/src/client/input/mod.rs @@ -0,0 +1,200 @@ +//! Input with binding abstraction +//! +//! Binding keys or combinations to specific actions with just raylib alone is difficult to handle in +//! input-heavy applications, as opposed to games. This is an optimisation effort. To understand how +//! it works, the first thing to know is that there are two main modes for bindings, which are local +//! and global. A local binding is specific to a certain area of the window and is used to block the +//! mouse from being sent to many different targets (think of a pop-up window over the editor, which +//! must capture the mouse). Global bindings will be processed as long as no local binding has +//! prevalence, but they do not have an area that needs to be managed by a handler. +//! +//! In summary, the local <-> global distinction is used to capture the mouse. +//! +//! Some elements want to capture the keyboard, for instance, when activating a text box, the text +//! input should only go to this box, but should a tool be bound to a character, it should not +//! activate when typing. For this purpose, any element may seize control as long as no other element +//! still has the focus. A channel is opened and no bindings will be processed. Instead the text +//! together with a few control characters is relayed directly to the channel, until the receiver +//! hangs up. +//! +//! In summary, a channel is used to seize control of the keyboard when typing into an element. + +pub mod binding; +pub mod button; + +pub use binding::*; +pub use button::*; + +use crate::math::{ExactSurface, Rect, Vec2}; +use crate::stable_vec::StableVec; +use raylib::ffi::KeyboardKey; +use raylib::RaylibHandle; +use std::collections::HashMap; +use std::sync::mpsc::{self, Receiver, Sender}; + +/// Input and binding handler this should only be created once per instance. +pub struct Input { + global_bindings: HashMap<Binding, bool>, + local_bindings: StableVec<(Rect<u16>, HashMap<Binding, bool>)>, + last_text: String, + text_pipe: Option<Sender<char>>, + mouse_pos: Vec2<u16>, +} + +impl Input { + /// Create a new Input and binding handler. + pub fn new(rl: &RaylibHandle) -> Self { + Self { + global_bindings: HashMap::new(), + local_bindings: StableVec::new(), + last_text: String::new(), + text_pipe: None, + mouse_pos: Vec2::new(rl.get_mouse_x() as u16, rl.get_mouse_y() as u16), + } + } + + /// Must be called on every frame of the program, since keypresses will be processed here. This + /// will not activate the binding function directly, since raylib is heavily polling focused. + pub fn update(&mut self, rl: &mut RaylibHandle) { + self.mouse_pos = Vec2::new(rl.get_mouse_x() as u16, rl.get_mouse_y() as u16); + /* Read the next character to be sent with some extra characters + * raylib doesn't recognize to be valid. + */ + let c = if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { + Some('\n') + } else if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) { + Some('\x1B') + } else if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { + Some('\x7f') + } else { + rl.get_key_pressed_number().map(|c| c as u8 as char) + }; + + /* Send the character to the listening entity or push it to the text that + * is currently being read for the keybindings. + */ + if let Some(text_pipe) = self.text_pipe.as_mut() { + if let Some(c) = c { + if text_pipe.send(c).is_err() { + self.last_text.push(c); + self.text_pipe = None; + } + } + } else if let Some(c) = c { + self.last_text.push(c); + } + + /* Update the local parts. The local stack has priority over the global + * bindings, so it is processed first, with the priority going from the + * top of the stack to the bottom in that order (reversed vec order) + */ + let mut mouse_blocked = false; + for (_, (rect, bindings)) in self.local_bindings.id_iter_mut().rev() { + if rect.contains_point(&self.mouse_pos) { + for (binding, state) in &mut bindings.iter_mut() { + *state = binding.is_pressed( + !mouse_blocked, + self.text_pipe.is_none(), + &self.last_text, + rl, + ); + + if *state { + self.last_text.clear(); + } + } + + mouse_blocked = true; + break; + } + } + + /* Process the global bindings, as long as nothing prevents the bindings + * from being processed like a local binding or the text being captured. + */ + for (binding, state) in self.global_bindings.iter_mut() { + *state = binding.is_pressed( + !mouse_blocked, + self.text_pipe.is_none(), + &self.last_text, + rl, + ); + + if *state { + self.last_text.clear(); + } + } + } + + /// Add a global binding. This is necessary so the input knows which key presses to monitor. + pub fn add_global(&mut self, binding: Binding) -> bool { + self.global_bindings.insert(binding, false).is_none() + } + + /// Add a local binding handler for the given area. Returns a unique and unchanging handler id. + /// Handlers with higher ids (that have been added later) are preferred over old handlers. + pub fn add_local_handler(&mut self, area: Rect<u16>) -> usize { + self.local_bindings.push((area, HashMap::new())) + } + + /// Add a local binding for the given handler. + pub fn add_local(&mut self, handler_id: usize, binding: Binding) -> bool { + self.local_bindings + .get_mut(handler_id) + .expect("Handler does not exist") + .1 + .insert(binding, false) + .is_none() + } + + /// Update the binding rectangle of a handler. + pub fn set_binding_rect(&mut self, handler_id: usize, rect: Rect<u16>) { + self.local_bindings + .get_mut(handler_id) + .expect("Handler does not exist") + .0 = rect; + } + + /// Check if a global binding has been activated this frame. If so, it returns true. + /// This will only activate once, so there is no need to worry about multiple function calls + /// when the user keeps the button down. + pub fn poll_global(&mut self, binding: &Binding) -> bool { + let state = self.global_bindings.get_mut(&binding); + if state.is_none() { + error!("Tried to poll binding that isn't registered."); + return false; + } + + *state.unwrap() + } + + /// Like `poll_global` bun instead checks the bindings of the local handler with the given id. + pub fn poll_local(&mut self, handler_id: usize, binding: &Binding) -> bool { + let (_, bindings) = self + .local_bindings + .get_mut(handler_id) + .expect("Invalid binding handler id"); + + let state = bindings.get_mut(&binding); + if state.is_none() { + error!("Tried to poll binding that isn't registered."); + return false; + } + + *state.unwrap() + } + + /// Attempts to capture all keyboard input from here on. If no other component is currently + /// capturing, it returns a receiver that can be used. When the entity no longer wants to + /// capture the keyboard, control must be returned by dropping the receiver. + pub fn try_capture_keyboard(&mut self) -> Option<Receiver<char>> { + if self.text_pipe.is_some() { + return None; + } + + let (tx, rx) = mpsc::channel(); + self.text_pipe = Some(tx); + + Some(rx) + } +} diff --git a/src/client/map/icon_mark.rs b/src/client/map/icon_mark.rs new file mode 100644 index 0000000..39fd554 --- /dev/null +++ b/src/client/map/icon_mark.rs @@ -0,0 +1,95 @@ +//! Icon marker on the map. For information about icons see [Icon](crate::) + +use super::icon_texture_manager::IconTextureManager; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Mappable; +use crate::client::transform::Transform; +use crate::math::Vec2; +use crate::world::{Component, Icon}; +use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; +use std::ops::{Deref, DerefMut}; +use std::rc::Rc; + +/// Describes an icon in the world and can be drawn. +#[derive(Clone)] +pub struct IconMark { + icon: Icon, + selected: bool, + textures: Rc<IconTextureManager>, +} + +impl IconMark { + /// Create a new icon marker. Requires the icon textures that are used to render this icon, as well as all + /// the information necessary to describe the icon itself. + pub fn new( + id: usize, + position: Vec2<f64>, + rotation: f64, + renderer: Rc<IconTextureManager>, + ) -> Self { + Self::from_icon( + Icon { + id, + position, + rotation, + }, + renderer, + ) + } + + /// Like `new()`, but with the icon data bundled into the icon type. + pub fn from_icon(icon: Icon, textures: Rc<IconTextureManager>) -> Self { + Self { + icon, + selected: false, + textures, + } + } +} + +impl Mappable for IconMark { + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + let (texture, info) = self.textures.get(self.id); + // Round the position to whole pixels to fix rotation problems. + let mut position_px = + transform.point_m_to_px(&(self.position - (info.anchor / info.pixels_per_m))); + position_px.x = position_px.x.floor(); + position_px.y = position_px.y.floor(); + rld.draw_texture_ex( + texture, + position_px, + self.rotation as f32, + (transform.pixels_per_m() / info.pixels_per_m) as f32, + if self.selected() { + DEFAULT_COLOURS.icon_selected + } else { + DEFAULT_COLOURS.icon_normal + }, + ); + } + + fn set_selected(&mut self, selected: bool) { + self.selected = selected; + } + + fn selected(&self) -> bool { + self.selected + } + + fn as_component(&self) -> &dyn Component { + self.deref() + } +} + +impl Deref for IconMark { + type Target = Icon; + + fn deref(&self) -> &Self::Target { + &self.icon + } +} +impl DerefMut for IconMark { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.icon + } +} diff --git a/src/client/map/icon_texture_manager.rs b/src/client/map/icon_texture_manager.rs new file mode 100644 index 0000000..c6b7fea --- /dev/null +++ b/src/client/map/icon_texture_manager.rs @@ -0,0 +1,91 @@ +//! Since the same icon may be used very often on a map, it becomes impracticalto let every icon have +//! it's own texture data saved, which is why a texture manager type of struct is used for it instead. + +use crate::math::Vec2; +use raylib::core::texture::Texture2D; +use raylib::{RaylibHandle, RaylibThread}; +use ron::de::from_reader; +use serde::Deserialize; +use std::fs::{self, File}; + +/// The directory containing all files related to icons. +pub const ICON_DIR: &str = "assets/icons"; + +#[derive(Deserialize)] +pub(super) struct IconFileInfo { + /// The position the icon should be anchored in pixels. This is the Vector it will be moved by + /// relative to the mouse pointer (to the left and up). + pub anchor: Vec2<f64>, + /// The scale of the icon as expressed in image pixels per real meter. + pub pixels_per_m: f64, +} + +/// Manager for all icon texture or rendering data. +pub struct IconTextureManager { + texture_data: Vec<(Texture2D, IconFileInfo)>, +} + +impl IconTextureManager { + /// Create a new icon manager. This loads all textures and information for icons that is needed + /// to draw them to the screen. Usually, there should be only one IconTextureManager, or at least one + /// manager per directory and program instance. + pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread) -> Self { + /* Read all available icons from the icon directory. SVGs do not need any special scale + * file, but pixel-based file formats require a RON-file declaring what the scale of the + * picture is right beside them. + */ + let mut image_files = Vec::new(); + for entry in fs::read_dir(ICON_DIR).expect("Could not open icon directory") { + let entry = entry.expect("Failed to read file from icon directory"); + + // Ignore the RON-files for now and put the image files into the vec + if entry + .path() + .extension() + .expect("Entry does not have a file extension") + != "ron" + { + image_files.push(entry); + } + } + + // Read the RON-files where it is necessary. + let mut texture_data = Vec::with_capacity(image_files.len()); + for file in image_files { + // TODO: Handle svg + + let texture = rl + .load_texture( + rlt, + file.path() + .to_str() + .expect("Unable to convert path to string."), + ) + .expect("Could not read image file"); + + let mut file = file.path(); + file.set_extension("ron"); + let ron = File::open(file).expect("Could not read ron file for icon information."); + let icon_info: IconFileInfo = + from_reader(ron).expect("Could not parse icon info from reader."); + + texture_data.push((texture, icon_info)); + } + + Self { texture_data } + } + + /// Get the textures needed to render an icon of type `icon_id`. + /// + /// # Panics + /// If the `icon_id` does not describe a valid icon (is out of bounds), there is no data to be + /// accessed and the function panics. + pub(super) fn get(&self, icon_id: usize) -> &(Texture2D, IconFileInfo) { + &self.texture_data[icon_id] + } + + /// The number of icons registered in this texture manager. + pub fn num_icons(&self) -> usize { + self.texture_data.len() + } +} diff --git a/src/client/map/mappable.rs b/src/client/map/mappable.rs new file mode 100644 index 0000000..39e774b --- /dev/null +++ b/src/client/map/mappable.rs @@ -0,0 +1,23 @@ +//! Something that's mappable can be placed on the map and drawn at a specific position. It has a +//! dimension on the map and may be transformable in various ways. + +use crate::client::transform::Transform; +use crate::world::Component; +use raylib::drawing::RaylibDrawHandle; + +/// Anything that can be added to the map or world must implement this trait. +pub trait Mappable { + /// Draw this to `rld` with the transform. An item that cannot be drawn is not mappable, so + /// this must always be implemented. + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform); + + /// Set the selection status of this item. If it is selected, actions that concern all selected + /// items will be applied to this item as well. + fn set_selected(&mut self, selected: bool); + + /// Get if this item is currently selected. + fn selected(&self) -> bool; + + /// Get this mappable as a world component. + fn as_component(&self) -> &dyn Component; +} diff --git a/src/client/map/mod.rs b/src/client/map/mod.rs new file mode 100644 index 0000000..eaab72f --- /dev/null +++ b/src/client/map/mod.rs @@ -0,0 +1,206 @@ +//! The map is a visual interpretation of all the items that make up the world. +//! +//! It's the main structure that the client uses to interact with the world, since +//! the world contains all the + +pub mod icon_mark; +pub mod icon_texture_manager; +pub mod mappable; +pub mod room_mark; +pub mod wall_mark; + +pub use icon_mark::*; +pub use mappable::Mappable; +pub use room_mark::*; +pub use wall_mark::*; + +use crate::client::Transform; +use crate::stable_vec::StableVec; +use crate::world::{Room, Wall, World}; +use icon_texture_manager::IconTextureManager; +use raylib::drawing::RaylibDrawHandle; +use raylib::{RaylibHandle, RaylibThread}; +use std::rc::Rc; + +/// The map containing all map elements that are seen on the screen. +pub struct Map { + rooms: StableVec<RoomMark>, + walls: StableVec<WallMark>, + icons: StableVec<IconMark>, + used_ids: StableVec<()>, + icon_renderer: Rc<IconTextureManager>, +} + +impl Map { + /// Create a new, empty map/world. + pub fn new(rl: &mut RaylibHandle, rlt: &RaylibThread) -> Self { + Self { + rooms: StableVec::new(), + walls: StableVec::new(), + icons: StableVec::new(), + used_ids: StableVec::new(), + icon_renderer: Rc::new(IconTextureManager::new(rl, rlt)), + } + } + + pub fn add_room(&mut self, id: usize, room: Room) -> bool { + if self.used_ids.try_insert(id, ()).is_ok() { + self.rooms + .try_insert(id, RoomMark::from_room(room)) + .unwrap(); + true + } else { + error!("Unable to add room. Id already in use."); + false + } + } + + /// Add a wall with a specific id. May fail if there already is an entity with that id. + pub fn add_wall(&mut self, id: usize, wall: Wall) -> bool { + if self.used_ids.try_insert(id, ()).is_ok() { + /* Check for intersections with any wall that was arleady created so the wall ends can be + * rendered properly. + */ + let mut start_intersects = false; + let mut end_intersects = false; + for (_, wall) in self.walls.iter() { + if wall.shape().contains_collinear(wall.shape().start) { + start_intersects = true; + } + if wall.shape().contains_collinear(wall.shape().end) { + end_intersects = true; + } + + // Currently, additional intersections can be ignored, since it is just a yes-no-question + if start_intersects && end_intersects { + break; + } + } + + self.walls + .try_insert( + id, + WallMark::from_wall(wall, start_intersects, end_intersects), + ) + .unwrap(); + true + } else { + error!("Unable to add wall. Id already in use."); + false + } + } + + /// Add an icon with a specific id. May fail if there already is an entity with that id. + pub fn add_icon(&mut self, id: usize, icon: IconMark) -> bool { + if self.used_ids.try_insert(id, ()).is_ok() { + self.icons.try_insert(id, icon).unwrap(); + true + } else { + error!("Unable to add icon. Id already in use."); + false + } + } + + /// Draw all elements of the map to the screen. This should be called after the background of the + /// map has already been drawn. + pub fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + for (_, element) in self.elements() { + element.draw(rld, transform); + } + } + + /// Get the icon-renderer that is currently used to render the icons. + pub fn icon_renderer(&self) -> Rc<IconTextureManager> { + self.icon_renderer.clone() + } + + /// Remove item with the id, if it exists. Returns `true` if an item existed, `false` otherwise. + pub fn remove(&mut self, id: usize) -> bool { + if self.used_ids.remove(id).is_none() { + return false; + } + + if self.rooms.remove(id).is_some() + || self.walls.remove(id).is_some() + || self.icons.remove(id).is_some() + { + true + } else { + panic!( + "Id {} was still registered, eventhough there was no such entity.", + id + ); + } + } + + /// Iterator over all elements as objects when an operation needs to go over all elements of the + /// map. + pub fn elements(&self) -> impl Iterator<Item = (usize, &dyn Mappable)> { + self.rooms + .iter() + .map(|(id, p)| (*id, p as &dyn Mappable)) + .chain(self.walls.iter().map(|(id, w)| (*id, w as &dyn Mappable))) + .chain(self.icons.iter().map(|(id, i)| (*id, i as &dyn Mappable))) + } + + /// Iterator over all elements, but the individual elements can be mutated. It is however + /// impossible to add or remove elements in this way. For that, use the dedicated functions. + pub fn elements_mut(&mut self) -> impl Iterator<Item = (usize, &mut dyn Mappable)> { + self.rooms + .id_iter_mut() + .map(|(id, p)| (id, p as &mut dyn Mappable)) + .chain( + self.walls + .id_iter_mut() + .map(|(id, w)| (id, w as &mut dyn Mappable)), + ) + .chain( + self.icons + .id_iter_mut() + .map(|(id, i)| (id, i as &mut dyn Mappable)), + ) + } + + /// Get the rooms of this map. + pub fn rooms(&self) -> &StableVec<RoomMark> { + &self.rooms + } + + /// Get the walls of this map. + pub fn walls(&self) -> &StableVec<WallMark> { + &self.walls + } + + /// Get the icons of this map. + pub fn icons(&self) -> &StableVec<IconMark> { + &self.icons + } + + /// Replace the internal map data with the data of the world provided. + /// (Load and replace) + pub fn set_data(&mut self, world: World) { + // Remove all data. + self.icons.clear(); + self.rooms.clear(); + self.walls.clear(); + + // Add all data from the map data. + self.add_data(world); + } + + /// Add the data provided to the current data on the map. All elements will + /// remain, with the additional elements being pushed also. This must be + /// used with caution, since the ids of the items will remain unchanged, and + /// items with the same id will therefore be ignored and not added. + pub fn add_data(&mut self, world: World) { + for (id, i) in world.icons().iter() { + self.add_icon(*id, IconMark::from_icon(*i, self.icon_renderer.clone())); + } + for (id, r) in world.rooms().iter() { + self.add_room(*id, r.clone()); + } + for (id, w) in world.walls().iter() { + self.add_wall(*id, w.clone()); + } + } +} diff --git a/src/client/map/room_mark.rs b/src/client/map/room_mark.rs new file mode 100644 index 0000000..a9777fb --- /dev/null +++ b/src/client/map/room_mark.rs @@ -0,0 +1,75 @@ +//! Polygon rooms are the standard rooms in graf karto. They can take the form of anything that a +//! [Polygon](crate::math::Polygon) can have. + +use super::Mappable; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::transform::Transform; +use crate::client::FLOAT_MARGIN; +use crate::math::{self, Triangle}; +use crate::world::{Component, Room}; +use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; +use std::ops::Deref; + +/// A polygon room, which can be placed and modified in the world. +pub struct RoomMark { + room: Room, + // The polygon shape, but in triangles, so the polygon does not have to be triangulated every frame. + triangulated: Vec<Triangle<f64>>, + selected: bool, +} + +impl RoomMark { + /// Create a room from the given polygon data. + pub fn from_room(room: Room) -> Self { + let shape = room.shape().clone(); + Self { + room, + triangulated: math::triangulate(shape, FLOAT_MARGIN), + selected: false, + } + } + + /* When the internal polygon changes, it must be retriangulated to be drawn on the screen + * properly, so this function must be called any time that happens. + */ + fn retriangulate(&mut self) { + self.triangulated = math::triangulate(self.room.shape().clone(), FLOAT_MARGIN); + } +} + +impl Mappable for RoomMark { + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + for triangle in &self.triangulated { + rld.draw_triangle( + transform.point_m_to_px(&triangle.corners()[0]), + transform.point_m_to_px(&triangle.corners()[1]), + transform.point_m_to_px(&triangle.corners()[2]), + if self.selected() { + DEFAULT_COLOURS.room_selected + } else { + DEFAULT_COLOURS.room_normal + }, + ) + } + } + + fn set_selected(&mut self, selected: bool) { + self.selected = selected; + } + + fn selected(&self) -> bool { + self.selected + } + + fn as_component(&self) -> &dyn Component { + self.deref() + } +} + +impl Deref for RoomMark { + type Target = Room; + + fn deref(&self) -> &Self::Target { + &self.room + } +} diff --git a/src/client/map/wall_mark.rs b/src/client/map/wall_mark.rs new file mode 100644 index 0000000..c51af9d --- /dev/null +++ b/src/client/map/wall_mark.rs @@ -0,0 +1,106 @@ +//! Walls, solid barriers that are generally unclimbable. +//! +//! This interpretation is generally up to the GM to decide, but generally speaking, a wall cannot be +//! crossed by a player. If special conditions apply (for instance, when the player wants to scale the +//! wall), a check is necessary. If a check is not necessary, then maybe you were not thinking about +//! a wall. + +use super::Mappable; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::transform::Transform; +use crate::math::Vec2; +use crate::world::{Component, Wall}; +use raylib::drawing::{RaylibDraw, RaylibDrawHandle}; +use std::ops::{Deref, DerefMut}; + +/// A solid wall a player cannot go through, except if special conditions apply. +pub struct WallMark { + wall: Wall, + selected: bool, + round_start: bool, + round_end: bool, +} + +impl WallMark { + /// Create a new wall from the deserialised data and information known from internal sources. + pub fn from_wall(wall: Wall, round_start: bool, round_end: bool) -> Self { + Self { + wall, + selected: false, + round_start, + round_end, + } + } + + /// Get the internal data for serialisation + pub fn data(&self) -> &Wall { + &self.wall + } +} + +fn draw_round_corner( + rld: &mut RaylibDrawHandle, + pos_px: Vec2<f64>, + transform: &Transform, + selected: bool, +) { + rld.draw_circle_v( + pos_px, + transform.length_m_to_px(0.05) as f32, + if selected { + DEFAULT_COLOURS.wall_selected + } else { + DEFAULT_COLOURS.wall_normal + }, + ); +} + +impl Mappable for WallMark { + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + let start_px = transform.point_m_to_px(&self.wall.shape().start); + let end_px = transform.point_m_to_px(&self.wall.shape().end); + rld.draw_line_ex( + start_px, + end_px, + transform.length_m_to_px(0.1) as f32, + if self.selected() { + DEFAULT_COLOURS.wall_selected + } else { + DEFAULT_COLOURS.wall_normal + }, + ); + + if self.round_start { + draw_round_corner(rld, start_px, transform, self.selected()); + } + if self.round_end { + draw_round_corner(rld, end_px, transform, self.selected()); + } + } + + fn set_selected(&mut self, selected: bool) { + self.selected = selected; + } + + fn selected(&self) -> bool { + self.selected + } + + fn as_component(&self) -> &dyn Component { + self.deref() + } +} + +impl Deref for WallMark { + type Target = Wall; + + fn deref(&self) -> &Self::Target { + &self.wall + } +} + +impl DerefMut for WallMark { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.wall + } +} diff --git a/src/client/mod.rs b/src/client/mod.rs new file mode 100644 index 0000000..9151d82 --- /dev/null +++ b/src/client/mod.rs @@ -0,0 +1,137 @@ +pub mod cli; +pub mod colours; +pub mod config; +pub mod editor; +pub mod grid; +pub mod gui; +pub mod input; +pub mod map; +pub mod snapping; +pub mod svg; +pub mod tool; +pub mod transform; + +use crate::net::Connection; +use cli::CLI; +use config::Config; +use editor::Editor; +use float_cmp::F64Margin; +use gui::{DimensionIndicator, ToolSidebar}; +use input::Input; +use raylib::prelude::*; +use snapping::Snapper; +use std::ffi::CString; +use std::io; +use std::net::{SocketAddr, TcpStream}; +use transform::Transform; + +/// Location of the file containing the style used for the raylib user interface. +pub const GUI_STYLE: &str = "assets/style/cyber.rgs"; +/// Location of the graf karto configuration options file. +pub const CONFIG_FILE: &str = "config.ron"; + +/// The acceptable error that is used throughout the project for two floats to be considered equal. +/// If it is set too low, the editor might not work properly, if set too high, the granularity may +/// become too low for certain purposes. +pub const FLOAT_MARGIN: F64Margin = F64Margin { + epsilon: 10000. * f64::EPSILON, + ulps: 0, +}; + +pub fn run(server_address: SocketAddr) { + let (mut rl, thread) = raylib::init().resizable().title("Hello there!").build(); + rl.set_target_fps(120); + rl.set_exit_key(None); + + // Load the configuration file, if available. + let config = match Config::from_file(CONFIG_FILE) { + Ok(config) => config, + Err(err) => { + /* Create a default config file if it doesn't exist, otherwise leave the incorrectly + * formatted/corrupted or otherwise unreadable file alone. + */ + let config = Config::default(); + if err.kind() == io::ErrorKind::NotFound { + warn!("Could not find a configuration file. Creating default."); + config + .write_file(CONFIG_FILE) + .expect("Could not write config file."); + } else { + error!( + "Could not read configuration file: {}\nUsing defaults for this run.", + err + ); + } + + config + } + }; + + // Load the preferred gui style + rl.gui_load_style(Some( + &CString::new(GUI_STYLE).expect("Could not create C string from style file name"), + )); + + // Connect to the server at the given address. + let server = TcpStream::connect(server_address).expect("Unable to connect to the server."); + info!( + "Connection to server on `{:?}` established", + server.peer_addr() + ); + let server = Connection::new(server); + + let mut input = Input::new(&rl); + config::register_bindings(&config, &mut input); + let mut editor = Editor::new(&mut rl, &thread, config, server); + let mut dimension_indicator = DimensionIndicator::new(); + let mut tool_sidebar = ToolSidebar::new(&mut rl, &thread, &mut input); + let mut snapper = Snapper::default(); + let mut cli = CLI::new(&mut input); + + let mut transform = Transform::new(); + let mut last_mouse_pos = rl.get_mouse_position(); + while !rl.window_should_close() { + let screen_width = rl.get_screen_width(); + let screen_height = rl.get_screen_height(); + + input.update(&mut rl); + + // Move the canvas together with the mouse + if rl.is_mouse_button_down(MouseButton::MOUSE_MIDDLE_BUTTON) { + transform.move_by_px(&(rl.get_mouse_position() - last_mouse_pos).into()); + } + // Update the last mouse position + last_mouse_pos = rl.get_mouse_position(); + + let mouse_wheel_move = rl.get_mouse_wheel_move(); + if mouse_wheel_move != 0. { + // Zoom in for positive and zoom out for negative mouse wheel rotations. + let scale_factor = if mouse_wheel_move > 0. { 1.2 } else { 1. / 1.2 }; + transform.try_zoom( + &rl.get_mouse_position().into(), + mouse_wheel_move.abs() as f64 * scale_factor, + ); + } + + cli.update(&mut editor, &mut input); + dimension_indicator.update(&mut editor, &mut rl); + snapper.update(&mut rl, cli.active()); + editor.update(&mut rl, &transform, &snapper, &mut input); + tool_sidebar.update(screen_height as u16, &mut input); + + // Drawing section + { + let mut d = rl.begin_drawing(&thread); + d.clear_background(Color::BLACK); + grid::draw_grid(&mut d, screen_width, screen_height, &transform); + editor.map().draw(&mut d, &transform); + + editor.draw_tools(&mut d, &transform); + tool_sidebar.draw(&mut d, &mut editor); + snapper.draw(&mut d); + gui::position_indicator_draw(&mut d, last_mouse_pos.into(), &transform, &snapper); + dimension_indicator.draw(&mut d, &transform); + cli.draw(&mut d); + } + } +} diff --git a/src/client/snapping.rs b/src/client/snapping.rs new file mode 100644 index 0000000..b8f9706 --- /dev/null +++ b/src/client/snapping.rs @@ -0,0 +1,81 @@ +//! Responsible for snapping a position with a granularity +//! +//! Most of us are not capable of adjusting everything with sub-pixel accuracy. For us filthy casuals, +//! Snapping was invented. However I hate programs where there is only one option for granularity, so +//! I thought it should be changeable. This module is responsible for snapping and managing the user +//! instructions telling the program what granularity should currently be used, if any. + +use crate::client::gui::DecimalNumBox; +use crate::math::{self, Vec2}; +use raylib::drawing::RaylibDrawHandle; +use raylib::ffi::KeyboardKey; +use raylib::RaylibHandle; + +/// The struct containing the current snapping information of the program. +pub struct Snapper { + grain: f64, + grain_gui: DecimalNumBox<f64>, +} + +impl Snapper { + /// Create a new snapper with the default granularity. + pub fn new() -> Self { + Self::default() + } + + /// Update the grain according to the input the program receives. + pub fn update(&mut self, rl: &mut RaylibHandle, keyboard_captured: bool) { + if !self.grain_gui.active() && rl.is_key_pressed(KeyboardKey::KEY_G) && !keyboard_captured { + self.grain_gui.set_active(true); + } + + if !self.grain_gui.active() { + return; + } + + if !keyboard_captured { + self.grain_gui.update(rl); + } + + if !keyboard_captured && rl.is_key_pressed(KeyboardKey::KEY_ENTER) { + self.grain_gui.set_active(false); + self.grain = self.grain_gui.value(); + self.grain_gui.set_value(self.grain); + } else if !keyboard_captured && rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) { + self.grain_gui.set_active(false); + self.grain_gui.set_value(self.grain); + } + } + + /// Draw the snapper gui + pub fn draw(&self, rld: &mut RaylibDrawHandle) { + self.grain_gui.draw( + rld, + "m", + &Vec2::new(15., (rld.get_screen_height() - 25) as f64), + ); + } + + /// Get the current granularity of the world snapping in meters. Snapping always starts at (0, 0) + pub fn grain(&self) -> f64 { + self.grain + } + + /// Snap a vector to the grid with the factor being the sub-grid accuracy. For instance, 0.5 will + /// snap to half a grid cell, while 2.0 would snap to every second grid cell. + pub fn snap(&self, pos: Vec2<f64>) -> Vec2<f64> { + Vec2::new( + math::round(pos.x, self.grain), + math::round(pos.y, self.grain), + ) + } +} + +impl Default for Snapper { + fn default() -> Self { + Self { + grain: 0.5, + grain_gui: DecimalNumBox::new(0.5), + } + } +} diff --git a/src/client/svg/mod.rs b/src/client/svg/mod.rs new file mode 100644 index 0000000..af066f1 --- /dev/null +++ b/src/client/svg/mod.rs @@ -0,0 +1,178 @@ +//! Module for drawing SVG files to the screen or maybe a texture etc. + +pub mod style; + +use crate::math::Vec2; +use raylib::drawing::RaylibDraw; +use std::fs::File; +use std::io::{self, Read}; +use std::ops::Deref; +use std::path::Path; +use std::str::FromStr; +use style::Style; +use svgtypes::{Path as SVGPath, PathSegment}; +use xmltree::{Element, XMLNode}; + +/// Find the first XML-Node with the given name. With depth, the maximum depth the +/// algorithm will search to can be set. If it is set to `None`, the algorithm will search the +/// entire sub-tree. Returns `None` if no such child can be found. Returns itself, in case the root +/// node is already of the name given. +pub fn find_first_node(root: Element, name: &str, depth: Option<usize>) -> Option<Element> { + // The abort condition of this recursive function. If the element itself is of the required + // name, return it. + if root.name == name { + return Some(root); + } + // Also abort, if the depth is reached. + if depth == Some(0) { + return None; + } + + // Decrease the depth by one for all calls on the children, if it is set. + let depth = match depth { + Some(depth) => Some(depth - 1), + None => None, + }; + + // Recursively look for the element in this node's children. + for child in root.children { + // We only care for the elements, not for comments, cdata etc. + if let XMLNode::Element(element) = child { + if let Some(element) = find_first_node(element, name, depth) { + return Some(element); + } + } + } + + None +} + +/// Read an svg file from the given path. On success, return the first graphics data that is read +/// from this file. This can be used to draw an SVG. +pub fn read_svg_file<P: AsRef<Path>>(file: P) -> io::Result<Element> { + let mut file = File::open(file)?; + let mut data = String::new(); + file.read_to_string(&mut data)?; + + let root: Element = match Element::parse(data.as_bytes()) { + Ok(root) => root, + Err(err) => return Err(io::Error::new(io::ErrorKind::InvalidData, err)), + }; + + match find_first_node(root, "g", None) { + Some(graphics) => Ok(graphics), + None => Err(io::Error::new( + io::ErrorKind::InvalidData, + "No graphics element in the file", + )), + } +} + +/// Trait that indicates a struct is capable of drawing SVG-elements. +pub trait DrawSVG { + /// Draw the elements given by `svg_data` and all its children to the implementor, with the + /// specified scale and translated by `position_px`. + fn draw_svg(&mut self, svg_data: &Element, pixels_per_m: f32, position_px: Vec2<f32>); +} + +impl<D> DrawSVG for D +where + D: RaylibDraw, +{ + fn draw_svg(&mut self, svg_data: &Element, pixels_per_m: f32, position_px: Vec2<f32>) { + assert_eq!(&svg_data.name, "g"); + + // Go through all the graphics children and draw them one by one + for child in &svg_data.children { + if let XMLNode::Element(child) = child { + match child.name.as_str() { + "path" => draw_path(self, child, pixels_per_m, position_px), + other => warn!("Unsupported SVG-Element {}", other), + } + } + } + } +} + +// Helper functions to draw specific parts of the SVG file -------------------- + +fn draw_path( + d: &mut impl RaylibDraw, + path_data: &Element, + pixels_per_m: f32, + position_px: Vec2<f32>, +) { + let style = if let Some(style_data) = path_data.attributes.get("style") { + match Style::from_str(style_data) { + Ok(style) => style, + Err(err) => { + warn!("Could not parse path style: {}", err); + warn!("Using default style instead"); + Style::default() + } + } + } else { + Style::default() + }; + + let move_data = match path_data.attributes.get("d") { + Some(d) => d, + None => { + error!("Unable to draw path, no move data found"); + return; + } + }; + + let mut path: SVGPath = match move_data.parse() { + Ok(mv) => mv, + Err(err) => { + error!( + "Unable to draw path, move data not correctly formatted: {}", + err + ); + return; + } + }; + + path.conv_to_absolute(); + + let mut current_pos: Vec2<f32> = Vec2::new(0., 0.); + for segment in path.deref() { + match segment { + PathSegment::MoveTo { x, y, .. } => { + current_pos.x = *x as f32; + current_pos.y = *y as f32; + } + PathSegment::LineTo { x, y, .. } => { + d.draw_line_ex( + current_pos * pixels_per_m / 1000. + position_px, + Vec2::new(*x as f32, *y as f32) * pixels_per_m / 1000. + position_px, + pixels_per_m * style.stroke_width / 1000., + style.stroke, + ); + current_pos.x = *x as f32; + current_pos.y = *y as f32; + } + PathSegment::HorizontalLineTo { x, .. } => { + d.draw_line_ex( + current_pos * pixels_per_m / 1000. + position_px, + Vec2::new(*x as f32, current_pos.y) * pixels_per_m / 1000. + position_px, + pixels_per_m * style.stroke_width / 1000., + style.stroke, + ); + current_pos.x = *x as f32; + } + PathSegment::VerticalLineTo { y, .. } => { + d.draw_line_ex( + current_pos * pixels_per_m / 1000. + position_px, + Vec2::new(current_pos.x, *y as f32) * pixels_per_m / 1000. + position_px, + pixels_per_m * style.stroke_width / 1000., + style.stroke, + ); + current_pos.y = *y as f32; + } + PathSegment::ClosePath { .. } => return, + other => warn!("Ignoring unsupported {:?}", other), + } + } +} diff --git a/src/client/svg/style.rs b/src/client/svg/style.rs new file mode 100644 index 0000000..7a0110e --- /dev/null +++ b/src/client/svg/style.rs @@ -0,0 +1,180 @@ +//! Style options for SVG components. +//! +//! For information on SVG styles, pleas see the SVG documentation. +// TODO: There should be a lib available that can be integrated into the program + +use raylib::ffi::Color; +use std::str::FromStr; + +/// Convert an html-style colour into a raylib Color-struct if possible. If there is an error in +/// the formatting, it returns `None`. +pub fn colour_from_html(html: &str) -> Option<Color> { + /* The html-code must be exactly seven characters long, one for the hash and two per primary + * colour. + */ + if html.len() != 7 { + return None; + } + + let extract_hex = |string: &str, pos: usize| { + u8::from_str_radix( + string.get(pos..pos + 2).expect("Could not split string"), + 16, + ) + .ok() + }; + let red: Option<u8> = extract_hex(html, 1); + let green: Option<u8> = extract_hex(html, 3); + let blue: Option<u8> = extract_hex(html, 5); + + if let (Some(r), Some(g), Some(b)) = (red, green, blue) { + Some(Color { r, g, b, a: 255 }) + } else { + None + } +} + +/// The style of the end of the stroke. +/// See [stroke-line-cap property](https://www.w3.org/TR/SVG11/painting.html#StrokeLinecapProperty) +/// in the SVG Documentation. +#[allow(missing_docs)] +pub enum StrokeLineCap { + Butt, + Round, + Square, +} + +/// The style of the joining corners of the stroke. +/// See [stroke-line-join property](https://www.w3.org/TR/SVG11/painting.html#StrokeLinejoinProperty) +/// in the SVG Documentation +#[allow(missing_docs)] +pub enum StrokeLineJoin { + Miter, + Round, + Bevel, +} + +/// The style of a path drawing instruction. +#[allow(missing_docs)] +pub struct Style { + pub fill: Option<Color>, + pub stroke: Color, + pub stroke_width: f32, + pub stroke_linecap: StrokeLineCap, + pub stroke_linejoin: StrokeLineJoin, + pub stroke_opacity: f32, +} + +impl FromStr for StrokeLineCap { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "butt" => Ok(Self::Butt), + "round" => Ok(Self::Round), + "square" => Ok(Self::Square), + _ => Err("No such line-cap style".to_owned()), + } + } +} + +impl Default for StrokeLineCap { + fn default() -> Self { + StrokeLineCap::Butt + } +} + +impl FromStr for StrokeLineJoin { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + match s { + "miter" => Ok(Self::Miter), + "round" => Ok(Self::Round), + "bevel" => Ok(Self::Bevel), + _ => Err("No such line-join style".to_owned()), + } + } +} + +impl Default for StrokeLineJoin { + fn default() -> Self { + StrokeLineJoin::Miter + } +} + +impl FromStr for Style { + type Err = String; + + fn from_str(s: &str) -> Result<Self, Self::Err> { + // Split into CSS-attributes + let attributes: Vec<&str> = s.split(';').collect(); + // Split the attributes into name and value pairs and parse them into a style struct + let mut style = Style::default(); + for attribute in attributes { + let attribute_parts: Vec<&str> = attribute.split(':').collect(); + if attribute_parts.len() != 2 { + continue; + } + + match attribute_parts[0].trim() { + "fill" => { + style.fill = match attribute_parts[1].trim() { + "none" => None, + colour => colour_from_html(colour), + } + } + "stroke" => { + style.stroke = match colour_from_html(attribute_parts[1].trim()) { + Some(c) => c, + None => { + return Err(format!( + "Could not parse colour from {}", + attribute_parts[1].trim() + )) + } + } + } + "stroke-width" => { + style.stroke_width = match attribute_parts[1].trim().parse::<f32>() { + Ok(width) => width, + Err(err) => return Err(format!("Could not parse stroke-width: {}", err)), + } + } + "stroke-linecap" => { + style.stroke_linecap = StrokeLineCap::from_str(attribute_parts[1].trim())? + } + "stroke-linejoin" => { + style.stroke_linejoin = StrokeLineJoin::from_str(attribute_parts[1].trim())? + } + "stroke-opacity" => { + style.stroke_width = match attribute_parts[1].trim().parse::<f32>() { + Ok(opacity) => opacity, + Err(err) => return Err(format!("Could not parse stroke-opacity: {}", err)), + } + } + attr => return Err(format!("Unknown attribute {}", attr)), + } + } + + Ok(style) + } +} + +impl Default for Style { + fn default() -> Self { + Self { + fill: None, + stroke: Color { + r: 0, + g: 0, + b: 0, + a: 255, + }, + stroke_width: 1., + stroke_linecap: StrokeLineCap::default(), + stroke_linejoin: StrokeLineJoin::default(), + stroke_opacity: 1., + } + } +} diff --git a/src/client/tool/deletion_tool.rs b/src/client/tool/deletion_tool.rs new file mode 100644 index 0000000..3095ff5 --- /dev/null +++ b/src/client/tool/deletion_tool.rs @@ -0,0 +1,82 @@ +//! A meta tool for selecting parts of a map and removing them in a single operation. +//! +//! The user can draw a rectangle, which currently must have it's side parallel to the x and y-axes +//! of the world. With the first node placement, the mode is started, while the second placement would +//! finish the process and delete all elements that are *completely* contained in the rectangle +//! (partially contained items are not deleted) or abort it, in which case the selection is removed +//! and nothing is deleted. + +use super::Tool; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::Connection; +use crate::math::{ExactSurface, Rect, Vec2}; +use crate::net::Cargo; +use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; + +/// The deletion tool itself. +pub struct DeletionTool { + deletion_rect: Option<(Vec2<f64>, Vec2<f64>)>, +} + +impl DeletionTool { + /// Create a new deletion tool, there should only be one deletion tool and it should be created + /// by the editor. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + deletion_rect: None, + } + } +} + +fn delete_rect((pos1, pos2): (&Vec2<f64>, &Vec2<f64>), map: &Map, server: &Connection<Cargo>) { + let bounds = Rect::bounding_rect(*pos1, *pos2); + + for (id, e) in map.elements() { + if bounds.contains_rect(&e.as_component().bounding_rect()) { + server.send(Cargo::Remove(id)); + } + } +} + +impl Tool for DeletionTool { + fn deactivate(&mut self) { + self.deletion_rect = None; + } + + fn update(&mut self, _map: &Map, mouse_pos_m: &Vec2<f64>) { + if let Some((_, ref mut pos2)) = &mut self.deletion_rect { + *pos2 = *mouse_pos_m; + } + } + + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + if let Some((pos1, pos2)) = self.deletion_rect { + let rect_px = transform.rect_m_to_px(&Rect::bounding_rect(pos1, pos2)); + rld.draw_rectangle_rec(rect_px, DEFAULT_COLOURS.deletion_rect); + rld.draw_rectangle_lines_ex(rect_px, 4, DEFAULT_COLOURS.deletion_rect_outline); + } + } + + fn place_single(&mut self, map: &mut Map, server: &Connection<Cargo>, mouse_pos_m: &Vec2<f64>) { + if let Some((pos1, pos2)) = self.deletion_rect { + delete_rect((&pos1, &pos2), &map, server); + self.deletion_rect = None; + } else { + self.deletion_rect = Some((*mouse_pos_m, *mouse_pos_m)); + } + } + + fn finish(&mut self, map: &mut Map, server: &Connection<Cargo>) { + if let Some((pos1, pos2)) = self.deletion_rect { + delete_rect((&pos1, &pos2), &map, server); + self.deletion_rect = None; + } + } + + fn abort(&mut self) { + self.deletion_rect = None; + } +} diff --git a/src/client/tool/icon_tool.rs b/src/client/tool/icon_tool.rs new file mode 100644 index 0000000..caf9d60 --- /dev/null +++ b/src/client/tool/icon_tool.rs @@ -0,0 +1,88 @@ +//! Tool for creating icons. For explanation of icons, please see +//! [the icon module](crate::map::icon). + +use crate::client::config::IconToolBinds; +use crate::client::input::Input; +use crate::client::map::{icon_texture_manager::IconTextureManager, IconMark, Map, Mappable}; +use crate::client::tool::Tool; +use crate::client::transform::Transform; +use crate::math::Vec2; +use crate::net::Cargo; +use crate::net::Connection; +use raylib::core::drawing::RaylibDrawHandle; +use std::ops::Deref; +use std::rc::Rc; + +/// The icon tool itself. +pub struct IconTool { + keybindings: IconToolBinds, + /// Saves whether the IconTool is the currently active tool or not. + active: bool, + /// The information of the icon that should be placed / is currently being placed, if it + /// exists. + current_icon: IconMark, + textures: Rc<IconTextureManager>, +} + +impl IconTool { + /// Create a new icon tool that renders icons with the provided icon renderer. There should only + /// be one instance of the tool for the program, which should be created in the editor. + pub fn new(keybindings: IconToolBinds, textures: Rc<IconTextureManager>) -> Self { + Self { + keybindings, + active: false, + current_icon: IconMark::new(0, Vec2::default(), 0., textures.clone()), + textures, + } + } +} + +impl Tool for IconTool { + fn activate(&mut self) { + self.active = true; + } + + fn deactivate(&mut self) { + self.active = false; + } + + fn update(&mut self, _map: &Map, mouse_pos_m: &Vec2<f64>) { + self.current_icon.position = *mouse_pos_m; + } + + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + if self.active { + self.current_icon.draw(rld, transform); + } + } + + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + _mouse_pos_m: &Vec2<f64>, + ) { + server.send(Cargo::AddIcon(self.current_icon.deref().clone())); + } + + fn handle_custom_bindings( + &mut self, + _map: &mut Map, + _server: &Connection<Cargo>, + input: &mut Input, + ) { + if input.poll_global(&self.keybindings.next) { + self.current_icon.id = (self.current_icon.id + 1) % self.textures.num_icons(); + } + if input.poll_global(&self.keybindings.previous) { + self.current_icon.id = + (self.current_icon.id + self.textures.num_icons() - 1) % self.textures.num_icons(); + } + if input.poll_global(&self.keybindings.rotate_clockwise) { + self.current_icon.rotation += 45.; + } + if input.poll_global(&self.keybindings.rotate_counterclockwise) { + self.current_icon.rotation -= 45.; + } + } +} diff --git a/src/client/tool/mod.rs b/src/client/tool/mod.rs new file mode 100644 index 0000000..08e1380 --- /dev/null +++ b/src/client/tool/mod.rs @@ -0,0 +1,98 @@ +//! Tools, which are user interfaces that must be specifically selected in order to do something. +//! +//! As stated, a tool is not simply everything that helps a user do something, think of it more as a +//! mode which must be elected by the user to perform a task on a specific object type or a class of +//! objects. If instead the operation is defined by the state of the program, it is not a tool, since +//! the user didn't explicitly ask for this function to be performed, but it is rather an option +//! that's inherent to the situation the user finds themselves in. + +pub mod deletion_tool; +pub mod icon_tool; +pub mod polygon_room_tool; +pub mod rect_room_tool; +pub mod selection_tool; +pub mod wall_tool; + +pub use deletion_tool::DeletionTool; +pub use icon_tool::IconTool; +pub use polygon_room_tool::PolygonRoomTool; +pub use rect_room_tool::RectRoomTool; +pub use selection_tool::SelectionTool; +pub use wall_tool::WallTool; + +use crate::client::input::Input; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::Connection; +use crate::math::Vec2; +use crate::net::Cargo; +use raylib::core::drawing::RaylibDrawHandle; + +#[derive(Copy, Clone, Debug, Hash, PartialEq, Eq)] +#[repr(u8)] +/// The types of tools available in graf karto. For information about the tool itself, please see the +/// referenced Tool's documentation. +pub enum ToolType { + /// See [RectRoomTool] for information on this tool. + RectRoomTool, + /// See [PolygonRoomTool] for information on this tool. + PolygonRoomTool, + /// See [WallTool] for information on this tool. + WallTool, + /// See [IconTool] for information on this tool. + IconTool, + /// See [DeletionTool] for information on this tool. + DeletionTool, + /// See [SelectionTool] for information on this tool. + SelectionTool, + /// Not a real tool but used to know how many tools are available. New tools must be added + /// above this variant. + // TODO: Since we now use a hash map in the editor, check if this is still necessary at all. + NumTools, +} + +/// Base trait for tools. A tool is something that performs a specific action on one or more types of +/// elements. It must be selected in order to be active. For this reason, the selection tool is a +/// tool (it must be selected from the toolbox), but the dimension indicator for instance is not, +/// since it is automatically updated when applicable. +pub trait Tool { + /// Code that needs to be called when this Tool is activated or reactivated goes here. + fn activate(&mut self) {} + /// Cleanup that needs to be done when the user switches from this tool to something else goes here. + fn deactivate(&mut self) {} + + /// Called on each frame when this tool is active. + fn update(&mut self, map: &Map, mouse_pos_m: &Vec2<f64>); + + /// Draw the contents of this tool. + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform); + + /// Generic keybinding. + /// Code to place a single node for this tool. + fn place_single( + &mut self, + _map: &mut Map, + _server: &Connection<Cargo>, + _mouse_pos_m: &Vec2<f64>, + ) { + } + + /// Generic keybinding. + /// Code to finish whatever one is doing with this tool currently and trying to apply the + /// changes to the map data. + fn finish(&mut self, _map: &mut Map, _server: &Connection<Cargo>) {} + + /// Generic keybinding. + /// Stop whatever one is doing with this tool and do not apply any changes to the map data. + fn abort(&mut self) {} + + /// If there are any additional keybindings that need to be handled by this tool, these can be + /// handled here. + fn handle_custom_bindings( + &mut self, + _map: &mut Map, + _server: &Connection<Cargo>, + _input: &mut Input, + ) { + } +} diff --git a/src/client/tool/polygon_room_tool.rs b/src/client/tool/polygon_room_tool.rs new file mode 100644 index 0000000..63456cc --- /dev/null +++ b/src/client/tool/polygon_room_tool.rs @@ -0,0 +1,141 @@ +//! Tool to create rooms in the shape of generic polygons. + +use super::Tool; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::client::FLOAT_MARGIN; +use crate::math::{self, PolygonGraph, Vec2}; +use crate::net::{Cargo, Connection}; +use crate::world::Room; +use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; + +/// The tool itself. +pub struct PolygonRoomTool { + unfinished_room: Option<(PolygonGraph<f64>, Vec2<f64>)>, + last_mouse_pos_m: Vec2<f64>, +} + +impl PolygonRoomTool { + /// Create a new polygon room tool. There should be only one instance and it should be created + /// in the editor. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + unfinished_room: None, + last_mouse_pos_m: Vec2::new(0., 0.), + } + } + + /* Helper function to try and finish the currently drawn polygon. If successful, it will add it + * to the map, clear the currently drawn polygon and return bool. Otherwise it will leave the + * unfinished polygon as is and return false without pushing anything. + */ + fn try_push(&mut self, server: &Connection<Cargo>) -> bool { + if self.unfinished_room.is_none() { + return false; + } + + match self + .unfinished_room + .as_ref() + .unwrap() + .0 + .clone() + .bounding_polygon(FLOAT_MARGIN) + { + Some(polygon) => { + server.send(Cargo::AddRoom(Room::new(polygon))); + self.unfinished_room = None; + true + } + None => false, + } + } +} + +impl Tool for PolygonRoomTool { + fn deactivate(&mut self) { + self.unfinished_room = None; + } + + fn update(&mut self, _map: &Map, mouse_pos_m: &Vec2<f64>) { + // Update the last mouse position that was seen for later use. + self.last_mouse_pos_m = *mouse_pos_m; + } + + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + if let Some((graph, last_node)) = &self.unfinished_room { + /* To turn the graph into a polygon, we need a copy, might as well do + * it now, so we can add the working corner to it. + */ + let mut graph = graph.clone(); + + // Add the current mouse position as the next position if possible. + graph.add_edge(&last_node, &self.last_mouse_pos_m); + + if graph.num_nodes() <= 1 { + // Only able to draw a point + rld.draw_circle_v( + transform.point_m_to_px(&self.last_mouse_pos_m), + transform.length_m_to_px(0.1) as f32, + DEFAULT_COLOURS.room_selected, + ); + } else if let Some(polygon) = graph.clone().bounding_polygon(FLOAT_MARGIN) { + let triangles = math::triangulate(polygon, FLOAT_MARGIN); + for triangle in triangles { + let triangle: [Vec2<f64>; 3] = triangle.into(); + rld.draw_triangle( + transform.point_m_to_px(&triangle[0]), + transform.point_m_to_px(&triangle[1]), + transform.point_m_to_px(&triangle[2]), + DEFAULT_COLOURS.room_selected, + ) + } + } else { + // For some reason the polygon creation failed. Draw lines for the edges instead. + for edge in graph.edge_iter() { + rld.draw_line_ex( + transform.point_m_to_px(&edge.start), + transform.point_m_to_px(&edge.end), + transform.length_m_to_px(0.1) as f32, + DEFAULT_COLOURS.room_selected, + ); + } + } + } + } + + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { + if let Some((ref mut graph, ref mut last_placed)) = &mut self.unfinished_room { + // If the corner already exists in the polygon, try to finish and push it after adding the + // next edge. + let try_finish = graph.has_node(&mouse_pos_m); + + // Add an edge from the last corner to the currently active position if possible. + if graph.add_edge(last_placed, &mouse_pos_m) { + *last_placed = *mouse_pos_m; + } + + if try_finish { + self.try_push(server); + } + } else { + // Start a new unfinished polygon + self.unfinished_room = Some((PolygonGraph::new(), *mouse_pos_m)); + } + } + + fn finish(&mut self, _map: &mut Map, server: &Connection<Cargo>) { + self.try_push(server); + } + + fn abort(&mut self) { + self.unfinished_room = None; + } +} diff --git a/src/client/tool/rect_room_tool.rs b/src/client/tool/rect_room_tool.rs new file mode 100644 index 0000000..41f2a91 --- /dev/null +++ b/src/client/tool/rect_room_tool.rs @@ -0,0 +1,91 @@ +//! The rectangle room tool is a specialised tool to create rooms of rectangular shape and with the +//! sides of the room parallel to the x and y-axes. This is often useful, when a quick room creation +//! is necessary and the shape of the room does not have to be very special. + +use super::Tool; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::math::{Rect, Vec2}; +use crate::net::{Cargo, Connection}; +use crate::world::Room; +use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; + +/// The tool to create simple, rectangular rooms. +pub struct RectRoomTool { + /// The rectangle that is currently being drawn by the user. Once it is finished, it will be + /// pushed into the room_rects. + unfinished_rect: Option<(Vec2<f64>, Vec2<f64>)>, +} + +impl RectRoomTool { + /// Create a new room tool where no rooms have been drawn yet. Should be created only once per + /// program instance and by the editor. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + unfinished_rect: None, + } + } +} + +impl Tool for RectRoomTool { + fn deactivate(&mut self) { + self.unfinished_rect = None; + } + + fn update(&mut self, _map: &Map, mouse_pos_m: &Vec2<f64>) { + if let Some((_, ref mut pos2)) = &mut self.unfinished_rect { + *pos2 = *mouse_pos_m; + } + } + + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + if let Some((pos1, pos2)) = self.unfinished_rect { + rld.draw_rectangle_rec( + transform.rect_m_to_px(&Rect::bounding_rect(pos1, pos2)), + DEFAULT_COLOURS.room_selected, + ); + } + } + + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { + // Try to finish the rectangle if it has been started. + if let Some((pos1, pos2)) = self.unfinished_rect { + if pos1 == pos2 { + warn!("Cannot place rectangle with start and endpoint being the same"); + return; + } + + server.send(Cargo::AddRoom(Room::new( + Rect::bounding_rect(pos1, pos2).into(), + ))); + self.unfinished_rect = None; + } else { + self.unfinished_rect = Some((*mouse_pos_m, *mouse_pos_m)); + } + } + + fn finish(&mut self, _map: &mut Map, server: &Connection<Cargo>) { + if let Some((pos1, pos2)) = self.unfinished_rect { + if pos1 == pos2 { + warn!("Cannot place rectangle with start and endpoint being the same"); + return; + } + + server.send(Cargo::AddRoom(Room::new( + Rect::bounding_rect(pos1, pos2).into(), + ))); + self.unfinished_rect = None; + } + } + + fn abort(&mut self) { + self.unfinished_rect = None; + } +} diff --git a/src/client/tool/selection_tool.rs b/src/client/tool/selection_tool.rs new file mode 100644 index 0000000..52c2155 --- /dev/null +++ b/src/client/tool/selection_tool.rs @@ -0,0 +1,74 @@ +//! Selection of items on the map. +//! +//! When selecting items on the map, the editor goes into a different mode than when editing a +//! specific kind of item. Actions that are available for specific types of items become +//! unavailable, while other actions that make use of the properties to a wide range of items +//! become available instead. +//! For this reason, the selection tool can be thought of as a kind of meta tool over tools. + +use super::Tool; +use crate::client::colours::DEFAULT_COLOURS; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::math::{ExactSurface, Rect, Vec2}; +use crate::net::{Cargo, Connection}; +use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; + +/// The selection tool makes it possible to select any item on the map when activated. +pub struct SelectionTool { + selection_rect: Option<(Vec2<f64>, Vec2<f64>)>, +} + +impl SelectionTool { + /// Create a new selection tool. There should be only one such tool per program instance and it + /// should be created in the editor. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + selection_rect: None, + } + } +} + +impl Tool for SelectionTool { + fn deactivate(&mut self) { + self.selection_rect = None; + } + + fn update(&mut self, _map: &Map, mouse_pos_m: &Vec2<f64>) { + if let Some((_, ref mut pos2)) = &mut self.selection_rect { + *pos2 = *mouse_pos_m; + } + } + + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + if let Some((pos1, pos2)) = self.selection_rect { + let rect_px = transform.rect_m_to_px(&Rect::bounding_rect(pos1, pos2)); + rld.draw_rectangle_rec(rect_px, DEFAULT_COLOURS.selection_rect); + rld.draw_rectangle_lines_ex(rect_px, 4, DEFAULT_COLOURS.selection_rect_outline); + } + } + + fn place_single( + &mut self, + map: &mut Map, + _server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { + if let Some((pos1, pos2)) = self.selection_rect { + // Select all items on the map that are inside of the selection rectangle + let bounds = Rect::bounding_rect(pos1, pos2); + for (_id, element) in map.elements_mut() { + // TODO: Make it possible to do this additively by custom keybinding. + element.set_selected(bounds.contains_rect(&element.as_component().bounding_rect())); + } + self.selection_rect = None; + } else { + self.selection_rect = Some((*mouse_pos_m, *mouse_pos_m)); + } + } + + fn abort(&mut self) { + self.selection_rect = None; + } +} diff --git a/src/client/tool/wall_tool.rs b/src/client/tool/wall_tool.rs new file mode 100644 index 0000000..857beea --- /dev/null +++ b/src/client/tool/wall_tool.rs @@ -0,0 +1,76 @@ +//! Tool to create walls. For information about walls, see also +//! [the wall module](crate::map::wall). + +use super::Tool; +use crate::client::map::Map; +use crate::client::transform::Transform; +use crate::math::{LineSegment, Vec2}; +use crate::net::{Cargo, Connection}; +use crate::world::Wall; +use raylib::core::drawing::{RaylibDraw, RaylibDrawHandle}; +use raylib::ffi::{Color, Vector2}; + +/// The wall tool to create solid barriers a player usually cannot cross. +pub struct WallTool { + unfinished_wall: Option<LineSegment<f64>>, +} + +impl WallTool { + /// Create a new wall tool. There should only be one wall tool per program instance, which should + /// be created inside of the editor. + #[allow(clippy::new_without_default)] + pub fn new() -> Self { + Self { + unfinished_wall: None, + } + } +} + +impl Tool for WallTool { + fn deactivate(&mut self) { + self.unfinished_wall = None; + } + + fn update(&mut self, _map: &Map, mouse_pos_m: &Vec2<f64>) { + if let Some(ref mut wall) = &mut self.unfinished_wall { + wall.end = *mouse_pos_m; + } + } + + fn draw(&self, rld: &mut RaylibDrawHandle, transform: &Transform) { + if let Some(ref wall) = self.unfinished_wall { + let start: Vector2 = transform.point_m_to_px(&wall.start).into(); + let end: Vector2 = transform.point_m_to_px(&wall.end).into(); + rld.draw_line_ex( + start, + end, + transform.length_m_to_px(0.1) as f32, + Color { + r: 150, + g: 200, + b: 150, + a: 255, + }, + ); + } + } + + fn place_single( + &mut self, + _map: &mut Map, + server: &Connection<Cargo>, + mouse_pos_m: &Vec2<f64>, + ) { + if let Some(wall) = self.unfinished_wall.take() { + // Continue with the next wall straight away. + self.unfinished_wall = Some(LineSegment::new(wall.end, wall.end)); + server.send(Cargo::AddWall(Wall::new(wall))); + } else { + self.unfinished_wall = Some(LineSegment::new(*mouse_pos_m, *mouse_pos_m)); + } + } + + fn abort(&mut self) { + self.unfinished_wall = None; + } +} diff --git a/src/client/transform.rs b/src/client/transform.rs new file mode 100644 index 0000000..147956c --- /dev/null +++ b/src/client/transform.rs @@ -0,0 +1,142 @@ +//! Transformation module +//! +//! Useful to turn on-screen coordinates into measurements of the "real" world the map describes +//! and the other way around. + +use crate::math::{Rect, Vec2}; + +const STANDARD_PIXELS_PER_M: f64 = 64.; +const MIN_PIXELS_PER_M: f64 = 5.; +const MAX_PIXELS_PER_M: f64 = 10_000.; + +/// A rigid 2D transformation. Since the translation must often be accessed directly and so far there +/// was no huge need for fancy transformation, this currently does not use any matrix transformations. +pub struct Transform { + /// The (not necessarily natural) number of pixels per m, i.e. the current scale of the map + pixels_per_m: f64, + /// The vector the entire on-screen map is moved by in pixels + translation_px: Vec2<f64>, +} + +impl Transform { + /// Create a new standard transformation for the map. + pub fn new() -> Self { + Self { + pixels_per_m: STANDARD_PIXELS_PER_M, + translation_px: Vec2::new(0., 0.), + } + } + + /// Convert a point that is given in meters into the corresponding point in pixels. + #[inline] + pub fn point_m_to_px(&self, point: &Vec2<f64>) -> Vec2<f64> { + // Start by converting the absolute position in meters into the absolute position in + // pixels, then add the translation of the screen. + (*point * self.pixels_per_m) + self.translation_px + } + + /// Convert an on-screen point into an absolute point with values in meters. + #[inline] + pub fn point_px_to_m(&self, point: &Vec2<f64>) -> Vec2<f64> { + // Start by subtracting the pixel translation and afterwards convert these absolute pixel + // measurements into meters. + (*point - self.translation_px) / self.pixels_per_m + } + + /// Convert a length given in meters into a length in pixels + #[inline] + pub fn length_m_to_px(&self, length: f64) -> f64 { + length * self.pixels_per_m + } + + /// Convert a length given in pixels into a length in meters + #[inline] + pub fn length_px_to_m(&self, length: f64) -> f64 { + length / self.pixels_per_m + } + + /// Convert a rectangle which has measurements in meters into one of pixels + #[inline] + pub fn rect_m_to_px(&self, rect: &Rect<f64>) -> Rect<f64> { + let left_upper = self.point_m_to_px(&Vec2::new(rect.x, rect.y)); + Rect::new( + left_upper.x, + left_upper.y, + self.length_m_to_px(rect.w), + self.length_m_to_px(rect.h), + ) + } + + /// Convert a rectangle which has measurements in pixels into one of meters + #[inline] + pub fn rect_px_to_m(&self, rect: &Rect<f64>) -> Rect<f64> { + let left_upper = self.point_px_to_m(&Vec2::new(rect.x, rect.y)); + Rect::new( + left_upper.x, + left_upper.y, + self.length_px_to_m(rect.w), + self.length_px_to_m(rect.h), + ) + } + + /// Attempts to zoom the pixels per meter by the amount of factor. + /// + /// # Arguments + /// `factor`: A number greater than one means zooming in, a number less than one means zooming out. What happens when you try to + /// zoom with a negative factor you'll have to figure out yourself. + /// `mouse_pos_px`: Position of the mouse cursor, this time not in meters, but in screen + /// pixels. This will be used to tether zoom on that point. + pub fn try_zoom(&mut self, mouse_pos_px: &Vec2<f64>, factor: f64) -> bool { + // Abort zooming when the scale would not be in the min-max-bounds anymore. + let desired_px_per_m = self.pixels_per_m * factor; + if (factor < 1. && desired_px_per_m <= MIN_PIXELS_PER_M) + || (factor > 1. && desired_px_per_m >= MAX_PIXELS_PER_M) + { + return false; + } + + // Save the absolute mouse position in meters for tethering later + let mouse_pos_m = self.point_px_to_m(&mouse_pos_px); + + // Make sure the desired scale stays within the bounds and in whole numbers + let desired_px_per_m = if desired_px_per_m < MIN_PIXELS_PER_M { + MIN_PIXELS_PER_M as u32 as f64 + } else if desired_px_per_m > MAX_PIXELS_PER_M { + MAX_PIXELS_PER_M as u32 as f64 + } else { + desired_px_per_m as u32 as f64 + }; + + /* Adjust to the desired scale and bring the map back to its desired position according to + * the mouse pointer position. + */ + self.pixels_per_m = desired_px_per_m; + self.translation_px += *mouse_pos_px - self.point_m_to_px(&mouse_pos_m); + + true + } + + /// Move the canvas by the vector in pixels. + pub fn move_by_px(&mut self, by: &Vec2<f64>) { + self.translation_px += *by; + } + + /// Get the current scale with the number of pixels (as a real number) that makes up a meter of + /// actual map space. + pub fn pixels_per_m(&self) -> f64 { + self.pixels_per_m + } + /// Get the current translation of the map on the screen. This is purely in pixels, rather than + /// meters, since a translation in meters does not make sense, because all map items have an + /// absolute position and the translation is merely used to see where on the screen anything + /// should be drawn. + pub fn translation_px(&self) -> &Vec2<f64> { + &self.translation_px + } +} + +impl Default for Transform { + fn default() -> Self { + Self::new() + } +} |
