From 485152bf8d39fa6b0a9c8a6c02fb069812d2eb69 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 <142654792+0SlowPoke0@users.noreply.github.com> Date: Tue, 9 Sep 2025 03:07:43 +0530 Subject: [PATCH] Add "Spiral" to the Shape tool and as a new node (#2803) * made spiral node * number of turns in decimal and arc-angle implementation * logarithmic spiral * unified log and arc spiral into spiral node * add spiral shape in shape tool * fix min value and degree unit * make it compile * updated the api * changed the function_name * [/] to update the turns widget in shape tool * Code review --------- Co-authored-by: Keavon Chambers --- .../node_graph/document_node_definitions.rs | 2 + .../document/node_graph/node_properties.rs | 62 ++++++++- .../graph_modification_utils.rs | 4 + .../tool/common_functionality/shapes/mod.rs | 1 + .../shapes/polygon_shape.rs | 48 +++---- .../shapes/shape_utility.rs | 2 + .../shapes/spiral_shape.rs | 116 ++++++++++++++++ .../messages/tool/tool_messages/shape_tool.rs | 128 ++++++++++++++++-- .../transform_layer_message_handler.rs | 2 +- node-graph/gcore/src/subpath/core.rs | 124 ++++++++++++++++- .../gcore/src/vector/generator_nodes.rs | 23 +++- node-graph/gcore/src/vector/misc.rs | 8 ++ node-graph/gcore/src/vector/vector_nodes.rs | 5 +- node-graph/graph-craft/src/document/value.rs | 1 + .../introduction/features-and-limitations.md | 1 + 15 files changed, 483 insertions(+), 44 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs 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 f09a847ec..a76bbccbe 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 @@ -1776,6 +1776,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( "monitor_properties".to_string(), @@ -2394,6 +2395,7 @@ impl DocumentNodeDefinition { /// `input_override` does not have to be the correct length. pub fn node_template_input_override(&self, input_override: impl IntoIterator>) -> NodeTemplate { let mut template = self.node_template.clone(); + // TODO: Replace the .enumerate() with changing the iterator to take a tuple of (index, input) so the user is forced to provide the correct index input_override.into_iter().enumerate().for_each(|(index, input_override)| { if let Some(input_override) = input_override { // Only value inputs can be overridden, since node inputs change graph structure and must be handled by the network interface 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 24328f353..0dd2d5906 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -23,7 +23,7 @@ use graphene_std::raster::{ use graphene_std::table::{Table, TableRow}; use graphene_std::text::{Font, TextAlign}; use graphene_std::transform::{Footprint, ReferencePoint, Transform}; -use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType}; +use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; pub(crate) fn string_properties(text: &str) -> Vec { @@ -1286,6 +1286,66 @@ 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 turns = number_widget(ParameterWidgetsInfo::new(node_id, TurnsInput::INDEX, true, context), NumberInput::default().min(0.1)); + let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, StartAngleInput::INDEX, true, context), NumberInput::default().unit("°")); + + let mut widgets = vec![spiral_type, LayoutGroup::Row { widgets: turns }, LayoutGroup::Row { widgets: start_angle }]; + + 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 angular_resolution = number_widget( + ParameterWidgetsInfo::new(node_id, AngularResolutionInput::INDEX, true, context), + NumberInput::default().min(1.).max(180.).unit("°"), + ); + + widgets.push(LayoutGroup::Row { widgets: angular_resolution }); + + 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 7624a74d2..3227832c9 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -363,6 +363,10 @@ pub fn get_arc_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInt NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arc") } +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 683d2d627..5031a6224 100644 --- a/editor/src/messages/tool/common_functionality/shapes/mod.rs +++ b/editor/src/messages/tool/common_functionality/shapes/mod.rs @@ -6,6 +6,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 51869d182..895bb4a21 100644 --- a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs @@ -158,34 +158,34 @@ impl Polygon { } } - pub fn increase_decrease_sides(increase: bool, document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, responses: &mut VecDeque) { - if let Some(layer) = shape_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; - }; + /// 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 decrease_or_increase_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(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; - }; + let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { + return; + }; - let new_dimension = if increase { n + 1 } else { (n - 1).max(3) }; + let new_dimension = if decrease { (n - 1).max(3) } else { n + 1 }; - responses.add(ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::Vertices(new_dimension), - }); + responses.add(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::Vertices(new_dimension), + }); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 1), - input: NodeInput::value(TaggedValue::U32(new_dimension), false), - }); - responses.add(NodeGraphMessage::RunDocumentGraph); - } + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, 1), + input: NodeInput::value(TaggedValue::U32(new_dimension), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); } } diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 0f40d4435..fb7d913fa 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -29,6 +29,7 @@ pub enum ShapeType { Star, Circle, Arc, + Spiral, Grid, Rectangle, Ellipse, @@ -43,6 +44,7 @@ impl ShapeType { Self::Circle => "Circle", Self::Arc => "Arc", Self::Grid => "Grid", + 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 000000000..abddfd7ab --- /dev/null +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -0,0 +1,116 @@ +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::{self, 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::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::NodeInputDecleration; +use graphene_std::vector::misc::SpiralType; +use std::collections::VecDeque; + +#[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(turns), false)), + Some(NodeInput::value(TaggedValue::F64(0.), false)), + Some(NodeInput::value(TaggedValue::F64(inner_radius), false)), + Some(NodeInput::value(TaggedValue::F64(0.1), false)), + Some(NodeInput::value(TaggedValue::F64(90.), false)), + ]) + } + + pub fn update_shape(document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, layer: LayerNodeIdentifier, shape_tool_data: &mut ShapeToolData, responses: &mut VecDeque) { + use graphene_std::vector::generator_nodes::spiral::*; + + 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(SpiralTypeInput::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, OuterRadiusInput::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) { + use graphene_std::vector::generator_nodes::spiral::*; + + let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral") else { + return; + }; + + let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { + return; + }; + + let Some(&TaggedValue::F64(mut turns)) = node_inputs.get(TurnsInput::INDEX).unwrap().as_value() else { + return; + }; + + if decrease { + turns = (turns - 1.).max(1.); + } else { + turns += 1.; + } + + responses.add(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::Turns(turns), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, TurnsInput::INDEX), + input: NodeInput::value(TaggedValue::F64(turns), false), + }); + } +} diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 3b73c435a..76a123cb3 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -14,6 +14,7 @@ use crate::messages::tool::common_functionality::shapes::grid_shape::Grid; 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}; @@ -22,7 +23,7 @@ use crate::messages::tool::common_functionality::utility_functions::{closest_poi use graph_craft::document::NodeId; use graphene_std::Color; use graphene_std::renderer::Quad; -use graphene_std::vector::misc::{ArcType, GridType}; +use graphene_std::vector::misc::{ArcType, GridType, SpiralType}; use std::vec; #[derive(Default, ExtractField)] @@ -40,6 +41,8 @@ pub struct ShapeToolOptions { shape_type: ShapeType, arc_type: ArcType, grid_type: GridType, + spiral_type: SpiralType, + turns: f64, } impl Default for ShapeToolOptions { @@ -51,6 +54,8 @@ impl Default for ShapeToolOptions { vertices: 5, shape_type: ShapeType::Polygon, arc_type: ArcType::Open, + spiral_type: SpiralType::Archimedean, + turns: 5., grid_type: GridType::Rectangular, } } @@ -67,6 +72,8 @@ pub enum ShapeOptionsUpdate { Vertices(u32), ShapeType(ShapeType), ArcType(ArcType), + SpiralType(SpiralType), + Turns(f64), GridType(GridType), } @@ -109,6 +116,20 @@ 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) + .mode(NumberInputMode::Increment) + .on_update(|number_input: &NumberInput| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::Turns(number_input.value.unwrap()), + } + .into() + }) + .widget_holder() +} + fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { let entries = vec![vec![ MenuListEntry::new("Polygon").label("Polygon").on_commit(move |_| { @@ -135,6 +156,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { } .into() }), + MenuListEntry::new("Spiral").label("Spiral").on_commit(move |_| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(ShapeType::Spiral), + } + .into() + }), MenuListEntry::new("Grid").label("Grid").on_commit(move |_| { ShapeToolMessage::UpdateOptions { options: ShapeOptionsUpdate::ShapeType(ShapeType::Grid), @@ -184,6 +211,24 @@ 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 { + options: ShapeOptionsUpdate::SpiralType(SpiralType::Archimedean), + } + .into() + }), + MenuListEntry::new("Logarithmic").label("Logarithmic").on_commit(move |_| { + ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::SpiralType(SpiralType::Logarithmic), + } + .into() + }), + ]]; + DropdownInput::new(entries).selected_index(Some(spiral_type as u32)).widget_holder() +} + fn create_grid_type_widget(grid_type: GridType) -> WidgetHolder { let entries = vec![ RadioEntryData::new("Rectangular").label("Rectangular").on_update(move |_| { @@ -221,6 +266,14 @@ 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::Related).widget_holder()); + + widgets.push(create_turns_widget(self.options.turns)); + widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); + } + if self.options.shape_type == ShapeType::Grid { widgets.push(create_grid_type_widget(self.options.grid_type)); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); @@ -327,6 +380,12 @@ impl<'a> MessageHandler> for Shap 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; + } ShapeOptionsUpdate::GridType(grid_type) => { self.options.grid_type = grid_type; } @@ -471,6 +530,18 @@ impl ShapeToolData { fn shape_tool_modifier_keys() -> [Key; 3] { [Key::Alt, Key::Shift, Key::Control] } + + fn decrease_or_increase_sides(&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::decrease_or_increase_sides(decrease, layer, document, responses), + ShapeType::Spiral => Spiral::update_turns(decrease, layer, document, responses), + _ => {} + } + } + + responses.add(NodeGraphMessage::RunDocumentGraph); + } } impl Fsm for ShapeToolFsmState { @@ -584,15 +655,32 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => { - responses.add(ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::Vertices(tool_options.vertices + 1), - }); + if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) { + responses.add(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::Vertices(tool_options.vertices + 1), + }); + } + + if matches!(tool_options.shape_type, ShapeType::Spiral) { + responses.add(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::Turns(tool_options.turns + 1.), + }); + } + self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => { - responses.add(ShapeToolMessage::UpdateOptions { - options: ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)), - }); + if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) { + responses.add(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)), + }); + } + + if matches!(tool_options.shape_type, ShapeType::Spiral) { + responses.add(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::Turns((tool_options.turns - 1.).max(1.)), + }); + } self } ( @@ -629,13 +717,11 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => { - Polygon::increase_decrease_sides(true, document, tool_data, responses); - + tool_data.decrease_or_increase_sides(document, tool_options.shape_type, responses, false); self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => { - Polygon::increase_decrease_sides(false, document, tool_data, responses); - + tool_data.decrease_or_increase_sides(document, tool_options.shape_type, responses, true); self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => { @@ -719,7 +805,9 @@ impl Fsm for ShapeToolFsmState { }; match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => tool_data.data.start(document, input), + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { + 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()); @@ -734,6 +822,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Star => Star::create_node(tool_options.vertices), ShapeType::Circle => Circle::create_node(), ShapeType::Arc => Arc::create_node(tool_options.arc_type), + ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns), ShapeType::Grid => Grid::create_node(tool_options.grid_type), ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Ellipse => Ellipse::create_node(), @@ -746,7 +835,7 @@ impl Fsm for ShapeToolFsmState { let defered_responses = &mut VecDeque::new(); match tool_data.current_shape { - ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { + ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => { defered_responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), @@ -783,6 +872,7 @@ impl Fsm for ShapeToolFsmState { ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses), + ShapeType::Spiral => Spiral::update_shape(document, input, layer, tool_data, responses), ShapeType::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses), ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses), @@ -964,6 +1054,9 @@ impl Fsm for ShapeToolFsmState { responses.add(DocumentMessage::AbortTransaction); tool_data.data.cleanup(responses); tool_data.current_shape = shape; + responses.add(ShapeToolMessage::UpdateOptions { + options: ShapeOptionsUpdate::ShapeType(shape), + }); responses.add(ShapeToolMessage::UpdateOptions { options: ShapeOptionsUpdate::ShapeType(shape), @@ -1000,6 +1093,10 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque 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(), @@ -1046,6 +1143,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]), + ShapeType::Spiral => HintGroup(vec![]), }; if !tool_hint_group.0.is_empty() { @@ -1056,6 +1154,10 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque 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 e971c3f31..d018a4ef5 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 @@ -119,7 +119,7 @@ impl MessageHandler> for 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/node-graph/gcore/src/subpath/core.rs b/node-graph/gcore/src/subpath/core.rs index 2dc8f3dc1..a8329d99c 100644 --- a/node-graph/gcore/src/subpath/core.rs +++ b/node-graph/gcore/src/subpath/core.rs @@ -1,8 +1,9 @@ use super::consts::*; use super::*; -use crate::vector::misc::point_to_dvec2; +use crate::vector::misc::{SpiralType, point_to_dvec2}; use glam::DVec2; use kurbo::PathSeg; +use std::f64::consts::TAU; pub struct PathSegPoints { pub p0: DVec2, @@ -315,4 +316,125 @@ impl Subpath { pub fn new_line(p1: DVec2, p2: DVec2) -> Self { Self::from_anchors([p1, p2], false) } + + pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, start_angle: 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 + start_angle; + + let b = calculate_b(a, turns, outer_radius, spiral_type); + + let mut theta = start_angle; + 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.; + + 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) + } +} + +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. * b) } diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 7a2ad1b24..05ad236a5 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -5,7 +5,7 @@ use crate::registry::types::{Angle, PixelSize}; use crate::subpath; use crate::table::Table; use crate::vector::Vector; -use crate::vector::misc::HandleId; +use crate::vector::misc::{HandleId, SpiralType}; use glam::DVec2; trait CornerRadius { @@ -75,6 +75,27 @@ fn arc( ))) } +#[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))] +fn spiral( + _: impl Ctx, + _primary: (), + spiral_type: SpiralType, + #[default(5.)] turns: f64, + #[default(0.)] start_angle: f64, + #[default(0.)] inner_radius: f64, + #[default(25)] outer_radius: f64, + #[default(90.)] angular_resolution: f64, +) -> Table { + Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_spiral( + inner_radius, + outer_radius, + turns, + start_angle.to_radians(), + angular_resolution.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 f88ee3854..291fc72c2 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -415,3 +415,11 @@ impl HandleId { } } } + +#[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/gcore/src/vector/vector_nodes.rs b/node-graph/gcore/src/vector/vector_nodes.rs index fc0200ec0..2ce635028 100644 --- a/node-graph/gcore/src/vector/vector_nodes.rs +++ b/node-graph/gcore/src/vector/vector_nodes.rs @@ -241,9 +241,8 @@ async fn repeat( #[node_macro::node(category("Instancing"), path(graphene_core::vector))] async fn circular_repeat( _: impl Ctx, - // TODO: Implement other graphical types. #[implementations(Table, Table, Table>, Table, Table)] instance: Table, - angle_offset: Angle, + start_angle: Angle, #[unit(" px")] #[default(5)] radius: f64, @@ -254,7 +253,7 @@ async fn circular_repeat( let mut result_table = Table::new(); for index in 0..count { - let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + angle_offset.to_radians()); + let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians()); let translation = DAffine2::from_translation(radius * DVec2::Y); let transform = angle * translation; diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 0db61bffc..0e0534d9f 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -242,6 +242,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")] diff --git a/website/content/learn/introduction/features-and-limitations.md b/website/content/learn/introduction/features-and-limitations.md index ee2accdb1..533dc7713 100644 --- a/website/content/learn/introduction/features-and-limitations.md +++ b/website/content/learn/introduction/features-and-limitations.md @@ -49,6 +49,7 @@ Next, that is fed into the | | | | | + The node's properties offer controls over settings like *Angle Offset* (what angle to start at), *Radius* (distance from the center), and *Instances* (how many copies to distribute). These parameters can also be exposed into the graph so they are driven by the calculated numerical outputs of other nodes instead of values you pick by hand. ### Raster compositing