Add "Spiral" to the Shape tool and as a new node (#2803)
* made spiral node * number of turns in decimal and arc-angle implementation * logarithmic spiral * unified log and arc spiral into spiral node * add spiral shape in shape tool * fix min value and degree unit * make it compile * updated the api * changed the function_name * [/] to update the turns widget in shape tool * Code review --------- Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
ee586be381
commit
485152bf8d
|
|
@ -1776,6 +1776,7 @@ fn static_node_properties() -> NodeProperties {
|
||||||
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
|
map.insert("math_properties".to_string(), Box::new(node_properties::math_properties));
|
||||||
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
|
map.insert("rectangle_properties".to_string(), Box::new(node_properties::rectangle_properties));
|
||||||
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
|
map.insert("grid_properties".to_string(), Box::new(node_properties::grid_properties));
|
||||||
|
map.insert("spiral_properties".to_string(), Box::new(node_properties::spiral_properties));
|
||||||
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
|
map.insert("sample_polyline_properties".to_string(), Box::new(node_properties::sample_polyline_properties));
|
||||||
map.insert(
|
map.insert(
|
||||||
"monitor_properties".to_string(),
|
"monitor_properties".to_string(),
|
||||||
|
|
@ -2394,6 +2395,7 @@ impl DocumentNodeDefinition {
|
||||||
/// `input_override` does not have to be the correct length.
|
/// `input_override` does not have to be the correct length.
|
||||||
pub fn node_template_input_override(&self, input_override: impl IntoIterator<Item = Option<NodeInput>>) -> NodeTemplate {
|
pub fn node_template_input_override(&self, input_override: impl IntoIterator<Item = Option<NodeInput>>) -> NodeTemplate {
|
||||||
let mut template = self.node_template.clone();
|
let mut template = self.node_template.clone();
|
||||||
|
// TODO: Replace the .enumerate() with changing the iterator to take a tuple of (index, input) so the user is forced to provide the correct index
|
||||||
input_override.into_iter().enumerate().for_each(|(index, input_override)| {
|
input_override.into_iter().enumerate().for_each(|(index, input_override)| {
|
||||||
if let Some(input_override) = input_override {
|
if let Some(input_override) = input_override {
|
||||||
// Only value inputs can be overridden, since node inputs change graph structure and must be handled by the network interface
|
// Only value inputs can be overridden, since node inputs change graph structure and must be handled by the network interface
|
||||||
|
|
|
||||||
|
|
@ -23,7 +23,7 @@ use graphene_std::raster::{
|
||||||
use graphene_std::table::{Table, TableRow};
|
use graphene_std::table::{Table, TableRow};
|
||||||
use graphene_std::text::{Font, TextAlign};
|
use graphene_std::text::{Font, TextAlign};
|
||||||
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
|
use graphene_std::transform::{Footprint, ReferencePoint, Transform};
|
||||||
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType};
|
use graphene_std::vector::misc::{ArcType, CentroidType, GridType, MergeByDistanceAlgorithm, PointSpacingType, SpiralType};
|
||||||
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops, GradientType, PaintOrder, StrokeAlign, StrokeCap, StrokeJoin};
|
||||||
|
|
||||||
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
pub(crate) fn string_properties(text: &str) -> Vec<LayoutGroup> {
|
||||||
|
|
@ -1286,6 +1286,66 @@ pub(crate) fn grid_properties(node_id: NodeId, context: &mut NodePropertiesConte
|
||||||
widgets
|
widgets
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn spiral_properties(node_id: NodeId, context: &mut NodePropertiesContext) -> Vec<LayoutGroup> {
|
||||||
|
use graphene_std::vector::generator_nodes::spiral::*;
|
||||||
|
|
||||||
|
let spiral_type = enum_choice::<SpiralType>()
|
||||||
|
.for_socket(ParameterWidgetsInfo::new(node_id, SpiralTypeInput::INDEX, true, context))
|
||||||
|
.property_row();
|
||||||
|
let turns = number_widget(ParameterWidgetsInfo::new(node_id, TurnsInput::INDEX, true, context), NumberInput::default().min(0.1));
|
||||||
|
let start_angle = number_widget(ParameterWidgetsInfo::new(node_id, StartAngleInput::INDEX, true, context), NumberInput::default().unit("°"));
|
||||||
|
|
||||||
|
let mut widgets = vec![spiral_type, LayoutGroup::Row { widgets: turns }, LayoutGroup::Row { widgets: start_angle }];
|
||||||
|
|
||||||
|
let document_node = match get_document_node(node_id, context) {
|
||||||
|
Ok(document_node) => document_node,
|
||||||
|
Err(err) => {
|
||||||
|
log::error!("Could not get document node in exposure_properties: {err}");
|
||||||
|
return Vec::new();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(spiral_type_input) = document_node.inputs.get(SpiralTypeInput::INDEX) else {
|
||||||
|
log::warn!("A widget failed to be built because its node's input index is invalid.");
|
||||||
|
return vec![];
|
||||||
|
};
|
||||||
|
if let Some(&TaggedValue::SpiralType(spiral_type)) = spiral_type_input.as_non_exposed_value() {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => {
|
||||||
|
let inner_radius = LayoutGroup::Row {
|
||||||
|
widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).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, outer_radius]);
|
||||||
|
}
|
||||||
|
SpiralType::Logarithmic => {
|
||||||
|
let inner_radius = LayoutGroup::Row {
|
||||||
|
widgets: number_widget(ParameterWidgetsInfo::new(node_id, InnerRadiusInput::INDEX, true, context), NumberInput::default().min(0.).unit(" px")),
|
||||||
|
};
|
||||||
|
|
||||||
|
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([inner_radius, outer_radius]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let angular_resolution = number_widget(
|
||||||
|
ParameterWidgetsInfo::new(node_id, AngularResolutionInput::INDEX, true, context),
|
||||||
|
NumberInput::default().min(1.).max(180.).unit("°"),
|
||||||
|
);
|
||||||
|
|
||||||
|
widgets.push(LayoutGroup::Row { widgets: angular_resolution });
|
||||||
|
|
||||||
|
widgets
|
||||||
|
}
|
||||||
|
|
||||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points.";
|
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SPACING: &str = "Use a point sampling density controlled by a distance between, or specific number of, points.";
|
||||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).";
|
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_SEPARATION: &str = "Distance between each instance (exact if 'Adaptive Spacing' is disabled, approximate if enabled).";
|
||||||
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
|
pub(crate) const SAMPLE_POLYLINE_TOOLTIP_QUANTITY: &str = "Number of points to place along the path.";
|
||||||
|
|
|
||||||
|
|
@ -363,6 +363,10 @@ pub fn get_arc_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInt
|
||||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arc")
|
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arc")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn get_spiral_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||||
|
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Spiral")
|
||||||
|
}
|
||||||
|
|
||||||
pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
pub fn get_text_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
|
||||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Text")
|
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Text")
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -6,6 +6,7 @@ pub mod line_shape;
|
||||||
pub mod polygon_shape;
|
pub mod polygon_shape;
|
||||||
pub mod rectangle_shape;
|
pub mod rectangle_shape;
|
||||||
pub mod shape_utility;
|
pub mod shape_utility;
|
||||||
|
pub mod spiral_shape;
|
||||||
pub mod star_shape;
|
pub mod star_shape;
|
||||||
|
|
||||||
pub use super::shapes::ellipse_shape::Ellipse;
|
pub use super::shapes::ellipse_shape::Ellipse;
|
||||||
|
|
|
||||||
|
|
@ -158,34 +158,34 @@ impl Polygon {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn increase_decrease_sides(increase: bool, document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, responses: &mut VecDeque<Message>) {
|
/// Updates the number of sides of a polygon or star node and syncs the Shape tool UI widget accordingly.
|
||||||
if let Some(layer) = shape_tool_data.data.layer {
|
/// Increases or decreases the side count based on user input, clamped to a minimum of 3.
|
||||||
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) else {
|
pub fn decrease_or_increase_sides(decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
return;
|
let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) else {
|
||||||
};
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface)
|
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface)
|
||||||
.find_node_inputs("Regular Polygon")
|
.find_node_inputs("Regular Polygon")
|
||||||
.or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star"))
|
.or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star"))
|
||||||
else {
|
else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else {
|
let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else {
|
||||||
return;
|
return;
|
||||||
};
|
};
|
||||||
|
|
||||||
let new_dimension = if increase { n + 1 } else { (n - 1).max(3) };
|
let new_dimension = if decrease { (n - 1).max(3) } else { n + 1 };
|
||||||
|
|
||||||
responses.add(ShapeToolMessage::UpdateOptions {
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
options: ShapeOptionsUpdate::Vertices(new_dimension),
|
options: ShapeOptionsUpdate::Vertices(new_dimension),
|
||||||
});
|
});
|
||||||
|
|
||||||
responses.add(NodeGraphMessage::SetInput {
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
input_connector: InputConnector::node(node_id, 1),
|
input_connector: InputConnector::node(node_id, 1),
|
||||||
input: NodeInput::value(TaggedValue::U32(new_dimension), false),
|
input: NodeInput::value(TaggedValue::U32(new_dimension), false),
|
||||||
});
|
});
|
||||||
responses.add(NodeGraphMessage::RunDocumentGraph);
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -29,6 +29,7 @@ pub enum ShapeType {
|
||||||
Star,
|
Star,
|
||||||
Circle,
|
Circle,
|
||||||
Arc,
|
Arc,
|
||||||
|
Spiral,
|
||||||
Grid,
|
Grid,
|
||||||
Rectangle,
|
Rectangle,
|
||||||
Ellipse,
|
Ellipse,
|
||||||
|
|
@ -43,6 +44,7 @@ impl ShapeType {
|
||||||
Self::Circle => "Circle",
|
Self::Circle => "Circle",
|
||||||
Self::Arc => "Arc",
|
Self::Arc => "Arc",
|
||||||
Self::Grid => "Grid",
|
Self::Grid => "Grid",
|
||||||
|
Self::Spiral => "Spiral",
|
||||||
Self::Rectangle => "Rectangle",
|
Self::Rectangle => "Rectangle",
|
||||||
Self::Ellipse => "Ellipse",
|
Self::Ellipse => "Ellipse",
|
||||||
Self::Line => "Line",
|
Self::Line => "Line",
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,116 @@
|
||||||
|
use super::*;
|
||||||
|
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::utility_types::document_metadata::LayerNodeIdentifier;
|
||||||
|
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
|
||||||
|
use crate::messages::tool::common_functionality::graph_modification_utils::{self, NodeGraphLayer};
|
||||||
|
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::NodeInput;
|
||||||
|
use graph_craft::document::value::TaggedValue;
|
||||||
|
use graphene_std::NodeInputDecleration;
|
||||||
|
use graphene_std::vector::misc::SpiralType;
|
||||||
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
#[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(turns), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::F64(0.), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::F64(inner_radius), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::F64(0.1), false)),
|
||||||
|
Some(NodeInput::value(TaggedValue::F64(90.), false)),
|
||||||
|
])
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn update_shape(document: &DocumentMessageHandler, ipp: &InputPreprocessorMessageHandler, layer: LayerNodeIdentifier, shape_tool_data: &mut ShapeToolData, responses: &mut VecDeque<Message>) {
|
||||||
|
use graphene_std::vector::generator_nodes::spiral::*;
|
||||||
|
|
||||||
|
let viewport_drag_start = shape_tool_data.data.viewport_drag_start(document);
|
||||||
|
|
||||||
|
let ignore = vec![layer];
|
||||||
|
let snap_data = SnapData::ignore(document, ipp, &ignore);
|
||||||
|
let config = SnapTypeConfiguration::default();
|
||||||
|
let document_mouse = document.metadata().document_to_viewport.inverse().transform_point2(ipp.mouse.position);
|
||||||
|
let snapped = shape_tool_data.data.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
|
||||||
|
let snapped_viewport_point = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
|
||||||
|
shape_tool_data.data.snap_manager.update_indicator(snapped);
|
||||||
|
|
||||||
|
let dragged_distance = (viewport_drag_start - snapped_viewport_point).length();
|
||||||
|
|
||||||
|
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::SpiralType(spiral_type)) = node_inputs.get(SpiralTypeInput::INDEX).unwrap().as_value() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let new_radius = match spiral_type {
|
||||||
|
SpiralType::Archimedean => dragged_distance,
|
||||||
|
SpiralType::Logarithmic => (dragged_distance).max(0.1),
|
||||||
|
};
|
||||||
|
|
||||||
|
responses.add(GraphOperationMessage::TransformSet {
|
||||||
|
layer,
|
||||||
|
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., viewport_drag_start),
|
||||||
|
transform_in: TransformIn::Viewport,
|
||||||
|
skip_rerender: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
|
input_connector: InputConnector::node(node_id, OuterRadiusInput::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(decrease: bool, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
|
||||||
|
use graphene_std::vector::generator_nodes::spiral::*;
|
||||||
|
|
||||||
|
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Spiral") else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(node_id) = graph_modification_utils::get_spiral_id(layer, &document.network_interface) else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
let Some(&TaggedValue::F64(mut turns)) = node_inputs.get(TurnsInput::INDEX).unwrap().as_value() else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
|
||||||
|
if decrease {
|
||||||
|
turns = (turns - 1.).max(1.);
|
||||||
|
} else {
|
||||||
|
turns += 1.;
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::Turns(turns),
|
||||||
|
});
|
||||||
|
|
||||||
|
responses.add(NodeGraphMessage::SetInput {
|
||||||
|
input_connector: InputConnector::node(node_id, TurnsInput::INDEX),
|
||||||
|
input: NodeInput::value(TaggedValue::F64(turns), false),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -14,6 +14,7 @@ use crate::messages::tool::common_functionality::shapes::grid_shape::Grid;
|
||||||
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
|
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
|
||||||
use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon;
|
use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon;
|
||||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays};
|
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays};
|
||||||
|
use crate::messages::tool::common_functionality::shapes::spiral_shape::Spiral;
|
||||||
use crate::messages::tool::common_functionality::shapes::star_shape::Star;
|
use crate::messages::tool::common_functionality::shapes::star_shape::Star;
|
||||||
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
|
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
|
||||||
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
|
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
|
||||||
|
|
@ -22,7 +23,7 @@ use crate::messages::tool::common_functionality::utility_functions::{closest_poi
|
||||||
use graph_craft::document::NodeId;
|
use graph_craft::document::NodeId;
|
||||||
use graphene_std::Color;
|
use graphene_std::Color;
|
||||||
use graphene_std::renderer::Quad;
|
use graphene_std::renderer::Quad;
|
||||||
use graphene_std::vector::misc::{ArcType, GridType};
|
use graphene_std::vector::misc::{ArcType, GridType, SpiralType};
|
||||||
use std::vec;
|
use std::vec;
|
||||||
|
|
||||||
#[derive(Default, ExtractField)]
|
#[derive(Default, ExtractField)]
|
||||||
|
|
@ -40,6 +41,8 @@ pub struct ShapeToolOptions {
|
||||||
shape_type: ShapeType,
|
shape_type: ShapeType,
|
||||||
arc_type: ArcType,
|
arc_type: ArcType,
|
||||||
grid_type: GridType,
|
grid_type: GridType,
|
||||||
|
spiral_type: SpiralType,
|
||||||
|
turns: f64,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ShapeToolOptions {
|
impl Default for ShapeToolOptions {
|
||||||
|
|
@ -51,6 +54,8 @@ impl Default for ShapeToolOptions {
|
||||||
vertices: 5,
|
vertices: 5,
|
||||||
shape_type: ShapeType::Polygon,
|
shape_type: ShapeType::Polygon,
|
||||||
arc_type: ArcType::Open,
|
arc_type: ArcType::Open,
|
||||||
|
spiral_type: SpiralType::Archimedean,
|
||||||
|
turns: 5.,
|
||||||
grid_type: GridType::Rectangular,
|
grid_type: GridType::Rectangular,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -67,6 +72,8 @@ pub enum ShapeOptionsUpdate {
|
||||||
Vertices(u32),
|
Vertices(u32),
|
||||||
ShapeType(ShapeType),
|
ShapeType(ShapeType),
|
||||||
ArcType(ArcType),
|
ArcType(ArcType),
|
||||||
|
SpiralType(SpiralType),
|
||||||
|
Turns(f64),
|
||||||
GridType(GridType),
|
GridType(GridType),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -109,6 +116,20 @@ fn create_sides_widget(vertices: u32) -> WidgetHolder {
|
||||||
.widget_holder()
|
.widget_holder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_turns_widget(turns: f64) -> WidgetHolder {
|
||||||
|
NumberInput::new(Some(turns))
|
||||||
|
.label("Turns")
|
||||||
|
.min(0.5)
|
||||||
|
.mode(NumberInputMode::Increment)
|
||||||
|
.on_update(|number_input: &NumberInput| {
|
||||||
|
ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::Turns(number_input.value.unwrap()),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
})
|
||||||
|
.widget_holder()
|
||||||
|
}
|
||||||
|
|
||||||
fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
|
fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
|
||||||
let entries = vec![vec![
|
let entries = vec![vec![
|
||||||
MenuListEntry::new("Polygon").label("Polygon").on_commit(move |_| {
|
MenuListEntry::new("Polygon").label("Polygon").on_commit(move |_| {
|
||||||
|
|
@ -135,6 +156,12 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
|
||||||
}
|
}
|
||||||
.into()
|
.into()
|
||||||
}),
|
}),
|
||||||
|
MenuListEntry::new("Spiral").label("Spiral").on_commit(move |_| {
|
||||||
|
ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::ShapeType(ShapeType::Spiral),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
MenuListEntry::new("Grid").label("Grid").on_commit(move |_| {
|
MenuListEntry::new("Grid").label("Grid").on_commit(move |_| {
|
||||||
ShapeToolMessage::UpdateOptions {
|
ShapeToolMessage::UpdateOptions {
|
||||||
options: ShapeOptionsUpdate::ShapeType(ShapeType::Grid),
|
options: ShapeOptionsUpdate::ShapeType(ShapeType::Grid),
|
||||||
|
|
@ -184,6 +211,24 @@ fn create_weight_widget(line_weight: f64) -> WidgetHolder {
|
||||||
.widget_holder()
|
.widget_holder()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn create_spiral_type_widget(spiral_type: SpiralType) -> WidgetHolder {
|
||||||
|
let entries = vec![vec![
|
||||||
|
MenuListEntry::new("Archimedean").label("Archimedean").on_commit(move |_| {
|
||||||
|
ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::SpiralType(SpiralType::Archimedean),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
MenuListEntry::new("Logarithmic").label("Logarithmic").on_commit(move |_| {
|
||||||
|
ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::SpiralType(SpiralType::Logarithmic),
|
||||||
|
}
|
||||||
|
.into()
|
||||||
|
}),
|
||||||
|
]];
|
||||||
|
DropdownInput::new(entries).selected_index(Some(spiral_type as u32)).widget_holder()
|
||||||
|
}
|
||||||
|
|
||||||
fn create_grid_type_widget(grid_type: GridType) -> WidgetHolder {
|
fn create_grid_type_widget(grid_type: GridType) -> WidgetHolder {
|
||||||
let entries = vec![
|
let entries = vec![
|
||||||
RadioEntryData::new("Rectangular").label("Rectangular").on_update(move |_| {
|
RadioEntryData::new("Rectangular").label("Rectangular").on_update(move |_| {
|
||||||
|
|
@ -221,6 +266,14 @@ impl LayoutHolder for ShapeTool {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if self.options.shape_type == ShapeType::Spiral {
|
||||||
|
widgets.push(create_spiral_type_widget(self.options.spiral_type));
|
||||||
|
widgets.push(Separator::new(SeparatorType::Related).widget_holder());
|
||||||
|
|
||||||
|
widgets.push(create_turns_widget(self.options.turns));
|
||||||
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
|
}
|
||||||
|
|
||||||
if self.options.shape_type == ShapeType::Grid {
|
if self.options.shape_type == ShapeType::Grid {
|
||||||
widgets.push(create_grid_type_widget(self.options.grid_type));
|
widgets.push(create_grid_type_widget(self.options.grid_type));
|
||||||
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
|
||||||
|
|
@ -327,6 +380,12 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
|
||||||
ShapeOptionsUpdate::ArcType(arc_type) => {
|
ShapeOptionsUpdate::ArcType(arc_type) => {
|
||||||
self.options.arc_type = arc_type;
|
self.options.arc_type = arc_type;
|
||||||
}
|
}
|
||||||
|
ShapeOptionsUpdate::SpiralType(spiral_type) => {
|
||||||
|
self.options.spiral_type = spiral_type;
|
||||||
|
}
|
||||||
|
ShapeOptionsUpdate::Turns(turns) => {
|
||||||
|
self.options.turns = turns;
|
||||||
|
}
|
||||||
ShapeOptionsUpdate::GridType(grid_type) => {
|
ShapeOptionsUpdate::GridType(grid_type) => {
|
||||||
self.options.grid_type = grid_type;
|
self.options.grid_type = grid_type;
|
||||||
}
|
}
|
||||||
|
|
@ -471,6 +530,18 @@ impl ShapeToolData {
|
||||||
fn shape_tool_modifier_keys() -> [Key; 3] {
|
fn shape_tool_modifier_keys() -> [Key; 3] {
|
||||||
[Key::Alt, Key::Shift, Key::Control]
|
[Key::Alt, Key::Shift, Key::Control]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn decrease_or_increase_sides(&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::decrease_or_increase_sides(decrease, layer, document, responses),
|
||||||
|
ShapeType::Spiral => Spiral::update_turns(decrease, layer, document, responses),
|
||||||
|
_ => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.add(NodeGraphMessage::RunDocumentGraph);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Fsm for ShapeToolFsmState {
|
impl Fsm for ShapeToolFsmState {
|
||||||
|
|
@ -584,15 +655,32 @@ impl Fsm for ShapeToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => {
|
(ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => {
|
||||||
responses.add(ShapeToolMessage::UpdateOptions {
|
if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) {
|
||||||
options: ShapeOptionsUpdate::Vertices(tool_options.vertices + 1),
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
});
|
options: ShapeOptionsUpdate::Vertices(tool_options.vertices + 1),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(tool_options.shape_type, ShapeType::Spiral) {
|
||||||
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::Turns(tool_options.turns + 1.),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => {
|
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => {
|
||||||
responses.add(ShapeToolMessage::UpdateOptions {
|
if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) {
|
||||||
options: ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)),
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
});
|
options: ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
if matches!(tool_options.shape_type, ShapeType::Spiral) {
|
||||||
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::Turns((tool_options.turns - 1.).max(1.)),
|
||||||
|
});
|
||||||
|
}
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(
|
(
|
||||||
|
|
@ -629,13 +717,11 @@ impl Fsm for ShapeToolFsmState {
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => {
|
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => {
|
||||||
Polygon::increase_decrease_sides(true, document, tool_data, responses);
|
tool_data.decrease_or_increase_sides(document, tool_options.shape_type, responses, false);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => {
|
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => {
|
||||||
Polygon::increase_decrease_sides(false, document, tool_data, responses);
|
tool_data.decrease_or_increase_sides(document, tool_options.shape_type, responses, true);
|
||||||
|
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => {
|
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => {
|
||||||
|
|
@ -719,7 +805,9 @@ impl Fsm for ShapeToolFsmState {
|
||||||
};
|
};
|
||||||
|
|
||||||
match tool_data.current_shape {
|
match tool_data.current_shape {
|
||||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => tool_data.data.start(document, input),
|
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => {
|
||||||
|
tool_data.data.start(document, input)
|
||||||
|
}
|
||||||
ShapeType::Line => {
|
ShapeType::Line => {
|
||||||
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
|
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
|
||||||
let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
|
let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
|
||||||
|
|
@ -734,6 +822,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
ShapeType::Star => Star::create_node(tool_options.vertices),
|
ShapeType::Star => Star::create_node(tool_options.vertices),
|
||||||
ShapeType::Circle => Circle::create_node(),
|
ShapeType::Circle => Circle::create_node(),
|
||||||
ShapeType::Arc => Arc::create_node(tool_options.arc_type),
|
ShapeType::Arc => Arc::create_node(tool_options.arc_type),
|
||||||
|
ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns),
|
||||||
ShapeType::Grid => Grid::create_node(tool_options.grid_type),
|
ShapeType::Grid => Grid::create_node(tool_options.grid_type),
|
||||||
ShapeType::Rectangle => Rectangle::create_node(),
|
ShapeType::Rectangle => Rectangle::create_node(),
|
||||||
ShapeType::Ellipse => Ellipse::create_node(),
|
ShapeType::Ellipse => Ellipse::create_node(),
|
||||||
|
|
@ -746,7 +835,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
let defered_responses = &mut VecDeque::new();
|
let defered_responses = &mut VecDeque::new();
|
||||||
|
|
||||||
match tool_data.current_shape {
|
match tool_data.current_shape {
|
||||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => {
|
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => {
|
||||||
defered_responses.add(GraphOperationMessage::TransformSet {
|
defered_responses.add(GraphOperationMessage::TransformSet {
|
||||||
layer,
|
layer,
|
||||||
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
|
||||||
|
|
@ -783,6 +872,7 @@ impl Fsm for ShapeToolFsmState {
|
||||||
ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses),
|
ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||||
ShapeType::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses),
|
ShapeType::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||||
ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses),
|
ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||||
|
ShapeType::Spiral => Spiral::update_shape(document, input, layer, tool_data, responses),
|
||||||
ShapeType::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses),
|
ShapeType::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses),
|
||||||
ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses),
|
ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||||
ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses),
|
ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses),
|
||||||
|
|
@ -964,6 +1054,9 @@ impl Fsm for ShapeToolFsmState {
|
||||||
responses.add(DocumentMessage::AbortTransaction);
|
responses.add(DocumentMessage::AbortTransaction);
|
||||||
tool_data.data.cleanup(responses);
|
tool_data.data.cleanup(responses);
|
||||||
tool_data.current_shape = shape;
|
tool_data.current_shape = shape;
|
||||||
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
|
options: ShapeOptionsUpdate::ShapeType(shape),
|
||||||
|
});
|
||||||
|
|
||||||
responses.add(ShapeToolMessage::UpdateOptions {
|
responses.add(ShapeToolMessage::UpdateOptions {
|
||||||
options: ShapeOptionsUpdate::ShapeType(shape),
|
options: ShapeOptionsUpdate::ShapeType(shape),
|
||||||
|
|
@ -1000,6 +1093,10 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
|
||||||
]),
|
]),
|
||||||
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]),
|
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]),
|
||||||
],
|
],
|
||||||
|
ShapeType::Spiral => vec![
|
||||||
|
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Spiral")]),
|
||||||
|
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Turns")]),
|
||||||
|
],
|
||||||
ShapeType::Ellipse => vec![HintGroup(vec![
|
ShapeType::Ellipse => vec![HintGroup(vec![
|
||||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
|
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
|
||||||
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
|
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
|
||||||
|
|
@ -1046,6 +1143,7 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
|
||||||
HintInfo::keys([Key::Control], "Lock Angle"),
|
HintInfo::keys([Key::Control], "Lock Angle"),
|
||||||
]),
|
]),
|
||||||
ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
|
ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
|
||||||
|
ShapeType::Spiral => HintGroup(vec![]),
|
||||||
};
|
};
|
||||||
|
|
||||||
if !tool_hint_group.0.is_empty() {
|
if !tool_hint_group.0.is_empty() {
|
||||||
|
|
@ -1056,6 +1154,10 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
|
||||||
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]));
|
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if matches!(shape, ShapeType::Spiral) {
|
||||||
|
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Turns")]));
|
||||||
|
}
|
||||||
|
|
||||||
HintData(common_hint_group)
|
HintData(common_hint_group)
|
||||||
}
|
}
|
||||||
ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![
|
ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![
|
||||||
|
|
|
||||||
|
|
@ -119,7 +119,7 @@ impl MessageHandler<TransformLayerMessage, TransformLayerMessageContext<'_>> for
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if !using_path_tool {
|
if !using_path_tool || !using_shape_tool {
|
||||||
self.pivot_gizmo.recalculate_transform(document);
|
self.pivot_gizmo.recalculate_transform(document);
|
||||||
*selected.pivot = self.pivot_gizmo.position(document);
|
*selected.pivot = self.pivot_gizmo.position(document);
|
||||||
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
|
self.local_pivot = document.metadata().document_to_viewport.inverse().transform_point2(*selected.pivot);
|
||||||
|
|
|
||||||
|
|
@ -1,8 +1,9 @@
|
||||||
use super::consts::*;
|
use super::consts::*;
|
||||||
use super::*;
|
use super::*;
|
||||||
use crate::vector::misc::point_to_dvec2;
|
use crate::vector::misc::{SpiralType, point_to_dvec2};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
use kurbo::PathSeg;
|
use kurbo::PathSeg;
|
||||||
|
use std::f64::consts::TAU;
|
||||||
|
|
||||||
pub struct PathSegPoints {
|
pub struct PathSegPoints {
|
||||||
pub p0: DVec2,
|
pub p0: DVec2,
|
||||||
|
|
@ -315,4 +316,125 @@ impl<PointId: Identifier> Subpath<PointId> {
|
||||||
pub fn new_line(p1: DVec2, p2: DVec2) -> Self {
|
pub fn new_line(p1: DVec2, p2: DVec2) -> Self {
|
||||||
Self::from_anchors([p1, p2], false)
|
Self::from_anchors([p1, p2], false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn new_spiral(a: f64, outer_radius: f64, turns: f64, start_angle: f64, delta_theta: f64, spiral_type: SpiralType) -> Self {
|
||||||
|
let mut manipulator_groups = Vec::new();
|
||||||
|
let mut prev_in_handle = None;
|
||||||
|
let theta_end = turns * std::f64::consts::TAU + start_angle;
|
||||||
|
|
||||||
|
let b = calculate_b(a, turns, outer_radius, spiral_type);
|
||||||
|
|
||||||
|
let mut theta = start_angle;
|
||||||
|
while theta < theta_end {
|
||||||
|
let theta_next = f64::min(theta + delta_theta, theta_end);
|
||||||
|
|
||||||
|
let p0 = spiral_point(theta, a, b, spiral_type);
|
||||||
|
let p3 = spiral_point(theta_next, a, b, spiral_type);
|
||||||
|
let t0 = spiral_tangent(theta, a, b, spiral_type);
|
||||||
|
let t1 = spiral_tangent(theta_next, a, b, spiral_type);
|
||||||
|
|
||||||
|
let arc_len = spiral_arc_length(theta, theta_next, a, b, spiral_type);
|
||||||
|
let d = arc_len / 3.;
|
||||||
|
|
||||||
|
let p1 = p0 + d * t0;
|
||||||
|
let p2 = p3 - d * t1;
|
||||||
|
|
||||||
|
manipulator_groups.push(ManipulatorGroup::new(p0, prev_in_handle, Some(p1)));
|
||||||
|
prev_in_handle = Some(p2);
|
||||||
|
|
||||||
|
// If final segment, end with anchor at theta_end
|
||||||
|
if (theta_next - theta_end).abs() < f64::EPSILON {
|
||||||
|
manipulator_groups.push(ManipulatorGroup::new(p3, prev_in_handle, None));
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
theta = theta_next;
|
||||||
|
}
|
||||||
|
|
||||||
|
Self::new(manipulator_groups, false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn calculate_b(a: f64, turns: f64, outer_radius: f64, spiral_type: SpiralType) -> f64 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => {
|
||||||
|
let total_theta = turns * TAU;
|
||||||
|
(outer_radius - a) / total_theta
|
||||||
|
}
|
||||||
|
SpiralType::Logarithmic => {
|
||||||
|
let total_theta = turns * TAU;
|
||||||
|
((outer_radius.abs() / a).ln()) / total_theta
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a point on the given spiral type at angle `theta`.
|
||||||
|
pub fn spiral_point(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => archimedean_spiral_point(theta, a, b),
|
||||||
|
SpiralType::Logarithmic => log_spiral_point(theta, a, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the tangent direction at angle `theta` for the given spiral type.
|
||||||
|
pub fn spiral_tangent(theta: f64, a: f64, b: f64, spiral_type: SpiralType) -> DVec2 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => archimedean_spiral_tangent(theta, a, b),
|
||||||
|
SpiralType::Logarithmic => log_spiral_tangent(theta, a, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length between two angles for the given spiral type.
|
||||||
|
pub fn spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64, spiral_type: SpiralType) -> f64 {
|
||||||
|
match spiral_type {
|
||||||
|
SpiralType::Archimedean => archimedean_spiral_arc_length(theta_start, theta_end, a, b),
|
||||||
|
SpiralType::Logarithmic => log_spiral_arc_length(theta_start, theta_end, a, b),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a point on a logarithmic spiral at angle `theta`.
|
||||||
|
pub fn log_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a * (b * theta).exp(); // a * e^(bθ)
|
||||||
|
DVec2::new(r * theta.cos(), -r * theta.sin())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length along a logarithmic spiral between two angles.
|
||||||
|
pub fn log_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
||||||
|
let factor = (1. + b * b).sqrt();
|
||||||
|
(a / b) * factor * ((b * theta_end).exp() - (b * theta_start).exp())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the tangent direction of a logarithmic spiral at angle `theta`.
|
||||||
|
pub fn log_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a * (b * theta).exp();
|
||||||
|
let dx = r * (b * theta.cos() - theta.sin());
|
||||||
|
let dy = r * (b * theta.sin() + theta.cos());
|
||||||
|
|
||||||
|
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns a point on an Archimedean spiral at angle `theta`.
|
||||||
|
pub fn archimedean_spiral_point(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a + b * theta;
|
||||||
|
DVec2::new(r * theta.cos(), -r * theta.sin())
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Returns the tangent direction of an Archimedean spiral at angle `theta`.
|
||||||
|
pub fn archimedean_spiral_tangent(theta: f64, a: f64, b: f64) -> DVec2 {
|
||||||
|
let r = a + b * theta;
|
||||||
|
let dx = b * theta.cos() - r * theta.sin();
|
||||||
|
let dy = b * theta.sin() + r * theta.cos();
|
||||||
|
DVec2::new(dx, -dy).normalize_or(DVec2::X)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length along an Archimedean spiral between two angles.
|
||||||
|
pub fn archimedean_spiral_arc_length(theta_start: f64, theta_end: f64, a: f64, b: f64) -> f64 {
|
||||||
|
archimedean_spiral_arc_length_origin(theta_end, a, b) - archimedean_spiral_arc_length_origin(theta_start, a, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes arc length from origin to a point on Archimedean spiral at angle `theta`.
|
||||||
|
pub fn archimedean_spiral_arc_length_origin(theta: f64, a: f64, b: f64) -> f64 {
|
||||||
|
let r = a + b * theta;
|
||||||
|
let sqrt_term = (r * r + b * b).sqrt();
|
||||||
|
(r * sqrt_term + b * b * ((r + sqrt_term).ln())) / (2. * b)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use crate::registry::types::{Angle, PixelSize};
|
||||||
use crate::subpath;
|
use crate::subpath;
|
||||||
use crate::table::Table;
|
use crate::table::Table;
|
||||||
use crate::vector::Vector;
|
use crate::vector::Vector;
|
||||||
use crate::vector::misc::HandleId;
|
use crate::vector::misc::{HandleId, SpiralType};
|
||||||
use glam::DVec2;
|
use glam::DVec2;
|
||||||
|
|
||||||
trait CornerRadius {
|
trait CornerRadius {
|
||||||
|
|
@ -75,6 +75,27 @@ fn arc(
|
||||||
)))
|
)))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[node_macro::node(category("Vector: Shape"), properties("spiral_properties"))]
|
||||||
|
fn spiral(
|
||||||
|
_: impl Ctx,
|
||||||
|
_primary: (),
|
||||||
|
spiral_type: SpiralType,
|
||||||
|
#[default(5.)] turns: f64,
|
||||||
|
#[default(0.)] start_angle: f64,
|
||||||
|
#[default(0.)] inner_radius: f64,
|
||||||
|
#[default(25)] outer_radius: f64,
|
||||||
|
#[default(90.)] angular_resolution: f64,
|
||||||
|
) -> Table<Vector> {
|
||||||
|
Table::new_from_element(Vector::from_subpath(subpath::Subpath::new_spiral(
|
||||||
|
inner_radius,
|
||||||
|
outer_radius,
|
||||||
|
turns,
|
||||||
|
start_angle.to_radians(),
|
||||||
|
angular_resolution.to_radians(),
|
||||||
|
spiral_type,
|
||||||
|
)))
|
||||||
|
}
|
||||||
|
|
||||||
#[node_macro::node(category("Vector: Shape"))]
|
#[node_macro::node(category("Vector: Shape"))]
|
||||||
fn ellipse(
|
fn ellipse(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
|
|
|
||||||
|
|
@ -415,3 +415,11 @@ impl HandleId {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[derive(Default, Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type, node_macro::ChoiceType)]
|
||||||
|
#[widget(Dropdown)]
|
||||||
|
pub enum SpiralType {
|
||||||
|
#[default]
|
||||||
|
Archimedean,
|
||||||
|
Logarithmic,
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -241,9 +241,8 @@ async fn repeat<I: 'n + Send + Clone>(
|
||||||
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
|
#[node_macro::node(category("Instancing"), path(graphene_core::vector))]
|
||||||
async fn circular_repeat<I: 'n + Send + Clone>(
|
async fn circular_repeat<I: 'n + Send + Clone>(
|
||||||
_: impl Ctx,
|
_: impl Ctx,
|
||||||
// TODO: Implement other graphical types.
|
|
||||||
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<I>,
|
#[implementations(Table<Graphic>, Table<Vector>, Table<Raster<CPU>>, Table<Color>, Table<GradientStops>)] instance: Table<I>,
|
||||||
angle_offset: Angle,
|
start_angle: Angle,
|
||||||
#[unit(" px")]
|
#[unit(" px")]
|
||||||
#[default(5)]
|
#[default(5)]
|
||||||
radius: f64,
|
radius: f64,
|
||||||
|
|
@ -254,7 +253,7 @@ async fn circular_repeat<I: 'n + Send + Clone>(
|
||||||
let mut result_table = Table::new();
|
let mut result_table = Table::new();
|
||||||
|
|
||||||
for index in 0..count {
|
for index in 0..count {
|
||||||
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + angle_offset.to_radians());
|
let angle = DAffine2::from_angle((TAU / count as f64) * index as f64 + start_angle.to_radians());
|
||||||
let translation = DAffine2::from_translation(radius * DVec2::Y);
|
let translation = DAffine2::from_translation(radius * DVec2::Y);
|
||||||
let transform = angle * translation;
|
let transform = angle * translation;
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -242,6 +242,7 @@ tagged_value! {
|
||||||
ArcType(graphene_core::vector::misc::ArcType),
|
ArcType(graphene_core::vector::misc::ArcType),
|
||||||
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
|
MergeByDistanceAlgorithm(graphene_core::vector::misc::MergeByDistanceAlgorithm),
|
||||||
PointSpacingType(graphene_core::vector::misc::PointSpacingType),
|
PointSpacingType(graphene_core::vector::misc::PointSpacingType),
|
||||||
|
SpiralType(graphene_core::vector::misc::SpiralType),
|
||||||
#[serde(alias = "LineCap")]
|
#[serde(alias = "LineCap")]
|
||||||
StrokeCap(graphene_core::vector::style::StrokeCap),
|
StrokeCap(graphene_core::vector::style::StrokeCap),
|
||||||
#[serde(alias = "LineJoin")]
|
#[serde(alias = "LineJoin")]
|
||||||
|
|
|
||||||
|
|
@ -49,6 +49,7 @@ Next, that is fed into the <img src="https://static.graphite.rs/content/learn/in
|
||||||
| <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-parameters-3__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> | <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-output-3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> |
|
| <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-parameters-3__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> | <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-output-3.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> |
|
||||||
| <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-parameters-4__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> | <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-output-4.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> |
|
| <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-parameters-4__2.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> | <img src="https://static.graphite.rs/content/learn/introduction/features-and-limitations/circular-repeat-node-output-4.avif" onerror="this.onerror = null; this.src = this.src.replace('.avif', '.png')" onload="this.width = this.naturalWidth / 2" alt="" /> |
|
||||||
|
|
||||||
|
<!-- TODO: Rename "Angle Offset" to "Start Angle" and redo the screenshots which show that -->
|
||||||
The node's properties offer controls over settings like *Angle Offset* (what angle to start at), *Radius* (distance from the center), and *Instances* (how many copies to distribute). These parameters can also be exposed into the graph so they are driven by the calculated numerical outputs of other nodes instead of values you pick by hand.
|
The node's properties offer controls over settings like *Angle Offset* (what angle to start at), *Radius* (distance from the center), and *Instances* (how many copies to distribute). These parameters can also be exposed into the graph so they are driven by the calculated numerical outputs of other nodes instead of values you pick by hand.
|
||||||
|
|
||||||
### Raster compositing
|
### Raster compositing
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue