diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index ae05d2588d..f8a89d1ccc 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -372,6 +372,80 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + #[allow(clippy::too_many_arguments)] + pub fn dashed_ellipse( + &mut self, + center: DVec2, + radius_x: f64, + radius_y: f64, + rotation: Option, + start_angle: Option, + end_angle: Option, + counterclockwise: Option, + color_fill: Option<&str>, + color_stroke: Option<&str>, + dash_width: Option, + dash_gap_width: Option, + dash_offset: Option, + ) { + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + let center = center.round(); + + self.start_dpi_aware_transform(); + + if let Some(dash_width) = dash_width { + let dash_gap_width = dash_gap_width.unwrap_or(1.); + let array = js_sys::Array::new(); + array.push(&JsValue::from(dash_width)); + array.push(&JsValue::from(dash_gap_width)); + + if let Some(dash_offset) = dash_offset { + if dash_offset != 0. { + self.render_context.set_line_dash_offset(dash_offset); + } + } + + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + + self.render_context.begin_path(); + self.render_context + .ellipse_with_anticlockwise( + center.x, + center.y, + radius_x, + radius_y, + rotation.unwrap_or_default(), + start_angle.unwrap_or_default(), + end_angle.unwrap_or(TAU), + counterclockwise.unwrap_or_default(), + ) + .expect("Failed to draw ellipse"); + self.render_context.set_stroke_style_str(color_stroke); + + if let Some(fill_color) = color_fill { + self.render_context.set_fill_style_str(fill_color); + self.render_context.fill(); + } + self.render_context.stroke(); + + // Reset the dash pattern back to solid + if dash_width.is_some() { + self.render_context + .set_line_dash(&JsValue::from(js_sys::Array::new())) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + if dash_offset.is_some() && dash_offset != Some(0.) { + self.render_context.set_line_dash_offset(0.); + } + + self.end_dpi_aware_transform(); + } + pub fn dashed_circle( &mut self, position: DVec2, diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs index 3f7bf6689e..da857a5a59 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs @@ -56,19 +56,44 @@ impl RadiusGizmo { pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { match &self.handle_state { RadiusGizmoState::Inactive => { - if let Some(((inner_radius, outer_radius, _, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { - let smaller_radius = (inner_radius.min(outer_radius)).max(5.); + if let Some(((inner_radius, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { let viewport = document.metadata().transform_to_viewport(layer); let layer_mouse = viewport.inverse().transform_point2(mouse_position); - if DVec2::ZERO.distance(layer_mouse) < smaller_radius.max(5.) { + let center = viewport.transform_point2(DVec2::ZERO); + + let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); + + let start_radius = spiral_point(0. + start_angle, inner_radius, b, spiral_type).distance(DVec2::ZERO); + let end_radius = spiral_point(turns * TAU + start_angle, inner_radius, b, spiral_type).distance(DVec2::ZERO); + + log::info!("start_radius {:?}", start_radius); + log::info!("end radius {:?}", end_radius); + + let larger_radius = (start_radius.max(end_radius)).max(5.); + let smaller_radius = (start_radius.min(end_radius)).max(5.); + + if layer_mouse.distance(DVec2::ZERO) < smaller_radius { + log::info!("reaching heeee"); self.layer = Some(layer); - self.initial_radius = inner_radius; + self.initial_radius = smaller_radius; self.spiral_type = spiral_type; self.previous_mouse_position = mouse_position; self.radius_index = if inner_radius > outer_radius { SPIRAL_OUTER_RADIUS_INDEX } else { SPIRAL_INNER_RADIUS_INDEX }; self.update_state(RadiusGizmoState::Hover); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + return; + } + + if (layer_mouse.distance(DVec2::ZERO) - larger_radius).abs() < 5. { + self.layer = Some(layer); + self.initial_radius = larger_radius; + self.spiral_type = spiral_type; + self.previous_mouse_position = mouse_position; + self.radius_index = if inner_radius > outer_radius { SPIRAL_INNER_RADIUS_INDEX } else { SPIRAL_OUTER_RADIUS_INDEX }; + self.update_state(RadiusGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + return; } } } @@ -82,16 +107,20 @@ impl RadiusGizmo { let Some(layer) = selected_spiral_layer.or(self.layer) else { return }; let viewport = document.metadata().transform_to_viewport(layer); - if let Some(((inner_radius, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + if let Some(((inner_radius, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let (radius, endpoint) = if self.radius_index == SPIRAL_INNER_RADIUS_INDEX { - (inner_radius, spiral_point(0., inner_radius, b, spiral_type)) + let endpoint = if self.radius_index == SPIRAL_INNER_RADIUS_INDEX { + spiral_point(0. + start_angle, inner_radius, b, spiral_type) } else { - (outer_radius, spiral_point(turns * TAU, inner_radius, b, spiral_type)) + spiral_point(turns * TAU + start_angle, inner_radius, b, spiral_type) }; + let viewport_center = viewport.transform_point2(DVec2::ZERO); + + let radius = viewport.transform_point2(endpoint).distance(viewport_center); + overlay_context.manipulator_handle(viewport.transform_point2(endpoint), true, Some(COLOR_OVERLAY_RED)); - overlay_context.dashed_circle(DVec2::ZERO, radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); + overlay_context.dashed_ellipse(viewport_center, radius, radius, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5)); } } _ => {} @@ -110,19 +139,14 @@ impl RadiusGizmo { .expect("Failed to find inputs of Spiral"); let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - - let center = DVec2::ZERO; let layer_drag_start = viewport_transform.inverse().transform_point2(drag_start); let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); let previous_mouse_layer = viewport_transform.inverse().transform_point2(self.previous_mouse_position); let sign = (current_mouse_layer - previous_mouse_layer).dot(layer_drag_start).signum(); - let delta = current_mouse_layer.distance(previous_mouse_layer) * sign; - let net_radius = current_mouse_layer.distance(DVec2::ZERO); - let Some(&TaggedValue::F64(radius)) = node_inputs.get(self.radius_index).expect("Failed to get radius of Spiral").as_value() else { return; }; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs index 5b61b4e996..94d121f569 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs @@ -1,4 +1,4 @@ -use crate::consts::{COLOR_OVERLAY_RED, SPIRAL_INNER_RADIUS_INDEX, SPIRAL_INNER_RADIUS_INDEX_GIZMO_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX}; +use crate::consts::{SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -8,15 +8,12 @@ use crate::messages::prelude::Responses; use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage}; use crate::messages::tool::common_functionality::graph_modification_utils::{self}; use crate::messages::tool::common_functionality::shape_editor::ShapeState; -use crate::messages::tool::common_functionality::shapes::shape_utility::{ - calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, -}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_spiral_type, spiral_point}; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::uuid::NodeId; -use graphene_std::vector::misc::{SpiralType, dvec2_to_point}; -use kurbo::{Line, ParamCurveNearest}; +use graphene_std::vector::misc::SpiralType; use std::collections::VecDeque; use std::f64::consts::TAU; @@ -28,14 +25,6 @@ pub enum TightnessGizmoState { Dragging, } -#[derive(Clone, Debug, Default, PartialEq)] -enum TightnessGizmoType { - #[default] - None, - Circle, - DashLines, -} - #[derive(Clone, Debug, Default)] pub struct TightnessGizmo { pub layer: Option, @@ -47,7 +36,6 @@ pub struct TightnessGizmo { angle: f64, spiral_slot: i32, previous_mouse: DVec2, - gizmo_type: TightnessGizmoType, } impl TightnessGizmo { @@ -55,7 +43,6 @@ impl TightnessGizmo { self.handle_state = TightnessGizmoState::Inactive; self.layer = None; self.gizmo_line_points = None; - self.gizmo_type = TightnessGizmoType::None; } pub fn update_state(&mut self, state: TightnessGizmoState) { @@ -76,8 +63,10 @@ impl TightnessGizmo { match &self.handle_state { TightnessGizmoState::Inactive => { // Archimedean - if let Some(((a, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { - if let Some((start, end, slot_index)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), outer_radius, turns, a, spiral_type, viewport) { + if let Some(((a, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + if let Some((start, end, slot_index)) = + Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), a, outer_radius, turns, start_angle, spiral_type, viewport) + { self.layer = Some(layer); self.initial_outer_radius = outer_radius; self.previous_mouse = mouse_position; @@ -85,34 +74,12 @@ impl TightnessGizmo { self.spiral_type = spiral_type; self.spiral_slot = slot_index; self.inner_radius = a; - self.gizmo_type = TightnessGizmoType::DashLines; self.angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X).rem_euclid(TAU); self.update_state(TightnessGizmoState::Hover); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); return; }; - - let center = viewport.transform_point2(DVec2::ZERO); - - let angle = if a > outer_radius { 0. } else { TAU }; - - let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, angle).or(get_log_spiral_end_point(layer, document, viewport, angle)) else { - return; - }; - - let close_to_circle = (endpoint.distance(center) - mouse_position.distance(center)).abs() < 5.; - - if close_to_circle { - self.layer = Some(layer); - self.inner_radius = a; - self.initial_outer_radius = outer_radius; - self.previous_mouse = mouse_position; - self.gizmo_type = TightnessGizmoType::Circle; - self.spiral_type = spiral_type; - self.update_state(TightnessGizmoState::Hover); - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - } } } TightnessGizmoState::Hover | TightnessGizmoState::Dragging => {} @@ -126,35 +93,43 @@ impl TightnessGizmo { let viewport = document.metadata().transform_to_viewport(layer); match &self.handle_state { - TightnessGizmoState::Hover | TightnessGizmoState::Dragging => match self.gizmo_type { - TightnessGizmoType::Circle => { - if let Some((inner_radius, outer_radius, _, _)) = extract_arc_or_log_spiral_parameters(layer, document) { - let required_radius = if self.inner_radius > self.initial_outer_radius { inner_radius } else { outer_radius }; - overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(8.), Some(4.), Some(0.5), Some(viewport)); + TightnessGizmoState::Hover | TightnessGizmoState::Dragging => { + if let Some((start, end)) = self.gizmo_line_points { + overlay_context.dashed_line(start, end, None, None, Some(4.0), Some(4.0), Some(0.5)); + if self.spiral_slot == 0 { + let required_radius = if self.inner_radius > self.initial_outer_radius { + viewport + .inverse() + .transform_point2(get_arc_spiral_end_point(layer, document, viewport, TAU).expect("Failed to get endpoints")) + .distance(DVec2::ZERO) + } else { + viewport + .inverse() + .transform_point2(get_arc_spiral_end_point(layer, document, viewport, 0.).expect("Failed to get endpoints")) + .distance(DVec2::ZERO) + }; + overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); } - } - TightnessGizmoType::DashLines => { - if let Some((start, end)) = self.gizmo_line_points { - overlay_context.dashed_line(start, end, None, None, Some(4.0), Some(4.0), Some(0.5)); - if self.spiral_slot == 0 { - let required_radius = if self.inner_radius > self.initial_outer_radius { - self.initial_outer_radius - } else { - self.inner_radius - }; - overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); - } - }; - } - TightnessGizmoType::None => {} - }, + }; + } + TightnessGizmoState::Inactive => {} } } - fn check_which_inter_segment(layer_mouse_position: DVec2, outer_radius: f64, turns: f64, inner_radius: f64, spiral_type: SpiralType, viewport: DAffine2) -> Option<(DVec2, DVec2, i32)> { + fn check_which_inter_segment( + layer_mouse_position: DVec2, + inner_radius: f64, + outer_radius: f64, + turns: f64, + start_angle: f64, + spiral_type: SpiralType, + viewport: DAffine2, + ) -> Option<(DVec2, DVec2, i32)> { let center = DVec2::ZERO; - let mut angle = layer_mouse_position.angle_to(DVec2::X).rem_euclid(TAU); + let angle = layer_mouse_position.angle_to(DVec2::X).rem_euclid(TAU); + let start_angle_rad = start_angle.to_radians(); + let spiral_theta = (angle - start_angle_rad).rem_euclid(TAU); let is_reversed = inner_radius > outer_radius; let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); @@ -163,54 +138,52 @@ impl TightnessGizmo { let viewport_mouse = viewport.transform_point2(layer_mouse_position); let viewport_center = viewport.transform_point2(center); - // Compute spiral endpoints at θ = 0 and θ = max let spiral_outer = spiral_point(max_theta, inner_radius, b, spiral_type); - let spiral_inner = spiral_point(0., inner_radius, b, spiral_type); - let viewport_outer = viewport.transform_point2(spiral_outer); - let viewport_inner = viewport.transform_point2(spiral_inner); - - let smaller_radius = inner_radius.min(outer_radius); - let adjusted_angle = if is_reversed { max_theta - (TAU - angle) } else { angle }; + let spiral_inner = spiral_point(0.0, inner_radius, b, spiral_type); + let viewport_outer = viewport.transform_point2(Self::rotate_point(spiral_outer, start_angle_rad)); + let viewport_inner = viewport.transform_point2(Self::rotate_point(spiral_inner, start_angle_rad)); let required_endpoint = if is_reversed { viewport_inner } else { viewport_outer }; + let mouse_distance = viewport_mouse.distance(viewport_center); + let max_radius = required_endpoint.distance(viewport_center); - // Reject if mouse is beyond spiral's radial extent - if viewport_mouse.distance(viewport_center) > required_endpoint.distance(viewport_center) { + if mouse_distance > max_radius { return None; } - let mouse_distance = viewport_mouse.distance(viewport_center); let mut segment_index = 0; - // First segment: from center to first spiral point at θ = adjusted_angle + // First segment: from center to first spiral point at spiral_theta { - let spiral_end = spiral_point(adjusted_angle, inner_radius, b, spiral_type); - let first_point = viewport.transform_point2(spiral_end); + let spiral_end = spiral_point(spiral_theta, inner_radius, b, spiral_type); + let first_point = viewport.transform_point2(Self::rotate_point(spiral_end, start_angle_rad)); + let r_end = first_point.distance(viewport_center); if mouse_distance <= r_end { - let direction = DVec2::new(adjusted_angle.cos(), -adjusted_angle.sin()); - return Some((viewport.transform_point2(smaller_radius.max(5.) * direction), first_point, segment_index)); + let direction = DVec2::new(angle.cos(), -angle.sin()); + let inner_point = viewport.transform_point2(inner_radius.max(5.0) * direction); + return Some((inner_point, first_point, segment_index)); } segment_index += 1; } - // Loop through each full turn segment along the spiral ray - let mut base_theta = adjusted_angle; - while if is_reversed { base_theta >= 0. } else { base_theta <= max_theta } { + // Remaining segments: full spiral loops + let mut base_theta = spiral_theta; + while if is_reversed { base_theta >= 0.0 } else { base_theta <= max_theta } { let theta_start = base_theta; let theta_end = if is_reversed { base_theta - TAU } else { base_theta + TAU }; - if (!is_reversed && theta_end > max_theta) || (is_reversed && theta_end < 0.) { + if (!is_reversed && theta_end > max_theta) || (is_reversed && theta_end < 0.0) { break; } let spiral_start = spiral_point(theta_start, inner_radius, b, spiral_type); let spiral_end = spiral_point(theta_end, inner_radius, b, spiral_type); - let viewport_start = viewport.transform_point2(spiral_start); - let viewport_end = viewport.transform_point2(spiral_end); + let viewport_start = viewport.transform_point2(Self::rotate_point(spiral_start, start_angle_rad)); + let viewport_end = viewport.transform_point2(Self::rotate_point(spiral_end, start_angle_rad)); let r_start = viewport_start.distance(viewport_center); let r_end = viewport_end.distance(viewport_center); @@ -221,20 +194,37 @@ impl TightnessGizmo { } base_theta = if is_reversed { base_theta - TAU } else { base_theta + TAU }; - segment_index += 1; } None } - pub fn calculate_updated_dash_lines(&self, inner_radius: f64, outer_radius: f64, turns: f64, spiral_type: SpiralType, viewport: DAffine2, drag_start: DVec2, reversed: bool) -> (DVec2, DVec2) { - let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let max_theta = turns * TAU; - let base_angle = if reversed { max_theta - (TAU - self.angle) } else { self.angle }; - let smaller_radius = inner_radius.min(outer_radius); + fn rotate_point(p: DVec2, angle: f64) -> DVec2 { + let cos_a = angle.cos(); + let sin_a = angle.sin(); + DVec2::new(p.x * cos_a - p.y * sin_a, p.x * sin_a + p.y * cos_a) + } - let center = DVec2::ZERO; + pub fn calculate_updated_dash_lines( + &self, + inner_radius: f64, + outer_radius: f64, + turns: f64, + start_angle: f64, + spiral_type: SpiralType, + viewport: DAffine2, + _drag_start: DVec2, + reversed: bool, + ) -> (DVec2, DVec2) { + let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); + let max_theta = turns * TAU + start_angle.to_radians(); + let base_angle = if reversed { + max_theta - (TAU - self.angle - start_angle.to_radians()) + } else { + self.angle - start_angle.to_radians() + }; + let smaller_radius = inner_radius.min(outer_radius); let (start_point, end_point) = if self.spiral_slot == 0 { ( @@ -287,7 +277,7 @@ impl TightnessGizmo { let reversed = self.inner_radius > self.initial_outer_radius; self.previous_mouse = input.mouse.position; - let (a, outer_radius, turns, _) = extract_arc_or_log_spiral_parameters(layer, document).expect("Failed to get archimedean spiral inner radius"); + let (a, outer_radius, turns, start_angle) = extract_arc_or_log_spiral_parameters(layer, document).expect("Failed to get archimedean spiral inner radius"); let (new_inner_radius, turns, new_outer_radius) = match self.spiral_type { SpiralType::Archimedean => { if reversed { @@ -305,8 +295,7 @@ impl TightnessGizmo { } }; - let b = calculate_b(new_inner_radius, turns, new_outer_radius, self.spiral_type); - self.gizmo_line_points = Some(self.calculate_updated_dash_lines(new_inner_radius, new_outer_radius, turns, self.spiral_type, viewport_transform, drag_start, reversed)); + self.gizmo_line_points = Some(self.calculate_updated_dash_lines(new_inner_radius, new_outer_radius, turns, start_angle, self.spiral_type, viewport_transform, drag_start, reversed)); let (index, new_radius) = if reversed { (SPIRAL_INNER_RADIUS_INDEX, new_inner_radius) @@ -320,27 +309,6 @@ impl TightnessGizmo { }); } - pub fn update_outer_radius_via_circle(&mut self, node_id: NodeId, viewport_transform: DAffine2, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { - let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); - let net_radius = current_mouse_layer.distance(DVec2::ZERO); - - let net_radius = match self.spiral_type { - SpiralType::Archimedean => net_radius.max(0.), - SpiralType::Logarithmic => net_radius.max(0.001), - }; - - let index = if self.initial_outer_radius > self.inner_radius { - SPIRAL_OUTER_RADIUS_INDEX - } else { - SPIRAL_INNER_RADIUS_INDEX - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, index), - input: NodeInput::value(TaggedValue::F64(net_radius), false), - }); - } - pub fn update_outer_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { let Some(layer) = self.layer else { return; @@ -352,11 +320,7 @@ impl TightnessGizmo { let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - match &self.gizmo_type { - TightnessGizmoType::Circle => self.update_outer_radius_via_circle(node_id, viewport_transform, input, responses), - TightnessGizmoType::DashLines => self.update_outer_radius_via_dashed_lines(layer, node_id, viewport_transform, document, input, responses, drag_start), - TightnessGizmoType::None => {} - } + self.update_outer_radius_via_dashed_lines(layer, node_id, viewport_transform, document, input, responses, drag_start); responses.add(NodeGraphMessage::RunDocumentGraph); } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs index 9bee1a1cb3..17d522f374 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs @@ -1,4 +1,4 @@ -use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX}; +use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_START_ANGLE, SPIRAL_TURNS_INDEX}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -9,7 +9,7 @@ use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPre 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::{ - calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, + calculate_b, extract_arc_or_log_spiral_parameters, get_arc_or_log_spiral_endpoints, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, }; use glam::DVec2; use graph_craft::document::NodeInput; @@ -18,6 +18,14 @@ use graphene_std::vector::misc::SpiralType; use std::collections::VecDeque; use std::f64::consts::TAU; +#[derive(Clone, Debug, Default, PartialEq)] +pub enum GizmoType { + #[default] + None, + Start, + End, +} + #[derive(Clone, Debug, Default, PartialEq)] pub enum SpiralTurnsState { #[default] @@ -34,8 +42,10 @@ pub struct SpiralTurns { initial_outer_radius: f64, initial_inner_radius: f64, initial_b: f64, + initial_start_angle: f64, previous_mouse_position: DVec2, total_angle_delta: f64, + gizmo_type: GizmoType, spiral_type: SpiralType, } @@ -43,6 +53,7 @@ impl SpiralTurns { pub fn cleanup(&mut self) { self.handle_state = SpiralTurnsState::Inactive; self.total_angle_delta = 0.; + self.gizmo_type = GizmoType::None; self.layer = None; } @@ -58,14 +69,26 @@ impl SpiralTurns { self.handle_state == SpiralTurnsState::Dragging } - pub fn store_initial_parameters(&mut self, layer: LayerNodeIdentifier, a: f64, turns: f64, outer_radius: f64, mouse_position: DVec2, spiral_type: SpiralType) { + pub fn store_initial_parameters( + &mut self, + layer: LayerNodeIdentifier, + a: f64, + turns: f64, + outer_radius: f64, + mouse_position: DVec2, + start_angle: f64, + gizmo_type: GizmoType, + spiral_type: SpiralType, + ) { self.layer = Some(layer); self.initial_turns = turns; self.initial_b = calculate_b(a, turns, outer_radius, spiral_type); self.initial_inner_radius = a; self.initial_outer_radius = outer_radius; + self.initial_start_angle = start_angle; self.previous_mouse_position = mouse_position; self.spiral_type = spiral_type; + self.gizmo_type = gizmo_type; self.update_state(SpiralTurnsState::Hover); } @@ -75,12 +98,21 @@ impl SpiralTurns { match &self.handle_state { SpiralTurnsState::Inactive => { // Archimedean - if let Some(((inner_radius, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + if let Some(((inner_radius, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let end_point = viewport.transform_point2(spiral_point(turns * TAU, inner_radius, b, spiral_type)); + let end_point = viewport.transform_point2(spiral_point(turns * TAU + start_angle.to_radians(), inner_radius, b, spiral_type)); + let start_point = viewport.transform_point2(spiral_point(0. + start_angle.to_radians(), inner_radius, b, spiral_type)); + if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { - self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, spiral_type); + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, start_angle, GizmoType::End, spiral_type); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + return; + } + + if mouse_position.distance(start_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, start_angle, GizmoType::Start, spiral_type); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + return; } } } @@ -97,15 +129,15 @@ impl SpiralTurns { // Is true only when hovered over the gizmo let selected = self.layer.is_some(); - if let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, TAU) { - overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED)); - return; + let angle = match self.gizmo_type { + GizmoType::End => TAU, + GizmoType::Start => 0., + GizmoType::None => return, }; - if let Some(endpoint) = get_log_spiral_end_point(layer, document, viewport, TAU) { + if let Some(endpoint) = get_arc_or_log_spiral_endpoints(layer, document, viewport, angle) { overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED)); - return; - }; + } } } } @@ -132,22 +164,46 @@ impl SpiralTurns { // Calculate the new outer radius based on spiral type and turn change let outer_radius_change = match self.spiral_type { SpiralType::Archimedean => turns_delta * (self.initial_b) * TAU, - SpiralType::Logarithmic => self.initial_inner_radius * (self.initial_b * TAU * turns_delta).exp(), + SpiralType::Logarithmic => self.initial_outer_radius * ((self.initial_b * TAU * turns_delta).exp() - 1.), }; let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { return; }; - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), - input: NodeInput::value(TaggedValue::F64(self.initial_turns + turns_delta), false), - }); + match self.gizmo_type { + GizmoType::Start => { + let sign = total_delta.signum() * -1.; + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_START_ANGLE), + input: NodeInput::value(TaggedValue::F64(self.initial_start_angle + total_delta), false), + }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), - input: NodeInput::value(TaggedValue::F64(self.initial_outer_radius + outer_radius_change), false), - }); + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_turns + turns_delta * sign), false), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_outer_radius + outer_radius_change * sign), false), + }); + } + GizmoType::End => { + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_turns + turns_delta), false), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_outer_radius + outer_radius_change), false), + }); + } + GizmoType::None => { + return; + } + } responses.add(NodeGraphMessage::RunDocumentGraph); 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 575a54b2a7..86db9c796b 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -1,5 +1,5 @@ use super::ShapeToolData; -use crate::consts::{SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_START_ANGLE, SPIRAL_TURNS_INDEX}; +use crate::consts::{SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_START_ANGLE, SPIRAL_TURNS_INDEX, SPIRAL_TYPE_INDEX}; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -237,42 +237,42 @@ pub fn extract_star_parameters(layer: Option, document: &Do pub fn extract_arc_or_log_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(f64, f64, f64, f64)> { let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; - let Some(spiral_type) = get_spiral_type(layer, document) else { + let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns)), Some(&TaggedValue::F64(start_angle))) = ( + node_inputs.get(SPIRAL_INNER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_START_ANGLE)?.as_value(), + ) else { return None; }; - if spiral_type == SpiralType::Logarithmic { - let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns)), Some(&TaggedValue::F64(start_angle))) = ( - node_inputs.get(SPIRAL_INNER_RADIUS_INDEX)?.as_value(), - node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), - node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), - node_inputs.get(SPIRAL_START_ANGLE)?.as_value(), - ) else { - return None; - }; - - return Some((inner_radius, tightness, turns, start_angle)); - } - - None + return Some((inner_radius, tightness, turns, start_angle)); } pub fn get_spiral_type(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option { let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; - let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(1).expect("Failed to get Spiral Type").as_value() else { + let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(SPIRAL_TYPE_INDEX).expect("Failed to get Spiral Type").as_value() else { return None; }; Some(spiral_type) } +pub fn get_arc_or_log_spiral_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { + let Some(spiral_type) = get_spiral_type(layer, document) else { return None }; + return match spiral_type { + SpiralType::Archimedean => get_arc_spiral_end_point(layer, document, viewport, theta), + SpiralType::Logarithmic => get_log_spiral_end_point(layer, document, viewport, theta), + }; +} + pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { - let Some((a, outer_radius, turns, _)) = extract_arc_or_log_spiral_parameters(layer, document) else { + let Some((a, outer_radius, turns, start_angle)) = extract_arc_or_log_spiral_parameters(layer, document) else { return None; }; - let theta = turns * theta; + let theta = turns * theta + start_angle.to_radians(); let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); let r = a + b * theta; @@ -280,28 +280,27 @@ pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentM } pub fn get_log_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { - let Some((_, outer_radius, turns, _)) = extract_arc_or_log_spiral_parameters(layer, document) else { + let Some((a, outer_radius, turns, start_angle)) = extract_arc_or_log_spiral_parameters(layer, document) else { return None; }; - Some(viewport.transform_point2(outer_radius * DVec2::new((turns * theta).cos(), -(turns * theta).sin()))) + let theta = turns * theta + start_angle.to_radians(); + let b = calculate_b(a, turns, outer_radius, SpiralType::Logarithmic); + let r = a * (b * theta).exp(); + Some(viewport.transform_point2(DVec2::new(r * theta.cos(), -r * theta.sin()))) } pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 { + let total_theta = turns * TAU; match spiral_type { - SpiralType::Archimedean => { - let total_theta = turns * TAU; - (outer_radius - a) / total_theta - } - SpiralType::Logarithmic => { - let total_theta = turns * TAU; - ((outer_radius.abs() / a).ln()) / total_theta - } + SpiralType::Archimedean => (outer_radius - a) / total_theta, + SpiralType::Logarithmic => ((outer_radius.abs() / a).ln()) / total_theta, } } /// Returns a point on the given spiral type at angle `theta`. pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 { + let theta = theta; match spiral_type { SpiralType::Archimedean => archimedean_spiral_point(theta, a, b), SpiralType::Logarithmic => log_spiral_point(theta, a, b), diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index b1f3b7c927..d56a67cce9 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -41,9 +41,9 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) { - self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); + // self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); self.turns_handle.handle_actions(selected_spiral_layer, mouse_position, document, responses); - self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); + // self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); } fn handle_click(&mut self) { @@ -196,6 +196,8 @@ impl Spiral { SpiralType::Logarithmic => (dragged_distance).max(0.1), }; + let angle = ipp.mouse.position.angle_to(DVec2::X); + responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., viewport_drag_start), @@ -228,3 +230,7 @@ impl Spiral { } } } + +pub fn calculate_circle_point(theta: f64, radius: f64) -> DVec2 { + radius * DVec2::new(theta.cos(), -theta.sin()) +}