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,42 +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 { + 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; @@ -315,12 +280,12 @@ impl Subpath { 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 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::spiral_arc_length(theta_next, a, b) - Self::spiral_arc_length(theta, 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; @@ -329,65 +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); - - 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 { - 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); + 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 { +/// Returns a point on the given spiral type at angle `theta`. +pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 { + match spiral_type { + SpiralType::Archimedean => archimedean_spiral_point(theta, a, b), + SpiralType::Logarithmic => log_spiral_point(theta, a, b), + } +} + +/// Returns the tangent direction at angle `theta` for the given spiral type. +pub fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 { + match spiral_type { + SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b), + SpiralType::Logarithmic => log_spiral_tangent(theta, a, b), + } +} + +/// Computes arc length between two angles for the given spiral type. +pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 { + match spiral_type { + SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b), + SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b), + } +} + +/// Splits a cubic Bézier curve at parameter `t`, returning the first half. +pub fn split_cubic_bezier(p0: DVec2, p1: DVec2, p2: DVec2, p3: DVec2, t: f64) -> (DVec2, DVec2, DVec2, DVec2) { + let p01 = p0.lerp(p1, t); + let p12 = p1.lerp(p2, t); + let p23 = p2.lerp(p3, t); + + let p012 = p01.lerp(p12, t); + let p123 = p12.lerp(p23, t); + + // final split point + let p0123 = p012.lerp(p123, t); + + // First half of the Bézier + (p0, p01, p012, p0123) +} + +/// Returns a point on a logarithmic spiral at angle `theta`. +pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a * (b * theta).exp(); // a * e^(bθ) + DVec2::new(r * theta.cos(), -r * theta.sin()) +} + +/// Computes arc length along a logarithmic spiral between two angles. +pub fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 { + let factor = (1. + b * b).sqrt(); + (a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp()) +} + +/// Returns the tangent direction of a logarithmic spiral at angle `theta`. +pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 { + let r = a * (b * theta).exp(); + let dx = r * (b * theta.cos() - theta.sin()); + let dy = r * (b * theta.sin() + theta.cos()); + + DVec2::new(dx, -dy).normalize() +} + +/// Returns a point on an Archimedean spiral at angle `theta`. +pub fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { let r = a + b * theta; DVec2::new(r * theta.cos(), -r * theta.sin()) } -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() +/// 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 wrap_angle(angle: f64) -> f64 { - (angle + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI +/// 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) } -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 +/// 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")]