This commit is contained in:
0SlowPoke0 2025-07-07 10:21:48 +02:00 committed by GitHub
commit 81eecf3bec
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 767 additions and 24 deletions

View file

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

View file

@ -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();

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::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
}

View file

@ -1,2 +1,3 @@
pub mod number_of_points_dial;
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 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

View file

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

View file

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

View file

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

View file

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

View file

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

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::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![

View file

@ -64,9 +64,9 @@ pub enum GridType {
#[widget(Radio)]
pub enum ArcType {
#[default]
Open,
Closed,
PieSlice,
Open = 0,
Closed = 1,
PieSlice = 2,
}
#[repr(C)]