Fix Shape tool arc gizmo snap visualization and pointer cursor icon when hovering or dragging (#2957)

* fixed cursor,arc bugs

* Update cursor icon

* send pointer-move only when required

* make it compile

* Code review

* remove repeated modifier key

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0
2025-07-31 00:04:08 -07:00
committed by GitHub
co-authored by Keavon Chambers
parent 3cc9dd79fb
commit 7e0a274bd1
13 changed files with 147 additions and 46 deletions
@@ -1,3 +1,4 @@
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;
@@ -123,6 +124,15 @@ impl ShapeGizmoHandlers {
Self::None => {}
}
}
pub fn gizmo_cursor_icon(&self) -> Option<MouseCursorIcon> {
match self {
Self::Star(h) => h.mouse_cursor_icon(),
Self::Polygon(h) => h.mouse_cursor_icon(),
Self::Arc(h) => h.mouse_cursor_icon(),
Self::None => None,
}
}
}
/// Central manager that coordinates shape gizmo handlers for interactive editing on the canvas.
@@ -256,4 +266,12 @@ impl GizmoManager {
}
}
}
/// Returns the cursor icon to display when hovering or dragging a gizmo.
///
/// If a gizmo is active (hovered or being manipulated), it returns the cursor icon associated with that gizmo;
/// otherwise, returns `None` to indicate the default crosshair cursor should be used.
pub fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
self.active_shape_handler.as_ref().and_then(|h| h.gizmo_cursor_icon())
}
}
@@ -1,10 +1,9 @@
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::prelude::DocumentMessageHandler;
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::*;
@@ -57,7 +56,7 @@ impl SweepAngleGizmo {
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>) {
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2) {
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 {
@@ -89,8 +88,6 @@ impl SweepAngleGizmo {
self.snap_angles = Self::calculate_snap_angles();
self.update_state(SweepAngleGizmoState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default });
}
}
}
@@ -127,6 +124,11 @@ impl SweepAngleGizmo {
// 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 };
// Draw the dashed line from center to drag start position
overlay_context.dashed_line(self.position_before_rotation, viewport.transform_point2(DVec2::ZERO), None, None, Some(5.), Some(5.), Some(0.5));
// Draw the angle, text and the bold line
self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context);
}
SweepAngleGizmoState::Snapped => {
@@ -143,6 +145,9 @@ impl SweepAngleGizmo {
// 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.));
// Draw the line from drag start to arc center
overlay_context.dashed_line(self.position_before_rotation, center, None, None, Some(5.), Some(5.), Some(0.5));
}
}
}
@@ -155,7 +160,6 @@ impl SweepAngleGizmo {
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();
@@ -170,7 +174,7 @@ impl SweepAngleGizmo {
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);
overlay_context.arc_sweep_angle(offset_angle, angle, final_point, bold_radius, center, &text, transform);
}
pub fn update_arc(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
@@ -209,12 +213,11 @@ impl SweepAngleGizmo {
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.initial_start_angle = current_start_angle;
self.update_state(SweepAngleGizmoState::Snapped);
self.apply_arc_update(node_id, self.initial_start_angle, self.initial_sweep_angle - wrapped, input, responses);
}
@@ -288,12 +291,13 @@ impl SweepAngleGizmo {
}
// Clamp sweep angle above 360°, switch to start
() if new_sweep_angle > 360. => {
let delta = angle_delta - (360. - current_sweep_angle);
let delta = angle_delta - (360. - new_sweep_angle);
let sign = -delta.signum();
self.total_angle_delta = angle_delta;
self.total_angle_delta = angle_delta - (360. - new_sweep_angle);
self.initial_sweep_angle = 360.;
self.endpoint = EndpointType::Start;
self.update_state(SweepAngleGizmoState::Snapped);
self.apply_arc_update(node_id, self.initial_start_angle + angle_delta, self.initial_sweep_angle + angle_delta.abs() * sign, input, responses);
}
@@ -339,7 +343,7 @@ impl SweepAngleGizmo {
pub fn calculate_snap_angles() -> Vec<f64> {
let mut snap_points = Vec::new();
for i in 0..8 {
for i in 0..=8 {
let snap_point = i as f64 * FRAC_PI_4;
snap_points.push(snap_point.to_degrees());
}
@@ -26,8 +26,8 @@ impl ArcGizmoHandler {
}
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 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);
}
fn is_any_gizmo_hovered(&self) -> bool {
@@ -74,6 +74,14 @@ impl ShapeGizmoHandler for ArcGizmoHandler {
arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context);
}
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
if self.sweep_angle_gizmo.hovered() || self.sweep_angle_gizmo.is_dragging_or_snapped() {
return Some(MouseCursorIcon::Default);
}
None
}
fn cleanup(&mut self) {
self.sweep_angle_gizmo.cleanup();
}
@@ -28,7 +28,7 @@ impl Ellipse {
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
let [center, lock_ratio, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_ellipse_id(layer, &document.network_interface) else {
@@ -53,7 +53,7 @@ impl Line {
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, _, lock_angle, snap_angle] = modifier;
let [center, snap_angle, lock_angle] = modifier;
shape_tool_data.line_data.drag_current = ipp.mouse.position;
@@ -89,6 +89,18 @@ impl ShapeGizmoHandler for PolygonGizmoHandler {
}
}
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() {
return Some(MouseCursorIcon::EWResize);
}
if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() {
return Some(MouseCursorIcon::Default);
}
None
}
fn cleanup(&mut self) {
self.number_of_points_dial.cleanup();
self.point_radius_handle.cleanup();
@@ -112,7 +124,7 @@ impl Polygon {
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
let [center, lock_ratio, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
// TODO: We need to determine how to allow the polygon node to make irregular shapes
@@ -28,7 +28,7 @@ impl Rectangle {
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
let [center, lock_ratio, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
let Some(node_id) = graph_modification_utils::get_rectangle_id(layer, &document.network_interface) else {
@@ -1,5 +1,6 @@
use super::ShapeToolData;
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT};
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;
@@ -74,7 +75,7 @@ impl ShapeType {
}
}
pub type ShapeToolModifierKey = [Key; 4];
pub type ShapeToolModifierKey = [Key; 3];
/// The `ShapeGizmoHandler` trait defines the interactive behavior and overlay logic for shape-specific tools in the editor.
/// A gizmo is a visual handle or control point used to manipulate a shape's properties (e.g., number of sides, radius, angle).
@@ -127,6 +128,8 @@ pub trait ShapeGizmoHandler {
///
/// For example, dragging states or hover flags should be cleared to avoid visual glitches when switching tools or shapes.
fn cleanup(&mut self);
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon>;
}
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
@@ -90,6 +90,18 @@ impl ShapeGizmoHandler for StarGizmoHandler {
self.number_of_points_dial.cleanup();
self.point_radius_handle.cleanup();
}
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
if self.number_of_points_dial.is_dragging() || self.number_of_points_dial.is_hovering() {
return Some(MouseCursorIcon::EWResize);
}
if self.point_radius_handle.is_dragging_or_snapped() || self.point_radius_handle.hovered() {
return Some(MouseCursorIcon::Default);
}
None
}
}
#[derive(Default)]
@@ -114,7 +126,7 @@ impl Star {
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let [center, lock_ratio, _, _] = modifier;
let [center, lock_ratio, _] = modifier;
if let Some([start, end]) = shape_tool_data.data.calculate_points(document, ipp, center, lock_ratio) {
// TODO: We need to determine how to allow the polygon node to make irregular shapes