Improve Shape tool arrow mode interactive drawing with angle modifier keys and endpoint gizmos (#3874)

* Make the order of Shape tool shape types consistent

* Add Arrow shape modifier keys and snapping support

* Add endpoint dragging to arrows

* Show the default cursor when hovering line/arrow endpoints

* Reduce duplicated function

* Fix incorrect coordinate spaces

* Improve endpoint dragging clarity
This commit is contained in:
Keavon Chambers 2026-03-10 03:41:35 -07:00 committed by GitHub
parent e7a2800665
commit 2910e50b2f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 177 additions and 99 deletions

View File

@ -1,5 +1,7 @@
use super::line_shape::{LineEnd, generate_line};
use super::shape_utility::ShapeToolModifierKey;
use super::*;
use crate::consts::BOUNDS_SELECT_THRESHOLD;
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::node_graph::document_node_definitions::{DefinitionIdentifier, resolve_document_node_type};
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
@ -7,6 +9,8 @@ use crate::messages::portfolio::document::utility_types::document_metadata::Laye
use crate::messages::portfolio::document::utility_types::network_interface::{InputConnector, NodeTemplate};
use crate::messages::prelude::*;
use crate::messages::tool::common_functionality::graph_modification_utils;
pub use crate::messages::tool::common_functionality::graph_modification_utils::NodeGraphLayer;
use crate::messages::tool::common_functionality::snapping::SnapData;
use glam::{DAffine2, DVec2};
use graph_craft::document::NodeInput;
use graph_craft::document::value::TaggedValue;
@ -31,20 +35,26 @@ impl Arrow {
pub fn update_shape(
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
_viewport: &ViewportMessageHandler,
viewport: &ViewportMessageHandler,
layer: LayerNodeIdentifier,
tool_data: &mut ShapeToolData,
_modifier: ShapeToolModifierKey,
modifier: ShapeToolModifierKey,
responses: &mut VecDeque<Message>,
) {
// Track current mouse position in viewport space
let [center, snap_angle, lock_angle] = modifier;
tool_data.line_data.drag_current = input.mouse.position;
// Compute arrow_to in document space
let document_to_viewport = document.metadata().document_to_viewport;
let start_document = tool_data.data.drag_start;
let end_document = document_to_viewport.inverse().transform_point2(input.mouse.position);
let arrow_to = end_document - start_document;
let keyboard = &input.keyboard;
let ignore = [layer];
let snap_data = SnapData::ignore(document, input, viewport, &ignore);
let mut document_points = generate_line(tool_data, snap_data, keyboard.key(lock_angle), keyboard.key(snap_angle), keyboard.key(center));
if tool_data.line_data.dragging_endpoint == Some(LineEnd::Start) {
document_points.swap(0, 1);
}
let arrow_to = document_points[1] - document_points[0];
if arrow_to.length() < 1e-6 {
return;
@ -54,7 +64,8 @@ impl Arrow {
return;
};
// Update Arrow node arrow_to in document space
let document_to_viewport = document.metadata().document_to_viewport;
responses.add(NodeGraphMessage::SetInput {
input_connector: InputConnector::node(node_id, 1),
input: NodeInput::value(TaggedValue::DVec2(arrow_to), false),
@ -63,7 +74,7 @@ impl Arrow {
let scope = downstream.inverse() * document_to_viewport;
responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_translation(start_document),
transform: DAffine2::from_translation(document_points[0]),
transform_in: TransformIn::Scope { scope },
skip_rerender: false,
});
@ -71,5 +82,35 @@ impl Arrow {
responses.add(NodeGraphMessage::RunDocumentGraph);
}
pub fn overlays(_document: &DocumentMessageHandler, _tool_data: &ShapeToolData, _overlay_context: &mut OverlayContext) {}
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
let arrow_layers: HashMap<LayerNodeIdentifier, [DVec2; 2]> = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.filter_map(|layer| {
let node_inputs = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::arrow::IDENTIFIER))?;
let Some(&TaggedValue::DVec2(arrow_to)) = node_inputs[1].as_value() else { return None };
let transform = document.metadata().transform_to_viewport(layer);
let viewport_start = transform.transform_point2(DVec2::ZERO);
let viewport_end = transform.transform_point2(arrow_to);
if !arrow_to.abs_diff_eq(DVec2::ZERO, f64::EPSILON * 1000.) {
let is_editing = shape_tool_data.line_data.editing_layer == Some(layer);
for (i, pos) in [viewport_start, viewport_end].into_iter().enumerate() {
let is_dragged = is_editing && matches!((i, &shape_tool_data.line_data.dragging_endpoint), (0, Some(LineEnd::Start)) | (1, Some(LineEnd::End)));
if is_dragged || (pos - mouse_position).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2) {
overlay_context.hover_manipulator_anchor(pos, is_dragged);
} else {
overlay_context.square(pos, Some(6.), None, None);
}
}
}
Some((layer, [DVec2::ZERO, arrow_to]))
})
.collect();
shape_tool_data.line_data.selected_layers_with_position.extend(arrow_layers);
}
}

View File

@ -87,7 +87,7 @@ impl Line {
});
}
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, overlay_context: &mut OverlayContext) {
pub fn overlays(document: &DocumentMessageHandler, shape_tool_data: &mut ShapeToolData, mouse_position: DVec2, overlay_context: &mut OverlayContext) {
shape_tool_data.line_data.selected_layers_with_position = document
.network_interface
.selected_nodes()
@ -106,8 +106,15 @@ impl Line {
let viewport_end = transform.transform_point2(line_to);
if !line_to.abs_diff_eq(DVec2::ZERO, f64::EPSILON * 1000.) {
overlay_context.line(viewport_start, viewport_end, None, None);
overlay_context.square(viewport_start, Some(6.), None, None);
overlay_context.square(viewport_end, Some(6.), None, None);
let is_editing = shape_tool_data.line_data.editing_layer == Some(layer);
for (i, pos) in [viewport_start, viewport_end].into_iter().enumerate() {
let is_dragged = is_editing && matches!((i, &shape_tool_data.line_data.dragging_endpoint), (0, Some(LineEnd::Start)) | (1, Some(LineEnd::End)));
if is_dragged || (pos - mouse_position).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2) {
overlay_context.hover_manipulator_anchor(pos, is_dragged);
} else {
overlay_context.square(pos, Some(6.), None, None);
}
}
}
// Store local-space positions for endpoint editing
@ -117,7 +124,7 @@ impl Line {
}
}
fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
pub fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle: bool, snap_angle: bool, center: bool) -> [DVec2; 2] {
let document_to_viewport = snap_data.document.metadata().document_to_viewport;
let mut document_points = [tool_data.data.drag_start, document_to_viewport.inverse().transform_point2(tool_data.line_data.drag_current)];
@ -180,42 +187,6 @@ fn generate_line(tool_data: &mut ShapeToolData, snap_data: SnapData, lock_angle:
document_points
}
pub fn clicked_on_line_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool {
let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs(&DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::line::IDENTIFIER)) else {
return false;
};
let Some(&TaggedValue::DVec2(line_to)) = node_inputs[1].as_value() else {
return false;
};
// Line goes from local origin (0,0) to line_to, positioned by the Transform node
let local_start = DVec2::ZERO;
let local_end = line_to;
let transform = document.metadata().transform_to_viewport(layer);
let viewport_x = transform.transform_vector2(DVec2::X).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let viewport_y = transform.transform_vector2(DVec2::Y).normalize_or_zero() * BOUNDS_SELECT_THRESHOLD;
let threshold_x = transform.inverse().transform_vector2(viewport_x).length();
let threshold_y = transform.inverse().transform_vector2(viewport_y).length();
let drag_start = input.mouse.position;
let [start, end] = [local_start, local_end].map(|point| transform.transform_point2(point));
let start_click = (drag_start.y - start.y).abs() < threshold_y && (drag_start.x - start.x).abs() < threshold_x;
let end_click = (drag_start.y - end.y).abs() < threshold_y && (drag_start.x - end.x).abs() < threshold_x;
if start_click || end_click {
shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
// Convert the anchor endpoint (the one NOT being dragged) to document space for drag_start
let anchor_local = if end_click { local_start } else { local_end };
shape_tool_data.data.drag_start = document.metadata().transform_to_document(layer).transform_point2(anchor_local);
shape_tool_data.line_data.editing_layer = Some(layer);
return true;
}
false
}
#[cfg(test)]
mod test_line_tool {
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;

View File

@ -1,5 +1,6 @@
use super::ShapeToolData;
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT};
use super::line_shape::LineEnd;
use crate::consts::{ARC_SWEEP_GIZMO_RADIUS, ARC_SWEEP_GIZMO_TEXT_HEIGHT, BOUNDS_SELECT_THRESHOLD};
use crate::messages::frontend::utility_types::MouseCursorIcon;
use crate::messages::message::Message;
use crate::messages::portfolio::document::node_graph::document_node_definitions::DefinitionIdentifier;
@ -33,10 +34,10 @@ pub enum ShapeType {
Arc,
Spiral,
Grid,
Rectangle,
Ellipse,
Arrow,
Line,
Line, // KEEP THIS AT THE END
Rectangle, // KEEP THIS AT THE END
Ellipse, // KEEP THIS AT THE END
}
impl ShapeType {
@ -46,12 +47,12 @@ impl ShapeType {
Self::Star => "Star",
Self::Circle => "Circle",
Self::Arc => "Arc",
Self::Grid => "Grid",
Self::Spiral => "Spiral",
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
Self::Grid => "Grid",
Self::Arrow => "Arrow",
Self::Line => "Line",
Self::Rectangle => "Rectangle",
Self::Ellipse => "Ellipse",
})
.into()
}
@ -61,7 +62,6 @@ impl ShapeType {
Self::Line => "Line Tool",
Self::Rectangle => "Rectangle Tool",
Self::Ellipse => "Ellipse Tool",
Self::Arrow => "Arrow Tool",
_ => "",
})
.into()
@ -80,7 +80,6 @@ impl ShapeType {
Self::Line => "VectorLineTool",
Self::Rectangle => "VectorRectangleTool",
Self::Ellipse => "VectorEllipseTool",
Self::Arrow => "VectorArrowTool",
_ => "",
})
.into()
@ -91,7 +90,6 @@ impl ShapeType {
Self::Line => ToolType::Line,
Self::Rectangle => ToolType::Rectangle,
Self::Ellipse => ToolType::Ellipse,
Self::Arrow => ToolType::Shape,
_ => ToolType::Shape,
}
}
@ -154,6 +152,42 @@ pub trait ShapeGizmoHandler {
fn mouse_cursor_icon(&self) -> Option<MouseCursorIcon>;
}
/// Check if the mouse clicked on either endpoint of a line-like shape (Line or Arrow).
pub fn clicked_on_shape_endpoints(layer: LayerNodeIdentifier, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, shape_tool_data: &mut ShapeToolData) -> bool {
let line_like_shape_nodes = [
DefinitionIdentifier::ProtoNode(graphene_std::vector::generator_nodes::line::IDENTIFIER),
DefinitionIdentifier::ProtoNode(graphene_std::vector_nodes::arrow::IDENTIFIER),
];
let node_graph_layer = NodeGraphLayer::new(layer, &document.network_interface);
let endpoint = line_like_shape_nodes.iter().find_map(|id| {
let node_inputs = node_graph_layer.find_node_inputs(id)?;
let &TaggedValue::DVec2(endpoint) = node_inputs[1].as_value()? else { return None };
Some(endpoint)
});
let Some(endpoint) = endpoint else { return false };
let local_start = DVec2::ZERO;
let local_end = endpoint;
let transform = document.metadata().transform_to_viewport(layer);
let mouse_pos = input.mouse.position;
let [start, end] = [local_start, local_end].map(|point| transform.transform_point2(point));
let start_click = (mouse_pos - start).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2);
let end_click = (mouse_pos - end).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2);
let endpoint_click = start_click || end_click;
if endpoint_click {
shape_tool_data.line_data.dragging_endpoint = Some(if end_click { LineEnd::End } else { LineEnd::Start });
let anchor_local = if end_click { local_start } else { local_end };
shape_tool_data.data.drag_start = document.metadata().transform_to_document(layer).transform_point2(anchor_local);
shape_tool_data.line_data.editing_layer = Some(layer);
}
endpoint_click
}
/// Center, Lock Ratio, Lock Angle, Snap Angle, Increase/Decrease Side
pub fn update_radius_sign(end: DVec2, start: DVec2, layer: LayerNodeIdentifier, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let sign_num = if end[1] > start[1] { 1. } else { -1. };

View File

@ -1,5 +1,5 @@
use super::tool_prelude::*;
use crate::consts::{DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE};
use crate::consts::{BOUNDS_SELECT_THRESHOLD, DEFAULT_STROKE_WIDTH, SNAP_POINT_TOLERANCE};
use crate::messages::portfolio::document::graph_operation::utility_types::TransformIn;
use crate::messages::portfolio::document::overlays::utility_types::OverlayContext;
use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier;
@ -12,9 +12,9 @@ use crate::messages::tool::common_functionality::shapes::arc_shape::Arc;
use crate::messages::tool::common_functionality::shapes::arrow_shape::Arrow;
use crate::messages::tool::common_functionality::shapes::circle_shape::Circle;
use crate::messages::tool::common_functionality::shapes::grid_shape::Grid;
use crate::messages::tool::common_functionality::shapes::line_shape::{LineToolData, clicked_on_line_endpoints};
use crate::messages::tool::common_functionality::shapes::line_shape::LineToolData;
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, clicked_on_shape_endpoints, transform_cage_overlays};
use crate::messages::tool::common_functionality::shapes::spiral_shape::Spiral;
use crate::messages::tool::common_functionality::shapes::star_shape::Star;
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
@ -645,11 +645,11 @@ impl Fsm for ShapeToolFsmState {
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let all_selected_layers_line = document
let all_selected_layers_line_or_arrow = document
.network_interface
.selected_nodes()
.selected_visible_and_unlocked_layers(&document.network_interface)
.all(|layer| graph_modification_utils::get_line_id(layer, &document.network_interface).is_some());
.all(|layer| graph_modification_utils::get_line_id(layer, &document.network_interface).is_some() || graph_modification_utils::get_arrow_id(layer, &document.network_interface).is_some());
let ToolMessage::Shape(event) = event else { return self };
@ -677,7 +677,15 @@ impl Fsm for ShapeToolFsmState {
let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. });
let hovering_over_gizmo = tool_data.gizmo_manager.hovering_over_gizmo();
if !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo {
// Check if hovering over a line/arrow endpoint (using data from previous overlay pass)
let hovering_over_endpoint = tool_data.line_data.selected_layers_with_position.iter().any(|(layer, endpoints)| {
let transform = document.metadata().transform_to_viewport(*layer);
endpoints
.iter()
.any(|&local_pos| (transform.transform_point2(local_pos) - input.mouse.position).length_squared() < BOUNDS_SELECT_THRESHOLD.powi(2))
});
if !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo && !hovering_over_endpoint {
tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input, viewport), &mut overlay_context);
}
@ -690,9 +698,13 @@ impl Fsm for ShapeToolFsmState {
anchor_overlays(document, &mut overlay_context);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
} else if matches!(self, ShapeToolFsmState::Ready(_)) {
Line::overlays(document, tool_data, &mut overlay_context);
Line::overlays(document, tool_data, input.mouse.position, &mut overlay_context);
Arrow::overlays(document, tool_data, input.mouse.position, &mut overlay_context);
if all_selected_layers_line {
if all_selected_layers_line_or_arrow {
let cursor = if hovering_over_endpoint { MouseCursorIcon::Default } else { MouseCursorIcon::Crosshair };
tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
return self;
}
@ -721,14 +733,20 @@ impl Fsm for ShapeToolFsmState {
}
}
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or_else(|| tool_data.transform_cage_mouse_icon(input));
let cursor = tool_data
.gizmo_manager
.mouse_cursor_icon()
.or_else(|| hovering_over_endpoint.then_some(MouseCursorIcon::Default))
.unwrap_or_else(|| tool_data.transform_cage_mouse_icon(input));
tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
}
if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) {
Line::overlays(document, tool_data, &mut overlay_context);
Line::overlays(document, tool_data, input.mouse.position, &mut overlay_context);
Arrow::overlays(document, tool_data, input.mouse.position, &mut overlay_context);
if tool_options.shape_type == ShapeType::Circle {
tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
}
@ -836,14 +854,14 @@ impl Fsm for ShapeToolFsmState {
return ShapeToolFsmState::ModifyingGizmo;
}
// If clicked on endpoints of a selected line, drag its endpoints
// If clicked on endpoints of a selected line or arrow, drag its endpoints
if let Some((layer, _, _)) = closest_point(
document,
mouse_pos,
SNAP_POINT_TOLERANCE,
document.network_interface.selected_nodes().selected_visible_and_unlocked_layers(&document.network_interface),
|_| false,
) && clicked_on_line_endpoints(layer, document, input, tool_data)
) && clicked_on_shape_endpoints(layer, document, input, tool_data)
&& !input.keyboard.key(Key::Control)
{
responses.add(DocumentMessage::StartTransaction);
@ -909,10 +927,10 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Arc => Arc::create_node(tool_options.arc_type),
ShapeType::Spiral => Spiral::create_node(tool_options.spiral_type, tool_options.turns),
ShapeType::Grid => Grid::create_node(tool_options.grid_type),
ShapeType::Rectangle => Rectangle::create_node(),
ShapeType::Ellipse => Ellipse::create_node(),
ShapeType::Arrow => Arrow::create_node(tool_options.arrow_shaft_width, tool_options.arrow_head_width, tool_options.arrow_head_length),
ShapeType::Line => Line::create_node(),
ShapeType::Rectangle => Rectangle::create_node(),
ShapeType::Ellipse => Ellipse::create_node(),
};
let nodes = vec![(NodeId(0), node)];
@ -980,12 +998,12 @@ impl Fsm for ShapeToolFsmState {
ShapeType::Star => Star::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Circle => Circle::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Arc => Arc::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Arrow => Arrow::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Spiral => Spiral::update_shape(document, input, viewport, layer, tool_data, responses),
ShapeType::Grid => Grid::update_shape(document, input, layer, tool_options.grid_type, tool_data, modifier, responses),
ShapeType::Arrow => Arrow::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Line => Line::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Rectangle => Rectangle::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Ellipse => Ellipse::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
ShapeType::Line => Line::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
}
// Auto-panning
@ -999,7 +1017,12 @@ impl Fsm for ShapeToolFsmState {
return ShapeToolFsmState::Ready(tool_data.current_shape);
};
if graph_modification_utils::get_arrow_id(layer, &document.network_interface).is_some() {
Arrow::update_shape(document, input, viewport, layer, tool_data, modifier, responses);
} else {
Line::update_shape(document, input, viewport, layer, tool_data, modifier, responses);
}
// Auto-panning
let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
tool_data.auto_panning.setup_by_mouse_position(input, viewport, &messages, responses);
@ -1206,15 +1229,30 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
]),
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Sides")]),
],
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(),
])],
ShapeType::Spiral => vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Spiral")]),
HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Turns")]),
],
ShapeType::Ellipse => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Ellipse"),
HintInfo::keys([Key::Shift], "Constrain Circular").prepend_plus(),
ShapeType::Grid => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Grid"),
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
ShapeType::Arrow => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arrow"),
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::Line => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Line"),
HintInfo::keys([Key::Shift], "15° Increments").prepend_plus(),
@ -1226,21 +1264,11 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
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"),
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::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(),
])],
ShapeType::Grid => vec![HintGroup(vec![
HintInfo::mouse(MouseMotion::LmbDrag, "Draw Grid"),
HintInfo::keys([Key::Shift], "Constrain Regular").prepend_plus(),
HintInfo::keys([Key::Alt], "From Center").prepend_plus(),
])],
ShapeType::Arrow => vec![HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Draw Arrow")])],
};
HintData(hint_groups)
}
@ -1248,17 +1276,21 @@ fn update_dynamic_hints(state: &ShapeToolFsmState, responses: &mut VecDeque<Mess
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::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Spiral => HintGroup(vec![]),
ShapeType::Grid => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Arrow => HintGroup(vec![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
ShapeType::Line => HintGroup(vec![
HintInfo::keys([Key::Shift], "15° Increments"),
HintInfo::keys([Key::Alt], "From Center"),
HintInfo::keys([Key::Control], "Lock Angle"),
]),
ShapeType::Arrow => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Angle")]),
ShapeType::Circle => HintGroup(vec![HintInfo::keys([Key::Alt], "From Center")]),
ShapeType::Spiral => HintGroup(vec![]),
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")]),
};
if !tool_hint_group.0.is_empty() {