Graphite/editor/src/messages/tool/tool_messages/shape_tool.rs

1192 lines
43 KiB
Rust

use super::tool_prelude::*;
use crate::consts::{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;
use crate::messages::tool::common_functionality::auto_panning::AutoPanning;
use crate::messages::tool::common_functionality::color_selector::{ToolColorOptions, ToolColorType};
use crate::messages::tool::common_functionality::gizmos::gizmo_manager::GizmoManager;
use crate::messages::tool::common_functionality::graph_modification_utils;
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::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::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::spiral_shape::Spiral;
use crate::messages::tool::common_functionality::shapes::star_shape::Star;
use crate::messages::tool::common_functionality::shapes::{Ellipse, Line, Rectangle};
use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapData, SnapTypeConfiguration};
use crate::messages::tool::common_functionality::transformation_cage::{BoundingBoxManager, EdgeBool};
use crate::messages::tool::common_functionality::utility_functions::{closest_point, resize_bounds, rotate_bounds, skew_bounds, transforming_transform_cage};
use graph_craft::document::NodeId;
use graphene_std::Color;
use graphene_std::renderer::Quad;
use graphene_std::vector::misc::{ArcType, GridType, SpiralType};
use std::vec;
#[derive(Default, ExtractField)]
pub struct ShapeTool {
fsm_state: ShapeToolFsmState,
tool_data: ShapeToolData,
options: ShapeToolOptions,
}
pub struct ShapeToolOptions {
line_weight: f64,
fill: ToolColorOptions,
stroke: ToolColorOptions,
vertices: u32,
shape_type: ShapeType,
arc_type: ArcType,
grid_type: GridType,
spiral_type: SpiralType,
turns: f64,
}
impl Default for ShapeToolOptions {
fn default() -> Self {
Self {
line_weight: DEFAULT_STROKE_WIDTH,
fill: ToolColorOptions::new_secondary(),
stroke: ToolColorOptions::new_primary(),
vertices: 5,
shape_type: ShapeType::Polygon,
arc_type: ArcType::Open,
spiral_type: SpiralType::Archimedean,
turns: 5.,
grid_type: GridType::Rectangular,
}
}
}
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ShapeOptionsUpdate {
FillColor(Option<Color>),
FillColorType(ToolColorType),
LineWeight(f64),
StrokeColor(Option<Color>),
StrokeColorType(ToolColorType),
WorkingColors(Option<Color>, Option<Color>),
Vertices(u32),
ShapeType(ShapeType),
ArcType(ArcType),
SpiralType(SpiralType),
Turns(f64),
GridType(GridType),
}
#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)]
pub enum ShapeToolMessage {
// Standard messages
Overlays { context: OverlayContext },
Abort,
WorkingColorChanged,
// Tool-specific messages
DragStart,
DragStop,
HideShapeTypeWidget { hide: bool },
PointerMove { modifier: ShapeToolModifierKey },
PointerOutsideViewport { modifier: ShapeToolModifierKey },
UpdateOptions { options: ShapeOptionsUpdate },
SetShape { shape: ShapeType },
IncreaseSides,
DecreaseSides,
NudgeSelectedLayers { delta_x: f64, delta_y: f64, resize: Key, resize_opposite_corner: Key },
}
fn create_sides_widget(vertices: u32) -> WidgetInstance {
NumberInput::new(Some(vertices as f64))
.label("Sides")
.int()
.min(3.)
.max(1000.)
.mode(NumberInputMode::Increment)
.on_update(|number_input: &NumberInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32),
}
.into()
})
.widget_instance()
}
fn create_turns_widget(turns: f64) -> WidgetInstance {
NumberInput::new(Some(turns))
.label("Turns")
.min(0.5)
.mode(NumberInputMode::Increment)
.on_update(|number_input: &NumberInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Turns(number_input.value.unwrap()),
}
.into()
})
.widget_instance()
}
fn create_shape_option_widget(shape_type: ShapeType) -> WidgetInstance {
let entries = vec![vec![
MenuListEntry::new("Polygon").label("Polygon").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Polygon),
}
.into()
}),
MenuListEntry::new("Star").label("Star").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Star),
}
.into()
}),
MenuListEntry::new("Circle").label("Circle").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Circle),
}
.into()
}),
MenuListEntry::new("Arc").label("Arc").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Arc),
}
.into()
}),
MenuListEntry::new("Spiral").label("Spiral").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Spiral),
}
.into()
}),
MenuListEntry::new("Grid").label("Grid").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(ShapeType::Grid),
}
.into()
}),
]];
DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_instance()
}
fn create_arc_type_widget(arc_type: ArcType) -> WidgetInstance {
let entries = vec![
RadioEntryData::new("Open").label("Open").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(ArcType::Open),
}
.into()
}),
RadioEntryData::new("Closed").label("Closed").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(ArcType::Closed),
}
.into()
}),
RadioEntryData::new("Pie").label("Pie").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ArcType(ArcType::PieSlice),
}
.into()
}),
];
RadioInput::new(entries).selected_index(Some(arc_type as u32)).widget_instance()
}
fn create_weight_widget(line_weight: f64) -> WidgetInstance {
NumberInput::new(Some(line_weight))
.unit(" px")
.label("Weight")
.min(0.)
.max((1_u64 << f64::MANTISSA_DIGITS) as f64)
.on_update(|number_input: &NumberInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::LineWeight(number_input.value.unwrap()),
}
.into()
})
.widget_instance()
}
fn create_spiral_type_widget(spiral_type: SpiralType) -> WidgetInstance {
let entries = vec![vec![
MenuListEntry::new("Archimedean").label("Archimedean").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::SpiralType(SpiralType::Archimedean),
}
.into()
}),
MenuListEntry::new("Logarithmic").label("Logarithmic").on_commit(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::SpiralType(SpiralType::Logarithmic),
}
.into()
}),
]];
DropdownInput::new(entries).selected_index(Some(spiral_type as u32)).widget_instance()
}
fn create_grid_type_widget(grid_type: GridType) -> WidgetInstance {
let entries = vec![
RadioEntryData::new("Rectangular").label("Rectangular").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::GridType(GridType::Rectangular),
}
.into()
}),
RadioEntryData::new("Isometric").label("Isometric").on_update(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::GridType(GridType::Isometric),
}
.into()
}),
];
RadioInput::new(entries).selected_index(Some(grid_type as u32)).widget_instance()
}
impl LayoutHolder for ShapeTool {
fn layout(&self) -> Layout {
let mut widgets = vec![];
if !self.tool_data.hide_shape_option_widget {
widgets.push(create_shape_option_widget(self.options.shape_type));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
if self.options.shape_type == ShapeType::Polygon || self.options.shape_type == ShapeType::Star {
widgets.push(create_sides_widget(self.options.vertices));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
}
if self.options.shape_type == ShapeType::Arc {
widgets.push(create_arc_type_widget(self.options.arc_type));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
}
}
if self.options.shape_type == ShapeType::Spiral {
widgets.push(create_spiral_type_widget(self.options.spiral_type));
widgets.push(Separator::new(SeparatorType::Related).widget_instance());
widgets.push(create_turns_widget(self.options.turns));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
}
if self.options.shape_type == ShapeType::Grid {
widgets.push(create_grid_type_widget(self.options.grid_type));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
}
if self.options.shape_type != ShapeType::Line {
widgets.append(&mut self.options.fill.create_widgets(
"Fill",
true,
|_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::FillColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::FillColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
}
widgets.append(&mut self.options.stroke.create_widgets(
"Stroke",
true,
|_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::StrokeColor(None),
}
.into()
},
|color_type: ToolColorType| {
WidgetCallback::new(move |_| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::StrokeColorType(color_type.clone()),
}
.into()
})
},
|color: &ColorInput| {
ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb())),
}
.into()
},
));
widgets.push(Separator::new(SeparatorType::Unrelated).widget_instance());
widgets.push(create_weight_widget(self.options.line_weight));
Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }]))
}
}
#[message_handler_data]
impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for ShapeTool {
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, context: &mut ToolActionMessageContext<'a>) {
let ToolMessage::Shape(ShapeToolMessage::UpdateOptions { options }) = message else {
self.fsm_state.process_event(message, &mut self.tool_data, context, &self.options, responses, true);
return;
};
match options {
ShapeOptionsUpdate::FillColor(color) => {
self.options.fill.custom_color = color;
self.options.fill.color_type = ToolColorType::Custom;
}
ShapeOptionsUpdate::FillColorType(color_type) => {
self.options.fill.color_type = color_type;
}
ShapeOptionsUpdate::LineWeight(line_weight) => {
self.options.line_weight = line_weight;
}
ShapeOptionsUpdate::StrokeColor(color) => {
self.options.stroke.custom_color = color;
self.options.stroke.color_type = ToolColorType::Custom;
}
ShapeOptionsUpdate::StrokeColorType(color_type) => {
self.options.stroke.color_type = color_type;
}
ShapeOptionsUpdate::WorkingColors(primary, secondary) => {
self.options.stroke.primary_working_color = primary;
self.options.stroke.secondary_working_color = secondary;
self.options.fill.primary_working_color = primary;
self.options.fill.secondary_working_color = secondary;
}
ShapeOptionsUpdate::ShapeType(shape) => {
self.options.shape_type = shape;
self.tool_data.current_shape = shape;
}
ShapeOptionsUpdate::Vertices(vertices) => {
self.options.vertices = vertices;
}
ShapeOptionsUpdate::ArcType(arc_type) => {
self.options.arc_type = arc_type;
}
ShapeOptionsUpdate::SpiralType(spiral_type) => {
self.options.spiral_type = spiral_type;
}
ShapeOptionsUpdate::Turns(turns) => {
self.options.turns = turns;
}
ShapeOptionsUpdate::GridType(grid_type) => {
self.options.grid_type = grid_type;
}
}
update_dynamic_hints(&self.fsm_state, responses, &self.tool_data);
self.send_layout(responses, LayoutTarget::ToolOptions);
}
fn actions(&self) -> ActionList {
match self.fsm_state {
ShapeToolFsmState::Ready(_) => actions!(ShapeToolMessageDiscriminant;
DragStart,
PointerMove,
SetShape,
Abort,
HideShapeTypeWidget,
IncreaseSides,
DecreaseSides,
NudgeSelectedLayers,
),
ShapeToolFsmState::Drawing(_)
| ShapeToolFsmState::ResizingBounds
| ShapeToolFsmState::DraggingLineEndpoints
| ShapeToolFsmState::RotatingBounds
| ShapeToolFsmState::ModifyingGizmo
| ShapeToolFsmState::SkewingBounds { .. } => {
actions!(ShapeToolMessageDiscriminant;
DragStop,
Abort,
PointerMove,
SetShape,
HideShapeTypeWidget,
IncreaseSides,
DecreaseSides,
NudgeSelectedLayers,
)
}
}
}
}
impl ToolMetadata for ShapeTool {
fn icon_name(&self) -> String {
"VectorPolygonTool".into()
}
fn tooltip_label(&self) -> String {
"Shape Tool".into()
}
fn tool_type(&self) -> ToolType {
ToolType::Shape
}
}
impl ToolTransition for ShapeTool {
fn event_to_message_map(&self) -> EventToMessageMap {
EventToMessageMap {
overlay_provider: Some(|context| ShapeToolMessage::Overlays { context }.into()),
tool_abort: Some(ShapeToolMessage::Abort.into()),
working_color_changed: Some(ShapeToolMessage::WorkingColorChanged.into()),
..Default::default()
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ShapeToolFsmState {
Ready(ShapeType),
Drawing(ShapeType),
// Gizmos
DraggingLineEndpoints,
ModifyingGizmo,
// Transform cage
ResizingBounds,
RotatingBounds,
SkewingBounds { skew: Key },
}
impl Default for ShapeToolFsmState {
fn default() -> Self {
ShapeToolFsmState::Ready(ShapeType::default())
}
}
#[derive(Clone, Debug, Default)]
pub struct ShapeToolData {
pub data: Resize,
auto_panning: AutoPanning,
// In viewport space
pub last_mouse_position: DVec2,
// Hide the dropdown menu when using Line, Rectangle, or Ellipse aliases
pub hide_shape_option_widget: bool,
// Shape-specific data
pub line_data: LineToolData,
// Used for by transform cage
pub bounding_box_manager: Option<BoundingBoxManager>,
layers_dragging: Vec<LayerNodeIdentifier>,
snap_candidates: Vec<SnapCandidatePoint>,
skew_edge: EdgeBool,
cursor: MouseCursorIcon,
// Current shape which is being drawn
current_shape: ShapeType,
// Gizmos
gizmo_manager: GizmoManager,
}
impl ShapeToolData {
fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler, viewport: &ViewportMessageHandler) {
self.snap_candidates.clear();
for &layer in &self.layers_dragging {
if (self.snap_candidates.len() as f64) < document.snapping_state.tolerance {
snapping::get_layer_snap_points(layer, &SnapData::new(document, input, viewport), &mut self.snap_candidates);
}
if let Some(bounds) = document.metadata().bounding_box_with_transform(layer, DAffine2::IDENTITY) {
let quad = document.metadata().transform_to_document(layer) * Quad::from_box(bounds);
snapping::get_bbox_points(quad, &mut self.snap_candidates, snapping::BBoxSnapValues::BOUNDING_BOX, document);
}
}
}
fn transform_cage_mouse_icon(&mut self, input: &InputPreprocessorMessageHandler) -> MouseCursorIcon {
let dragging_bounds = self
.bounding_box_manager
.as_mut()
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
.is_some();
self.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Crosshair, |bounds| {
let cursor_icon = bounds.get_cursor(input, true, dragging_bounds, Some(self.skew_edge));
if cursor_icon == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor_icon }
})
}
fn shape_tool_modifier_keys() -> [Key; 3] {
[Key::Alt, Key::Shift, Key::Control]
}
fn decrease_or_increase_sides(&self, document: &DocumentMessageHandler, shape_type: ShapeType, responses: &mut VecDeque<Message>, decrease: bool) {
if let Some(layer) = self.data.layer {
match shape_type {
ShapeType::Star | ShapeType::Polygon => Polygon::decrease_or_increase_sides(decrease, layer, document, responses),
ShapeType::Spiral => Spiral::update_turns(decrease, layer, document, responses),
_ => {}
}
}
responses.add(NodeGraphMessage::RunDocumentGraph);
}
}
impl Fsm for ShapeToolFsmState {
type ToolData = ShapeToolData;
type ToolOptions = ShapeToolOptions;
fn transition(
self,
event: ToolMessage,
tool_data: &mut Self::ToolData,
ToolActionMessageContext {
document,
global_tool_data,
input,
preferences,
shape_editor,
viewport,
..
}: &mut ToolActionMessageContext,
tool_options: &Self::ToolOptions,
responses: &mut VecDeque<Message>,
) -> Self {
let all_selected_layers_line = 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());
let ToolMessage::Shape(event) = event else { return self };
match (self, event) {
(_, ShapeToolMessage::Overlays { context: mut overlay_context }) => {
let mouse_position = tool_data
.data
.snap_manager
.indicator_pos()
.map(|pos| document.metadata().document_to_viewport.transform_point2(pos))
.unwrap_or(input.mouse.position);
if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) {
tool_data.gizmo_manager.handle_actions(mouse_position, document, responses);
tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
}
if matches!(self, ShapeToolFsmState::ModifyingGizmo) && !input.keyboard.key(Key::Control) {
tool_data.gizmo_manager.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or(MouseCursorIcon::Crosshair);
tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
}
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 {
tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input, viewport), &mut overlay_context);
}
if modifying_transform_cage && !matches!(self, ShapeToolFsmState::ModifyingGizmo) {
transform_cage_overlays(document, tool_data, &mut overlay_context);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: tool_data.cursor });
}
if input.keyboard.key(Key::Control) && matches!(self, ShapeToolFsmState::Ready(_)) {
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);
if all_selected_layers_line {
return self;
}
if !hovering_over_gizmo {
transform_cage_overlays(document, tool_data, &mut overlay_context);
}
let dragging_bounds = tool_data
.bounding_box_manager
.as_mut()
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
.is_some();
if let Some(bounds) = tool_data.bounding_box_manager.as_mut() {
let edges = bounds.check_selected_edges(input.mouse.position);
let is_skewing = matches!(self, ShapeToolFsmState::SkewingBounds { .. });
let is_near_square = edges.is_some_and(|hover_edge| bounds.over_extended_edge_midpoint(input.mouse.position, hover_edge));
if is_skewing || (dragging_bounds && is_near_square && !hovering_over_gizmo) {
bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge);
}
if dragging_bounds
&& !is_skewing && !hovering_over_gizmo
&& let Some(edges) = edges
{
tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position);
}
}
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().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);
if tool_options.shape_type == ShapeType::Circle {
tool_data.gizmo_manager.overlays(document, input, shape_editor, mouse_position, &mut overlay_context);
}
}
self
}
(ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => {
if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) {
responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Vertices(tool_options.vertices + 1),
});
}
if matches!(tool_options.shape_type, ShapeType::Spiral) {
responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Turns(tool_options.turns + 1.),
});
}
self
}
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => {
if matches!(tool_options.shape_type, ShapeType::Star | ShapeType::Polygon) {
responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)),
});
}
if matches!(tool_options.shape_type, ShapeType::Spiral) {
responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::Turns((tool_options.turns - 1.).max(1.)),
});
}
self
}
(
ShapeToolFsmState::Ready(_),
ShapeToolMessage::NudgeSelectedLayers {
delta_x,
delta_y,
resize,
resize_opposite_corner,
},
) => {
responses.add(DocumentMessage::NudgeSelectedLayers {
delta_x,
delta_y,
resize,
resize_opposite_corner,
});
self
}
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::NudgeSelectedLayers { .. }) => {
let increase = input.keyboard.key(Key::ArrowUp);
let decrease = input.keyboard.key(Key::ArrowDown);
if increase {
responses.add(ShapeToolMessage::IncreaseSides);
return self;
}
if decrease {
responses.add(ShapeToolMessage::DecreaseSides);
return self;
}
self
}
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::IncreaseSides) => {
tool_data.decrease_or_increase_sides(document, tool_options.shape_type, responses, false);
self
}
(ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => {
tool_data.decrease_or_increase_sides(document, tool_options.shape_type, responses, true);
self
}
(ShapeToolFsmState::Ready(_), ShapeToolMessage::DragStart) => {
tool_data.line_data.drag_start = input.mouse.position;
// Snapped position in viewport space
let mouse_pos = tool_data
.data
.snap_manager
.indicator_pos()
.map(|pos| document.metadata().document_to_viewport.transform_point2(pos))
.unwrap_or(input.mouse.position);
tool_data.line_data.drag_current = mouse_pos;
if tool_data.gizmo_manager.handle_click() && !input.keyboard.key(Key::Accel) {
tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos);
responses.add(DocumentMessage::StartTransaction);
let cursor = tool_data.gizmo_manager.mouse_cursor_icon().unwrap_or(MouseCursorIcon::Crosshair);
tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
// Send a PointerMove message to refresh the cursor icon
responses.add(ShapeToolMessage::PointerMove {
modifier: ShapeToolData::shape_tool_modifier_keys(),
});
responses.add(DocumentMessage::StartTransaction);
return ShapeToolFsmState::ModifyingGizmo;
}
// If clicked on endpoints of a selected line, 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,
preferences,
) && clicked_on_line_endpoints(layer, document, input, tool_data)
&& !input.keyboard.key(Key::Control)
{
return ShapeToolFsmState::DraggingLineEndpoints;
}
let (resize, rotate, skew) = transforming_transform_cage(document, &mut tool_data.bounding_box_manager, input, responses, &mut tool_data.layers_dragging, None);
if !input.keyboard.key(Key::Control) {
// Helper function to update cursor and send pointer move message
let update_cursor_and_pointer = |tool_data: &mut ShapeToolData, responses: &mut VecDeque<Message>| {
let cursor = tool_data.transform_cage_mouse_icon(input);
tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
responses.add(ShapeToolMessage::PointerMove {
modifier: ShapeToolData::shape_tool_modifier_keys(),
});
};
match (resize, rotate, skew) {
(true, false, false) => {
tool_data.get_snap_candidates(document, input, viewport);
update_cursor_and_pointer(tool_data, responses);
return ShapeToolFsmState::ResizingBounds;
}
(false, true, false) => {
tool_data.data.drag_start = mouse_pos;
update_cursor_and_pointer(tool_data, responses);
return ShapeToolFsmState::RotatingBounds;
}
(false, false, true) => {
tool_data.get_snap_candidates(document, input, viewport);
update_cursor_and_pointer(tool_data, responses);
return ShapeToolFsmState::SkewingBounds { skew: Key::Control };
}
_ => {}
}
};
match tool_data.current_shape {
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => {
tool_data.data.start(document, input, viewport);
}
ShapeType::Line => {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data
.data
.snap_manager
.free_snap(&SnapData::new(document, input, viewport), &point, SnapTypeConfiguration::default());
tool_data.data.drag_start = snapped.snapped_point_document;
}
}
responses.add(DocumentMessage::StartTransaction);
let node = match tool_data.current_shape {
ShapeType::Polygon => Polygon::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::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::Line => Line::create_node(document, tool_data.data.drag_start),
};
let nodes = vec![(NodeId(0), node)];
let layer = graph_modification_utils::new_custom(NodeId::new(), nodes, document.new_layer_bounding_artboard(input, viewport), responses);
let defered_responses = &mut VecDeque::new();
match tool_data.current_shape {
ShapeType::Polygon | ShapeType::Star | ShapeType::Circle | ShapeType::Arc | ShapeType::Spiral | ShapeType::Grid | ShapeType::Rectangle | ShapeType::Ellipse => {
defered_responses.add(GraphOperationMessage::TransformSet {
layer,
transform: DAffine2::from_scale_angle_translation(DVec2::ONE, 0., input.mouse.position),
transform_in: TransformIn::Viewport,
skip_rerender: false,
});
tool_options.fill.apply_fill(layer, defered_responses);
}
ShapeType::Line => {
tool_data.line_data.weight = tool_options.line_weight;
tool_data.line_data.editing_layer = Some(layer);
}
}
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
tool_options.stroke.apply_stroke(tool_options.line_weight, layer, defered_responses);
tool_data.data.layer = Some(layer);
responses.add(DeferMessage::AfterGraphRun {
messages: defered_responses.drain(..).collect(),
});
responses.add(NodeGraphMessage::RunDocumentGraph);
ShapeToolFsmState::Drawing(tool_data.current_shape)
}
(ShapeToolFsmState::Drawing(shape), ShapeToolMessage::PointerMove { modifier }) => {
let Some(layer) = tool_data.data.layer else {
return ShapeToolFsmState::Ready(shape);
};
match tool_data.current_shape {
ShapeType::Polygon => Polygon::update_shape(document, input, viewport, layer, tool_data, modifier, responses),
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::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::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
let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
tool_data.auto_panning.setup_by_mouse_position(input, viewport, &messages, responses);
self
}
(ShapeToolFsmState::DraggingLineEndpoints, ShapeToolMessage::PointerMove { modifier }) => {
let Some(layer) = tool_data.line_data.editing_layer else {
return ShapeToolFsmState::Ready(tool_data.current_shape);
};
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);
self
}
(ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove { .. }) => {
tool_data.gizmo_manager.handle_update(tool_data.data.viewport_drag_start(document), document, input, responses);
responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::ModifyingGizmo
}
(ShapeToolFsmState::ResizingBounds, ShapeToolMessage::PointerMove { modifier }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
let messages = [ShapeToolMessage::PointerOutsideViewport { modifier }.into(), ShapeToolMessage::PointerMove { modifier }.into()];
resize_bounds(
document,
responses,
bounds,
&mut tool_data.layers_dragging,
&mut tool_data.data.snap_manager,
&mut tool_data.snap_candidates,
input,
viewport,
input.keyboard.key(modifier[0]),
input.keyboard.key(modifier[1]),
ToolType::Shape,
);
tool_data.auto_panning.setup_by_mouse_position(input, viewport, &messages, responses);
}
responses.add(OverlaysMessage::Draw);
ShapeToolFsmState::ResizingBounds
}
(ShapeToolFsmState::RotatingBounds, ShapeToolMessage::PointerMove { modifier }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
rotate_bounds(
document,
responses,
bounds,
&mut tool_data.layers_dragging,
tool_data.data.drag_start,
input.mouse.position,
input.keyboard.key(modifier[1]),
ToolType::Shape,
);
}
ShapeToolFsmState::RotatingBounds
}
(ShapeToolFsmState::SkewingBounds { skew }, ShapeToolMessage::PointerMove { .. }) => {
if let Some(bounds) = &mut tool_data.bounding_box_manager {
skew_bounds(
document,
responses,
bounds,
input.keyboard.key(skew),
&mut tool_data.layers_dragging,
input.mouse.position,
ToolType::Shape,
);
}
ShapeToolFsmState::SkewingBounds { skew }
}
(_, ShapeToolMessage::PointerMove { .. }) => {
let dragging_bounds = tool_data
.bounding_box_manager
.as_mut()
.and_then(|bounding_box| bounding_box.check_selected_edges(input.mouse.position))
.is_some();
let cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Crosshair, |bounds| {
let cursor = bounds.get_cursor(input, true, dragging_bounds, Some(tool_data.skew_edge));
if cursor == MouseCursorIcon::Default { MouseCursorIcon::Crosshair } else { cursor }
});
if tool_data.cursor != cursor {
tool_data.cursor = cursor;
responses.add(FrontendMessage::UpdateMouseCursor { cursor });
}
tool_data.data.snap_manager.preview_draw(&SnapData::new(document, input, viewport), input.mouse.position);
responses.add(OverlaysMessage::Draw);
self
}
(ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. }, ShapeToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, viewport, responses)
&& let Some(bounds) = &mut tool_data.bounding_box_manager
{
bounds.center_of_transformation += shift;
bounds.original_bound_transform.translation += shift;
}
self
}
(ShapeToolFsmState::Ready(_), ShapeToolMessage::PointerOutsideViewport { .. }) => self,
(_, ShapeToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, viewport, responses);
self
}
(
ShapeToolFsmState::Drawing(_)
| ShapeToolFsmState::DraggingLineEndpoints
| ShapeToolFsmState::ResizingBounds
| ShapeToolFsmState::RotatingBounds
| ShapeToolFsmState::SkewingBounds { .. }
| ShapeToolFsmState::ModifyingGizmo,
ShapeToolMessage::DragStop,
) => {
input.mouse.finish_transaction(tool_data.data.drag_start, responses);
tool_data.data.cleanup(responses);
tool_data.gizmo_manager.handle_cleanup();
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
tool_data.line_data.dragging_endpoint = None;
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
ShapeToolFsmState::Ready(tool_data.current_shape)
}
(
ShapeToolFsmState::Drawing(_)
| ShapeToolFsmState::DraggingLineEndpoints
| ShapeToolFsmState::ResizingBounds
| ShapeToolFsmState::RotatingBounds
| ShapeToolFsmState::SkewingBounds { .. }
| ShapeToolFsmState::ModifyingGizmo,
ShapeToolMessage::Abort,
) => {
responses.add(DocumentMessage::AbortTransaction);
tool_data.data.cleanup(responses);
tool_data.line_data.dragging_endpoint = None;
tool_data.gizmo_manager.handle_cleanup();
if let Some(bounds) = &mut tool_data.bounding_box_manager {
bounds.original_transforms.clear();
}
tool_data.cursor = MouseCursorIcon::Crosshair;
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair });
ShapeToolFsmState::Ready(tool_data.current_shape)
}
(_, ShapeToolMessage::WorkingColorChanged) => {
responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::WorkingColors(Some(global_tool_data.primary_color), Some(global_tool_data.secondary_color)),
});
self
}
(_, ShapeToolMessage::SetShape { shape }) => {
responses.add(DocumentMessage::AbortTransaction);
tool_data.data.cleanup(responses);
tool_data.current_shape = shape;
responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(shape),
});
responses.add(ShapeToolMessage::UpdateOptions {
options: ShapeOptionsUpdate::ShapeType(shape),
});
ShapeToolFsmState::Ready(shape)
}
(_, ShapeToolMessage::HideShapeTypeWidget { hide }) => {
tool_data.hide_shape_option_widget = hide;
responses.add(ToolMessage::RefreshToolOptions);
self
}
_ => self,
}
}
fn update_hints(&self, _responses: &mut VecDeque<Message>) {
// Moved logic to update_dynamic_hints
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
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::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(),
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(),
])],
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(),
])],
};
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::Grid => HintGroup(vec![HintInfo::keys([Key::Shift], "Constrain Regular"), 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")]),
ShapeType::Spiral => HintGroup(vec![]),
};
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")]));
}
if matches!(shape, ShapeType::Spiral) {
common_hint_group.push(HintGroup(vec![HintInfo::multi_keys([[Key::BracketLeft], [Key::BracketRight]], "Decrease/Increase Turns")]));
}
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()])]),
};
hint_data.send_layout(responses);
}