//! 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, MouseButton, 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, }, } /// 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, } 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 = Vec2::default(); let mut max: Vec2 = 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) { // Abort the ruling state when doing anything with the mouse. if input.poll_global(&Button::Mouse(MouseButton::Left).into()) || input.poll_global(&Button::Mouse(MouseButton::Right).into()) { self.state = State::Watching; return; } // 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::() { 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) { 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 } }