From d8a22033ca44f690a05c3eb0a8a3cbdb4a7fa3ff Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Tue, 15 Jul 2025 01:01:33 +0530 Subject: [PATCH 1/6] merged with circle and impl inner radius gizmo for arc --- .../document/overlays/utility_types.rs | 84 +++++++++--- .../gizmos/gizmo_manager.rs | 15 ++ .../shape_gizmos/circle_radius_handle.rs | 129 ++++++++++++++++++ .../gizmos/shape_gizmos/mod.rs | 1 + .../graph_modification_utils.rs | 4 + .../common_functionality/shapes/arc_shape.rs | 25 +++- .../shapes/circle_shape.rs | 104 ++++++++++++++ .../tool/common_functionality/shapes/mod.rs | 1 + .../shapes/shape_utility.rs | 25 +++- .../messages/tool/tool_messages/shape_tool.rs | 16 ++- 10 files changed, 376 insertions(+), 28 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_radius_handle.rs create mode 100644 editor/src/messages/tool/common_functionality/shapes/circle_shape.rs diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index d50dfb9d85..b994b34636 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -294,6 +294,73 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + pub fn dashed_circle( + &mut self, + position: DVec2, + radius: f64, + color_fill: Option<&str>, + color_stroke: Option<&str>, + dash_width: Option, + dash_gap_width: Option, + dash_offset: Option, + transform: Option, + ) { + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + let position = position.round(); + + self.start_dpi_aware_transform(); + + if let Some(transform) = transform { + let [a, b, c, d, e, f] = transform.to_cols_array(); + self.render_context.transform(a, b, c, d, e, f).expect("Failed to transform circle"); + } + + if let Some(dash_width) = dash_width { + let dash_gap_width = dash_gap_width.unwrap_or(1.); + let array = js_sys::Array::new(); + array.push(&JsValue::from(dash_width)); + array.push(&JsValue::from(dash_gap_width)); + + if let Some(dash_offset) = dash_offset { + if dash_offset != 0. { + self.render_context.set_line_dash_offset(dash_offset); + } + } + + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + + self.render_context.begin_path(); + self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); + self.render_context.set_stroke_style_str(color_stroke); + + if let Some(fill_color) = color_fill { + self.render_context.set_fill_style_str(fill_color); + self.render_context.fill(); + } + self.render_context.stroke(); + + // Reset the dash pattern back to solid + if dash_width.is_some() { + self.render_context + .set_line_dash(&JsValue::from(js_sys::Array::new())) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + if dash_offset.is_some() && dash_offset != Some(0.) { + self.render_context.set_line_dash_offset(0.); + } + + self.end_dpi_aware_transform(); + } + + pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { + self.dashed_circle(position, radius, color_fill, color_stroke, None, None, None, None); + } + pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { self.start_dpi_aware_transform(); @@ -374,23 +441,6 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { - let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); - let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); - let position = position.round(); - - self.start_dpi_aware_transform(); - - self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); - self.render_context.set_fill_style_str(color_fill); - self.render_context.set_stroke_style_str(color_stroke); - self.render_context.fill(); - self.render_context.stroke(); - - self.end_dpi_aware_transform(); - } - pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) { let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize; let step = (end_at - start_from) / segments as f64; diff --git a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index b22aa998ab..9651d68aea 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -5,6 +5,7 @@ use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageH use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::arc_shape::ArcGizmoHandler; +use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGizmoHandler; use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; @@ -25,6 +26,7 @@ pub enum ShapeGizmoHandlers { Star(StarGizmoHandler), Polygon(PolygonGizmoHandler), Arc(ArcGizmoHandler), + Circle(CircleGizmoHandler), } impl ShapeGizmoHandlers { @@ -35,6 +37,7 @@ impl ShapeGizmoHandlers { Self::Star(_) => "star", Self::Polygon(_) => "polygon", Self::Arc(_) => "arc", + Self::Circle(_) => "circle", Self::None => "none", } } @@ -45,6 +48,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.handle_state(layer, mouse_position, document, responses), Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses), Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses), + Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses), Self::None => {} } } @@ -55,6 +59,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.is_any_gizmo_hovered(), Self::Polygon(h) => h.is_any_gizmo_hovered(), Self::Arc(h) => h.is_any_gizmo_hovered(), + Self::Circle(h) => h.is_any_gizmo_hovered(), Self::None => false, } } @@ -65,6 +70,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.handle_click(), Self::Polygon(h) => h.handle_click(), Self::Arc(h) => h.handle_click(), + Self::Circle(h) => h.handle_click(), Self::None => {} } } @@ -75,6 +81,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.handle_update(drag_start, document, input, responses), Self::Polygon(h) => h.handle_update(drag_start, document, input, responses), Self::Arc(h) => h.handle_update(drag_start, document, input, responses), + Self::Circle(h) => h.handle_update(drag_start, document, input, responses), Self::None => {} } } @@ -85,6 +92,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.cleanup(), Self::Polygon(h) => h.cleanup(), Self::Arc(h) => h.cleanup(), + Self::Circle(h) => h.cleanup(), Self::None => {} } } @@ -103,6 +111,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), + Self::Circle(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -120,6 +129,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), + Self::Circle(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -160,6 +170,11 @@ impl GizmoManager { return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new())); } + // Polygon + if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::default())); + } + None } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_radius_handle.rs new file mode 100644 index 0000000000..7d46f09c46 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_radius_handle.rs @@ -0,0 +1,129 @@ +use crate::consts::GIZMO_HIDE_THRESHOLD; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector}; +use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::prelude::{FrontendMessage, Responses}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_arc_id}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_arc_parameters, extract_circle_radius}; +use glam::DVec2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum RadiusHandleState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub struct RadiusHandle { + pub layer: Option, + initial_radius: f64, + handle_state: RadiusHandleState, + angle: f64, + previous_mouse_position: DVec2, +} + +impl RadiusHandle { + pub fn cleanup(&mut self) { + self.handle_state = RadiusHandleState::Inactive; + self.layer = None; + } + + pub fn hovered(&self) -> bool { + self.handle_state == RadiusHandleState::Hover + } + + pub fn is_dragging_or_snapped(&self) -> bool { + self.handle_state == RadiusHandleState::Dragging + } + + pub fn update_state(&mut self, state: RadiusHandleState) { + self.handle_state = state; + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { + match &self.handle_state { + RadiusHandleState::Inactive => { + let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else { + return; + }; + let viewport = document.metadata().transform_to_viewport(layer); + + let angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X); + + let point_position = viewport.transform_point2(get_circle_point_position(angle, radius.abs())); + let center = viewport.transform_point2(DVec2::ZERO); + + log::info!("reaching here"); + if point_position.distance(center) < GIZMO_HIDE_THRESHOLD { + return; + } + + if mouse_position.distance(center) <= point_position.distance(center) { + self.layer = Some(layer); + self.initial_radius = radius; + self.previous_mouse_position = mouse_position; + self.angle = angle; + self.update_state(RadiusHandleState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + } + } + + RadiusHandleState::Dragging | RadiusHandleState::Hover => {} + } + } + + pub fn overlays(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + match &self.handle_state { + RadiusHandleState::Inactive => {} + + RadiusHandleState::Dragging | RadiusHandleState::Hover => { + let Some(layer) = self.layer else { return }; + let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else { + return; + }; + let viewport = document.metadata().transform_to_viewport(layer); + + overlay_context.dashed_circle(DVec2::ZERO, radius.abs(), None, None, Some(20.), Some(4.), Some(0.5), Some(viewport)); + // overlay_context.dashed_line(center, point_position, None, None, Some(4.), Some(4.), Some(0.5)); + } + } + } + + pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { + let Some(layer) = self.layer else { return }; + + let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface).or(get_arc_id(layer, &document.network_interface)) else { + return; + }; + + let Some(current_radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else { + return; + }; + let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); + let center = viewport_transform.transform_point2(DVec2::ZERO); + + let delta_vector = viewport_transform.inverse().transform_point2(input.mouse.position) - viewport_transform.inverse().transform_point2(self.previous_mouse_position); + let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center; + let sign = radius.dot(delta_vector).signum(); + + let net_delta = delta_vector.length() * sign * self.initial_radius.signum(); + self.previous_mouse_position = input.mouse.position; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::F64(current_radius + net_delta), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } +} + +fn get_circle_point_position(theta: f64, radius: f64) -> DVec2 { + DVec2::new(radius * theta.cos(), -radius * theta.sin()) +} diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs index a5df795c30..1898a89fc3 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs @@ -1,3 +1,4 @@ +pub mod circle_radius_handle; pub mod number_of_points_dial; pub mod point_radius_handle; pub mod sweep_angle_gizmo; diff --git a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 7adbe09d36..3ba8350a3b 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -333,6 +333,10 @@ pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill") } +pub fn get_circle_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Circle") +} + pub fn get_ellipse_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Ellipse") } diff --git a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs index 8f1b347546..f63a8d09a3 100644 --- a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs @@ -4,6 +4,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_radius_handle::{RadiusHandle, RadiusHandleState}; use crate::messages::tool::common_functionality::gizmos::shape_gizmos::sweep_angle_gizmo::{SweepAngleGizmo, SweepAngleGizmoState}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, arc_outline}; @@ -17,6 +18,7 @@ use std::collections::VecDeque; #[derive(Clone, Debug, Default)] pub struct ArcGizmoHandler { sweep_angle_gizmo: SweepAngleGizmo, + arc_radius_handle: RadiusHandle, } impl ArcGizmoHandler { @@ -26,24 +28,33 @@ impl ArcGizmoHandler { } impl ShapeGizmoHandler for ArcGizmoHandler { - fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { - self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position, responses); + fn handle_state(&mut self, selected_shape_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + self.sweep_angle_gizmo.handle_actions(selected_shape_layer, document, mouse_position, responses); + self.arc_radius_handle.handle_actions(selected_shape_layer, document, mouse_position, responses); } fn is_any_gizmo_hovered(&self) -> bool { - self.sweep_angle_gizmo.hovered() + self.sweep_angle_gizmo.hovered() || self.arc_radius_handle.hovered() } fn handle_click(&mut self) { if self.sweep_angle_gizmo.hovered() { self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging); } + + if self.arc_radius_handle.hovered() { + self.arc_radius_handle.update_state(RadiusHandleState::Dragging); + } } - fn handle_update(&mut self, _drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { if self.sweep_angle_gizmo.is_dragging_or_snapped() { self.sweep_angle_gizmo.update_arc(document, input, responses); } + + if self.arc_radius_handle.is_dragging_or_snapped() { + self.arc_radius_handle.update_inner_radius(document, input, responses, drag_start); + } } fn dragging_overlays( @@ -58,6 +69,10 @@ impl ShapeGizmoHandler for ArcGizmoHandler { self.sweep_angle_gizmo.overlays(None, document, input, mouse_position, overlay_context); arc_outline(self.sweep_angle_gizmo.layer, document, overlay_context); } + + if self.arc_radius_handle.is_dragging_or_snapped() { + self.arc_radius_handle.overlays(document, overlay_context); + } } fn overlays( @@ -70,12 +85,14 @@ impl ShapeGizmoHandler for ArcGizmoHandler { overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext, ) { self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context); + self.arc_radius_handle.overlays(document, overlay_context); arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context); } fn cleanup(&mut self) { self.sweep_angle_gizmo.cleanup(); + self.arc_radius_handle.cleanup(); } } #[derive(Default)] diff --git a/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs new file mode 100644 index 0000000000..a69b085e63 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs @@ -0,0 +1,104 @@ +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; + +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_radius_handle::{RadiusHandle, RadiusHandleState}; +use crate::messages::tool::common_functionality::graph_modification_utils; + +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; + +#[derive(Clone, Debug, Default)] +pub struct CircleGizmoHandler { + circle_radius_handle: RadiusHandle, +} + +impl ShapeGizmoHandler for CircleGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + self.circle_radius_handle.hovered() + } + + fn handle_state(&mut self, selected_circle_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + self.circle_radius_handle.handle_actions(selected_circle_layer, document, mouse_position, responses); + } + + fn handle_click(&mut self) { + if self.circle_radius_handle.hovered() { + self.circle_radius_handle.update_state(RadiusHandleState::Dragging); + } + } + + fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + if self.circle_radius_handle.is_dragging_or_snapped() { + self.circle_radius_handle.update_inner_radius(document, input, responses, drag_start); + } + } + + fn overlays( + &self, + document: &DocumentMessageHandler, + _selected_circle_layer: Option, + _input: &InputPreprocessorMessageHandler, + _shape_editor: &mut &mut ShapeState, + _mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + self.circle_radius_handle.overlays(document, overlay_context); + } + + fn dragging_overlays( + &self, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + _shape_editor: &mut &mut ShapeState, + _mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + if self.circle_radius_handle.is_dragging_or_snapped() { + self.circle_radius_handle.overlays(document, overlay_context); + } + } + + fn cleanup(&mut self) { + self.circle_radius_handle.cleanup(); + } +} + +#[derive(Default)] +pub struct Circle; + +impl Circle { + pub fn create_node() -> NodeTemplate { + let node_type = resolve_document_node_type("Circle").expect("Circle can't be found"); + node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.), false))]) + } + + pub fn update_shape(document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, layer: LayerNodeIdentifier, responses: &mut VecDeque) { + let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface) else { + return; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + let radius = ipp.mouse.position.distance(center); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::F64(radius), false), + }); + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_translation(center), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs index 812b22c513..a994ac52d1 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -1,4 +1,5 @@ pub mod arc_shape; +pub mod circle_shape; pub mod ellipse_shape; pub mod line_shape; pub mod polygon_shape; diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index b274d5de54..063b19b759 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -24,11 +24,12 @@ use std::f64::consts::{PI, TAU}; pub enum ShapeType { #[default] Polygon = 0, - Star, - Arc, - Rectangle, - Ellipse, - Line, + Star = 1, + Circle = 2, + Arc = 3, + Rectangle = 4, + Ellipse = 5, + Line = 6, } impl ShapeType { @@ -40,6 +41,7 @@ impl ShapeType { Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", + Self::Circle => "Circle", }) .into() } @@ -277,6 +279,19 @@ pub fn arc_end_points_ignore_layer(radius: f64, start_angle: f64, sweep_angle: f } /// Calculate the viewport position of a star vertex given its index +/// Extract the node input values of Circle. +/// Returns an option of (radius). +pub fn extract_circle_radius(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option { + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Circle")?; + + let Some(&TaggedValue::F64(radius)) = node_inputs.get(1)?.as_value() else { + return None; + }; + + Some(radius) +} + +/// Calculate the viewport position of as a star vertex given its index pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 { let angle = ((vertex_index as f64) * PI) / (n as f64); let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 }; diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 4c4ab4e4a1..e7926b4300 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -11,6 +11,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::shapes::arc_shape::Arc; +use crate::messages::tool::common_functionality::shapes::circle_shape::Circle; use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints}; use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays}; @@ -110,6 +111,9 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { MenuListEntry::new("Star") .label("Star") .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), + MenuListEntry::new("Circle") + .label("Circle") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Circle)).into()), MenuListEntry::new("Arc") .label("Arc") .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()), @@ -446,6 +450,9 @@ impl Fsm for ShapeToolFsmState { if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) { Line::overlays(document, tool_data, &mut overlay_context); + if matches!(tool_options.shape_type, ShapeType::Circle) { + tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context); + } } self @@ -602,7 +609,7 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Arc | ShapeType::Rectangle => tool_data.data.start(document, input), + ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Circle => tool_data.data.start(document, input), ShapeType::Line => { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); @@ -619,6 +626,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), ShapeType::Line => Line::create_node(document, tool_data.data.drag_start), + ShapeType::Circle => Circle::create_node(), }; let nodes = vec![(NodeId(0), node)]; @@ -627,7 +635,7 @@ impl Fsm for ShapeToolFsmState { let defered_responses = &mut VecDeque::new(); match tool_data.current_shape { - ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star => { + ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star | ShapeType::Circle => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -664,6 +672,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Circle => Circle::update_shape(document, input, layer, responses), ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses), } @@ -844,6 +853,7 @@ impl Fsm for ShapeToolFsmState { tool_data.data.cleanup(responses); tool_data.current_shape = shape; + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(shape))); ShapeToolFsmState::Ready(shape) } (_, ShapeToolMessage::HideShapeTypeWidget(hide)) => { @@ -883,6 +893,7 @@ impl Fsm for ShapeToolFsmState { HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), HintInfo::keys([Key::Alt], "From Center").prepend_plus(), ])], + ShapeType::Circle => vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle")])], }; HintData(hint_groups) } @@ -897,6 +908,7 @@ impl Fsm for ShapeToolFsmState { HintInfo::keys([Key::Alt], "From Center"), HintInfo::keys([Key::Control], "Lock Angle"), ]), + ShapeType::Circle => HintGroup(vec![]), }; common_hint_group.push(tool_hint_group); From 1beb0be158f8ccea979b426d809ad7c65ad06975 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Wed, 16 Jul 2025 13:44:33 +0530 Subject: [PATCH 2/6] impl radius-gizmo for arc --- .../document/overlays/utility_types.rs | 74 +++++++++++++++++++ ..._handle.rs => circle_arc_radius_handle.rs} | 56 +++++++++++--- .../gizmos/shape_gizmos/mod.rs | 2 +- .../common_functionality/shapes/arc_shape.rs | 2 +- .../shapes/circle_shape.rs | 2 +- 5 files changed, 123 insertions(+), 13 deletions(-) rename editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/{circle_radius_handle.rs => circle_arc_radius_handle.rs} (65%) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index b994b34636..9fc9cb1e7a 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -294,6 +294,80 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + #[allow(clippy::too_many_arguments)] + pub fn dashed_ellipse( + &mut self, + center: DVec2, + radius_x: f64, + radius_y: f64, + rotation: Option, + start_angle: Option, + end_angle: Option, + counterclockwise: Option, + color_fill: Option<&str>, + color_stroke: Option<&str>, + dash_width: Option, + dash_gap_width: Option, + dash_offset: Option, + ) { + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + let center = center.round(); + + self.start_dpi_aware_transform(); + + if let Some(dash_width) = dash_width { + let dash_gap_width = dash_gap_width.unwrap_or(1.); + let array = js_sys::Array::new(); + array.push(&JsValue::from(dash_width)); + array.push(&JsValue::from(dash_gap_width)); + + if let Some(dash_offset) = dash_offset { + if dash_offset != 0. { + self.render_context.set_line_dash_offset(dash_offset); + } + } + + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + + self.render_context.begin_path(); + self.render_context + .ellipse_with_anticlockwise( + center.x, + center.y, + radius_x, + radius_y, + rotation.unwrap_or_default(), + start_angle.unwrap_or_default(), + end_angle.unwrap_or(TAU), + counterclockwise.unwrap_or_default(), + ) + .expect("Failed to draw ellipse"); + self.render_context.set_stroke_style_str(color_stroke); + + if let Some(fill_color) = color_fill { + self.render_context.set_fill_style_str(fill_color); + self.render_context.fill(); + } + self.render_context.stroke(); + + // Reset the dash pattern back to solid + if dash_width.is_some() { + self.render_context + .set_line_dash(&JsValue::from(js_sys::Array::new())) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + if dash_offset.is_some() && dash_offset != Some(0.) { + self.render_context.set_line_dash_offset(0.); + } + + self.end_dpi_aware_transform(); + } + pub fn dashed_circle( &mut self, position: DVec2, diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs similarity index 65% rename from editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_radius_handle.rs rename to editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs index 7d46f09c46..ea71fb79f9 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs @@ -5,12 +5,13 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector}; use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; use crate::messages::prelude::{FrontendMessage, Responses}; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_arc_id}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_arc_id, get_stroke_width}; use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_arc_parameters, extract_circle_radius}; -use glam::DVec2; +use glam::{DAffine2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use std::collections::VecDeque; +use std::f64::consts::FRAC_PI_2; #[derive(Clone, Debug, Default, PartialEq)] pub enum RadiusHandleState { @@ -47,6 +48,18 @@ impl RadiusHandle { self.handle_state = state; } + pub fn check_if_inside_dash_lines(angle: f64, mouse_position: DVec2, viewport: DAffine2, radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool { + let center = viewport.transform_point2(DVec2::ZERO); + if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) { + let layer_mouse = viewport.inverse().transform_point2(mouse_position); + let spacing = 3. * stroke_width; + layer_mouse.distance(DVec2::ZERO) >= (radius - spacing) && layer_mouse.distance(DVec2::ZERO) <= (radius + spacing) + } else { + let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs())); + mouse_position.distance(center) <= point_position.distance(center) + } + } + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { match &self.handle_state { RadiusHandleState::Inactive => { @@ -54,18 +67,15 @@ impl RadiusHandle { return; }; let viewport = document.metadata().transform_to_viewport(layer); - let angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X); - - let point_position = viewport.transform_point2(get_circle_point_position(angle, radius.abs())); + let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs())); let center = viewport.transform_point2(DVec2::ZERO); - log::info!("reaching here"); if point_position.distance(center) < GIZMO_HIDE_THRESHOLD { return; } - if mouse_position.distance(center) <= point_position.distance(center) { + if Self::check_if_inside_dash_lines(angle, mouse_position, viewport, radius.abs(), document, layer) { self.layer = Some(layer); self.initial_radius = radius; self.previous_mouse_position = mouse_position; @@ -89,9 +99,35 @@ impl RadiusHandle { return; }; let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + let start_point = viewport.transform_point2(calculate_circle_point_position(0., radius)).distance(center); + let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(center); + + if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) { + let threshold = 15.0; + let min_radius = start_point.min(end_point); + + let extra_spacing = if min_radius < threshold { + 10.0 * (min_radius / threshold) // smoothly scales from 0 → 10 + } else { + 10.0 + }; + + let spacing = stroke_width + extra_spacing; + let smaller_radius_x = (start_point - spacing).abs(); + let smaller_radius_y = (end_point - spacing).abs(); + + let larger_radius_x = (start_point + spacing).abs(); + let larger_radius_y = (end_point + spacing).abs(); + + overlay_context.dashed_ellipse(center, smaller_radius_x, smaller_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5)); + overlay_context.dashed_ellipse(center, larger_radius_x, larger_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5)); + + return; + } - overlay_context.dashed_circle(DVec2::ZERO, radius.abs(), None, None, Some(20.), Some(4.), Some(0.5), Some(viewport)); - // overlay_context.dashed_line(center, point_position, None, None, Some(4.), Some(4.), Some(0.5)); + overlay_context.dashed_ellipse(center, start_point, end_point, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5)); } } } @@ -124,6 +160,6 @@ impl RadiusHandle { } } -fn get_circle_point_position(theta: f64, radius: f64) -> DVec2 { +fn calculate_circle_point_position(theta: f64, radius: f64) -> DVec2 { DVec2::new(radius * theta.cos(), -radius * theta.sin()) } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs index 1898a89fc3..710584f471 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs @@ -1,4 +1,4 @@ -pub mod circle_radius_handle; +pub mod circle_arc_radius_handle; pub mod number_of_points_dial; pub mod point_radius_handle; pub mod sweep_angle_gizmo; diff --git a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs index f63a8d09a3..b51e69eb4a 100644 --- a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs @@ -4,7 +4,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; -use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_radius_handle::{RadiusHandle, RadiusHandleState}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState}; use crate::messages::tool::common_functionality::gizmos::shape_gizmos::sweep_angle_gizmo::{SweepAngleGizmo, SweepAngleGizmoState}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, arc_outline}; diff --git a/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs index a69b085e63..0789e9d99b 100644 --- a/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs @@ -4,7 +4,7 @@ use crate::messages::portfolio::document::overlays::utility_types::OverlayContex use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; -use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_radius_handle::{RadiusHandle, RadiusHandleState}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; From 1d61515798f897f00388207d58a6c508a4ea1309 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Wed, 16 Jul 2025 14:00:44 +0530 Subject: [PATCH 3/6] fix only one gizmo shown at a time --- .../tool/common_functionality/shapes/arc_shape.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs index b51e69eb4a..0d07cdb50e 100644 --- a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs @@ -38,6 +38,11 @@ impl ShapeGizmoHandler for ArcGizmoHandler { } fn handle_click(&mut self) { + if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() { + self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging); + self.arc_radius_handle.update_state(RadiusHandleState::Inactive); + return; + } if self.sweep_angle_gizmo.hovered() { self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging); } @@ -84,6 +89,10 @@ impl ShapeGizmoHandler for ArcGizmoHandler { mouse_position: DVec2, overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext, ) { + if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() { + self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context); + return; + } self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context); self.arc_radius_handle.overlays(document, overlay_context); From 5696f474e4fc7ffcc4297f7b6b36c620a0dcfd7e Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Thu, 31 Jul 2025 16:34:56 -0700 Subject: [PATCH 4/6] Code review --- .../gizmos/gizmo_manager.rs | 3 +-- .../shape_gizmos/circle_arc_radius_handle.rs | 19 ++++++++----------- .../common_functionality/shapes/arc_shape.rs | 1 + .../shapes/circle_shape.rs | 2 -- .../shapes/shape_utility.rs | 14 +++++++------- .../messages/tool/tool_messages/shape_tool.rs | 18 ++++++++++-------- 6 files changed, 27 insertions(+), 30 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index 8c77719b00..57cbb4ed33 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -180,8 +180,7 @@ impl GizmoManager { if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new())); } - - // Polygon + // Circle if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::default())); } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs index 96e43b6828..80279b6911 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs @@ -1,8 +1,9 @@ use crate::consts::GIZMO_HIDE_THRESHOLD; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::message::Message; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector}; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; use crate::messages::prelude::{FrontendMessage, Responses}; use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_arc_id, get_stroke_width}; @@ -80,11 +81,12 @@ impl RadiusHandle { self.initial_radius = radius; self.previous_mouse_position = mouse_position; self.angle = angle; + self.update_state(RadiusHandleState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); } } - RadiusHandleState::Dragging | RadiusHandleState::Hover => {} } } @@ -92,7 +94,6 @@ impl RadiusHandle { pub fn overlays(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { match &self.handle_state { RadiusHandleState::Inactive => {} - RadiusHandleState::Dragging | RadiusHandleState::Hover => { let Some(layer) = self.layer else { return }; let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else { @@ -105,14 +106,11 @@ impl RadiusHandle { let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(center); if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) { - let threshold = 15.0; + let threshold = 15.; let min_radius = start_point.min(end_point); - let extra_spacing = if min_radius < threshold { - 10.0 * (min_radius / threshold) // smoothly scales from 0 → 10 - } else { - 10.0 - }; + // Smoothly scales from 0 → 10 as the radius approaches the threshold + let extra_spacing = if min_radius < threshold { 10. * (min_radius / threshold) } else { 10. }; let spacing = stroke_width + extra_spacing; let smaller_radius_x = (start_point - spacing).abs(); @@ -134,14 +132,13 @@ impl RadiusHandle { pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { let Some(layer) = self.layer else { return }; - let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface).or(get_arc_id(layer, &document.network_interface)) else { return; }; - let Some(current_radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else { return; }; + let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); let center = viewport_transform.transform_point2(DVec2::ZERO); diff --git a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs index 640606ecd9..7d8b49cf8a 100644 --- a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs @@ -43,6 +43,7 @@ impl ShapeGizmoHandler for ArcGizmoHandler { self.arc_radius_handle.update_state(RadiusHandleState::Inactive); return; } + if self.sweep_angle_gizmo.hovered() { self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging); } diff --git a/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs index 9e767eda6b..0686a362a3 100644 --- a/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs @@ -3,10 +3,8 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions: use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; - use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState}; use crate::messages::tool::common_functionality::graph_modification_utils; - use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::tool_messages::tool_prelude::*; diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index ebb6625294..53cafc6073 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -25,12 +25,12 @@ use std::f64::consts::{PI, TAU}; pub enum ShapeType { #[default] Polygon = 0, - Star = 1, - Circle = 2, - Arc = 3, - Rectangle = 4, - Ellipse = 5, - Line = 6, + Star, + Circle, + Arc, + Rectangle, + Ellipse, + Line, } impl ShapeType { @@ -38,11 +38,11 @@ impl ShapeType { (match self { Self::Polygon => "Polygon", Self::Star => "Star", + Self::Circle => "Circle", Self::Arc => "Arc", Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", - Self::Circle => "Circle", }) .into() } diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index c4f5ef8b0e..531999047e 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -476,7 +476,7 @@ impl Fsm for ShapeToolFsmState { if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) { Line::overlays(document, tool_data, &mut overlay_context); - if matches!(tool_options.shape_type, ShapeType::Circle) { + if tool_options.shape_type == ShapeType::Circle { tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context); } } @@ -657,7 +657,7 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Circle => tool_data.data.start(document, input), + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => tool_data.data.start(document, input), ShapeType::Line => { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); @@ -670,11 +670,11 @@ impl Fsm for ShapeToolFsmState { let node = match tool_data.current_shape { ShapeType::Polygon => Polygon::create_node(tool_options.vertices), ShapeType::Star => Star::create_node(tool_options.vertices), + ShapeType::Circle => Circle::create_node(), ShapeType::Arc => Arc::create_node(tool_options.arc_type), ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), ShapeType::Line => Line::create_node(document, tool_data.data.drag_start), - ShapeType::Circle => Circle::create_node(), }; let nodes = vec![(NodeId(0), node)]; @@ -683,7 +683,7 @@ impl Fsm for ShapeToolFsmState { let defered_responses = &mut VecDeque::new(); match tool_data.current_shape { - ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star | ShapeType::Circle => { + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -715,13 +715,13 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses), - ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses), - ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Circle => Circle::update_shape(document, input, layer, responses), ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), } // Auto-panning @@ -959,7 +959,9 @@ impl Fsm for ShapeToolFsmState { ShapeType::Circle => HintGroup(vec![]), }; - common_hint_group.push(tool_hint_group); + if !tool_hint_group.0.is_empty() { + common_hint_group.push(tool_hint_group); + } if matches!(shape, ShapeType::Polygon | ShapeType::Star) { common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")])); From 8227d42562999eebce6288e00b43cbe98513f552 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Fri, 1 Aug 2025 13:38:03 +0530 Subject: [PATCH 5/6] make hints update when changing shape,add default behaviour when dragging to make circle earlier fixed to from center --- .../tool/common_functionality/resize.rs | 108 +++++++---- .../common_functionality/shapes/arc_shape.rs | 2 - .../shapes/circle_shape.rs | 27 ++- .../messages/tool/tool_messages/shape_tool.rs | 169 ++++++++++-------- 4 files changed, 180 insertions(+), 126 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/resize.rs b/editor/src/messages/tool/common_functionality/resize.rs index fc8f1ee6da..47143503c0 100644 --- a/editor/src/messages/tool/common_functionality/resize.rs +++ b/editor/src/messages/tool/common_functionality/resize.rs @@ -48,71 +48,101 @@ impl Resize { /// Compute the drag start and end based on the current mouse position. Ignores the state of the layer. /// If you want to only draw whilst a layer exists, use [`Resize::calculate_points`]. pub fn calculate_points_ignore_layer(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, in_document: bool) -> [DVec2; 2] { + let ratio = input.keyboard.get(lock_ratio as usize); + let center = input.keyboard.get(center as usize); + + // Use shared snapping logic with optional center and ratio constraints, considering if coordinates are in document space. + self.compute_snapped_resize_points(document, input, center, ratio, in_document) + } + + pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option { + let points_viewport = self.calculate_points(document, input, center, lock_ratio)?; + Some( + GraphOperationMessage::TransformSet { + layer: self.layer?, + transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]), + transform_in: TransformIn::Viewport, + skip_rerender, + } + .into(), + ) + } + + pub fn calculate_circle_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key) -> [DVec2; 2] { + let center = input.keyboard.get(center as usize); + + // Use shared snapping logic with enforced aspect ratio and optional center snapping. + self.compute_snapped_resize_points(document, input, center, true, false) + } + + /// Calculates two points in viewport space from a drag, applying snapping, optional center mode, and aspect ratio locking. + fn compute_snapped_resize_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: bool, lock_ratio: bool, in_document: bool) -> [DVec2; 2] { let start = self.viewport_drag_start(document); let mouse = input.mouse.position; let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz); - let document_mouse = document_to_viewport.inverse().transform_point2(mouse); + let drag_start = self.drag_start; let mut points_viewport = [start, mouse]; + let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] }; - let ratio = input.keyboard.get(lock_ratio as usize); - let center = input.keyboard.get(center as usize); - let snap_data = SnapData::ignore(document, input, &ignore); - let config = SnapTypeConfiguration::default(); - if ratio { + let snap_data = &SnapData::ignore(document, input, &ignore); + + if lock_ratio { let viewport_size = points_viewport[1] - points_viewport[0]; - let raw_size = if in_document { document_to_viewport.inverse() } else { DAffine2::IDENTITY }.transform_vector2(viewport_size); + let raw_size = if in_document { + document_to_viewport.inverse().transform_vector2(viewport_size) + } else { + viewport_size + }; + let adjusted_size = raw_size.abs().max(raw_size.abs().yx()) * raw_size.signum(); let size = if in_document { document_to_viewport.transform_vector2(adjusted_size) } else { adjusted_size }; - points_viewport[1] = points_viewport[0] + size; + points_viewport[1] = points_viewport[0] + size; let end_document = document_to_viewport.inverse().transform_point2(points_viewport[1]); let constraint = SnapConstraint::Line { - origin: self.drag_start, - direction: end_document - self.drag_start, + origin: drag_start, + direction: end_document - drag_start, }; + if center { - let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config); - let far = SnapCandidatePoint::handle(2. * self.drag_start - end_document); - let snapped_far = self.snap_manager.constrained_snap(&snap_data, &far, constraint, config); + let snapped = self + .snap_manager + .constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default()); + let far = SnapCandidatePoint::handle(2. * drag_start - end_document); + let snapped_far = self.snap_manager.constrained_snap(snap_data, &far, constraint, SnapTypeConfiguration::default()); let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; + points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document); - points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document); + points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document); self.snap_manager.update_indicator(best); } else { - let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config); + let snapped = self + .snap_manager + .constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default()); points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document); self.snap_manager.update_indicator(snapped); } - } else if center { - let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config); - let opposite = 2. * self.drag_start - document_mouse; - let snapped_far = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(opposite), config); - let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; - points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document); - points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document); - self.snap_manager.update_indicator(best); } else { - let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config); - points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document); - self.snap_manager.update_indicator(snapped); + let document_mouse = document_to_viewport.inverse().transform_point2(mouse); + if center { + let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default()); + let opposite = 2. * drag_start - document_mouse; + let snapped_far = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(opposite), SnapTypeConfiguration::default()); + let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far }; + + points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document); + points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document); + self.snap_manager.update_indicator(best); + } else { + let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default()); + points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document); + self.snap_manager.update_indicator(snapped); + } } points_viewport } - pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option { - let points_viewport = self.calculate_points(document, input, center, lock_ratio)?; - Some( - GraphOperationMessage::TransformSet { - layer: self.layer?, - transform: DAffine2::from_scale_angle_translation(points_viewport[1] - points_viewport[0], 0., points_viewport[0]), - transform_in: TransformIn::Viewport, - skip_rerender, - } - .into(), - ) - } - pub fn cleanup(&mut self, responses: &mut VecDeque) { self.snap_manager.cleanup(responses); self.layer = None; diff --git a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs index 7d8b49cf8a..af9c880019 100644 --- a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs @@ -149,11 +149,9 @@ impl Arc { // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly if dimensions.x > dimensions.y { scale.x = dimensions.x / dimensions.y; - scale.y = 1.; radius = dimensions.y / 2.; } else { scale.y = dimensions.y / dimensions.x; - scale.x = 1.; radius = dimensions.x / 2.; } diff --git a/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs index 0686a362a3..1e58f872cc 100644 --- a/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs @@ -6,7 +6,8 @@ use crate::messages::portfolio::document::utility_types::network_interface::{Inp use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; -use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; +use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, ShapeToolModifierKey}; +use crate::messages::tool::tool_messages::shape_tool::ShapeToolData; use crate::messages::tool::tool_messages::tool_prelude::*; use glam::DAffine2; use graph_craft::document::NodeInput; @@ -85,15 +86,29 @@ impl Circle { node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.), false))]) } - pub fn update_shape(document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, layer: LayerNodeIdentifier, responses: &mut VecDeque) { + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let center = modifier[0]; + let [start, end] = shape_tool_data.data.calculate_circle_points(document, ipp, center); let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface) else { return; }; - let viewport = document.metadata().transform_to_viewport(layer); - let center = viewport.transform_point2(DVec2::ZERO); + let dimensions = (start - end).abs(); + let radius: f64; - let radius = ipp.mouse.position.distance(center); + // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly + if dimensions.x > dimensions.y { + radius = dimensions.y / 2.; + } else { + radius = dimensions.x / 2.; + } responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 1), @@ -102,7 +117,7 @@ impl Circle { responses.add(GraphOperationMessage::TransformSet { layer, - transform: DAffine2::from_translation(center), + transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., start.midpoint(end)), transform_in: TransformIn::Viewport, skip_rerender: false, }); diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 531999047e..8086550f16 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -233,7 +233,7 @@ impl<'a> MessageHandler> for Shap } } - self.fsm_state.update_hints(responses); + update_dynamic_hints(&self.fsm_state, responses, &self.tool_data); self.send_layout(responses, LayoutTarget::ToolOptions); } @@ -717,7 +717,7 @@ impl Fsm for ShapeToolFsmState { match tool_data.current_shape { ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses), - ShapeType::Circle => Circle::update_shape(document, input, layer, responses), + ShapeType::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses), @@ -913,89 +913,100 @@ impl Fsm for ShapeToolFsmState { } } - fn update_hints(&self, responses: &mut VecDeque) { - let hint_data = match self { - ShapeToolFsmState::Ready(shape) => { - let hint_groups = match shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => vec![ - HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), - HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ]), - HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]), - ], - ShapeType::Ellipse => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"), - HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])], - ShapeType::Line => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), - HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), - ])], - ShapeType::Rectangle => vec![HintGroup(vec![ - HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"), - HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), - HintInfo::keys([Key::Alt], "From Center").prepend_plus(), - ])], - ShapeType::Circle => vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle")])], - }; - HintData(hint_groups) - } - ShapeToolFsmState::Drawing(shape) => { - let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; - let tool_hint_group = match shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), - ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), - ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), - ShapeType::Line => HintGroup(vec![ - HintInfo::keys([Key::Shift], "15° Increments"), - HintInfo::keys([Key::Alt], "From Center"), - HintInfo::keys([Key::Control], "Lock Angle"), - ]), - ShapeType::Circle => HintGroup(vec![]), - }; - - if !tool_hint_group.0.is_empty() { - common_hint_group.push(tool_hint_group); - } + fn update_hints(&self, _responses: &mut VecDeque) { + // Moved logic to update_dynamic_hints + } - if matches!(shape, ShapeType::Polygon | ShapeType::Star) { - common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")])); - } + fn update_cursor(&self, responses: &mut VecDeque) { + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); + } +} - HintData(common_hint_group) - } - ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![ +fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque, tool_data: &ShapeToolData) { + let hint_data = match state { + ShapeToolFsmState::Ready(_) => { + let hint_groups = match tool_data.current_shape { + ShapeType::Polygon | ShapeType::Star => vec![ + HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), + HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ]), + HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]), + ], + ShapeType::Ellipse => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"), + HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Line => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"), + HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(), + ])], + ShapeType::Rectangle => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"), + HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Circle => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle"), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + ShapeType::Arc => vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arc"), + HintInfo::keys([Key::Shift], "Constrain Arc").prepend_plus(), + HintInfo::keys([Key::Alt], "From Center").prepend_plus(), + ])], + }; + HintData(hint_groups) + } + ShapeToolFsmState::Drawing(shape) => { + let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; + let tool_hint_group = match shape { + ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Line => HintGroup(vec![ HintInfo::keys([Key::Shift], "15° Increments"), HintInfo::keys([Key::Alt], "From Center"), HintInfo::keys([Key::Control], "Lock Angle"), ]), - ]), - ShapeToolFsmState::ResizingBounds => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]), - ]), - ShapeToolFsmState::RotatingBounds => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]), - ]), - ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), - HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]), - ]), - ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), - }; + ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), + }; - responses.add(FrontendMessage::UpdateInputHints { hint_data }); - } + if !tool_hint_group.0.is_empty() { + common_hint_group.push(tool_hint_group); + } - fn update_cursor(&self, responses: &mut VecDeque) { - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); - } + if matches!(shape, ShapeType::Polygon | ShapeType::Star) { + common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")])); + } + + HintData(common_hint_group) + } + ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![ + HintInfo::keys([Key::Shift], "15° Increments"), + HintInfo::keys([Key::Alt], "From Center"), + HintInfo::keys([Key::Control], "Lock Angle"), + ]), + ]), + ShapeToolFsmState::ResizingBounds => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]), + ]), + ShapeToolFsmState::RotatingBounds => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]), + ]), + ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]), + HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]), + ]), + ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]), + }; + responses.add(FrontendMessage::UpdateInputHints { hint_data }); } From bec6544a2b74c247e8a8cd63493e19edcf34e91f Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Sat, 2 Aug 2025 02:37:22 +0530 Subject: [PATCH 6/6] fixed arc-radius hover threshold and show arc-endpoint when hover over arc-radius gizmo --- .../document/overlays/utility_types.rs | 2 +- .../document/overlays/utility_types_vello.rs | 2 +- .../shape_gizmos/circle_arc_radius_handle.rs | 30 ++++++++++++------- .../gizmos/shape_gizmos/sweep_angle_gizmo.rs | 24 +++++++++------ .../common_functionality/shapes/arc_shape.rs | 23 +++++++++++--- 5 files changed, 56 insertions(+), 25 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 206c1aba81..c871921d5d 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -715,7 +715,7 @@ impl OverlayContext { } pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) { - self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED)); + self.manipulator_handle(end_point_position, true, None); self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians()); self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index c819665596..4f3b612273 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -558,7 +558,7 @@ impl OverlayContext { #[allow(clippy::too_many_arguments)] pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) { - self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED)); + self.manipulator_handle(end_point_position, true, None); self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians()); self.text(text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs index 80279b6911..b0af7d208e 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs @@ -52,15 +52,31 @@ impl RadiusHandle { pub fn check_if_inside_dash_lines(angle: f64, mouse_position: DVec2, viewport: DAffine2, radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool { let center = viewport.transform_point2(DVec2::ZERO); if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) { - let layer_mouse = viewport.inverse().transform_point2(mouse_position); - let spacing = 3. * stroke_width; - layer_mouse.distance(DVec2::ZERO) >= (radius - spacing) && layer_mouse.distance(DVec2::ZERO) <= (radius + spacing) + let circle_point = calculate_circle_point_position(angle, radius.abs()); + let direction = circle_point.normalize(); + let mouse_distance = mouse_position.distance(center); + + let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.); + + let inner_point = viewport.transform_point2(circle_point - direction * spacing).distance(center); + let outer_point = viewport.transform_point2(circle_point + direction * spacing).distance(center); + + mouse_distance >= inner_point && mouse_distance <= outer_point } else { let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs())); mouse_position.distance(center) <= point_position.distance(center) } } + fn calculate_extra_spacing(viewport: DAffine2, radius: f64, viewport_center: DVec2, stroke_width: f64, threshold: f64) -> f64 { + let start_point = viewport.transform_point2(calculate_circle_point_position(0., radius)).distance(viewport_center); + let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(viewport_center); + let min_radius = start_point.min(end_point); + let extra_spacing = if min_radius < threshold { 10. * (min_radius / threshold) } else { 10. }; + + stroke_width + extra_spacing + } + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { match &self.handle_state { RadiusHandleState::Inactive => { @@ -106,13 +122,7 @@ impl RadiusHandle { let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(center); if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) { - let threshold = 15.; - let min_radius = start_point.min(end_point); - - // Smoothly scales from 0 → 10 as the radius approaches the threshold - let extra_spacing = if min_radius < threshold { 10. * (min_radius / threshold) } else { 10. }; - - let spacing = stroke_width + extra_spacing; + let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.); let smaller_radius_x = (start_point - spacing).abs(); let smaller_radius_y = (end_point - spacing).abs(); diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs index b0a45addf9..68d69d9eba 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs @@ -1,4 +1,4 @@ -use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD}; +use crate::consts::{ARC_SNAP_THRESHOLD, GIZMO_HIDE_THRESHOLD}; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -104,17 +104,17 @@ impl SweepAngleGizmo { match self.handle_state { SweepAngleGizmoState::Inactive => { - // Draw both endpoint handles if an arc is selected let Some((point1, point2)) = arc_end_points(selected_arc_layer, document) else { return }; - overlay_context.manipulator_handle(point1, false, Some(COLOR_OVERLAY_RED)); - overlay_context.manipulator_handle(point2, false, Some(COLOR_OVERLAY_RED)); + overlay_context.manipulator_handle(point1, false, None); + overlay_context.manipulator_handle(point2, false, None); } SweepAngleGizmoState::Hover => { // Highlight the currently hovered endpoint only let Some((point1, point2)) = arc_end_points(self.layer, document) else { return }; - let point = if self.endpoint == EndpointType::Start { point1 } else { point2 }; - overlay_context.manipulator_handle(point, true, Some(COLOR_OVERLAY_RED)); + let (point, other_point) = if self.endpoint == EndpointType::Start { (point1, point2) } else { (point2, point1) }; + overlay_context.manipulator_handle(point, true, None); + overlay_context.manipulator_handle(other_point, false, None); } SweepAngleGizmoState::Dragging => { // Show snapping guides and angle arc while dragging @@ -123,11 +123,17 @@ impl SweepAngleGizmo { let viewport = document.metadata().transform_to_viewport(layer); // Depending on which endpoint is being dragged, draw guides relative to the static point - let point = if self.endpoint == EndpointType::End { current_end } else { current_start }; + let (point, other_point) = if self.endpoint == EndpointType::End { + (current_end, current_start) + } else { + (current_start, current_end) + }; // Draw the dashed line from center to drag start position overlay_context.dashed_line(self.position_before_rotation, viewport.transform_point2(DVec2::ZERO), None, None, Some(5.), Some(5.), Some(0.5)); + overlay_context.manipulator_handle(other_point, false, None); + // Draw the angle, text and the bold line self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context); } @@ -143,8 +149,8 @@ impl SweepAngleGizmo { self.dragging_snapping_overlays(a, b, tilt_offset, viewport, overlay_context); // Draw lines from endpoints to the arc center - overlay_context.line(start, center, Some(COLOR_OVERLAY_RED), Some(2.)); - overlay_context.line(end, center, Some(COLOR_OVERLAY_RED), Some(2.)); + overlay_context.line(start, center, None, Some(2.)); + overlay_context.line(end, center, None, Some(2.)); // Draw the line from drag start to arc center overlay_context.dashed_line(self.position_before_rotation, center, None, None, Some(5.), Some(5.), Some(0.5)); diff --git a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs index af9c880019..68803598de 100644 --- a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs @@ -38,6 +38,7 @@ impl ShapeGizmoHandler for ArcGizmoHandler { } fn handle_click(&mut self) { + // If hovering over both the gizmos give priority to sweep angle gizmo if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() { self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging); self.arc_radius_handle.update_state(RadiusHandleState::Inactive); @@ -77,6 +78,7 @@ impl ShapeGizmoHandler for ArcGizmoHandler { } if self.arc_radius_handle.is_dragging() { + self.sweep_angle_gizmo.overlays(self.arc_radius_handle.layer, document, input, mouse_position, overlay_context); self.arc_radius_handle.overlays(document, overlay_context); } } @@ -84,20 +86,29 @@ impl ShapeGizmoHandler for ArcGizmoHandler { fn overlays( &self, document: &DocumentMessageHandler, - selected_shape_layers: Option, + selected_shape_layer: Option, input: &InputPreprocessorMessageHandler, _shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState, mouse_position: DVec2, overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext, ) { + // If hovering over both the gizmos give priority to sweep angle gizmo if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() { - self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context); + self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context); return; } - self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context); + + if self.arc_radius_handle.hovered() { + let layer = self.arc_radius_handle.layer; + + self.arc_radius_handle.overlays(document, overlay_context); + self.sweep_angle_gizmo.overlays(layer, document, input, mouse_position, overlay_context); + } + + self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context); self.arc_radius_handle.overlays(document, overlay_context); - arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context); + arc_outline(selected_shape_layer.or(self.sweep_angle_gizmo.layer), document, overlay_context); } fn mouse_cursor_icon(&self) -> Option { @@ -105,6 +116,10 @@ impl ShapeGizmoHandler for ArcGizmoHandler { return Some(MouseCursorIcon::Default); } + if self.arc_radius_handle.hovered() || self.arc_radius_handle.is_dragging() { + return Some(MouseCursorIcon::EWResize); + } + None }