aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/cli/cmd/edit.rs35
-rw-r--r--src/cli/cmd/mod.rs59
-rw-r--r--src/cli/cmd/read.rs38
-rw-r--r--src/cli/cmd/save.rs42
-rw-r--r--src/cli/mod.rs104
-rw-r--r--src/colours.rs32
-rw-r--r--src/editor.rs14
-rw-r--r--src/gui/mod.rs2
-rw-r--r--src/gui/position_indicator.rs31
-rw-r--r--src/gui/tool_sidebar.rs3
-rw-r--r--src/main.rs10
-rw-r--r--src/map/data.rs33
-rw-r--r--src/map/mod.rs49
-rw-r--r--src/map/polygon_room.rs9
14 files changed, 442 insertions, 19 deletions
diff --git a/src/cli/cmd/edit.rs b/src/cli/cmd/edit.rs
new file mode 100644
index 0000000..797edc6
--- /dev/null
+++ b/src/cli/cmd/edit.rs
@@ -0,0 +1,35 @@
+//! Replace the contents of the currently edited map with contents from a file.
+
+use super::Command;
+use super::{CmdParseError, FromArgs};
+use crate::map::MapData;
+use crate::Editor;
+use std::path::PathBuf;
+
+pub struct Edit {
+ file: PathBuf,
+}
+
+impl FromArgs for Edit {
+ fn from_args(args: &[&str]) -> Result<Self, CmdParseError> {
+ if args.len() != 1 {
+ return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1));
+ }
+
+ Ok(Self {
+ file: PathBuf::from(args[0]),
+ })
+ }
+}
+
+impl Command for Edit {
+ fn process(&self, editor: &mut Editor) -> Result<String, String> {
+ let data = match MapData::load_from_file(&self.file) {
+ Ok(data) => data,
+ Err(err) => return Err(format!("Unable to read file: {:?}", &self.file)),
+ };
+
+ editor.map_mut().set_data(data);
+ Ok(format!("Map data from {:?} loaded.", &self.file))
+ }
+}
diff --git a/src/cli/cmd/mod.rs b/src/cli/cmd/mod.rs
new file mode 100644
index 0000000..42e865a
--- /dev/null
+++ b/src/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 save;
+
+pub use edit::*;
+pub use read::*;
+pub use save::*;
+
+use crate::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(Save::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/cli/cmd/read.rs b/src/cli/cmd/read.rs
new file mode 100644
index 0000000..4ac671c
--- /dev/null
+++ b/src/cli/cmd/read.rs
@@ -0,0 +1,38 @@
+//! Read the contents of a file and add it to the currently edited map.
+
+use super::Command;
+use super::{CmdParseError, FromArgs};
+use crate::map::MapData;
+use crate::Editor;
+use std::path::PathBuf;
+
+pub struct Read {
+ file: PathBuf,
+}
+
+impl FromArgs for Read {
+ fn from_args(args: &[&str]) -> Result<Self, CmdParseError> {
+ if args.len() != 1 {
+ return Err(CmdParseError::WrongNumberOfArgs(args.len(), 1..=1));
+ }
+
+ Ok(Self {
+ file: PathBuf::from(args[0]),
+ })
+ }
+}
+
+impl Command for Read {
+ fn process(&self, editor: &mut Editor) -> Result<String, String> {
+ let data = match MapData::load_from_file(&self.file) {
+ Ok(data) => data,
+ Err(err) => return Err(format!("Unable to read file: {:?}", &self.file)),
+ };
+
+ editor.map_mut().add_data(data);
+ Ok(format!(
+ "Map data from {:?} read and added to the current buffer.",
+ &self.file
+ ))
+ }
+}
diff --git a/src/cli/cmd/save.rs b/src/cli/cmd/save.rs
new file mode 100644
index 0000000..2c022cf
--- /dev/null
+++ b/src/cli/cmd/save.rs
@@ -0,0 +1,42 @@
+//! Save the contents of the map to disk
+
+use super::Command;
+use super::{CmdParseError, FromArgs};
+use crate::map::MapData;
+use crate::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 Save {
+ destination: PathBuf,
+}
+
+impl FromArgs for Save {
+ 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 Save {
+ fn process(&self, editor: &mut Editor) -> Result<String, String> {
+ let data = MapData::extract_data(editor.map());
+
+ match data.write_to_file(&self.destination) {
+ Ok(_) => Ok(format!(
+ "Successfully wrote contents to `{:?}`",
+ &self.destination
+ )),
+ Err(e) => Err(format!(
+ "Unable to write to `{:?}`. Error: {:?}",
+ &self.destination, e
+ )),
+ }
+ }
+}
diff --git a/src/cli/mod.rs b/src/cli/mod.rs
new file mode 100644
index 0000000..e96070f
--- /dev/null
+++ b/src/cli/mod.rs
@@ -0,0 +1,104 @@
+//! 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::colours::DEFAULT_COLOURS;
+use crate::math::Vec2;
+use crate::Editor;
+use raylib::drawing::{RaylibDraw, RaylibDrawHandle};
+use raylib::ffi::KeyboardKey;
+use raylib::RaylibHandle;
+
+/// The command line interface. Should be created only once per program instance.
+pub struct CLI {
+ text: String,
+ active: bool,
+}
+
+impl CLI {
+ /// Create a CLI for this instance
+ pub fn new() -> Self {
+ Self {
+ text: String::new(),
+ active: false,
+ }
+ }
+
+ /// Activates the CLI, which will now capture keyboard input and execute commands accordingly.
+ pub fn activate(&mut self) {
+ if !self.active {
+ self.text = ";".to_owned();
+ self.active = true;
+ }
+ }
+
+ /// Handle input for the command line and perform any commands the user may want to run.
+ pub fn update(&mut self, rl: &mut RaylibHandle, editor: &mut Editor) {
+ /* 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 rl.is_key_pressed(KeyboardKey::KEY_SEMICOLON) {
+ // Don't write the keypress again.
+ rl.get_key_pressed();
+ self.activate();
+ } else {
+ return;
+ }
+ }
+
+ // The CLI is currently active. Handle input to it.
+ if let Some(key) = rl.get_key_pressed_number() {
+ self.text.push(key as u8 as char);
+ } else if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) {
+ self.text.pop();
+ } else if rl.is_key_pressed(KeyboardKey::KEY_ESCAPE) {
+ self.text.clear();
+ }
+
+ // When the text is empty, there is also no command marker, so set as inactive and leave.
+ if self.text.is_empty() {
+ self.active = false;
+ return;
+ }
+
+ // Perform the entered command, when the enter-key is pressed.
+ if rl.is_key_pressed(KeyboardKey::KEY_ENTER) {
+ self.active = false;
+ 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),
+ }
+ }
+ }
+
+ /// 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/colours.rs b/src/colours.rs
index 4a3b799..d7c728c 100644
--- a/src/colours.rs
+++ b/src/colours.rs
@@ -32,9 +32,17 @@ pub struct Colours {
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 {
@@ -115,12 +123,36 @@ impl Colours {
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/editor.rs b/src/editor.rs
index d541fb6..e652ddc 100644
--- a/src/editor.rs
+++ b/src/editor.rs
@@ -121,20 +121,6 @@ impl Editor {
}
}
- /*
- TODO: reintroduce saving and loading
- // Handle saving and loading the editor contents to the swap file
- if rl.is_key_pressed(KeyboardKey::KEY_S) {
- self.map_data
- .write_file("swap.ron")
- .expect("Unable to write buffer file");
- } else if rl.is_key_pressed(KeyboardKey::KEY_L) {
- self.map_data
- .load_file("swap.ron")
- .expect("Unable to read buffer file");
- }
- */
-
let mouse_pos_m = transform.point_px_to_m(&rl.get_mouse_position().into());
let snapped_mouse_pos = snap_to_grid(mouse_pos_m, SNAP_SIZE);
diff --git a/src/gui/mod.rs b/src/gui/mod.rs
index a94122e..f8630d7 100644
--- a/src/gui/mod.rs
+++ b/src/gui/mod.rs
@@ -8,7 +8,9 @@
//! called from any point in the program except the main loop, where the user input is polled.
pub mod dimension_indicator;
+pub mod position_indicator;
pub mod tool_sidebar;
pub use self::dimension_indicator::*;
+pub use self::position_indicator::*;
pub use self::tool_sidebar::*;
diff --git a/src/gui/position_indicator.rs b/src/gui/position_indicator.rs
new file mode 100644
index 0000000..b6d0dac
--- /dev/null
+++ b/src/gui/position_indicator.rs
@@ -0,0 +1,31 @@
+//! 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::colours::DEFAULT_COLOURS;
+use crate::math::Vec2;
+use crate::transform::Transform;
+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,
+) {
+ let mouse_pos_m = transform.point_px_to_m(&mouse_pos_px);
+
+ rld.draw_circle_v(mouse_pos_px, 2., DEFAULT_COLOURS.position_indicator);
+ rld.draw_text(
+ &format!("({:.3}m, {:.3}m)", mouse_pos_m.x, mouse_pos_m.y),
+ mouse_pos_px.x as i32 - 30,
+ mouse_pos_px.y as i32 - 30,
+ 20,
+ DEFAULT_COLOURS.position_text,
+ );
+}
diff --git a/src/gui/tool_sidebar.rs b/src/gui/tool_sidebar.rs
index b7618e0..78041e7 100644
--- a/src/gui/tool_sidebar.rs
+++ b/src/gui/tool_sidebar.rs
@@ -30,6 +30,9 @@ impl ToolSidebar {
}
fn panel_rect(screen_height: u16) -> Rect<f32> {
+ /* 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 as f32)
}
diff --git a/src/main.rs b/src/main.rs
index 16afe1f..9a08586 100644
--- a/src/main.rs
+++ b/src/main.rs
@@ -22,6 +22,7 @@
extern crate log;
pub mod button;
+pub mod cli;
pub mod colours;
pub mod config;
pub mod editor;
@@ -34,6 +35,7 @@ pub mod tool;
pub mod transform;
pub mod transformable;
+use cli::CLI;
use config::Config;
use editor::Editor;
use float_cmp::F64Margin;
@@ -95,6 +97,7 @@ fn main() {
let mut editor = Editor::new(&mut rl, &thread, config);
let mut dimension_indicator = DimensionIndicator::new();
let tool_sidebar = ToolSidebar::new(&mut rl, &thread);
+ let mut cli = CLI::new();
let mut transform = Transform::new();
let mut last_mouse_pos = rl.get_mouse_position();
@@ -119,8 +122,8 @@ fn main() {
);
}
+ cli.update(&mut rl, &mut editor);
dimension_indicator.update(editor.map_mut(), &mut rl);
-
editor.update(
&mut rl,
&transform,
@@ -135,9 +138,10 @@ fn main() {
editor.map().draw(&mut d, &transform);
editor.draw_tools(&mut d, &transform);
- tool_sidebar.draw(screen_height as u16, &mut d, &mut editor);
-
+ gui::position_indicator_draw(&mut d, last_mouse_pos.into(), &transform);
dimension_indicator.draw(&mut d, &transform);
+ tool_sidebar.draw(screen_height as u16, &mut d, &mut editor);
+ cli.draw(&mut d);
}
}
}
diff --git a/src/map/data.rs b/src/map/data.rs
index 1031d3c..f7ec484 100644
--- a/src/map/data.rs
+++ b/src/map/data.rs
@@ -1,6 +1,6 @@
//! Module containing the raw map data version of the map.
-use super::{IconData, PolygonRoomData, RectRoomData, WallData};
+use super::{IconData, Map, PolygonRoomData, RectRoomData, WallData};
use ron::de::from_reader;
use ron::ser::{to_string_pretty, PrettyConfig};
use serde::{Deserialize, Serialize};
@@ -35,8 +35,37 @@ impl MapData {
}
}
+ /// Creates a data struct from the Map. It is important to note, that this data element is not
+ /// bound to the Map in any way after this, so changing anything won't change anything in the map.
+ /// It is useful however to for instance serialize this map without extra rendering information
+ /// included.
+ pub fn extract_data(map: &Map) -> Self {
+ Self {
+ rect_rooms: map
+ .rect_rooms()
+ .iter()
+ .map(|r| (r as &RectRoomData).clone())
+ .collect(),
+ polygon_rooms: map
+ .polygon_rooms()
+ .iter()
+ .map(|p| (p as &PolygonRoomData).clone())
+ .collect(),
+ walls: map
+ .walls()
+ .iter()
+ .map(|w| (w as &WallData).clone())
+ .collect(),
+ icons: map
+ .icons()
+ .iter()
+ .map(|i| (i as &IconData).clone())
+ .collect(),
+ }
+ }
+
/// Load the map data from a file. Fails if the file does not exist or cannot be correctly parsed.
- pub fn load_from_file<P: AsRef<Path>>(&mut self, path: P) -> io::Result<Self> {
+ pub fn load_from_file<P: AsRef<Path>>(path: P) -> io::Result<Self> {
let file = File::open(&path)?;
let data: Self = match from_reader(file) {
Ok(data) => data,
diff --git a/src/map/mod.rs b/src/map/mod.rs
index 88a7e6c..70f65b3 100644
--- a/src/map/mod.rs
+++ b/src/map/mod.rs
@@ -146,4 +146,53 @@ impl Map {
.chain(self.walls.iter_mut().map(|w| w as &mut dyn Mappable))
.chain(self.icons.iter_mut().map(|i| i as &mut dyn Mappable))
}
+
+ /// Get the rectangular rooms of this map.
+ pub fn rect_rooms(&self) -> &Vec<RectRoom> {
+ &self.rect_rooms
+ }
+
+ /// Get the polygon rooms of this map.
+ pub fn polygon_rooms(&self) -> &Vec<PolygonRoom> {
+ &self.polygon_rooms
+ }
+
+ /// Get the walls of this map.
+ pub fn walls(&self) -> &Vec<Wall> {
+ &self.walls
+ }
+
+ /// Get the icons of this map.
+ pub fn icons(&self) -> &Vec<Icon> {
+ &self.icons
+ }
+
+ /// Replace the internal map data with the data provided. (Load and replace)
+ pub fn set_data(&mut self, data: MapData) {
+ // Remove all data.
+ self.icons.clear();
+ self.polygon_rooms.clear();
+ self.rect_rooms.clear();
+ self.walls.clear();
+
+ // Add all data from the map data.
+ self.add_data(data);
+ }
+
+ /// Add the data provided to the current data on the map. All elements will remain, with the
+ /// additional elements being pushed also.
+ pub fn add_data(&mut self, data: MapData) {
+ for i in data.icons {
+ self.push_icon(Icon::from_data(i, self.icon_renderer.clone()))
+ }
+ for p in data.polygon_rooms {
+ self.push_polygon_room(p);
+ }
+ for r in data.rect_rooms {
+ self.push_rect_room(r);
+ }
+ for w in data.walls {
+ self.push_wall(w);
+ }
+ }
}
diff --git a/src/map/polygon_room.rs b/src/map/polygon_room.rs
index ead4e76..2a29436 100644
--- a/src/map/polygon_room.rs
+++ b/src/map/polygon_room.rs
@@ -9,6 +9,7 @@ use crate::transformable::NonRigidTransformable;
use crate::FLOAT_MARGIN;
use nalgebra::{Matrix3, Point2};
use raylib::drawing::{RaylibDraw, RaylibDrawHandle};
+use std::ops::Deref;
/// Data type for the Polygon room.
pub type PolygonRoomData = Polygon<f64>;
@@ -86,3 +87,11 @@ impl NonRigidTransformable for PolygonRoom {
self.retriangulate();
}
}
+
+impl Deref for PolygonRoom {
+ type Target = PolygonRoomData;
+
+ fn deref(&self) -> &Self::Target {
+ &self.data
+ }
+}