Add Arc drawing mode to the Shape tool and the associated angle gizmos (#2757)

* implement arc gizmo handler

* fixed wrapping need to fix snapping and overlays

* fixed all the issues

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0 2025-07-27 03:25:38 +05:30 committed by GitHub
parent a1796dbc08
commit 3a8c1b6f97
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 691 additions and 26 deletions

View File

@ -126,6 +126,9 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9; pub const POINT_RADIUS_HANDLE_SEGMENT_THRESHOLD: f64 = 7.9;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2; pub const NUMBER_OF_POINTS_DIAL_SPOKE_EXTENSION: f64 = 1.2;
pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.; pub const NUMBER_OF_POINTS_DIAL_SPOKE_LENGTH: f64 = 10.;
pub const ARC_SNAP_THRESHOLD: f64 = 5.;
pub const ARC_SWEEP_GIZMO_RADIUS: f64 = 14.;
pub const ARC_SWEEP_GIZMO_TEXT_HEIGHT: f64 = 12.;
pub const GIZMO_HIDE_THRESHOLD: f64 = 20.; pub const GIZMO_HIDE_THRESHOLD: f64 = 20.;
// SCROLLBARS // SCROLLBARS

View File

@ -1,8 +1,8 @@
use super::utility_functions::overlay_canvas_context; use super::utility_functions::overlay_canvas_context;
use crate::consts::{ use crate::consts::{
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL,
COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE,
PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
}; };
use crate::messages::prelude::Message; use crate::messages::prelude::Message;
use bezier_rs::{Bezier, Subpath}; use bezier_rs::{Bezier, Subpath};
@ -423,6 +423,14 @@ impl OverlayContext {
self.render_context.stroke(); self.render_context.stroke();
} }
pub fn draw_arc_gizmo_angle(&mut self, pivot: DVec2, bold_radius: f64, dash_radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
let end_point1 = pivot + bold_radius * DVec2::from_angle(angle + offset_angle);
let end_point2 = pivot + dash_radius * DVec2::from_angle(offset_angle);
self.line(pivot, end_point1, None, None);
self.dashed_line(pivot, end_point2, None, None, Some(2.), Some(2.), Some(0.5));
self.draw_arc(pivot, arc_radius, offset_angle, (angle) % TAU + offset_angle);
}
pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) { pub fn draw_angle(&mut self, pivot: DVec2, radius: f64, arc_radius: f64, offset_angle: f64, angle: f64) {
let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle); let end_point1 = pivot + radius * DVec2::from_angle(angle + offset_angle);
let end_point2 = pivot + radius * DVec2::from_angle(offset_angle); let end_point2 = pivot + radius * DVec2::from_angle(offset_angle);
@ -584,6 +592,12 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, dash_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
self.draw_arc_gizmo_angle(pivot, bold_radius, dash_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
}
/// Used by the Pen and Path tools to outline the path of the shape. /// Used by the Pen and Path tools to outline the path of the shape.
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) { pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();

View File

@ -517,8 +517,8 @@ impl<'a> Selected<'a> {
tool_type: &'a ToolType, tool_type: &'a ToolType,
pen_handle: Option<&'a mut DVec2>, pen_handle: Option<&'a mut DVec2>,
) -> Self { ) -> Self {
// If user is using the Select tool then use the original layer transforms // If user is using the Select tool or Shape tool then use the original layer transforms
if (*tool_type == ToolType::Select) && (*original_transforms == OriginalTransforms::Path(HashMap::new())) { if (*tool_type == ToolType::Select || *tool_type == ToolType::Shape) && (*original_transforms == OriginalTransforms::Path(HashMap::new())) {
*original_transforms = OriginalTransforms::Layer(HashMap::new()); *original_transforms = OriginalTransforms::Layer(HashMap::new());
} }

View File

@ -338,6 +338,7 @@ const NODE_REPLACEMENTS: &[NodeReplacement<'static>] = &[
node: graphene_std::raster_nodes::gradient_map::gradient_map::IDENTIFIER, node: graphene_std::raster_nodes::gradient_map::gradient_map::IDENTIFIER,
aliases: &[ aliases: &[
"graphene_raster_nodes::gradient_map::GradientMapNode", "graphene_raster_nodes::gradient_map::GradientMapNode",
"graphene_raster_nodes::adjustments::GradientMapNode",
"graphene_core::raster::adjustments::GradientMapNode", "graphene_core::raster::adjustments::GradientMapNode",
"graphene_core::raster::GradientMapNode", "graphene_core::raster::GradientMapNode",
], ],

View File

@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler}; use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::arc_shape::ArcGizmoHandler;
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler; 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::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers {
None, None,
Star(StarGizmoHandler), Star(StarGizmoHandler),
Polygon(PolygonGizmoHandler), Polygon(PolygonGizmoHandler),
Arc(ArcGizmoHandler),
} }
impl ShapeGizmoHandlers { impl ShapeGizmoHandlers {
@ -32,6 +34,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(_) => "star", Self::Star(_) => "star",
Self::Polygon(_) => "polygon", Self::Polygon(_) => "polygon",
Self::Arc(_) => "arc",
Self::None => "none", Self::None => "none",
} }
} }
@ -41,6 +44,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses), Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses), Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses),
Self::None => {} Self::None => {}
} }
} }
@ -50,6 +54,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(h) => h.is_any_gizmo_hovered(), Self::Star(h) => h.is_any_gizmo_hovered(),
Self::Polygon(h) => h.is_any_gizmo_hovered(), Self::Polygon(h) => h.is_any_gizmo_hovered(),
Self::Arc(h) => h.is_any_gizmo_hovered(),
Self::None => false, Self::None => false,
} }
} }
@ -59,6 +64,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(h) => h.handle_click(), Self::Star(h) => h.handle_click(),
Self::Polygon(h) => h.handle_click(), Self::Polygon(h) => h.handle_click(),
Self::Arc(h) => h.handle_click(),
Self::None => {} Self::None => {}
} }
} }
@ -68,6 +74,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(h) => h.handle_update(drag_start, document, input, responses), Self::Star(h) => h.handle_update(drag_start, document, input, responses),
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses), Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
Self::Arc(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {} Self::None => {}
} }
} }
@ -77,6 +84,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(h) => h.cleanup(), Self::Star(h) => h.cleanup(),
Self::Polygon(h) => h.cleanup(), Self::Polygon(h) => h.cleanup(),
Self::Arc(h) => h.cleanup(),
Self::None => {} Self::None => {}
} }
} }
@ -94,6 +102,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), 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::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::None => {} Self::None => {}
} }
} }
@ -110,6 +119,7 @@ impl ShapeGizmoHandlers {
match self { match self {
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), 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::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::None => {} Self::None => {}
} }
} }
@ -141,11 +151,14 @@ impl GizmoManager {
if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() { if graph_modification_utils::get_star_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default())); return Some(ShapeGizmoHandlers::Star(StarGizmoHandler::default()));
} }
// Polygon // Polygon
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() { if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default())); return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
} }
// Arc
if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new()));
}
None None
} }

View File

@ -1,2 +1,3 @@
pub mod number_of_points_dial; pub mod number_of_points_dial;
pub mod point_radius_handle; pub mod point_radius_handle;
pub mod sweep_angle_gizmo;

View File

@ -262,7 +262,6 @@ impl PointRadiusHandle {
}; };
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
match snapping_index { match snapping_index {
// Make a triangle with previous two points // Make a triangle with previous two points
@ -274,41 +273,57 @@ impl PointRadiusHandle {
overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(before_outer_position, outer_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let before_outer_position = viewport.inverse().transform_point2(before_outer_position);
let outer_position = viewport.inverse().transform_point2(outer_position);
let point_position = viewport.inverse().transform_point2(point_position);
let l1 = (before_outer_position - outer_position).length() * 0.2; let l1 = (before_outer_position - outer_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return }; let Some(l1_direction) = (before_outer_position - outer_position).try_normalize() else { return };
let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return }; let Some(l2_direction) = (point_position - outer_position).try_normalize() else { return };
let Some(direction) = (center - outer_position).try_normalize() else { return }; let Some(direction) = (-outer_position).try_normalize() else { return };
let new_point = SQRT_2 * l1 * direction + outer_position; let new_point = SQRT_2 * l1 * direction + outer_position;
let before_outer_position = l1 * l1_direction + outer_position; let before_outer_position = l1 * l1_direction + outer_position;
let point_position = l1 * l2_direction + outer_position; let point_position = l1 * l2_direction + outer_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(
overlay_context.line(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); viewport.transform_point2(before_outer_position),
viewport.transform_point2(new_point),
Some(COLOR_OVERLAY_RED),
Some(3.),
);
overlay_context.line(viewport.transform_point2(new_point), viewport.transform_point2(point_position), Some(COLOR_OVERLAY_RED), Some(3.));
} }
1 => { 1 => {
let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2); let before_outer_position = star_vertex_position(viewport, (self.point as i32) - 1, sides, radius1, radius2);
let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2); let after_point_position = star_vertex_position(viewport, (self.point as i32) + 1, sides, radius1, radius2);
let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2); let point_position = star_vertex_position(viewport, self.point as i32, sides, radius1, radius2);
overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(before_outer_position, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(point_position, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
let before_outer_position = viewport.inverse().transform_point2(before_outer_position);
let after_point_position = viewport.inverse().transform_point2(after_point_position);
let point_position = viewport.inverse().transform_point2(point_position);
let l1 = (before_outer_position - point_position).length() * 0.2; let l1 = (before_outer_position - point_position).length() * 0.2;
let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return }; let Some(l1_direction) = (before_outer_position - point_position).try_normalize() else { return };
let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return }; let Some(l2_direction) = (after_point_position - point_position).try_normalize() else { return };
let Some(direction) = (center - point_position).try_normalize() else { return }; let Some(direction) = (-point_position).try_normalize() else { return };
let new_point = SQRT_2 * l1 * direction + point_position; let new_point = SQRT_2 * l1 * direction + point_position;
let before_outer_position = l1 * l1_direction + point_position; let before_outer_position = l1 * l1_direction + point_position;
let after_point_position = l1 * l2_direction + point_position; let after_point_position = l1 * l2_direction + point_position;
overlay_context.line(before_outer_position, new_point, Some(COLOR_OVERLAY_RED), Some(3.)); overlay_context.line(
overlay_context.line(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.)); viewport.transform_point2(before_outer_position),
viewport.transform_point2(new_point),
Some(COLOR_OVERLAY_RED),
Some(3.),
);
overlay_context.line(viewport.transform_point2(new_point), viewport.transform_point2(after_point_position), Some(COLOR_OVERLAY_RED), Some(3.));
} }
i => { i => {
// Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0 // Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0

View File

@ -0,0 +1,355 @@
use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD};
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::{DocumentMessageHandler, FrontendMessage};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shapes::shape_utility::{arc_end_points, calculate_arc_text_transform, extract_arc_parameters, format_rounded};
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DVec2;
use graph_craft::document::value::TaggedValue;
use graph_craft::document::{NodeId, NodeInput};
use std::collections::VecDeque;
use std::f64::consts::FRAC_PI_4;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum SweepAngleGizmoState {
#[default]
Inactive,
Hover,
Dragging,
Snapped,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub enum EndpointType {
#[default]
None,
Start,
End,
}
#[derive(Clone, Debug, Default)]
pub struct SweepAngleGizmo {
pub layer: Option<LayerNodeIdentifier>,
endpoint: EndpointType,
initial_start_angle: f64,
initial_sweep_angle: f64,
position_before_rotation: DVec2,
previous_mouse_position: DVec2,
total_angle_delta: f64,
snap_angles: Vec<f64>,
handle_state: SweepAngleGizmoState,
}
impl SweepAngleGizmo {
pub fn hovered(&self) -> bool {
self.handle_state == SweepAngleGizmoState::Hover
}
pub fn update_state(&mut self, state: SweepAngleGizmoState) {
self.handle_state = state;
}
pub fn is_dragging_or_snapped(&self) -> bool {
self.handle_state == SweepAngleGizmoState::Dragging || self.handle_state == SweepAngleGizmoState::Snapped
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
if self.handle_state == SweepAngleGizmoState::Inactive {
let Some((start, end)) = arc_end_points(Some(layer), document) else { return };
let Some((_, start_angle, sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else {
return;
};
let center = document.metadata().transform_to_viewport(layer).transform_point2(DVec2::ZERO);
if center.distance(start) < GIZMO_HIDE_THRESHOLD {
return;
}
let (close_to_gizmo, endpoint_type) = if mouse_position.distance(start) < 5. {
(true, EndpointType::Start)
} else if mouse_position.distance(end) < 5. {
(true, EndpointType::End)
} else {
(false, EndpointType::None)
};
if close_to_gizmo {
self.layer = Some(layer);
self.initial_start_angle = start_angle;
self.initial_sweep_angle = sweep_angle;
self.previous_mouse_position = mouse_position;
self.total_angle_delta = 0.;
self.position_before_rotation = if endpoint_type == EndpointType::End { end } else { start };
self.endpoint = endpoint_type;
self.snap_angles = Self::calculate_snap_angles();
self.update_state(SweepAngleGizmoState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
}
pub fn overlays(
&self,
selected_arc_layer: Option<LayerNodeIdentifier>,
document: &DocumentMessageHandler,
_input: &InputPreprocessorMessageHandler,
_mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
let tilt_offset = document.document_ptz.unmodified_tilt();
match self.handle_state {
SweepAngleGizmoState::Inactive => {
// Draw both endpoint handles if an arc is selected
let Some((point1, point2)) = arc_end_points(selected_arc_layer, document) else { return };
overlay_context.manipulator_handle(point1, false, Some(COLOR_OVERLAY_RED));
overlay_context.manipulator_handle(point2, false, Some(COLOR_OVERLAY_RED));
}
SweepAngleGizmoState::Hover => {
// Highlight the currently hovered endpoint only
let Some((point1, point2)) = arc_end_points(self.layer, document) else { return };
let point = if self.endpoint == EndpointType::Start { point1 } else { point2 };
overlay_context.manipulator_handle(point, true, Some(COLOR_OVERLAY_RED));
}
SweepAngleGizmoState::Dragging => {
// Show snapping guides and angle arc while dragging
let Some(layer) = self.layer else { return };
let Some((current_start, current_end)) = arc_end_points(self.layer, document) else { return };
let viewport = document.metadata().transform_to_viewport(layer);
// Depending on which endpoint is being dragged, draw guides relative to the static point
let point = if self.endpoint == EndpointType::End { current_end } else { current_start };
self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context);
}
SweepAngleGizmoState::Snapped => {
// When snapping is active, draw snapping arcs and angular guidelines
let Some((start, end)) = arc_end_points(self.layer, document) else { return };
let Some(layer) = self.layer else { return };
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
// Draw snapping arc and angle overlays between the two points
let (a, b) = if self.endpoint == EndpointType::Start { (end, start) } else { (start, end) };
self.dragging_snapping_overlays(a, b, tilt_offset, viewport, overlay_context);
// Draw lines from endpoints to the arc center
overlay_context.line(start, center, Some(COLOR_OVERLAY_RED), Some(2.));
overlay_context.line(end, center, Some(COLOR_OVERLAY_RED), Some(2.));
}
}
}
/// Draws the visual overlay during arc handle dragging or snapping interactions.
/// This includes the dynamic arc sweep, angle label, and visual guides centered around the arc's origin.
pub fn dragging_snapping_overlays(&self, initial_point: DVec2, final_point: DVec2, tilt_offset: f64, viewport: DAffine2, overlay_context: &mut OverlayContext) {
let center = viewport.transform_point2(DVec2::ZERO);
let initial_vector = initial_point - center;
let final_vector = final_point - center;
let offset_angle = initial_vector.to_angle() + tilt_offset;
let dash_radius = initial_point.distance(center);
let bold_radius = final_point.distance(center);
let angle = initial_vector.angle_to(final_vector).to_degrees();
let display_angle = viewport
.inverse()
.transform_point2(final_point)
.angle_to(viewport.inverse().transform_point2(initial_point))
.to_degrees();
let text = format!("{}°", format_rounded(display_angle, 2));
let text_texture_width = overlay_context.get_width(&text) / 2.;
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);
overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, dash_radius, center, &text, transform);
}
pub fn update_arc(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
let Some(layer) = self.layer else { return };
let Some((_, current_start_angle, current_sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let angle_delta = viewport
.inverse()
.transform_point2(self.previous_mouse_position)
.angle_to(viewport.inverse().transform_point2(input.mouse.position))
.to_degrees();
let angle = self.total_angle_delta + angle_delta;
let Some(node_id) = graph_modification_utils::get_arc_id(layer, &document.network_interface) else {
return;
};
self.update_state(SweepAngleGizmoState::Dragging);
match self.endpoint {
EndpointType::Start => {
// Dragging start changes both start and sweep
let sign = -angle.signum();
let mut total = angle;
let new_start_angle = self.initial_start_angle + total;
let new_sweep_angle = self.initial_sweep_angle + total.abs() * sign;
match () {
// Clamp sweep angle to 360°
() if new_sweep_angle > 360. => {
let wrapped = new_sweep_angle % 360.;
self.total_angle_delta = -wrapped;
// Remaining drag gets passed to the ending endpoint
let rest_angle = angle_delta + wrapped;
self.endpoint = EndpointType::End;
self.initial_sweep_angle = 360.;
self.initial_start_angle = current_start_angle + rest_angle;
self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle - wrapped, input, responses);
}
() if new_sweep_angle < 0. => {
let rest_angle = angle_delta + new_sweep_angle;
self.total_angle_delta = new_sweep_angle.abs();
self.endpoint = EndpointType::End;
self.initial_sweep_angle = 0.;
self.initial_start_angle = current_start_angle + rest_angle;
self.apply_arc_update(node_id, self.initial_start_angle, new_sweep_angle.abs(), input, responses);
}
// Wrap start angle > 180° back into [-180°, 180°] and adjust sweep
() if new_start_angle > 180. => {
let overflow = new_start_angle % 180.;
let rest_angle = angle_delta - overflow;
// We wrap the angle back into [-180°, 180°] range by jumping from +180° to -180°
// Example: dragging past 190° becomes -170°, and we subtract the overshoot from sweep
// Sweep angle must shrink to maintain consistent arc
self.total_angle_delta = rest_angle;
self.initial_start_angle = -180.;
self.initial_sweep_angle = current_sweep_angle - rest_angle;
self.apply_arc_update(node_id, self.initial_start_angle + overflow, self.initial_sweep_angle - overflow, input, responses);
}
// Wrap start angle < -180° back into [-180°, 180°] and adjust sweep
() if new_start_angle < -180. => {
let underflow = new_start_angle % 180.;
let rest_angle = angle_delta - underflow;
// We wrap the angle back into [-180°, 180°] by jumping from -190° to +170°
// Sweep must grow to reflect continued clockwise drag past -180°
// Start angle flips from -190° to +170°, and sweep increases accordingly
self.total_angle_delta = underflow;
self.initial_start_angle = 180.;
self.initial_sweep_angle = current_sweep_angle + rest_angle.abs();
self.apply_arc_update(node_id, self.initial_start_angle + underflow, self.initial_sweep_angle + underflow.abs(), input, responses);
}
_ => {
if let Some(snapped_delta) = self.check_snapping(self.initial_sweep_angle + total.abs() * sign) {
total += snapped_delta;
self.update_state(SweepAngleGizmoState::Snapped);
}
self.total_angle_delta = angle;
self.apply_arc_update(node_id, self.initial_start_angle + total, self.initial_sweep_angle + total.abs() * sign, input, responses);
}
}
}
EndpointType::End => {
// Dragging the end only changes sweep angle
let mut total = angle;
let new_sweep_angle = self.initial_sweep_angle + angle;
match () {
// Clamp sweep angle below 0°, switch to start
() if new_sweep_angle < 0. => {
let delta = angle_delta - current_sweep_angle;
let sign = -delta.signum();
self.initial_sweep_angle = 0.;
self.total_angle_delta = delta;
self.endpoint = EndpointType::Start;
self.apply_arc_update(node_id, self.initial_start_angle + delta, self.initial_sweep_angle + delta.abs() * sign, input, responses);
}
// Clamp sweep angle above 360°, switch to start
() if new_sweep_angle > 360. => {
let delta = angle_delta - (360. - current_sweep_angle);
let sign = -delta.signum();
self.total_angle_delta = angle_delta;
self.initial_sweep_angle = 360.;
self.endpoint = EndpointType::Start;
self.apply_arc_update(node_id, self.initial_start_angle + angle_delta, self.initial_sweep_angle + angle_delta.abs() * sign, input, responses);
}
_ => {
if let Some(snapped_delta) = self.check_snapping(self.initial_sweep_angle + angle) {
total += snapped_delta;
self.update_state(SweepAngleGizmoState::Snapped);
}
self.total_angle_delta = angle;
self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle + total, input, responses);
}
}
}
EndpointType::None => {}
}
}
/// Applies the updated start and sweep angles to the arc.
fn apply_arc_update(&mut self, node_id: NodeId, start_angle: f64, sweep_angle: f64, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
self.snap_angles = Self::calculate_snap_angles();
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 2),
input: NodeInput::value(TaggedValue::F64(start_angle), false),
});
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 3),
input: NodeInput::value(TaggedValue::F64(sweep_angle), false),
});
self.previous_mouse_position = input.mouse.position;
responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn check_snapping(&self, new_sweep_angle: f64) -> Option<f64> {
self.snap_angles.iter().find(|angle| (**angle - new_sweep_angle).abs() <= ARC_SNAP_THRESHOLD).map(|angle| {
let delta = angle - new_sweep_angle;
if self.endpoint == EndpointType::End { delta } else { -delta }
})
}
pub fn calculate_snap_angles() -> Vec<f64> {
let mut snap_points = Vec::new();
for i in 0..8 {
let snap_point = i as f64 * FRAC_PI_4;
snap_points.push(snap_point.to_degrees());
}
snap_points
}
pub fn cleanup(&mut self) {
self.layer = None;
self.endpoint = EndpointType::None;
self.handle_state = SweepAngleGizmoState::Inactive;
}
}

View File

@ -352,6 +352,10 @@ pub fn get_star_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Star") NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Star")
} }
pub fn get_arc_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Arc")
}
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")
} }

View File

@ -0,0 +1,138 @@
use super::shape_utility::ShapeToolModifierKey;
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::gizmos::shape_gizmos::sweep_angle_gizmo::{SweepAngleGizmo, SweepAngleGizmoState};
use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, arc_outline};
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::vector::misc::ArcType;
use std::collections::VecDeque;
#[derive(Clone, Debug, Default)]
pub struct ArcGizmoHandler {
sweep_angle_gizmo: SweepAngleGizmo,
}
impl ArcGizmoHandler {
pub fn new() -> Self {
Self { ..Default::default() }
}
}
impl ShapeGizmoHandler for ArcGizmoHandler {
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position, responses);
}
fn is_any_gizmo_hovered(&self) -> bool {
self.sweep_angle_gizmo.hovered()
}
fn handle_click(&mut self) {
if self.sweep_angle_gizmo.hovered() {
self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging);
}
}
fn handle_update(&mut self, _drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.sweep_angle_gizmo.is_dragging_or_snapped() {
self.sweep_angle_gizmo.update_arc(document, input, responses);
}
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState,
mouse_position: DVec2,
overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext,
) {
if self.sweep_angle_gizmo.is_dragging_or_snapped() {
self.sweep_angle_gizmo.overlays(None, document, input, mouse_position, overlay_context);
arc_outline(self.sweep_angle_gizmo.layer, document, overlay_context);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
selected_shape_layers: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState,
mouse_position: DVec2,
overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext,
) {
self.sweep_angle_gizmo.overlays(selected_shape_layers, document, input, mouse_position, overlay_context);
arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context);
}
fn cleanup(&mut self) {
self.sweep_angle_gizmo.cleanup();
}
}
#[derive(Default)]
pub struct Arc;
impl Arc {
pub fn create_node(arc_type: ArcType) -> NodeTemplate {
let node_type = resolve_document_node_type("Arc").expect("Ellipse node does not exist");
node_type.node_template_input_override([
None,
Some(NodeInput::value(TaggedValue::F64(0.5), false)),
Some(NodeInput::value(TaggedValue::F64(0.), false)),
Some(NodeInput::value(TaggedValue::F64(270.), false)),
Some(NodeInput::value(TaggedValue::ArcType(arc_type), false)),
])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let (center, lock_ratio) = (modifier[0], modifier[1]);
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_arc_id(layer, &document.network_interface) else {
return;
};
let dimensions = (start - end).abs();
let mut scale = DVec2::ONE;
let radius: f64;
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y;
scale.y = 1.;
radius = dimensions.y / 2.;
} else {
scale.y = dimensions.y / dimensions.x;
scale.x = 1.;
radius = dimensions.x / 2.;
}
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64(radius), false),
});
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(scale, 0., start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}
}

View File

@ -1,3 +1,4 @@
pub mod arc_shape;
pub mod ellipse_shape; pub mod ellipse_shape;
pub mod line_shape; pub mod line_shape;
pub mod polygon_shape; pub mod polygon_shape;

View File

@ -1,4 +1,5 @@
use super::ShapeToolData; use super::ShapeToolData;
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT};
use crate::messages::message::Message; use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; 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::document_metadata::LayerNodeIdentifier;
@ -14,7 +15,7 @@ use glam::{DAffine2, DMat2, DVec2};
use graph_craft::document::NodeInput; use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue; use graph_craft::document::value::TaggedValue;
use graphene_std::vector::click_target::ClickTargetType; use graphene_std::vector::click_target::ClickTargetType;
use graphene_std::vector::misc::dvec2_to_point; use graphene_std::vector::misc::{ArcType, dvec2_to_point};
use kurbo::{BezPath, PathEl, Shape}; use kurbo::{BezPath, PathEl, Shape};
use std::collections::VecDeque; use std::collections::VecDeque;
use std::f64::consts::{PI, TAU}; use std::f64::consts::{PI, TAU};
@ -23,10 +24,11 @@ use std::f64::consts::{PI, TAU};
pub enum ShapeType { pub enum ShapeType {
#[default] #[default]
Polygon = 0, Polygon = 0,
Star = 1, Star,
Rectangle = 2, Arc,
Ellipse = 3, Rectangle,
Line = 4, Ellipse,
Line,
} }
impl ShapeType { impl ShapeType {
@ -34,6 +36,7 @@ impl ShapeType {
(match self { (match self {
Self::Polygon => "Polygon", Self::Polygon => "Polygon",
Self::Star => "Star", Self::Star => "Star",
Self::Arc => "Arc",
Self::Rectangle => "Rectangle", Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse", Self::Ellipse => "Ellipse",
Self::Line => "Line", Self::Line => "Line",
@ -234,7 +237,46 @@ pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document:
Some((n, radius)) Some((n, radius))
} }
/// Calculate the viewport position of as a star vertex given its index /// Extract the node input values of an arc.
/// Returns an option of (radius, start angle, sweep angle, arc type).
pub fn extract_arc_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(f64, f64, f64, ArcType)> {
let node_inputs = NodeGraphLayer::new(layer?, &document.network_interface).find_node_inputs("Arc")?;
let (Some(&TaggedValue::F64(radius)), Some(&TaggedValue::F64(start_angle)), Some(&TaggedValue::F64(sweep_angle)), Some(&TaggedValue::ArcType(arc_type))) = (
node_inputs.get(1)?.as_value(),
node_inputs.get(2)?.as_value(),
node_inputs.get(3)?.as_value(),
node_inputs.get(4)?.as_value(),
) else {
return None;
};
Some((radius, start_angle, sweep_angle, arc_type))
}
/// Calculate the viewport positions of arc endpoints
pub fn arc_end_points(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(DVec2, DVec2)> {
let (radius, start_angle, sweep_angle, _) = extract_arc_parameters(Some(layer?), document)?;
let viewport = document.metadata().transform_to_viewport(layer?);
arc_end_points_ignore_layer(radius, start_angle, sweep_angle, Some(viewport))
}
pub fn arc_end_points_ignore_layer(radius: f64, start_angle: f64, sweep_angle: f64, viewport: Option<DAffine2>) -> Option<(DVec2, DVec2)> {
let end_angle = start_angle.to_radians() + sweep_angle.to_radians();
let start_point = radius * DVec2::from_angle(start_angle.to_radians());
let end_point = radius * DVec2::from_angle(end_angle);
if let Some(transform) = viewport {
return Some((transform.transform_point2(start_point), transform.transform_point2(end_point)));
}
Some((start_point, end_point))
}
/// Calculate the viewport position of a star vertex given its index
pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 { pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 {
let angle = ((vertex_index as f64) * PI) / (n as f64); let angle = ((vertex_index as f64) * PI) / (n as f64);
let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 }; let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 };
@ -290,6 +332,29 @@ pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMe
overlay_context.outline(subpath.iter(), viewport, None); overlay_context.outline(subpath.iter(), viewport, None);
} }
/// Outlines the geometric shape made by an Arc node
pub fn arc_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
let Some(layer) = layer else { return };
let Some((radius, start_angle, sweep_angle, arc_type)) = extract_arc_parameters(Some(layer), document) else {
return;
};
let subpath: Vec<ClickTargetType> = vec![ClickTargetType::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,
},
))];
let viewport = document.metadata().transform_to_viewport(layer);
overlay_context.outline(subpath.iter(), viewport, None);
}
/// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications /// Check if the the cursor is inside the geometric star shape made by the Star node without any upstream node modifications
pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool { pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool {
let mut paths = Vec::new(); let mut paths = Vec::new();
@ -363,3 +428,32 @@ pub fn draw_snapping_ticks(snap_radii: &[f64], direction: DVec2, viewport: DAffi
overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.)); overlay_context.line(tick_position, tick_position - tick_direction * 5., None, Some(2.));
} }
} }
/// Wraps an angle (in radians) into the range [0, 2π).
pub fn wrap_to_tau(angle: f64) -> f64 {
(angle % TAU + TAU) % TAU
}
pub fn format_rounded(value: f64, precision: usize) -> String {
format!("{value:.precision$}").trim_end_matches('0').trim_end_matches('.').to_string()
}
/// Gives the approximated angle to display in degrees, given an angle in degrees.
pub fn calculate_display_angle(angle: f64) -> f64 {
if angle.is_sign_positive() {
angle - (angle / 360.).floor() * 360.
} else if angle.is_sign_negative() {
angle - ((angle / 360.).floor() + 1.) * 360.
} else {
angle
}
}
pub fn calculate_arc_text_transform(angle: f64, offset_angle: f64, center: DVec2, width: f64) -> DAffine2 {
let text_angle_on_unit_circle = DVec2::from_angle((angle.to_radians() % TAU) / 2. + offset_angle);
let text_texture_position = DVec2::new(
(ARC_SWEEP_GIZMO_RADIUS + 4. + width) * text_angle_on_unit_circle.x,
(ARC_SWEEP_GIZMO_RADIUS + ARC_SWEEP_GIZMO_TEXT_HEIGHT) * text_angle_on_unit_circle.y,
);
DAffine2::from_translation(text_texture_position + center)
}

View File

@ -10,6 +10,7 @@ use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoMan
use crate::messages::tool::common_functionality::graph_modification_utils; 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::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::shapes::arc_shape::Arc;
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};
@ -109,10 +110,28 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
MenuListEntry::new("Star") MenuListEntry::new("Star")
.label("Star") .label("Star")
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()),
MenuListEntry::new("Arc")
.label("Arc")
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()),
]]; ]];
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder()
} }
fn create_arc_type_widget(arc_type: ArcType) -> WidgetHolder {
let entries = vec![
RadioEntryData::new("Open")
.label("Open")
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Open)).into()),
RadioEntryData::new("Closed")
.label("Closed")
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::Closed)).into()),
RadioEntryData::new("Pie")
.label("Pie")
.on_update(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ArcType(ArcType::PieSlice)).into()),
];
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_holder()
}
fn create_weight_widget(line_weight: f64) -> WidgetHolder { fn create_weight_widget(line_weight: f64) -> WidgetHolder {
NumberInput::new(Some(line_weight)) NumberInput::new(Some(line_weight))
.unit(" px") .unit(" px")
@ -135,6 +154,11 @@ impl LayoutHolder for ShapeTool {
widgets.push(create_sides_widget(self.options.vertices)); widgets.push(create_sides_widget(self.options.vertices));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
} }
if self.options.shape_type == ShapeType::Arc {
widgets.push(create_arc_type_widget(self.options.arc_type));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder());
}
} }
if self.options.shape_type != ShapeType::Line { if self.options.shape_type != ShapeType::Line {
@ -578,7 +602,7 @@ impl Fsm for ShapeToolFsmState {
}; };
match tool_data.current_shape { match tool_data.current_shape {
ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle => tool_data.data.start(document, input), ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Arc | ShapeType::Rectangle => 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());
@ -591,6 +615,7 @@ impl Fsm for ShapeToolFsmState {
let node = match tool_data.current_shape { let node = match tool_data.current_shape {
ShapeType::Polygon => Polygon::create_node(tool_options.vertices), ShapeType::Polygon => Polygon::create_node(tool_options.vertices),
ShapeType::Star => Star::create_node(tool_options.vertices), ShapeType::Star => Star::create_node(tool_options.vertices),
ShapeType::Arc => Arc::create_node(tool_options.arc_type),
ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Rectangle => Rectangle::create_node(),
ShapeType::Ellipse => Ellipse::create_node(), ShapeType::Ellipse => Ellipse::create_node(),
ShapeType::Line => Line::create_node(document, tool_data.data.drag_start), ShapeType::Line => Line::create_node(document, tool_data.data.drag_start),
@ -602,7 +627,7 @@ impl Fsm for ShapeToolFsmState {
responses.add(Message::StartBuffer); responses.add(Message::StartBuffer);
match tool_data.current_shape { match tool_data.current_shape {
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star => { ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star => {
responses.add(GraphOperationMessage::TransformSet { 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),
@ -635,6 +660,7 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses),
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::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses),
} }
// Auto-panning // Auto-panning
@ -829,7 +855,7 @@ impl Fsm for ShapeToolFsmState {
let hint_data = match self { let hint_data = match self {
ShapeToolFsmState::Ready(shape) => { ShapeToolFsmState::Ready(shape) => {
let hint_groups = match shape { let hint_groups = match shape {
ShapeType::Polygon | ShapeType::Star => vec![ ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => vec![
HintGroup(vec![ HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"), HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(), HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
@ -859,7 +885,7 @@ impl Fsm for ShapeToolFsmState {
ShapeToolFsmState::Drawing(shape) => { ShapeToolFsmState::Drawing(shape) => {
let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]; let mut common_hint_group = vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])];
let tool_hint_group = match shape { let tool_hint_group = match shape {
ShapeType::Polygon | ShapeType::Star => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Rectangle => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Square"), HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]), ShapeType::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Line => HintGroup(vec![ ShapeType::Line => HintGroup(vec![

View File

@ -67,7 +67,7 @@ pub enum GridType {
#[widget(Radio)] #[widget(Radio)]
pub enum ArcType { pub enum ArcType {
#[default] #[default]
Open, Open = 0,
Closed, Closed,
PieSlice, PieSlice,
} }