From e1a1b1101af69bae35d4c63656d57bd93d4b972a Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Sun, 29 Jun 2025 13:23:03 +0530 Subject: [PATCH 01/13] made spiral node --- .../transform_layer_message_handler.rs | 2 +- libraries/bezier-rs/src/subpath/core.rs | 51 ++++++++++++++++++- libraries/bezier-rs/src/utils.rs | 43 ++++++++++++++++ .../gcore/src/vector/generator_nodes.rs | 24 +++++++++ 4 files changed, 118 insertions(+), 2 deletions(-) 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 287faf00d1..994e47d636 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 @@ -176,7 +176,7 @@ impl MessageHandler> for TransformLayer return; } - if !using_path_tool { + if !using_path_tool || !using_shape_tool { *selected.pivot = selected.mean_average_of_pivots(); self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot); self.grab_target = document.metadata().document_to_viewport.inverse().transform_point2(selected.mean_average_of_pivots()); diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index a18550db6d..b38039e9ef 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::{BezierHandles, consts::*}; use glam::DVec2; use std::fmt::Write; @@ -271,6 +271,55 @@ impl Subpath { ) } + pub fn spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a + b * theta; + DVec2::new(r * theta.cos(), -r * theta.sin()) + } + + fn 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() + } + + pub fn wrap_angle(angle: f64) -> f64 { + (angle + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI + } + + fn spiral_arc_length(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) + } + + pub fn generate_equal_arc_bezier_spiral2(a: f64, b: f64, turns: u32, delta_theta: f64, angle_offset: f64) -> Self { + let mut manipulator_groups = Vec::new(); + let mut prev_in_handle = None; + let mut theta = 0.; + let theta_end = angle_offset + turns as f64 * std::f64::consts::TAU; + + while theta < theta_end { + let theta_next = f64::min(theta + delta_theta, theta_end); + let p0 = Self::spiral_point(theta, a, b); + let p3 = Self::spiral_point(theta_next, a, b); + let t0 = Self::spiral_tangent(theta, a, b); + let t1 = Self::spiral_tangent(theta_next, a, b); + + let arc_len = Self::spiral_arc_length(theta_next, a, b) - Self::spiral_arc_length(theta, a, b); + 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); + 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/utils.rs b/libraries/bezier-rs/src/utils.rs index 2e80b80e8e..ff1ca6db0a 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -302,6 +302,49 @@ pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt: Ok(()) } +pub fn spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a + b * theta; + DVec2::new(r * theta.cos(), -r * theta.sin()) +} + +pub fn spiral_tangent(theta: f64, b: f64) -> DVec2 { + let dx = b * (theta.cos() - theta * theta.sin()); + let dy = b * (theta.sin() + theta * theta.cos()); + DVec2::new(dx, dy).normalize() +} + +pub fn wrap_angle(angle: f64) -> f64 { + (angle + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI +} + +pub fn bezier_point(p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64) -> DVec2 { + let u = 1.0 - t; + p0 * u * u * u + p1 * 3.0 * u * u * t + p2 * 3.0 * u * t * t + p3 * t * t * t +} + +pub fn bezier_derivative(p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64) -> DVec2 { + let u = 1.0 - t; + -3.0 * u * u * p0 + 3.0 * (u * u - 2.0 * u * t) * p1 + 3.0 * (2.0 * u * t - t * t) * p2 + 3.0 * t * t * p3 +} + +pub fn esq_for_d(p0: DVec2, t0: DVec2, p3: DVec2, t1: DVec2, theta0: f64, theta1: f64, d: f64, a: f64, b: f64, samples: usize) -> f64 { + let p1 = p0 + d * t0; + let p2 = p3 - d * t1; + let mut total = 0.0; + for i in 1..samples { + let t = i as f64 / samples as f64; + let bez = bezier_point(p0, p1, p2, p3, t); + let bez_d = bezier_derivative(p0, p1, p2, p3, t); + let bez_angle = bez_d.y.atan2(bez_d.x); + + let theta = theta0 + (theta1 - theta0) * t; + let spiral_angle = theta; + let diff = wrap_angle(bez_angle - spiral_angle); + total += diff * diff * bez_d.length(); + } + total / samples as f64 +} + #[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 adab1ce531..ff8dff507d 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -1,3 +1,5 @@ +use std::f64::consts::{FRAC_PI_4, FRAC_PI_8, TAU}; + use super::misc::{ArcType, AsU64, GridType}; use super::{PointId, SegmentId, StrokeId}; use crate::Ctx; @@ -65,6 +67,28 @@ fn arc( ))) } +#[node_macro::node(category("Vector: Shape"))] +fn spiral( + _: impl Ctx, + _primary: (), + #[default(1.)] inner_radius: f64, + #[default(1.)] tightness: f64, + #[default(6)] + #[hard_min(1.)] + turns: u32, + #[default(0.)] + #[range((0., 360.))] + angle_offset: f64, +) -> VectorDataTable { + VectorDataTable::new(VectorData::from_subpath(Subpath::generate_equal_arc_bezier_spiral2( + inner_radius, + tightness, + turns, + FRAC_PI_4, + angle_offset.to_radians(), + ))) +} + #[node_macro::node(category("Vector: Shape"))] fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25)] radius_y: f64) -> VectorDataTable { let radius = DVec2::new(radius_x, radius_y); From 2b819c11b9b8757bb52d4092d1934fa6b155022a Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Mon, 30 Jun 2025 15:26:58 +0530 Subject: [PATCH 02/13] number of turns in decimal and arc-angle implementation --- libraries/bezier-rs/src/subpath/core.rs | 38 ++++++++++++++++--- .../gcore/src/vector/generator_nodes.rs | 7 ++-- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index b38039e9ef..cd26d13cdf 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -293,14 +293,28 @@ impl Subpath { (r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2.0 * b) } - pub fn generate_equal_arc_bezier_spiral2(a: f64, b: f64, turns: u32, delta_theta: f64, angle_offset: f64) -> Self { + 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); + + let p0123 = p012.lerp(p123, t); // final split point + + (p0, p01, p012, p0123) // First half of the Bézier + } + + pub fn generate_equal_arc_bezier_spiral2(a: f64, b: f64, turns: f64, delta_theta: f64) -> Self { let mut manipulator_groups = Vec::new(); let mut prev_in_handle = None; - let mut theta = 0.; - let theta_end = angle_offset + turns as f64 * std::f64::consts::TAU; + let theta_end = turns * std::f64::consts::TAU; + let mut theta = 0.0; while theta < theta_end { - let theta_next = f64::min(theta + delta_theta, theta_end); + let theta_next = theta + delta_theta; + let p0 = Self::spiral_point(theta, a, b); let p3 = Self::spiral_point(theta_next, a, b); let t0 = Self::spiral_tangent(theta, a, b); @@ -312,8 +326,20 @@ impl Subpath { 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); + 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) = Self::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; } diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index ff8dff507d..77eaf72341 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -75,16 +75,15 @@ fn spiral( #[default(1.)] tightness: f64, #[default(6)] #[hard_min(1.)] - turns: u32, - #[default(0.)] - #[range((0., 360.))] + turns: f64, + #[default(45.)] + #[range((1., 180.))] angle_offset: f64, ) -> VectorDataTable { VectorDataTable::new(VectorData::from_subpath(Subpath::generate_equal_arc_bezier_spiral2( inner_radius, tightness, turns, - FRAC_PI_4, angle_offset.to_radians(), ))) } From b8edd8aa4b928d7cbf1fe4d2df2e59f01593cc88 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Mon, 30 Jun 2025 17:27:25 +0530 Subject: [PATCH 03/13] logarithmic spiral --- libraries/bezier-rs/src/subpath/core.rs | 58 +++++++++++++++++++ .../gcore/src/vector/generator_nodes.rs | 22 ++++++- 2 files changed, 79 insertions(+), 1 deletion(-) diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index cd26d13cdf..6d2656e0d7 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -346,6 +346,64 @@ impl Subpath { Self::new(manipulator_groups, false) } + 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()) + } + + 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()) + } + + 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() + } + + pub fn generate_logarithmic_spiral(a: f64, b: f64, turns: f64, delta_theta: f64) -> 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 = Self::log_spiral_point(theta, a, b); + let p3 = Self::log_spiral_point(theta_next, a, b); + let t0 = Self::log_spiral_tangent(theta, a, b); + let t1 = Self::log_spiral_tangent(theta_next, a, b); + + let arc_len = Self::log_spiral_arc_length(theta, theta_next, a, b); + 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) = Self::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/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 77eaf72341..c37c57a881 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -68,7 +68,7 @@ fn arc( } #[node_macro::node(category("Vector: Shape"))] -fn spiral( +fn archimedean_spiral( _: impl Ctx, _primary: (), #[default(1.)] inner_radius: f64, @@ -88,6 +88,26 @@ fn spiral( ))) } +#[node_macro::node(category("Vector: Shape"))] +fn logarithmic_spiral( + _: impl Ctx, + _primary: (), + #[range((0.1, 1.))] + #[default(0.5)] + start_radius: f64, + #[range((0.1, 1.))] + #[default(0.2)] + growth: f64, + #[default(3)] + #[hard_min(0.5)] + turns: f64, + #[default(45.)] + #[range((1., 180.))] + angle_offset: f64, +) -> VectorDataTable { + VectorDataTable::new(VectorData::from_subpath(Subpath::generate_logarithmic_spiral(start_radius, growth, turns, angle_offset.to_radians()))) +} + #[node_macro::node(category("Vector: Shape"))] fn ellipse(_: impl Ctx, _primary: (), #[default(50)] radius_x: f64, #[default(25)] radius_y: f64) -> VectorDataTable { let radius = DVec2::new(radius_x, radius_y); From 917e79e53ddbab6b26bd6f92300a60d85f9eeaa3 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Tue, 1 Jul 2025 23:01:51 +0530 Subject: [PATCH 04/13] unified log and arc spiral into spiral node --- .../node_graph/document_node_definitions.rs | 1 + .../document/node_graph/node_properties.rs | 69 ++++++++++- libraries/bezier-rs/src/subpath/core.rs | 109 ++---------------- libraries/bezier-rs/src/subpath/structs.rs | 6 + libraries/bezier-rs/src/utils.rs | 104 ++++++++++++----- .../gcore/src/vector/generator_nodes.rs | 53 +++------ node-graph/gcore/src/vector/misc.rs | 8 ++ node-graph/graph-craft/src/document/value.rs | 1 + 8 files changed, 184 insertions(+), 167 deletions(-) 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 63a001ba57..ae63fe737c 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 @@ -2150,6 +2150,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 d074781c14..3a6b785974 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}; @@ -1227,6 +1227,73 @@ 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 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 spiral_type = enum_choice::() + .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, SpiralTypeInput::INDEX, true, context)) + .property_row(); + + let mut widgets = vec![spiral_type]; + + 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 start_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default()), + }; + + let tightness = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, TightnessInput::INDEX, true, context), NumberInput::default()), + }; + + widgets.extend([start_radius, tightness]); + } + SpiralType::Logarithmic => { + let start_radius = LayoutGroup::Row { + widgets: number_widget( + ParameterWidgetsInfo::from_index(document_node, node_id, StartRadiusInput::INDEX, true, context), + NumberInput::default().min(0.1), + ), + }; + + let growth = LayoutGroup::Row { + widgets: number_widget( + ParameterWidgetsInfo::from_index(document_node, node_id, GrowthInput::INDEX, true, context), + NumberInput::default().max(1.).min(0.1).increment_behavior(NumberInputIncrementBehavior::Add).increment_step(0.02), + ), + }; + + widgets.extend([start_radius, growth]); + } + } + } + + let turns = number_widget( + ParameterWidgetsInfo::from_index(document_node, node_id, TurnsInput::INDEX, true, context), + NumberInput::default().min(0.1), + ); + let angle_offset = number_widget( + ParameterWidgetsInfo::from_index(document_node, node_id, AngleOffsetInput::INDEX, true, context), + NumberInput::default().min(0.1).max(180.), + ); + + 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/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 6d2656e0d7..bf8d2ecc9a 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -1,5 +1,5 @@ use super::*; -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,100 +271,7 @@ impl Subpath { ) } - pub fn spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { - let r = a + b * theta; - DVec2::new(r * theta.cos(), -r * theta.sin()) - } - - fn 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() - } - - pub fn wrap_angle(angle: f64) -> f64 { - (angle + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI - } - - fn spiral_arc_length(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) - } - - 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); - - let p0123 = p012.lerp(p123, t); // final split point - - (p0, p01, p012, p0123) // First half of the Bézier - } - - pub fn generate_equal_arc_bezier_spiral2(a: f64, b: f64, turns: f64, delta_theta: f64) -> 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 = Self::spiral_point(theta, a, b); - let p3 = Self::spiral_point(theta_next, a, b); - let t0 = Self::spiral_tangent(theta, a, b); - let t1 = Self::spiral_tangent(theta_next, a, b); - - let arc_len = Self::spiral_arc_length(theta_next, a, b) - Self::spiral_arc_length(theta, a, b); - 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) = Self::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) - } - - 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()) - } - - 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()) - } - - 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() - } - - pub fn generate_logarithmic_spiral(a: f64, b: f64, turns: f64, delta_theta: f64) -> Self { + 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; @@ -373,12 +280,12 @@ impl Subpath { while theta < theta_end { let theta_next = theta + delta_theta; - let p0 = Self::log_spiral_point(theta, a, b); - let p3 = Self::log_spiral_point(theta_next, a, b); - let t0 = Self::log_spiral_tangent(theta, a, b); - let t1 = Self::log_spiral_tangent(theta_next, a, b); + 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 = Self::log_spiral_arc_length(theta, theta_next, a, b); + let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type); let d = arc_len / 3.0; let p1 = p0 + d * t0; @@ -387,7 +294,7 @@ impl Subpath { 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) = Self::split_cubic_bezier(p0, p1, p2, p3, t); + 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); 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 ff1ca6db0a..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,47 +302,91 @@ pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt: Ok(()) } -pub fn spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { - let r = a + b * 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), + } +} + +/// 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()) } -pub fn spiral_tangent(theta: f64, b: f64) -> DVec2 { - let dx = b * (theta.cos() - theta * theta.sin()); - let dy = b * (theta.sin() + theta * theta.cos()); - DVec2::new(dx, dy).normalize() +/// 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()) } -pub fn wrap_angle(angle: f64) -> f64 { - (angle + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI +/// 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() } -pub fn bezier_point(p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64) -> DVec2 { - let u = 1.0 - t; - p0 * u * u * u + p1 * 3.0 * u * u * t + p2 * 3.0 * u * t * t + p3 * t * t * t +/// 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()) } -pub fn bezier_derivative(p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64) -> DVec2 { - let u = 1.0 - t; - -3.0 * u * u * p0 + 3.0 * (u * u - 2.0 * u * t) * p1 + 3.0 * (2.0 * u * t - t * t) * p2 + 3.0 * t * t * p3 +/// 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() } -pub fn esq_for_d(p0: DVec2, t0: DVec2, p3: DVec2, t1: DVec2, theta0: f64, theta1: f64, d: f64, a: f64, b: f64, samples: usize) -> f64 { - let p1 = p0 + d * t0; - let p2 = p3 - d * t1; - let mut total = 0.0; - for i in 1..samples { - let t = i as f64 / samples as f64; - let bez = bezier_point(p0, p1, p2, p3, t); - let bez_d = bezier_derivative(p0, p1, p2, p3, t); - let bez_angle = bez_d.y.atan2(bez_d.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) +} - let theta = theta0 + (theta1 - theta0) * t; - let spiral_angle = theta; - let diff = wrap_angle(bez_angle - spiral_angle); - total += diff * diff * bez_d.length(); - } - total / samples as f64 +/// 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)] diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index c37c57a881..139a58b4da 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -1,9 +1,8 @@ -use std::f64::consts::{FRAC_PI_4, FRAC_PI_8, TAU}; - 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; @@ -67,45 +66,29 @@ fn arc( ))) } -#[node_macro::node(category("Vector: Shape"))] -fn archimedean_spiral( +#[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))] +fn spiral( _: impl Ctx, _primary: (), + spiral_type: SpiralType, + #[default(0.5)] start_radius: f64, #[default(1.)] inner_radius: f64, + #[default(0.2)] growth: f64, #[default(1.)] tightness: f64, - #[default(6)] - #[hard_min(1.)] - turns: f64, - #[default(45.)] - #[range((1., 180.))] - angle_offset: f64, + #[default(6)] turns: f64, + #[default(45.)] angle_offset: f64, ) -> VectorDataTable { - VectorDataTable::new(VectorData::from_subpath(Subpath::generate_equal_arc_bezier_spiral2( - inner_radius, - tightness, - turns, - angle_offset.to_radians(), - ))) -} + let (a, b) = match spiral_type { + SpiralType::Archimedean => (inner_radius, tightness), + SpiralType::Logarithmic => (start_radius, growth), + }; -#[node_macro::node(category("Vector: Shape"))] -fn logarithmic_spiral( - _: impl Ctx, - _primary: (), - #[range((0.1, 1.))] - #[default(0.5)] - start_radius: f64, - #[range((0.1, 1.))] - #[default(0.2)] - growth: f64, - #[default(3)] - #[hard_min(0.5)] - turns: f64, - #[default(45.)] - #[range((1., 180.))] - angle_offset: f64, -) -> VectorDataTable { - VectorDataTable::new(VectorData::from_subpath(Subpath::generate_logarithmic_spiral(start_radius, growth, turns, angle_offset.to_radians()))) + 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"))] 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 742b155557..2e5dc3b4ec 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")] From 91a5c880906ecdf853f4fe8002cd30793bfca854 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Thu, 3 Jul 2025 16:52:26 +0530 Subject: [PATCH 05/13] add spiral shape in shape tool --- .../document/node_graph/node_properties.rs | 16 +- .../graph_modification_utils.rs | 4 + .../tool/common_functionality/shapes/mod.rs | 1 + .../shapes/polygon_shape.rs | 35 ++++ .../shapes/shape_utility.rs | 8 +- .../shapes/spiral_shape.rs | 121 ++++++++++++++ .../messages/tool/tool_messages/shape_tool.rs | 155 +++++++++++------- .../gcore/src/vector/generator_nodes.rs | 2 +- 8 files changed, 272 insertions(+), 70 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs 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 1ecaecb005..de7cb5507d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1251,11 +1251,17 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon match spiral_type { SpiralType::Archimedean => { let start_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default()), + widgets: number_widget( + ParameterWidgetsInfo::from_index(document_node, node_id, InnerRadiusInput::INDEX, true, context), + NumberInput::default().min(0.01), + ), }; let tightness = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::from_index(document_node, node_id, TightnessInput::INDEX, true, context), NumberInput::default()), + widgets: number_widget( + ParameterWidgetsInfo::from_index(document_node, node_id, TightnessInput::INDEX, true, context), + NumberInput::default().unit(" px"), + ), }; widgets.extend([start_radius, tightness]); @@ -1264,14 +1270,14 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon let start_radius = LayoutGroup::Row { widgets: number_widget( ParameterWidgetsInfo::from_index(document_node, node_id, StartRadiusInput::INDEX, true, context), - NumberInput::default().min(0.1), + NumberInput::default().min(0.001), ), }; let growth = LayoutGroup::Row { widgets: number_widget( ParameterWidgetsInfo::from_index(document_node, node_id, GrowthInput::INDEX, true, context), - NumberInput::default().max(1.).min(0.1).increment_behavior(NumberInputIncrementBehavior::Add).increment_step(0.02), + NumberInput::default().max(0.5).min(0.1).increment_behavior(NumberInputIncrementBehavior::Add).increment_step(0.01), ), }; @@ -1286,7 +1292,7 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon ); let angle_offset = number_widget( ParameterWidgetsInfo::from_index(document_node, node_id, AngleOffsetInput::INDEX, true, context), - NumberInput::default().min(0.1).max(180.), + NumberInput::default().min(0.1).max(180.).unit(" °"), ); widgets.extend([LayoutGroup::Row { widgets: turns }, LayoutGroup::Row { widgets: angle_offset }]); 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 765bfc1eac..81b26b988a 100644 --- a/editor/src/messages/tool/common_functionality/graph_modification_utils.rs +++ b/editor/src/messages/tool/common_functionality/graph_modification_utils.rs @@ -346,6 +346,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 5320000404..9cce9aa38f 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/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 63a338186f..4774be4e10 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -80,7 +80,7 @@ fn spiral( _primary: (), spiral_type: SpiralType, #[default(0.5)] start_radius: f64, - #[default(1.)] inner_radius: f64, + #[default(0.)] inner_radius: f64, #[default(0.2)] growth: f64, #[default(1.)] tightness: f64, #[default(6)] turns: f64, From 1773c925ef51f39a822d95187b19f8e3e4e9ea2e Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Thu, 3 Jul 2025 17:30:01 +0530 Subject: [PATCH 06/13] fix min value and degree unit --- .../portfolio/document/node_graph/node_properties.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) 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 de7cb5507d..018182c106 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1250,10 +1250,10 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon if let Some(&TaggedValue::SpiralType(spiral_type)) = spiral_type_input.as_non_exposed_value() { match spiral_type { SpiralType::Archimedean => { - let start_radius = LayoutGroup::Row { + let inner_radius = LayoutGroup::Row { widgets: number_widget( ParameterWidgetsInfo::from_index(document_node, node_id, InnerRadiusInput::INDEX, true, context), - NumberInput::default().min(0.01), + NumberInput::default().min(0.), ), }; @@ -1264,7 +1264,7 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon ), }; - widgets.extend([start_radius, tightness]); + widgets.extend([inner_radius, tightness]); } SpiralType::Logarithmic => { let start_radius = LayoutGroup::Row { @@ -1292,7 +1292,7 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon ); let angle_offset = number_widget( ParameterWidgetsInfo::from_index(document_node, node_id, AngleOffsetInput::INDEX, true, context), - NumberInput::default().min(0.1).max(180.).unit(" °"), + NumberInput::default().min(0.1).max(180.).unit("°"), ); widgets.extend([LayoutGroup::Row { widgets: turns }, LayoutGroup::Row { widgets: angle_offset }]); From 8870409b87f1bce733fc530bf28ab7c9dc740fba Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Thu, 10 Jul 2025 16:25:51 +0530 Subject: [PATCH 07/13] make it compile --- .../document/node_graph/node_properties.rs | 35 +++++++------------ 1 file changed, 12 insertions(+), 23 deletions(-) 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 82e2ee97dc..48b91e818d 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1205,6 +1205,12 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte 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) => { @@ -1212,11 +1218,6 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon return Vec::new(); } }; - let spiral_type = enum_choice::() - .for_socket(ParameterWidgetsInfo::from_index(document_node, node_id, SpiralTypeInput::INDEX, true, context)) - .property_row(); - - let mut widgets = vec![spiral_type]; 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."); @@ -1226,32 +1227,23 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon match spiral_type { SpiralType::Archimedean => { let inner_radius = LayoutGroup::Row { - widgets: number_widget( - ParameterWidgetsInfo::from_index(document_node, node_id, InnerRadiusInput::INDEX, true, context), - NumberInput::default().min(0.), - ), + widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.)), }; let tightness = LayoutGroup::Row { - widgets: number_widget( - ParameterWidgetsInfo::from_index(document_node, node_id, TightnessInput::INDEX, true, context), - NumberInput::default().unit(" px"), - ), + 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::from_index(document_node, node_id, StartRadiusInput::INDEX, true, context), - NumberInput::default().min(0.001), - ), + widgets: number_widget(ParameterWidgetsInfo::new(node_id, StartRadiusInput::INDEX, true, context), NumberInput::default().min(0.)), }; let growth = LayoutGroup::Row { widgets: number_widget( - ParameterWidgetsInfo::from_index(document_node, node_id, GrowthInput::INDEX, true, context), + ParameterWidgetsInfo::new(node_id, GrowthInput::INDEX, true, context), NumberInput::default().max(0.5).min(0.1).increment_behavior(NumberInputIncrementBehavior::Add).increment_step(0.01), ), }; @@ -1261,12 +1253,9 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon } } - let turns = number_widget( - ParameterWidgetsInfo::from_index(document_node, node_id, TurnsInput::INDEX, true, context), - NumberInput::default().min(0.1), - ); + let turns = number_widget(ParameterWidgetsInfo::new(node_id, TurnsInput::INDEX, true, context), NumberInput::default().min(0.1)); let angle_offset = number_widget( - ParameterWidgetsInfo::from_index(document_node, node_id, AngleOffsetInput::INDEX, true, context), + ParameterWidgetsInfo::new(node_id, AngleOffsetInput::INDEX, true, context), NumberInput::default().min(0.1).max(180.).unit("°"), ); From f3b6ab30855ec314a227137eb58da47df68233a3 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Thu, 10 Jul 2025 13:20:34 +0530 Subject: [PATCH 08/13] impl turns handle gizmo --- editor/src/consts.rs | 6 + .../document/node_graph/node_properties.rs | 21 +-- .../gizmos/gizmo_manager.rs | 25 ++- .../gizmos/shape_gizmos/mod.rs | 1 + .../shape_gizmos/number_of_points_dial.rs | 6 +- .../shape_gizmos/point_radius_handle.rs | 6 +- .../shape_gizmos/spiral_turns_handle.rs | 163 ++++++++++++++++++ .../shapes/polygon_shape.rs | 13 +- .../shapes/shape_utility.rs | 115 +++++++++++- .../shapes/spiral_shape.rs | 117 +++++++++---- .../common_functionality/shapes/star_shape.rs | 13 +- .../messages/tool/tool_messages/shape_tool.rs | 14 +- libraries/bezier-rs/src/subpath/core.rs | 26 ++- libraries/bezier-rs/src/utils.rs | 34 ++-- .../gcore/src/vector/generator_nodes.rs | 15 +- 15 files changed, 464 insertions(+), 111 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 58585e2dab..1a95aa2015 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -150,3 +150,9 @@ pub const AUTO_SAVE_TIMEOUT_SECONDS: u64 = 15; // INPUT pub const DOUBLE_CLICK_MILLISECONDS: u64 = 500; + +/// SPIRAL NODE INPUT INDICES +pub const SPIRAL_TYPE_INDEX: usize = 1; +pub const SPIRAL_INNER_RADIUS: usize = 2; +pub const SPIRAL_OUTER_RADIUS_INDEX: usize = 3; +pub const SPIRAL_TURNS_INDEX: usize = 4; diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 48b91e818d..9f27f5ed5b 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1227,28 +1227,25 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon match spiral_type { SpiralType::Archimedean => { let inner_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.)), + widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")), }; - let tightness = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, TightnessInput::INDEX, true, context), NumberInput::default().unit(" px")), + let outer_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().unit(" px")), }; - widgets.extend([inner_radius, tightness]); + widgets.extend([inner_radius, outer_radius]); } SpiralType::Logarithmic => { - let start_radius = LayoutGroup::Row { - widgets: number_widget(ParameterWidgetsInfo::new(node_id, StartRadiusInput::INDEX, true, context), NumberInput::default().min(0.)), + let inner_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")), }; - let growth = LayoutGroup::Row { - widgets: number_widget( - ParameterWidgetsInfo::new(node_id, GrowthInput::INDEX, true, context), - NumberInput::default().max(0.5).min(0.1).increment_behavior(NumberInputIncrementBehavior::Add).increment_step(0.01), - ), + let outer_radius = LayoutGroup::Row { + widgets: number_widget(ParameterWidgetsInfo::new(node_id, OuterRadiusInput::INDEX, true, context), NumberInput::default().min(0.1).unit(" px")), }; - widgets.extend([start_radius, growth]); + widgets.extend([inner_radius, outer_radius]); } } } diff --git a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs index 703c85e14d..0fc0de9d1f 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/gizmo_manager.rs @@ -6,6 +6,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; +use crate::messages::tool::common_functionality::shapes::spiral_shape::SpiralGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; use glam::DVec2; use std::collections::VecDeque; @@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers { None, Star(StarGizmoHandler), Polygon(PolygonGizmoHandler), + Spiral(SpiralGizmoHandler), } impl ShapeGizmoHandlers { @@ -32,15 +34,17 @@ impl ShapeGizmoHandlers { match self { Self::Star(_) => "star", Self::Polygon(_) => "polygon", + Self::Spiral(_) => "spiral", Self::None => "none", } } /// Dispatches interaction state updates to the corresponding shape-specific handler. - pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { match self { - Self::Star(h) => h.handle_state(layer, mouse_position, document, responses), - Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses), + Self::Star(h) => h.handle_state(layer, mouse_position, document, input, responses), + Self::Polygon(h) => h.handle_state(layer, mouse_position, document, input, responses), + Self::Spiral(h) => h.handle_state(layer, mouse_position, document, input, responses), Self::None => {} } } @@ -50,6 +54,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.is_any_gizmo_hovered(), Self::Polygon(h) => h.is_any_gizmo_hovered(), + Self::Spiral(h) => h.is_any_gizmo_hovered(), Self::None => false, } } @@ -59,6 +64,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.handle_click(), Self::Polygon(h) => h.handle_click(), + Self::Spiral(h) => h.handle_click(), Self::None => {} } } @@ -68,6 +74,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.handle_update(drag_start, document, input, responses), Self::Polygon(h) => h.handle_update(drag_start, document, input, responses), + Self::Spiral(h) => h.handle_update(drag_start, document, input, responses), Self::None => {} } } @@ -77,6 +84,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.cleanup(), Self::Polygon(h) => h.cleanup(), + Self::Spiral(h) => h.cleanup(), Self::None => {} } } @@ -94,6 +102,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), + Self::Spiral(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -110,6 +119,7 @@ impl ShapeGizmoHandlers { match self { Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), + Self::Spiral(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::None => {} } } @@ -147,6 +157,11 @@ impl GizmoManager { return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default())); } + // Spiral + if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() { + return Some(ShapeGizmoHandlers::Spiral(SpiralGizmoHandler::default())); + } + None } @@ -158,12 +173,12 @@ impl GizmoManager { /// Called every frame to check selected layers and update the active shape gizmo, if hovered. /// /// Also groups all shape layers with the same kind of gizmo to support overlays for multi-shape editing. - pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec)> = Vec::new(); for layer in document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface) { if let Some(mut handler) = Self::detect_shape_handler(layer, document) { - handler.handle_state(layer, mouse_position, document, responses); + handler.handle_state(layer, mouse_position, document, input, responses); let is_hovered = handler.is_any_gizmo_hovered(); if is_hovered { diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs index 2b88dddd5e..857d8d9e22 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/mod.rs @@ -1,2 +1,3 @@ pub mod number_of_points_dial; pub mod point_radius_handle; +pub mod spiral_turns_handle; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs index 3995a1f401..fa49c3c0f2 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/number_of_points_dial.rs @@ -41,7 +41,7 @@ impl NumberOfPointsDial { self.handle_state = state; } - pub fn is_hovering(&self) -> bool { + pub fn hovered(&self) -> bool { self.handle_state == NumberOfPointsDialState::Hover } @@ -189,8 +189,8 @@ impl NumberOfPointsDial { } pub fn update_number_of_sides(&self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { - let delta = input.mouse.position - document.metadata().document_to_viewport.transform_point2(drag_start); - let sign = (input.mouse.position.x - document.metadata().document_to_viewport.transform_point2(drag_start).x).signum(); + let delta = input.mouse.position - drag_start; + let sign = (input.mouse.position.x - drag_start.x).signum(); let net_delta = (delta.length() / 25.).round() * sign; let Some(layer) = self.layer else { return }; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/point_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/point_radius_handle.rs index fc00e078cf..95ab3815f1 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/point_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/point_radius_handle.rs @@ -426,14 +426,12 @@ impl PointRadiusHandle { }; let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - let document_transform = document.network_interface.document_metadata().transform_to_document(layer); - let center = viewport_transform.transform_point2(DVec2::ZERO); let radius_index = self.radius_index; let original_radius = self.initial_radius; - let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - document_transform.inverse().transform_point2(drag_start); - let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center; + let delta = viewport_transform.inverse().transform_point2(input.mouse.position) - viewport_transform.inverse().transform_point2(drag_start); + let radius = viewport_transform.inverse().transform_point2(drag_start); let projection = delta.project_onto(radius); let sign = radius.dot(delta).signum(); diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs new file mode 100644 index 0000000000..5d9bff779b --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs @@ -0,0 +1,163 @@ +use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX}; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::network_interface::InputConnector; +use crate::messages::prelude::Responses; +use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::tool::common_functionality::graph_modification_utils; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::{ + calculate_b, extract_arc_spiral_parameters, extract_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, +}; +use glam::DVec2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::misc::SpiralType; +use std::collections::VecDeque; +use std::f64::consts::TAU; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum SpiralTurnsState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct SpiralTurns { + pub layer: Option, + pub handle_state: SpiralTurnsState, + initial_turns: f64, + initial_outer_radius: f64, + initial_inner_radius: f64, + initial_b: f64, + previous_mouse_position: DVec2, + total_angle_delta: f64, + spiral_type: SpiralType, +} + +impl SpiralTurns { + pub fn cleanup(&mut self) { + self.handle_state = SpiralTurnsState::Inactive; + self.total_angle_delta = 0.; + self.layer = None; + } + + pub fn update_state(&mut self, state: SpiralTurnsState) { + self.handle_state = state; + } + + pub fn hovered(&self) -> bool { + self.handle_state == SpiralTurnsState::Hover + } + + pub fn is_dragging(&self) -> bool { + self.handle_state == SpiralTurnsState::Dragging + } + + pub fn store_initial_parameters(&mut self, layer: LayerNodeIdentifier, a: f64, turns: f64, outer_radius: f64, mouse_position: DVec2, spiral_type: SpiralType) { + self.layer = Some(layer); + self.initial_turns = turns; + self.initial_b = calculate_b(a, turns, outer_radius, spiral_type); + self.initial_inner_radius = a; + self.initial_outer_radius = outer_radius; + self.previous_mouse_position = mouse_position; + self.spiral_type = spiral_type; + self.update_state(SpiralTurnsState::Hover); + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + let viewport = document.metadata().transform_to_viewport(layer); + + match &self.handle_state { + SpiralTurnsState::Inactive => { + // Archimedean + if let Some(((inner_radius, outer_radius, turns), end_point)) = extract_arc_spiral_parameters(layer, document).zip(get_arc_spiral_end_point(layer, document, viewport, TAU)) { + if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, SpiralType::Archimedean); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } + } + + // Logarithmic + if let Some(((inner_radius, outer_radius, turns), end_point)) = extract_log_spiral_parameters(layer, document).zip(get_log_spiral_end_point(layer, document, viewport, TAU)) { + if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, SpiralType::Logarithmic); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } + } + } + SpiralTurnsState::Hover | SpiralTurnsState::Dragging => {} + } + } + + pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option, _shape_editor: &mut &mut ShapeState, _mouse_position: DVec2, overlay_context: &mut OverlayContext) { + match &self.handle_state { + SpiralTurnsState::Inactive | SpiralTurnsState::Hover | SpiralTurnsState::Dragging => { + let Some(layer) = layer.or(self.layer) else { return }; + let viewport = document.metadata().transform_to_viewport(layer); + + // Is true only when hovered over the gizmo + let selected = self.layer.is_some(); + + if let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, TAU) { + overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED)); + return; + }; + + if let Some(endpoint) = get_log_spiral_end_point(layer, document, viewport, TAU) { + overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED)); + return; + }; + } + } + } + + pub fn update_number_of_turns(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + let Some(layer) = self.layer else { + return; + }; + + let viewport = document.metadata().transform_to_viewport(layer); + + let angle_delta = viewport + .inverse() + .transform_point2(input.mouse.position) + .angle_to(viewport.inverse().transform_point2(self.previous_mouse_position)) + .to_degrees(); + + // Increase the number of turns and outer radius in unison such that growth and tightness remain same + let total_delta = self.total_angle_delta + angle_delta; + + // Convert the total angle (in degrees) to number of full turns + let turns_delta = total_delta / 360.; + + // Calculate the new outer radius based on spiral type and turn change + let outer_radius_change = match self.spiral_type { + SpiralType::Archimedean => turns_delta * (self.initial_b) * TAU, + SpiralType::Logarithmic => self.initial_inner_radius * (self.initial_b * TAU * turns_delta).exp(), + }; + + let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { + return; + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_turns + turns_delta), false), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_outer_radius + outer_radius_change), false), + }); + + responses.add(NodeGraphMessage::RunDocumentGraph); + + self.total_angle_delta += angle_delta; + self.previous_mouse_position = input.mouse.position; + } +} diff --git a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs index c37dcf2ed8..f8af8200e4 100644 --- a/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/polygon_shape.rs @@ -30,16 +30,23 @@ pub struct PolygonGizmoHandler { impl ShapeGizmoHandler for PolygonGizmoHandler { fn is_any_gizmo_hovered(&self) -> bool { - self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered() + self.number_of_points_dial.hovered() || self.point_radius_handle.hovered() } - fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + fn handle_state( + &mut self, + selected_star_layer: LayerNodeIdentifier, + mouse_position: DVec2, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) { self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses); self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses); } fn handle_click(&mut self) { - if self.number_of_points_dial.is_hovering() { + if self.number_of_points_dial.hovered() { self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging); return; } diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 7bdcadf382..aa75262e67 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -1,4 +1,5 @@ use super::ShapeToolData; +use crate::consts::{SPIRAL_INNER_RADIUS, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX}; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -14,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::vector::click_target::ClickTargetType; -use graphene_std::vector::misc::dvec2_to_point; +use graphene_std::vector::misc::{SpiralType, dvec2_to_point}; use kurbo::{BezPath, PathEl, Shape}; use std::collections::VecDeque; use std::f64::consts::{PI, TAU}; @@ -81,7 +82,14 @@ pub trait ShapeGizmoHandler { /// Called every frame to update the gizmo's interaction state based on the mouse position and selection. /// /// This includes detecting hover states and preparing interaction flags or visual feedback (e.g., highlighting a hovered handle). - fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque); + fn handle_state( + &mut self, + selected_shape_layers: LayerNodeIdentifier, + mouse_position: DVec2, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ); /// Called when a mouse click occurs over the canvas and a gizmo handle is hovered. /// @@ -224,6 +232,109 @@ pub fn extract_star_parameters(layer: Option, document: &Do Some((sides, radius_1, radius_2)) } +/// Extract the node input values of Archimedean spiral. +/// Returns an option of (Inner radius, Outer radius, Turns, ). +pub fn extract_arc_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(f64, f64, f64)> { + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; + + let Some(spiral_type) = get_spiral_type(layer, document) else { + return None; + }; + + if spiral_type == SpiralType::Archimedean { + let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns))) = ( + node_inputs.get(SPIRAL_INNER_RADIUS)?.as_value(), + node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), + ) else { + return None; + }; + + return Some((inner_radius, tightness, turns)); + } + + None +} + +/// Extract the node input values of Logarithmic spiral. +/// Returns an option of (Start radius, Outer radius, Turns, ). +pub fn extract_log_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(f64, f64, f64)> { + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; + + let Some(spiral_type) = get_spiral_type(layer, document) else { + return None; + }; + + if spiral_type == SpiralType::Logarithmic { + let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns))) = ( + node_inputs.get(SPIRAL_INNER_RADIUS)?.as_value(), + node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), + ) else { + return None; + }; + + return Some((inner_radius, tightness, turns)); + } + + None +} + +pub fn get_spiral_type(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option { + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; + + let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(1).expect("Failed to get Spiral Type").as_value() else { + return None; + }; + + Some(spiral_type) +} + +pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { + let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) else { + return None; + }; + + let theta = turns * theta; + let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); + let r = a + b * theta; + + Some(viewport.transform_point2(DVec2::new(r * theta.cos(), -r * theta.sin()))) +} + +pub fn get_log_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { + let Some((_start_radius, outer_radius, turns)) = extract_log_spiral_parameters(layer, document) else { + return None; + }; + + Some(viewport.transform_point2(outer_radius * DVec2::new((turns * theta).cos(), -(turns * theta).sin()))) +} + +pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 { + match spiral_type { + SpiralType::Archimedean => { + let total_theta = turns * TAU; + (outer_radius - a) / total_theta + } + SpiralType::Logarithmic => { + let total_theta = turns * TAU; + ((outer_radius.abs() / a).ln()) / total_theta + } + } +} + +/// Returns a point on an Archimedean spiral at angle `theta`. +pub fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a + b * theta; + DVec2::new(r * theta.cos(), -r * theta.sin()) +} + +/// Returns a point on a logarithmic spiral at angle `theta`. +pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a * (b * theta).exp(); // a * e^(bθ) + DVec2::new(r * theta.cos(), -r * theta.sin()) +} + /// Extract the node input values of Polygon. /// Returns an option of (sides, radius). pub fn extract_polygon_parameters(layer: Option, document: &DocumentMessageHandler) -> Option<(u32, f64)> { diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index 0370c07ed8..60fcc395c3 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -1,33 +1,102 @@ use super::*; +use crate::consts::{SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX, SPIRAL_TYPE_INDEX}; use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn; use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; +use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::spiral_turns_handle::{SpiralTurns, SpiralTurnsState}; use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::snapping::{SnapCandidatePoint, SnapData, SnapTypeConfiguration}; use crate::messages::tool::tool_messages::shape_tool::ShapeOptionsUpdate; use crate::messages::tool::tool_messages::tool_prelude::*; use glam::DAffine2; -use graph_craft::document::NodeId; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::vector::misc::SpiralType; use std::collections::VecDeque; -use std::f64::consts::TAU; + +#[derive(Clone, Debug, Default)] +pub struct SpiralGizmoHandler { + turns_handle: SpiralTurns, +} + +impl ShapeGizmoHandler for SpiralGizmoHandler { + fn is_any_gizmo_hovered(&self) -> bool { + self.turns_handle.hovered() + } + + fn handle_state( + &mut self, + selected_spiral_layer: LayerNodeIdentifier, + _mouse_position: DVec2, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) { + self.turns_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); + } + + fn handle_click(&mut self) { + if self.turns_handle.hovered() { + self.turns_handle.update_state(SpiralTurnsState::Dragging); + } + } + + fn handle_update(&mut self, _drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + if self.turns_handle.is_dragging() { + self.turns_handle.update_number_of_turns(document, input, responses); + } + } + + fn overlays( + &self, + document: &DocumentMessageHandler, + selected_spiral_layer: Option, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + self.turns_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); + } + + fn dragging_overlays( + &self, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + if self.turns_handle.is_dragging() { + self.turns_handle.overlays(document, None, shape_editor, mouse_position, overlay_context); + } + } + + fn cleanup(&mut self) { + self.turns_handle.cleanup(); + } +} #[derive(Default)] pub struct Spiral; impl Spiral { pub fn create_node(spiral_type: SpiralType, turns: f64) -> NodeTemplate { + let inner_radius = match spiral_type { + SpiralType::Archimedean => 0., + SpiralType::Logarithmic => 0.1, + }; + let node_type = resolve_document_node_type("Spiral").expect("Spiral node can't be found"); node_type.node_template_input_override([ None, Some(NodeInput::value(TaggedValue::SpiralType(spiral_type), false)), - Some(NodeInput::value(TaggedValue::F64(0.001), false)), - Some(NodeInput::value(TaggedValue::F64(0.1), false)), - None, + Some(NodeInput::value(TaggedValue::F64(inner_radius), false)), Some(NodeInput::value(TaggedValue::F64(0.1), false)), Some(NodeInput::value(TaggedValue::F64(turns), false)), ]) @@ -54,11 +123,14 @@ impl Spiral { return; }; - let Some(&TaggedValue::F64(turns)) = node_inputs.get(6).unwrap().as_value() else { + let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(SPIRAL_TYPE_INDEX).unwrap().as_value() else { return; }; - Self::update_radius(node_id, dragged_distance, turns, responses); + let new_radius = match spiral_type { + SpiralType::Archimedean => dragged_distance, + SpiralType::Logarithmic => (dragged_distance).max(0.1), + }; responses.add(GraphOperationMessage::TransformSet { layer, @@ -66,38 +138,29 @@ impl Spiral { transform_in: TransformIn::Viewport, skip_rerender: false, }); - } - - pub fn update_radius(node_id: NodeId, drag_length: f64, turns: f64, responses: &mut VecDeque) { - let archimedean_radius = drag_length / (turns * TAU); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 5), - input: NodeInput::value(TaggedValue::F64(archimedean_radius), false), - }); - // 0.2 is the default parameter - let factor = (0.2 * turns * TAU).exp(); - let logarithmic_radius = drag_length / factor; responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 2), - input: NodeInput::value(TaggedValue::F64(logarithmic_radius), false), + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(new_radius), false), }); } /// Updates the number of turns of a spiral node and recalculates its radius based on drag distance. /// Also updates the Shape Tool's turns UI widget to reflect the change. - pub fn update_turns(drag_start: DVec2, decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + pub fn update_turns(decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque) { let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { return; }; - let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral") else { return; }; - let Some(&TaggedValue::F64(n)) = node_inputs.get(6).unwrap().as_value() else { return }; + let Some(&TaggedValue::F64(n)) = node_inputs.get(SPIRAL_TURNS_INDEX).unwrap().as_value() else { + return; + }; let input: NodeInput; + let turns: f64; if decrease { turns = (n - 1.).max(1.); @@ -108,14 +171,10 @@ impl Spiral { input = NodeInput::value(TaggedValue::F64(turns), false); responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Turns(turns))); } - - let drag_length = drag_start.distance(ipp.mouse.position); - - Self::update_radius(node_id, drag_length, turns, responses); - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, 6), + input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), input, }); + responses.add(NodeGraphMessage::RunDocumentGraph); } } diff --git a/editor/src/messages/tool/common_functionality/shapes/star_shape.rs b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs index 653b22f3ba..5404ca75d1 100644 --- a/editor/src/messages/tool/common_functionality/shapes/star_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/star_shape.rs @@ -25,16 +25,23 @@ pub struct StarGizmoHandler { impl ShapeGizmoHandler for StarGizmoHandler { fn is_any_gizmo_hovered(&self) -> bool { - self.number_of_points_dial.is_hovering() || self.point_radius_handle.hovered() + self.number_of_points_dial.hovered() || self.point_radius_handle.hovered() } - fn handle_state(&mut self, selected_star_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque) { + fn handle_state( + &mut self, + selected_star_layer: LayerNodeIdentifier, + mouse_position: DVec2, + document: &DocumentMessageHandler, + _input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + ) { self.number_of_points_dial.handle_actions(selected_star_layer, mouse_position, document, responses); self.point_radius_handle.handle_actions(selected_star_layer, document, mouse_position, responses); } fn handle_click(&mut self) { - if self.number_of_points_dial.is_hovering() { + if self.number_of_points_dial.hovered() { self.number_of_points_dial.update_state(NumberOfPointsDialState::Dragging); return; } diff --git a/editor/src/messages/tool/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 28eced2ad1..5e36aed2f8 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -51,7 +51,7 @@ impl Default for ShapeToolOptions { shape_type: ShapeType::Polygon, arc_type: ArcType::Open, spiral_type: SpiralType::Archimedean, - turns: 5., + turns: 3., } } } @@ -370,14 +370,14 @@ impl ShapeToolData { } } - fn increase_no_sides_turns(&self, document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, shape_type: ShapeType, responses: &mut VecDeque, decrease: bool) { + fn increase_no_sides_turns(&self, document: &DocumentMessageHandler, shape_type: ShapeType, responses: &mut VecDeque, decrease: bool) { if let Some(layer) = self.data.layer { match shape_type { ShapeType::Star | ShapeType::Polygon => { Polygon::update_sides(decrease, layer, document, responses); } ShapeType::Spiral => { - Spiral::update_turns(self.data.viewport_drag_start(document), decrease, layer, document, ipp, responses); + Spiral::update_turns(decrease, layer, document, responses); } _ => {} } @@ -425,7 +425,7 @@ impl Fsm for ShapeToolFsmState { let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds); if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) { - tool_data.gizmo_manger.handle_actions(mouse_position, document, responses); + tool_data.gizmo_manger.handle_actions(mouse_position, document, input, responses); tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context); } @@ -539,11 +539,11 @@ impl Fsm for ShapeToolFsmState { self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => { - tool_data.increase_no_sides_turns(document, input, tool_options.shape_type, responses, false); + tool_data.increase_no_sides_turns(document, tool_options.shape_type, responses, false); self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => { - tool_data.increase_no_sides_turns(document, input, tool_options.shape_type, responses, true); + tool_data.increase_no_sides_turns(document, tool_options.shape_type, responses, true); self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => { @@ -680,7 +680,7 @@ impl Fsm for ShapeToolFsmState { } (ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => { responses.add(DocumentMessage::StartTransaction); - tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, document, input, responses); + tool_data.gizmo_manger.handle_update(tool_data.data.viewport_drag_start(document), document, input, responses); responses.add(OverlaysMessage::Draw); diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index bf8d2ecc9a..1f5106eeda 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -1,7 +1,8 @@ use super::*; -use crate::utils::{format_point, spiral_arc_length, spiral_point, spiral_tangent, split_cubic_bezier}; -use crate::{BezierHandles, consts::*}; +use crate::utils::{calculate_b, format_point, spiral_arc_length, spiral_point, spiral_tangent}; +use crate::{BezierHandles, TValue, consts::*}; use glam::DVec2; +use std::f64::consts::TAU; use std::fmt::Write; /// Functionality relating to core `Subpath` operations, such as constructors and `iter`. @@ -271,14 +272,16 @@ impl Subpath { ) } - pub fn new_spiral(a: f64, b: f64, turns: f64, delta_theta: f64, spiral_type: SpiralType) -> Self { + pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, delta_theta: f64, spiral_type: SpiralType) -> Self { let mut manipulator_groups = Vec::new(); let mut prev_in_handle = None; let theta_end = turns * std::f64::consts::TAU; + let b = calculate_b(a, turns, outer_radius, spiral_type); + let mut theta = 0.0; while theta < theta_end { - let theta_next = theta + delta_theta; + let theta_next = f64::min(theta + delta_theta, theta_end); let p0 = spiral_point(theta, a, b, spiral_type); let p3 = spiral_point(theta_next, a, b, spiral_type); @@ -291,18 +294,13 @@ impl Subpath { let p1 = p0 + d * t0; let p2 = p3 - d * t1; - let is_last_segment = theta_next >= theta_end; - if is_last_segment { - let t = (theta_end - theta) / (theta_next - theta); // t in [0, 1] - let (trim_p0, trim_p1, trim_p2, trim_p3) = split_cubic_bezier(p0, p1, p2, p3, t); + manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1))); + prev_in_handle = Some(p2); - manipulator_groups.push(ManipulatorGroup::new(trim_p0, prev_in_handle, Some(trim_p1))); - prev_in_handle = Some(trim_p2); - manipulator_groups.push(ManipulatorGroup::new(trim_p3, prev_in_handle, None)); + // If final segment, end with anchor at theta_end + if (theta_next - theta_end).abs() < f64::EPSILON { + manipulator_groups.push(ManipulatorGroup::new(p3, prev_in_handle, None)); break; - } else { - manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1))); - prev_in_handle = Some(p2); } theta = theta_next; diff --git a/libraries/bezier-rs/src/utils.rs b/libraries/bezier-rs/src/utils.rs index 179d744376..4762e7377a 100644 --- a/libraries/bezier-rs/src/utils.rs +++ b/libraries/bezier-rs/src/utils.rs @@ -1,6 +1,7 @@ use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; use crate::{ManipulatorGroup, SpiralType, Subpath}; use glam::{BVec2, DMat2, DVec2}; +use std::f64::consts::TAU; use std::fmt::Write; #[derive(Copy, Clone, PartialEq)] @@ -302,6 +303,19 @@ pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt: Ok(()) } +pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 { + match spiral_type { + SpiralType::Archimedean => { + let total_theta = turns * TAU; + (outer_radius - a) / total_theta + } + SpiralType::Logarithmic => { + let total_theta = turns * TAU; + ((outer_radius.abs() / a).ln()) / total_theta + } + } +} + /// Returns a point on the given spiral type at angle `theta`. pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 { match spiral_type { @@ -326,22 +340,6 @@ pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spira } } -/// Splits a cubic Bézier curve at parameter `t`, returning the first half. -pub fn split_cubic_bezier(p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64) -> (DVec2, DVec2, DVec2, DVec2) { - let p01 = p0.lerp(p1, t); - let p12 = p1.lerp(p2, t); - let p23 = p2.lerp(p3, t); - - let p012 = p01.lerp(p12, t); - let p123 = p12.lerp(p23, t); - - // final split point - let p0123 = p012.lerp(p123, t); - - // First half of the Bézier - (p0, p01, p012, p0123) -} - /// Returns a point on a logarithmic spiral at angle `theta`. pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { let r = a * (b * theta).exp(); // a * e^(bθ) @@ -360,7 +358,7 @@ pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 { let dx = r * (b * theta.cos() - theta.sin()); let dy = r * (b * theta.sin() + theta.cos()); - DVec2::new(dx, -dy).normalize() + DVec2::new(dx, -dy).normalize_or(DVec2::X) } /// Returns a point on an Archimedean spiral at angle `theta`. @@ -374,7 +372,7 @@ pub fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 { let r = a + b * theta; let dx = b * theta.cos() - r * theta.sin(); let dy = b * theta.sin() + r * theta.cos(); - DVec2::new(dx, -dy).normalize() + DVec2::new(dx, -dy).normalize_or(DVec2::X) } /// Computes arc length along an Archimedean spiral between two angles. diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index 4774be4e10..e04d1546ae 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -79,24 +79,17 @@ fn spiral( _: impl Ctx, _primary: (), spiral_type: SpiralType, - #[default(0.5)] start_radius: f64, #[default(0.)] inner_radius: f64, - #[default(0.2)] growth: f64, - #[default(1.)] tightness: f64, - #[default(6)] turns: f64, - #[default(45.)] angle_offset: f64, + #[default(25)] outer_radius: f64, + #[default(5.)] turns: f64, + #[default(90.)] angle_offset: f64, ) -> VectorDataTable { - let (a, b) = match spiral_type { - SpiralType::Archimedean => (inner_radius, tightness), - SpiralType::Logarithmic => (start_radius, growth), - }; - let spiral_type = match spiral_type { SpiralType::Archimedean => bezier_rs::SpiralType::Archimedean, SpiralType::Logarithmic => bezier_rs::SpiralType::Logarithmic, }; - VectorDataTable::new(VectorData::from_subpath(Subpath::new_spiral(a, b, turns, angle_offset.to_radians(), spiral_type))) + VectorDataTable::new(VectorData::from_subpath(Subpath::new_spiral(inner_radius, outer_radius, turns, angle_offset.to_radians(), spiral_type))) } #[node_macro::node(category("Vector: Shape"))] From 0ec46dbd1232628e976547414e9567055d078eef Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Thu, 10 Jul 2025 14:11:30 +0530 Subject: [PATCH 09/13] impl inner outer gizmos --- editor/src/consts.rs | 1 + .../document/overlays/utility_types.rs | 58 +++- .../arc_spiral_inner_radius_handle.rs | 171 ++++++++++ .../gizmos/shape_gizmos/mod.rs | 2 + .../shape_gizmos/spiral_tightness_gizmo.rs | 292 ++++++++++++++++++ .../shapes/shape_utility.rs | 2 +- .../shapes/spiral_shape.rs | 86 ++++-- .../messages/tool/tool_messages/shape_tool.rs | 1 + 8 files changed, 585 insertions(+), 28 deletions(-) create mode 100644 editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs create mode 100644 editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 1a95aa2015..f05f156b86 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -125,6 +125,7 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.; pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9; pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2; pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.; +pub const SPIRAL_INNER_RADIUS_GIZMO_THRESHOLD: f64 = 10.; pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; // SCROLLBARS diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 3d12ba4961..7c26b166b7 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -372,23 +372,73 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { - let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE); + pub fn dashed_circle( + &mut self, + position: DVec2, + radius: f64, + color_fill: Option<&str>, + color_stroke: Option<&str>, + dash_width: Option, + dash_gap_width: Option, + dash_offset: Option, + transform: Option, + ) { let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); let position = position.round(); self.start_dpi_aware_transform(); + if let Some(transform) = transform { + let [a, b, c, d, e, f] = transform.to_cols_array(); + self.render_context.transform(a, b, c, d, e, f); + } + + if let Some(dash_width) = dash_width { + let dash_gap_width = dash_gap_width.unwrap_or(1.); + let array = js_sys::Array::new(); + array.push(&JsValue::from(dash_width)); + array.push(&JsValue::from(dash_gap_width)); + + if let Some(dash_offset) = dash_offset { + if dash_offset != 0. { + self.render_context.set_line_dash_offset(dash_offset); + } + } + + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + self.render_context.begin_path(); self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle"); - self.render_context.set_fill_style_str(color_fill); self.render_context.set_stroke_style_str(color_stroke); - self.render_context.fill(); + + if let Some(fill_color) = color_fill { + self.render_context.set_fill_style_str(fill_color); + self.render_context.fill(); + } self.render_context.stroke(); + // Reset the dash pattern back to solid + if dash_width.is_some() { + self.render_context + .set_line_dash(&JsValue::from(js_sys::Array::new())) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + if dash_offset.is_some() && dash_offset != Some(0.) { + self.render_context.set_line_dash_offset(0.); + } + self.end_dpi_aware_transform(); } + pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) { + self.dashed_circle(position, radius, color_fill, color_stroke, None, None, None, None); + } + pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) { let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize; let step = (end_at - start_from) / segments as f64; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs new file mode 100644 index 0000000000..247027a83f --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs @@ -0,0 +1,171 @@ +use crate::consts::ARCHIMEDEAN_INNER_RADIUS_INDEX; +use crate::consts::COLOR_OVERLAY_RED; +use crate::consts::GIZMO_HIDE_THRESHOLD; +use crate::consts::LOGARITHMIC_START_RADIUS_INDEX; +use crate::consts::NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH; +use crate::consts::SPIRAL_INNER_RADIUS_GIZMO_THRESHOLD; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector}; +use crate::messages::prelude::FrontendMessage; +use crate::messages::prelude::Responses; +use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; +use crate::messages::tool::common_functionality::shapes::shape_utility::archimedean_spiral_point; +use crate::messages::tool::common_functionality::shapes::shape_utility::calculate_b; +use crate::messages::tool::common_functionality::shapes::shape_utility::extract_arc_spiral_parameters; +use crate::messages::tool::common_functionality::shapes::shape_utility::extract_log_spiral_parameters; +use crate::messages::tool::common_functionality::shapes::shape_utility::get_arc_spiral_end_point; +use crate::messages::tool::common_functionality::shapes::shape_utility::get_log_spiral_end_point; +use glam::DVec2; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::num_traits::sign; +use graphene_std::vector::misc::SpiralType; +use graphene_std::vector::misc::dvec2_to_point; +use kurbo::BezPath; +use kurbo::Circle; +use kurbo::Line; +use kurbo::ParamCurveNearest; +use kurbo::Point; +use std::collections::VecDeque; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum RadiusGizmoState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct RadiusGizmo { + pub layer: Option, + pub handle_state: RadiusGizmoState, + pub spiral_type: SpiralType, + initial_radius: f64, + previous_mouse: DVec2, +} + +impl RadiusGizmo { + pub fn cleanup(&mut self) { + self.layer = None; + self.handle_state = RadiusGizmoState::Inactive; + self.initial_radius = 0.; + self.previous_mouse = DVec2::ZERO; + } + + pub fn hovered(&self) -> bool { + self.handle_state == RadiusGizmoState::Hover + } + + pub fn is_dragging(&self) -> bool { + self.handle_state == RadiusGizmoState::Dragging + } + + pub fn update_state(&mut self, state: RadiusGizmoState) { + self.handle_state = state; + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { + match &self.handle_state { + RadiusGizmoState::Inactive => { + // Archimedean + if let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) { + let viewport = document.metadata().transform_to_viewport(layer); + let layer_mouse = viewport.inverse().transform_point2(mouse_position); + + let center = viewport.transform_point2(DVec2::ZERO); + + if (DVec2::ZERO.distance(layer_mouse) - a).abs() < 5. { + self.layer = Some(layer); + self.initial_radius = a; + self.spiral_type = SpiralType::Archimedean; + self.update_state(RadiusGizmoState::Hover); + self.previous_mouse = mouse_position; + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + } + } + + // Logarithmic + if let Some((a, outer_radius, turns)) = extract_log_spiral_parameters(layer, document) { + let viewport = document.metadata().transform_to_viewport(layer); + let layer_mouse = viewport.inverse().transform_point2(mouse_position); + + if (DVec2::ZERO.distance(layer_mouse) - a).abs() < 5. { + self.layer = Some(layer); + self.initial_radius = a; + self.spiral_type = SpiralType::Logarithmic; + self.update_state(RadiusGizmoState::Hover); + self.previous_mouse = mouse_position; + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + } + } + } + RadiusGizmoState::Hover | RadiusGizmoState::Dragging => {} + } + } + + pub fn overlays( + &self, + document: &DocumentMessageHandler, + selected_spiral_layer: Option, + input: &InputPreprocessorMessageHandler, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + match &self.handle_state { + _ => { + let Some(layer) = selected_spiral_layer.or(self.layer) else { return }; + + let viewport = document.metadata().transform_to_viewport(layer); + if let Some((radius, _, _)) = extract_arc_spiral_parameters(layer, document).or(extract_log_spiral_parameters(layer, document)) { + overlay_context.dashed_circle(DVec2::ZERO, radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); + } + } + } + } + + pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { + let Some(layer) = self.layer else { return }; + + let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else { + return; + }; + + let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); + let center = viewport_transform.transform_point2(DVec2::ZERO); + let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); + let previous_mouse_layer = viewport_transform.inverse().transform_point2(self.previous_mouse); + let drag_start = viewport_transform.inverse().transform_point2(drag_start); + let center_layer = DVec2::ZERO; + + let delta_vector = current_mouse_layer - previous_mouse_layer; + let sign = (current_mouse_layer - previous_mouse_layer).dot(drag_start - center_layer).signum(); + let delta = delta_vector.length() * sign; + + self.previous_mouse = input.mouse.position; + + let (net_radius, index) = match self.spiral_type { + SpiralType::Archimedean => { + let current_radius = extract_arc_spiral_parameters(layer, document) + .map(|(a, _, _)| a) + .expect("Failed to get archimedean spiral inner radius"); + ((current_radius + delta).max(0.), ARCHIMEDEAN_INNER_RADIUS_INDEX) + } + SpiralType::Logarithmic => { + let current_radius = extract_log_spiral_parameters(layer, document) + .map(|(a, _, _)| a) + .expect("Failed to get logarithmic spiral inner radius"); + ((current_radius + delta).max(0.001), LOGARITHMIC_START_RADIUS_INDEX) + } + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, index), + input: NodeInput::value(TaggedValue::F64(net_radius), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } +} 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 857d8d9e22..b604ec1ee7 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,3 +1,5 @@ +pub mod arc_spiral_inner_radius_handle; pub mod number_of_points_dial; pub mod point_radius_handle; +pub mod spiral_tightness_gizmo; pub mod spiral_turns_handle; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs new file mode 100644 index 0000000000..dbf8882a38 --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs @@ -0,0 +1,292 @@ +use crate::consts::{COLOR_OVERLAY_RED, SPIRAL_INNER_RADIUS_GIZMO_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::{self, get_stroke_width}; +use crate::messages::tool::common_functionality::shape_editor::ShapeState; +use crate::messages::tool::common_functionality::shapes::shape_utility::{ + archimedean_spiral_point, calculate_b, extract_arc_spiral_parameters, extract_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, +}; +use glam::{DAffine2, DVec2}; +use graph_craft::document::NodeInput; +use graph_craft::document::value::TaggedValue; +use graphene_std::vector::misc::{SpiralType, dvec2_to_point}; +use kurbo::{Line, ParamCurveNearest}; +use std::collections::VecDeque; +use std::f64::consts::TAU; + +#[derive(Clone, Debug, Default, PartialEq)] +pub enum TightnessGizmoState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct TightnessGizmo { + pub layer: Option, + pub handle_state: TightnessGizmoState, + initial_outer_radius: f64, + spiral_type: SpiralType, + gizmo_line_points: Option<(DVec2, DVec2)>, + previous_mouse: DVec2, +} + +impl TightnessGizmo { + pub fn cleanup(&mut self) { + self.handle_state = TightnessGizmoState::Inactive; + self.layer = None; + self.gizmo_line_points = None; + } + + pub fn update_state(&mut self, state: TightnessGizmoState) { + self.handle_state = state; + } + + pub fn hovered(&self) -> bool { + self.handle_state == TightnessGizmoState::Hover + } + + pub fn is_dragging(&self) -> bool { + self.handle_state == TightnessGizmoState::Dragging + } + + 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 { + TightnessGizmoState::Inactive => { + // Archimedean + if let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) { + let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); + if let Some((start, end)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), outer_radius, turns, a, viewport) { + let line = Line::new(dvec2_to_point(start), dvec2_to_point(end)); + if line.nearest(dvec2_to_point(mouse_position), 1e-6).distance_sq < SPIRAL_INNER_RADIUS_GIZMO_THRESHOLD { + self.layer = Some(layer); + self.initial_outer_radius = outer_radius; + self.previous_mouse = mouse_position; + self.gizmo_line_points = Some((start, end)); + self.spiral_type = SpiralType::Archimedean; + self.update_state(TightnessGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } + return; + }; + + let layer_mouse = viewport.inverse().transform_point2(mouse_position); + + let center = viewport.transform_point2(DVec2::ZERO); + + let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, TAU) else { return }; + + let close_to_circle = (DVec2::ZERO.distance(layer_mouse) - outer_radius).abs() < 5.; + + if close_to_circle { + self.layer = Some(layer); + self.initial_outer_radius = outer_radius; + self.previous_mouse = mouse_position; + // self.gizmo_line_points = Some((start, end)); + self.spiral_type = SpiralType::Archimedean; + self.update_state(TightnessGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + } + } + + // // Logarithmic + // if let Some(((_, _, turns), end_point)) = extract_log_spiral_parameters(layer, document).zip(get_log_spiral_end_point(layer, document, viewport)) { + // if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { + // self.layer = Some(layer); + // self.initial_turns = turns; + // self.previous_mouse_position = mouse_position; + // self.spiral_type = SpiralType::Logarithmic; + // self.update_state(SpiralTurnsState::Hover); + // responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + // } + // } + } + TightnessGizmoState::Hover | TightnessGizmoState::Dragging => { + // let Some(layer) = self.layer else { return }; + + // let viewport = document.metadata().transform_to_viewport(layer); + // let center = viewport.transform_point2(DVec2::ZERO); + + // if mouse_position.distance(center) > NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsDialState::Hover) { + // self.update_state(NumberOfPointsDialState::Inactive); + // self.layer = None; + // responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + // } + } + } + } + + pub fn overlays( + &self, + document: &DocumentMessageHandler, + selected_spiral_layer: Option, + _shape_editor: &mut &mut ShapeState, + mouse_position: DVec2, + overlay_context: &mut OverlayContext, + ) { + let Some(layer) = selected_spiral_layer.or(self.layer) else { + return; + }; + let viewport = document.metadata().transform_to_viewport(layer); + + match &self.handle_state { + TightnessGizmoState::Hover | TightnessGizmoState::Dragging => { + let Some(layer) = selected_spiral_layer.or(self.layer) else { + return; + }; + + if let Some((start, end)) = self.gizmo_line_points { + overlay_context.dashed_line(start, end, Some(COLOR_OVERLAY_RED), None, Some(4.0), Some(4.0), Some(0.5)); + }; + + let viewport = document.metadata().transform_to_viewport(layer); + if let Some((_, outer_radius, _)) = extract_arc_spiral_parameters(layer, document).or(extract_log_spiral_parameters(layer, document)) { + overlay_context.dashed_circle(DVec2::ZERO, outer_radius.max(5.), None, Some(COLOR_OVERLAY_RED), Some(8.), Some(4.), Some(0.5), Some(viewport)); + } + + // let viewport = document.metadata().transform_to_viewport(layer); + // if let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) { + // let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); + // let Some((start, end)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), outer_radius, turns, a, b, viewport) else { + // return; + // }; + // overlay_context.dashed_line(start, end, None, None, Some(4.), Some(4.), Some(0.5)); + // } + } + TightnessGizmoState::Inactive => { + if let Some((_, outer_radius, _)) = extract_arc_spiral_parameters(layer, document).or(extract_log_spiral_parameters(layer, document)) { + overlay_context.dashed_circle(DVec2::ZERO, outer_radius.max(5.), None, Some(COLOR_OVERLAY_RED), Some(8.), Some(4.), Some(0.5), Some(viewport)); + } + } + } + } + + fn check_which_inter_segment(mouse_position: DVec2, outer_radius: f64, turns: f64, a: f64, transform: DAffine2) -> Option<(DVec2, DVec2)> { + let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); + let center = DVec2::ZERO; + let angle = mouse_position.angle_to(DVec2::X).rem_euclid(TAU); + + let viewport_mouse = transform.transform_point2(mouse_position); + let viewport_center = transform.transform_point2(center); + + let max_theta = turns * TAU; + let spiral_outer = archimedean_spiral_point(max_theta, a, b); + let viewport_outer = transform.transform_point2(spiral_outer); + + if viewport_mouse.distance(viewport_center) > viewport_outer.distance(viewport_center) { + return None; + } + + let mouse_distance = viewport_mouse.distance(viewport_center); + + let mut segment_index = 0; + + // ---- First segment: from center to spiral at θ = angle + { + let start = viewport_center; + let spiral_end = archimedean_spiral_point(angle, a, b); + let end = transform.transform_point2(spiral_end); + + let r_end = end.distance(viewport_center); + + if mouse_distance <= r_end { + return Some(Self::calculate_gizmo_line_points(viewport_center, end)); + } + + segment_index += 1; + } + + // ---- Remaining segments: each full turn outward along the ray + let mut base_theta = angle; + + while base_theta <= max_theta { + let theta_start = base_theta; + let theta_end = base_theta + TAU; + + if theta_end > max_theta { + break; + } + + let spiral_start = archimedean_spiral_point(theta_start, a, b); + let spiral_end = archimedean_spiral_point(theta_end, a, b); + + let viewport_start = transform.transform_point2(spiral_start); + let viewport_end = transform.transform_point2(spiral_end); + + let r_start = viewport_start.distance(viewport_center); + let r_end = viewport_end.distance(viewport_center); + + if mouse_distance >= r_start && mouse_distance <= r_end { + return Some(Self::calculate_gizmo_line_points(viewport_start, viewport_end)); + } + + base_theta += TAU; + segment_index += 1; + } + + None + } + + // (start_point,end_point) + fn calculate_gizmo_line_points(start_point: DVec2, end_point: DVec2) -> (DVec2, DVec2) { + let length = start_point.distance(end_point); + let factor = 0.25 * length; + + let direction = (end_point - start_point).normalize(); + + let new_endpoint = end_point - direction * factor; + let new_start_point = start_point + direction * factor; + + (new_start_point, new_endpoint) + } + + pub fn update_number_of_turns(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { + let Some(layer) = self.layer else { + return; + }; + + let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else { + return; + }; + + let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); + let center = viewport_transform.transform_point2(DVec2::ZERO); + let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); + let previous_mouse_layer = viewport_transform.inverse().transform_point2(self.previous_mouse); + let drag_start = viewport_transform.inverse().transform_point2(drag_start); + let center_layer = DVec2::ZERO; + + let delta_vector = current_mouse_layer - previous_mouse_layer; + let sign = (current_mouse_layer - previous_mouse_layer).dot(drag_start - center_layer).signum(); + let delta = delta_vector.length() * sign; + + self.previous_mouse = input.mouse.position; + + let (a, turns, net_radius) = match self.spiral_type { + SpiralType::Archimedean => { + let (a, outer_radius, turns) = extract_arc_spiral_parameters(layer, document).expect("Failed to get archimedean spiral inner radius"); + (a, turns, (outer_radius + delta).max(0.0)) + } + SpiralType::Logarithmic => { + let (a, outer_radius, turns) = extract_log_spiral_parameters(layer, document).expect("Failed to get logarithmic spiral inner radius"); + (a, turns, (outer_radius + delta).max(0.001)) + } + }; + + self.gizmo_line_points = Self::check_which_inter_segment(current_mouse_layer, net_radius, turns, a, viewport_transform); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(net_radius), 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 aa75262e67..c11dc31100 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -303,7 +303,7 @@ pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentM } pub fn get_log_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { - let Some((_start_radius, outer_radius, turns)) = extract_log_spiral_parameters(layer, document) else { + let Some((_, outer_radius, turns)) = extract_log_spiral_parameters(layer, document) else { return None; }; diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index 60fcc395c3..ef1548cbc9 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -1,10 +1,12 @@ use super::*; -use crate::consts::{SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX, SPIRAL_TYPE_INDEX}; +use crate::consts::{SPIRAL_OUTER_RADIUS_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::arc_spiral_inner_radius_handle::{RadiusGizmo, RadiusGizmoState}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::spiral_tightness_gizmo::{TightnessGizmo, TightnessGizmoState}; 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; @@ -21,64 +23,116 @@ use std::collections::VecDeque; #[derive(Clone, Debug, Default)] pub struct SpiralGizmoHandler { + radius_handle: RadiusGizmo, turns_handle: SpiralTurns, + tightness_handle: TightnessGizmo, } impl ShapeGizmoHandler for SpiralGizmoHandler { fn is_any_gizmo_hovered(&self) -> bool { - self.turns_handle.hovered() + self.radius_handle.hovered() || self.turns_handle.hovered() || self.tightness_handle.hovered() } fn handle_state( &mut self, selected_spiral_layer: LayerNodeIdentifier, - _mouse_position: DVec2, + mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) { - self.turns_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); + self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); + self.turns_handle.handle_actions(selected_spiral_layer, mouse_position, document, responses); + self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); } fn handle_click(&mut self) { + if self.radius_handle.hovered() { + self.radius_handle.update_state(RadiusGizmoState::Dragging); + return; + } + + if self.turns_handle.hovered() && self.tightness_handle.hovered() { + self.turns_handle.update_state(SpiralTurnsState::Dragging); + self.tightness_handle.update_state(TightnessGizmoState::Inactive); + return; + } + + if self.radius_handle.hovered() && self.tightness_handle.hovered() { + self.radius_handle.update_state(RadiusGizmoState::Dragging); + self.tightness_handle.update_state(TightnessGizmoState::Inactive); + return; + } + if self.turns_handle.hovered() { self.turns_handle.update_state(SpiralTurnsState::Dragging); } + + if self.tightness_handle.hovered() { + self.tightness_handle.update_state(TightnessGizmoState::Dragging); + } } - fn handle_update(&mut self, _drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + if self.radius_handle.is_dragging() { + self.radius_handle.update_inner_radius(document, input, responses, drag_start); + } + if self.turns_handle.is_dragging() { self.turns_handle.update_number_of_turns(document, input, responses); } + + if self.tightness_handle.is_dragging() { + self.tightness_handle.update_number_of_turns(document, input, responses, drag_start); + } } fn overlays( &self, document: &DocumentMessageHandler, selected_spiral_layer: Option, - _input: &InputPreprocessorMessageHandler, + input: &InputPreprocessorMessageHandler, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext, ) { + if self.radius_handle.hovered() && self.tightness_handle.hovered() { + self.radius_handle.overlays(document, selected_spiral_layer, input, mouse_position, overlay_context); + return; + } + self.radius_handle.overlays(document, selected_spiral_layer, input, mouse_position, overlay_context); self.turns_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); + self.tightness_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); + + // polygon_outline(selected_polygon_layer, document, overlay_context); } fn dragging_overlays( &self, document: &DocumentMessageHandler, - _input: &InputPreprocessorMessageHandler, + input: &InputPreprocessorMessageHandler, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext, ) { - if self.turns_handle.is_dragging() { + if self.radius_handle.is_dragging() { + self.radius_handle.overlays(document, None, input, mouse_position, overlay_context); + } + + if self.radius_handle.is_dragging() { self.turns_handle.overlays(document, None, shape_editor, mouse_position, overlay_context); } + + if self.tightness_handle.is_dragging() { + self.tightness_handle.overlays(document, None, shape_editor, mouse_position, overlay_context); + } } fn cleanup(&mut self) { + // self.number_of_points_dial.cleanup(); + self.radius_handle.cleanup(); self.turns_handle.cleanup(); + self.tightness_handle.cleanup(); } } @@ -148,33 +202,19 @@ impl Spiral { /// 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 Some(&TaggedValue::F64(n)) = node_inputs.get(6).unwrap().as_value() else { return }; 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/tool_messages/shape_tool.rs b/editor/src/messages/tool/tool_messages/shape_tool.rs index 5e36aed2f8..858137ed5c 100644 --- a/editor/src/messages/tool/tool_messages/shape_tool.rs +++ b/editor/src/messages/tool/tool_messages/shape_tool.rs @@ -438,6 +438,7 @@ impl Fsm for ShapeToolFsmState { if !is_resizing_or_rotating && !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo { tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); } if modifying_transform_cage && !matches!(self, ShapeToolFsmState::ModifyingGizmo) { From 8e43744d7f4304a8512de99f8e3fef0e136a824e Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Sun, 13 Jul 2025 23:21:07 +0530 Subject: [PATCH 10/13] impl radius 1 and radius 2 for inner-radius and tightness,need to fix for turns --- editor/src/consts.rs | 5 +- .../document/node_graph/node_properties.rs | 7 +- .../document/overlays/utility_types.rs | 2 +- .../arc_spiral_inner_radius_handle.rs | 171 --------- .../gizmos/shape_gizmos/mod.rs | 2 +- .../spiral_inner_radius_handle.rs | 143 ++++++++ .../shape_gizmos/spiral_tightness_gizmo.rs | 335 +++++++++++------- .../shape_gizmos/spiral_turns_handle.rs | 16 +- .../shapes/shape_utility.rs | 47 +-- .../shapes/spiral_shape.rs | 34 +- libraries/bezier-rs/src/subpath/core.rs | 6 +- .../gcore/src/vector/generator_nodes.rs | 10 +- 12 files changed, 412 insertions(+), 366 deletions(-) delete mode 100644 editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs create mode 100644 editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs diff --git a/editor/src/consts.rs b/editor/src/consts.rs index f05f156b86..ea52e76620 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -125,7 +125,7 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.; pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9; pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2; pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.; -pub const SPIRAL_INNER_RADIUS_GIZMO_THRESHOLD: f64 = 10.; +pub const SPIRAL_INNER_RADIUS_INDEX_GIZMO_THRESHOLD: f64 = 10.; pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; // SCROLLBARS @@ -154,6 +154,7 @@ 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_INNER_RADIUS_INDEX: usize = 2; pub const SPIRAL_OUTER_RADIUS_INDEX: usize = 3; pub const SPIRAL_TURNS_INDEX: usize = 4; +pub const SPIRAL_START_ANGLE: usize = 6; 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 9f27f5ed5b..9e65e14b62 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1255,8 +1255,13 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon ParameterWidgetsInfo::new(node_id, AngleOffsetInput::INDEX, true, context), NumberInput::default().min(0.1).max(180.).unit("°"), ); + let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, StartAngleInput::INDEX, true, context), NumberInput::default().unit("°")); - widgets.extend([LayoutGroup::Row { widgets: turns }, LayoutGroup::Row { widgets: angle_offset }]); + widgets.extend([ + LayoutGroup::Row { widgets: turns }, + LayoutGroup::Row { widgets: angle_offset }, + LayoutGroup::Row { widgets: start_angle }, + ]); widgets } diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 7c26b166b7..ae05d2588d 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -390,7 +390,7 @@ impl OverlayContext { if let Some(transform) = transform { let [a, b, c, d, e, f] = transform.to_cols_array(); - self.render_context.transform(a, b, c, d, e, f); + self.render_context.transform(a, b, c, d, e, f).expect("Failed to transform circle"); } if let Some(dash_width) = dash_width { diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs deleted file mode 100644 index 247027a83f..0000000000 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/arc_spiral_inner_radius_handle.rs +++ /dev/null @@ -1,171 +0,0 @@ -use crate::consts::ARCHIMEDEAN_INNER_RADIUS_INDEX; -use crate::consts::COLOR_OVERLAY_RED; -use crate::consts::GIZMO_HIDE_THRESHOLD; -use crate::consts::LOGARITHMIC_START_RADIUS_INDEX; -use crate::consts::NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH; -use crate::consts::SPIRAL_INNER_RADIUS_GIZMO_THRESHOLD; -use crate::messages::frontend::utility_types::MouseCursorIcon; -use crate::messages::message::Message; -use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; -use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector}; -use crate::messages::prelude::FrontendMessage; -use crate::messages::prelude::Responses; -use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; -use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; -use crate::messages::tool::common_functionality::shapes::shape_utility::archimedean_spiral_point; -use crate::messages::tool::common_functionality::shapes::shape_utility::calculate_b; -use crate::messages::tool::common_functionality::shapes::shape_utility::extract_arc_spiral_parameters; -use crate::messages::tool::common_functionality::shapes::shape_utility::extract_log_spiral_parameters; -use crate::messages::tool::common_functionality::shapes::shape_utility::get_arc_spiral_end_point; -use crate::messages::tool::common_functionality::shapes::shape_utility::get_log_spiral_end_point; -use glam::DVec2; -use graph_craft::document::NodeInput; -use graph_craft::document::value::TaggedValue; -use graphene_std::num_traits::sign; -use graphene_std::vector::misc::SpiralType; -use graphene_std::vector::misc::dvec2_to_point; -use kurbo::BezPath; -use kurbo::Circle; -use kurbo::Line; -use kurbo::ParamCurveNearest; -use kurbo::Point; -use std::collections::VecDeque; - -#[derive(Clone, Debug, Default, PartialEq)] -pub enum RadiusGizmoState { - #[default] - Inactive, - Hover, - Dragging, -} - -#[derive(Clone, Debug, Default)] -pub struct RadiusGizmo { - pub layer: Option, - pub handle_state: RadiusGizmoState, - pub spiral_type: SpiralType, - initial_radius: f64, - previous_mouse: DVec2, -} - -impl RadiusGizmo { - pub fn cleanup(&mut self) { - self.layer = None; - self.handle_state = RadiusGizmoState::Inactive; - self.initial_radius = 0.; - self.previous_mouse = DVec2::ZERO; - } - - pub fn hovered(&self) -> bool { - self.handle_state == RadiusGizmoState::Hover - } - - pub fn is_dragging(&self) -> bool { - self.handle_state == RadiusGizmoState::Dragging - } - - pub fn update_state(&mut self, state: RadiusGizmoState) { - self.handle_state = state; - } - - pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { - match &self.handle_state { - RadiusGizmoState::Inactive => { - // Archimedean - if let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) { - let viewport = document.metadata().transform_to_viewport(layer); - let layer_mouse = viewport.inverse().transform_point2(mouse_position); - - let center = viewport.transform_point2(DVec2::ZERO); - - if (DVec2::ZERO.distance(layer_mouse) - a).abs() < 5. { - self.layer = Some(layer); - self.initial_radius = a; - self.spiral_type = SpiralType::Archimedean; - self.update_state(RadiusGizmoState::Hover); - self.previous_mouse = mouse_position; - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); - } - } - - // Logarithmic - if let Some((a, outer_radius, turns)) = extract_log_spiral_parameters(layer, document) { - let viewport = document.metadata().transform_to_viewport(layer); - let layer_mouse = viewport.inverse().transform_point2(mouse_position); - - if (DVec2::ZERO.distance(layer_mouse) - a).abs() < 5. { - self.layer = Some(layer); - self.initial_radius = a; - self.spiral_type = SpiralType::Logarithmic; - self.update_state(RadiusGizmoState::Hover); - self.previous_mouse = mouse_position; - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); - } - } - } - RadiusGizmoState::Hover | RadiusGizmoState::Dragging => {} - } - } - - pub fn overlays( - &self, - document: &DocumentMessageHandler, - selected_spiral_layer: Option, - input: &InputPreprocessorMessageHandler, - mouse_position: DVec2, - overlay_context: &mut OverlayContext, - ) { - match &self.handle_state { - _ => { - let Some(layer) = selected_spiral_layer.or(self.layer) else { return }; - - let viewport = document.metadata().transform_to_viewport(layer); - if let Some((radius, _, _)) = extract_arc_spiral_parameters(layer, document).or(extract_log_spiral_parameters(layer, document)) { - overlay_context.dashed_circle(DVec2::ZERO, radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); - } - } - } - } - - pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { - let Some(layer) = self.layer else { return }; - - let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else { - return; - }; - - let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - let center = viewport_transform.transform_point2(DVec2::ZERO); - let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); - let previous_mouse_layer = viewport_transform.inverse().transform_point2(self.previous_mouse); - let drag_start = viewport_transform.inverse().transform_point2(drag_start); - let center_layer = DVec2::ZERO; - - let delta_vector = current_mouse_layer - previous_mouse_layer; - let sign = (current_mouse_layer - previous_mouse_layer).dot(drag_start - center_layer).signum(); - let delta = delta_vector.length() * sign; - - self.previous_mouse = input.mouse.position; - - let (net_radius, index) = match self.spiral_type { - SpiralType::Archimedean => { - let current_radius = extract_arc_spiral_parameters(layer, document) - .map(|(a, _, _)| a) - .expect("Failed to get archimedean spiral inner radius"); - ((current_radius + delta).max(0.), ARCHIMEDEAN_INNER_RADIUS_INDEX) - } - SpiralType::Logarithmic => { - let current_radius = extract_log_spiral_parameters(layer, document) - .map(|(a, _, _)| a) - .expect("Failed to get logarithmic spiral inner radius"); - ((current_radius + delta).max(0.001), LOGARITHMIC_START_RADIUS_INDEX) - } - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, index), - input: NodeInput::value(TaggedValue::F64(net_radius), false), - }); - responses.add(NodeGraphMessage::RunDocumentGraph); - } -} 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 b604ec1ee7..8bc8065c61 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,5 +1,5 @@ -pub mod arc_spiral_inner_radius_handle; pub mod number_of_points_dial; pub mod point_radius_handle; +pub mod spiral_inner_radius_handle; pub mod spiral_tightness_gizmo; pub mod spiral_turns_handle; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs new file mode 100644 index 0000000000..3f7bf6689e --- /dev/null +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs @@ -0,0 +1,143 @@ +use crate::consts::{COLOR_OVERLAY_RED, SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX}; +use crate::messages::frontend::utility_types::MouseCursorIcon; +use crate::messages::message::Message; +use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::{overlays::utility_types::OverlayContext, utility_types::network_interface::InputConnector}; +use crate::messages::prelude::FrontendMessage; +use crate::messages::prelude::Responses; +use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler, NodeGraphMessage}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{calculate_b, get_spiral_type}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_arc_or_log_spiral_parameters, spiral_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 RadiusGizmoState { + #[default] + Inactive, + Hover, + Dragging, +} + +#[derive(Clone, Debug, Default)] +pub struct RadiusGizmo { + pub layer: Option, + pub handle_state: RadiusGizmoState, + pub spiral_type: SpiralType, + radius_index: usize, + previous_mouse_position: DVec2, + initial_radius: f64, +} + +impl RadiusGizmo { + pub fn cleanup(&mut self) { + self.layer = None; + self.handle_state = RadiusGizmoState::Inactive; + self.initial_radius = 0.; + } + + pub fn hovered(&self) -> bool { + self.handle_state == RadiusGizmoState::Hover + } + + pub fn is_dragging(&self) -> bool { + self.handle_state == RadiusGizmoState::Dragging + } + + pub fn update_state(&mut self, state: RadiusGizmoState) { + self.handle_state = state; + } + + pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { + match &self.handle_state { + RadiusGizmoState::Inactive => { + if let Some(((inner_radius, outer_radius, _, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + let smaller_radius = (inner_radius.min(outer_radius)).max(5.); + let viewport = document.metadata().transform_to_viewport(layer); + let layer_mouse = viewport.inverse().transform_point2(mouse_position); + + if DVec2::ZERO.distance(layer_mouse) < smaller_radius.max(5.) { + self.layer = Some(layer); + self.initial_radius = inner_radius; + self.spiral_type = spiral_type; + self.previous_mouse_position = mouse_position; + self.radius_index = if inner_radius > outer_radius { SPIRAL_OUTER_RADIUS_INDEX } else { SPIRAL_INNER_RADIUS_INDEX }; + self.update_state(RadiusGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + } + } + } + RadiusGizmoState::Hover | RadiusGizmoState::Dragging => {} + } + } + + pub fn overlays(&self, document: &DocumentMessageHandler, selected_spiral_layer: Option, overlay_context: &mut OverlayContext) { + match &self.handle_state { + RadiusGizmoState::Hover | RadiusGizmoState::Dragging => { + let Some(layer) = selected_spiral_layer.or(self.layer) else { return }; + + let viewport = document.metadata().transform_to_viewport(layer); + if let Some(((inner_radius, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); + let (radius, endpoint) = if self.radius_index == SPIRAL_INNER_RADIUS_INDEX { + (inner_radius, spiral_point(0., inner_radius, b, spiral_type)) + } else { + (outer_radius, spiral_point(turns * TAU, inner_radius, b, spiral_type)) + }; + + overlay_context.manipulator_handle(viewport.transform_point2(endpoint), true, Some(COLOR_OVERLAY_RED)); + overlay_context.dashed_circle(DVec2::ZERO, radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); + } + } + _ => {} + } + } + + pub fn update_inner_radius(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + let Some(layer) = self.layer else { return }; + + let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { + return; + }; + + let node_inputs = NodeGraphLayer::new(layer, &document.network_interface) + .find_node_inputs("Spiral") + .expect("Failed to find inputs of Spiral"); + + let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); + + let center = DVec2::ZERO; + let layer_drag_start = viewport_transform.inverse().transform_point2(drag_start); + + let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); + let previous_mouse_layer = viewport_transform.inverse().transform_point2(self.previous_mouse_position); + + let sign = (current_mouse_layer - previous_mouse_layer).dot(layer_drag_start).signum(); + + let delta = current_mouse_layer.distance(previous_mouse_layer) * sign; + + let net_radius = current_mouse_layer.distance(DVec2::ZERO); + + let Some(&TaggedValue::F64(radius)) = node_inputs.get(self.radius_index).expect("Failed to get radius of Spiral").as_value() else { + return; + }; + + let net_radius = match self.spiral_type { + SpiralType::Archimedean => (radius + delta).max(0.), + SpiralType::Logarithmic => (radius + delta).max(0.001), + }; + + self.previous_mouse_position = input.mouse.position; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, self.radius_index), + input: NodeInput::value(TaggedValue::F64(net_radius), false), + }); + responses.add(NodeGraphMessage::RunDocumentGraph); + } +} diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs index dbf8882a38..5b61b4e996 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs @@ -1,4 +1,4 @@ -use crate::consts::{COLOR_OVERLAY_RED, SPIRAL_INNER_RADIUS_GIZMO_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX}; +use crate::consts::{COLOR_OVERLAY_RED, SPIRAL_INNER_RADIUS_INDEX, SPIRAL_INNER_RADIUS_INDEX_GIZMO_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -6,14 +6,15 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye 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::{self, get_stroke_width}; +use crate::messages::tool::common_functionality::graph_modification_utils::{self}; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::{ - archimedean_spiral_point, calculate_b, extract_arc_spiral_parameters, extract_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, + calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, }; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; +use graphene_std::uuid::NodeId; use graphene_std::vector::misc::{SpiralType, dvec2_to_point}; use kurbo::{Line, ParamCurveNearest}; use std::collections::VecDeque; @@ -27,6 +28,14 @@ pub enum TightnessGizmoState { Dragging, } +#[derive(Clone, Debug, Default, PartialEq)] +enum TightnessGizmoType { + #[default] + None, + Circle, + DashLines, +} + #[derive(Clone, Debug, Default)] pub struct TightnessGizmo { pub layer: Option, @@ -34,7 +43,11 @@ pub struct TightnessGizmo { initial_outer_radius: f64, spiral_type: SpiralType, gizmo_line_points: Option<(DVec2, DVec2)>, + inner_radius: f64, + angle: f64, + spiral_slot: i32, previous_mouse: DVec2, + gizmo_type: TightnessGizmoType, } impl TightnessGizmo { @@ -42,6 +55,7 @@ impl TightnessGizmo { self.handle_state = TightnessGizmoState::Inactive; self.layer = None; self.gizmo_line_points = None; + self.gizmo_type = TightnessGizmoType::None; } pub fn update_state(&mut self, state: TightnessGizmoState) { @@ -62,203 +76,205 @@ impl TightnessGizmo { match &self.handle_state { TightnessGizmoState::Inactive => { // Archimedean - if let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) { - let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); - if let Some((start, end)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), outer_radius, turns, a, viewport) { - let line = Line::new(dvec2_to_point(start), dvec2_to_point(end)); - if line.nearest(dvec2_to_point(mouse_position), 1e-6).distance_sq < SPIRAL_INNER_RADIUS_GIZMO_THRESHOLD { - self.layer = Some(layer); - self.initial_outer_radius = outer_radius; - self.previous_mouse = mouse_position; - self.gizmo_line_points = Some((start, end)); - self.spiral_type = SpiralType::Archimedean; - self.update_state(TightnessGizmoState::Hover); - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - } + if let Some(((a, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + if let Some((start, end, slot_index)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), outer_radius, turns, a, spiral_type, viewport) { + self.layer = Some(layer); + self.initial_outer_radius = outer_radius; + self.previous_mouse = mouse_position; + self.gizmo_line_points = Some((start, end)); + self.spiral_type = spiral_type; + self.spiral_slot = slot_index; + self.inner_radius = a; + self.gizmo_type = TightnessGizmoType::DashLines; + self.angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X).rem_euclid(TAU); + self.update_state(TightnessGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + return; }; - let layer_mouse = viewport.inverse().transform_point2(mouse_position); - let center = viewport.transform_point2(DVec2::ZERO); - let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, TAU) else { return }; + let angle = if a > outer_radius { 0. } else { TAU }; - let close_to_circle = (DVec2::ZERO.distance(layer_mouse) - outer_radius).abs() < 5.; + let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, angle).or(get_log_spiral_end_point(layer, document, viewport, angle)) else { + return; + }; + + let close_to_circle = (endpoint.distance(center) - mouse_position.distance(center)).abs() < 5.; if close_to_circle { self.layer = Some(layer); + self.inner_radius = a; self.initial_outer_radius = outer_radius; self.previous_mouse = mouse_position; - // self.gizmo_line_points = Some((start, end)); - self.spiral_type = SpiralType::Archimedean; + self.gizmo_type = TightnessGizmoType::Circle; + self.spiral_type = spiral_type; self.update_state(TightnessGizmoState::Hover); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } - - // // Logarithmic - // if let Some(((_, _, turns), end_point)) = extract_log_spiral_parameters(layer, document).zip(get_log_spiral_end_point(layer, document, viewport)) { - // if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { - // self.layer = Some(layer); - // self.initial_turns = turns; - // self.previous_mouse_position = mouse_position; - // self.spiral_type = SpiralType::Logarithmic; - // self.update_state(SpiralTurnsState::Hover); - // responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - // } - // } - } - TightnessGizmoState::Hover | TightnessGizmoState::Dragging => { - // let Some(layer) = self.layer else { return }; - - // let viewport = document.metadata().transform_to_viewport(layer); - // let center = viewport.transform_point2(DVec2::ZERO); - - // if mouse_position.distance(center) > NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH && matches!(&self.handle_state, NumberOfPointsDialState::Hover) { - // self.update_state(NumberOfPointsDialState::Inactive); - // self.layer = None; - // responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - // } } + TightnessGizmoState::Hover | TightnessGizmoState::Dragging => {} } } - pub fn overlays( - &self, - document: &DocumentMessageHandler, - selected_spiral_layer: Option, - _shape_editor: &mut &mut ShapeState, - mouse_position: DVec2, - overlay_context: &mut OverlayContext, - ) { + pub fn overlays(&self, document: &DocumentMessageHandler, selected_spiral_layer: Option, _shape_editor: &mut &mut ShapeState, overlay_context: &mut OverlayContext) { let Some(layer) = selected_spiral_layer.or(self.layer) else { return; }; let viewport = document.metadata().transform_to_viewport(layer); match &self.handle_state { - TightnessGizmoState::Hover | TightnessGizmoState::Dragging => { - let Some(layer) = selected_spiral_layer.or(self.layer) else { - return; - }; - - if let Some((start, end)) = self.gizmo_line_points { - overlay_context.dashed_line(start, end, Some(COLOR_OVERLAY_RED), None, Some(4.0), Some(4.0), Some(0.5)); - }; - - let viewport = document.metadata().transform_to_viewport(layer); - if let Some((_, outer_radius, _)) = extract_arc_spiral_parameters(layer, document).or(extract_log_spiral_parameters(layer, document)) { - overlay_context.dashed_circle(DVec2::ZERO, outer_radius.max(5.), None, Some(COLOR_OVERLAY_RED), Some(8.), Some(4.), Some(0.5), Some(viewport)); + TightnessGizmoState::Hover | TightnessGizmoState::Dragging => match self.gizmo_type { + TightnessGizmoType::Circle => { + if let Some((inner_radius, outer_radius, _, _)) = extract_arc_or_log_spiral_parameters(layer, document) { + let required_radius = if self.inner_radius > self.initial_outer_radius { inner_radius } else { outer_radius }; + overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(8.), Some(4.), Some(0.5), Some(viewport)); + } } - - // let viewport = document.metadata().transform_to_viewport(layer); - // if let Some((a, outer_radius, turns)) = extract_arc_spiral_parameters(layer, document) { - // let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); - // let Some((start, end)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), outer_radius, turns, a, b, viewport) else { - // return; - // }; - // overlay_context.dashed_line(start, end, None, None, Some(4.), Some(4.), Some(0.5)); - // } - } - TightnessGizmoState::Inactive => { - if let Some((_, outer_radius, _)) = extract_arc_spiral_parameters(layer, document).or(extract_log_spiral_parameters(layer, document)) { - overlay_context.dashed_circle(DVec2::ZERO, outer_radius.max(5.), None, Some(COLOR_OVERLAY_RED), Some(8.), Some(4.), Some(0.5), Some(viewport)); + TightnessGizmoType::DashLines => { + if let Some((start, end)) = self.gizmo_line_points { + overlay_context.dashed_line(start, end, None, None, Some(4.0), Some(4.0), Some(0.5)); + if self.spiral_slot == 0 { + let required_radius = if self.inner_radius > self.initial_outer_radius { + self.initial_outer_radius + } else { + self.inner_radius + }; + overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); + } + }; } - } + TightnessGizmoType::None => {} + }, + TightnessGizmoState::Inactive => {} } } - fn check_which_inter_segment(mouse_position: DVec2, outer_radius: f64, turns: f64, a: f64, transform: DAffine2) -> Option<(DVec2, DVec2)> { - let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); + fn check_which_inter_segment(layer_mouse_position: DVec2, outer_radius: f64, turns: f64, inner_radius: f64, spiral_type: SpiralType, viewport: DAffine2) -> Option<(DVec2, DVec2, i32)> { let center = DVec2::ZERO; - let angle = mouse_position.angle_to(DVec2::X).rem_euclid(TAU); - - let viewport_mouse = transform.transform_point2(mouse_position); - let viewport_center = transform.transform_point2(center); + let mut angle = layer_mouse_position.angle_to(DVec2::X).rem_euclid(TAU); + let is_reversed = inner_radius > outer_radius; + let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); let max_theta = turns * TAU; - let spiral_outer = archimedean_spiral_point(max_theta, a, b); - let viewport_outer = transform.transform_point2(spiral_outer); - if viewport_mouse.distance(viewport_center) > viewport_outer.distance(viewport_center) { + let viewport_mouse = viewport.transform_point2(layer_mouse_position); + let viewport_center = viewport.transform_point2(center); + + // Compute spiral endpoints at θ = 0 and θ = max + let spiral_outer = spiral_point(max_theta, inner_radius, b, spiral_type); + let spiral_inner = spiral_point(0., inner_radius, b, spiral_type); + let viewport_outer = viewport.transform_point2(spiral_outer); + let viewport_inner = viewport.transform_point2(spiral_inner); + + let smaller_radius = inner_radius.min(outer_radius); + let adjusted_angle = if is_reversed { max_theta - (TAU - angle) } else { angle }; + + let required_endpoint = if is_reversed { viewport_inner } else { viewport_outer }; + + // Reject if mouse is beyond spiral's radial extent + if viewport_mouse.distance(viewport_center) > required_endpoint.distance(viewport_center) { return None; } let mouse_distance = viewport_mouse.distance(viewport_center); - let mut segment_index = 0; - // ---- First segment: from center to spiral at θ = angle + // First segment: from center to first spiral point at θ = adjusted_angle { - let start = viewport_center; - let spiral_end = archimedean_spiral_point(angle, a, b); - let end = transform.transform_point2(spiral_end); - - let r_end = end.distance(viewport_center); + let spiral_end = spiral_point(adjusted_angle, inner_radius, b, spiral_type); + let first_point = viewport.transform_point2(spiral_end); + let r_end = first_point.distance(viewport_center); if mouse_distance <= r_end { - return Some(Self::calculate_gizmo_line_points(viewport_center, end)); + let direction = DVec2::new(adjusted_angle.cos(), -adjusted_angle.sin()); + return Some((viewport.transform_point2(smaller_radius.max(5.) * direction), first_point, segment_index)); } segment_index += 1; } - // ---- Remaining segments: each full turn outward along the ray - let mut base_theta = angle; - - while base_theta <= max_theta { + // Loop through each full turn segment along the spiral ray + let mut base_theta = adjusted_angle; + while if is_reversed { base_theta >= 0. } else { base_theta <= max_theta } { let theta_start = base_theta; - let theta_end = base_theta + TAU; + let theta_end = if is_reversed { base_theta - TAU } else { base_theta + TAU }; - if theta_end > max_theta { + if (!is_reversed && theta_end > max_theta) || (is_reversed && theta_end < 0.) { break; } - let spiral_start = archimedean_spiral_point(theta_start, a, b); - let spiral_end = archimedean_spiral_point(theta_end, a, b); + let spiral_start = spiral_point(theta_start, inner_radius, b, spiral_type); + let spiral_end = spiral_point(theta_end, inner_radius, b, spiral_type); - let viewport_start = transform.transform_point2(spiral_start); - let viewport_end = transform.transform_point2(spiral_end); + let viewport_start = viewport.transform_point2(spiral_start); + let viewport_end = viewport.transform_point2(spiral_end); let r_start = viewport_start.distance(viewport_center); let r_end = viewport_end.distance(viewport_center); - if mouse_distance >= r_start && mouse_distance <= r_end { - return Some(Self::calculate_gizmo_line_points(viewport_start, viewport_end)); + if mouse_distance >= r_start.min(r_end) && mouse_distance <= r_start.max(r_end) { + let (point1, point2) = Self::calculate_gizmo_line_points(viewport_start, viewport_end); + return Some((point1, point2, segment_index)); } - base_theta += TAU; + base_theta = if is_reversed { base_theta - TAU } else { base_theta + TAU }; + segment_index += 1; } None } + pub fn calculate_updated_dash_lines(&self, inner_radius: f64, outer_radius: f64, turns: f64, spiral_type: SpiralType, viewport: DAffine2, drag_start: DVec2, reversed: bool) -> (DVec2, DVec2) { + let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); + let max_theta = turns * TAU; + let base_angle = if reversed { max_theta - (TAU - self.angle) } else { self.angle }; + let smaller_radius = inner_radius.min(outer_radius); + + let center = DVec2::ZERO; + + let (start_point, end_point) = if self.spiral_slot == 0 { + ( + viewport.transform_point2(smaller_radius * DVec2::new(base_angle.cos(), -base_angle.sin())), + viewport.transform_point2(spiral_point(base_angle, inner_radius, b, spiral_type)), + ) + } else { + let ref_angle = (self.spiral_slot as f64 - 1.) * TAU + base_angle; + let end_point_angle = if reversed { ref_angle - TAU } else { ref_angle + TAU }; + ( + viewport.transform_point2(spiral_point(ref_angle, inner_radius, b, spiral_type)), + viewport.transform_point2(spiral_point(end_point_angle, inner_radius, b, spiral_type)), + ) + }; + + Self::calculate_gizmo_line_points(start_point, end_point) + } + // (start_point,end_point) fn calculate_gizmo_line_points(start_point: DVec2, end_point: DVec2) -> (DVec2, DVec2) { let length = start_point.distance(end_point); - let factor = 0.25 * length; - let direction = (end_point - start_point).normalize(); + let direction = (end_point - start_point).normalize_or_zero(); - let new_endpoint = end_point - direction * factor; - let new_start_point = start_point + direction * factor; + let new_endpoint = end_point - direction * length; + let new_start_point = start_point + direction * length; (new_start_point, new_endpoint) } - pub fn update_number_of_turns(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { - let Some(layer) = self.layer else { - return; - }; - - let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface).or(graph_modification_utils::get_polygon_id(layer, &document.network_interface)) else { - return; - }; - - let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - let center = viewport_transform.transform_point2(DVec2::ZERO); + pub fn update_outer_radius_via_dashed_lines( + &mut self, + layer: LayerNodeIdentifier, + node_id: NodeId, + viewport_transform: DAffine2, + document: &DocumentMessageHandler, + input: &InputPreprocessorMessageHandler, + responses: &mut VecDeque, + drag_start: DVec2, + ) { let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); let previous_mouse_layer = viewport_transform.inverse().transform_point2(self.previous_mouse); let drag_start = viewport_transform.inverse().transform_point2(drag_start); @@ -268,25 +284,80 @@ impl TightnessGizmo { let sign = (current_mouse_layer - previous_mouse_layer).dot(drag_start - center_layer).signum(); let delta = delta_vector.length() * sign; + let reversed = self.inner_radius > self.initial_outer_radius; self.previous_mouse = input.mouse.position; - let (a, turns, net_radius) = match self.spiral_type { + let (a, outer_radius, turns, _) = extract_arc_or_log_spiral_parameters(layer, document).expect("Failed to get archimedean spiral inner radius"); + let (new_inner_radius, turns, new_outer_radius) = match self.spiral_type { SpiralType::Archimedean => { - let (a, outer_radius, turns) = extract_arc_spiral_parameters(layer, document).expect("Failed to get archimedean spiral inner radius"); - (a, turns, (outer_radius + delta).max(0.0)) + if reversed { + ((a + delta).max(0.), turns, outer_radius) + } else { + (a, turns, (outer_radius + delta).max(0.)) + } } SpiralType::Logarithmic => { - let (a, outer_radius, turns) = extract_log_spiral_parameters(layer, document).expect("Failed to get logarithmic spiral inner radius"); - (a, turns, (outer_radius + delta).max(0.001)) + if reversed { + ((a + delta).max(0.001), turns, outer_radius) + } else { + (a, turns, (outer_radius + delta).max(0.001)) + } } }; - self.gizmo_line_points = Self::check_which_inter_segment(current_mouse_layer, net_radius, turns, a, viewport_transform); + let b = calculate_b(new_inner_radius, turns, new_outer_radius, self.spiral_type); + self.gizmo_line_points = Some(self.calculate_updated_dash_lines(new_inner_radius, new_outer_radius, turns, self.spiral_type, viewport_transform, drag_start, reversed)); + + let (index, new_radius) = if reversed { + (SPIRAL_INNER_RADIUS_INDEX, new_inner_radius) + } else { + (SPIRAL_OUTER_RADIUS_INDEX, new_outer_radius) + }; responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input_connector: InputConnector::node(node_id, index), + input: NodeInput::value(TaggedValue::F64(new_radius), false), + }); + } + + pub fn update_outer_radius_via_circle(&mut self, node_id: NodeId, viewport_transform: DAffine2, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { + let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); + let net_radius = current_mouse_layer.distance(DVec2::ZERO); + + let net_radius = match self.spiral_type { + SpiralType::Archimedean => net_radius.max(0.), + SpiralType::Logarithmic => net_radius.max(0.001), + }; + + let index = if self.initial_outer_radius > self.inner_radius { + SPIRAL_OUTER_RADIUS_INDEX + } else { + SPIRAL_INNER_RADIUS_INDEX + }; + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, index), input: NodeInput::value(TaggedValue::F64(net_radius), false), }); + } + + pub fn update_outer_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { + let Some(layer) = self.layer else { + return; + }; + + let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { + return; + }; + + let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); + + match &self.gizmo_type { + TightnessGizmoType::Circle => self.update_outer_radius_via_circle(node_id, viewport_transform, input, responses), + TightnessGizmoType::DashLines => self.update_outer_radius_via_dashed_lines(layer, node_id, viewport_transform, document, input, responses, drag_start), + TightnessGizmoType::None => {} + } + responses.add(NodeGraphMessage::RunDocumentGraph); } } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs index 5d9bff779b..9bee1a1cb3 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs @@ -9,7 +9,7 @@ use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPre use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::{ - calculate_b, extract_arc_spiral_parameters, extract_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, + calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, }; use glam::DVec2; use graph_craft::document::NodeInput; @@ -75,17 +75,11 @@ impl SpiralTurns { 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 let Some(((inner_radius, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); + let end_point = viewport.transform_point2(spiral_point(turns * TAU, inner_radius, b, spiral_type)); 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); + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, spiral_type); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } 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 c11dc31100..575a54b2a7 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -1,5 +1,5 @@ use super::ShapeToolData; -use crate::consts::{SPIRAL_INNER_RADIUS, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX}; +use crate::consts::{SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_START_ANGLE, 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; @@ -232,33 +232,9 @@ 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)> { +pub fn extract_arc_or_log_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(f64, f64, f64, f64)> { let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; let Some(spiral_type) = get_spiral_type(layer, document) else { @@ -266,15 +242,16 @@ pub fn extract_log_spiral_parameters(layer: LayerNodeIdentifier, document: &Docu }; 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(), + let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns)), Some(&TaggedValue::F64(start_angle))) = ( + node_inputs.get(SPIRAL_INNER_RADIUS_INDEX)?.as_value(), node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_START_ANGLE)?.as_value(), ) else { return None; }; - return Some((inner_radius, tightness, turns)); + return Some((inner_radius, tightness, turns, start_angle)); } None @@ -291,7 +268,7 @@ pub fn get_spiral_type(layer: LayerNodeIdentifier, document: &DocumentMessageHan } 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 { + let Some((a, outer_radius, turns, _)) = extract_arc_or_log_spiral_parameters(layer, document) else { return None; }; @@ -303,7 +280,7 @@ pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentM } pub fn get_log_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { - let Some((_, outer_radius, turns)) = extract_log_spiral_parameters(layer, document) else { + let Some((_, outer_radius, turns, _)) = extract_arc_or_log_spiral_parameters(layer, document) else { return None; }; @@ -323,6 +300,14 @@ pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralTyp } } +/// 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 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; diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index ef1548cbc9..b1f3b7c927 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -5,7 +5,7 @@ use crate::messages::portfolio::document::node_graph::document_node_definitions: 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::arc_spiral_inner_radius_handle::{RadiusGizmo, RadiusGizmoState}; +use crate::messages::tool::common_functionality::gizmos::shape_gizmos::spiral_inner_radius_handle::{RadiusGizmo, RadiusGizmoState}; use crate::messages::tool::common_functionality::gizmos::shape_gizmos::spiral_tightness_gizmo::{TightnessGizmo, TightnessGizmoState}; use crate::messages::tool::common_functionality::gizmos::shape_gizmos::spiral_turns_handle::{SpiralTurns, SpiralTurnsState}; use crate::messages::tool::common_functionality::graph_modification_utils; @@ -52,6 +52,12 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { return; } + if self.turns_handle.hovered() && self.radius_handle.hovered() { + self.turns_handle.update_state(SpiralTurnsState::Dragging); + self.radius_handle.update_state(RadiusGizmoState::Inactive); + return; + } + if self.turns_handle.hovered() && self.tightness_handle.hovered() { self.turns_handle.update_state(SpiralTurnsState::Dragging); self.tightness_handle.update_state(TightnessGizmoState::Inactive); @@ -75,7 +81,7 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { if self.radius_handle.is_dragging() { - self.radius_handle.update_inner_radius(document, input, responses, drag_start); + self.radius_handle.update_inner_radius(drag_start, document, input, responses); } if self.turns_handle.is_dragging() { @@ -83,7 +89,7 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { } if self.tightness_handle.is_dragging() { - self.tightness_handle.update_number_of_turns(document, input, responses, drag_start); + self.tightness_handle.update_outer_radius(document, input, responses, drag_start); } } @@ -91,32 +97,36 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { &self, document: &DocumentMessageHandler, selected_spiral_layer: Option, - input: &InputPreprocessorMessageHandler, + _input: &InputPreprocessorMessageHandler, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext, ) { if self.radius_handle.hovered() && self.tightness_handle.hovered() { - self.radius_handle.overlays(document, selected_spiral_layer, input, mouse_position, overlay_context); + self.radius_handle.overlays(document, selected_spiral_layer, overlay_context); return; } - self.radius_handle.overlays(document, selected_spiral_layer, input, mouse_position, overlay_context); - self.turns_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); - self.tightness_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); - // polygon_outline(selected_polygon_layer, document, overlay_context); + if (self.turns_handle.hovered() && self.radius_handle.hovered()) || (self.turns_handle.hovered() && self.tightness_handle.hovered()) { + self.turns_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); + return; + } + + self.radius_handle.overlays(document, selected_spiral_layer, overlay_context); + self.turns_handle.overlays(document, selected_spiral_layer, shape_editor, mouse_position, overlay_context); + self.tightness_handle.overlays(document, selected_spiral_layer, shape_editor, overlay_context); } fn dragging_overlays( &self, document: &DocumentMessageHandler, - input: &InputPreprocessorMessageHandler, + _input: &InputPreprocessorMessageHandler, shape_editor: &mut &mut ShapeState, mouse_position: DVec2, overlay_context: &mut OverlayContext, ) { if self.radius_handle.is_dragging() { - self.radius_handle.overlays(document, None, input, mouse_position, overlay_context); + self.radius_handle.overlays(document, None, overlay_context); } if self.radius_handle.is_dragging() { @@ -124,7 +134,7 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { } if self.tightness_handle.is_dragging() { - self.tightness_handle.overlays(document, None, shape_editor, mouse_position, overlay_context); + self.tightness_handle.overlays(document, None, shape_editor, overlay_context); } } diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index 1f5106eeda..fd83334b2b 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -272,14 +272,14 @@ impl Subpath { ) } - pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, delta_theta: f64, spiral_type: SpiralType) -> Self { + 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; + let theta_end = turns * std::f64::consts::TAU + start_angle; let b = calculate_b(a, turns, outer_radius, spiral_type); - let mut theta = 0.0; + let mut theta = start_angle; while theta < theta_end { let theta_next = f64::min(theta + delta_theta, theta_end); diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index e04d1546ae..939d945f04 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -83,13 +83,21 @@ fn spiral( #[default(25)] outer_radius: f64, #[default(5.)] turns: f64, #[default(90.)] angle_offset: f64, + #[default(0.)] start_angle: 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))) + VectorDataTable::new(VectorData::from_subpath(Subpath::new_spiral( + inner_radius, + outer_radius, + turns, + start_angle.to_radians(), + angle_offset.to_radians(), + spiral_type, + ))) } #[node_macro::node(category("Vector: Shape"))] From 892c29b6a7fc8732fb71bb13fa40793c4e0dbd13 Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Fri, 18 Jul 2025 12:12:46 +0530 Subject: [PATCH 11/13] make outer and inner same gizmo and fix turns and tightness handle --- .../document/overlays/utility_types.rs | 74 +++++++ .../spiral_inner_radius_handle.rs | 52 +++-- .../shape_gizmos/spiral_tightness_gizmo.rs | 202 +++++++----------- .../shape_gizmos/spiral_turns_handle.rs | 100 +++++++-- .../shapes/shape_utility.rs | 57 +++-- .../shapes/spiral_shape.rs | 10 +- 6 files changed, 309 insertions(+), 186 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index ae05d2588d..f8a89d1ccc 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -372,6 +372,80 @@ impl OverlayContext { self.end_dpi_aware_transform(); } + #[allow(clippy::too_many_arguments)] + pub fn dashed_ellipse( + &mut self, + center: DVec2, + radius_x: f64, + radius_y: f64, + rotation: Option, + start_angle: Option, + end_angle: Option, + counterclockwise: Option, + color_fill: Option<&str>, + color_stroke: Option<&str>, + dash_width: Option, + dash_gap_width: Option, + dash_offset: Option, + ) { + let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE); + let center = center.round(); + + self.start_dpi_aware_transform(); + + if let Some(dash_width) = dash_width { + let dash_gap_width = dash_gap_width.unwrap_or(1.); + let array = js_sys::Array::new(); + array.push(&JsValue::from(dash_width)); + array.push(&JsValue::from(dash_gap_width)); + + if let Some(dash_offset) = dash_offset { + if dash_offset != 0. { + self.render_context.set_line_dash_offset(dash_offset); + } + } + + self.render_context + .set_line_dash(&JsValue::from(array)) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + + self.render_context.begin_path(); + self.render_context + .ellipse_with_anticlockwise( + center.x, + center.y, + radius_x, + radius_y, + rotation.unwrap_or_default(), + start_angle.unwrap_or_default(), + end_angle.unwrap_or(TAU), + counterclockwise.unwrap_or_default(), + ) + .expect("Failed to draw ellipse"); + self.render_context.set_stroke_style_str(color_stroke); + + if let Some(fill_color) = color_fill { + self.render_context.set_fill_style_str(fill_color); + self.render_context.fill(); + } + self.render_context.stroke(); + + // Reset the dash pattern back to solid + if dash_width.is_some() { + self.render_context + .set_line_dash(&JsValue::from(js_sys::Array::new())) + .map_err(|error| log::warn!("Error drawing dashed line: {:?}", error)) + .ok(); + } + if dash_offset.is_some() && dash_offset != Some(0.) { + self.render_context.set_line_dash_offset(0.); + } + + self.end_dpi_aware_transform(); + } + pub fn dashed_circle( &mut self, position: DVec2, diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs index 3f7bf6689e..da857a5a59 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs @@ -56,19 +56,44 @@ impl RadiusGizmo { pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque) { match &self.handle_state { RadiusGizmoState::Inactive => { - if let Some(((inner_radius, outer_radius, _, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { - let smaller_radius = (inner_radius.min(outer_radius)).max(5.); + if let Some(((inner_radius, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { let viewport = document.metadata().transform_to_viewport(layer); let layer_mouse = viewport.inverse().transform_point2(mouse_position); - if DVec2::ZERO.distance(layer_mouse) < smaller_radius.max(5.) { + let center = viewport.transform_point2(DVec2::ZERO); + + let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); + + let start_radius = spiral_point(0. + start_angle, inner_radius, b, spiral_type).distance(DVec2::ZERO); + let end_radius = spiral_point(turns * TAU + start_angle, inner_radius, b, spiral_type).distance(DVec2::ZERO); + + log::info!("start_radius {:?}", start_radius); + log::info!("end radius {:?}", end_radius); + + let larger_radius = (start_radius.max(end_radius)).max(5.); + let smaller_radius = (start_radius.min(end_radius)).max(5.); + + if layer_mouse.distance(DVec2::ZERO) < smaller_radius { + log::info!("reaching heeee"); self.layer = Some(layer); - self.initial_radius = inner_radius; + self.initial_radius = smaller_radius; self.spiral_type = spiral_type; self.previous_mouse_position = mouse_position; self.radius_index = if inner_radius > outer_radius { SPIRAL_OUTER_RADIUS_INDEX } else { SPIRAL_INNER_RADIUS_INDEX }; self.update_state(RadiusGizmoState::Hover); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + return; + } + + if (layer_mouse.distance(DVec2::ZERO) - larger_radius).abs() < 5. { + self.layer = Some(layer); + self.initial_radius = larger_radius; + self.spiral_type = spiral_type; + self.previous_mouse_position = mouse_position; + self.radius_index = if inner_radius > outer_radius { SPIRAL_INNER_RADIUS_INDEX } else { SPIRAL_OUTER_RADIUS_INDEX }; + self.update_state(RadiusGizmoState::Hover); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize }); + return; } } } @@ -82,16 +107,20 @@ impl RadiusGizmo { let Some(layer) = selected_spiral_layer.or(self.layer) else { return }; let viewport = document.metadata().transform_to_viewport(layer); - if let Some(((inner_radius, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + if let Some(((inner_radius, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let (radius, endpoint) = if self.radius_index == SPIRAL_INNER_RADIUS_INDEX { - (inner_radius, spiral_point(0., inner_radius, b, spiral_type)) + let endpoint = if self.radius_index == SPIRAL_INNER_RADIUS_INDEX { + spiral_point(0. + start_angle, inner_radius, b, spiral_type) } else { - (outer_radius, spiral_point(turns * TAU, inner_radius, b, spiral_type)) + spiral_point(turns * TAU + start_angle, inner_radius, b, spiral_type) }; + let viewport_center = viewport.transform_point2(DVec2::ZERO); + + let radius = viewport.transform_point2(endpoint).distance(viewport_center); + overlay_context.manipulator_handle(viewport.transform_point2(endpoint), true, Some(COLOR_OVERLAY_RED)); - overlay_context.dashed_circle(DVec2::ZERO, radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); + overlay_context.dashed_ellipse(viewport_center, radius, radius, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5)); } } _ => {} @@ -110,19 +139,14 @@ impl RadiusGizmo { .expect("Failed to find inputs of Spiral"); let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - - let center = DVec2::ZERO; let layer_drag_start = viewport_transform.inverse().transform_point2(drag_start); let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); let previous_mouse_layer = viewport_transform.inverse().transform_point2(self.previous_mouse_position); let sign = (current_mouse_layer - previous_mouse_layer).dot(layer_drag_start).signum(); - let delta = current_mouse_layer.distance(previous_mouse_layer) * sign; - let net_radius = current_mouse_layer.distance(DVec2::ZERO); - let Some(&TaggedValue::F64(radius)) = node_inputs.get(self.radius_index).expect("Failed to get radius of Spiral").as_value() else { return; }; diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs index 5b61b4e996..94d121f569 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs @@ -1,4 +1,4 @@ -use crate::consts::{COLOR_OVERLAY_RED, SPIRAL_INNER_RADIUS_INDEX, SPIRAL_INNER_RADIUS_INDEX_GIZMO_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX}; +use crate::consts::{SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -8,15 +8,12 @@ use crate::messages::prelude::Responses; use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPreprocessorMessageHandler, NodeGraphMessage}; use crate::messages::tool::common_functionality::graph_modification_utils::{self}; use crate::messages::tool::common_functionality::shape_editor::ShapeState; -use crate::messages::tool::common_functionality::shapes::shape_utility::{ - calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, -}; +use crate::messages::tool::common_functionality::shapes::shape_utility::{calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_spiral_type, spiral_point}; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; use graphene_std::uuid::NodeId; -use graphene_std::vector::misc::{SpiralType, dvec2_to_point}; -use kurbo::{Line, ParamCurveNearest}; +use graphene_std::vector::misc::SpiralType; use std::collections::VecDeque; use std::f64::consts::TAU; @@ -28,14 +25,6 @@ pub enum TightnessGizmoState { Dragging, } -#[derive(Clone, Debug, Default, PartialEq)] -enum TightnessGizmoType { - #[default] - None, - Circle, - DashLines, -} - #[derive(Clone, Debug, Default)] pub struct TightnessGizmo { pub layer: Option, @@ -47,7 +36,6 @@ pub struct TightnessGizmo { angle: f64, spiral_slot: i32, previous_mouse: DVec2, - gizmo_type: TightnessGizmoType, } impl TightnessGizmo { @@ -55,7 +43,6 @@ impl TightnessGizmo { self.handle_state = TightnessGizmoState::Inactive; self.layer = None; self.gizmo_line_points = None; - self.gizmo_type = TightnessGizmoType::None; } pub fn update_state(&mut self, state: TightnessGizmoState) { @@ -76,8 +63,10 @@ impl TightnessGizmo { match &self.handle_state { TightnessGizmoState::Inactive => { // Archimedean - if let Some(((a, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { - if let Some((start, end, slot_index)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), outer_radius, turns, a, spiral_type, viewport) { + if let Some(((a, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + if let Some((start, end, slot_index)) = + Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), a, outer_radius, turns, start_angle, spiral_type, viewport) + { self.layer = Some(layer); self.initial_outer_radius = outer_radius; self.previous_mouse = mouse_position; @@ -85,34 +74,12 @@ impl TightnessGizmo { self.spiral_type = spiral_type; self.spiral_slot = slot_index; self.inner_radius = a; - self.gizmo_type = TightnessGizmoType::DashLines; self.angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X).rem_euclid(TAU); self.update_state(TightnessGizmoState::Hover); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); return; }; - - let center = viewport.transform_point2(DVec2::ZERO); - - let angle = if a > outer_radius { 0. } else { TAU }; - - let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, angle).or(get_log_spiral_end_point(layer, document, viewport, angle)) else { - return; - }; - - let close_to_circle = (endpoint.distance(center) - mouse_position.distance(center)).abs() < 5.; - - if close_to_circle { - self.layer = Some(layer); - self.inner_radius = a; - self.initial_outer_radius = outer_radius; - self.previous_mouse = mouse_position; - self.gizmo_type = TightnessGizmoType::Circle; - self.spiral_type = spiral_type; - self.update_state(TightnessGizmoState::Hover); - responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); - } } } TightnessGizmoState::Hover | TightnessGizmoState::Dragging => {} @@ -126,35 +93,43 @@ impl TightnessGizmo { let viewport = document.metadata().transform_to_viewport(layer); match &self.handle_state { - TightnessGizmoState::Hover | TightnessGizmoState::Dragging => match self.gizmo_type { - TightnessGizmoType::Circle => { - if let Some((inner_radius, outer_radius, _, _)) = extract_arc_or_log_spiral_parameters(layer, document) { - let required_radius = if self.inner_radius > self.initial_outer_radius { inner_radius } else { outer_radius }; - overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(8.), Some(4.), Some(0.5), Some(viewport)); + TightnessGizmoState::Hover | TightnessGizmoState::Dragging => { + if let Some((start, end)) = self.gizmo_line_points { + overlay_context.dashed_line(start, end, None, None, Some(4.0), Some(4.0), Some(0.5)); + if self.spiral_slot == 0 { + let required_radius = if self.inner_radius > self.initial_outer_radius { + viewport + .inverse() + .transform_point2(get_arc_spiral_end_point(layer, document, viewport, TAU).expect("Failed to get endpoints")) + .distance(DVec2::ZERO) + } else { + viewport + .inverse() + .transform_point2(get_arc_spiral_end_point(layer, document, viewport, 0.).expect("Failed to get endpoints")) + .distance(DVec2::ZERO) + }; + overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); } - } - TightnessGizmoType::DashLines => { - if let Some((start, end)) = self.gizmo_line_points { - overlay_context.dashed_line(start, end, None, None, Some(4.0), Some(4.0), Some(0.5)); - if self.spiral_slot == 0 { - let required_radius = if self.inner_radius > self.initial_outer_radius { - self.initial_outer_radius - } else { - self.inner_radius - }; - overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); - } - }; - } - TightnessGizmoType::None => {} - }, + }; + } + TightnessGizmoState::Inactive => {} } } - fn check_which_inter_segment(layer_mouse_position: DVec2, outer_radius: f64, turns: f64, inner_radius: f64, spiral_type: SpiralType, viewport: DAffine2) -> Option<(DVec2, DVec2, i32)> { + fn check_which_inter_segment( + layer_mouse_position: DVec2, + inner_radius: f64, + outer_radius: f64, + turns: f64, + start_angle: f64, + spiral_type: SpiralType, + viewport: DAffine2, + ) -> Option<(DVec2, DVec2, i32)> { let center = DVec2::ZERO; - let mut angle = layer_mouse_position.angle_to(DVec2::X).rem_euclid(TAU); + let angle = layer_mouse_position.angle_to(DVec2::X).rem_euclid(TAU); + let start_angle_rad = start_angle.to_radians(); + let spiral_theta = (angle - start_angle_rad).rem_euclid(TAU); let is_reversed = inner_radius > outer_radius; let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); @@ -163,54 +138,52 @@ impl TightnessGizmo { let viewport_mouse = viewport.transform_point2(layer_mouse_position); let viewport_center = viewport.transform_point2(center); - // Compute spiral endpoints at θ = 0 and θ = max let spiral_outer = spiral_point(max_theta, inner_radius, b, spiral_type); - let spiral_inner = spiral_point(0., inner_radius, b, spiral_type); - let viewport_outer = viewport.transform_point2(spiral_outer); - let viewport_inner = viewport.transform_point2(spiral_inner); - - let smaller_radius = inner_radius.min(outer_radius); - let adjusted_angle = if is_reversed { max_theta - (TAU - angle) } else { angle }; + let spiral_inner = spiral_point(0.0, inner_radius, b, spiral_type); + let viewport_outer = viewport.transform_point2(Self::rotate_point(spiral_outer, start_angle_rad)); + let viewport_inner = viewport.transform_point2(Self::rotate_point(spiral_inner, start_angle_rad)); let required_endpoint = if is_reversed { viewport_inner } else { viewport_outer }; + let mouse_distance = viewport_mouse.distance(viewport_center); + let max_radius = required_endpoint.distance(viewport_center); - // Reject if mouse is beyond spiral's radial extent - if viewport_mouse.distance(viewport_center) > required_endpoint.distance(viewport_center) { + if mouse_distance > max_radius { return None; } - let mouse_distance = viewport_mouse.distance(viewport_center); let mut segment_index = 0; - // First segment: from center to first spiral point at θ = adjusted_angle + // First segment: from center to first spiral point at spiral_theta { - let spiral_end = spiral_point(adjusted_angle, inner_radius, b, spiral_type); - let first_point = viewport.transform_point2(spiral_end); + let spiral_end = spiral_point(spiral_theta, inner_radius, b, spiral_type); + let first_point = viewport.transform_point2(Self::rotate_point(spiral_end, start_angle_rad)); + let r_end = first_point.distance(viewport_center); if mouse_distance <= r_end { - let direction = DVec2::new(adjusted_angle.cos(), -adjusted_angle.sin()); - return Some((viewport.transform_point2(smaller_radius.max(5.) * direction), first_point, segment_index)); + let direction = DVec2::new(angle.cos(), -angle.sin()); + let inner_point = viewport.transform_point2(inner_radius.max(5.0) * direction); + return Some((inner_point, first_point, segment_index)); } segment_index += 1; } - // Loop through each full turn segment along the spiral ray - let mut base_theta = adjusted_angle; - while if is_reversed { base_theta >= 0. } else { base_theta <= max_theta } { + // Remaining segments: full spiral loops + let mut base_theta = spiral_theta; + while if is_reversed { base_theta >= 0.0 } else { base_theta <= max_theta } { let theta_start = base_theta; let theta_end = if is_reversed { base_theta - TAU } else { base_theta + TAU }; - if (!is_reversed && theta_end > max_theta) || (is_reversed && theta_end < 0.) { + if (!is_reversed && theta_end > max_theta) || (is_reversed && theta_end < 0.0) { break; } let spiral_start = spiral_point(theta_start, inner_radius, b, spiral_type); let spiral_end = spiral_point(theta_end, inner_radius, b, spiral_type); - let viewport_start = viewport.transform_point2(spiral_start); - let viewport_end = viewport.transform_point2(spiral_end); + let viewport_start = viewport.transform_point2(Self::rotate_point(spiral_start, start_angle_rad)); + let viewport_end = viewport.transform_point2(Self::rotate_point(spiral_end, start_angle_rad)); let r_start = viewport_start.distance(viewport_center); let r_end = viewport_end.distance(viewport_center); @@ -221,21 +194,38 @@ impl TightnessGizmo { } base_theta = if is_reversed { base_theta - TAU } else { base_theta + TAU }; - segment_index += 1; } None } - pub fn calculate_updated_dash_lines(&self, inner_radius: f64, outer_radius: f64, turns: f64, spiral_type: SpiralType, viewport: DAffine2, drag_start: DVec2, reversed: bool) -> (DVec2, DVec2) { + fn rotate_point(p: DVec2, angle: f64) -> DVec2 { + let cos_a = angle.cos(); + let sin_a = angle.sin(); + DVec2::new(p.x * cos_a - p.y * sin_a, p.x * sin_a + p.y * cos_a) + } + + pub fn calculate_updated_dash_lines( + &self, + inner_radius: f64, + outer_radius: f64, + turns: f64, + start_angle: f64, + spiral_type: SpiralType, + viewport: DAffine2, + _drag_start: DVec2, + reversed: bool, + ) -> (DVec2, DVec2) { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let max_theta = turns * TAU; - let base_angle = if reversed { max_theta - (TAU - self.angle) } else { self.angle }; + let max_theta = turns * TAU + start_angle.to_radians(); + let base_angle = if reversed { + max_theta - (TAU - self.angle - start_angle.to_radians()) + } else { + self.angle - start_angle.to_radians() + }; let smaller_radius = inner_radius.min(outer_radius); - let center = DVec2::ZERO; - let (start_point, end_point) = if self.spiral_slot == 0 { ( viewport.transform_point2(smaller_radius * DVec2::new(base_angle.cos(), -base_angle.sin())), @@ -287,7 +277,7 @@ impl TightnessGizmo { let reversed = self.inner_radius > self.initial_outer_radius; self.previous_mouse = input.mouse.position; - let (a, outer_radius, turns, _) = extract_arc_or_log_spiral_parameters(layer, document).expect("Failed to get archimedean spiral inner radius"); + let (a, outer_radius, turns, start_angle) = extract_arc_or_log_spiral_parameters(layer, document).expect("Failed to get archimedean spiral inner radius"); let (new_inner_radius, turns, new_outer_radius) = match self.spiral_type { SpiralType::Archimedean => { if reversed { @@ -305,8 +295,7 @@ impl TightnessGizmo { } }; - let b = calculate_b(new_inner_radius, turns, new_outer_radius, self.spiral_type); - self.gizmo_line_points = Some(self.calculate_updated_dash_lines(new_inner_radius, new_outer_radius, turns, self.spiral_type, viewport_transform, drag_start, reversed)); + self.gizmo_line_points = Some(self.calculate_updated_dash_lines(new_inner_radius, new_outer_radius, turns, start_angle, self.spiral_type, viewport_transform, drag_start, reversed)); let (index, new_radius) = if reversed { (SPIRAL_INNER_RADIUS_INDEX, new_inner_radius) @@ -320,27 +309,6 @@ impl TightnessGizmo { }); } - pub fn update_outer_radius_via_circle(&mut self, node_id: NodeId, viewport_transform: DAffine2, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque) { - let current_mouse_layer = viewport_transform.inverse().transform_point2(input.mouse.position); - let net_radius = current_mouse_layer.distance(DVec2::ZERO); - - let net_radius = match self.spiral_type { - SpiralType::Archimedean => net_radius.max(0.), - SpiralType::Logarithmic => net_radius.max(0.001), - }; - - let index = if self.initial_outer_radius > self.inner_radius { - SPIRAL_OUTER_RADIUS_INDEX - } else { - SPIRAL_INNER_RADIUS_INDEX - }; - - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, index), - input: NodeInput::value(TaggedValue::F64(net_radius), false), - }); - } - pub fn update_outer_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, drag_start: DVec2) { let Some(layer) = self.layer else { return; @@ -352,11 +320,7 @@ impl TightnessGizmo { let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer); - match &self.gizmo_type { - TightnessGizmoType::Circle => self.update_outer_radius_via_circle(node_id, viewport_transform, input, responses), - TightnessGizmoType::DashLines => self.update_outer_radius_via_dashed_lines(layer, node_id, viewport_transform, document, input, responses, drag_start), - TightnessGizmoType::None => {} - } + self.update_outer_radius_via_dashed_lines(layer, node_id, viewport_transform, document, input, responses, drag_start); responses.add(NodeGraphMessage::RunDocumentGraph); } diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs index 9bee1a1cb3..17d522f374 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs @@ -1,4 +1,4 @@ -use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_TURNS_INDEX}; +use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_START_ANGLE, SPIRAL_TURNS_INDEX}; use crate::messages::frontend::utility_types::MouseCursorIcon; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; @@ -9,7 +9,7 @@ use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPre use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::{ - calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, + calculate_b, extract_arc_or_log_spiral_parameters, get_arc_or_log_spiral_endpoints, get_arc_spiral_end_point, get_log_spiral_end_point, get_spiral_type, spiral_point, }; use glam::DVec2; use graph_craft::document::NodeInput; @@ -18,6 +18,14 @@ use graphene_std::vector::misc::SpiralType; use std::collections::VecDeque; use std::f64::consts::TAU; +#[derive(Clone, Debug, Default, PartialEq)] +pub enum GizmoType { + #[default] + None, + Start, + End, +} + #[derive(Clone, Debug, Default, PartialEq)] pub enum SpiralTurnsState { #[default] @@ -34,8 +42,10 @@ pub struct SpiralTurns { initial_outer_radius: f64, initial_inner_radius: f64, initial_b: f64, + initial_start_angle: f64, previous_mouse_position: DVec2, total_angle_delta: f64, + gizmo_type: GizmoType, spiral_type: SpiralType, } @@ -43,6 +53,7 @@ impl SpiralTurns { pub fn cleanup(&mut self) { self.handle_state = SpiralTurnsState::Inactive; self.total_angle_delta = 0.; + self.gizmo_type = GizmoType::None; self.layer = None; } @@ -58,14 +69,26 @@ impl SpiralTurns { self.handle_state == SpiralTurnsState::Dragging } - pub fn store_initial_parameters(&mut self, layer: LayerNodeIdentifier, a: f64, turns: f64, outer_radius: f64, mouse_position: DVec2, spiral_type: SpiralType) { + pub fn store_initial_parameters( + &mut self, + layer: LayerNodeIdentifier, + a: f64, + turns: f64, + outer_radius: f64, + mouse_position: DVec2, + start_angle: f64, + gizmo_type: GizmoType, + spiral_type: SpiralType, + ) { self.layer = Some(layer); self.initial_turns = turns; self.initial_b = calculate_b(a, turns, outer_radius, spiral_type); self.initial_inner_radius = a; self.initial_outer_radius = outer_radius; + self.initial_start_angle = start_angle; self.previous_mouse_position = mouse_position; self.spiral_type = spiral_type; + self.gizmo_type = gizmo_type; self.update_state(SpiralTurnsState::Hover); } @@ -75,12 +98,21 @@ impl SpiralTurns { match &self.handle_state { SpiralTurnsState::Inactive => { // Archimedean - if let Some(((inner_radius, outer_radius, turns, _), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { + if let Some(((inner_radius, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let end_point = viewport.transform_point2(spiral_point(turns * TAU, inner_radius, b, spiral_type)); + let end_point = viewport.transform_point2(spiral_point(turns * TAU + start_angle.to_radians(), inner_radius, b, spiral_type)); + let start_point = viewport.transform_point2(spiral_point(0. + start_angle.to_radians(), inner_radius, b, spiral_type)); + if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { - self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, spiral_type); + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, start_angle, GizmoType::End, spiral_type); + responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + return; + } + + if mouse_position.distance(start_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD { + self.store_initial_parameters(layer, inner_radius, turns, outer_radius, mouse_position, start_angle, GizmoType::Start, spiral_type); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); + return; } } } @@ -97,15 +129,15 @@ impl SpiralTurns { // Is true only when hovered over the gizmo let selected = self.layer.is_some(); - if let Some(endpoint) = get_arc_spiral_end_point(layer, document, viewport, TAU) { - overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED)); - return; + let angle = match self.gizmo_type { + GizmoType::End => TAU, + GizmoType::Start => 0., + GizmoType::None => return, }; - if let Some(endpoint) = get_log_spiral_end_point(layer, document, viewport, TAU) { + if let Some(endpoint) = get_arc_or_log_spiral_endpoints(layer, document, viewport, angle) { overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED)); - return; - }; + } } } } @@ -132,22 +164,46 @@ impl SpiralTurns { // Calculate the new outer radius based on spiral type and turn change let outer_radius_change = match self.spiral_type { SpiralType::Archimedean => turns_delta * (self.initial_b) * TAU, - SpiralType::Logarithmic => self.initial_inner_radius * (self.initial_b * TAU * turns_delta).exp(), + SpiralType::Logarithmic => self.initial_outer_radius * ((self.initial_b * TAU * turns_delta).exp() - 1.), }; let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else { return; }; - responses.add(NodeGraphMessage::SetInput { - input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), - input: NodeInput::value(TaggedValue::F64(self.initial_turns + turns_delta), false), - }); - - 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), - }); + match self.gizmo_type { + GizmoType::Start => { + let sign = total_delta.signum() * -1.; + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_START_ANGLE), + input: NodeInput::value(TaggedValue::F64(self.initial_start_angle + total_delta), false), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_turns + turns_delta * sign), false), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_outer_radius + outer_radius_change * sign), false), + }); + } + GizmoType::End => { + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_TURNS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_turns + turns_delta), false), + }); + + responses.add(NodeGraphMessage::SetInput { + input_connector: InputConnector::node(node_id, SPIRAL_OUTER_RADIUS_INDEX), + input: NodeInput::value(TaggedValue::F64(self.initial_outer_radius + outer_radius_change), false), + }); + } + GizmoType::None => { + return; + } + } responses.add(NodeGraphMessage::RunDocumentGraph); diff --git a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs index 575a54b2a7..86db9c796b 100644 --- a/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs +++ b/editor/src/messages/tool/common_functionality/shapes/shape_utility.rs @@ -1,5 +1,5 @@ use super::ShapeToolData; -use crate::consts::{SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_START_ANGLE, SPIRAL_TURNS_INDEX}; +use crate::consts::{SPIRAL_INNER_RADIUS_INDEX, SPIRAL_OUTER_RADIUS_INDEX, SPIRAL_START_ANGLE, SPIRAL_TURNS_INDEX, SPIRAL_TYPE_INDEX}; use crate::messages::message::Message; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -237,42 +237,42 @@ pub fn extract_star_parameters(layer: Option, document: &Do pub fn extract_arc_or_log_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(f64, f64, f64, f64)> { let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; - let Some(spiral_type) = get_spiral_type(layer, document) else { + let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns)), Some(&TaggedValue::F64(start_angle))) = ( + node_inputs.get(SPIRAL_INNER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), + node_inputs.get(SPIRAL_START_ANGLE)?.as_value(), + ) else { return None; }; - if spiral_type == SpiralType::Logarithmic { - let (Some(&TaggedValue::F64(inner_radius)), Some(&TaggedValue::F64(tightness)), Some(&TaggedValue::F64(turns)), Some(&TaggedValue::F64(start_angle))) = ( - node_inputs.get(SPIRAL_INNER_RADIUS_INDEX)?.as_value(), - node_inputs.get(SPIRAL_OUTER_RADIUS_INDEX)?.as_value(), - node_inputs.get(SPIRAL_TURNS_INDEX)?.as_value(), - node_inputs.get(SPIRAL_START_ANGLE)?.as_value(), - ) else { - return None; - }; - - return Some((inner_radius, tightness, turns, start_angle)); - } - - None + return Some((inner_radius, tightness, turns, start_angle)); } pub fn get_spiral_type(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option { let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral")?; - let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(1).expect("Failed to get Spiral Type").as_value() else { + let Some(&TaggedValue::SpiralType(spiral_type)) = node_inputs.get(SPIRAL_TYPE_INDEX).expect("Failed to get Spiral Type").as_value() else { return None; }; Some(spiral_type) } +pub fn get_arc_or_log_spiral_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { + let Some(spiral_type) = get_spiral_type(layer, document) else { return None }; + return match spiral_type { + SpiralType::Archimedean => get_arc_spiral_end_point(layer, document, viewport, theta), + SpiralType::Logarithmic => get_log_spiral_end_point(layer, document, viewport, theta), + }; +} + pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { - let Some((a, outer_radius, turns, _)) = extract_arc_or_log_spiral_parameters(layer, document) else { + let Some((a, outer_radius, turns, start_angle)) = extract_arc_or_log_spiral_parameters(layer, document) else { return None; }; - let theta = turns * theta; + let theta = turns * theta + start_angle.to_radians(); let b = calculate_b(a, turns, outer_radius, SpiralType::Archimedean); let r = a + b * theta; @@ -280,28 +280,27 @@ pub fn get_arc_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentM } pub fn get_log_spiral_end_point(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option { - let Some((_, outer_radius, turns, _)) = extract_arc_or_log_spiral_parameters(layer, document) else { + let Some((a, outer_radius, turns, start_angle)) = extract_arc_or_log_spiral_parameters(layer, document) else { return None; }; - Some(viewport.transform_point2(outer_radius * DVec2::new((turns * theta).cos(), -(turns * theta).sin()))) + let theta = turns * theta + start_angle.to_radians(); + let b = calculate_b(a, turns, outer_radius, SpiralType::Logarithmic); + let r = a * (b * theta).exp(); + Some(viewport.transform_point2(DVec2::new(r * theta.cos(), -r * theta.sin()))) } pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 { + let total_theta = turns * TAU; match spiral_type { - SpiralType::Archimedean => { - let total_theta = turns * TAU; - (outer_radius - a) / total_theta - } - SpiralType::Logarithmic => { - let total_theta = turns * TAU; - ((outer_radius.abs() / a).ln()) / total_theta - } + SpiralType::Archimedean => (outer_radius - a) / total_theta, + SpiralType::Logarithmic => ((outer_radius.abs() / a).ln()) / total_theta, } } /// Returns a point on the given spiral type at angle `theta`. pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 { + let theta = theta; match spiral_type { SpiralType::Archimedean => archimedean_spiral_point(theta, a, b), SpiralType::Logarithmic => log_spiral_point(theta, a, b), diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index b1f3b7c927..d56a67cce9 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -41,9 +41,9 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) { - self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); + // self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); self.turns_handle.handle_actions(selected_spiral_layer, mouse_position, document, responses); - self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); + // self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); } fn handle_click(&mut self) { @@ -196,6 +196,8 @@ impl Spiral { SpiralType::Logarithmic => (dragged_distance).max(0.1), }; + let angle = ipp.mouse.position.angle_to(DVec2::X); + responses.add(GraphOperationMessage::TransformSet { layer, transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., viewport_drag_start), @@ -228,3 +230,7 @@ impl Spiral { } } } + +pub fn calculate_circle_point(theta: f64, radius: f64) -> DVec2 { + radius * DVec2::new(theta.cos(), -theta.sin()) +} From 6cf7d4d127f2b55a5f153032500f96e21d6f08fb Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Mon, 21 Jul 2025 01:59:36 +0530 Subject: [PATCH 12/13] need to fix the updated dash lines --- .../spiral_inner_radius_handle.rs | 14 +- .../shape_gizmos/spiral_tightness_gizmo.rs | 174 ++++++++++++------ .../shapes/spiral_shape.rs | 4 +- 3 files changed, 128 insertions(+), 64 deletions(-) diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs index da857a5a59..ea60b9cc5a 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_inner_radius_handle.rs @@ -64,17 +64,14 @@ impl RadiusGizmo { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let start_radius = spiral_point(0. + start_angle, inner_radius, b, spiral_type).distance(DVec2::ZERO); - let end_radius = spiral_point(turns * TAU + start_angle, inner_radius, b, spiral_type).distance(DVec2::ZERO); - - log::info!("start_radius {:?}", start_radius); - log::info!("end radius {:?}", end_radius); + let start_radius = spiral_point(0. + start_angle.to_radians(), inner_radius, b, spiral_type).distance(DVec2::ZERO); + let end_radius = spiral_point(turns * TAU + start_angle.to_radians(), inner_radius, b, spiral_type).distance(DVec2::ZERO); let larger_radius = (start_radius.max(end_radius)).max(5.); let smaller_radius = (start_radius.min(end_radius)).max(5.); if layer_mouse.distance(DVec2::ZERO) < smaller_radius { - log::info!("reaching heeee"); + log::info!("reachinhere inner radius"); self.layer = Some(layer); self.initial_radius = smaller_radius; self.spiral_type = spiral_type; @@ -86,6 +83,7 @@ impl RadiusGizmo { } if (layer_mouse.distance(DVec2::ZERO) - larger_radius).abs() < 5. { + log::info!("reaching bigger radius"); self.layer = Some(layer); self.initial_radius = larger_radius; self.spiral_type = spiral_type; @@ -110,9 +108,9 @@ impl RadiusGizmo { if let Some(((inner_radius, outer_radius, turns, start_angle), spiral_type)) = extract_arc_or_log_spiral_parameters(layer, document).zip(get_spiral_type(layer, document)) { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); let endpoint = if self.radius_index == SPIRAL_INNER_RADIUS_INDEX { - spiral_point(0. + start_angle, inner_radius, b, spiral_type) + spiral_point(0. + start_angle.to_radians(), inner_radius, b, spiral_type) } else { - spiral_point(turns * TAU + start_angle, inner_radius, b, spiral_type) + spiral_point(turns * TAU + start_angle.to_radians(), inner_radius, b, spiral_type) }; let viewport_center = viewport.transform_point2(DVec2::ZERO); diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs index 94d121f569..8588b45a22 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs @@ -35,6 +35,7 @@ pub struct TightnessGizmo { inner_radius: f64, angle: f64, spiral_slot: i32, + turns: f64, previous_mouse: DVec2, } @@ -67,12 +68,14 @@ impl TightnessGizmo { if let Some((start, end, slot_index)) = Self::check_which_inter_segment(viewport.inverse().transform_point2(mouse_position), a, outer_radius, turns, start_angle, spiral_type, viewport) { + log::info!("check which inter-segment {:?}", slot_index); self.layer = Some(layer); self.initial_outer_radius = outer_radius; self.previous_mouse = mouse_position; self.gizmo_line_points = Some((start, end)); self.spiral_type = spiral_type; self.spiral_slot = slot_index; + self.turns = turns; self.inner_radius = a; self.angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X).rem_euclid(TAU); self.update_state(TightnessGizmoState::Hover); @@ -94,20 +97,22 @@ impl TightnessGizmo { match &self.handle_state { TightnessGizmoState::Hover | TightnessGizmoState::Dragging => { + let is_reversed = self.inner_radius > self.initial_outer_radius; if let Some((start, end)) = self.gizmo_line_points { overlay_context.dashed_line(start, end, None, None, Some(4.0), Some(4.0), Some(0.5)); - if self.spiral_slot == 0 { - let required_radius = if self.inner_radius > self.initial_outer_radius { - viewport - .inverse() - .transform_point2(get_arc_spiral_end_point(layer, document, viewport, TAU).expect("Failed to get endpoints")) - .distance(DVec2::ZERO) - } else { - viewport - .inverse() - .transform_point2(get_arc_spiral_end_point(layer, document, viewport, 0.).expect("Failed to get endpoints")) - .distance(DVec2::ZERO) - }; + if self.spiral_slot == 0 && !is_reversed { + let required_radius = viewport + .inverse() + .transform_point2(get_arc_spiral_end_point(layer, document, viewport, 0.).expect("Failed to get endpoints")) + .distance(DVec2::ZERO); + overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); + } + + if self.spiral_slot == self.turns.floor() as i32 && is_reversed { + let required_radius = viewport + .inverse() + .transform_point2(get_arc_spiral_end_point(layer, document, viewport, TAU).expect("Failed to get endpoints")) + .distance(DVec2::ZERO); overlay_context.dashed_circle(DVec2::ZERO, required_radius.max(5.), None, None, Some(4.), Some(4.), Some(0.5), Some(viewport)); } }; @@ -129,19 +134,28 @@ impl TightnessGizmo { let center = DVec2::ZERO; let angle = layer_mouse_position.angle_to(DVec2::X).rem_euclid(TAU); let start_angle_rad = start_angle.to_radians(); - let spiral_theta = (angle - start_angle_rad).rem_euclid(TAU); + let normalized_start_angle = start_angle_rad.rem_euclid(TAU); let is_reversed = inner_radius > outer_radius; let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); let max_theta = turns * TAU; + let spiral_theta = if is_reversed { + angle + ((start_angle.to_radians() / TAU).floor() * TAU) + turns.floor() * TAU + } else { + angle + ((start_angle.to_radians() / TAU).floor() * TAU) + }; + let viewport_mouse = viewport.transform_point2(layer_mouse_position); let viewport_center = viewport.transform_point2(center); - let spiral_outer = spiral_point(max_theta, inner_radius, b, spiral_type); - let spiral_inner = spiral_point(0.0, inner_radius, b, spiral_type); - let viewport_outer = viewport.transform_point2(Self::rotate_point(spiral_outer, start_angle_rad)); - let viewport_inner = viewport.transform_point2(Self::rotate_point(spiral_inner, start_angle_rad)); + let spiral_outer = spiral_point(max_theta + start_angle_rad, inner_radius, b, spiral_type); + let spiral_inner = spiral_point(0. + start_angle_rad, inner_radius, b, spiral_type); + let viewport_outer = viewport.transform_point2(spiral_outer); + let viewport_inner = viewport.transform_point2(spiral_inner); + + let first_segment = spiral_point(angle + ((start_angle.to_radians() / TAU).floor() * TAU), inner_radius, b, spiral_type); + let fist_segment_point_distance = viewport.transform_point2(first_segment).distance(viewport_center); let required_endpoint = if is_reversed { viewport_inner } else { viewport_outer }; let mouse_distance = viewport_mouse.distance(viewport_center); @@ -151,61 +165,112 @@ impl TightnessGizmo { return None; } - let mut segment_index = 0; + if is_reversed && mouse_distance > fist_segment_point_distance { + return None; + } - // First segment: from center to first spiral point at spiral_theta - { + let mut segment_index = if is_reversed { turns.floor() as i32 } else { 0 }; + + if !is_reversed { let spiral_end = spiral_point(spiral_theta, inner_radius, b, spiral_type); - let first_point = viewport.transform_point2(Self::rotate_point(spiral_end, start_angle_rad)); + let first_point = viewport.transform_point2(spiral_end); let r_end = first_point.distance(viewport_center); if mouse_distance <= r_end { let direction = DVec2::new(angle.cos(), -angle.sin()); - let inner_point = viewport.transform_point2(inner_radius.max(5.0) * direction); + let radius = if is_reversed { spiral_outer.distance(DVec2::ZERO) } else { spiral_inner.distance(DVec2::ZERO) }; + let inner_point = viewport.transform_point2(radius.max(5.0) * direction); return Some((inner_point, first_point, segment_index)); } + if angle <= normalized_start_angle { + let spiral_end = spiral_point(spiral_theta + TAU, inner_radius, b, spiral_type); + let first_point = viewport.transform_point2(spiral_end); + if mouse_distance <= first_point.distance(viewport_center) { + let direction = DVec2::new(angle.cos(), -angle.sin()); + + let radius = if is_reversed { spiral_outer.distance(DVec2::ZERO) } else { spiral_inner.distance(DVec2::ZERO) }; + let inner_point = viewport.transform_point2(radius.max(5.0) * direction); + + return Some((inner_point, first_point, segment_index)); + } + } + segment_index += 1; + } else { + let spiral_end = spiral_point(spiral_theta, inner_radius, b, spiral_type); + let first_point = viewport.transform_point2(spiral_end); + + let r_end = first_point.distance(viewport_center); + + if mouse_distance <= r_end { + let direction = DVec2::new(angle.cos(), -angle.sin()); + let radius = if is_reversed { spiral_outer.distance(DVec2::ZERO) } else { spiral_inner.distance(DVec2::ZERO) }; + let inner_point = viewport.transform_point2(radius.max(5.0) * direction); + return Some((inner_point, first_point, segment_index)); + } + + if angle >= (max_theta + start_angle_rad).rem_euclid(TAU) { + let spiral_end = spiral_point(spiral_theta - TAU, inner_radius, b, spiral_type); + let first_point = viewport.transform_point2(spiral_end); + if mouse_distance <= first_point.distance(viewport_center) { + let direction = DVec2::new(angle.cos(), -angle.sin()); + + let radius = if is_reversed { spiral_outer.distance(DVec2::ZERO) } else { spiral_inner.distance(DVec2::ZERO) }; + let inner_point = viewport.transform_point2(radius.max(5.0) * direction); + + return Some((inner_point, first_point, segment_index)); + } + } + + segment_index -= 1; } // Remaining segments: full spiral loops let mut base_theta = spiral_theta; - while if is_reversed { base_theta >= 0.0 } else { base_theta <= max_theta } { + while if is_reversed { + base_theta >= (start_angle_rad).rem_euclid(TAU) + } else { + base_theta <= max_theta + start_angle_rad + } { let theta_start = base_theta; let theta_end = if is_reversed { base_theta - TAU } else { base_theta + TAU }; + log::info!("segment index {:?}, theta_start {:?} ,theta_end {:?}", segment_index, theta_start.to_degrees(), theta_end.to_degrees()); - if (!is_reversed && theta_end > max_theta) || (is_reversed && theta_end < 0.0) { + if (!is_reversed && theta_end > max_theta + start_angle_rad) || (is_reversed && theta_end < 0.0) { break; } + if is_reversed && (theta_start > max_theta + start_angle_rad || theta_end > max_theta + start_angle_rad) { + base_theta -= TAU; + segment_index -= 1; + continue; + } let spiral_start = spiral_point(theta_start, inner_radius, b, spiral_type); let spiral_end = spiral_point(theta_end, inner_radius, b, spiral_type); - let viewport_start = viewport.transform_point2(Self::rotate_point(spiral_start, start_angle_rad)); - let viewport_end = viewport.transform_point2(Self::rotate_point(spiral_end, start_angle_rad)); + let viewport_start = viewport.transform_point2(spiral_start); + let viewport_end = viewport.transform_point2(spiral_end); let r_start = viewport_start.distance(viewport_center); let r_end = viewport_end.distance(viewport_center); if mouse_distance >= r_start.min(r_end) && mouse_distance <= r_start.max(r_end) { - let (point1, point2) = Self::calculate_gizmo_line_points(viewport_start, viewport_end); - return Some((point1, point2, segment_index)); + return Some((viewport_start, viewport_end, segment_index)); } base_theta = if is_reversed { base_theta - TAU } else { base_theta + TAU }; - segment_index += 1; + if is_reversed { + segment_index -= 1; + } else { + segment_index += 1; + } } None } - fn rotate_point(p: DVec2, angle: f64) -> DVec2 { - let cos_a = angle.cos(); - let sin_a = angle.sin(); - DVec2::new(p.x * cos_a - p.y * sin_a, p.x * sin_a + p.y * cos_a) - } - pub fn calculate_updated_dash_lines( &self, inner_radius: f64, @@ -218,19 +283,32 @@ impl TightnessGizmo { reversed: bool, ) -> (DVec2, DVec2) { let b = calculate_b(inner_radius, turns, outer_radius, spiral_type); - let max_theta = turns * TAU + start_angle.to_radians(); + let start_angle_rad = start_angle.to_radians(); + let max_theta = turns * TAU + start_angle_rad; + let base_angle = if reversed { - max_theta - (TAU - self.angle - start_angle.to_radians()) + self.angle + ((start_angle_rad / TAU).floor() * TAU) + turns.floor() * TAU } else { - self.angle - start_angle.to_radians() + self.angle + ((start_angle_rad / TAU).floor() * TAU) }; - let smaller_radius = inner_radius.min(outer_radius); - let (start_point, end_point) = if self.spiral_slot == 0 { + let (start_point, end_point) = if self.spiral_slot == 0 && !reversed { + let endpoint = spiral_point(0. + start_angle_rad, inner_radius, b, spiral_type); + let radius = endpoint.distance(DVec2::ZERO); + ( - viewport.transform_point2(smaller_radius * DVec2::new(base_angle.cos(), -base_angle.sin())), + viewport.transform_point2(radius * DVec2::new(self.angle.cos(), -self.angle.sin())), viewport.transform_point2(spiral_point(base_angle, inner_radius, b, spiral_type)), ) + } else if self.spiral_slot == self.turns.floor() as i32 && reversed { + log::info!("am i plzz reaching here"); + let radius = spiral_point(max_theta, inner_radius, b, spiral_type).distance(DVec2::ZERO); + let endpoint = if self.angle >= (max_theta + start_angle_rad).rem_euclid(TAU) { + viewport.transform_point2(spiral_point(base_angle - TAU, inner_radius, b, spiral_type)) + } else { + viewport.transform_point2(spiral_point(base_angle, inner_radius, b, spiral_type)) + }; + (viewport.transform_point2(radius * DVec2::new(self.angle.cos(), -self.angle.sin())), endpoint) } else { let ref_angle = (self.spiral_slot as f64 - 1.) * TAU + base_angle; let end_point_angle = if reversed { ref_angle - TAU } else { ref_angle + TAU }; @@ -240,19 +318,7 @@ impl TightnessGizmo { ) }; - Self::calculate_gizmo_line_points(start_point, end_point) - } - - // (start_point,end_point) - fn calculate_gizmo_line_points(start_point: DVec2, end_point: DVec2) -> (DVec2, DVec2) { - let length = start_point.distance(end_point); - - let direction = (end_point - start_point).normalize_or_zero(); - - let new_endpoint = end_point - direction * length; - let new_start_point = start_point + direction * length; - - (new_start_point, new_endpoint) + (start_point, end_point) } pub fn update_outer_radius_via_dashed_lines( diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index d56a67cce9..662cba920f 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -42,8 +42,8 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { responses: &mut VecDeque, ) { // self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); - self.turns_handle.handle_actions(selected_spiral_layer, mouse_position, document, responses); - // self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); + // self.turns_handle.handle_actions(selected_spiral_layer, mouse_position, document, responses); + self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); } fn handle_click(&mut self) { From bcbf30bde5a9fa292111359f548c2e0ff302e00c Mon Sep 17 00:00:00 2001 From: 0SlowPoke0 Date: Tue, 12 Aug 2025 12:09:19 +0530 Subject: [PATCH 13/13] fixed all bugs and divide futher when angle greater than 180 --- .../document/node_graph/node_properties.rs | 2 +- .../shape_gizmos/spiral_tightness_gizmo.rs | 18 +++++-- .../shape_gizmos/spiral_turns_handle.rs | 2 +- .../shapes/spiral_shape.rs | 4 +- libraries/bezier-rs/src/subpath/core.rs | 51 ++++++++++++------- 5 files changed, 49 insertions(+), 28 deletions(-) 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 9e65e14b62..db683ba4e8 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -1253,7 +1253,7 @@ pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesCon 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("°"), + NumberInput::default().min(1.).max(180.).unit("°"), ); let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, StartAngleInput::INDEX, true, context), NumberInput::default().unit("°")); diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs index 8588b45a22..70b1e017fc 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_tightness_gizmo.rs @@ -9,6 +9,7 @@ use crate::messages::prelude::{DocumentMessageHandler, FrontendMessage, InputPre use crate::messages::tool::common_functionality::graph_modification_utils::{self}; use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shapes::shape_utility::{calculate_b, extract_arc_or_log_spiral_parameters, get_arc_spiral_end_point, get_spiral_type, spiral_point}; +use crate::messages::tool::common_functionality::shapes::spiral_shape::calculate_circle_point; use glam::{DAffine2, DVec2}; use graph_craft::document::NodeInput; use graph_craft::document::value::TaggedValue; @@ -236,7 +237,6 @@ impl TightnessGizmo { } { let theta_start = base_theta; let theta_end = if is_reversed { base_theta - TAU } else { base_theta + TAU }; - log::info!("segment index {:?}, theta_start {:?} ,theta_end {:?}", segment_index, theta_start.to_degrees(), theta_end.to_degrees()); if (!is_reversed && theta_end > max_theta + start_angle_rad) || (is_reversed && theta_end < 0.0) { break; @@ -247,6 +247,10 @@ impl TightnessGizmo { continue; } + if is_reversed && theta_end < start_angle_rad { + break; + } + let spiral_start = spiral_point(theta_start, inner_radius, b, spiral_type); let spiral_end = spiral_point(theta_end, inner_radius, b, spiral_type); @@ -294,14 +298,14 @@ impl TightnessGizmo { let (start_point, end_point) = if self.spiral_slot == 0 && !reversed { let endpoint = spiral_point(0. + start_angle_rad, inner_radius, b, spiral_type); + let circle_point = calculate_circle_point(self.angle, inner_radius.max(5.)); let radius = endpoint.distance(DVec2::ZERO); ( - viewport.transform_point2(radius * DVec2::new(self.angle.cos(), -self.angle.sin())), + viewport.transform_point2(circle_point), viewport.transform_point2(spiral_point(base_angle, inner_radius, b, spiral_type)), ) } else if self.spiral_slot == self.turns.floor() as i32 && reversed { - log::info!("am i plzz reaching here"); let radius = spiral_point(max_theta, inner_radius, b, spiral_type).distance(DVec2::ZERO); let endpoint = if self.angle >= (max_theta + start_angle_rad).rem_euclid(TAU) { viewport.transform_point2(spiral_point(base_angle - TAU, inner_radius, b, spiral_type)) @@ -310,8 +314,12 @@ impl TightnessGizmo { }; (viewport.transform_point2(radius * DVec2::new(self.angle.cos(), -self.angle.sin())), endpoint) } else { - let ref_angle = (self.spiral_slot as f64 - 1.) * TAU + base_angle; - let end_point_angle = if reversed { ref_angle - TAU } else { ref_angle + TAU }; + let ref_angle = if reversed { + base_angle - (turns - self.spiral_slot as f64) * TAU + } else { + (self.spiral_slot as f64 - 1.) * TAU + base_angle + }; + let end_point_angle = ref_angle + TAU; ( viewport.transform_point2(spiral_point(ref_angle, inner_radius, b, spiral_type)), viewport.transform_point2(spiral_point(end_point_angle, inner_radius, b, spiral_type)), diff --git a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs index 17d522f374..1107b584b0 100644 --- a/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs +++ b/editor/src/messages/tool/common_functionality/gizmos/shape_gizmos/spiral_turns_handle.rs @@ -173,7 +173,7 @@ impl SpiralTurns { match self.gizmo_type { GizmoType::Start => { - let sign = total_delta.signum() * -1.; + let sign = -1.; responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, SPIRAL_START_ANGLE), input: NodeInput::value(TaggedValue::F64(self.initial_start_angle + total_delta), false), diff --git a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs index 662cba920f..e2b54bf2f5 100644 --- a/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs +++ b/editor/src/messages/tool/common_functionality/shapes/spiral_shape.rs @@ -41,8 +41,8 @@ impl ShapeGizmoHandler for SpiralGizmoHandler { input: &InputPreprocessorMessageHandler, responses: &mut VecDeque, ) { - // self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); - // self.turns_handle.handle_actions(selected_spiral_layer, mouse_position, document, responses); + self.radius_handle.handle_actions(selected_spiral_layer, document, input.mouse.position, responses); + self.turns_handle.handle_actions(selected_spiral_layer, mouse_position, document, responses); self.tightness_handle.handle_actions(selected_spiral_layer, input.mouse.position, document, responses); } diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index fd83334b2b..137e6c7b51 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -281,32 +281,45 @@ impl Subpath { let mut theta = start_angle; while theta < theta_end { - let theta_next = f64::min(theta + delta_theta, theta_end); + let next_theta = f64::min(theta + delta_theta, theta_end); + Self::fit_spiral_segment(theta, next_theta, a, b, spiral_type, &mut manipulator_groups, &mut prev_in_handle); + theta = next_theta; + } + + // Add final anchor point + let p_last = spiral_point(theta_end, a, b, spiral_type); + manipulator_groups.push(ManipulatorGroup::new(p_last, prev_in_handle, None)); - 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); + Self::new(manipulator_groups, false) + } - let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type); - let d = arc_len / 3.0; + fn fit_spiral_segment(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType, manipulator_groups: &mut Vec>, prev_in_handle: &mut Option) { + let delta = (theta_end - theta_start).abs(); - let p1 = p0 + d * t0; - let p2 = p3 - d * t1; + // Split large arcs into two halves + if delta > std::f64::consts::FRAC_PI_2 { + let mid = (theta_start + theta_end) / 2.0; + Self::fit_spiral_segment(theta_start, mid, a, b, spiral_type, manipulator_groups, prev_in_handle); + Self::fit_spiral_segment(mid, theta_end, a, b, spiral_type, manipulator_groups, prev_in_handle); + return; + } - manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1))); - prev_in_handle = Some(p2); + // Compute endpoints and tangents + let p0 = spiral_point(theta_start, a, b, spiral_type); + let p3 = spiral_point(theta_end, a, b, spiral_type); + let t0 = spiral_tangent(theta_start, a, b, spiral_type); + let t1 = spiral_tangent(theta_end, a, b, spiral_type); - // 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; - } + // Use fixed handle length: 1/3 of the arc length + let arc_len = spiral_arc_length(theta_start, theta_end, a, b, spiral_type); + let h_in = arc_len / 3.0; + let h_out = arc_len / 3.0; - theta = theta_next; - } + let p1 = p0 + h_in * t0; + let p2 = p3 - h_out * t1; - Self::new(manipulator_groups, false) + manipulator_groups.push(ManipulatorGroup::new(p0, *prev_in_handle, Some(p1))); + *prev_in_handle = Some(p2); } /// Constructs an ellipse with `corner1` and `corner2` as the two corners of the bounding box.