diff options
| -rw-r--r-- | src/editor.rs | 12 | ||||
| -rw-r--r-- | src/grid.rs | 14 | ||||
| -rw-r--r-- | src/gui/decimal_num_box.rs | 173 | ||||
| -rw-r--r-- | src/gui/mod.rs | 2 | ||||
| -rw-r--r-- | src/main.rs | 8 | ||||
| -rw-r--r-- | src/math/mod.rs | 14 | ||||
| -rw-r--r-- | src/snapping.rs | 79 |
7 files changed, 284 insertions, 18 deletions
diff --git a/src/editor.rs b/src/editor.rs index d541fb6..8e025dc 100644 --- a/src/editor.rs +++ b/src/editor.rs @@ -7,8 +7,8 @@ use crate::button::{Button, MouseButton}; use crate::config::Config; -use crate::grid::{snap_to_grid, SNAP_SIZE}; use crate::map::Map; +use crate::snapping::Snapper; use crate::tool::*; use crate::transform::Transform; use raylib::core::drawing::RaylibDrawHandle; @@ -106,7 +106,13 @@ impl Editor { /// 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, mouse_blocked: bool) { + pub fn update( + &mut self, + rl: &mut RaylibHandle, + transform: &Transform, + snapper: &Snapper, + mouse_blocked: bool, + ) { // Handle keybindings for tool change for (&tool_type, (_, activation_key)) in self.tools.iter() { if activation_key.is_pressed(rl, false) { @@ -136,7 +142,7 @@ impl Editor { */ 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); + 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; diff --git a/src/grid.rs b/src/grid.rs index d1c4b15..9134a49 100644 --- a/src/grid.rs +++ b/src/grid.rs @@ -1,22 +1,10 @@ //! The grid used to divide the map into evenly sized chunks. use crate::colours::DEFAULT_COLOURS; -use crate::math::{self, Vec2}; +use crate::math; use crate::transform::Transform; use raylib::drawing::RaylibDraw; -/// The internal grid length which will be used to snap things to it. -pub const SNAP_SIZE: f64 = 0.5; - -/// 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_to_grid(mut vec: Vec2<f64>, snap_fraction: f64) -> Vec2<f64> { - vec.x = math::round(vec.x, snap_fraction); - vec.y = math::round(vec.y, snap_fraction); - - vec -} - /// 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 diff --git a/src/gui/decimal_num_box.rs b/src/gui/decimal_num_box.rs new file mode 100644 index 0000000..e9395f7 --- /dev/null +++ b/src/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/gui/mod.rs b/src/gui/mod.rs index f8630d7..62173ec 100644 --- a/src/gui/mod.rs +++ b/src/gui/mod.rs @@ -7,10 +7,12 @@ //! 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/main.rs b/src/main.rs index 1cfc31b..c9ebe48 100644 --- a/src/main.rs +++ b/src/main.rs @@ -29,6 +29,7 @@ pub mod grid; pub mod gui; pub mod map; pub mod math; +pub mod snapping; pub mod svg; pub mod tool; pub mod transform; @@ -38,6 +39,7 @@ use config::Config; use editor::Editor; use gui::{DimensionIndicator, ToolSidebar}; use raylib::prelude::*; +use snapping::Snapper; use std::ffi::CString; use std::io; use transform::Transform; @@ -86,6 +88,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 snapper = Snapper::default(); let mut transform = Transform::new(); let mut last_mouse_pos = rl.get_mouse_position(); @@ -111,10 +114,12 @@ fn main() { } dimension_indicator.update(editor.map_mut(), &mut rl); + snapper.update(&mut rl); editor.update( &mut rl, &transform, + &snapper, ToolSidebar::mouse_captured(screen_height as u16, last_mouse_pos.into()), ); @@ -126,6 +131,9 @@ 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); + snapper.draw(&mut d); + 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); diff --git a/src/math/mod.rs b/src/math/mod.rs index c9c1c6e..4035de2 100644 --- a/src/math/mod.rs +++ b/src/math/mod.rs @@ -14,7 +14,8 @@ pub use self::surface::*; pub use self::triangle::*; pub use self::vec2::*; -use num_traits::Float; +use nalgebra::RealField; +use num_traits::Pow; use std::cmp::Ordering; /// Round a floating point number to the nearest step given by the step argument. For instance, if @@ -22,7 +23,7 @@ use std::cmp::Ordering; /// 0.25 to 0.74999... will be 0.5 and so on. pub fn round<T>(num: T, step: T) -> T where - T: Float, + T: RealField, { // Only positive steps will be accepted. assert!(step > T::zero()); @@ -38,6 +39,15 @@ where } } +/// Like round, but instead of rounding to a certain fraction, rounds to the nth decimal place instead +/// of taking a granularity. +pub fn round_nth_decimal<T>(num: T, decimal_place: u16) -> T +where + T: RealField + Pow<u16, Output = T>, +{ + round(num, nalgebra::convert::<f64, T>(0.1).pow(decimal_place)) +} + /// Works like `std::cmp::max`, however also allows partial comparisons. It is specifically /// designed so functions that should be able to use f32 and f64 work, eventhough these do not /// implement Ord. The downside of this function however is, that its behaviour is undefined when diff --git a/src/snapping.rs b/src/snapping.rs new file mode 100644 index 0000000..325b62e --- /dev/null +++ b/src/snapping.rs @@ -0,0 +1,79 @@ +//! 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::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) { + if !self.grain_gui.active() && rl.is_key_pressed(KeyboardKey::KEY_G) { + self.grain_gui.set_active(true); + } + + if !self.grain_gui.active() { + return; + } + + self.grain_gui.update(rl); + + if 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 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), + } + } +} |
