From 1f278799d6ca6a79ac03e6b6115430a64c2905c6 Mon Sep 17 00:00:00 2001 From: James Lindsay <78500760+0HyperCube@users.noreply.github.com> Date: Sat, 10 Aug 2024 12:56:02 +0100 Subject: [PATCH] Add quick measure overlays with Alt pressed (#1894) * Measure with alt * Code review --------- Co-authored-by: Keavon Chambers --- .../document/overlays/utility_types.rs | 40 ++++++++++++++++--- .../tool/common_functionality/measure.rs | 36 +++++++++++++++++ .../messages/tool/common_functionality/mod.rs | 1 + .../messages/tool/tool_messages/path_tool.rs | 4 ++ .../tool/tool_messages/select_tool.rs | 19 ++++++++- 5 files changed, 93 insertions(+), 7 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/measure.rs diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 56cb0fc6..27fe689c 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -78,7 +78,9 @@ impl OverlayContext { let position = position.round() - DVec2::splat(0.5); self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU).expect("draw circle"); + self.render_context + .arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU) + .expect("Failed to draw the circle"); let fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(fill)); @@ -127,7 +129,7 @@ impl OverlayContext { let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); let position = position.round(); self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("draw circle"); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(color_fill)); self.render_context.set_stroke_style(&wasm_bindgen::JsValue::from_str(color_stroke)); self.render_context.fill(); @@ -139,7 +141,7 @@ impl OverlayContext { // Circle self.render_context.begin_path(); - self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., TAU).expect("draw circle"); + self.render_context.arc(x, y, PIVOT_DIAMETER / 2., 0., TAU).expect("Failed to draw the circle"); self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_YELLOW)); self.render_context.fill(); @@ -247,7 +249,7 @@ impl OverlayContext { pub fn text(&self, text: &str, pos: DVec2, background: &str, padding: f64) { let pos = pos.round(); - let metrics = self.render_context.measure_text(text).expect("measure text"); + let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions"); self.render_context.set_fill_style(&background.into()); self.render_context.fill_rect( pos.x + metrics.actual_bounding_box_left(), @@ -258,6 +260,34 @@ impl OverlayContext { self.render_context.set_fill_style(&"white".into()); self.render_context .fill_text(text, pos.x + padding, pos.y - padding - metrics.font_bounding_box_descent()) - .expect("draw text"); + .expect("Failed to draw the text on the canvas"); + } + + pub fn angle_text(&self, text: &str, pos: DVec2, direction: DVec2, padding: f64, pivot: Pivot) { + self.render_context.translate(pos.x, pos.y).expect("Failed to translate the render context to the specified position"); + + let angle = -direction.angle_to(DVec2::X); + self.render_context.rotate(angle).expect("Failed to rotate the render context to the specified angle"); + + let metrics = self.render_context.measure_text(text).expect("Failed to measure the text dimensions"); + + self.render_context.set_fill_style(&wasm_bindgen::JsValue::from_str(COLOR_OVERLAY_BLUE)); + + let local_position = match pivot { + Pivot::LeftCentreY => DVec2::new(padding, (metrics.actual_bounding_box_ascent() + metrics.actual_bounding_box_descent()) / 2.), + Pivot::TopCentreX => DVec2::new( + -(metrics.actual_bounding_box_right() + metrics.actual_bounding_box_left()) / 2., + padding + metrics.font_bounding_box_ascent(), + ), + }; + self.render_context + .fill_text(text, local_position.x, local_position.y) + .expect("Failed to draw the text at the calculated position"); + self.render_context.reset_transform().expect("Failed to reset the render context transform"); } } + +pub enum Pivot { + LeftCentreY, + TopCentreX, +} diff --git a/editor/src/messages/tool/common_functionality/measure.rs b/editor/src/messages/tool/common_functionality/measure.rs new file mode 100644 index 00000000..edaa080b --- /dev/null +++ b/editor/src/messages/tool/common_functionality/measure.rs @@ -0,0 +1,36 @@ +use crate::messages::portfolio::document::overlays::utility_types::{self, OverlayContext}; +use crate::messages::tool::tool_messages::tool_prelude::*; + +use graphene_std::renderer::Rect; + +pub fn overlay(selected_bounds: Rect, hovered_bounds: Rect, transform: DAffine2, document_to_viewport: DAffine2, overlay_context: &mut OverlayContext) { + let transform_to_document = document_to_viewport.inverse() * transform; + if selected_bounds.intersects(hovered_bounds) { + // TODO: I'm not sure what to do here? + return; + } + + // Always do horizontal then vertical from the selected + let turn_x = selected_bounds.center().x.clamp(hovered_bounds.min().x, hovered_bounds.max().x); + let turn_y = hovered_bounds.center().y.clamp(selected_bounds.min().y, selected_bounds.max().y); + + let selected_x = turn_x.clamp(selected_bounds.min().x, selected_bounds.max().x); + let hovered_y = turn_y.clamp(hovered_bounds.min().y, hovered_bounds.max().y); + + if turn_x != selected_x { + let min_viewport = transform.transform_point2(DVec2::new(turn_x.min(selected_x), turn_y)); + let max_viewport = transform.transform_point2(DVec2::new(turn_x.max(selected_x), turn_y)); + overlay_context.line(min_viewport, max_viewport); + let length = format!("{:.2}", transform_to_document.transform_vector2(DVec2::X * (turn_x - selected_x)).length()); + let direction = -(min_viewport - max_viewport).normalize_or_zero(); + overlay_context.angle_text(&length, (min_viewport + max_viewport) / 2., direction, 5., utility_types::Pivot::TopCentreX); + } + if turn_y != hovered_y { + let min_viewport = transform.transform_point2(DVec2::new(turn_x, turn_y.min(hovered_y))); + let max_viewport = transform.transform_point2(DVec2::new(turn_x, turn_y.max(hovered_y))); + overlay_context.line(min_viewport, max_viewport); + let length = format!("{:.2}", transform_to_document.transform_vector2(DVec2::Y * (turn_y - hovered_y)).length()); + let direction = (min_viewport - max_viewport).normalize_or_zero().perp(); + overlay_context.angle_text(&length, (min_viewport + max_viewport) / 2., direction, 5., utility_types::Pivot::LeftCentreY); + } +} diff --git a/editor/src/messages/tool/common_functionality/mod.rs b/editor/src/messages/tool/common_functionality/mod.rs index 7a67adcf..e3e273fa 100644 --- a/editor/src/messages/tool/common_functionality/mod.rs +++ b/editor/src/messages/tool/common_functionality/mod.rs @@ -1,6 +1,7 @@ pub mod auto_panning; pub mod color_selector; pub mod graph_modification_utils; +pub mod measure; pub mod pivot; pub mod resize; pub mod shape_editor; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 26e4c3a4..2e46710f 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -486,6 +486,7 @@ impl Fsm for PathToolFsmState { // `Self::InsertPoint` case: (Self::InsertPoint, PathToolMessage::MouseDown { .. } | PathToolMessage::Enter { .. }) => { tool_data.double_click_handled = true; + // TODO: Don't use `Key::Shift` directly, instead take it as a variable from the input mappings list like in all other places let shift = input.keyboard.get(Key::Shift as usize); tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { shift }) } @@ -499,8 +500,11 @@ impl Fsm for PathToolFsmState { (Self::InsertPoint, PathToolMessage::GRS { key: propagate }) => { // MAYBE: use `InputMapperMessage::KeyDown(..)` instead match propagate { + // TODO: Don't use `Key::G` directly, instead take it as a variable from the input mappings list like in all other places Key::KeyG => responses.add(TransformLayerMessage::BeginGrab), + // TODO: Don't use `Key::R` directly, instead take it as a variable from the input mappings list like in all other places Key::KeyR => responses.add(TransformLayerMessage::BeginRotate), + // TODO: Don't use `Key::S` directly, instead take it as a variable from the input mappings list like in all other places Key::KeyS => responses.add(TransformLayerMessage::BeginScale), _ => warn!("Unexpected GRS key"), } diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index 7aa65513..9349e14a 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -10,14 +10,15 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::transformation::Selected; -use crate::messages::tool::common_functionality::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapManager}; use crate::messages::tool::common_functionality::transformation_cage::*; +use crate::messages::tool::common_functionality::{auto_panning::AutoPanning, measure}; use graph_craft::document::NodeId; use graphene_core::renderer::Quad; +use graphene_std::renderer::Rect; use graphene_std::vector::misc::BooleanOperation; use std::fmt; @@ -471,13 +472,27 @@ impl Fsm for SelectToolFsmState { // Update the selection box overlay_context.quad(quad); + } // Only highlight layers if the viewport is not being panned (middle mouse button is pressed) - } else if !input.keyboard.get(Key::Mmb as usize) { + // TODO: Don't use `Key::Mmb` directly, instead take it as a variable from the input mappings list like in all other places + else if !input.keyboard.get(Key::Mmb as usize) { // Get the layer the user is hovering over let click = document.click(input); let not_selected_click = click.filter(|&hovered_layer| !document.network_interface.selected_nodes(&[]).unwrap().selected_layers_contains(hovered_layer, document.metadata())); if let Some(layer) = not_selected_click { overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); + + // Measure with Alt held down + // TODO: Don't use `Key::Alt` directly, instead take it as a variable from the input mappings list like in all other places + if input.keyboard.get(Key::Alt as usize) { + let hovered_bounds = document + .metadata() + .bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer)); + + if let [Some(selected_bounds), Some(hovered_bounds)] = [bounds, hovered_bounds].map(|rect| rect.map(Rect::from_box)) { + measure::overlay(selected_bounds, hovered_bounds, transform, document.metadata().document_to_viewport, &mut overlay_context); + } + } } }