diff options
Diffstat (limited to 'src/gui/dimension_indicator.rs')
| -rw-r--r-- | src/gui/dimension_indicator.rs | 280 |
1 files changed, 225 insertions, 55 deletions
diff --git a/src/gui/dimension_indicator.rs b/src/gui/dimension_indicator.rs index cc12f0b..e8848fe 100644 --- a/src/gui/dimension_indicator.rs +++ b/src/gui/dimension_indicator.rs @@ -1,74 +1,250 @@ -use crate::math::Vec2; +//! 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::colours::DEFAULT_COLOURS; +use crate::map::Map; +use crate::math::{self, Rect, Vec2}; use crate::transform::Transform; +use nalgebra::{Matrix3, Vector2}; use raylib::drawing::RaylibDraw; -use raylib::ffi::Color; +use raylib::ffi::KeyboardKey; +use raylib::RaylibHandle; + +/// A state the [DimensionIndicator] is currently in. This determines the behaviour of it and what +/// inputs it might be waiting for. +enum State { + /// In this state, the indicator is not trying to read any keyboard input, but will instead watch + /// for any changes to the dimensions from a different source, updating its display. + Watching, + /// In this state, the indicator will capture keyboard input and attempt to set the dimensions + /// according to whatever was entered. If the dimensions cannot be set, the indicator will use + /// the last valid dimensions. + Ruling { + dim_x: String, + dim_y: String, + editing_x: bool, + }, +} +/// Used to render the horizontal and vertical dimensions of whatever is selected on the map and, if +/// the user so desires edit them directly by entering values into it. pub struct DimensionIndicator { - /// The lines that are used to draw the Dimension Indicator. For a rectangle for instance these - /// would be two. One for width and one for height. - length_lines: Vec<(Vec2<f64>, Vec2<f64>)>, + /// The [State] the dimension indicator is currently in. + state: State, + /// The last dimensions that were valid. + bounds: Rect<f64>, +} + +impl Default for State { + fn default() -> Self { + Self::Watching + } } impl DimensionIndicator { - #[allow(clippy::new_without_default)] + /// Create a new dimension indicator. While it is possible to have multiple instances, this is + /// not generally recommended, since they will need to be managed carefully or otherwise steal + /// keystrokes from each other. pub fn new() -> Self { Self { - length_lines: Vec::new(), + state: State::default(), + bounds: Rect::new(0., 0., 0., 0.), } } - pub fn from_corner_points(corner_points: &[Vec2<f64>]) -> Self { - let mut this = Self::new(); - this.update_dimensions(corner_points); - - this + /// Update whatever is selected on the map according to the dimension indicator rules and rulers. + pub fn update(&mut self, map: &mut Map, rl: &mut RaylibHandle) { + match &self.state { + &State::Watching => self.update_watching(map, rl), + &State::Ruling { .. } => self.update_ruling(map, rl), + }; } - pub fn clear_dimensions(&mut self) { - self.length_lines.clear(); + fn update_watching(&mut self, map: &Map, rl: &RaylibHandle) { + let mut min: Vec2<f64> = Vec2::default(); + let mut max: Vec2<f64> = Vec2::default(); + + /* Try to find selected items. If no items exist, the dimension indicator is set to its + * default, otherwise it is adjusted to the size of the combined selection. + */ + let mut selection_exists = false; + for e in map.elements() { + if e.selected() { + let element_bounds = e.bounding_rect(); + if selection_exists { + // Adjust the currently detected selection size. + min.x = math::partial_min(min.x, element_bounds.x); + min.y = math::partial_min(min.y, element_bounds.y); + max.x = math::partial_max(max.x, element_bounds.x + element_bounds.w); + max.y = math::partial_max(max.y, element_bounds.y + element_bounds.h); + } else { + // No selection size detected yet. Set now. + min.x = element_bounds.x; + min.y = element_bounds.y; + max.x = element_bounds.x + element_bounds.w; + max.y = element_bounds.y + element_bounds.h; + } + selection_exists = true; + } + } + + // Set the current selection limits, if any. + self.bounds = if selection_exists { + Rect::bounding_rect(min, max) + } else { + Rect::new(0., 0., 0., 0.) + }; + + // Check if the user wants to change into editing mode, which the user can only do if there + // is a selection to begin with. + if selection_exists && rl.is_key_pressed(KeyboardKey::KEY_TAB) { + self.state = State::Ruling { + dim_x: self.bounds.w.to_string(), + dim_y: self.bounds.h.to_string(), + editing_x: true, + }; + } } - /// Update the dimensions by analysing a given set of points and adjusting the internal - /// (measured) dimensions. - pub fn update_dimensions(&mut self, corner_points: &[Vec2<f64>]) { - if corner_points.len() < 2 { - warn!("Cannot discern dimensions when not at least two points are given. The dimensions were not updated."); + fn update_ruling(&mut self, map: &mut Map, rl: &mut RaylibHandle) { + // Get the currently edited dimension for processing. + let (edited_dim, editing_x) = match &mut self.state { + State::Watching => panic!("Called ruler update when in watching state"), + State::Ruling { + dim_x, + dim_y, + editing_x, + } => { + if *editing_x { + (dim_x, editing_x) + } else { + (dim_y, editing_x) + } + } + }; + + // Switch the currently edited dimension when pressing tab. + if rl.is_key_pressed(KeyboardKey::KEY_TAB) { + *editing_x = !*editing_x; + return; + } + // Finish editing mode on enter. + if rl.is_key_pressed(KeyboardKey::KEY_ENTER) { + self.state = State::Watching; return; } - // Discern the bounding box for the given corner points. - let mut min = corner_points[0]; - let mut max = corner_points[0]; - for point in corner_points.iter().skip(1) { - if point.x < min.x { - min.x = point.x; - } - if point.x > max.x { - max.x = point.x; - } + // Marker to see if the dimensions will have to be checked for an update. + let mut dimension_changed = false; + // Delete the last character of the dimension on backspace. + if rl.is_key_pressed(KeyboardKey::KEY_BACKSPACE) { + edited_dim.pop(); + dimension_changed = true; + } + /* Capture the current key and try to add it to the string of the current dimension, + * if possible. + */ + else if let Some(key) = rl.get_key_pressed() { + match key { + // Add a decimal point to the dimension if possible. + KeyboardKey::KEY_PERIOD => { + if !edited_dim.contains('.') { + edited_dim.push('.'); + } + // Nothing changed here, since there is an implicit .0 at the end. + } + // Handle the entered key if it is a number to append it to the currently edited dimension. + _ => { + if key as u16 >= KeyboardKey::KEY_ZERO as u16 + && key as u16 <= KeyboardKey::KEY_NINE as u16 + { + edited_dim.push(key as u8 as char); + dimension_changed = true; + } + } + }; + } + + if dimension_changed { + /* Try to parse the dimension from the currently edited string. If it + * is valid, change the dimensions of the currently selected items. If + * not, ignore the change and wait for a valid dimension. + */ + if let Ok(dim) = edited_dim.parse::<f64>() { + let new_bounds = if *editing_x { + Rect::new(self.bounds.x, self.bounds.y, dim, self.bounds.h) + } else { + Rect::new(self.bounds.x, self.bounds.y, self.bounds.h, dim) + }; - if point.y < min.y { - min.y = point.y; + self.set_bounds(map, new_bounds); } - if point.y > max.y { - max.y = point.y; + } + } + + /// 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, map: &mut Map, 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 element in map.elements_mut() { + if element.selected() { + if let Some(transformable) = element.as_non_rigid_mut() { + transformable.apply_matrix(&transform); + } } } - // For now, only use the width and height vectors. - // TODO: Change to a more sophisticated approach. - self.length_lines.clear(); - // Horizontal dimensions, left to right. - self.length_lines - .push((Vec2::new(min.x, max.y), Vec2::new(max.x, max.y))); - // Vertical dimensions, bottom to top. - self.length_lines - .push((Vec2::new(max.x, max.y), Vec2::new(max.x, min.y))); + self.bounds = bounds; } + /// Draw the dimensions detected on the current selection. pub fn draw(&self, rld: &mut impl RaylibDraw, transform: &Transform) { - // Draw all the dimension lines. - for (start, end) in &self.length_lines { + /* 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; @@ -89,27 +265,21 @@ impl DimensionIndicator { let end_px = transform.point_m_to_px(end) + line_normal * 10.; /* Draw the indicator line, with stubs at both ends. */ - let line_colour = Color { - r: 200, - g: 200, - b: 200, - a: 255, - }; // First the two stubs. rld.draw_line_ex( start_px - line_normal * 5., start_px + line_normal * 5., 2., - line_colour, + DEFAULT_COLOURS.dimension_indicators, ); rld.draw_line_ex( end_px - line_normal * 5., end_px + line_normal * 5., 2., - line_colour, + DEFAULT_COLOURS.dimension_indicators, ); // Then the actual indicator line. - rld.draw_line_ex(start_px, end_px, 2., line_colour); + 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 @@ -117,11 +287,11 @@ impl DimensionIndicator { */ let text_pos = transform.point_m_to_px(&((*end + *start) / 2.)) + line_normal * 20.; rld.draw_text( - &format!("{}m", &(*end - *start).length()), + &format!("{}m", dim_str), text_pos.x as i32, text_pos.y as i32, 20, - line_colour, + DEFAULT_COLOURS.dimension_text, ); } } |
