mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 23:38:06 +08:00
impl turns handle gizmo
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -1,2 +1,3 @@
|
||||
pub mod number_of_points_dial;
|
||||
pub mod point_radius_handle;
|
||||
pub mod spiral_turns_handle;
|
||||
|
||||
@@ -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 };
|
||||
|
||||
@@ -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();
|
||||
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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)> {
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"))]
|
||||
|
||||
Reference in New Issue
Block a user