mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
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:
parent
33de539d6d
commit
adfcff7599
7 changed files with 131 additions and 9 deletions
|
@ -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);
|
||||
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -137,3 +137,10 @@ pub enum AppendType {
|
|||
IgnoreStart,
|
||||
SmoothJoin(f64),
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
|
||||
pub enum ArcType {
|
||||
Open,
|
||||
Closed,
|
||||
PieSlice,
|
||||
}
|
||||
|
|
|
@ -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"))]
|
||||
|
|
|
@ -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,
|
||||
}
|
||||
|
|
|
@ -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),
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue