From 07d104e3f0ea457711ba70b2f339ceade5441996 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Thu, 26 Jun 2025 18:31:03 +0530 Subject: [PATCH 1/3] implement arc gizmo handler --- editor/src/consts.rs | 3 + .../document/overlays/utility_types.rs | 10 +- .../gizmos/gizmo_manager.rs | 13 + .../gizmos/shape_gizmos/mod.rs | 1 + .../shape_gizmos/point_radius_handle.rs | 33 +- .../gizmos/shape_gizmos/sweep_angle_gizmo.rs | 322 ++++++++++++++++++ .../graph_modification_utils.rs | 4 + .../common_functionality/shapes/arc_shape.rs | 139 ++++++++ .../tool/common_functionality/shapes/mod.rs | 1 + .../shapes/shape_utility.rs | 112 +++++- .../messages/tool/tool_messages/shape_tool.rs | 34 +- .../gcore/src/vector/generator_nodes.rs | 13 +- node-graph/gcore/src/vector/misc.rs | 6 +- 13 files changed, 656 insertions(+), 35 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs create mode 100644 editor/src/messages/tool/common_functionality/shapes/arc_shape.rs diff --git a/editor/src/consts.rs b/editor/src/consts.rs index c943efe24b..4e7a6340f7 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -124,6 +124,9 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.; pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9; pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2; pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.; +pub const ARC_SNAP_THRESHOLD: f64 = 5.; +pub const ARC_SWEEP_GIZMO_RADIUS: f64 = 14.; +pub const ARC_SWEEP_GIZMO_TEXT_HEIGHT: f64 = 12.; pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; // SCROLLBARS diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index c1b1649baa..58cd0d16f4 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -1,7 +1,7 @@ use super::utility_functions::overlay_canvas_context; use crate::consts::{ - COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, - COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, + ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, + COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, }; use crate::messages::prelude::Message; use bezier_rs::{Bezier, Subpath}; @@ -550,6 +550,12 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, radius: f64, pivot: DVec2, text: &str, transform: DAffine2) { + self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED)); + self.draw_angle(pivot, radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians()); + self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); + } + /// Used by the Pen and Path tools to outline the path of the shape. pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { self.start_dpi_aware_transform(); 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 703c85e14d..226c0bfd9e 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler}; 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::polygon_shape::PolygonGizmoHandler; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; @@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers { None, Star(StarGizmoHandler), Polygon(PolygonGizmoHandler), + Arc(ArcGizmoHandler), } impl ShapeGizmoHandlers { @@ -32,6 +34,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(_) => "star", Self::Polygon(_) => "polygon", + Self::Arc(_) => "arc", Self::None => "none", } } @@ -41,6 +44,7 @@ impl ShapeGizmoHandlers { match self { 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::None => {} } } @@ -50,6 +54,7 @@ impl ShapeGizmoHandlers { match self { 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::None => false, } } @@ -59,6 +64,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.handle_click(), Self::Polygon(h) => h.handle_click(), + Self::Arc(h) => h.handle_click(), Self::None => {} } } @@ -68,6 +74,7 @@ impl ShapeGizmoHandlers { match self { 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::None => {} } } @@ -77,6 +84,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.cleanup(), Self::Polygon(h) => h.cleanup(), + Self::Arc(h) => h.cleanup(), Self::None => {} } } @@ -94,6 +102,7 @@ impl ShapeGizmoHandlers { match self { 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::None => {} } } @@ -110,6 +119,7 @@ impl ShapeGizmoHandlers { match self { 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::None => {} } } @@ -146,6 +156,9 @@ impl GizmoManager { if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() { return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default())); } + if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new())); + } None } 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 2b88dddd5e..a5df795c30 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,2 +1,3 @@ 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/point_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/point_radius_handle.rs index fc00e078cf..32f6eed661 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/point_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/point_radius_handle.rs @@ -262,7 +262,6 @@ impl PointRadiusHandle { }; let viewport = document.metadata().transform_to_viewport(layer); - let center = viewport.transform_point2(DVec2::ZERO); match snapping_index { // Make a triangle with previous two points @@ -274,41 +273,57 @@ impl PointRadiusHandle { overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + let before_outer_position = viewport.inverse().transform_point2(before_outer_position); + let outer_position = viewport.inverse().transform_point2(outer_position); + let point_position = viewport.inverse().transform_point2(point_position); + let l1 = (before_outer_position - outer_position).length() * 0.2; let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return }; let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return }; - let Some(direction) = (center - outer_position).try_normalize() else { return }; + let Some(direction) = (-outer_position).try_normalize() else { return }; let new_point = SQRT_2 * l1 * direction + outer_position; let before_outer_position = l1 * l1_direction + outer_position; let point_position = l1 * l2_direction + outer_position; - overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.)); - overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line( + viewport.transform_point2(before_outer_position), + viewport.transform_point2(new_point), + Some(COLOR_OVERLAY_RED), + Some(3.), + ); + overlay_context.line(viewport.transform_point2(new_point), viewport.transform_point2(point_position), Some(COLOR_OVERLAY_RED), Some(3.)); } 1 => { let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2); - let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2); - let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2); overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + let before_outer_position = viewport.inverse().transform_point2(before_outer_position); + let after_point_position = viewport.inverse().transform_point2(after_point_position); + let point_position = viewport.inverse().transform_point2(point_position); + let l1 = (before_outer_position - point_position).length() * 0.2; let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return }; let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return }; - let Some(direction) = (center - point_position).try_normalize() else { return }; + let Some(direction) = (-point_position).try_normalize() else { return }; let new_point = SQRT_2 * l1 * direction + point_position; let before_outer_position = l1 * l1_direction + point_position; let after_point_position = l1 * l2_direction + point_position; - overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.)); - overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); + overlay_context.line( + viewport.transform_point2(before_outer_position), + viewport.transform_point2(new_point), + Some(COLOR_OVERLAY_RED), + Some(3.), + ); + overlay_context.line(viewport.transform_point2(new_point), viewport.transform_point2(after_point_position), Some(COLOR_OVERLAY_RED), Some(3.)); } i => { // Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0 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 new file mode 100644 index 0000000000..0de10830e1 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs @@ -0,0 +1,322 @@ +use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD}; +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::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shapes::shape_utility::{ + arc_end_points, arc_end_points_ignore_layer, calculate_arc_text_transform, calculate_display_angle, extract_arc_parameters, wrap_to_tau, +}; +use crate::messages::tool::tool_messages::tool_prelude::*; +use crate::messages::{ + frontend::utility_types::MouseCursorIcon, + message::Message, + prelude::{DocumentMessageHandler, FrontendMessage}, +}; +use glam::DVec2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use std::collections::VecDeque; +use std::f64::consts::FRAC_PI_4; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum SweepAngleGizmoState { + #[default] + Inactive, + Hover, + Dragging, + Snapped, +} + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum EndpointType { + #[default] + None, + Start, + End, +} + +#[derive(Clone, Debug, Default)] +pub struct SweepAngleGizmo { + pub layer: Option, + endpoint: EndpointType, + initial_start_angle: f64, + initial_sweep_angle: f64, + previous_mouse_position: DVec2, + total_angle_delta: f64, + snap_angles: Vec, + handle_state: SweepAngleGizmoState, +} + +impl SweepAngleGizmo { + pub fn hovered(&self) -> bool { + self.handle_state == SweepAngleGizmoState::Hover + } + + pub fn update_state(&mut self, state: SweepAngleGizmoState) { + self.handle_state = state; + } + + pub fn is_dragging_or_snapped(&self) -> bool { + self.handle_state == SweepAngleGizmoState::Dragging || self.handle_state == SweepAngleGizmoState::Snapped + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { + match self.handle_state { + SweepAngleGizmoState::Inactive => { + let Some((start, end)) = arc_end_points(Some(layer), document) else { return }; + let Some((_, start_angle, sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else { + return; + }; + + let center = document.metadata().transform_to_viewport(layer).transform_point2(DVec2::ZERO); + + if center.distance(start) < GIZMO_HIDE_THRESHOLD { + return; + } + + if mouse_position.distance(start) < 5. { + self.layer = Some(layer); + self.initial_start_angle = start_angle; + self.initial_sweep_angle = sweep_angle; + self.previous_mouse_position = mouse_position; + self.total_angle_delta = 0.; + self.endpoint = EndpointType::Start; + self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle); + self.update_state(SweepAngleGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + return; + } + + if mouse_position.distance(end) < 5. { + self.layer = Some(layer); + self.initial_start_angle = start_angle; + self.initial_sweep_angle = sweep_angle; + self.previous_mouse_position = mouse_position; + self.total_angle_delta = 0.; + self.endpoint = EndpointType::End; + self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle); + self.update_state(SweepAngleGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + + return; + } + } + + SweepAngleGizmoState::Hover => {} + SweepAngleGizmoState::Dragging => {} + SweepAngleGizmoState::Snapped => {} + } + } + + pub fn overlays( + &self, + selected_arc_layer: Option, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + _mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + let format_rounded = |value: f64, precision: usize| format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string(); + let tilt_offset = document.document_ptz.unmodified_tilt(); + + match self.handle_state { + SweepAngleGizmoState::Inactive => { + 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)); + } + SweepAngleGizmoState::Hover => { + let Some((point1, point2)) = arc_end_points(self.layer, document) else { return }; + + if matches!(self.endpoint, EndpointType::Start) { + overlay_context.manipulator_handle(point1, true, Some(COLOR_OVERLAY_RED)); + } else { + overlay_context.manipulator_handle(point2, true, Some(COLOR_OVERLAY_RED)); + } + } + SweepAngleGizmoState::Dragging => { + let Some(layer) = self.layer else { return }; + let Some((start, end)) = arc_end_points(self.layer, document) else { return }; + + let viewport = document.metadata().transform_to_viewport(layer); + let center = viewport.transform_point2(DVec2::ZERO); + + let Some((radius, _, _, _)) = extract_arc_parameters(self.layer, document) else { + return; + }; + + let Some((initial_start, initial_end)) = arc_end_points_ignore_layer(radius, self.initial_start_angle, self.initial_sweep_angle, Some(viewport)) else { + return; + }; + + let angle = self.total_angle_delta; + + let display_angle = calculate_display_angle(angle); + + let text = format!("{}°", format_rounded(display_angle, 2)); + let text_texture_width = overlay_context.get_width(&text) / 2.; + + if self.endpoint == EndpointType::End { + let initial_vector = initial_end - center; + let offset_angle = initial_vector.to_angle() + tilt_offset; + + let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); + + overlay_context.arc_sweep_angle(offset_angle, angle, end, radius, center, &text, transform); + } else { + let initial_vector = initial_start - center; + let offset_angle = initial_vector.to_angle() + tilt_offset; + + let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); + + overlay_context.arc_sweep_angle(offset_angle, angle, start, radius, center, &text, transform); + } + } + + SweepAngleGizmoState::Snapped => { + let Some((current_start, current_end)) = arc_end_points(self.layer, document) else { + return; + }; + let Some((radius, _, _, _)) = extract_arc_parameters(self.layer, document) else { return }; + let Some(layer) = self.layer else { return }; + let viewport = document.metadata().transform_to_viewport(layer); + + let center = viewport.transform_point2(DVec2::ZERO); + + if self.endpoint == EndpointType::Start { + let initial_vector = current_end - center; + let final_vector = current_start - center; + let offset_angle = initial_vector.to_angle() + tilt_offset; + + let angle = initial_vector.angle_to(final_vector).to_degrees(); + let display_angle = calculate_display_angle(angle); + + let text = format!("{}°", format_rounded(display_angle, 2)); + let text_texture_width = overlay_context.get_width(&text) / 2.; + + let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); + + overlay_context.arc_sweep_angle(offset_angle, angle, current_start, radius, center, &text, transform); + } else { + let initial_vector = current_start - center; + let final_vector = current_end - center; + let offset_angle = initial_vector.to_angle() + tilt_offset; + + let angle = initial_vector.angle_to(final_vector).to_degrees(); + let display_angle = calculate_display_angle(angle); + + let text = format!("{}°", format_rounded(display_angle, 2)); + let text_texture_width = overlay_context.get_width(&text) / 2.; + + let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); + + overlay_context.arc_sweep_angle(offset_angle, angle, current_end, radius, center, &text, transform); + } + + overlay_context.line(current_start, center, Some(COLOR_OVERLAY_RED), Some(2.0)); + overlay_context.line(current_end, center, Some(COLOR_OVERLAY_RED), Some(2.0)); + } + } + } + + pub fn update_arc(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + let Some(layer) = self.layer else { + return; + }; + + let Some((_, start_angle, _, _)) = extract_arc_parameters(Some(layer), document) else { + return; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + let angle = self.total_angle_delta + + viewport + .inverse() + .transform_point2(self.previous_mouse_position) + .angle_to(viewport.inverse().transform_point2(input.mouse.position)) + .to_degrees(); + + let Some(node_id) = graph_modification_utils::get_arc_id(layer, &document.network_interface) else { + return; + }; + + self.update_state(SweepAngleGizmoState::Dragging); + if self.endpoint == EndpointType::End { + let mut total = angle; + if let Some(snapped_delta) = self.check_snapping(start_angle, self.initial_sweep_angle + angle) { + total += snapped_delta; + self.update_state(SweepAngleGizmoState::Snapped); + } + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 3), + input: NodeInput::value(TaggedValue::F64(self.initial_sweep_angle + total), false), + }); + } else { + let sign = angle.signum() * -1.; + let mut total = angle; + + if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle + angle, self.initial_sweep_angle + total.abs() * sign) { + total += snapped_delta; + self.update_state(SweepAngleGizmoState::Snapped); + } + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64(self.initial_start_angle + total), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 3), + input: NodeInput::value(TaggedValue::F64(self.initial_sweep_angle + total.abs() * sign), false), + }); + } + self.previous_mouse_position = input.mouse.position; + self.total_angle_delta = angle; + responses.add(NodeGraphMessage::RunDocumentGraph); + } + + pub fn check_snapping(&self, new_start_angle: f64, new_sweep_angle: f64) -> Option { + let wrapped_sweep_angle = wrap_to_tau(new_sweep_angle.to_radians()).to_degrees(); + let wrapped_start_angle = wrap_to_tau(new_start_angle.to_radians()).to_degrees(); + if self.endpoint == EndpointType::End { + return self + .snap_angles + .iter() + .find(|angle| ((**angle) - (wrapped_sweep_angle)).abs() < ARC_SNAP_THRESHOLD) + .map(|angle| angle - wrapped_sweep_angle); + } else { + return self + .snap_angles + .iter() + .find(|angle| ((**angle) - (wrapped_start_angle)).abs() < ARC_SNAP_THRESHOLD) + .map(|angle| angle - wrapped_start_angle); + } + } + + pub fn calculate_snap_angles(&self, initial_start_angle: f64, initial_sweep_angle: f64) -> Vec { + let mut snap_points = Vec::new(); + let sign = initial_start_angle.signum() * -1.; + let end_angle = initial_start_angle.abs().to_radians() * sign - initial_sweep_angle.to_radians(); + let wrapped_end_angle = wrap_to_tau(-end_angle); + + if self.endpoint == EndpointType::End { + for i in 0..8 { + let snap_point = i as f64 * FRAC_PI_4; + snap_points.push(snap_point.to_degrees()); + } + } + + if self.endpoint == EndpointType::Start { + for i in 0..8 { + let snap_point = wrap_to_tau(wrapped_end_angle + i as f64 * FRAC_PI_4); + snap_points.push(snap_point.to_degrees()); + } + } + + snap_points + } + + pub fn cleanup(&mut self) { + self.layer = None; + self.endpoint = EndpointType::None; + self.handle_state = SweepAngleGizmoState::Inactive; + } +} 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 e06d34b9e2..8d7e5319e5 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -346,6 +346,10 @@ pub fn get_star_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Star") } +pub fn get_arc_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arc") +} + pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Text") } diff --git a/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs new file mode 100644 index 0000000000..ed74097c3d --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/arc_shape.rs @@ -0,0 +1,139 @@ +use super::shape_utility::ShapeToolModifierKey; +use super::*; +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::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::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}; +use crate::messages::tool::tool_messages::tool_prelude::*; +use glam::DAffine2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::misc::ArcType; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default)] +pub struct ArcGizmoHandler { + sweep_angle_gizmo: SweepAngleGizmo, +} + +impl ArcGizmoHandler { + pub fn new() -> Self { + Self { ..Default::default() } + } +} + +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 is_any_gizmo_hovered(&self) -> bool { + self.sweep_angle_gizmo.hovered() + } + + fn handle_click(&mut self) { + if self.sweep_angle_gizmo.hovered() { + self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging); + return; + } + } + + 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); + } + } + + fn dragging_overlays( + &self, + document: &DocumentMessageHandler, + 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 self.sweep_angle_gizmo.is_dragging_or_snapped() { + self.sweep_angle_gizmo.overlays(None, document, input, mouse_position, overlay_context); + arc_outline(self.sweep_angle_gizmo.layer, document, overlay_context); + } + } + + fn overlays( + &self, + document: &DocumentMessageHandler, + selected_shape_layers: 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); + + arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context); + } + + fn cleanup(&mut self) { + self.sweep_angle_gizmo.cleanup(); + } +} +#[derive(Default)] +pub struct Arc; + +impl Arc { + pub fn create_node(arc_type: ArcType) -> NodeTemplate { + let node_type = resolve_document_node_type("Arc").expect("Ellipse node does not exist"); + node_type.node_template_input_override([ + None, + Some(NodeInput::value(TaggedValue::F64(0.5), false)), + Some(NodeInput::value(TaggedValue::F64(0.), false)), + Some(NodeInput::value(TaggedValue::F64(270.), false)), + Some(NodeInput::value(TaggedValue::ArcType(arc_type), false)), + ]) + } + + pub fn update_shape( + document: &DocumentMessageHandler, + ipp: &InputPreprocessorMessageHandler, + layer: LayerNodeIdentifier, + shape_tool_data: &mut ShapeToolData, + modifier: ShapeToolModifierKey, + responses: &mut VecDeque, + ) { + let (center, lock_ratio) = (modifier[0], modifier[1]); + if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) { + let Some(node_id) = graph_modification_utils::get_arc_id(layer, &document.network_interface) else { + return; + }; + + let dimensions = (start - end).abs(); + let mut scale = DVec2::ONE; + let radius: f64; + + // 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.; + } + + 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(scale, 0.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 44f40b5982..812b22c513 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -1,3 +1,4 @@ +pub mod arc_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 be61c9764b..7ad3552ecc 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -1,4 +1,5 @@ use super::ShapeToolData; +use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT}; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -14,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::renderer::ClickTargetType; -use graphene_std::vector::misc::dvec2_to_point; +use graphene_std::vector::misc::{ArcType, dvec2_to_point}; use kurbo::{BezPath, PathEl, Shape}; use std::collections::VecDeque; use std::f64::consts::{PI, TAU}; @@ -24,9 +25,10 @@ pub enum ShapeType { #[default] Polygon = 0, Star = 1, - Rectangle = 2, - Ellipse = 3, - Line = 4, + Arc = 2, + Rectangle = 3, + Ellipse = 4, + Line = 5, } impl ShapeType { @@ -34,6 +36,7 @@ impl ShapeType { (match self { Self::Polygon => "Polygon", Self::Star => "Star", + Self::Arc => "Arc", Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", @@ -234,7 +237,54 @@ pub fn extract_polygon_parameters(layer: Option, document: Some((n, radius)) } -/// Calculate the viewport position of as a star vertex given its index +/// Extract the node input values of Arc +pub fn extract_arc_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(f64, f64, f64, ArcType)> { + let Some(layer) = layer else { + return None; + }; + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Arc")?; + + let (Some(&TaggedValue::F64(radius)), Some(&TaggedValue::F64(start_angle)), Some(&TaggedValue::F64(sweep_angle)), Some(&TaggedValue::ArcType(arc_type))) = ( + node_inputs.get(1)?.as_value(), + node_inputs.get(2)?.as_value(), + node_inputs.get(3)?.as_value(), + node_inputs.get(4)?.as_value(), + ) else { + return None; + }; + + Some((radius, start_angle, sweep_angle, arc_type)) +} + +/// Calculate the viewport positions of arc endpoints +pub fn arc_end_points(layer: Option, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2)> { + let Some(layer) = layer else { + return None; + }; + let Some((radius, start_angle, sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else { + return None; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + + arc_end_points_ignore_layer(radius, start_angle, sweep_angle, Some(viewport)) +} + +pub fn arc_end_points_ignore_layer(radius: f64, start_angle: f64, sweep_angle: f64, viewport: Option) -> Option<(DVec2, DVec2)> { + let sign = start_angle.signum() * -1.; + let end_angle = start_angle.abs().to_radians() * sign - sweep_angle.to_radians(); + + let start_point = radius * DVec2::from_angle(start_angle.to_radians()); + let end_point = radius * DVec2::from_angle(-end_angle); + + if let Some(transform) = viewport { + return Some((transform.transform_point2(start_point), transform.transform_point2(end_point))); + } + + Some((start_point, end_point)) +} + +/// Calculate the viewport position of 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 }; @@ -290,7 +340,32 @@ pub fn polygon_outline(layer: Option, document: &DocumentMe overlay_context.outline(subpath.iter(), viewport, None); } -/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications +/// Outlines the geometric shape made by arc-node +pub fn arc_outline(layer: Option, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) { + let Some(layer) = layer else { + return; + }; + + let Some((radius, start_angle, sweep_angle, arc_type)) = extract_arc_parameters(Some(layer), document) else { + return; + }; + + let subpath: Vec = vec![ClickTargetType::Subpath(Subpath::new_arc( + radius, + start_angle / 360. * std::f64::consts::TAU, + sweep_angle / 360. * std::f64::consts::TAU, + match arc_type { + ArcType::Open => bezier_rs::ArcType::Open, + ArcType::Closed => bezier_rs::ArcType::Closed, + ArcType::PieSlice => bezier_rs::ArcType::PieSlice, + }, + ))]; + let viewport = document.metadata().transform_to_viewport(layer); + + overlay_context.outline(subpath.iter(), viewport, None); +} + +/// Check if the the cursor is inside the geometric star-shape made by star-node without any upstream node modifications pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool { let mut paths = Vec::new(); @@ -363,3 +438,28 @@ pub fn draw_snapping_ticks(snap_radii: &[f64], direction: DVec2, viewport: DAffi overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.)); } } + +/// Wraps an angle (in radians) into the range [0, 2π). +pub fn wrap_to_tau(angle: f64) -> f64 { + (angle % TAU + TAU) % TAU +} + +// Give the approximated angle to display in degrees(Note : The input is in degrees) +pub fn calculate_display_angle(angle: f64) -> f64 { + if angle.is_sign_positive() { + angle - (angle / 360.).floor() * 360. + } else if angle.is_sign_negative() { + angle - ((angle / 360.).floor() + 1.) * 360. + } else { + angle + } +} + +pub fn calculate_arc_text_transform(angle: f64, offset_angle: f64, center: DVec2, width: f64) -> DAffine2 { + let text_angle_on_unit_circle = DVec2::from_angle((angle.to_radians() % TAU) / 2. + offset_angle); + let text_texture_position = DVec2::new( + (ARC_SWEEP_GIZMO_RADIUS + 4. + width) * text_angle_on_unit_circle.x, + (ARC_SWEEP_GIZMO_RADIUS + ARC_SWEEP_GIZMO_TEXT_HEIGHT) * text_angle_on_unit_circle.y, + ); + DAffine2::from_translation(text_texture_position + center) +} diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 96dbc5ab01..f908f29581 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -10,6 +10,7 @@ use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoMan 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::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}; @@ -109,10 +110,28 @@ 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("Arc") + .label("Arc") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()), ]]; DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() } +fn create_arc_type_widget(arc_type: ArcType) -> WidgetHolder { + let entries = vec![ + RadioEntryData::new("Open") + .label("Open") + .on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Open)).into()), + RadioEntryData::new("Closed") + .label("Closed") + .on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Closed)).into()), + RadioEntryData::new("Pie") + .label("Pie") + .on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::PieSlice)).into()), + ]; + RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_holder() +} + fn create_weight_widget(line_weight: f64) -> WidgetHolder { NumberInput::new(Some(line_weight)) .unit(" px") @@ -135,6 +154,11 @@ impl LayoutHolder for ShapeTool { widgets.push(create_sides_widget(self.options.vertices)); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); } + + if self.options.shape_type == ShapeType::Arc { + widgets.push(create_arc_type_widget(self.options.arc_type)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + } } if self.options.shape_type != ShapeType::Line { @@ -578,7 +602,7 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle => tool_data.data.start(document, input), + ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Arc | ShapeType::Rectangle => 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()); @@ -591,6 +615,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::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), @@ -602,7 +627,7 @@ impl Fsm for ShapeToolFsmState { responses.add(Message::StartBuffer); match tool_data.current_shape { - ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star => { + ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star => { responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -635,6 +660,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::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses), } // Auto-panning @@ -829,7 +855,7 @@ impl Fsm for ShapeToolFsmState { let hint_data = match self { ShapeToolFsmState::Ready(shape) => { let hint_groups = match shape { - ShapeType::Polygon | ShapeType::Star => vec![ + ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => vec![ HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), @@ -859,7 +885,7 @@ impl Fsm for ShapeToolFsmState { 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 => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), + 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![ diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index adab1ce531..7d72c328a7 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -1,7 +1,7 @@ use super::misc::{ArcType, AsU64, GridType}; use super::{PointId, SegmentId, StrokeId}; use crate::Ctx; -use crate::registry::types::{Angle, PixelSize}; +use crate::registry::types::PixelSize; use crate::vector::{HandleId, VectorData, VectorDataTable}; use bezier_rs::Subpath; use glam::DVec2; @@ -43,16 +43,7 @@ fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataT } #[node_macro::node(category("Vector: Shape"))] -fn arc( - _: impl Ctx, - _primary: (), - #[default(50.)] radius: f64, - start_angle: Angle, - #[default(270.)] - #[range((0., 360.))] - sweep_angle: Angle, - arc_type: ArcType, -) -> VectorDataTable { +fn arc(_: impl Ctx, _primary: (), #[default(50.)] radius: f64, start_angle: f64, #[default(270.)] sweep_angle: f64, arc_type: ArcType) -> VectorDataTable { VectorDataTable::new(VectorData::from_subpath(Subpath::new_arc( radius, start_angle / 360. * std::f64::consts::TAU, diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index f29286b605..d77d8a3486 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -89,9 +89,9 @@ pub enum GridType { #[widget(Radio)] pub enum ArcType { #[default] - Open, - Closed, - PieSlice, + Open = 0, + Closed = 1, + PieSlice = 2, } #[repr(C)] From 0b548e5281698826255fcffdb89192da06244539 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Fri, 4 Jul 2025 19:45:02 +0530 Subject: [PATCH 2/3] fixed wrapping need to fix snapping and overlays --- .../gizmos/shape_gizmos/sweep_angle_gizmo.rs | 176 ++++++++++++++---- .../gcore/src/vector/generator_nodes.rs | 2 +- 2 files changed, 145 insertions(+), 33 deletions(-) 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 0de10830e1..191d09f285 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 @@ -13,6 +13,7 @@ use crate::messages::{ prelude::{DocumentMessageHandler, FrontendMessage}, }; use glam::DVec2; +use graph_craft::document::NodeId; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use std::collections::VecDeque; @@ -41,6 +42,7 @@ pub struct SweepAngleGizmo { endpoint: EndpointType, initial_start_angle: f64, initial_sweep_angle: f64, + initial_start_point: DVec2, previous_mouse_position: DVec2, total_angle_delta: f64, snap_angles: Vec, @@ -203,6 +205,7 @@ impl SweepAngleGizmo { let offset_angle = initial_vector.to_angle() + tilt_offset; let angle = initial_vector.angle_to(final_vector).to_degrees(); + log::info!("angle {:?}", angle); let display_angle = calculate_display_angle(angle); let text = format!("{}°", format_rounded(display_angle, 2)); @@ -224,52 +227,161 @@ impl SweepAngleGizmo { return; }; - let Some((_, start_angle, _, _)) = extract_arc_parameters(Some(layer), document) else { + let Some((_, current_start_angle, current_sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else { return; }; let viewport = document.metadata().transform_to_viewport(layer); - let angle = self.total_angle_delta - + viewport - .inverse() - .transform_point2(self.previous_mouse_position) - .angle_to(viewport.inverse().transform_point2(input.mouse.position)) - .to_degrees(); + let angle_delta = viewport + .inverse() + .transform_point2(self.previous_mouse_position) + .angle_to(viewport.inverse().transform_point2(input.mouse.position)) + .to_degrees(); + let angle = self.total_angle_delta + angle_delta; let Some(node_id) = graph_modification_utils::get_arc_id(layer, &document.network_interface) else { return; }; self.update_state(SweepAngleGizmoState::Dragging); - if self.endpoint == EndpointType::End { - let mut total = angle; - if let Some(snapped_delta) = self.check_snapping(start_angle, self.initial_sweep_angle + angle) { - total += snapped_delta; - self.update_state(SweepAngleGizmoState::Snapped); + + match self.endpoint { + EndpointType::Start => { + // Dragging start changes both start and sweep + + let sign = angle.signum() * -1.; + let mut total = angle; + + let new_start_angle = self.initial_start_angle + total; + let new_sweep_angle = self.initial_sweep_angle + total.abs() * sign; + + // Clamp sweep angle to 360° + if new_sweep_angle > 360. { + let wrapped = new_sweep_angle % 360.; + self.total_angle_delta = -wrapped; + + // Remaining drag gets passed to the end endpoint + let rest_angle = angle_delta + wrapped; + self.endpoint = EndpointType::End; + + self.initial_sweep_angle = 360.; + self.initial_start_angle = current_start_angle + rest_angle; + + self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle - wrapped, input, responses); + return; + } + + if new_sweep_angle < 0. { + let rest_angle = angle_delta + new_sweep_angle; + + self.total_angle_delta = new_sweep_angle.abs(); + self.endpoint = EndpointType::End; + + self.initial_sweep_angle = 0.; + self.initial_start_angle = current_start_angle + rest_angle; + + self.apply_arc_update(node_id, self.initial_start_angle, new_sweep_angle.abs(), input, responses); + return; + } + + // Wrap start angle > 180° back into [-180°, 180°] and adjust sweep + if new_start_angle > 180. { + let overflow = new_start_angle % 180.; + let rest_angle = angle_delta - overflow; + + // We wrap the angle back into [-180°, 180°] range by jumping from +180° to -180° + // Example: dragging past 190° becomes -170°, and we subtract the overshoot from sweep + // Sweep angle must shrink to maintain consistent arc + self.total_angle_delta = rest_angle; + self.initial_start_angle = -180.; + self.initial_sweep_angle = current_sweep_angle - rest_angle; + + self.apply_arc_update(node_id, self.initial_start_angle + overflow, self.initial_sweep_angle - overflow, input, responses); + return; + } + + // Wrap start angle < -180° back into [-180°, 180°] and adjust sweep + if new_start_angle < -180. { + let underflow = new_start_angle % 180.; + let rest_angle = angle_delta - underflow; + + // We wrap the angle back into [-180°, 180°] by jumping from -190° to +170° + // Sweep must grow to reflect continued clockwise drag past -180° + // Start angle flips from -190° to +170°, and sweep increases accordingly + self.total_angle_delta = underflow; + self.initial_start_angle = 180.; + self.initial_sweep_angle = current_sweep_angle + rest_angle.abs(); + + self.apply_arc_update(node_id, self.initial_start_angle + underflow, self.initial_sweep_angle + underflow.abs(), input, responses); + return; + } + + if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle + angle, self.initial_sweep_angle + total.abs() * sign) { + total += snapped_delta; + self.update_state(SweepAngleGizmoState::Snapped); + } + + self.total_angle_delta = angle; + self.apply_arc_update(node_id, self.initial_start_angle + total, self.initial_sweep_angle + total.abs() * sign, input, responses); } - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 3), - input: NodeInput::value(TaggedValue::F64(self.initial_sweep_angle + total), false), - }); - } else { - let sign = angle.signum() * -1.; - let mut total = angle; + EndpointType::End => { + // Dragging the end only changes sweep angle + + let mut total = angle; + let new_sweep_angle = self.initial_sweep_angle + angle; + + // Clamp sweep angle below 0°, switch to start + if new_sweep_angle < 0. { + let delta = angle_delta - current_sweep_angle; + let sign = delta.signum() * -1.; + + self.initial_sweep_angle = 0.; + self.total_angle_delta = delta; + self.endpoint = EndpointType::Start; + + self.apply_arc_update(node_id, self.initial_start_angle + delta, self.initial_sweep_angle + delta.abs() * sign, input, responses); + return; + } - if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle + angle, self.initial_sweep_angle + total.abs() * sign) { - total += snapped_delta; - self.update_state(SweepAngleGizmoState::Snapped); + // Clamp sweep angle above 360°, switch to start + if new_sweep_angle > 360. { + let delta = angle_delta - (360. - current_sweep_angle); + let sign = delta.signum() * -1.; + + self.total_angle_delta = angle_delta; + self.initial_sweep_angle = 360.; + self.endpoint = EndpointType::Start; + + self.apply_arc_update(node_id, self.initial_start_angle + angle_delta, self.initial_sweep_angle + angle_delta.abs() * sign, input, responses); + return; + } + + if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle, self.initial_sweep_angle + angle) { + total += snapped_delta; + self.update_state(SweepAngleGizmoState::Snapped); + } + + self.total_angle_delta = angle; + self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle + total, input, responses); } - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::F64(self.initial_start_angle + total), false), - }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 3), - input: NodeInput::value(TaggedValue::F64(self.initial_sweep_angle + total.abs() * sign), false), - }); + EndpointType::None => {} } + } + + /// Applies the updated start and sweep angles to the arc. + fn apply_arc_update(&mut self, node_id: NodeId, start_angle: f64, sweep_angle: f64, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 2), + input: NodeInput::value(TaggedValue::F64(start_angle), false), + }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 3), + input: NodeInput::value(TaggedValue::F64(sweep_angle), false), + }); + self.previous_mouse_position = input.mouse.position; - self.total_angle_delta = angle; responses.add(NodeGraphMessage::RunDocumentGraph); } @@ -299,7 +411,7 @@ impl SweepAngleGizmo { if self.endpoint == EndpointType::End { for i in 0..8 { - let snap_point = i as f64 * FRAC_PI_4; + let snap_point = wrap_to_tau(i as f64 * FRAC_PI_4 + initial_start_angle); snap_points.push(snap_point.to_degrees()); } } diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 13ecc9e3f4..6211e2d1b1 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -1,7 +1,7 @@ use super::misc::{ArcType, AsU64, GridType}; use super::{PointId, SegmentId, StrokeId}; use crate::Ctx; -use crate::registry::types::PixelSize; +use crate::registry::types::{Angle, PixelSize}; use crate::vector::{HandleId, VectorData, VectorDataTable}; use bezier_rs::Subpath; use glam::DVec2; From 50f2411483fdd9874c6ec01d0ee8ed96e1c10070 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Mon, 14 Jul 2025 02:51:52 +0530 Subject: [PATCH 3/3] fixed all the issues --- .../document/overlays/utility_types.rs | 12 +- .../gizmos/shape_gizmos/sweep_angle_gizmo.rs | 183 ++++++------------ .../shapes/shape_utility.rs | 12 +- 3 files changed, 82 insertions(+), 125 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 77d5468a73..ed763014ea 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -414,6 +414,14 @@ impl OverlayContext { self.render_context.stroke(); } + pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, dash_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) { + let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle); + let end_point2 = pivot + dash_radius * DVec2::from_angle(offset_angle); + self.line(pivot, end_point1, None, None); + self.dashed_line(pivot, end_point2, None, None, Some(2.), Some(2.), Some(0.5)); + self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle); + } + pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) { let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle); let end_point2 = pivot + radius * DVec2::from_angle(offset_angle); @@ -551,9 +559,9 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, radius: f64, pivot: DVec2, text: &str, transform: DAffine2) { + pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, dash_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) { self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED)); - self.draw_angle(pivot, radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians()); + self.draw_arc_gizmo_angle(pivot, bold_radius, dash_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/sweep_angle_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/sweep_angle_gizmo.rs index 191d09f285..6ba6f5b71b 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 @@ -3,9 +3,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; use crate::messages::tool::common_functionality::graph_modification_utils; -use crate::messages::tool::common_functionality::shapes::shape_utility::{ - arc_end_points, arc_end_points_ignore_layer, calculate_arc_text_transform, calculate_display_angle, extract_arc_parameters, wrap_to_tau, -}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{arc_end_points, calculate_arc_text_transform, extract_arc_parameters, format_rounded}; use crate::messages::tool::tool_messages::tool_prelude::*; use crate::messages::{ frontend::utility_types::MouseCursorIcon, @@ -42,7 +40,7 @@ pub struct SweepAngleGizmo { endpoint: EndpointType, initial_start_angle: f64, initial_sweep_angle: f64, - initial_start_point: DVec2, + position_before_rotation: DVec2, previous_mouse_position: DVec2, total_angle_delta: f64, snap_angles: Vec, @@ -76,30 +74,25 @@ impl SweepAngleGizmo { return; } - if mouse_position.distance(start) < 5. { - self.layer = Some(layer); - self.initial_start_angle = start_angle; - self.initial_sweep_angle = sweep_angle; - self.previous_mouse_position = mouse_position; - self.total_angle_delta = 0.; - self.endpoint = EndpointType::Start; - self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle); - self.update_state(SweepAngleGizmoState::Hover); - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - return; - } + let (close_to_gizmo, endpoint_type) = if mouse_position.distance(start) < 5. { + (true, EndpointType::Start) + } else if mouse_position.distance(end) < 5. { + (true, EndpointType::End) + } else { + (false, EndpointType::None) + }; - if mouse_position.distance(end) < 5. { + if close_to_gizmo { self.layer = Some(layer); self.initial_start_angle = start_angle; self.initial_sweep_angle = sweep_angle; self.previous_mouse_position = mouse_position; self.total_angle_delta = 0.; - self.endpoint = EndpointType::End; - self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle); + self.position_before_rotation = if endpoint_type == EndpointType::End { end } else { start }; + self.endpoint = endpoint_type; + self.snap_angles = Self::calculate_snap_angles(); self.update_state(SweepAngleGizmoState::Hover); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - return; } } @@ -118,16 +111,17 @@ impl SweepAngleGizmo { _mouse_position: DVec2, overlay_context: &mut OverlayContext, ) { - let format_rounded = |value: f64, precision: usize| format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string(); let tilt_offset = document.document_ptz.unmodified_tilt(); 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)); } SweepAngleGizmoState::Hover => { + // Highlight the currently hovered endpoint only let Some((point1, point2)) = arc_end_points(self.layer, document) else { return }; if matches!(self.endpoint, EndpointType::Start) { @@ -137,91 +131,65 @@ impl SweepAngleGizmo { } } SweepAngleGizmoState::Dragging => { + // Show snapping guides and angle arc while dragging let Some(layer) = self.layer else { return }; - let Some((start, end)) = arc_end_points(self.layer, document) else { return }; - + let Some((current_start, current_end)) = arc_end_points(self.layer, document) else { return }; let viewport = document.metadata().transform_to_viewport(layer); - let center = viewport.transform_point2(DVec2::ZERO); - - let Some((radius, _, _, _)) = extract_arc_parameters(self.layer, document) else { - return; - }; - - let Some((initial_start, initial_end)) = arc_end_points_ignore_layer(radius, self.initial_start_angle, self.initial_sweep_angle, Some(viewport)) else { - return; - }; - - let angle = self.total_angle_delta; - - let display_angle = calculate_display_angle(angle); - - let text = format!("{}°", format_rounded(display_angle, 2)); - let text_texture_width = overlay_context.get_width(&text) / 2.; + // Depending on which endpoint is being dragged, draw guides relative to the static point if self.endpoint == EndpointType::End { - let initial_vector = initial_end - center; - let offset_angle = initial_vector.to_angle() + tilt_offset; - - let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); - - overlay_context.arc_sweep_angle(offset_angle, angle, end, radius, center, &text, transform); + self.dragging_snapping_overlays(self.position_before_rotation, current_end, tilt_offset, viewport, overlay_context); } else { - let initial_vector = initial_start - center; - let offset_angle = initial_vector.to_angle() + tilt_offset; - - let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); - - overlay_context.arc_sweep_angle(offset_angle, angle, start, radius, center, &text, transform); + self.dragging_snapping_overlays(self.position_before_rotation, current_start, tilt_offset, viewport, overlay_context); } } - SweepAngleGizmoState::Snapped => { - let Some((current_start, current_end)) = arc_end_points(self.layer, document) else { - return; - }; - let Some((radius, _, _, _)) = extract_arc_parameters(self.layer, document) else { return }; + // When snapping is active, draw snapping arcs and angular guidelines + let Some((current_start, current_end)) = arc_end_points(self.layer, document) else { return }; let Some(layer) = self.layer else { return }; let viewport = document.metadata().transform_to_viewport(layer); - let center = viewport.transform_point2(DVec2::ZERO); + // Draw snapping arc and angle overlays between the two points if self.endpoint == EndpointType::Start { - let initial_vector = current_end - center; - let final_vector = current_start - center; - let offset_angle = initial_vector.to_angle() + tilt_offset; - - let angle = initial_vector.angle_to(final_vector).to_degrees(); - let display_angle = calculate_display_angle(angle); - - let text = format!("{}°", format_rounded(display_angle, 2)); - let text_texture_width = overlay_context.get_width(&text) / 2.; - - let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); - - overlay_context.arc_sweep_angle(offset_angle, angle, current_start, radius, center, &text, transform); + self.dragging_snapping_overlays(current_end, current_start, tilt_offset, viewport, overlay_context); } else { - let initial_vector = current_start - center; - let final_vector = current_end - center; - let offset_angle = initial_vector.to_angle() + tilt_offset; - - let angle = initial_vector.angle_to(final_vector).to_degrees(); - log::info!("angle {:?}", angle); - let display_angle = calculate_display_angle(angle); - - let text = format!("{}°", format_rounded(display_angle, 2)); - let text_texture_width = overlay_context.get_width(&text) / 2.; - - let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); - - overlay_context.arc_sweep_angle(offset_angle, angle, current_end, radius, center, &text, transform); + self.dragging_snapping_overlays(current_start, current_end, tilt_offset, viewport, overlay_context); } + // Draw lines from endpoints to the arc center overlay_context.line(current_start, center, Some(COLOR_OVERLAY_RED), Some(2.0)); overlay_context.line(current_end, center, Some(COLOR_OVERLAY_RED), Some(2.0)); } } } + /// Draws the visual overlay during arc handle dragging or snapping interactions. + /// This includes the dynamic arc sweep, angle label, and visual guides centered around the arc's origin. + pub fn dragging_snapping_overlays(&self, initial_point: DVec2, final_point: DVec2, tilt_offset: f64, viewport: DAffine2, overlay_context: &mut OverlayContext) { + let center = viewport.transform_point2(DVec2::ZERO); + let initial_vector = initial_point - center; + let final_vector = final_point - center; + let offset_angle = initial_vector.to_angle() + tilt_offset; + + let dash_radius = initial_point.distance(center); + let bold_radius = final_point.distance(center); + + let angle = initial_vector.angle_to(final_vector).to_degrees(); + let display_angle = viewport + .inverse() + .transform_point2(final_point) + .angle_to(viewport.inverse().transform_point2(initial_point)) + .to_degrees(); + + let text = format!("{}°", format_rounded(display_angle, 2)); + let text_texture_width = overlay_context.get_width(&text) / 2.; + + let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width); + + overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, dash_radius, center, &text, transform); + } + pub fn update_arc(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { let Some(layer) = self.layer else { return; @@ -316,7 +284,7 @@ impl SweepAngleGizmo { return; } - if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle + angle, self.initial_sweep_angle + total.abs() * sign) { + if let Some(snapped_delta) = self.check_snapping(self.initial_sweep_angle + total.abs() * sign) { total += snapped_delta; self.update_state(SweepAngleGizmoState::Snapped); } @@ -356,7 +324,7 @@ impl SweepAngleGizmo { return; } - if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle, self.initial_sweep_angle + angle) { + if let Some(snapped_delta) = self.check_snapping(self.initial_sweep_angle + angle) { total += snapped_delta; self.update_state(SweepAngleGizmoState::Snapped); } @@ -370,7 +338,7 @@ impl SweepAngleGizmo { /// Applies the updated start and sweep angles to the arc. fn apply_arc_update(&mut self, node_id: NodeId, start_angle: f64, sweep_angle: f64, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { - self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle); + self.snap_angles = Self::calculate_snap_angles(); responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 2), @@ -385,42 +353,19 @@ impl SweepAngleGizmo { responses.add(NodeGraphMessage::RunDocumentGraph); } - pub fn check_snapping(&self, new_start_angle: f64, new_sweep_angle: f64) -> Option { - let wrapped_sweep_angle = wrap_to_tau(new_sweep_angle.to_radians()).to_degrees(); - let wrapped_start_angle = wrap_to_tau(new_start_angle.to_radians()).to_degrees(); - if self.endpoint == EndpointType::End { - return self - .snap_angles - .iter() - .find(|angle| ((**angle) - (wrapped_sweep_angle)).abs() < ARC_SNAP_THRESHOLD) - .map(|angle| angle - wrapped_sweep_angle); - } else { - return self - .snap_angles - .iter() - .find(|angle| ((**angle) - (wrapped_start_angle)).abs() < ARC_SNAP_THRESHOLD) - .map(|angle| angle - wrapped_start_angle); - } + pub fn check_snapping(&self, new_sweep_angle: f64) -> Option { + return self.snap_angles.iter().find(|angle| (**angle - new_sweep_angle).abs() <= ARC_SNAP_THRESHOLD).map(|angle| { + let delta = angle - new_sweep_angle; + if self.endpoint == EndpointType::End { delta } else { -delta } + }); } - pub fn calculate_snap_angles(&self, initial_start_angle: f64, initial_sweep_angle: f64) -> Vec { + pub fn calculate_snap_angles() -> Vec { let mut snap_points = Vec::new(); - let sign = initial_start_angle.signum() * -1.; - let end_angle = initial_start_angle.abs().to_radians() * sign - initial_sweep_angle.to_radians(); - let wrapped_end_angle = wrap_to_tau(-end_angle); - - if self.endpoint == EndpointType::End { - for i in 0..8 { - let snap_point = wrap_to_tau(i as f64 * FRAC_PI_4 + initial_start_angle); - snap_points.push(snap_point.to_degrees()); - } - } - if self.endpoint == EndpointType::Start { - for i in 0..8 { - let snap_point = wrap_to_tau(wrapped_end_angle + i as f64 * FRAC_PI_4); - snap_points.push(snap_point.to_degrees()); - } + for i in 0..8 { + let snap_point = i as f64 * FRAC_PI_4; + snap_points.push(snap_point.to_degrees()); } snap_points 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 4eaf508158..057cc412a1 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -237,7 +237,8 @@ pub fn extract_polygon_parameters(layer: Option, document: Some((n, radius)) } -/// Extract the node input values of Arc +/// Extract the node input values of Arc. +/// Returns an option of (radius, start angle, sweep angle, arc type). pub fn extract_arc_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(f64, f64, f64, ArcType)> { let Some(layer) = layer else { return None; @@ -271,11 +272,10 @@ pub fn arc_end_points(layer: Option, document: &DocumentMes } pub fn arc_end_points_ignore_layer(radius: f64, start_angle: f64, sweep_angle: f64, viewport: Option) -> Option<(DVec2, DVec2)> { - let sign = start_angle.signum() * -1.; - let end_angle = start_angle.abs().to_radians() * sign - sweep_angle.to_radians(); + let end_angle = start_angle.to_radians() + sweep_angle.to_radians(); let start_point = radius * DVec2::from_angle(start_angle.to_radians()); - let end_point = radius * DVec2::from_angle(-end_angle); + let end_point = radius * DVec2::from_angle(end_angle); if let Some(transform) = viewport { return Some((transform.transform_point2(start_point), transform.transform_point2(end_point))); @@ -444,6 +444,10 @@ pub fn wrap_to_tau(angle: f64) -> f64 { (angle % TAU + TAU) % TAU } +pub fn format_rounded(value: f64, precision: usize) -> String { + format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string() +} + // Give the approximated angle to display in degrees(Note : The input is in degrees) pub fn calculate_display_angle(angle: f64) -> f64 { if angle.is_sign_positive() {