aboutsummaryrefslogtreecommitdiff
path: root/src/gui/dimension_indicator.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/gui/dimension_indicator.rs')
-rw-r--r--src/gui/dimension_indicator.rs280
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,
);
}
}