Add 'Circle' to the Shape tool and its associated gizmos (#2914)

* merged with circle and impl inner radius gizmo for arc

* impl radius-gizmo for arc

* fix only one gizmo shown at a time

* Code review

* make hints update when changing shape,add default behaviour when dragging to make circle earlier fixed to from center

* fixed arc-radius hover threshold and show arc-endpoint when hover over arc-radius gizmo

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0 2025-08-02 10:53:21 +05:30 committed by GitHub
parent 97bd0ebac4
commit 523132da17
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 741 additions and 166 deletions

View File

@ -294,6 +294,147 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
#[allow(clippy::too_many_arguments)]
pub fn dashed_ellipse(
&mut self,
center: DVec2,
radius_x: f64,
radius_y: f64,
rotation: Option<f64>,
start_angle: Option<f64>,
end_angle: Option<f64>,
counterclockwise: Option<bool>,
color_fill: Option<&str>,
color_stroke: Option<&str>,
dash_width: Option<f64>,
dash_gap_width: Option<f64>,
dash_offset: Option<f64>,
) {
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let center = center.round();
self.start_dpi_aware_transform();
if let Some(dash_width) = dash_width {
let dash_gap_width = dash_gap_width.unwrap_or(1.);
let array = js_sys::Array::new();
array.push(&JsValue::from(dash_width));
array.push(&JsValue::from(dash_gap_width));
if let Some(dash_offset) = dash_offset {
if dash_offset != 0. {
self.render_context.set_line_dash_offset(dash_offset);
}
}
self.render_context
.set_line_dash(&JsValue::from(array))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
self.render_context.begin_path();
self.render_context
.ellipse_with_anticlockwise(
center.x,
center.y,
radius_x,
radius_y,
rotation.unwrap_or_default(),
start_angle.unwrap_or_default(),
end_angle.unwrap_or(TAU),
counterclockwise.unwrap_or_default(),
)
.expect("Failed to draw ellipse");
self.render_context.set_stroke_style_str(color_stroke);
if let Some(fill_color) = color_fill {
self.render_context.set_fill_style_str(fill_color);
self.render_context.fill();
}
self.render_context.stroke();
// Reset the dash pattern back to solid
if dash_width.is_some() {
self.render_context
.set_line_dash(&JsValue::from(js_sys::Array::new()))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
if dash_offset.is_some() && dash_offset != Some(0.) {
self.render_context.set_line_dash_offset(0.);
}
self.end_dpi_aware_transform();
}
pub fn dashed_circle(
&mut self,
position: DVec2,
radius: f64,
color_fill: Option<&str>,
color_stroke: Option<&str>,
dash_width: Option<f64>,
dash_gap_width: Option<f64>,
dash_offset: Option<f64>,
transform: Option<DAffine2>,
) {
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round();
self.start_dpi_aware_transform();
if let Some(transform) = transform {
let [a, b, c, d, e, f] = transform.to_cols_array();
self.render_context.transform(a, b, c, d, e, f).expect("Failed to transform circle");
}
if let Some(dash_width) = dash_width {
let dash_gap_width = dash_gap_width.unwrap_or(1.);
let array = js_sys::Array::new();
array.push(&JsValue::from(dash_width));
array.push(&JsValue::from(dash_gap_width));
if let Some(dash_offset) = dash_offset {
if dash_offset != 0. {
self.render_context.set_line_dash_offset(dash_offset);
}
}
self.render_context
.set_line_dash(&JsValue::from(array))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
self.render_context.begin_path();
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
self.render_context.set_stroke_style_str(color_stroke);
if let Some(fill_color) = color_fill {
self.render_context.set_fill_style_str(fill_color);
self.render_context.fill();
}
self.render_context.stroke();
// Reset the dash pattern back to solid
if dash_width.is_some() {
self.render_context
.set_line_dash(&JsValue::from(js_sys::Array::new()))
.map_err(|error| log::warn!("Error drawing dashed line: {:?}", error))
.ok();
}
if dash_offset.is_some() && dash_offset != Some(0.) {
self.render_context.set_line_dash_offset(0.);
}
self.end_dpi_aware_transform();
}
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
self.dashed_circle(position, radius, color_fill, color_stroke, None, None, None, None);
}
pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) { pub fn manipulator_handle(&mut self, position: DVec2, selected: bool, color: Option<&str>) {
self.start_dpi_aware_transform(); self.start_dpi_aware_transform();
@ -374,23 +515,6 @@ impl OverlayContext {
self.end_dpi_aware_transform(); self.end_dpi_aware_transform();
} }
pub fn circle(&mut self, position: DVec2, radius: f64, color_fill: Option<&str>, color_stroke: Option<&str>) {
let color_fill = color_fill.unwrap_or(COLOR_OVERLAY_WHITE);
let color_stroke = color_stroke.unwrap_or(COLOR_OVERLAY_BLUE);
let position = position.round();
self.start_dpi_aware_transform();
self.render_context.begin_path();
self.render_context.arc(position.x, position.y, radius, 0., TAU).expect("Failed to draw the circle");
self.render_context.set_fill_style_str(color_fill);
self.render_context.set_stroke_style_str(color_stroke);
self.render_context.fill();
self.render_context.stroke();
self.end_dpi_aware_transform();
}
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) { pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize; let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
let step = (end_at - start_from) / segments as f64; let step = (end_at - start_from) / segments as f64;
@ -591,7 +715,7 @@ impl OverlayContext {
} }
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) { pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED)); self.manipulator_handle(end_point_position, true, None);
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians()); self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); self.text(&text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
} }

View File

@ -345,6 +345,23 @@ impl OverlayContext {
self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle); self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color_stroke), None, &circle);
} }
pub fn dashed_ellipse(
&mut self,
_center: DVec2,
_radius_x: f64,
_radius_y: f64,
_rotation: Option<f64>,
_start_angle: Option<f64>,
_end_angle: Option<f64>,
_counterclockwise: Option<bool>,
_color_fill: Option<&str>,
_color_stroke: Option<&str>,
_dash_width: Option<f64>,
_dash_gap_width: Option<f64>,
_dash_offset: Option<f64>,
) {
}
pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) { pub fn draw_arc(&mut self, center: DVec2, radius: f64, start_from: f64, end_at: f64) {
let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize; let segments = ((end_at - start_from).abs() / (std::f64::consts::PI / 4.)).ceil() as usize;
let step = (end_at - start_from) / segments as f64; let step = (end_at - start_from) / segments as f64;
@ -541,7 +558,7 @@ impl OverlayContext {
#[allow(clippy::too_many_arguments)] #[allow(clippy::too_many_arguments)]
pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) { pub fn arc_sweep_angle(&mut self, offset_angle: f64, angle: f64, end_point_position: DVec2, bold_radius: f64, pivot: DVec2, text: &str, transform: DAffine2) {
self.manipulator_handle(end_point_position, true, Some(COLOR_OVERLAY_RED)); self.manipulator_handle(end_point_position, true, None);
self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians()); self.draw_arc_gizmo_angle(pivot, bold_radius, ARC_SWEEP_GIZMO_RADIUS, offset_angle, angle.to_radians());
self.text(text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]); self.text(text, COLOR_OVERLAY_BLUE, None, transform, 16., [Pivot::Middle, Pivot::Middle]);
} }

View File

@ -6,6 +6,7 @@ use crate::messages::prelude::{DocumentMessageHandler, InputPreprocessorMessageH
use crate::messages::tool::common_functionality::graph_modification_utils; use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::shape_editor::ShapeState; use crate::messages::tool::common_functionality::shape_editor::ShapeState;
use crate::messages::tool::common_functionality::shapes::arc_shape::ArcGizmoHandler; use crate::messages::tool::common_functionality::shapes::arc_shape::ArcGizmoHandler;
use crate::messages::tool::common_functionality::shapes::circle_shape::CircleGizmoHandler;
use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler; use crate::messages::tool::common_functionality::shapes::polygon_shape::PolygonGizmoHandler;
use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler; use crate::messages::tool::common_functionality::shapes::shape_utility::ShapeGizmoHandler;
use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler; use crate::messages::tool::common_functionality::shapes::star_shape::StarGizmoHandler;
@ -26,6 +27,7 @@ pub enum ShapeGizmoHandlers {
Star(StarGizmoHandler), Star(StarGizmoHandler),
Polygon(PolygonGizmoHandler), Polygon(PolygonGizmoHandler),
Arc(ArcGizmoHandler), Arc(ArcGizmoHandler),
Circle(CircleGizmoHandler),
} }
impl ShapeGizmoHandlers { impl ShapeGizmoHandlers {
@ -36,6 +38,7 @@ impl ShapeGizmoHandlers {
Self::Star(_) => "star", Self::Star(_) => "star",
Self::Polygon(_) => "polygon", Self::Polygon(_) => "polygon",
Self::Arc(_) => "arc", Self::Arc(_) => "arc",
Self::Circle(_) => "circle",
Self::None => "none", Self::None => "none",
} }
} }
@ -46,6 +49,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.handle_state(layer, mouse_position, document, responses), Self::Star(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses), Self::Polygon(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses), Self::Arc(h) => h.handle_state(layer, mouse_position, document, responses),
Self::Circle(h) => h.handle_state(layer, mouse_position, document, responses),
Self::None => {} Self::None => {}
} }
} }
@ -56,6 +60,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.is_any_gizmo_hovered(), Self::Star(h) => h.is_any_gizmo_hovered(),
Self::Polygon(h) => h.is_any_gizmo_hovered(), Self::Polygon(h) => h.is_any_gizmo_hovered(),
Self::Arc(h) => h.is_any_gizmo_hovered(), Self::Arc(h) => h.is_any_gizmo_hovered(),
Self::Circle(h) => h.is_any_gizmo_hovered(),
Self::None => false, Self::None => false,
} }
} }
@ -66,6 +71,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.handle_click(), Self::Star(h) => h.handle_click(),
Self::Polygon(h) => h.handle_click(), Self::Polygon(h) => h.handle_click(),
Self::Arc(h) => h.handle_click(), Self::Arc(h) => h.handle_click(),
Self::Circle(h) => h.handle_click(),
Self::None => {} Self::None => {}
} }
} }
@ -76,6 +82,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.handle_update(drag_start, document, input, responses), Self::Star(h) => h.handle_update(drag_start, document, input, responses),
Self::Polygon(h) => h.handle_update(drag_start, document, input, responses), Self::Polygon(h) => h.handle_update(drag_start, document, input, responses),
Self::Arc(h) => h.handle_update(drag_start, document, input, responses), Self::Arc(h) => h.handle_update(drag_start, document, input, responses),
Self::Circle(h) => h.handle_update(drag_start, document, input, responses),
Self::None => {} Self::None => {}
} }
} }
@ -86,6 +93,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.cleanup(), Self::Star(h) => h.cleanup(),
Self::Polygon(h) => h.cleanup(), Self::Polygon(h) => h.cleanup(),
Self::Arc(h) => h.cleanup(), Self::Arc(h) => h.cleanup(),
Self::Circle(h) => h.cleanup(),
Self::None => {} Self::None => {}
} }
} }
@ -104,6 +112,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Star(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context), Self::Arc(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.overlays(document, layer, input, shape_editor, mouse_position, overlay_context),
Self::None => {} Self::None => {}
} }
} }
@ -121,6 +130,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Star(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Polygon(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context), Self::Arc(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::Circle(h) => h.dragging_overlays(document, input, shape_editor, mouse_position, overlay_context),
Self::None => {} Self::None => {}
} }
} }
@ -130,6 +140,7 @@ impl ShapeGizmoHandlers {
Self::Star(h) => h.mouse_cursor_icon(), Self::Star(h) => h.mouse_cursor_icon(),
Self::Polygon(h) => h.mouse_cursor_icon(), Self::Polygon(h) => h.mouse_cursor_icon(),
Self::Arc(h) => h.mouse_cursor_icon(), Self::Arc(h) => h.mouse_cursor_icon(),
Self::Circle(h) => h.mouse_cursor_icon(),
Self::None => None, Self::None => None,
} }
} }
@ -169,6 +180,10 @@ impl GizmoManager {
if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() { if graph_modification_utils::get_arc_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new())); return Some(ShapeGizmoHandlers::Arc(ArcGizmoHandler::new()));
} }
// Circle
if graph_modification_utils::get_circle_id(layer, &document.network_interface).is_some() {
return Some(ShapeGizmoHandlers::Circle(CircleGizmoHandler::default()));
}
None None
} }

View File

@ -0,0 +1,172 @@
use crate::consts::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, InputPreprocessorMessageHandler, NodeGraphMessage};
use crate::messages::prelude::{FrontendMessage, Responses};
use crate::messages::tool::common_functionality::graph_modification_utils::{self, get_arc_id, get_stroke_width};
use crate::messages::tool::common_functionality::shapes::shape_utility::{extract_arc_parameters, extract_circle_radius};
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
use std::collections::VecDeque;
use std::f64::consts::FRAC_PI_2;
#[derive(Clone, Debug, Default, PartialEq)]
pub enum RadiusHandleState {
#[default]
Inactive,
Hover,
Dragging,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct RadiusHandle {
pub layer: Option<LayerNodeIdentifier>,
initial_radius: f64,
handle_state: RadiusHandleState,
angle: f64,
previous_mouse_position: DVec2,
}
impl RadiusHandle {
pub fn cleanup(&mut self) {
self.handle_state = RadiusHandleState::Inactive;
self.layer = None;
}
pub fn hovered(&self) -> bool {
self.handle_state == RadiusHandleState::Hover
}
pub fn is_dragging(&self) -> bool {
self.handle_state == RadiusHandleState::Dragging
}
pub fn update_state(&mut self, state: RadiusHandleState) {
self.handle_state = state;
}
pub fn check_if_inside_dash_lines(angle: f64, mouse_position: DVec2, viewport: DAffine2, radius: f64, document: &DocumentMessageHandler, layer: LayerNodeIdentifier) -> bool {
let center = viewport.transform_point2(DVec2::ZERO);
if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) {
let circle_point = calculate_circle_point_position(angle, radius.abs());
let direction = circle_point.normalize();
let mouse_distance = mouse_position.distance(center);
let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.);
let inner_point = viewport.transform_point2(circle_point - direction * spacing).distance(center);
let outer_point = viewport.transform_point2(circle_point + direction * spacing).distance(center);
mouse_distance >= inner_point && mouse_distance <= outer_point
} else {
let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs()));
mouse_position.distance(center) <= point_position.distance(center)
}
}
fn calculate_extra_spacing(viewport: DAffine2, radius: f64, viewport_center: DVec2, stroke_width: f64, threshold: f64) -> f64 {
let start_point = viewport.transform_point2(calculate_circle_point_position(0., radius)).distance(viewport_center);
let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(viewport_center);
let min_radius = start_point.min(end_point);
let extra_spacing = if min_radius < threshold { 10. * (min_radius / threshold) } else { 10. };
stroke_width + extra_spacing
}
pub fn handle_actions(&mut self, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, mouse_position: DVec2, responses: &mut VecDeque<Message>) {
match &self.handle_state {
RadiusHandleState::Inactive => {
let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let angle = viewport.inverse().transform_point2(mouse_position).angle_to(DVec2::X);
let point_position = viewport.transform_point2(calculate_circle_point_position(angle, radius.abs()));
let center = viewport.transform_point2(DVec2::ZERO);
if point_position.distance(center) < GIZMO_HIDE_THRESHOLD {
return;
}
if Self::check_if_inside_dash_lines(angle, mouse_position, viewport, radius.abs(), document, layer) {
self.layer = Some(layer);
self.initial_radius = radius;
self.previous_mouse_position = mouse_position;
self.angle = angle;
self.update_state(RadiusHandleState::Hover);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::EWResize });
}
}
RadiusHandleState::Dragging | RadiusHandleState::Hover => {}
}
}
pub fn overlays(&self, document: &DocumentMessageHandler, overlay_context: &mut OverlayContext) {
match &self.handle_state {
RadiusHandleState::Inactive => {}
RadiusHandleState::Dragging | RadiusHandleState::Hover => {
let Some(layer) = self.layer else { return };
let Some(radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
return;
};
let viewport = document.metadata().transform_to_viewport(layer);
let center = viewport.transform_point2(DVec2::ZERO);
let start_point = viewport.transform_point2(calculate_circle_point_position(0., radius)).distance(center);
let end_point = viewport.transform_point2(calculate_circle_point_position(FRAC_PI_2, radius)).distance(center);
if let Some(stroke_width) = get_stroke_width(layer, &document.network_interface) {
let spacing = Self::calculate_extra_spacing(viewport, radius, center, stroke_width, 15.);
let smaller_radius_x = (start_point - spacing).abs();
let smaller_radius_y = (end_point - spacing).abs();
let larger_radius_x = (start_point + spacing).abs();
let larger_radius_y = (end_point + spacing).abs();
overlay_context.dashed_ellipse(center, smaller_radius_x, smaller_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
overlay_context.dashed_ellipse(center, larger_radius_x, larger_radius_y, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
return;
}
overlay_context.dashed_ellipse(center, start_point, end_point, None, None, None, None, None, None, Some(4.), Some(4.), Some(0.5));
}
}
}
pub fn update_inner_radius(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>, drag_start: DVec2) {
let Some(layer) = self.layer else { return };
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface).or(get_arc_id(layer, &document.network_interface)) else {
return;
};
let Some(current_radius) = extract_circle_radius(layer, document).or(extract_arc_parameters(Some(layer), document).map(|(r, _, _, _)| r)) else {
return;
};
let viewport_transform = document.network_interface.document_metadata().transform_to_viewport(layer);
let center = viewport_transform.transform_point2(DVec2::ZERO);
let delta_vector = viewport_transform.inverse().transform_point2(input.mouse.position) - viewport_transform.inverse().transform_point2(self.previous_mouse_position);
let radius = document.metadata().document_to_viewport.transform_point2(drag_start) - center;
let sign = radius.dot(delta_vector).signum();
let net_delta = delta_vector.length() * sign * self.initial_radius.signum();
self.previous_mouse_position = input.mouse.position;
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::F64(current_radius + net_delta), false),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
fn calculate_circle_point_position(theta: f64, radius: f64) -> DVec2 {
DVec2::new(radius * theta.cos(), -radius * theta.sin())
}

View File

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

View File

@ -1,4 +1,4 @@
use crate::consts::{ARC_SNAP_THRESHOLD, COLOR_OVERLAY_RED, GIZMO_HIDE_THRESHOLD}; use crate::consts::{ARC_SNAP_THRESHOLD, GIZMO_HIDE_THRESHOLD};
use crate::messages::message::Message; use crate::messages::message::Message;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@ -104,17 +104,17 @@ impl SweepAngleGizmo {
match self.handle_state { match self.handle_state {
SweepAngleGizmoState::Inactive => { SweepAngleGizmoState::Inactive => {
// Draw both endpoint handles if an arc is selected
let Some((point1, point2)) = arc_end_points(selected_arc_layer, document) else { return }; 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(point1, false, None);
overlay_context.manipulator_handle(point2, false, Some(COLOR_OVERLAY_RED)); overlay_context.manipulator_handle(point2, false, None);
} }
SweepAngleGizmoState::Hover => { SweepAngleGizmoState::Hover => {
// Highlight the currently hovered endpoint only // Highlight the currently hovered endpoint only
let Some((point1, point2)) = arc_end_points(self.layer, document) else { return }; let Some((point1, point2)) = arc_end_points(self.layer, document) else { return };
let point = if self.endpoint == EndpointType::Start { point1 } else { point2 }; let (point, other_point) = if self.endpoint == EndpointType::Start { (point1, point2) } else { (point2, point1) };
overlay_context.manipulator_handle(point, true, Some(COLOR_OVERLAY_RED)); overlay_context.manipulator_handle(point, true, None);
overlay_context.manipulator_handle(other_point, false, None);
} }
SweepAngleGizmoState::Dragging => { SweepAngleGizmoState::Dragging => {
// Show snapping guides and angle arc while dragging // Show snapping guides and angle arc while dragging
@ -123,11 +123,17 @@ impl SweepAngleGizmo {
let viewport = document.metadata().transform_to_viewport(layer); let viewport = document.metadata().transform_to_viewport(layer);
// Depending on which endpoint is being dragged, draw guides relative to the static point // 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 }; let (point, other_point) = if self.endpoint == EndpointType::End {
(current_end, current_start)
} else {
(current_start, current_end)
};
// Draw the dashed line from center to drag start position // 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)); overlay_context.dashed_line(self.position_before_rotation, viewport.transform_point2(DVec2::ZERO), None, None, Some(5.), Some(5.), Some(0.5));
overlay_context.manipulator_handle(other_point, false, None);
// Draw the angle, text and the bold line // Draw the angle, text and the bold line
self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context); self.dragging_snapping_overlays(self.position_before_rotation, point, tilt_offset, viewport, overlay_context);
} }
@ -143,8 +149,8 @@ impl SweepAngleGizmo {
self.dragging_snapping_overlays(a, b, tilt_offset, viewport, overlay_context); self.dragging_snapping_overlays(a, b, tilt_offset, viewport, overlay_context);
// Draw lines from endpoints to the arc center // Draw lines from endpoints to the arc center
overlay_context.line(start, center, Some(COLOR_OVERLAY_RED), Some(2.)); overlay_context.line(start, center, None, Some(2.));
overlay_context.line(end, center, Some(COLOR_OVERLAY_RED), Some(2.)); overlay_context.line(end, center, None, Some(2.));
// Draw the line from drag start to arc center // 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)); overlay_context.dashed_line(self.position_before_rotation, center, None, None, Some(5.), Some(5.), Some(0.5));

View File

@ -333,6 +333,10 @@ pub fn get_fill_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkIn
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill") NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Fill")
} }
pub fn get_circle_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Circle")
}
pub fn get_ellipse_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> { pub fn get_ellipse_id(layer: LayerNodeIdentifier, network_interface: &NodeNetworkInterface) -> Option<NodeId> {
NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Ellipse") NodeGraphLayer::new(layer, network_interface).upstream_node_id_from_name("Ellipse")
} }

View File

@ -48,56 +48,11 @@ impl Resize {
/// Compute the drag start and end based on the current mouse position. Ignores the state of the layer. /// Compute the drag start and end based on the current mouse position. Ignores the state of the layer.
/// If you want to only draw whilst a layer exists, use [`Resize::calculate_points`]. /// If you want to only draw whilst a layer exists, use [`Resize::calculate_points`].
pub fn calculate_points_ignore_layer(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, in_document: bool) -> [DVec2; 2] { pub fn calculate_points_ignore_layer(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, in_document: bool) -> [DVec2; 2] {
let start = self.viewport_drag_start(document);
let mouse = input.mouse.position;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
let mut points_viewport = [start, mouse];
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
let ratio = input.keyboard.get(lock_ratio as usize); let ratio = input.keyboard.get(lock_ratio as usize);
let center = input.keyboard.get(center as usize); let center = input.keyboard.get(center as usize);
let snap_data = SnapData::ignore(document, input, &ignore);
let config = SnapTypeConfiguration::default();
if ratio {
let viewport_size = points_viewport[1] - points_viewport[0];
let raw_size = if in_document { document_to_viewport.inverse() } else { DAffine2::IDENTITY }.transform_vector2(viewport_size);
let adjusted_size = raw_size.abs().max(raw_size.abs().yx()) * raw_size.signum();
let size = if in_document { document_to_viewport.transform_vector2(adjusted_size) } else { adjusted_size };
points_viewport[1] = points_viewport[0] + size;
let end_document = document_to_viewport.inverse().transform_point2(points_viewport[1]); // Use shared snapping logic with optional center and ratio constraints, considering if coordinates are in document space.
let constraint = SnapConstraint::Line { self.compute_snapped_resize_points(document, input, center, ratio, in_document)
origin: self.drag_start,
direction: end_document - self.drag_start,
};
if center {
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config);
let far = SnapCandidatePoint::handle(2. * self.drag_start - end_document);
let snapped_far = self.snap_manager.constrained_snap(&snap_data, &far, constraint, config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.constrained_snap(&snap_data, &SnapCandidatePoint::handle(end_document), constraint, config);
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
} else if center {
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
let opposite = 2. * self.drag_start - document_mouse;
let snapped_far = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(opposite), config);
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(self.drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.free_snap(&snap_data, &SnapCandidatePoint::handle(document_mouse), config);
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
points_viewport
} }
pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> { pub fn calculate_transform(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key, lock_ratio: Key, skip_rerender: bool) -> Option<Message> {
@ -113,6 +68,81 @@ impl Resize {
) )
} }
pub fn calculate_circle_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: Key) -> [DVec2; 2] {
let center = input.keyboard.get(center as usize);
// Use shared snapping logic with enforced aspect ratio and optional center snapping.
self.compute_snapped_resize_points(document, input, center, true, false)
}
/// Calculates two points in viewport space from a drag, applying snapping, optional center mode, and aspect ratio locking.
fn compute_snapped_resize_points(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, center: bool, lock_ratio: bool, in_document: bool) -> [DVec2; 2] {
let start = self.viewport_drag_start(document);
let mouse = input.mouse.position;
let document_to_viewport = document.navigation_handler.calculate_offset_transform(input.viewport_bounds.center(), &document.document_ptz);
let drag_start = self.drag_start;
let mut points_viewport = [start, mouse];
let ignore = if let Some(layer) = self.layer { vec![layer] } else { vec![] };
let snap_data = &SnapData::ignore(document, input, &ignore);
if lock_ratio {
let viewport_size = points_viewport[1] - points_viewport[0];
let raw_size = if in_document {
document_to_viewport.inverse().transform_vector2(viewport_size)
} else {
viewport_size
};
let adjusted_size = raw_size.abs().max(raw_size.abs().yx()) * raw_size.signum();
let size = if in_document { document_to_viewport.transform_vector2(adjusted_size) } else { adjusted_size };
points_viewport[1] = points_viewport[0] + size;
let end_document = document_to_viewport.inverse().transform_point2(points_viewport[1]);
let constraint = SnapConstraint::Line {
origin: drag_start,
direction: end_document - drag_start,
};
if center {
let snapped = self
.snap_manager
.constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default());
let far = SnapCandidatePoint::handle(2. * drag_start - end_document);
let snapped_far = self.snap_manager.constrained_snap(snap_data, &far, constraint, SnapTypeConfiguration::default());
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self
.snap_manager
.constrained_snap(snap_data, &SnapCandidatePoint::handle(end_document), constraint, SnapTypeConfiguration::default());
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
} else {
let document_mouse = document_to_viewport.inverse().transform_point2(mouse);
if center {
let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default());
let opposite = 2. * drag_start - document_mouse;
let snapped_far = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(opposite), SnapTypeConfiguration::default());
let best = if snapped_far.other_snap_better(&snapped) { snapped } else { snapped_far };
points_viewport[0] = document_to_viewport.transform_point2(best.snapped_point_document);
points_viewport[1] = document_to_viewport.transform_point2(drag_start * 2. - best.snapped_point_document);
self.snap_manager.update_indicator(best);
} else {
let snapped = self.snap_manager.free_snap(snap_data, &SnapCandidatePoint::handle(document_mouse), SnapTypeConfiguration::default());
points_viewport[1] = document_to_viewport.transform_point2(snapped.snapped_point_document);
self.snap_manager.update_indicator(snapped);
}
}
points_viewport
}
pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) { pub fn cleanup(&mut self, responses: &mut VecDeque<Message>) {
self.snap_manager.cleanup(responses); self.snap_manager.cleanup(responses);
self.layer = None; self.layer = None;

View File

@ -4,6 +4,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Transf
use crate::messages::portfolio::document::node_graph::document_node_definitions::resolve_document_node_type; 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::document_metadata::LayerNodeIdentifier;
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate}; use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState};
use crate::messages::tool::common_functionality::gizmos::shape_gizmos::sweep_angle_gizmo::{SweepAngleGizmo, SweepAngleGizmoState}; 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::graph_modification_utils;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, arc_outline}; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeGizmoHandler, arc_outline};
@ -17,6 +18,7 @@ use std::collections::VecDeque;
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct ArcGizmoHandler { pub struct ArcGizmoHandler {
sweep_angle_gizmo: SweepAngleGizmo, sweep_angle_gizmo: SweepAngleGizmo,
arc_radius_handle: RadiusHandle,
} }
impl ArcGizmoHandler { impl ArcGizmoHandler {
@ -26,24 +28,40 @@ impl ArcGizmoHandler {
} }
impl ShapeGizmoHandler for ArcGizmoHandler { impl ShapeGizmoHandler for ArcGizmoHandler {
fn handle_state(&mut self, selected_shape_layers: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, _responses: &mut VecDeque<Message>) { fn handle_state(&mut self, selected_shape_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.sweep_angle_gizmo.handle_actions(selected_shape_layers, document, mouse_position); self.sweep_angle_gizmo.handle_actions(selected_shape_layer, document, mouse_position);
self.arc_radius_handle.handle_actions(selected_shape_layer, document, mouse_position, responses);
} }
fn is_any_gizmo_hovered(&self) -> bool { fn is_any_gizmo_hovered(&self) -> bool {
self.sweep_angle_gizmo.hovered() self.sweep_angle_gizmo.hovered() || self.arc_radius_handle.hovered()
} }
fn handle_click(&mut self) { fn handle_click(&mut self) {
// If hovering over both the gizmos give priority to sweep angle gizmo
if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() {
self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging);
self.arc_radius_handle.update_state(RadiusHandleState::Inactive);
return;
}
if self.sweep_angle_gizmo.hovered() { if self.sweep_angle_gizmo.hovered() {
self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging); self.sweep_angle_gizmo.update_state(SweepAngleGizmoState::Dragging);
} }
if self.arc_radius_handle.hovered() {
self.arc_radius_handle.update_state(RadiusHandleState::Dragging);
}
} }
fn handle_update(&mut self, _drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) { 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() { if self.sweep_angle_gizmo.is_dragging_or_snapped() {
self.sweep_angle_gizmo.update_arc(document, input, responses); self.sweep_angle_gizmo.update_arc(document, input, responses);
} }
if self.arc_radius_handle.is_dragging() {
self.arc_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
} }
fn dragging_overlays( fn dragging_overlays(
@ -58,20 +76,39 @@ impl ShapeGizmoHandler for ArcGizmoHandler {
self.sweep_angle_gizmo.overlays(None, document, input, mouse_position, overlay_context); self.sweep_angle_gizmo.overlays(None, document, input, mouse_position, overlay_context);
arc_outline(self.sweep_angle_gizmo.layer, document, overlay_context); arc_outline(self.sweep_angle_gizmo.layer, document, overlay_context);
} }
if self.arc_radius_handle.is_dragging() {
self.sweep_angle_gizmo.overlays(self.arc_radius_handle.layer, document, input, mouse_position, overlay_context);
self.arc_radius_handle.overlays(document, overlay_context);
}
} }
fn overlays( fn overlays(
&self, &self,
document: &DocumentMessageHandler, document: &DocumentMessageHandler,
selected_shape_layers: Option<LayerNodeIdentifier>, selected_shape_layer: Option<LayerNodeIdentifier>,
input: &InputPreprocessorMessageHandler, input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState, _shape_editor: &mut &mut crate::messages::tool::common_functionality::shape_editor::ShapeState,
mouse_position: DVec2, mouse_position: DVec2,
overlay_context: &mut crate::messages::portfolio::document::overlays::utility_types::OverlayContext, 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); // If hovering over both the gizmos give priority to sweep angle gizmo
if self.sweep_angle_gizmo.hovered() && self.arc_radius_handle.hovered() {
self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context);
return;
}
arc_outline(selected_shape_layers.or(self.sweep_angle_gizmo.layer), document, overlay_context); if self.arc_radius_handle.hovered() {
let layer = self.arc_radius_handle.layer;
self.arc_radius_handle.overlays(document, overlay_context);
self.sweep_angle_gizmo.overlays(layer, document, input, mouse_position, overlay_context);
}
self.sweep_angle_gizmo.overlays(selected_shape_layer, document, input, mouse_position, overlay_context);
self.arc_radius_handle.overlays(document, overlay_context);
arc_outline(selected_shape_layer.or(self.sweep_angle_gizmo.layer), document, overlay_context);
} }
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> { fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
@ -79,11 +116,16 @@ impl ShapeGizmoHandler for ArcGizmoHandler {
return Some(MouseCursorIcon::Default); return Some(MouseCursorIcon::Default);
} }
if self.arc_radius_handle.hovered() || self.arc_radius_handle.is_dragging() {
return Some(MouseCursorIcon::EWResize);
}
None None
} }
fn cleanup(&mut self) { fn cleanup(&mut self) {
self.sweep_angle_gizmo.cleanup(); self.sweep_angle_gizmo.cleanup();
self.arc_radius_handle.cleanup();
} }
} }
#[derive(Default)] #[derive(Default)]
@ -122,11 +164,9 @@ impl Arc {
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly // We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
if dimensions.x > dimensions.y { if dimensions.x > dimensions.y {
scale.x = dimensions.x / dimensions.y; scale.x = dimensions.x / dimensions.y;
scale.y = 1.;
radius = dimensions.y / 2.; radius = dimensions.y / 2.;
} else { } else {
scale.y = dimensions.y / dimensions.x; scale.y = dimensions.y / dimensions.x;
scale.x = 1.;
radius = dimensions.x / 2.; radius = dimensions.x / 2.;
} }

View File

@ -0,0 +1,125 @@
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::overlays::utility_types::OverlayContext;
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::circle_arc_radius_handle::{RadiusHandle, RadiusHandleState};
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::shape_utility::{ShapeGizmoHandler, ShapeToolModifierKey};
use crate::messages::tool::tool_messages::shape_tool::ShapeToolData;
use crate::messages::tool::tool_messages::tool_prelude::*;
use glam::DAffine2;
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
#[derive(Clone, Debug, Default)]
pub struct CircleGizmoHandler {
circle_radius_handle: RadiusHandle,
}
impl ShapeGizmoHandler for CircleGizmoHandler {
fn is_any_gizmo_hovered(&self) -> bool {
self.circle_radius_handle.hovered()
}
fn handle_state(&mut self, selected_circle_layer: LayerNodeIdentifier, mouse_position: DVec2, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
self.circle_radius_handle.handle_actions(selected_circle_layer, document, mouse_position, responses);
}
fn handle_click(&mut self) {
if self.circle_radius_handle.hovered() {
self.circle_radius_handle.update_state(RadiusHandleState::Dragging);
}
}
fn handle_update(&mut self, drag_start: DVec2, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, responses: &mut VecDeque<Message>) {
if self.circle_radius_handle.is_dragging() {
self.circle_radius_handle.update_inner_radius(document, input, responses, drag_start);
}
}
fn overlays(
&self,
document: &DocumentMessageHandler,
_selected_circle_layer: Option<LayerNodeIdentifier>,
_input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut ShapeState,
_mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
self.circle_radius_handle.overlays(document, overlay_context);
}
fn dragging_overlays(
&self,
document: &DocumentMessageHandler,
_input: &InputPreprocessorMessageHandler,
_shape_editor: &mut &mut ShapeState,
_mouse_position: DVec2,
overlay_context: &mut OverlayContext,
) {
if self.circle_radius_handle.is_dragging() {
self.circle_radius_handle.overlays(document, overlay_context);
}
}
fn cleanup(&mut self) {
self.circle_radius_handle.cleanup();
}
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon> {
if self.circle_radius_handle.hovered() || self.circle_radius_handle.is_dragging() {
return Some(MouseCursorIcon::EWResize);
}
None
}
}
#[derive(Default)]
pub struct Circle;
impl Circle {
pub fn create_node() -> NodeTemplate {
let node_type = resolve_document_node_type("Circle").expect("Circle can't be found");
node_type.node_template_input_override([None, Some(NodeInput::value(TaggedValue::F64(0.), false))])
}
pub fn update_shape(
document: &DocumentMessageHandler,
ipp: &InputPreprocessorMessageHandler,
layer: LayerNodeIdentifier,
shape_tool_data: &mut ShapeToolData,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
let center = modifier[0];
let [start, end] = shape_tool_data.data.calculate_circle_points(document, ipp, center);
let Some(node_id) = graph_modification_utils::get_circle_id(layer, &document.network_interface) else {
return;
};
let dimensions = (start - end).abs();
let radius: f64;
// We keep the smaller dimension's scale at 1 and scale the other dimension accordingly
if dimensions.x > dimensions.y {
radius = dimensions.y / 2.;
} else {
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(DVec2::ONE, 0., start.midpoint(end)),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
}
}

View File

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

View File

@ -26,6 +26,7 @@ pub enum ShapeType {
#[default] #[default]
Polygon = 0, Polygon = 0,
Star, Star,
Circle,
Arc, Arc,
Rectangle, Rectangle,
Ellipse, Ellipse,
@ -37,6 +38,7 @@ impl ShapeType {
(match self { (match self {
Self::Polygon => "Polygon", Self::Polygon => "Polygon",
Self::Star => "Star", Self::Star => "Star",
Self::Circle => "Circle",
Self::Arc => "Arc", Self::Arc => "Arc",
Self::Rectangle => "Rectangle", Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse", Self::Ellipse => "Ellipse",
@ -280,6 +282,19 @@ pub fn arc_end_points_ignore_layer(radius: f64, start_angle: f64, sweep_angle: f
} }
/// Calculate the viewport position of a star vertex given its index /// Calculate the viewport position of a star vertex given its index
/// Extract the node input values of Circle.
/// Returns an option of (radius).
pub fn extract_circle_radius(layer: LayerNodeIdentifier, document: &DocumentMessageHandler) -> Option<f64> {
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Circle")?;
let Some(&TaggedValue::F64(radius)) = node_inputs.get(1)?.as_value() else {
return None;
};
Some(radius)
}
/// Calculate the viewport position of as a star vertex given its index
pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 { pub fn star_vertex_position(viewport: DAffine2, vertex_index: i32, n: u32, radius1: f64, radius2: f64) -> DVec2 {
let angle = ((vertex_index as f64) * PI) / (n as f64); let angle = ((vertex_index as f64) * PI) / (n as f64);
let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 }; let radius = if vertex_index % 2 == 0 { radius1 } else { radius2 };

View File

@ -11,6 +11,7 @@ use crate::messages::tool::common_functionality::graph_modification_utils;
use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer; use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::resize::Resize; use crate::messages::tool::common_functionality::resize::Resize;
use crate::messages::tool::common_functionality::shapes::arc_shape::Arc; use crate::messages::tool::common_functionality::shapes::arc_shape::Arc;
use crate::messages::tool::common_functionality::shapes::circle_shape::Circle;
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints}; use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon; use crate::messages::tool::common_functionality::shapes::polygon_shape::Polygon;
use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays}; use crate::messages::tool::common_functionality::shapes::shape_utility::{ShapeToolModifierKey, ShapeType, anchor_overlays, transform_cage_overlays};
@ -110,6 +111,9 @@ fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder {
MenuListEntry::new("Star") MenuListEntry::new("Star")
.label("Star") .label("Star")
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()),
MenuListEntry::new("Circle")
.label("Circle")
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Circle)).into()),
MenuListEntry::new("Arc") MenuListEntry::new("Arc")
.label("Arc") .label("Arc")
.on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()), .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Arc)).into()),
@ -229,7 +233,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Shap
} }
} }
self.fsm_state.update_hints(responses); update_dynamic_hints(&self.fsm_state, responses, &self.tool_data);
self.send_layout(responses, LayoutTarget::ToolOptions); self.send_layout(responses, LayoutTarget::ToolOptions);
} }
@ -472,6 +476,9 @@ impl Fsm for ShapeToolFsmState {
if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) { if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) {
Line::overlays(document, tool_data, &mut overlay_context); Line::overlays(document, tool_data, &mut overlay_context);
if tool_options.shape_type == ShapeType::Circle {
tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
}
} }
self self
@ -650,7 +657,7 @@ impl Fsm for ShapeToolFsmState {
}; };
match tool_data.current_shape { match tool_data.current_shape {
ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Arc | ShapeType::Rectangle => tool_data.data.start(document, input), ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => tool_data.data.start(document, input),
ShapeType::Line => { ShapeType::Line => {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
@ -663,6 +670,7 @@ impl Fsm for ShapeToolFsmState {
let node = match tool_data.current_shape { let node = match tool_data.current_shape {
ShapeType::Polygon => Polygon::create_node(tool_options.vertices), ShapeType::Polygon => Polygon::create_node(tool_options.vertices),
ShapeType::Star => Star::create_node(tool_options.vertices), ShapeType::Star => Star::create_node(tool_options.vertices),
ShapeType::Circle => Circle::create_node(),
ShapeType::Arc => Arc::create_node(tool_options.arc_type), ShapeType::Arc => Arc::create_node(tool_options.arc_type),
ShapeType::Rectangle => Rectangle::create_node(), ShapeType::Rectangle => Rectangle::create_node(),
ShapeType::Ellipse => Ellipse::create_node(), ShapeType::Ellipse => Ellipse::create_node(),
@ -675,7 +683,7 @@ impl Fsm for ShapeToolFsmState {
let defered_responses = &mut VecDeque::new(); let defered_responses = &mut VecDeque::new();
match tool_data.current_shape { match tool_data.current_shape {
ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Arc | ShapeType::Polygon | ShapeType::Star => { ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Rectangle | ShapeType::Ellipse => {
defered_responses.add(GraphOperationMessage::TransformSet { defered_responses.add(GraphOperationMessage::TransformSet {
layer, layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position), transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
@ -707,12 +715,13 @@ impl Fsm for ShapeToolFsmState {
}; };
match tool_data.current_shape { match tool_data.current_shape {
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::Circle => Circle::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Rectangle => Rectangle::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Ellipse => Ellipse::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Line => Line::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Star => Star::update_shape(document, input, layer, tool_data, modifier, responses),
ShapeType::Arc => Arc::update_shape(document, input, layer, tool_data, modifier, responses),
} }
// Auto-panning // Auto-panning
@ -892,6 +901,7 @@ impl Fsm for ShapeToolFsmState {
tool_data.data.cleanup(responses); tool_data.data.cleanup(responses);
tool_data.current_shape = shape; tool_data.current_shape = shape;
responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(shape)));
ShapeToolFsmState::Ready(shape) ShapeToolFsmState::Ready(shape)
} }
(_, ShapeToolMessage::HideShapeTypeWidget(hide)) => { (_, ShapeToolMessage::HideShapeTypeWidget(hide)) => {
@ -903,85 +913,100 @@ impl Fsm for ShapeToolFsmState {
} }
} }
fn update_hints(&self, responses: &mut VecDeque<Message>) { fn update_hints(&self, _responses: &mut VecDeque<Message>) {
let hint_data = match self { // Moved logic to update_dynamic_hints
ShapeToolFsmState::Ready(shape) => {
let hint_groups = match shape {
ShapeType::Polygon | ShapeType::Star | ShapeType::Arc => vec![
HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
]),
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]),
],
ShapeType::Ellipse => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
ShapeType::Line => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"),
HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(),
])],
ShapeType::Rectangle => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"),
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
};
HintData(hint_groups)
}
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 | 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![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
};
common_hint_group.push(tool_hint_group);
if matches!(shape, ShapeType::Polygon | ShapeType::Star) {
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]));
}
HintData(common_hint_group)
}
ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
]),
ShapeToolFsmState::ResizingBounds => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]),
]),
ShapeToolFsmState::RotatingBounds => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]),
]),
ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]),
]),
ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
};
responses.add(FrontendMessage::UpdateInputHints { hint_data });
} }
fn update_cursor(&self, responses: &mut VecDeque<Message>) { fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
} }
} }
fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Message>, tool_data: &ShapeToolData) {
let hint_data = match state {
ShapeToolFsmState::Ready(_) => {
let hint_groups = match tool_data.current_shape {
ShapeType::Polygon | ShapeType::Star => vec![
HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Polygon"),
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
]),
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]),
],
ShapeType::Ellipse => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
ShapeType::Line => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"),
HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
HintInfo::keys([Key::Control], "Lock Angle").prepend_plus(),
])],
ShapeType::Rectangle => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Rectangle"),
HintInfo::keys([Key::Shift], "Constrain Square").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
ShapeType::Circle => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Circle"),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
ShapeType::Arc => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arc"),
HintInfo::keys([Key::Shift], "Constrain Arc").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
};
HintData(hint_groups)
}
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 | 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![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
};
if !tool_hint_group.0.is_empty() {
common_hint_group.push(tool_hint_group);
}
if matches!(shape, ShapeType::Polygon | ShapeType::Star) {
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]));
}
HintData(common_hint_group)
}
ShapeToolFsmState::DraggingLineEndpoints => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
]),
ShapeToolFsmState::ResizingBounds => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Alt], "From Pivot"), HintInfo::keys([Key::Shift], "Preserve Aspect Ratio")]),
]),
ShapeToolFsmState::RotatingBounds => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Shift], "15° Increments")]),
]),
ShapeToolFsmState::SkewingBounds { .. } => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![HintInfo::keys([Key::Control], "Unlock Slide")]),
]),
ShapeToolFsmState::ModifyingGizmo => HintData(vec![HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])]),
};
responses.add(FrontendMessage::UpdateInputHints { hint_data });
}