impl turns handle gizmo

This commit is contained in:
0SlowPoke0
2025-07-10 13:20:34 +05:30
parent 8870409b87
commit f3b6ab3085
15 changed files with 464 additions and 111 deletions

View File

@@ -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;

View File

@@ -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]);
}
}
}

View File

@@ -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<Message>) {
pub fn handle_state(&mut self, layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
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<Message>) {
pub fn handle_actions(&mut self, mouse_position: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
let mut handlers_layer: Vec<(ShapeGizmoHandlers, Vec<LayerNodeIdentifier>)> = 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 {

View File

@@ -1,2 +1,3 @@
pub mod number_of_points_dial;
pub mod point_radius_handle;
pub mod spiral_turns_handle;

View File

@@ -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<Message>, 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 };

View File

@@ -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();

View File

@@ -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<LayerNodeIdentifier>,
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<Message>) {
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<LayerNodeIdentifier>, _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<Message>) {
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;
}
}

View File

@@ -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<Message>) {
fn handle_state(
&mut self,
selected_star_layer: LayerNodeIdentifier,
mouse_position: DVec2,
document: &DocumentMessageHandler,
_input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) {
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;
}

View File

@@ -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<Message>);
fn handle_state(
&mut self,
selected_shape_layers: LayerNodeIdentifier,
mouse_position: DVec2,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
);
/// 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<LayerNodeIdentifier>, 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<SpiralType> {
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<DVec2> {
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<DVec2> {
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<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(u32, f64)> {

View File

@@ -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<Message>,
) {
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<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 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<Message>) {
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<Message>) {
pub fn update_turns(decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
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);
}
}

View File

@@ -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<Message>) {
fn handle_state(
&mut self,
selected_star_layer: LayerNodeIdentifier,
mouse_position: DVec2,
document: &DocumentMessageHandler,
_input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) {
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;
}

View File

@@ -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<Message>, decrease: bool) {
fn increase_no_sides_turns(&self, document: &DocumentMessageHandler, shape_type: ShapeType, responses: &mut VecDeque<Message>, 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);

View File

@@ -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<PointId: crate::Identifier> Subpath<PointId> {
)
}
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<PointId: crate::Identifier> Subpath<PointId> {
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;

View File

@@ -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.

View File

@@ -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"))]