aboutsummaryrefslogtreecommitdiff
path: root/src/client
diff options
context:
space:
mode:
Diffstat (limited to 'src/client')
-rw-r--r--src/client/cli/cmd/edit.rs53
-rw-r--r--src/client/cli/cmd/mod.rs59
-rw-r--r--src/client/cli/cmd/read.rs55
-rw-r--r--src/client/cli/cmd/write.rs41
-rw-r--r--src/client/cli/mod.rs128
-rw-r--r--src/client/colours.rs158
-rw-r--r--src/client/config.rs166
-rw-r--r--src/client/editor.rs228
-rw-r--r--src/client/grid.rs56
-rw-r--r--src/client/gui/decimal_num_box.rs173
-rw-r--r--src/client/gui/dimension_indicator.rs311
-rw-r--r--src/client/gui/mod.rs18
-rw-r--r--src/client/gui/position_indicator.rs37
-rw-r--r--src/client/gui/tool_sidebar.rs91
-rw-r--r--src/client/input/binding.rs123
-rw-r--r--src/client/input/button.rs177
-rw-r--r--src/client/input/mod.rs200
-rw-r--r--src/client/map/icon_mark.rs100
-rw-r--r--src/client/map/icon_texture_manager.rs91
-rw-r--r--src/client/map/mappable.rs23
-rw-r--r--src/client/map/mod.rs253
-rw-r--r--src/client/map/room_mark.rs81
-rw-r--r--src/client/map/wall_mark.rs112
-rw-r--r--src/client/mod.rs137
-rw-r--r--src/client/snapping.rs81
-rw-r--r--src/client/svg/mod.rs178
-rw-r--r--src/client/svg/style.rs180
-rw-r--r--src/client/tool/deletion_tool.rs82
-rw-r--r--src/client/tool/icon_tool.rs88
-rw-r--r--src/client/tool/mod.rs98
-rw-r--r--src/client/tool/polygon_room_tool.rs141
-rw-r--r--src/client/tool/rect_room_tool.rs96
-rw-r--r--src/client/tool/selection_tool.rs74
-rw-r--r--src/client/tool/wall_tool.rs76
-rw-r--r--src/client/transform.rs142
35 files changed, 4107 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..1cfb530
--- /dev/null
+++ b/src/client/cli/cmd/edit.rs
@@ -0,0 +1,53 @@
+//! Replace the contents of the currently edited map with contents from a file.
+
+use super::Command;
+use super::{CmdParseError, FromArgs};
+use crate::client::Editor;
+use crate::net::Cargo;
+use crate::world::World;
+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 world = match World::load_from_file(&self.file) {
+ Ok(world) => world,
+ Err(err) => {
+ return Err(format!(
+ "Unable to read file: {:?}, reason: {:?}",
+ &self.file, err
+ ))
+ }
+ };
+
+ // Clear all data from the world, afterwards add all components from the file.
+ editor.server().send(Cargo::ClearAll);
+ for (_, icon) in world.icons().iter() {
+ editor.server().send(Cargo::AddIcon(icon.clone()));
+ }
+ for (_, room) in world.rooms().iter() {
+ editor.server().send(Cargo::AddRoom(room.clone()));
+ }
+ for (_, wall) in world.walls().iter() {
+ editor.server().send(Cargo::AddWall(wall.clone()));
+ }
+
+ 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..3b20308
--- /dev/null
+++ b/src/client/cli/cmd/read.rs
@@ -0,0 +1,55 @@
+//! 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::net::Cargo;
+use crate::world::World;
+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 world = match World::load_from_file(&self.file) {
+ Ok(data) => data,
+ Err(err) => {
+ return Err(format!(
+ "Unable to read file: {:?}, reason: {:?}",
+ &self.file, err
+ ))
+ }
+ };
+
+ // Send all components of the file to the server.
+ for (_, icon) in world.icons().iter() {
+ editor.server().send(Cargo::AddIcon(icon.clone()));
+ }
+ for (_, room) in world.rooms().iter() {
+ editor.server().send(Cargo::AddRoom(room.clone()));
+ }
+ for (_, wall) in world.walls().iter() {
+ editor.server().send(Cargo::AddWall(wall.clone()));
+ }
+
+ 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..37d5a0a
--- /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 world = editor.map().clone_as_world();
+
+ match world.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..14f25ef
--- /dev/null
+++ b/src/client/editor.rs
@@ -0,0 +1,228 @@
+//! 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,
+ ) {
+ self.poll_net();
+
+ // 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);
+ }
+
+ fn poll_net(&mut self) {
+ while let Some(cargo) = self.server().next_packet() {
+ match cargo {
+ Cargo::SetRoom((id, new_room)) => match self.map.get_room_mut(id) {
+ Some(mark) => mark.set_room(new_room),
+ None => {
+ self.map.add_room(id, new_room);
+ }
+ },
+ Cargo::SetIcon((id, new_icon)) => match self.map.get_icon_mut(id) {
+ Some(mark) => mark.set_icon(new_icon),
+ None => {
+ self.map.add_icon(id, new_icon);
+ }
+ },
+ Cargo::SetWall((id, new_wall)) => match self.map.get_wall_mut(id) {
+ Some(mark) => mark.set_wall(new_wall),
+ None => {
+ self.map.add_wall(id, new_wall);
+ }
+ },
+ Cargo::ClearAll => self.map.clear(),
+ Cargo::Remove(id) => {
+ self.map.remove(id);
+ }
+ Cargo::AddMapData(_) => unimplemented!(),
+ Cargo::UpdateMapData(_) => unimplemented!(),
+ Cargo::AddIcon(_)
+ | Cargo::AddRoom(_)
+ | Cargo::AddWall(_)
+ | Cargo::ApplyMatrix(_) => {
+ error!("Packet is only valid in Client -> Server direction")
+ }
+ }
+ }
+ }
+
+ /// 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..ebad78b
--- /dev/null
+++ b/src/client/gui/dimension_indicator.rs
@@ -0,0 +1,311 @@
+//! 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::input::{Button, Input, Scancode};
+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;
+use std::sync::mpsc::Receiver;
+
+/// 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,
+ text_pipe: Receiver<char>,
+ },
+}
+
+/// 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 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(input: &mut Input) -> Self {
+ input.add_global(Button::Scancode(Scancode::Tab).into());
+
+ Self {
+ state: State::default(),
+ bounds: Rect::new(0., 0., 0., 0.),
+ }
+ }
+
+ /// Update whatever is selected on the map according to the dimension indicator rules and rulers.
+ pub fn update(&mut self, editor: &Editor, input: &mut Input) {
+ match self.state {
+ State::Watching => self.update_watching(editor.map(), input),
+ State::Ruling { .. } => self.update_ruling(editor, input),
+ };
+ }
+
+ fn update_watching(&mut self, map: &Map, input: &mut Input) {
+ 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 && input.poll_global(&Button::Scancode(Scancode::Tab).into()) {
+ // Try to capture the keyboard and go into the ruling state.
+ if let Some(text_pipe) = input.try_capture_keyboard() {
+ self.state = State::Ruling {
+ dim_x: self.bounds.w.to_string(),
+ dim_y: self.bounds.h.to_string(),
+ editing_x: true,
+ text_pipe,
+ };
+ }
+ }
+ }
+
+ fn update_ruling(&mut self, editor: &Editor, input: &mut Input) {
+ // Get the currently edited dimension for processing.
+ let (edited_dim, editing_x, text_pipe) = match &mut self.state {
+ State::Watching => panic!("Called ruler update when in watching state"),
+ State::Ruling {
+ dim_x,
+ dim_y,
+ editing_x,
+ text_pipe,
+ } => {
+ if *editing_x {
+ (dim_x, editing_x, text_pipe)
+ } else {
+ (dim_y, editing_x, text_pipe)
+ }
+ }
+ };
+
+ let next_char = match text_pipe.try_recv() {
+ Ok(c) => c,
+ Err(_) => return,
+ };
+
+ let update_bounds = match next_char {
+ '\t' => {
+ // Switch the currently edited dimension when pressing tab.
+ *editing_x = !*editing_x;
+ return;
+ }
+ '\n' => {
+ // Finish editing mode on enter.
+ self.state = State::Watching;
+ return;
+ }
+ '\x7f' => {
+ // Delete last char on backspace
+ edited_dim.pop();
+ true
+ }
+ key => {
+ match key {
+ // Add a decimal point to the dimension if possible.
+ '.' => {
+ if !edited_dim.contains('.') {
+ edited_dim.push('.');
+ }
+ // Nothing changed here, since there is an implicit .0 at the end.
+ false
+ }
+ // Handle the entered key if it is a number to append it to the currently edited dimension.
+ '0'..='9' => {
+ edited_dim.push(key);
+ true
+ }
+ _ => false,
+ }
+ }
+ };
+
+ if update_bounds {
+ /* 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, dbg!(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,
+ );
+ }
+ }
+}
+
+impl Default for State {
+ fn default() -> Self {
+ Self::Watching
+ }
+}
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..02ed501
--- /dev/null
+++ b/src/client/map/icon_mark.rs
@@ -0,0 +1,100 @@
+//! 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,
+ }
+ }
+
+ /// Set the inner icon this icon mark is referencing.
+ pub fn set_icon(&mut self, icon: Icon) {
+ self.icon = icon;
+ }
+}
+
+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..7a0613c
--- /dev/null
+++ b/src/client/map/mod.rs
@@ -0,0 +1,253 @@
+//! 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::{Icon, Room, Wall, World};
+use icon_texture_manager::IconTextureManager;
+use raylib::drawing::RaylibDrawHandle;
+use raylib::{RaylibHandle, RaylibThread};
+use std::ops::Deref;
+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: Icon) -> bool {
+ if self.used_ids.try_insert(id, ()).is_ok() {
+ self.icons
+ .try_insert(id, IconMark::from_icon(icon, self.icon_renderer.clone()))
+ .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)),
+ )
+ }
+
+ /// Remove all items from the map.
+ pub fn clear(&mut self) {
+ self.rooms.clear();
+ self.walls.clear();
+ self.icons.clear();
+ self.used_ids.clear();
+ }
+
+ /// Get the rooms of this map.
+ pub fn rooms(&self) -> &StableVec<RoomMark> {
+ &self.rooms
+ }
+ /// Get a room with the given id mutably, in case it exists.
+ pub fn get_room_mut(&mut self, id: usize) -> Option<&mut RoomMark> {
+ self.rooms.get_mut(id)
+ }
+
+ /// Get the walls of this map.
+ pub fn walls(&self) -> &StableVec<WallMark> {
+ &self.walls
+ }
+ /// Get a wall with the given id mutably, in case it exists.
+ pub fn get_wall_mut(&mut self, id: usize) -> Option<&mut WallMark> {
+ self.walls.get_mut(id)
+ }
+
+ /// Get the icons of this map.
+ pub fn icons(&self) -> &StableVec<IconMark> {
+ &self.icons
+ }
+ /// Get an icon with the given id mutably, in case it exists.
+ pub fn get_icon_mut(&mut self, id: usize) -> Option<&mut IconMark> {
+ self.icons.get_mut(id)
+ }
+
+ /// 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, i.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());
+ }
+ }
+
+ /// Creates a world from
+ pub fn clone_as_world(&self) -> World {
+ let rooms = self
+ .rooms
+ .iter()
+ .map(|(id, mark)| (*id, mark.deref().clone()))
+ .collect();
+ let rooms = StableVec::from_raw_unchecked(rooms);
+ let icons = self
+ .icons
+ .iter()
+ .map(|(id, mark)| (*id, mark.deref().clone()))
+ .collect();
+ let icons = StableVec::from_raw_unchecked(icons);
+ let walls = self
+ .walls
+ .iter()
+ .map(|(id, mark)| (*id, mark.deref().clone()))
+ .collect();
+ let walls = StableVec::from_raw_unchecked(walls);
+
+ World::from_raw_unchecked(rooms, walls, icons, self.used_ids.clone())
+ }
+}
diff --git a/src/client/map/room_mark.rs b/src/client/map/room_mark.rs
new file mode 100644
index 0000000..5c0ca98
--- /dev/null
+++ b/src/client/map/room_mark.rs
@@ -0,0 +1,81 @@
+//! 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,
+ }
+ }
+
+ /// Replace the room this mark describes with the new room.
+ pub fn set_room(&mut self, room: Room) {
+ self.room = room;
+ self.retriangulate();
+ }
+
+ /* 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..76ac03b
--- /dev/null
+++ b/src/client/map/wall_mark.rs
@@ -0,0 +1,112 @@
+//! 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,
+ }
+ }
+
+ /// Set the internal wall of this wall mark.
+ pub fn set_wall(&mut self, wall: Wall) {
+ // XXX: Rounded edges??
+ self.wall = wall;
+ }
+
+ /// 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..28d9e20
--- /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(&mut input);
+ 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 input);
+ 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..60bf246
--- /dev/null
+++ b/src/client/tool/rect_room_tool.rs
@@ -0,0 +1,96 @@
+//! 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::client::FLOAT_MARGIN;
+use crate::math::{Rect, Vec2};
+use crate::net::{Cargo, Connection};
+use crate::world::Room;
+use float_cmp::ApproxEq;
+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,
+ }
+ }
+}
+
+/* Try to create a rectangle from the given points, but only if they don't create
+ * a rectangle with no area.
+ */
+fn try_into_bounding_rect(pos1: Vec2<f64>, pos2: Vec2<f64>) -> Option<Rect<f64>> {
+ let bounding_rect = Rect::bounding_rect(pos1, pos2);
+
+ if !bounding_rect.area().approx_eq(0., FLOAT_MARGIN) {
+ Some(bounding_rect)
+ } else {
+ 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 let Some(rect) = try_into_bounding_rect(pos1, pos2) {
+ server.send(Cargo::AddRoom(Room::new(rect.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 let Some(rect) = try_into_bounding_rect(pos1, pos2) {
+ server.send(Cargo::AddRoom(Room::new(rect.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()
+ }
+}