Add "Spiral" to the Shape tool and as a new node (#2803)

* made spiral node

* number of turns in decimal and arc-angle implementation

* logarithmic spiral

* unified log and arc spiral into spiral node

* add spiral shape in shape tool

* fix min value and degree unit

* make it compile

* updated the api

* changed the function_name

* [/] to update the turns widget in shape tool

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0
2025-09-09 03:07:43 +05:30
committed by GitHub
parent ee586be381
commit 485152bf8d
15 changed files with 483 additions and 44 deletions

View File

@@ -1,8 +1,9 @@
use super::consts::*;
use super::*;
use crate::vector::misc::point_to_dvec2;
use crate::vector::misc::{SpiralType, point_to_dvec2};
use glam::DVec2;
use kurbo::PathSeg;
use std::f64::consts::TAU;
pub struct PathSegPoints {
pub p0: DVec2,
@@ -315,4 +316,125 @@ impl<PointId: Identifier> Subpath<PointId> {
pub fn new_line(p1: DVec2, p2: DVec2) -> Self {
Self::from_anchors([p1, p2], false)
}
pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, start_angle: f64, delta_theta: f64, spiral_type: SpiralType) -> Self {
let mut manipulator_groups = Vec::new();
let mut prev_in_handle = None;
let theta_end = turns * std::f64::consts::TAU + start_angle;
let b = calculate_b(a, turns, outer_radius, spiral_type);
let mut theta = start_angle;
while theta < theta_end {
let theta_next = f64::min(theta + delta_theta, theta_end);
let p0 = spiral_point(theta, a, b, spiral_type);
let p3 = spiral_point(theta_next, a, b, spiral_type);
let t0 = spiral_tangent(theta, a, b, spiral_type);
let t1 = spiral_tangent(theta_next, a, b, spiral_type);
let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type);
let d = arc_len / 3.;
let p1 = p0 + d * t0;
let p2 = p3 - d * t1;
manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1)));
prev_in_handle = Some(p2);
// If final segment, end with anchor at theta_end
if (theta_next - theta_end).abs() < f64::EPSILON {
manipulator_groups.push(ManipulatorGroup::new(p3, prev_in_handle, None));
break;
}
theta = theta_next;
}
Self::new(manipulator_groups, false)
}
}
pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
match spiral_type {
SpiralType::Archimedean => {
let total_theta = turns * TAU;
(outer_radius - a) / total_theta
}
SpiralType::Logarithmic => {
let total_theta = turns * TAU;
((outer_radius.abs() / a).ln()) / total_theta
}
}
}
/// Returns a point on the given spiral type at angle `theta`.
pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
match spiral_type {
SpiralType::Archimedean => archimedean_spiral_point(theta, a, b),
SpiralType::Logarithmic => log_spiral_point(theta, a, b),
}
}
/// Returns the tangent direction at angle `theta` for the given spiral type.
pub fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
match spiral_type {
SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b),
SpiralType::Logarithmic => log_spiral_tangent(theta, a, b),
}
}
/// Computes arc length between two angles for the given spiral type.
pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 {
match spiral_type {
SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b),
SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b),
}
}
/// Returns a point on a logarithmic spiral at angle `theta`.
pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a * (b * theta).exp(); // a * e^(bθ)
DVec2::new(r * theta.cos(), -r * theta.sin())
}
/// Computes arc length along a logarithmic spiral between two angles.
pub fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
let factor = (1. + b * b).sqrt();
(a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp())
}
/// Returns the tangent direction of a logarithmic spiral at angle `theta`.
pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a * (b * theta).exp();
let dx = r * (b * theta.cos() - theta.sin());
let dy = r * (b * theta.sin() + theta.cos());
DVec2::new(dx, -dy).normalize_or(DVec2::X)
}
/// Returns a point on an Archimedean spiral at angle `theta`.
pub fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a + b * theta;
DVec2::new(r * theta.cos(), -r * theta.sin())
}
/// Returns the tangent direction of an Archimedean spiral at angle `theta`.
pub fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
let r = a + b * theta;
let dx = b * theta.cos() - r * theta.sin();
let dy = b * theta.sin() + r * theta.cos();
DVec2::new(dx, -dy).normalize_or(DVec2::X)
}
/// Computes arc length along an Archimedean spiral between two angles.
pub fn archimedean_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
archimedean_spiral_arc_length_origin(theta_end, a, b) - archimedean_spiral_arc_length_origin(theta_start, a, b)
}
/// Computes arc length from origin to a point on Archimedean spiral at angle `theta`.
pub fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 {
let r = a + b * theta;
let sqrt_term = (r * r + b * b).sqrt();
(r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2. * b)
}

View File

@@ -5,7 +5,7 @@ use crate::registry::types::{Angle, PixelSize};
use crate::subpath;
use crate::table::Table;
use crate::vector::Vector;
use crate::vector::misc::HandleId;
use crate::vector::misc::{HandleId, SpiralType};
use glam::DVec2;
trait CornerRadius {
@@ -75,6 +75,27 @@ fn arc(
)))
}
#[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))]
fn spiral(
_: impl Ctx,
_primary: (),
spiral_type: SpiralType,
#[default(5.)] turns: f64,
#[default(0.)] start_angle: f64,
#[default(0.)] inner_radius: f64,
#[default(25)] outer_radius: f64,
#[default(90.)] angular_resolution: f64,
) -> Table<Vector> {
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_spiral(
inner_radius,
outer_radius,
turns,
start_angle.to_radians(),
angular_resolution.to_radians(),
spiral_type,
)))
}
#[node_macro::node(category("Vector: Shape"))]
fn ellipse(
_: impl Ctx,

View File

@@ -415,3 +415,11 @@ impl HandleId {
}
}
}
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
#[widget(Dropdown)]
pub enum SpiralType {
#[default]
Archimedean,
Logarithmic,
}

View File

@@ -241,9 +241,8 @@ async fn repeat<I: 'n + Send + Clone>(
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
async fn circular_repeat<I: 'n + Send + Clone>(
_: impl Ctx,
// TODO: Implement other graphical types.
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<I>,
angle_offset: Angle,
start_angle: Angle,
#[unit(" px")]
#[default(5)]
radius: f64,
@@ -254,7 +253,7 @@ async fn circular_repeat<I: 'n + Send + Clone>(
let mut result_table = Table::new();
for index in 0..count {
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + angle_offset.to_radians());
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians());
let translation = DAffine2::from_translation(radius * DVec2::Y);
let transform = angle * translation;

View File

@@ -242,6 +242,7 @@ tagged_value! {
ArcType(graphene_core::vector::misc::ArcType),
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
PointSpacingType(graphene_core::vector::misc::PointSpacingType),
SpiralType(graphene_core::vector::misc::SpiralType),
#[serde(alias = "LineCap")]
StrokeCap(graphene_core::vector::style::StrokeCap),
#[serde(alias = "LineJoin")]