mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2025-07-07 15:55:00 +00:00
Merge 0b548e5281
into 24c6281644
This commit is contained in:
commit
81eecf3bec
12 changed files with 767 additions and 24 deletions
|
@ -124,6 +124,9 @@ pub const POINT_RADIUS_HANDLE_SNAP_THRESHOLD: f64 = 8.;
|
|||
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_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.;
|
||||
|
||||
// SCROLLBARS
|
||||
|
|
|
@ -1,7 +1,8 @@
|
|||
use super::utility_functions::overlay_canvas_context;
|
||||
use crate::consts::{
|
||||
COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER,
|
||||
COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER,
|
||||
ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COMPASS_ROSE_ARROW_SIZE,
|
||||
COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, MANIPULATOR_GROUP_MARKER_SIZE, PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS,
|
||||
PIVOT_DIAMETER,
|
||||
};
|
||||
use crate::messages::prelude::Message;
|
||||
use bezier_rs::{Bezier, Subpath};
|
||||
|
@ -550,6 +551,12 @@ impl OverlayContext {
|
|||
self.end_dpi_aware_transform();
|
||||
}
|
||||
|
||||
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
|
||||
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED));
|
||||
self.draw_angle(pivot, 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.
|
||||
pub fn outline_vector(&mut self, vector_data: &VectorData, transform: DAffine2) {
|
||||
self.start_dpi_aware_transform();
|
||||
|
|
|
@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
|
|||
use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageHandler};
|
||||
use crate::messages::tool::common_functionality::graph_modification_utils;
|
||||
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::shape_utility::ShapeGizmoHandler;
|
||||
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
|
||||
|
@ -23,6 +24,7 @@ pub enum ShapeGizmoHandlers {
|
|||
None,
|
||||
Star(StarGizmoHandler),
|
||||
Polygon(PolygonGizmoHandler),
|
||||
Arc(ArcGizmoHandler),
|
||||
}
|
||||
|
||||
impl ShapeGizmoHandlers {
|
||||
|
@ -32,6 +34,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
Self::Star(_) => "star",
|
||||
Self::Polygon(_) => "polygon",
|
||||
Self::Arc(_) => "arc",
|
||||
Self::None => "none",
|
||||
}
|
||||
}
|
||||
|
@ -41,6 +44,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
Self::Star(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 => {}
|
||||
}
|
||||
}
|
||||
|
@ -50,6 +54,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
Self::Star(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,
|
||||
}
|
||||
}
|
||||
|
@ -59,6 +64,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
Self::Star(h) => h.handle_click(),
|
||||
Self::Polygon(h) => h.handle_click(),
|
||||
Self::Arc(h) => h.handle_click(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
@ -68,6 +74,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
Self::Star(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 => {}
|
||||
}
|
||||
}
|
||||
|
@ -77,6 +84,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
Self::Star(h) => h.cleanup(),
|
||||
Self::Polygon(h) => h.cleanup(),
|
||||
Self::Arc(h) => h.cleanup(),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
@ -94,6 +102,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
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::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
@ -110,6 +119,7 @@ impl ShapeGizmoHandlers {
|
|||
match self {
|
||||
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::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
|
||||
Self::None => {}
|
||||
}
|
||||
}
|
||||
|
@ -146,6 +156,9 @@ impl GizmoManager {
|
|||
if graph_modification_utils::get_polygon_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Polygon(PolygonGizmoHandler::default()));
|
||||
}
|
||||
if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() {
|
||||
return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new()));
|
||||
}
|
||||
|
||||
None
|
||||
}
|
||||
|
|
|
@ -1,2 +1,3 @@
|
|||
pub mod number_of_points_dial;
|
||||
pub mod point_radius_handle;
|
||||
pub mod sweep_angle_gizmo;
|
||||
|
|
|
@ -262,7 +262,6 @@ impl PointRadiusHandle {
|
|||
};
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
match snapping_index {
|
||||
// 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(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 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(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 before_outer_position = l1 * l1_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(new_point, point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(
|
||||
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 => {
|
||||
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 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(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 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(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 before_outer_position = l1 * l1_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(new_point, after_point_position, Some(COLOR_OVERLAY_RED), Some(3.));
|
||||
overlay_context.line(
|
||||
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 => {
|
||||
// Use `self.point` as absolute reference as it matches the index of vertices of the star starting from 0
|
||||
|
|
|
@ -0,0 +1,434 @@
|
|||
use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD};
|
||||
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::tool::common_functionality::graph_modification_utils;
|
||||
use crate::messages::tool::common_functionality::shapes::shape_utility::{
|
||||
arc_end_points, arc_end_points_ignore_layer, calculate_arc_text_transform, calculate_display_angle, extract_arc_parameters, wrap_to_tau,
|
||||
};
|
||||
use crate::messages::tool::tool_messages::tool_prelude::*;
|
||||
use crate::messages::{
|
||||
frontend::utility_types::MouseCursorIcon,
|
||||
message::Message,
|
||||
prelude::{DocumentMessageHandler, FrontendMessage},
|
||||
};
|
||||
use glam::DVec2;
|
||||
use graph_craft::document::NodeId;
|
||||
use graph_craft::document::NodeInput;
|
||||
use graph_craft::document::value::TaggedValue;
|
||||
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,
|
||||
initial_start_point: 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>) {
|
||||
match 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;
|
||||
}
|
||||
|
||||
if mouse_position.distance(start) < 5. {
|
||||
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.endpoint = EndpointType::Start;
|
||||
self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle);
|
||||
self.update_state(SweepAngleGizmoState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
return;
|
||||
}
|
||||
|
||||
if mouse_position.distance(end) < 5. {
|
||||
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.endpoint = EndpointType::End;
|
||||
self.snap_angles = self.calculate_snap_angles(start_angle, sweep_angle);
|
||||
self.update_state(SweepAngleGizmoState::Hover);
|
||||
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
|
||||
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
SweepAngleGizmoState::Hover => {}
|
||||
SweepAngleGizmoState::Dragging => {}
|
||||
SweepAngleGizmoState::Snapped => {}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn overlays(
|
||||
&self,
|
||||
selected_arc_layer: Option<LayerNodeIdentifier>,
|
||||
document: &DocumentMessageHandler,
|
||||
_input: &InputPreprocessorMessageHandler,
|
||||
_mouse_position: DVec2,
|
||||
overlay_context: &mut OverlayContext,
|
||||
) {
|
||||
let format_rounded = |value: f64, precision: usize| format!("{:.*}", precision, value).trim_end_matches('0').trim_end_matches('.').to_string();
|
||||
let tilt_offset = document.document_ptz.unmodified_tilt();
|
||||
|
||||
match self.handle_state {
|
||||
SweepAngleGizmoState::Inactive => {
|
||||
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 => {
|
||||
let Some((point1, point2)) = arc_end_points(self.layer, document) else { return };
|
||||
|
||||
if matches!(self.endpoint, EndpointType::Start) {
|
||||
overlay_context.manipulator_handle(point1, true, Some(COLOR_OVERLAY_RED));
|
||||
} else {
|
||||
overlay_context.manipulator_handle(point2, true, Some(COLOR_OVERLAY_RED));
|
||||
}
|
||||
}
|
||||
SweepAngleGizmoState::Dragging => {
|
||||
let Some(layer) = self.layer else { return };
|
||||
let Some((start, end)) = arc_end_points(self.layer, document) else { return };
|
||||
|
||||
let viewport = document.metadata().transform_to_viewport(layer);
|
||||
let center = viewport.transform_point2(DVec2::ZERO);
|
||||
|
||||
let Some((radius, _, _, _)) = extract_arc_parameters(self.layer, document) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let Some((initial_start, initial_end)) = arc_end_points_ignore_layer(radius, self.initial_start_angle, self.initial_sweep_angle, Some(viewport)) else {
|
||||
return;
|
||||
};
|
||||
|
||||
let angle = self.total_angle_delta;
|
||||
|
||||
let display_angle = calculate_display_angle(angle);
|
||||
|
||||
let text = format!("{}°", format_rounded(display_angle, 2));
|
||||
let text_texture_width = overlay_context.get_width(&text) / 2.;
|
||||
|
||||
if self.endpoint == EndpointType::End {
|
||||
let initial_vector = initial_end - center;
|
||||
let offset_angle = initial_vector.to_angle() + tilt_offset;
|
||||
|
||||
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);
|
||||
|
||||
overlay_context.arc_sweep_angle(offset_angle, angle, end, radius, center, &text, transform);
|
||||
} else {
|
||||
let initial_vector = initial_start - center;
|
||||
let offset_angle = initial_vector.to_angle() + tilt_offset;
|
||||
|
||||
let transform = calculate_arc_text_transform(angle, offset_angle, center, text_texture_width);
|
||||
|
||||
overlay_context.arc_sweep_angle(offset_angle, angle, start, radius, center, &text, transform);
|
||||
}
|
||||
}
|
||||
|
||||
SweepAngleGizmoState::Snapped => {
|
||||
let Some((current_start, current_end)) = arc_end_points(self.layer, document) else {
|
||||
return;
|
||||
};
|
||||
let Some((radius, _, _, _)) = extract_arc_parameters(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);
|
||||
|
||||
if self.endpoint == EndpointType::Start {
|
||||
let initial_vector = current_end - center;
|
||||
let final_vector = current_start - center;
|
||||
let offset_angle = initial_vector.to_angle() + tilt_offset;
|
||||
|
||||
let angle = initial_vector.angle_to(final_vector).to_degrees();
|
||||
let display_angle = calculate_display_angle(angle);
|
||||
|
||||
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, current_start, radius, center, &text, transform);
|
||||
} else {
|
||||
let initial_vector = current_start - center;
|
||||
let final_vector = current_end - center;
|
||||
let offset_angle = initial_vector.to_angle() + tilt_offset;
|
||||
|
||||
let angle = initial_vector.angle_to(final_vector).to_degrees();
|
||||
log::info!("angle {:?}", angle);
|
||||
let display_angle = calculate_display_angle(angle);
|
||||
|
||||
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, current_end, radius, center, &text, transform);
|
||||
}
|
||||
|
||||
overlay_context.line(current_start, center, Some(COLOR_OVERLAY_RED), Some(2.0));
|
||||
overlay_context.line(current_end, center, Some(COLOR_OVERLAY_RED), Some(2.0));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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() * -1.;
|
||||
let mut total = angle;
|
||||
|
||||
let new_start_angle = self.initial_start_angle + total;
|
||||
let new_sweep_angle = self.initial_sweep_angle + total.abs() * sign;
|
||||
|
||||
// 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 end 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);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle + angle, 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;
|
||||
|
||||
// Clamp sweep angle below 0°, switch to start
|
||||
if new_sweep_angle < 0. {
|
||||
let delta = angle_delta - current_sweep_angle;
|
||||
let sign = delta.signum() * -1.;
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
// 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() * -1.;
|
||||
|
||||
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);
|
||||
return;
|
||||
}
|
||||
|
||||
if let Some(snapped_delta) = self.check_snapping(self.initial_start_angle, 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(start_angle, sweep_angle);
|
||||
|
||||
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_start_angle: f64, new_sweep_angle: f64) -> Option<f64> {
|
||||
let wrapped_sweep_angle = wrap_to_tau(new_sweep_angle.to_radians()).to_degrees();
|
||||
let wrapped_start_angle = wrap_to_tau(new_start_angle.to_radians()).to_degrees();
|
||||
if self.endpoint == EndpointType::End {
|
||||
return self
|
||||
.snap_angles
|
||||
.iter()
|
||||
.find(|angle| ((**angle) - (wrapped_sweep_angle)).abs() < ARC_SNAP_THRESHOLD)
|
||||
.map(|angle| angle - wrapped_sweep_angle);
|
||||
} else {
|
||||
return self
|
||||
.snap_angles
|
||||
.iter()
|
||||
.find(|angle| ((**angle) - (wrapped_start_angle)).abs() < ARC_SNAP_THRESHOLD)
|
||||
.map(|angle| angle - wrapped_start_angle);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn calculate_snap_angles(&self, initial_start_angle: f64, initial_sweep_angle: f64) -> Vec<f64> {
|
||||
let mut snap_points = Vec::new();
|
||||
let sign = initial_start_angle.signum() * -1.;
|
||||
let end_angle = initial_start_angle.abs().to_radians() * sign - initial_sweep_angle.to_radians();
|
||||
let wrapped_end_angle = wrap_to_tau(-end_angle);
|
||||
|
||||
if self.endpoint == EndpointType::End {
|
||||
for i in 0..8 {
|
||||
let snap_point = wrap_to_tau(i as f64 * FRAC_PI_4 + initial_start_angle);
|
||||
snap_points.push(snap_point.to_degrees());
|
||||
}
|
||||
}
|
||||
|
||||
if self.endpoint == EndpointType::Start {
|
||||
for i in 0..8 {
|
||||
let snap_point = wrap_to_tau(wrapped_end_angle + 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;
|
||||
}
|
||||
}
|
|
@ -346,6 +346,10 @@ pub fn get_star_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
|
|||
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> {
|
||||
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Text")
|
||||
}
|
||||
|
|
|
@ -0,0 +1,139 @@
|
|||
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);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
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.0, start.midpoint(end)),
|
||||
transform_in: TransformIn::Viewport,
|
||||
skip_rerender: false,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
|
@ -1,3 +1,4 @@
|
|||
pub mod arc_shape;
|
||||
pub mod ellipse_shape;
|
||||
pub mod line_shape;
|
||||
pub mod polygon_shape;
|
||||
|
|
|
@ -1,4 +1,5 @@
|
|||
use super::ShapeToolData;
|
||||
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT};
|
||||
use crate::messages::message::Message;
|
||||
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
|
||||
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::value::TaggedValue;
|
||||
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 std::collections::VecDeque;
|
||||
use std::f64::consts::{PI, TAU};
|
||||
|
@ -24,9 +25,10 @@ pub enum ShapeType {
|
|||
#[default]
|
||||
Polygon = 0,
|
||||
Star = 1,
|
||||
Rectangle = 2,
|
||||
Ellipse = 3,
|
||||
Line = 4,
|
||||
Arc = 2,
|
||||
Rectangle = 3,
|
||||
Ellipse = 4,
|
||||
Line = 5,
|
||||
}
|
||||
|
||||
impl ShapeType {
|
||||
|
@ -34,6 +36,7 @@ impl ShapeType {
|
|||
(match self {
|
||||
Self::Polygon => "Polygon",
|
||||
Self::Star => "Star",
|
||||
Self::Arc => "Arc",
|
||||
Self::Rectangle => "Rectangle",
|
||||
Self::Ellipse => "Ellipse",
|
||||
Self::Line => "Line",
|
||||
|
@ -234,7 +237,54 @@ pub fn extract_polygon_parameters(layer: Option<LayerNodeIdentifier>, document:
|
|||
Some((n, radius))
|
||||
}
|
||||
|
||||
/// Calculate the viewport position of as a star vertex given its index
|
||||
/// Extract the node input values of Arc
|
||||
pub fn extract_arc_parameters(layer: Option<LayerNodeIdentifier>, document: &DocumentMessageHandler) -> Option<(f64, f64, f64, ArcType)> {
|
||||
let Some(layer) = layer else {
|
||||
return None;
|
||||
};
|
||||
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 Some(layer) = layer else {
|
||||
return None;
|
||||
};
|
||||
let Some((radius, start_angle, sweep_angle, _)) = extract_arc_parameters(Some(layer), document) else {
|
||||
return None;
|
||||
};
|
||||
|
||||
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 sign = start_angle.signum() * -1.;
|
||||
let end_angle = start_angle.abs().to_radians() * sign - 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 {
|
||||
let angle = ((vertex_index as f64) * PI) / (n as f64);
|
||||
let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 };
|
||||
|
@ -290,7 +340,32 @@ pub fn polygon_outline(layer: Option<LayerNodeIdentifier>, document: &DocumentMe
|
|||
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
|
||||
/// Outlines the geometric shape made by 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 star-node without any upstream node modifications
|
||||
pub fn inside_star(viewport: DAffine2, n: u32, radius1: f64, radius2: f64, mouse_position: DVec2) -> bool {
|
||||
let mut paths = Vec::new();
|
||||
|
||||
|
@ -363,3 +438,28 @@ 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.));
|
||||
}
|
||||
}
|
||||
|
||||
/// Wraps an angle (in radians) into the range [0, 2π).
|
||||
pub fn wrap_to_tau(angle: f64) -> f64 {
|
||||
(angle % TAU + TAU) % TAU
|
||||
}
|
||||
|
||||
// Give the approximated angle to display in degrees(Note : The input is 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)
|
||||
}
|
||||
|
|
|
@ -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::NodeGraphLayer;
|
||||
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::polygon_shape::Polygon;
|
||||
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")
|
||||
.label("Star")
|
||||
.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()
|
||||
}
|
||||
|
||||
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 {
|
||||
NumberInput::new(Some(line_weight))
|
||||
.unit(" px")
|
||||
|
@ -135,6 +154,11 @@ impl LayoutHolder for ShapeTool {
|
|||
widgets.push(create_sides_widget(self.options.vertices));
|
||||
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 {
|
||||
|
@ -578,7 +602,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
};
|
||||
|
||||
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 => {
|
||||
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());
|
||||
|
@ -591,6 +615,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
let node = match tool_data.current_shape {
|
||||
ShapeType::Polygon => Polygon::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::Ellipse => Ellipse::create_node(),
|
||||
ShapeType::Line => Line::create_node(document, tool_data.data.drag_start),
|
||||
|
@ -602,7 +627,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
responses.add(Message::StartBuffer);
|
||||
|
||||
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 {
|
||||
layer,
|
||||
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::Polygon => Polygon::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
|
||||
|
@ -829,7 +855,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
let hint_data = match self {
|
||||
ShapeToolFsmState::Ready(shape) => {
|
||||
let hint_groups = match shape {
|
||||
ShapeType::Polygon | ShapeType::Star => vec![
|
||||
ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => vec![
|
||||
HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
|
||||
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
|
||||
|
@ -859,7 +885,7 @@ impl Fsm for ShapeToolFsmState {
|
|||
ShapeToolFsmState::Drawing(shape) => {
|
||||
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 {
|
||||
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::Ellipse => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Circular"), HintInfo::keys([Key::Alt], "From Center")]),
|
||||
ShapeType::Line => HintGroup(vec![
|
||||
|
|
|
@ -64,9 +64,9 @@ pub enum GridType {
|
|||
#[widget(Radio)]
|
||||
pub enum ArcType {
|
||||
#[default]
|
||||
Open,
|
||||
Closed,
|
||||
PieSlice,
|
||||
Open = 0,
|
||||
Closed = 1,
|
||||
PieSlice = 2,
|
||||
}
|
||||
|
||||
#[repr(C)]
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue