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..48b91e818d 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,68 @@ 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.)), + }; + + let tightness = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, TightnessInput::INDEX, true, context), NumberInput::default().unit(" px")), + }; + + widgets.extend([inner_radius, tightness]); + } + SpiralType::Logarithmic => { + let start_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, StartRadiusInput::INDEX, true, context), NumberInput::default().min(0.)), + }; + + 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), + ), + }; + + widgets.extend([start_radius, growth]); + } + } + } + + 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/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..c37dcf2ed8 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; @@ -148,4 +150,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..7bdcadf382 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -24,9 +24,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 +35,7 @@ impl ShapeType { (match self { Self::Polygon => "Polygon", Self::Star => "Star", + Self::Spiral => "Spiral", Self::Rectangle => "Rectangle", Self::Ellipse => "Ellipse", Self::Line => "Line", 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..0370c07ed8 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -0,0 +1,121 @@ +use super::*; +use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; +use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; +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(Default)] +pub struct Spiral; + +impl Spiral { + pub fn create_node(spiral_type: SpiralType, turns: f64) -> NodeTemplate { + 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(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::F64(turns)) = node_inputs.get(6).unwrap().as_value() else { + return; + }; + + Self::update_radius(node_id, dragged_distance, turns, responses); + + responses.add(GraphOperationMessage::TransformSet { + layer, + transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., viewport_drag_start), + 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), + }); + } + + /// 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) { + 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 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))); + } + + 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, + }); + } +} diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index e4ba44b04a..28eced2ad1 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: 5., } } } @@ -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, ipp: &InputPreprocessorMessageHandler, 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); + } + _ => {} + } + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + } } impl Fsm for ShapeToolFsmState { @@ -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, input, 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, input, 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 @@ -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..bf8d2ecc9a 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -1,6 +1,6 @@ use super::*; -use crate::consts::*; -use crate::utils::format_point; +use crate::utils::{format_point, spiral_arc_length, spiral_point, spiral_tangent, split_cubic_bezier}; +use crate::{BezierHandles, consts::*}; use glam::DVec2; use std::fmt::Write; @@ -271,6 +271,46 @@ impl Subpath { ) } + pub fn new_spiral(a: f64, b: 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 mut theta = 0.0; + while theta < theta_end { + let theta_next = theta + delta_theta; + + 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; + + 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(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)); + break; + } else { + manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1))); + prev_in_handle = Some(p2); + } + + 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..179d744376 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -1,5 +1,5 @@ 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::fmt::Write; @@ -302,6 +302,93 @@ pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt: Ok(()) } +/// 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), + } +} + +/// 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θ) + 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() +} + +/// 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() +} + +/// 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..4774be4e10 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,31 @@ fn arc( ))) } +#[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))] +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, +) -> 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))) +} + #[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")]