diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index a71a3ca3cc..c871921d5d 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -294,6 +294,147 @@ 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, + 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 +515,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; @@ -591,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 f62adcf926..4f3b612273 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -345,6 +345,23 @@ impl OverlayContext { self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle); } + 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, + ) { + } + 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; @@ -541,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/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index 4f3d9a4e89..57cbb4ed33 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -6,6 +6,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; @@ -26,6 +27,7 @@ pub enum ShapeGizmoHandlers { Star(StarGizmoHandler), Polygon(PolygonGizmoHandler), Arc(ArcGizmoHandler), + Circle(CircleGizmoHandler), } impl ShapeGizmoHandlers { @@ -36,6 +38,7 @@ impl ShapeGizmoHandlers { Self::Star(_) => "star", Self::Polygon(_) => "polygon", Self::Arc(_) => "arc", + Self::Circle(_) => "circle", Self::None => "none", } } @@ -46,6 +49,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 => {} } } @@ -56,6 +60,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, } } @@ -66,6 +71,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 => {} } } @@ -76,6 +82,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 => {} } } @@ -86,6 +93,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 => {} } } @@ -104,6 +112,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 => {} } } @@ -121,6 +130,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 => {} } } @@ -130,6 +140,7 @@ impl ShapeGizmoHandlers { Self::Star(h) => h.mouse_cursor_icon(), Self::Polygon(h) => h.mouse_cursor_icon(), Self::Arc(h) => h.mouse_cursor_icon(), + Self::Circle(h) => h.mouse_cursor_icon(), Self::None => None, } } @@ -169,6 +180,10 @@ impl GizmoManager { if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new())); } + // Circle + 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_arc_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs new file mode 100644 index 0000000000..b0af7d208e --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/circle_arc_radius_handle.rs @@ -0,0 +1,172 @@ +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::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}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_arc_parameters, extract_circle_radius}; +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 { + #[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(&self) -> bool { + self.handle_state == RadiusHandleState::Dragging + } + + pub fn update_state(&mut self, state: RadiusHandleState) { + 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 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 => { + 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(calculate_circle_point_position(angle, radius.abs())); + let center = viewport.transform_point2(DVec2::ZERO); + + if point_position.distance(center) < GIZMO_HIDE_THRESHOLD { + return; + } + + 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; + 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); + 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 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(); + + 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_ellipse(center, start_point, end_point, None, None, None, None, 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 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 a5df795c30..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,3 +1,4 @@ +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/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/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/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 76e3a21e10..68803598de 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_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}; @@ -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,40 @@ 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); + 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); + 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 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); + return; + } + 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() { + self.arc_radius_handle.update_inner_radius(document, input, responses, drag_start); + } } fn dragging_overlays( @@ -58,20 +76,39 @@ 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() { + self.sweep_angle_gizmo.overlays(self.arc_radius_handle.layer, document, input, mouse_position, overlay_context); + self.arc_radius_handle.overlays(document, overlay_context); + } } 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, ) { - self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context); + // 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_layer, document, input, mouse_position, overlay_context); + return; + } + + 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 { @@ -79,11 +116,16 @@ 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 } fn cleanup(&mut self) { self.sweep_angle_gizmo.cleanup(); + self.arc_radius_handle.cleanup(); } } #[derive(Default)] @@ -122,11 +164,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 new file mode 100644 index 0000000000..1e58f872cc --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/circle_shape.rs @@ -0,0 +1,125 @@ +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_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, 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; +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() { + 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() { + self.circle_radius_handle.overlays(document, overlay_context); + } + } + + fn cleanup(&mut self) { + self.circle_radius_handle.cleanup(); + } + + fn mouse_cursor_icon(&self) -> Option { + if self.circle_radius_handle.hovered() || self.circle_radius_handle.is_dragging() { + return Some(MouseCursorIcon::EWResize); + } + + None + } +} + +#[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, + 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 dimensions = (start - end).abs(); + let radius: f64; + + // 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), + input: NodeInput::value(TaggedValue::F64(radius), false), + }); + + responses.add(GraphOperationMessage::TransformSet { + layer, + 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/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 c5a6351cbe..53cafc6073 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -26,6 +26,7 @@ pub enum ShapeType { #[default] Polygon = 0, Star, + Circle, Arc, Rectangle, Ellipse, @@ -37,6 +38,7 @@ impl ShapeType { (match self { Self::Polygon => "Polygon", Self::Star => "Star", + Self::Circle => "Circle", Self::Arc => "Arc", Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", @@ -280,6 +282,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 9fec5cb480..8086550f16 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()), @@ -229,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); } @@ -472,6 +476,9 @@ impl Fsm for ShapeToolFsmState { if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) { Line::overlays(document, tool_data, &mut overlay_context); + if tool_options.shape_type == ShapeType::Circle { + tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context); + } } self @@ -650,7 +657,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::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()); @@ -663,6 +670,7 @@ 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(), @@ -675,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::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), @@ -707,12 +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, 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), + ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), } // Auto-panning @@ -892,6 +901,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)) => { @@ -903,85 +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(), - ])], - }; - 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"), - ]), - }; - - 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 }); }