Add gizmos for interacting with the Spiral node (#2851)

* 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

* impl turns handle gizmo

* chore : Refactoring PR #2851 for current code base with some fixes

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Co-authored-by: Annonnymmousss <jatin02012006@gmail.com>
This commit is contained in:
0SlowPoke0 2026-02-13 04:03:40 +05:30 committed by GitHub
parent cd241095a2
commit ea68d62ec4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 353 additions and 3 deletions

View File

@ -10,6 +10,7 @@ use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGiz
use crate::messages::tool::common_functionality::shapes::grid_shape::GridGizmoHandler;
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;
@ -30,6 +31,7 @@ pub enum ShapeGizmoHandlers {
Arc(ArcGizmoHandler),
Circle(CircleGizmoHandler),
Grid(GridGizmoHandler),
Spiral(SpiralGizmoHandler),
}
impl ShapeGizmoHandlers {
@ -42,6 +44,7 @@ impl ShapeGizmoHandlers {
Self::Arc(_) => "arc",
Self::Circle(_) => "circle",
Self::Grid(_) => "grid",
Self::Spiral(_) => "spiral",
Self::None => "none",
}
}
@ -54,6 +57,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Grid(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Spiral(h) => h.handle_state(layer, mouse_position, document, responses),
Self::None => {}
}
}
@ -66,6 +70,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.is_any_gizmo_hovered(),
Self::Circle(h) => h.is_any_gizmo_hovered(),
Self::Grid(h) => h.is_any_gizmo_hovered(),
Self::Spiral(h) => h.is_any_gizmo_hovered(),
Self::None => false,
}
}
@ -78,6 +83,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.handle_click(),
Self::Circle(h) => h.handle_click(),
Self::Grid(h) => h.handle_click(),
Self::Spiral(h) => h.handle_click(),
Self::None => {}
}
}
@ -90,6 +96,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.handle_update(drag_start, document, input, responses),
Self::Circle(h) => h.handle_update(drag_start, document, input, responses),
Self::Grid(h) => h.handle_update(drag_start, document, input, responses),
Self::Spiral(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {}
}
}
@ -102,6 +109,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.cleanup(),
Self::Circle(h) => h.cleanup(),
Self::Grid(h) => h.cleanup(),
Self::Spiral(h) => h.cleanup(),
Self::None => {}
}
}
@ -122,6 +130,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Grid(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 => {}
}
}
@ -141,6 +150,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Grid(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 => {}
}
}
@ -152,6 +162,7 @@ impl ShapeGizmoHandlers {
Self::Arc(h) => h.mouse_cursor_icon(),
Self::Circle(h) => h.mouse_cursor_icon(),
Self::Grid(h) => h.mouse_cursor_icon(),
Self::Spiral(h) => h.mouse_cursor_icon(),
Self::None => None,
}
}
@ -199,6 +210,10 @@ impl GizmoManager {
if graph_modification_utils::get_grid_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Grid(GridGizmoHandler::default()));
}
// Spiral
if graph_modification_utils::get_spiral_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Spiral(SpiralGizmoHandler::default()));
}
None
}

View File

@ -2,4 +2,5 @@ pub mod circle_arc_radius_handle;
pub mod grid_rows_columns_gizmo;
pub mod number_of_points_dial;
pub mod point_radius_handle;
pub mod spiral_turns_handle;
pub mod sweep_angle_gizmo;

View File

@ -0,0 +1,227 @@
use crate::consts::{COLOR_OVERLAY_RED, POINT_RADIUS_HANDLE_SNAP_THRESHOLD};
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, 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::extract_spiral_parameters;
use crate::messages::tool::common_functionality::shapes::spiral_shape::calculate_spiral_endpoints;
use glam::DVec2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::NodeInputDecleration;
use graphene_std::subpath::{calculate_growth_factor, spiral_point};
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]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default)]
pub struct SpiralTurns {
pub layer: Option<LayerNodeIdentifier>,
pub handle_state: SpiralTurnsState,
initial_turns: f64,
initial_outer_radius: f64,
initial_inner_radius: f64,
initial_growth_factor: f64,
initial_start_angle: f64,
previous_mouse_position: DVec2,
total_angle_delta: f64,
gizmo_type: GizmoType,
spiral_type: SpiralType,
}
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;
}
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,
inner_radius: f64,
outer_radius: f64,
turns: f64,
start_angle: f64,
mouse_position: DVec2,
gizmo_type: GizmoType,
spiral_type: SpiralType,
) {
self.layer = Some(layer);
self.initial_turns = turns;
self.initial_growth_factor = calculate_growth_factor(inner_radius, turns, outer_radius, spiral_type);
self.initial_inner_radius = inner_radius;
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);
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, _responses: &mut VecDeque<Message>) {
let viewport = document.metadata().transform_to_viewport(layer);
match &self.handle_state {
SpiralTurnsState::Inactive => {
if let Some((spiral_type, start_angle, inner_radius, outer_radius, turns, _)) = extract_spiral_parameters(layer, document) {
let growth_factor = calculate_growth_factor(inner_radius, turns, outer_radius, spiral_type);
let end_point = viewport.transform_point2(spiral_point(turns * TAU + start_angle.to_radians(), inner_radius, growth_factor, spiral_type));
let start_point = viewport.transform_point2(spiral_point(0. + start_angle.to_radians(), inner_radius, growth_factor, spiral_type));
if mouse_position.distance(end_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD {
self.store_initial_parameters(layer, inner_radius, outer_radius, turns, start_angle, mouse_position, GizmoType::End, spiral_type);
} else if mouse_position.distance(start_point) < POINT_RADIUS_HANDLE_SNAP_THRESHOLD {
self.store_initial_parameters(layer, inner_radius, outer_radius, turns, start_angle, mouse_position, GizmoType::Start, spiral_type);
}
}
}
SpiralTurnsState::Hover | SpiralTurnsState::Dragging => {}
}
}
pub fn overlays(&self, document: &DocumentMessageHandler, layer: Option<LayerNodeIdentifier>, _shape_editor: &mut &mut ShapeState, _mouse_position: DVec2, overlay_context: &mut OverlayContext) {
let Some(layer) = layer.or(self.layer) else { return };
let viewport = document.metadata().transform_to_viewport(layer);
match &self.handle_state {
SpiralTurnsState::Inactive => {
if let Some((p1, p2)) = calculate_spiral_endpoints(layer, document, viewport, 0.).zip(calculate_spiral_endpoints(layer, document, viewport, TAU)) {
overlay_context.manipulator_handle(p1, false, None);
overlay_context.manipulator_handle(p2, false, None);
}
}
SpiralTurnsState::Hover | SpiralTurnsState::Dragging => {
// Is true only when hovered over the gizmo
let selected = self.layer.is_some();
let angle = match self.gizmo_type {
GizmoType::End => TAU,
GizmoType::Start => 0.,
GizmoType::None => return,
};
if let Some(endpoint) = calculate_spiral_endpoints(layer, document, viewport, angle) {
overlay_context.manipulator_handle(endpoint, selected, Some(COLOR_OVERLAY_RED));
}
}
}
}
pub fn update_number_of_turns(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
use graphene_std::vector::generator_nodes::spiral::*;
let Some(layer) = self.layer else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let angle_delta = viewport
.inverse()
.transform_vector2(input.mouse.position - center)
.angle_to(viewport.inverse().transform_vector2(self.previous_mouse_position - center))
.to_degrees();
// Skip update if angle calculation produced NaN or infinity (can happen when mouse is at center)
// Also skip very small angle changes to reduce jitter near center
if !angle_delta.is_finite() || angle_delta.abs() < 0.5 {
self.previous_mouse_position = input.mouse.position;
return;
}
// 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_growth_factor) * TAU,
SpiralType::Logarithmic => self.initial_outer_radius * ((self.initial_growth_factor * TAU * turns_delta).exp() - 1.),
};
// Skip if outer_radius calculation produced invalid values
if !outer_radius_change.is_finite() {
return;
}
let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else {
return;
};
match self.gizmo_type {
GizmoType::Start => {
let sign = -1.;
let new_turns = (self.initial_turns + turns_delta * sign).max(0.5);
let new_outer_radius = (self.initial_outer_radius + outer_radius_change * sign).max(0.1);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, StartAngleInput::INDEX),
input: NodeInput::value(TaggedValue::F64(self.initial_start_angle + total_delta), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, TurnsInput::INDEX),
input: NodeInput::value(TaggedValue::F64(new_turns), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, OuterRadiusInput::INDEX),
input: NodeInput::value(TaggedValue::F64(new_outer_radius), false),
});
}
GizmoType::End => {
let new_turns = (self.initial_turns + turns_delta).max(0.5);
let new_outer_radius = (self.initial_outer_radius + outer_radius_change).max(0.1);
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, TurnsInput::INDEX),
input: NodeInput::value(TaggedValue::F64(new_turns), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, OuterRadiusInput::INDEX),
input: NodeInput::value(TaggedValue::F64(new_outer_radius), false),
});
}
GizmoType::None => {
return;
}
}
responses.add(NodeGraphMessage::RunDocumentGraph);
self.total_angle_delta += angle_delta;
self.previous_mouse_position = input.mouse.position;
}
}

View File

@ -18,7 +18,7 @@ use graph_craft::document::value::TaggedValue;
use graphene_std::NodeInputDecleration;
use graphene_std::subpath::{self, Subpath};
use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::{ArcType, GridType, dvec2_to_point};
use graphene_std::vector::misc::{ArcType, GridType, SpiralType, dvec2_to_point};
use kurbo::{BezPath, PathEl, Shape};
use std::collections::VecDeque;
use std::f64::consts::{PI, TAU};
@ -293,6 +293,35 @@ pub fn extract_arc_parameters(layer: Option<LayerNodeIdentifier>, document: &Doc
Some((radius, start_angle, sweep_angle, arc_type))
}
/// Extract the node input values of spiral.
/// Returns an option of (spiral type, start angle, inner radius, outer radius, turns, angle resolution).
pub fn extract_spiral_parameters(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<(SpiralType, f64, f64, f64, f64, f64)> {
use graphene_std::vector::generator_nodes::spiral::*;
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::spiral::IDENTIFIER))?;
let (
Some(&TaggedValue::SpiralType(spiral_type)),
Some(&TaggedValue::F64(start_angle)),
Some(&TaggedValue::F64(inner_radius)),
Some(&TaggedValue::F64(outer_radius)),
Some(&TaggedValue::F64(turns)),
Some(&TaggedValue::F64(angle_resolution)),
) = (
node_inputs.get(SpiralTypeInput::INDEX)?.as_value(),
node_inputs.get(StartAngleInput::INDEX)?.as_value(),
node_inputs.get(InnerRadiusInput::INDEX)?.as_value(),
node_inputs.get(OuterRadiusInput::INDEX)?.as_value(),
node_inputs.get(TurnsInput::INDEX)?.as_value(),
node_inputs.get(AngularResolutionInput::INDEX)?.as_value(),
)
else {
return None;
};
Some((spiral_type, start_angle, inner_radius, outer_radius, turns, angle_resolution))
}
/// Calculate the viewport positions of arc endpoints
pub fn arc_end_points(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2)> {
let (radius, start_angle, sweep_angle, _) = extract_arc_parameters(Some(layer?), document)?;

View File

@ -1,9 +1,14 @@
use super::*;
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, 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::{self, NodeGraphLayer};
use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, extract_spiral_parameters};
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::*;
@ -11,9 +16,82 @@ use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use graphene_std::NodeInputDecleration;
use graphene_std::subpath::{calculate_growth_factor, spiral_point};
use graphene_std::vector::misc::SpiralType;
use std::collections::VecDeque;
#[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, responses: &mut VecDeque<Message>) {
self.turns_handle.handle_actions(selected_spiral_layer, 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<Message>) {
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<LayerNodeIdentifier>,
_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 mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
if self.turns_handle.hovered() || self.turns_handle.is_dragging() {
return Some(MouseCursorIcon::Default);
}
None
}
fn cleanup(&mut self) {
self.turns_handle.cleanup();
}
}
/// Calculates the position of a spiral endpoint at a given angle offset (0 = start, TAU = end).
pub fn calculate_spiral_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, viewport: DAffine2, theta: f64) -> Option<DVec2> {
let (spiral_type, start_angle, a, outer_radius, turns, _) = extract_spiral_parameters(layer, document)?;
let b = calculate_growth_factor(a, turns, outer_radius, spiral_type);
let theta = turns * theta + start_angle.to_radians();
Some(viewport.transform_point2(spiral_point(theta, a, b, spiral_type)))
}
#[derive(Default)]
pub struct Spiral;

View File

@ -348,7 +348,7 @@ impl<PointId: Identifier> Subpath<PointId> {
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 b = calculate_growth_factor(a, turns, outer_radius, spiral_type);
let mut theta = start_angle;
while theta < theta_end {
@ -381,7 +381,7 @@ impl<PointId: Identifier> Subpath<PointId> {
}
}
pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
pub fn calculate_growth_factor(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
match spiral_type {
SpiralType::Archimedean => {
let total_theta = turns * TAU;