diff --git a/editor/src/messages/portfolio/document/node_graph/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_properties.rs index 1cc243f524..a265e115fc 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_properties.rs @@ -24,6 +24,7 @@ use graphene_std::application_io::TextureFrameTable; use graphene_std::ops::XY; use graphene_std::transform::Footprint; use graphene_std::vector::VectorDataTable; +use graphene_std::vector::misc::ArcType; use graphene_std::vector::misc::{BooleanOperation, GridType}; use graphene_std::vector::style::{Fill, FillChoice, FillType, GradientStops}; use graphene_std::{GraphicGroupTable, RasterFrame}; @@ -208,6 +209,7 @@ pub(crate) fn property_from_type( Some(x) if x == TypeId::of::() => grid_type_widget(document_node, node_id, index, name, description, true), Some(x) if x == TypeId::of::() => line_cap_widget(document_node, node_id, index, name, description, true), Some(x) if x == TypeId::of::() => line_join_widget(document_node, node_id, index, name, description, true), + Some(x) if x == TypeId::of::() => arc_type_widget(document_node, node_id, index, name, description, true), Some(x) if x == TypeId::of::() => vec![ DropdownInput::new(vec![vec![ MenuListEntry::new("Solid") @@ -1219,6 +1221,31 @@ pub fn line_join_widget(document_node: &DocumentNode, node_id: NodeId, index: us LayoutGroup::Row { widgets } } +pub fn arc_type_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, blank_assist: bool) -> LayoutGroup { + let mut widgets = start_widgets(document_node, node_id, index, name, description, FrontendGraphDataType::General, blank_assist); + let Some(input) = document_node.inputs.get(index) else { + log::warn!("A widget failed to be built because its node's input index is invalid."); + return LayoutGroup::Row { widgets: vec![] }; + }; + if let Some(&TaggedValue::ArcType(arc_type)) = input.as_non_exposed_value() { + let entries = [("Open", ArcType::Open), ("Closed", ArcType::Closed), ("Pie Slice", ArcType::PieSlice)] + .into_iter() + .map(|(name, val)| { + RadioEntryData::new(format!("{val:?}")) + .label(name) + .on_update(update_value(move |_| TaggedValue::ArcType(val), node_id, index)) + .on_commit(commit_value) + }) + .collect(); + + widgets.extend_from_slice(&[ + Separator::new(SeparatorType::Unrelated).widget_holder(), + RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_holder(), + ]); + } + LayoutGroup::Row { widgets } +} + pub fn color_widget(document_node: &DocumentNode, node_id: NodeId, index: usize, name: &str, description: &str, color_button: ColorInput, blank_assist: bool) -> LayoutGroup { let mut widgets = start_widgets(document_node, node_id, index, name, description, FrontendGraphDataType::General, blank_assist); diff --git a/editor/src/messages/tool/tool_messages/pen_tool.rs b/editor/src/messages/tool/tool_messages/pen_tool.rs index 3315ddf98b..3d14812187 100644 --- a/editor/src/messages/tool/tool_messages/pen_tool.rs +++ b/editor/src/messages/tool/tool_messages/pen_tool.rs @@ -1139,7 +1139,7 @@ impl PenToolData { let extension_choice = should_extend(document, viewport, tolerance, selected_nodes.selected_layers(document.metadata()), preferences); if let Some((layer, point, position)) = extension_choice { self.current_layer = Some(layer); - self.extend_existing_path(document, layer, point, position, responses); + self.extend_existing_path(document, layer, point, position); return; } @@ -1191,7 +1191,7 @@ impl PenToolData { } /// Perform extension of an existing path - fn extend_existing_path(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, point: PointId, position: DVec2, responses: &mut VecDeque) { + fn extend_existing_path(&mut self, document: &DocumentMessageHandler, layer: LayerNodeIdentifier, point: PointId, position: DVec2) { let vector_data = document.network_interface.compute_modified_vector(layer); let (handle_start, in_segment) = if let Some(vector_data) = &vector_data { vector_data diff --git a/libraries/bezier-rs/src/subpath/core.rs b/libraries/bezier-rs/src/subpath/core.rs index e67fc256d0..69a75eb1f6 100644 --- a/libraries/bezier-rs/src/subpath/core.rs +++ b/libraries/bezier-rs/src/subpath/core.rs @@ -293,6 +293,66 @@ impl Subpath { Self::new(manipulator_groups, true) } + /// Constructs an arc by a `radius`, `angle_start` and `angle_size`. Angles must be in radians. Slice option makes it look like pie or pacman. + pub fn new_arc(radius: f64, start_angle: f64, sweep_angle: f64, arc_type: ArcType) -> Self { + // Prevents glitches from numerical imprecision that have been observed during animation playback after about a minute + let start_angle = start_angle % (std::f64::consts::TAU * 2.); + let sweep_angle = sweep_angle % (std::f64::consts::TAU * 2.); + + let original_start_angle = start_angle; + let sweep_angle_sign = sweep_angle.signum(); + + let mut start_angle = 0.; + let mut sweep_angle = sweep_angle.abs(); + + if (sweep_angle / std::f64::consts::TAU).floor() as u32 % 2 == 0 { + sweep_angle %= std::f64::consts::TAU; + } else { + start_angle = sweep_angle % std::f64::consts::TAU; + sweep_angle = std::f64::consts::TAU - start_angle; + } + + sweep_angle *= sweep_angle_sign; + start_angle *= sweep_angle_sign; + start_angle += original_start_angle; + + let closed = arc_type == ArcType::Closed; + let slice = arc_type == ArcType::PieSlice; + + let center = DVec2::new(0., 0.); + let segments = (sweep_angle.abs() / (std::f64::consts::PI / 4.)).ceil().max(1.) as usize; + let step = sweep_angle / segments as f64; + let factor = 4. / 3. * (step / 2.).sin() / (1. + (step / 2.).cos()); + + let mut manipulator_groups = Vec::with_capacity(segments); + let mut prev_in_handle = None; + let mut prev_end = DVec2::new(0., 0.); + + for i in 0..segments { + let start_angle = start_angle + step * i as f64; + let end_angle = start_angle + step; + let start_vec = DVec2::from_angle(start_angle); + let end_vec = DVec2::from_angle(end_angle); + + let start = center + radius * start_vec; + let end = center + radius * end_vec; + + let handle_start = start + start_vec.perp() * radius * factor; + let handle_end = end - end_vec.perp() * radius * factor; + + manipulator_groups.push(ManipulatorGroup::new(start, prev_in_handle, Some(handle_start))); + prev_in_handle = Some(handle_end); + prev_end = end; + } + manipulator_groups.push(ManipulatorGroup::new(prev_end, prev_in_handle, None)); + + if slice { + manipulator_groups.push(ManipulatorGroup::new(center, None, None)); + } + + Self::new(manipulator_groups, closed || slice) + } + /// Constructs a regular polygon (ngon). Based on `sides` and `radius`, which is the distance from the center to any vertex. pub fn new_regular_polygon(center: DVec2, sides: u64, radius: f64) -> Self { let sides = sides.max(3); diff --git a/libraries/bezier-rs/src/subpath/structs.rs b/libraries/bezier-rs/src/subpath/structs.rs index faf0a98a87..f0ef24dd7d 100644 --- a/libraries/bezier-rs/src/subpath/structs.rs +++ b/libraries/bezier-rs/src/subpath/structs.rs @@ -137,3 +137,10 @@ pub enum AppendType { IgnoreStart, SmoothJoin(f64), } + +#[derive(Copy, Clone, Eq, PartialEq, Hash)] +pub enum ArcType { + Open, + Closed, + PieSlice, +} diff --git a/node-graph/gcore/src/vector/generator_nodes.rs b/node-graph/gcore/src/vector/generator_nodes.rs index a80433deda..a5dc3dd2cf 100644 --- a/node-graph/gcore/src/vector/generator_nodes.rs +++ b/node-graph/gcore/src/vector/generator_nodes.rs @@ -1,6 +1,7 @@ -use super::misc::{AsU64, GridType}; +use super::misc::{ArcType, AsU64, GridType}; use super::{PointId, SegmentId, StrokeId}; use crate::Ctx; +use crate::registry::types::Angle; use crate::vector::{HandleId, VectorData, VectorDataTable}; use bezier_rs::Subpath; use glam::DVec2; @@ -36,15 +37,32 @@ impl CornerRadius for [f64; 4] { } #[node_macro::node(category("Vector: Shape"))] -fn circle( +fn circle(_: impl Ctx, _primary: (), #[default(50.)] radius: f64) -> VectorDataTable { + let radius = radius.abs(); + VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius)))) +} + +#[node_macro::node(category("Vector: Shape"))] +fn arc( _: impl Ctx, _primary: (), - #[default(50.)] - #[min(0.)] - radius: f64, + #[default(50.)] radius: f64, + start_angle: Angle, + #[default(270.)] + #[range((0., 360.))] + sweep_angle: Angle, + arc_type: ArcType, ) -> VectorDataTable { - let radius = radius.max(0.); - VectorDataTable::new(VectorData::from_subpath(Subpath::new_ellipse(DVec2::splat(-radius), DVec2::splat(radius)))) + VectorDataTable::new(VectorData::from_subpath(Subpath::new_arc( + radius, + start_angle / 360. * std::f64::consts::TAU, + sweep_angle / 360. * std::f64::consts::TAU, + match arc_type { + ArcType::Open => bezier_rs::ArcType::Open, + ArcType::Closed => bezier_rs::ArcType::Closed, + ArcType::PieSlice => bezier_rs::ArcType::PieSlice, + }, + ))) } #[node_macro::node(category("Vector: Shape"))] diff --git a/node-graph/gcore/src/vector/misc.rs b/node-graph/gcore/src/vector/misc.rs index a63988a06f..eb1f2ba20c 100644 --- a/node-graph/gcore/src/vector/misc.rs +++ b/node-graph/gcore/src/vector/misc.rs @@ -92,3 +92,12 @@ pub enum GridType { Rectangular, Isometric, } + +#[repr(C)] +#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, serde::Serialize, serde::Deserialize, Hash, DynAny, specta::Type)] +pub enum ArcType { + #[default] + Open, + Closed, + PieSlice, +} diff --git a/node-graph/graph-craft/src/document/value.rs b/node-graph/graph-craft/src/document/value.rs index 7dfa767187..8d59d0bade 100644 --- a/node-graph/graph-craft/src/document/value.rs +++ b/node-graph/graph-craft/src/document/value.rs @@ -214,6 +214,7 @@ tagged_value! { RelativeAbsolute(graphene_core::raster::RelativeAbsolute), SelectiveColorChoice(graphene_core::raster::SelectiveColorChoice), GridType(graphene_core::vector::misc::GridType), + ArcType(graphene_core::vector::misc::ArcType), LineCap(graphene_core::vector::style::LineCap), LineJoin(graphene_core::vector::style::LineJoin), FillType(graphene_core::vector::style::FillType),