New node: Arc (#2470)

* init

* add closed and slice options

* Make it work beyond -360 to 360 degrees

* Switch "closed" and "slice" to ArcType enum

* Update default ranges

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
ilya sheprut 2025-04-18 12:42:07 +03:00 committed by GitHub
parent 33de539d6d
commit adfcff7599
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 131 additions and 9 deletions

View file

@ -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::<GridType>() => grid_type_widget(document_node, node_id, index, name, description, true),
Some(x) if x == TypeId::of::<LineCap>() => line_cap_widget(document_node, node_id, index, name, description, true),
Some(x) if x == TypeId::of::<LineJoin>() => line_join_widget(document_node, node_id, index, name, description, true),
Some(x) if x == TypeId::of::<ArcType>() => arc_type_widget(document_node, node_id, index, name, description, true),
Some(x) if x == TypeId::of::<FillType>() => 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);

View file

@ -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<Message>) {
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

View file

@ -293,6 +293,66 @@ impl<PointId: crate::Identifier> Subpath<PointId> {
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);

View file

@ -137,3 +137,10 @@ pub enum AppendType {
IgnoreStart,
SmoothJoin(f64),
}
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
pub enum ArcType {
Open,
Closed,
PieSlice,
}

View file

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

View file

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

View file

@ -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),