diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 58585e2dab..1a95aa2015 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -150,3 +150,9 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15; // INPUT pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500; + +/// SPIRAL NODE INPUT INDICES +pub const SPIRAL_TYPE_INDEX: usize = 1; +pub const SPIRAL_INNER_RADIUS: usize = 2; +pub const SPIRAL_OUTER_RADIUS_INDEX: usize = 3; +pub const SPIRAL_TURNS_INDEX: usize = 4; diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 48b91e818d..9f27f5ed5b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1227,28 +1227,25 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon match spiral_type { SpiralType::Archimedean => { let inner_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.)), + widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")), }; - let tightness = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, TightnessInput::INDEX, true, context), NumberInput::default().unit(" px")), + let outer_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().unit(" px")), }; - widgets.extend([inner_radius, tightness]); + widgets.extend([inner_radius, outer_radius]); } SpiralType::Logarithmic => { - let start_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, StartRadiusInput::INDEX, true, context), NumberInput::default().min(0.)), + let inner_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")), }; - let growth = LayoutGroup::Row { - widgets: number_widget( - ParameterWidgetsInfo::new(node_id, GrowthInput::INDEX, true, context), - NumberInput::default().max(0.5).min(0.1).increment_behavior(NumberInputIncrementBehavior::Add).increment_step(0.01), - ), + let outer_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().min(0.1).unit(" px")), }; - widgets.extend([start_radius, growth]); + widgets.extend([inner_radius, outer_radius]); } } } 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..0fc0de9d1f 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -6,6 +6,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; 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::spiral_shape::SpiralGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; use glam::DVec2; use std::collections::VecDeque; @@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers { None, Star(StarGizmoHandler), Polygon(PolygonGizmoHandler), + Spiral(SpiralGizmoHandler), } impl ShapeGizmoHandlers { @@ -32,15 +34,17 @@ impl ShapeGizmoHandlers { match self { Self::Star(_) => "star", Self::Polygon(_) => "polygon", + Self::Spiral(_) => "spiral", Self::None => "none", } } /// Dispatches interaction state updates to the corresponding shape-specific handler. - pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { 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::Star(h) => h.handle_state(layer, mouse_position, document, input, responses), + Self::Polygon(h) => h.handle_state(layer, mouse_position, document, input, responses), + Self::Spiral(h) => h.handle_state(layer, mouse_position, document, input, 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::Spiral(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::Spiral(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::Spiral(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::Spiral(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::Spiral(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::Spiral(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -147,6 +157,11 @@ impl GizmoManager { return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default())); } + // Spiral + if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Spiral(SpiralGizmoHandler::default())); + } + None } @@ -158,12 +173,12 @@ impl GizmoManager { /// Called every frame to check selected layers and update the active shape gizmo, if hovered. /// /// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing. - pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec)> = Vec::new(); for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) { if let Some(mut handler) = Self::detect_shape_handler(layer, document) { - handler.handle_state(layer, mouse_position, document, responses); + handler.handle_state(layer, mouse_position, document, input, responses); let is_hovered = handler.is_any_gizmo_hovered(); if is_hovered { 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..857d8d9e22 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 spiral_turns_handle; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs index 3995a1f401..fa49c3c0f2 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs @@ -41,7 +41,7 @@ impl NumberOfPointsDial { self.handle_state = state; } - pub fn is_hovering(&self) -> bool { + pub fn hovered(&self) -> bool { self.handle_state == NumberOfPointsDialState::Hover } @@ -189,8 +189,8 @@ impl NumberOfPointsDial { } pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { - let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start); - let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum(); + let delta = input.mouse.position - drag_start; + let sign = (input.mouse.position.x - drag_start.x).signum(); let net_delta = (delta.length() / 25.).round() * sign; let Some(layer) = self.layer else { return }; 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..95ab3815f1 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 @@ -426,14 +426,12 @@ impl PointRadiusHandle { }; let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - let document_transform = document.network_interface.document_metadata().transform_to_document(layer); - let center = viewport_transform.transform_point2(DVec2::ZERO); let radius_index = self.radius_index; let original_radius = self.initial_radius; - let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - document_transform.inverse().transform_point2(drag_start); - let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center; + let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - viewport_transform.inverse().transform_point2(drag_start); + let radius = viewport_transform.inverse().transform_point2(drag_start); let projection = delta.project_onto(radius); let sign = radius.dot(delta).signum(); 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 new file mode 100644 index 0000000000..5d9bff779b --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs @@ -0,0 +1,163 @@ +use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX}; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::prelude::Responses; +use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage}; +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_spiral_parameters, extract_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, +}; +use glam::DVec2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::misc::SpiralType; +use std::collections::VecDeque; +use std::f64::consts::TAU; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum SpiralTurnsState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct SpiralTurns { + pub layer: Option, + pub handle_state: SpiralTurnsState, + initial_turns: f64, + initial_outer_radius: f64, + initial_inner_radius: f64, + initial_b: f64, + previous_mouse_position: DVec2, + total_angle_delta: f64, + spiral_type: SpiralType, +} + +impl SpiralTurns { + pub fn cleanup(&mut self) { + self.handle_state = SpiralTurnsState::Inactive; + self.total_angle_delta = 0.; + self.layer = None; + } + + pub fn update_state(&mut self, state: SpiralTurnsState) { + self.handle_state = state; + } + + pub fn hovered(&self) -> bool { + self.handle_state == SpiralTurnsState::Hover + } + + pub fn is_dragging(&self) -> bool { + 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) { + 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.previous_mouse_position = mouse_position; + self.spiral_type = spiral_type; + self.update_state(SpiralTurnsState::Hover); + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let viewport = document.metadata().transform_to_viewport(layer); + + match &self.handle_state { + SpiralTurnsState::Inactive => { + // Archimedean + if let Some(((inner_radius, outer_radius, turns), end_point)) = extract_arc_spiral_parameters(layer, document).zip(get_arc_spiral_end_point(layer, document, viewport, TAU)) { + if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, SpiralType::Archimedean); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } + } + + // Logarithmic + if let Some(((inner_radius, outer_radius, turns), end_point)) = extract_log_spiral_parameters(layer, document).zip(get_log_spiral_end_point(layer, document, viewport, TAU)) { + if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, SpiralType::Logarithmic); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } + } + } + SpiralTurnsState::Hover | SpiralTurnsState::Dragging => {} + } + } + + pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option, _shape_editor: &mut &mut ShapeState, _mouse_position: DVec2, overlay_context: &mut OverlayContext) { + match &self.handle_state { + SpiralTurnsState::Inactive | SpiralTurnsState::Hover | SpiralTurnsState::Dragging => { + let Some(layer) = layer.or(self.layer) else { return }; + let viewport = document.metadata().transform_to_viewport(layer); + + // 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; + }; + + if let Some(endpoint) = get_log_spiral_end_point(layer, document, viewport, TAU) { + overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED)); + return; + }; + } + } + } + + pub fn update_number_of_turns(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + let Some(layer) = self.layer else { + return; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + + let angle_delta = viewport + .inverse() + .transform_point2(input.mouse.position) + .angle_to(viewport.inverse().transform_point2(self.previous_mouse_position)) + .to_degrees(); + + // Increase the number of turns and outer radius in unison such that growth and tightness remain same + let total_delta = self.total_angle_delta + angle_delta; + + // Convert the total angle (in degrees) to number of full turns + let turns_delta = total_delta / 360.; + + // 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(), + }; + + 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), + }); + + 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::RunDocumentGraph); + + self.total_angle_delta += angle_delta; + self.previous_mouse_position = input.mouse.position; + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs index c37dcf2ed8..f8af8200e4 100644 --- a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs @@ -30,16 +30,23 @@ pub struct PolygonGizmoHandler { impl ShapeGizmoHandler for PolygonGizmoHandler { fn is_any_gizmo_hovered(&self) -> bool { - self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered() + self.number_of_points_dial.hovered() || self.point_radius_handle.hovered() } - fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + fn handle_state( + &mut self, + selected_star_layer: LayerNodeIdentifier, + mouse_position: DVec2, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) { self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses); self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses); } fn handle_click(&mut self) { - if self.number_of_points_dial.is_hovering() { + if self.number_of_points_dial.hovered() { self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging); return; } 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 7bdcadf382..aa75262e67 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::{SPIRAL_INNER_RADIUS, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_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; @@ -14,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::vector::click_target::ClickTargetType; -use graphene_std::vector::misc::dvec2_to_point; +use graphene_std::vector::misc::{SpiralType, dvec2_to_point}; use kurbo::{BezPath, PathEl, Shape}; use std::collections::VecDeque; use std::f64::consts::{PI, TAU}; @@ -81,7 +82,14 @@ pub trait ShapeGizmoHandler { /// Called every frame to update the gizmo's interaction state based on the mouse position and selection. /// /// This includes detecting hover states and preparing interaction flags or visual feedback (e.g., highlighting a hovered handle). - fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque); + fn handle_state( + &mut self, + selected_shape_layers: LayerNodeIdentifier, + mouse_position: DVec2, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ); /// Called when a mouse click occurs over the canvas and a gizmo handle is hovered. /// @@ -224,6 +232,109 @@ pub fn extract_star_parameters(layer: Option, document: &Do Some((sides, radius_1, radius_2)) } +/// Extract the node input values of Archimedean spiral. +/// Returns an option of (Inner radius, Outer radius, Turns, ). +pub fn extract_arc_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(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 { + return None; + }; + + if spiral_type == SpiralType::Archimedean { + let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns))) = ( + node_inputs.get(SPIRAL_INNER_RADIUS)?.as_value(), + node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), + ) else { + return None; + }; + + return Some((inner_radius, tightness, turns)); + } + + None +} + +/// Extract the node input values of Logarithmic spiral. +/// Returns an option of (Start radius, Outer radius, Turns, ). +pub fn extract_log_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(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 { + return None; + }; + + if spiral_type == SpiralType::Logarithmic { + let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns))) = ( + node_inputs.get(SPIRAL_INNER_RADIUS)?.as_value(), + node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), + ) else { + return None; + }; + + return Some((inner_radius, tightness, turns)); + } + + None +} + +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 { + return None; + }; + + Some(spiral_type) +} + +pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { + let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) else { + return None; + }; + + let theta = turns * theta; + let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); + let r = a + b * theta; + + Some(viewport.transform_point2(DVec2::new(r * theta.cos(), -r * theta.sin()))) +} + +pub fn get_log_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { + let Some((_start_radius, outer_radius, turns)) = extract_log_spiral_parameters(layer, document) else { + return None; + }; + + Some(viewport.transform_point2(outer_radius * DVec2::new((turns * theta).cos(), -(turns * theta).sin()))) +} + +pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 { + 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 + } + } +} + +/// Returns a point on an Archimedean spiral at angle `theta`. +pub fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a + b * theta; + DVec2::new(r * theta.cos(), -r * theta.sin()) +} + +/// Returns a point on a logarithmic spiral at angle `theta`. +pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a * (b * theta).exp(); // a * e^(bθ) + DVec2::new(r * theta.cos(), -r * theta.sin()) +} + /// Extract the node input values of Polygon. /// Returns an option of (sides, radius). pub fn extract_polygon_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(u32, f64)> { 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 0370c07ed8..60fcc395c3 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -1,33 +1,102 @@ use super::*; +use crate::consts::{SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX, SPIRAL_TYPE_INDEX}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::spiral_turns_handle::{SpiralTurns, SpiralTurnsState}; 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::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapTypeConfiguration}; use crate::messages::tool::tool_messages::shape_tool::ShapeOptionsUpdate; use crate::messages::tool::tool_messages::tool_prelude::*; use glam::DAffine2; -use graph_craft::document::NodeId; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::vector::misc::SpiralType; use std::collections::VecDeque; -use std::f64::consts::TAU; + +#[derive(Clone, Debug, Default)] +pub struct SpiralGizmoHandler { + turns_handle: SpiralTurns, +} + +impl ShapeGizmoHandler for SpiralGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + self.turns_handle.hovered() + } + + fn handle_state( + &mut self, + selected_spiral_layer: LayerNodeIdentifier, + _mouse_position: DVec2, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) { + self.turns_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); + } + + fn handle_click(&mut self) { + if self.turns_handle.hovered() { + self.turns_handle.update_state(SpiralTurnsState::Dragging); + } + } + + fn handle_update(&mut self, _drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + if self.turns_handle.is_dragging() { + self.turns_handle.update_number_of_turns(document, input, responses); + } + } + + fn overlays( + &self, + document: &DocumentMessageHandler, + selected_spiral_layer: Option, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + self.turns_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); + } + + fn dragging_overlays( + &self, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + if self.turns_handle.is_dragging() { + self.turns_handle.overlays(document, None, shape_editor, mouse_position, overlay_context); + } + } + + fn cleanup(&mut self) { + self.turns_handle.cleanup(); + } +} #[derive(Default)] pub struct Spiral; impl Spiral { pub fn create_node(spiral_type: SpiralType, turns: f64) -> NodeTemplate { + let inner_radius = match spiral_type { + SpiralType::Archimedean => 0., + SpiralType::Logarithmic => 0.1, + }; + let node_type = resolve_document_node_type("Spiral").expect("Spiral node can't be found"); node_type.node_template_input_override([ None, Some(NodeInput::value(TaggedValue::SpiralType(spiral_type), false)), - Some(NodeInput::value(TaggedValue::F64(0.001), false)), - Some(NodeInput::value(TaggedValue::F64(0.1), false)), - None, + Some(NodeInput::value(TaggedValue::F64(inner_radius), false)), Some(NodeInput::value(TaggedValue::F64(0.1), false)), Some(NodeInput::value(TaggedValue::F64(turns), false)), ]) @@ -54,11 +123,14 @@ impl Spiral { return; }; - let Some(&TaggedValue::F64(turns)) = node_inputs.get(6).unwrap().as_value() else { + let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(SPIRAL_TYPE_INDEX).unwrap().as_value() else { return; }; - Self::update_radius(node_id, dragged_distance, turns, responses); + let new_radius = match spiral_type { + SpiralType::Archimedean => dragged_distance, + SpiralType::Logarithmic => (dragged_distance).max(0.1), + }; responses.add(GraphOperationMessage::TransformSet { layer, @@ -66,38 +138,29 @@ impl Spiral { transform_in: TransformIn::Viewport, skip_rerender: false, }); - } - pub fn update_radius(node_id: NodeId, drag_length: f64, turns: f64, responses: &mut VecDeque) { - let archimedean_radius = drag_length / (turns * TAU); responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 5), - input: NodeInput::value(TaggedValue::F64(archimedean_radius), false), - }); - - // 0.2 is the default parameter - let factor = (0.2 * turns * TAU).exp(); - let logarithmic_radius = drag_length / factor; - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::F64(logarithmic_radius), false), + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(new_radius), false), }); } /// Updates the number of turns of a spiral node and recalculates its radius based on drag distance. /// Also updates the Shape Tool's turns UI widget to reflect the change. - pub fn update_turns(drag_start: DVec2, decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + pub fn update_turns(decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque) { let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { return; }; - let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral") else { return; }; - let Some(&TaggedValue::F64(n)) = node_inputs.get(6).unwrap().as_value() else { return }; + let Some(&TaggedValue::F64(n)) = node_inputs.get(SPIRAL_TURNS_INDEX).unwrap().as_value() else { + return; + }; let input: NodeInput; + let turns: f64; if decrease { turns = (n - 1.).max(1.); @@ -108,14 +171,10 @@ impl Spiral { input = NodeInput::value(TaggedValue::F64(turns), false); responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Turns(turns))); } - - let drag_length = drag_start.distance(ipp.mouse.position); - - Self::update_radius(node_id, drag_length, turns, responses); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 6), + input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), input, }); + responses.add(NodeGraphMessage::RunDocumentGraph); } } diff --git a/editor/src/messages/tool/common_functionality/shapes/star_shape.rs b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs index 653b22f3ba..5404ca75d1 100644 --- a/editor/src/messages/tool/common_functionality/shapes/star_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs @@ -25,16 +25,23 @@ pub struct StarGizmoHandler { impl ShapeGizmoHandler for StarGizmoHandler { fn is_any_gizmo_hovered(&self) -> bool { - self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered() + self.number_of_points_dial.hovered() || self.point_radius_handle.hovered() } - fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + fn handle_state( + &mut self, + selected_star_layer: LayerNodeIdentifier, + mouse_position: DVec2, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) { self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses); self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses); } fn handle_click(&mut self) { - if self.number_of_points_dial.is_hovering() { + if self.number_of_points_dial.hovered() { self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging); return; } diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 28eced2ad1..5e36aed2f8 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -51,7 +51,7 @@ impl Default for ShapeToolOptions { shape_type: ShapeType::Polygon, arc_type: ArcType::Open, spiral_type: SpiralType::Archimedean, - turns: 5., + turns: 3., } } } @@ -370,14 +370,14 @@ impl ShapeToolData { } } - fn increase_no_sides_turns(&self, document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, shape_type: ShapeType, responses: &mut VecDeque, decrease: bool) { + fn increase_no_sides_turns(&self, document: &DocumentMessageHandler, shape_type: ShapeType, responses: &mut VecDeque, decrease: bool) { if let Some(layer) = self.data.layer { match shape_type { ShapeType::Star | ShapeType::Polygon => { Polygon::update_sides(decrease, layer, document, responses); } ShapeType::Spiral => { - Spiral::update_turns(self.data.viewport_drag_start(document), decrease, layer, document, ipp, responses); + Spiral::update_turns(decrease, layer, document, responses); } _ => {} } @@ -425,7 +425,7 @@ impl Fsm for ShapeToolFsmState { let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds); if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) { - tool_data.gizmo_manger.handle_actions(mouse_position, document, responses); + tool_data.gizmo_manger.handle_actions(mouse_position, document, input, responses); tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context); } @@ -539,11 +539,11 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => { - tool_data.increase_no_sides_turns(document, input, tool_options.shape_type, responses, false); + tool_data.increase_no_sides_turns(document, tool_options.shape_type, responses, false); self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => { - tool_data.increase_no_sides_turns(document, input, tool_options.shape_type, responses, true); + tool_data.increase_no_sides_turns(document, tool_options.shape_type, responses, true); self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => { @@ -680,7 +680,7 @@ impl Fsm for ShapeToolFsmState { } (ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => { responses.add(DocumentMessage::StartTransaction); - tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, document, input, responses); + tool_data.gizmo_manger.handle_update(tool_data.data.viewport_drag_start(document), document, input, responses); responses.add(OverlaysMessage::Draw); diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index bf8d2ecc9a..1f5106eeda 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -1,7 +1,8 @@ use super::*; -use crate::utils::{format_point, spiral_arc_length, spiral_point, spiral_tangent, split_cubic_bezier}; -use crate::{BezierHandles, consts::*}; +use crate::utils::{calculate_b, format_point, spiral_arc_length, spiral_point, spiral_tangent}; +use crate::{BezierHandles, TValue, consts::*}; use glam::DVec2; +use std::f64::consts::TAU; use std::fmt::Write; /// Functionality relating to core `Subpath` operations, such as constructors and `iter`. @@ -271,14 +272,16 @@ impl Subpath { ) } - pub fn new_spiral(a: f64, b: f64, turns: f64, delta_theta: f64, spiral_type: SpiralType) -> Self { + pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, delta_theta: f64, spiral_type: SpiralType) -> Self { let mut manipulator_groups = Vec::new(); let mut prev_in_handle = None; let theta_end = turns * std::f64::consts::TAU; + let b = calculate_b(a, turns, outer_radius, spiral_type); + let mut theta = 0.0; while theta < theta_end { - let theta_next = theta + delta_theta; + let theta_next = f64::min(theta + delta_theta, theta_end); let p0 = spiral_point(theta, a, b, spiral_type); let p3 = spiral_point(theta_next, a, b, spiral_type); @@ -291,18 +294,13 @@ impl Subpath { let p1 = p0 + d * t0; let p2 = p3 - d * t1; - let is_last_segment = theta_next >= theta_end; - if is_last_segment { - let t = (theta_end - theta) / (theta_next - theta); // t in [0, 1] - let (trim_p0, trim_p1, trim_p2, trim_p3) = split_cubic_bezier(p0, p1, p2, p3, t); + manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1))); + prev_in_handle = Some(p2); - manipulator_groups.push(ManipulatorGroup::new(trim_p0, prev_in_handle, Some(trim_p1))); - prev_in_handle = Some(trim_p2); - manipulator_groups.push(ManipulatorGroup::new(trim_p3, prev_in_handle, None)); + // If final segment, end with anchor at theta_end + if (theta_next - theta_end).abs() < f64::EPSILON { + manipulator_groups.push(ManipulatorGroup::new(p3, prev_in_handle, None)); break; - } else { - manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1))); - prev_in_handle = Some(p2); } theta = theta_next; diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 179d744376..4762e7377a 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -1,6 +1,7 @@ use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; use crate::{ManipulatorGroup, SpiralType, Subpath}; use glam::{BVec2, DMat2, DVec2}; +use std::f64::consts::TAU; use std::fmt::Write; #[derive(Copy, Clone, PartialEq)] @@ -302,6 +303,19 @@ pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt: Ok(()) } +pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 { + 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 + } + } +} + /// 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 { match spiral_type { @@ -326,22 +340,6 @@ pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spira } } -/// Splits a cubic Bézier curve at parameter `t`, returning the first half. -pub fn split_cubic_bezier(p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64) -> (DVec2, DVec2, DVec2, DVec2) { - let p01 = p0.lerp(p1, t); - let p12 = p1.lerp(p2, t); - let p23 = p2.lerp(p3, t); - - let p012 = p01.lerp(p12, t); - let p123 = p12.lerp(p23, t); - - // final split point - let p0123 = p012.lerp(p123, t); - - // First half of the Bézier - (p0, p01, p012, p0123) -} - /// Returns a point on a logarithmic spiral at angle `theta`. pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { let r = a * (b * theta).exp(); // a * e^(bθ) @@ -360,7 +358,7 @@ pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 { let dx = r * (b * theta.cos() - theta.sin()); let dy = r * (b * theta.sin() + theta.cos()); - DVec2::new(dx, -dy).normalize() + DVec2::new(dx, -dy).normalize_or(DVec2::X) } /// Returns a point on an Archimedean spiral at angle `theta`. @@ -374,7 +372,7 @@ pub fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 { let r = a + b * theta; let dx = b * theta.cos() - r * theta.sin(); let dy = b * theta.sin() + r * theta.cos(); - DVec2::new(dx, -dy).normalize() + DVec2::new(dx, -dy).normalize_or(DVec2::X) } /// Computes arc length along an Archimedean spiral between two angles. diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 4774be4e10..e04d1546ae 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -79,24 +79,17 @@ fn spiral( _: impl Ctx, _primary: (), spiral_type: SpiralType, - #[default(0.5)] start_radius: f64, #[default(0.)] inner_radius: f64, - #[default(0.2)] growth: f64, - #[default(1.)] tightness: f64, - #[default(6)] turns: f64, - #[default(45.)] angle_offset: f64, + #[default(25)] outer_radius: f64, + #[default(5.)] turns: f64, + #[default(90.)] angle_offset: f64, ) -> VectorDataTable { - let (a, b) = match spiral_type { - SpiralType::Archimedean => (inner_radius, tightness), - SpiralType::Logarithmic => (start_radius, growth), - }; - let spiral_type = match spiral_type { SpiralType::Archimedean => bezier_rs::SpiralType::Archimedean, SpiralType::Logarithmic => bezier_rs::SpiralType::Logarithmic, }; - VectorDataTable::new(VectorData::from_subpath(Subpath::new_spiral(a, b, turns, angle_offset.to_radians(), spiral_type))) + VectorDataTable::new(VectorData::from_subpath(Subpath::new_spiral(inner_radius, outer_radius, turns, angle_offset.to_radians(), spiral_type))) } #[node_macro::node(category("Vector: Shape"))]