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/document_node_definitions.rs b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs index 3c85102098..c88094b7e4 100644 --- a/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs +++ b/editor/src/messages/portfolio/document/node_graph/document_node_definitions.rs @@ -1812,6 +1812,7 @@ fn static_node_properties() -> NodeProperties { map.insert("math_properties".to_string(), Box::new(node_properties::math_properties)); map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties)); map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties)); + map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties)); map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties)); map.insert( "identity_properties".to_string(), 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 9551ed2094..9f27f5ed5b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -23,9 +23,9 @@ use graphene_std::raster_types::{CPU, GPU, RasterDataTable}; use graphene_std::text::Font; use graphene_std::transform::{Footprint, ReferencePoint}; use graphene_std::vector::VectorDataTable; -use graphene_std::vector::misc::GridType; use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm}; use graphene_std::vector::misc::{CentroidType, PointSpacingType}; +use graphene_std::vector::misc::{GridType, SpiralType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops}; use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::{GraphicGroupTable, NodeInputDecleration}; @@ -1202,6 +1202,65 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte widgets } +pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec { + use graphene_std::vector::generator_nodes::spiral::*; + + let spiral_type = enum_choice::() + .for_socket(ParameterWidgetsInfo::new(node_id, SpiralTypeInput::INDEX, true, context)) + .property_row(); + + let mut widgets = vec![spiral_type]; + + let document_node = match get_document_node(node_id, context) { + Ok(document_node) => document_node, + Err(err) => { + log::error!("Could not get document node in exposure_properties: {err}"); + return Vec::new(); + } + }; + + let Some(spiral_type_input) = document_node.inputs.get(SpiralTypeInput::INDEX) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return vec![]; + }; + if let Some(&TaggedValue::SpiralType(spiral_type)) = spiral_type_input.as_non_exposed_value() { + 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.).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, outer_radius]); + } + SpiralType::Logarithmic => { + let inner_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")), + }; + + 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([inner_radius, outer_radius]); + } + } + } + + let turns = number_widget(ParameterWidgetsInfo::new(node_id, TurnsInput::INDEX, true, context), NumberInput::default().min(0.1)); + let angle_offset = number_widget( + ParameterWidgetsInfo::new(node_id, AngleOffsetInput::INDEX, true, context), + NumberInput::default().min(0.1).max(180.).unit("°"), + ); + + widgets.extend([LayoutGroup::Row { widgets: turns }, LayoutGroup::Row { widgets: angle_offset }]); + + widgets +} + pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points."; pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled)."; pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path."; 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/graph_modification_utils.rs b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs index 33c19d0c0d..da7bdb987f 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -352,6 +352,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_spiral_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option { + NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Spiral") +} + 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/mod.rs b/editor/src/messages/tool/common_functionality/shapes/mod.rs index 44f40b5982..2e406db583 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -3,6 +3,7 @@ pub mod line_shape; pub mod polygon_shape; pub mod rectangle_shape; pub mod shape_utility; +pub mod spiral_shape; pub mod star_shape; pub use super::shapes::ellipse_shape::Ellipse; 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 82dcf10cfc..f8af8200e4 100644 --- a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs @@ -11,9 +11,11 @@ use crate::messages::tool::common_functionality::gizmos::shape_gizmos::number_of use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandle; use crate::messages::tool::common_functionality::gizmos::shape_gizmos::point_radius_handle::PointRadiusHandleState; 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::shapes::shape_utility::polygon_outline; +use crate::messages::tool::tool_messages::shape_tool::ShapeOptionsUpdate; use crate::messages::tool::tool_messages::tool_prelude::*; use glam::DAffine2; use graph_craft::document::NodeInput; @@ -28,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; } @@ -148,4 +157,37 @@ impl Polygon { }); } } + + /// Updates the number of sides of a polygon or star node and syncs the Shape Tool UI widget accordingly. + /// Increases or decreases the side count based on user input, clamped to a minimum of 3. + pub fn update_sides(decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) else { + return; + }; + + let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs("Regular Polygon") + .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) + else { + return; + }; + + let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { return }; + + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1))); + + let input: NodeInput; + if decrease { + input = NodeInput::value(TaggedValue::U32((n - 1).max(3)), false); + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((n - 1).max(3)))); + } else { + input = NodeInput::value(TaggedValue::U32(n + 1), false); + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1))); + } + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input, + }); + } } 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 955984150b..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}; @@ -24,9 +25,10 @@ pub enum ShapeType { #[default] Polygon = 0, Star = 1, - Rectangle = 2, - Ellipse = 3, - Line = 4, + Spiral = 2, + Rectangle = 3, + Ellipse = 4, + Line = 5, } impl ShapeType { @@ -34,6 +36,7 @@ impl ShapeType { (match self { Self::Polygon => "Polygon", Self::Star => "Star", + Self::Spiral => "Spiral", Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", @@ -79,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. /// @@ -222,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 new file mode 100644 index 0000000000..60fcc395c3 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -0,0 +1,180 @@ +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::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::misc::SpiralType; +use std::collections::VecDeque; + +#[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(inner_radius), false)), + Some(NodeInput::value(TaggedValue::F64(0.1), false)), + Some(NodeInput::value(TaggedValue::F64(turns), false)), + ]) + } + + pub fn update_shape(document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, layer: LayerNodeIdentifier, shape_tool_data: &mut ShapeToolData, responses: &mut VecDeque) { + let viewport_drag_start = shape_tool_data.data.viewport_drag_start(document); + + let ignore = vec![layer]; + let snap_data = SnapData::ignore(document, ipp, &ignore); + let config = SnapTypeConfiguration::default(); + let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(ipp.mouse.position); + let snapped = shape_tool_data.data.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config); + let snapped_viewport_point = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); + shape_tool_data.data.snap_manager.update_indicator(snapped); + + let dragged_distance = (viewport_drag_start - snapped_viewport_point).length(); + + 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::SpiralType(spiral_type)) = node_inputs.get(SPIRAL_TYPE_INDEX).unwrap().as_value() else { + return; + }; + + let new_radius = match spiral_type { + SpiralType::Archimedean => dragged_distance, + SpiralType::Logarithmic => (dragged_distance).max(0.1), + }; + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., viewport_drag_start), + transform_in: TransformIn::Viewport, + skip_rerender: false, + }); + + responses.add(NodeGraphMessage::SetInput { + 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(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(SPIRAL_TURNS_INDEX).unwrap().as_value() else { + return; + }; + + let input: NodeInput; + + let turns: f64; + if decrease { + turns = (n - 1.).max(1.); + input = NodeInput::value(TaggedValue::F64(turns), false); + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Turns(turns))); + } else { + turns = n + 1.; + input = NodeInput::value(TaggedValue::F64(turns), false); + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Turns(turns))); + } + responses.add(NodeGraphMessage::SetInput { + 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 e4ba44b04a..5e36aed2f8 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -3,26 +3,24 @@ use crate::consts::{DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; 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::auto_panning::AutoPanning; use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType}; use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoManager; 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::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}; +use crate::messages::tool::common_functionality::shapes::spiral_shape::Spiral; use crate::messages::tool::common_functionality::shapes::star_shape::Star; use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle}; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration}; use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool}; use crate::messages::tool::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage}; -use graph_craft::document::value::TaggedValue; -use graph_craft::document::{NodeId, NodeInput}; +use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::renderer::Quad; -use graphene_std::vector::misc::ArcType; +use graphene_std::vector::misc::{ArcType, SpiralType}; use std::vec; #[derive(Default)] @@ -39,6 +37,8 @@ pub struct ShapeToolOptions { vertices: u32, shape_type: ShapeType, arc_type: ArcType, + spiral_type: SpiralType, + turns: f64, } impl Default for ShapeToolOptions { @@ -50,6 +50,8 @@ impl Default for ShapeToolOptions { vertices: 5, shape_type: ShapeType::Polygon, arc_type: ArcType::Open, + spiral_type: SpiralType::Archimedean, + turns: 3., } } } @@ -65,6 +67,8 @@ pub enum ShapeOptionsUpdate { Vertices(u32), ShapeType(ShapeType), ArcType(ArcType), + SpiralType(SpiralType), + Turns(f64), } #[impl_message(Message, ToolMessage, Shape)] @@ -101,6 +105,16 @@ fn create_sides_widget(vertices: u32) -> WidgetHolder { .widget_holder() } +fn create_turns_widget(turns: f64) -> WidgetHolder { + NumberInput::new(Some(turns)) + .label("Turns") + .min(0.5) + .max(1000.) + .mode(NumberInputMode::Increment) + .on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Turns(number_input.value.unwrap() as f64)).into()) + .widget_holder() +} + fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { let entries = vec![vec![ MenuListEntry::new("Polygon") @@ -109,6 +123,9 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { MenuListEntry::new("Star") .label("Star") .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), + MenuListEntry::new("Spiral") + .label("Spiral") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Spiral)).into()), ]]; DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() } @@ -123,6 +140,18 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder { .widget_holder() } +fn create_spiral_type_widget(spiral_type: SpiralType) -> WidgetHolder { + let entries = vec![vec![ + MenuListEntry::new("Archimedean") + .label("Archimedean") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::SpiralType(SpiralType::Archimedean)).into()), + MenuListEntry::new("Logarithmic") + .label("Logarithmic") + .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::SpiralType(SpiralType::Logarithmic)).into()), + ]]; + DropdownInput::new(entries).selected_index(Some(spiral_type as u32)).widget_holder() +} + impl LayoutHolder for ShapeTool { fn layout(&self) -> Layout { let mut widgets = vec![]; @@ -137,6 +166,13 @@ impl LayoutHolder for ShapeTool { } } + if self.options.shape_type == ShapeType::Spiral { + widgets.push(create_spiral_type_widget(self.options.spiral_type)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + widgets.push(create_turns_widget(self.options.turns)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + } + if self.options.shape_type != ShapeType::Line { widgets.append(&mut self.options.fill.create_widgets( "Fill", @@ -203,6 +239,12 @@ impl<'a> MessageHandler> for ShapeTo ShapeOptionsUpdate::ArcType(arc_type) => { self.options.arc_type = arc_type; } + ShapeOptionsUpdate::SpiralType(spiral_type) => { + self.options.spiral_type = spiral_type; + } + ShapeOptionsUpdate::Turns(turns) => { + self.options.turns = turns; + } } self.fsm_state.update_hints(responses); @@ -327,6 +369,22 @@ impl ShapeToolData { } } } + + 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(decrease, layer, document, responses); + } + _ => {} + } + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + } } impl Fsm for ShapeToolFsmState { @@ -367,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); } @@ -427,11 +485,24 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => { - responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(tool_options.vertices + 1))); + if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(tool_options.vertices + 1))); + } + + if matches!(tool_options.shape_type, ShapeType::Spiral) { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Turns(tool_options.turns + 1.))); + } + self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => { - responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)))); + if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)))); + } + + if matches!(tool_options.shape_type, ShapeType::Spiral) { + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Turns((tool_options.turns - 1.).max(1.)))); + } self } ( @@ -468,61 +539,11 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => { - if let Some(layer) = tool_data.data.layer { - let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) - else { - return self; - }; - - let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) - .find_node_inputs("Regular Polygon") - .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) - else { - return self; - }; - - let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { - return self; - }; - - responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1))); - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::U32(n + 1), false), - }); - responses.add(NodeGraphMessage::RunDocumentGraph); - } - + tool_data.increase_no_sides_turns(document, tool_options.shape_type, responses, false); self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => { - if let Some(layer) = tool_data.data.layer { - let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) - else { - return self; - }; - - let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) - .find_node_inputs("Regular Polygon") - .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) - else { - return self; - }; - - let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { - return self; - }; - - responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((n - 1).max(3)))); - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::U32((n - 1).max(3)), false), - }); - responses.add(NodeGraphMessage::RunDocumentGraph); - } - + tool_data.increase_no_sides_turns(document, tool_options.shape_type, responses, true); self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => { @@ -578,7 +599,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::Rectangle | ShapeType::Spiral => 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()); @@ -594,6 +615,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), ShapeType::Line => Line::create_node(document, tool_data.data.drag_start), + ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns), }; let nodes = vec![(NodeId(0), node)]; @@ -602,7 +624,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::Polygon | ShapeType::Star | ShapeType::Spiral => { responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -635,6 +657,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::Spiral => Spiral::update_shape(document, input, layer, tool_data, responses), } // Auto-panning @@ -657,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); @@ -813,6 +836,7 @@ impl Fsm for ShapeToolFsmState { responses.add(DocumentMessage::AbortTransaction); tool_data.data.cleanup(responses); tool_data.current_shape = shape; + responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(shape))); ShapeToolFsmState::Ready(shape) } @@ -837,6 +861,10 @@ impl Fsm for ShapeToolFsmState { ]), HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]), ], + ShapeType::Spiral => vec![ + HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Spiral")]), + HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Turns")]), + ], ShapeType::Ellipse => vec![HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"), HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(), @@ -867,6 +895,7 @@ impl Fsm for ShapeToolFsmState { HintInfo::keys([Key::Alt], "From Center"), HintInfo::keys([Key::Control], "Lock Angle"), ]), + _ => HintGroup(vec![]), }; common_hint_group.push(tool_hint_group); @@ -875,6 +904,10 @@ impl Fsm for ShapeToolFsmState { common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")])); } + if matches!(shape, ShapeType::Spiral) { + common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Turns")])); + } + HintData(common_hint_group) } ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![ diff --git a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs index b9b9880e98..acc0ed83a2 100644 --- a/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs +++ b/editor/src/messages/tool/transform_layer/transform_layer_message_handler.rs @@ -237,7 +237,7 @@ impl MessageHandler> for TransformLayer return; } - if !using_path_tool { + if !using_path_tool || !using_shape_tool { self.pivot_gizmo.recalculate_transform(document); *selected.pivot = self.pivot_gizmo.position(document); self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot); diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index a18550db6d..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::consts::*; -use crate::utils::format_point; +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,6 +272,43 @@ impl Subpath { ) } + 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 = 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); + let t0 = spiral_tangent(theta, a, b, spiral_type); + let t1 = spiral_tangent(theta_next, a, b, spiral_type); + + let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type); + let d = arc_len / 3.0; + + let p1 = p0 + d * t0; + let p2 = p3 - d * t1; + + manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1))); + prev_in_handle = Some(p2); + + // 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; + } + + theta = theta_next; + } + + Self::new(manipulator_groups, false) + } + /// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box. pub fn new_ellipse(corner1: DVec2, corner2: DVec2) -> Self { let size = (corner1 - corner2).abs(); diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index f0ef24dd7d..38a67a0836 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -144,3 +144,9 @@ pub enum ArcType { Closed, PieSlice, } + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum SpiralType { + Archimedean, + Logarithmic, +} diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 2e80b80e8e..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, Subpath}; +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,90 @@ 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 { + SpiralType::Archimedean => archimedean_spiral_point(theta, a, b), + SpiralType::Logarithmic => log_spiral_point(theta, a, b), + } +} + +/// Returns the tangent direction at angle `theta` for the given spiral type. +pub fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 { + match spiral_type { + SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b), + SpiralType::Logarithmic => log_spiral_tangent(theta, a, b), + } +} + +/// Computes arc length between two angles for the given spiral type. +pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 { + match spiral_type { + SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b), + SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b), + } +} + +/// 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()) +} + +/// Computes arc length along a logarithmic spiral between two angles. +pub fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 { + let factor = (1. + b * b).sqrt(); + (a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp()) +} + +/// Returns the tangent direction of a logarithmic spiral at angle `theta`. +pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a * (b * theta).exp(); + let dx = r * (b * theta.cos() - theta.sin()); + let dy = r * (b * theta.sin() + theta.cos()); + + DVec2::new(dx, -dy).normalize_or(DVec2::X) +} + +/// 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 the tangent direction of an Archimedean spiral at angle `theta`. +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_or(DVec2::X) +} + +/// Computes arc length along an Archimedean spiral between two angles. +pub fn archimedean_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 { + archimedean_spiral_arc_length_origin(theta_end, a, b) - archimedean_spiral_arc_length_origin(theta_start, a, b) +} + +/// Computes arc length from origin to a point on Archimedean spiral at angle `theta`. +pub fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 { + let r = a + b * theta; + let sqrt_term = (r * r + b * b).sqrt(); + (r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2.0 * b) +} + #[cfg(test)] mod tests { use super::*; diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 6211e2d1b1..e04d1546ae 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -2,6 +2,7 @@ use super::misc::{ArcType, AsU64, GridType}; use super::{PointId, SegmentId, StrokeId}; use crate::Ctx; use crate::registry::types::{Angle, PixelSize}; +use crate::vector::misc::SpiralType; use crate::vector::{HandleId, VectorData, VectorDataTable}; use bezier_rs::Subpath; use glam::DVec2; @@ -73,6 +74,24 @@ fn arc( ))) } +#[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))] +fn spiral( + _: impl Ctx, + _primary: (), + spiral_type: SpiralType, + #[default(0.)] inner_radius: f64, + #[default(25)] outer_radius: f64, + #[default(5.)] turns: f64, + #[default(90.)] angle_offset: f64, +) -> VectorDataTable { + 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(inner_radius, outer_radius, turns, angle_offset.to_radians(), spiral_type))) +} + #[node_macro::node(category("Vector: Shape"))] fn ellipse( _: impl Ctx, diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index 4196c45d7d..41a66764bf 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -96,3 +96,11 @@ pub fn point_to_dvec2(point: Point) -> DVec2 { pub fn dvec2_to_point(value: DVec2) -> Point { Point { x: value.x, y: value.y } } + +#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)] +#[widget(Dropdown)] +pub enum SpiralType { + #[default] + Archimedean, + Logarithmic, +} diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index c8c896290c..67018f4395 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -236,6 +236,7 @@ tagged_value! { ArcType(graphene_core::vector::misc::ArcType), MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm), PointSpacingType(graphene_core::vector::misc::PointSpacingType), + SpiralType(graphene_core::vector::misc::SpiralType), #[serde(alias = "LineCap")] StrokeCap(graphene_core::vector::style::StrokeCap), #[serde(alias = "LineJoin")]