unified log and arc spiral into spiral node

This commit is contained in:
0SlowPoke0
2025-07-01 23:01:51 +05:30
parent 0dbf3155c7
commit 917e79e53d
8 changed files with 188 additions and 171 deletions
@@ -2150,6 +2150,7 @@ fn static_node_properties() -> NodeProperties {
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties)); 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("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_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("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
map.insert( map.insert(
"identity_properties".to_string(), "identity_properties".to_string(),
@@ -23,9 +23,9 @@ use graphene_std::raster_types::{CPU, GPU, RasterDataTable};
use graphene_std::text::Font; use graphene_std::text::Font;
use graphene_std::transform::{Footprint, ReferencePoint}; use graphene_std::transform::{Footprint, ReferencePoint};
use graphene_std::vector::VectorDataTable; use graphene_std::vector::VectorDataTable;
use graphene_std::vector::misc::GridType;
use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm}; use graphene_std::vector::misc::{ArcType, MergeByDistanceAlgorithm};
use graphene_std::vector::misc::{CentroidType, PointSpacingType}; 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::{Fill, FillChoice, FillType, GradientStops};
use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin}; use graphene_std::vector::style::{GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
use graphene_std::{GraphicGroupTable, NodeInputDecleration}; use graphene_std::{GraphicGroupTable, NodeInputDecleration};
@@ -1227,6 +1227,73 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
widgets widgets
} }
pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
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::<SpiralType>()
.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_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_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."; pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
+8 -101
View File
@@ -1,5 +1,5 @@
use super::*; 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 crate::{BezierHandles, consts::*};
use glam::DVec2; use glam::DVec2;
use std::fmt::Write; use std::fmt::Write;
@@ -271,42 +271,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
) )
} }
pub fn spiral_point(theta: f64, a: f64, b: f64) -> DVec2 { pub fn new_spiral(a: f64, b: f64, turns: f64, delta_theta: f64, spiral_type: SpiralType) -> Self {
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 manipulator_groups = Vec::new();
let mut prev_in_handle = None; let mut prev_in_handle = None;
let theta_end = turns * std::f64::consts::TAU; let theta_end = turns * std::f64::consts::TAU;
@@ -315,12 +280,12 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
while theta < theta_end { while theta < theta_end {
let theta_next = theta + delta_theta; let theta_next = theta + delta_theta;
let p0 = Self::spiral_point(theta, a, b); let p0 = spiral_point(theta, a, b, spiral_type);
let p3 = Self::spiral_point(theta_next, a, b); let p3 = spiral_point(theta_next, a, b, spiral_type);
let t0 = Self::spiral_tangent(theta, a, b); let t0 = spiral_tangent(theta, a, b, spiral_type);
let t1 = Self::spiral_tangent(theta_next, a, b); 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 d = arc_len / 3.0;
let p1 = p0 + d * t0; let p1 = p0 + d * t0;
@@ -329,65 +294,7 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
let is_last_segment = theta_next >= theta_end; let is_last_segment = theta_next >= theta_end;
if is_last_segment { if is_last_segment {
let t = (theta_end - theta) / (theta_next - theta); // t in [0, 1] 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);
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);
manipulator_groups.push(ManipulatorGroup::new(trim_p0, prev_in_handle, Some(trim_p1))); manipulator_groups.push(ManipulatorGroup::new(trim_p0, prev_in_handle, Some(trim_p1)));
prev_in_handle = Some(trim_p2); prev_in_handle = Some(trim_p2);
@@ -144,3 +144,9 @@ pub enum ArcType {
Closed, Closed,
PieSlice, PieSlice,
} }
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum SpiralType {
Archimedean,
Logarithmic,
}
+78 -34
View File
@@ -1,5 +1,5 @@
use crate::consts::{MAX_ABSOLUTE_DIFFERENCE, STRICT_MAX_ABSOLUTE_DIFFERENCE}; 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 glam::{BVec2, DMat2, DVec2};
use std::fmt::Write; use std::fmt::Write;
@@ -302,47 +302,91 @@ pub fn format_point(svg: &mut String, prefix: &str, x: f64, y: f64) -> std::fmt:
Ok(()) 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; let r = a + b * theta;
DVec2::new(r * theta.cos(), -r * theta.sin()) DVec2::new(r * theta.cos(), -r * theta.sin())
} }
pub fn spiral_tangent(theta: f64, b: f64) -> DVec2 { /// Returns the tangent direction of an Archimedean spiral at angle `theta`.
let dx = b * (theta.cos() - theta * theta.sin()); pub fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
let dy = b * (theta.sin() + theta * theta.cos()); let r = a + b * theta;
DVec2::new(dx, dy).normalize() 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 { /// Computes arc length along an Archimedean spiral between two angles.
(angle + std::f64::consts::PI).rem_euclid(2.0 * std::f64::consts::PI) - std::f64::consts::PI 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 { /// Computes arc length from origin to a point on Archimedean spiral at angle `theta`.
let u = 1.0 - t; pub fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 {
p0 * u * u * u + p1 * 3.0 * u * u * t + p2 * 3.0 * u * t * t + p3 * t * t * t 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 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)] #[cfg(test)]
+18 -35
View File
@@ -1,9 +1,8 @@
use std::f64::consts::{FRAC_PI_4, FRAC_PI_8, TAU};
use super::misc::{ArcType, AsU64, GridType}; use super::misc::{ArcType, AsU64, GridType};
use super::{PointId, SegmentId, StrokeId}; use super::{PointId, SegmentId, StrokeId};
use crate::Ctx; use crate::Ctx;
use crate::registry::types::{Angle, PixelSize}; use crate::registry::types::{Angle, PixelSize};
use crate::vector::misc::SpiralType;
use crate::vector::{HandleId, VectorData, VectorDataTable}; use crate::vector::{HandleId, VectorData, VectorDataTable};
use bezier_rs::Subpath; use bezier_rs::Subpath;
use glam::DVec2; use glam::DVec2;
@@ -67,45 +66,29 @@ fn arc(
))) )))
} }
#[node_macro::node(category("Vector: Shape"))] #[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))]
fn archimedean_spiral( fn spiral(
_: impl Ctx, _: impl Ctx,
_primary: (), _primary: (),
spiral_type: SpiralType,
#[default(0.5)] start_radius: f64,
#[default(1.)] inner_radius: f64, #[default(1.)] inner_radius: f64,
#[default(0.2)] growth: f64,
#[default(1.)] tightness: f64, #[default(1.)] tightness: f64,
#[default(6)] #[default(6)] turns: f64,
#[hard_min(1.)] #[default(45.)] angle_offset: f64,
turns: f64,
#[default(45.)]
#[range((1., 180.))]
angle_offset: f64,
) -> VectorDataTable { ) -> VectorDataTable {
VectorDataTable::new(VectorData::from_subpath(Subpath::generate_equal_arc_bezier_spiral2( let (a, b) = match spiral_type {
inner_radius, SpiralType::Archimedean => (inner_radius, tightness),
tightness, SpiralType::Logarithmic => (start_radius, growth),
turns, };
angle_offset.to_radians(),
)))
}
#[node_macro::node(category("Vector: Shape"))] let spiral_type = match spiral_type {
fn logarithmic_spiral( SpiralType::Archimedean => bezier_rs::SpiralType::Archimedean,
_: impl Ctx, SpiralType::Logarithmic => bezier_rs::SpiralType::Logarithmic,
_primary: (), };
#[range((0.1, 1.))]
#[default(0.5)] VectorDataTable::new(VectorData::from_subpath(Subpath::new_spiral(a, b, turns, angle_offset.to_radians(), spiral_type)))
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"))] #[node_macro::node(category("Vector: Shape"))]
+8
View File
@@ -96,3 +96,11 @@ pub fn point_to_dvec2(point: Point) -> DVec2 {
pub fn dvec2_to_point(value: DVec2) -> Point { pub fn dvec2_to_point(value: DVec2) -> Point {
Point { x: value.x, y: value.y } 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,
}
@@ -236,6 +236,7 @@ tagged_value! {
ArcType(graphene_core::vector::misc::ArcType), ArcType(graphene_core::vector::misc::ArcType),
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm), MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
PointSpacingType(graphene_core::vector::misc::PointSpacingType), PointSpacingType(graphene_core::vector::misc::PointSpacingType),
SpiralType(graphene_core::vector::misc::SpiralType),
#[serde(alias = "LineCap")] #[serde(alias = "LineCap")]
StrokeCap(graphene_core::vector::style::StrokeCap), StrokeCap(graphene_core::vector::style::StrokeCap),
#[serde(alias = "LineJoin")] #[serde(alias = "LineJoin")]