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::portfolio::document::utility_types::network_interface::InputConnector; 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::graph_modification_utils::NodeGraphLayer; use crate::messages::tool::common_functionality::resize::Resize; 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::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::value::TaggedValue; use graph_craft::document::{NodeId, NodeInput}; use graphene_std::Color; use graphene_std::renderer::Quad; use graphene_std::vector::misc::ArcType; use std::vec; #[derive(Default)] 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, } 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, } } } #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum ShapeOptionsUpdate { FillColor(Option), FillColorType(ToolColorType), LineWeight(f64), StrokeColor(Option), StrokeColorType(ToolColorType), WorkingColors(Option, Option), Vertices(u32), ShapeType(ShapeType), ArcType(ArcType), } #[impl_message(Message, ToolMessage, Shape)] #[derive(PartialEq, Clone, Debug, serde::Serialize, serde::Deserialize, specta::Type)] pub enum ShapeToolMessage { // Standard messages Overlays(OverlayContext), Abort, WorkingColorChanged, // Tool-specific messages DragStart, DragStop, HideShapeTypeWidget(bool), PointerMove(ShapeToolModifierKey), PointerOutsideViewport(ShapeToolModifierKey), UpdateOptions(ShapeOptionsUpdate), SetShape(ShapeType), IncreaseSides, DecreaseSides, NudgeSelectedLayers { delta_x: f64, delta_y: f64, resize: Key, resize_opposite_corner: Key }, } fn create_sides_widget(vertices: u32) -> WidgetHolder { NumberInput::new(Some(vertices as f64)) .label("Sides") .int() .min(3.) .max(1000.) .mode(NumberInputMode::Increment) .on_update(|number_input: &NumberInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(number_input.value.unwrap() as u32)).into()) .widget_holder() } fn create_shape_option_widget(shape_type: ShapeType) -> WidgetHolder { let entries = vec![vec![ MenuListEntry::new("Polygon") .label("Polygon") .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Polygon)).into()), MenuListEntry::new("Star") .label("Star") .on_commit(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::ShapeType(ShapeType::Star)).into()), ]]; DropdownInput::new(entries).selected_index(Some(shape_type as u32)).widget_holder() } fn create_weight_widget(line_weight: f64) -> WidgetHolder { 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(ShapeOptionsUpdate::LineWeight(number_input.value.unwrap())).into()) .widget_holder() } 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_holder()); 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_holder()); } } if self.options.shape_type != ShapeType::Line { widgets.append(&mut self.options.fill.create_widgets( "Fill", true, |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColorType(color_type.clone())).into()), |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::FillColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); } widgets.append(&mut self.options.stroke.create_widgets( "Stroke", true, |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(None)).into(), |color_type: ToolColorType| WidgetCallback::new(move |_| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColorType(color_type.clone())).into()), |color: &ColorInput| ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::StrokeColor(color.value.as_solid().map(|color| color.to_linear_srgb()))).into(), )); widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(create_weight_widget(self.options.line_weight)); Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } impl<'a> MessageHandler> for ShapeTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { let ToolMessage::Shape(ShapeToolMessage::UpdateOptions(action)) = message else { self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &self.options, responses, true); return; }; match action { 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; } } self.fsm_state.update_hints(responses); 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(&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(|overlay_context| ShapeToolMessage::Overlays(overlay_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, layers_dragging: Vec, snap_candidates: Vec, skew_edge: EdgeBool, cursor: MouseCursorIcon, // Current shape which is being drawn current_shape: ShapeType, // Gizmos gizmo_manger: GizmoManager, } impl ShapeToolData { fn get_snap_candidates(&mut self, document: &DocumentMessageHandler, input: &InputPreprocessorMessageHandler) { 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), &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); } } } } impl Fsm for ShapeToolFsmState { type ToolData = ShapeToolData; type ToolOptions = ShapeToolOptions; fn transition( self, event: ToolMessage, tool_data: &mut Self::ToolData, ToolActionHandlerData { document, global_tool_data, input, preferences, shape_editor, .. }: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque, ) -> 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(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); let is_resizing_or_rotating = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::SkewingBounds { .. } | ShapeToolFsmState::RotatingBounds); if matches!(self, Self::Ready(_)) && !input.keyboard.key(Key::Control) { tool_data.gizmo_manger.handle_actions(mouse_position, document, responses); tool_data.gizmo_manger.overlays(document, input, shape_editor, mouse_position, &mut overlay_context); } if matches!(self, ShapeToolFsmState::ModifyingGizmo) && !input.keyboard.key(Key::Control) { tool_data.gizmo_manger.dragging_overlays(document, input, shape_editor, mouse_position, &mut overlay_context); } let modifying_transform_cage = matches!(self, ShapeToolFsmState::ResizingBounds | ShapeToolFsmState::RotatingBounds | ShapeToolFsmState::SkewingBounds { .. }); let hovering_over_gizmo = tool_data.gizmo_manger.hovering_over_gizmo(); if !is_resizing_or_rotating && !matches!(self, ShapeToolFsmState::ModifyingGizmo) && !modifying_transform_cage && !hovering_over_gizmo { tool_data.data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); } if modifying_transform_cage && !matches!(self, ShapeToolFsmState::ModifyingGizmo) { transform_cage_overlays(document, tool_data, &mut overlay_context); } if input.keyboard.key(Key::Control) && matches!(self, ShapeToolFsmState::Ready(_)) { anchor_overlays(document, &mut overlay_context); } 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 && !is_resizing_or_rotating && !hovering_over_gizmo) { bounds.render_skew_gizmos(&mut overlay_context, tool_data.skew_edge); } if !is_skewing && dragging_bounds && !hovering_over_gizmo { if let Some(edges) = edges { tool_data.skew_edge = bounds.get_closest_edge(edges, input.mouse.position); } } } } if matches!(self, ShapeToolFsmState::Drawing(_) | ShapeToolFsmState::DraggingLineEndpoints) { Line::overlays(document, tool_data, &mut overlay_context); } self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::IncreaseSides) => { responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(tool_options.vertices + 1))); self } (ShapeToolFsmState::Ready(_), ShapeToolMessage::DecreaseSides) => { responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((tool_options.vertices - 1).max(3)))); 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) => { if let Some(layer) = tool_data.data.layer { let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) else { return self; }; let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) .find_node_inputs("Regular Polygon") .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) else { return self; }; let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { return self; }; responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices(n + 1))); responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 1), input: NodeInput::value(TaggedValue::U32(n + 1), false), }); responses.add(NodeGraphMessage::RunDocumentGraph); } self } (ShapeToolFsmState::Drawing(_), ShapeToolMessage::DecreaseSides) => { if let Some(layer) = tool_data.data.layer { let Some(node_id) = graph_modification_utils::get_polygon_id(layer, &document.network_interface).or(graph_modification_utils::get_star_id(layer, &document.network_interface)) else { return self; }; let Some(node_inputs) = NodeGraphLayer::new(layer, &document.network_interface) .find_node_inputs("Regular Polygon") .or(NodeGraphLayer::new(layer, &document.network_interface).find_node_inputs("Star")) else { return self; }; let Some(&TaggedValue::U32(n)) = node_inputs.get(1).unwrap().as_value() else { return self; }; responses.add(ShapeToolMessage::UpdateOptions(ShapeOptionsUpdate::Vertices((n - 1).max(3)))); responses.add(NodeGraphMessage::SetInput { input_connector: InputConnector::node(node_id, 1), input: NodeInput::value(TaggedValue::U32((n - 1).max(3)), false), }); responses.add(NodeGraphMessage::RunDocumentGraph); } 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_manger.handle_click() { tool_data.data.drag_start = document.metadata().document_to_viewport.inverse().transform_point2(mouse_pos); 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, ) { if 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); if !input.keyboard.key(Key::Control) { match (resize, rotate, skew) { (true, false, false) => { tool_data.get_snap_candidates(document, input); return ShapeToolFsmState::ResizingBounds; } (false, true, false) => { tool_data.data.drag_start = mouse_pos; return ShapeToolFsmState::RotatingBounds; } (false, false, true) => { tool_data.get_snap_candidates(document, input); return ShapeToolFsmState::SkewingBounds { skew: Key::Control }; } _ => {} } }; match tool_data.current_shape { ShapeType::Polygon | ShapeType::Star | ShapeType::Ellipse | ShapeType::Rectangle => tool_data.data.start(document, input), ShapeType::Line => { let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let snapped = tool_data.data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); 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::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), responses); responses.add(Message::StartBuffer); match tool_data.current_shape { ShapeType::Ellipse | ShapeType::Rectangle | ShapeType::Polygon | ShapeType::Star => { 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, 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, responses); tool_options.stroke.apply_stroke(tool_options.line_weight, layer, responses); tool_data.data.layer = Some(layer); 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::Rectangle => Rectangle::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::Polygon => Polygon::update_shape(document, input, layer, tool_data, modifier, responses), ShapeType::Star => Star::update_shape(document, input, 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, &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, 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, &messages, responses); self } (ShapeToolFsmState::ModifyingGizmo, ShapeToolMessage::PointerMove(..)) => { responses.add(DocumentMessage::StartTransaction); tool_data.gizmo_manger.handle_update(tool_data.data.drag_start, 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, input.keyboard.key(modifier[0]), input.keyboard.key(modifier[1]), ToolType::Shape, ); tool_data.auto_panning.setup_by_mouse_position(input, &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 && !input.keyboard.key(Key::Control) && !all_selected_layers_line { tool_data.cursor = cursor; responses.add(FrontendMessage::UpdateMouseCursor { cursor }); } tool_data.data.snap_manager.preview_draw(&SnapData::new(document, input), 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, responses) { if 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, 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_manger.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_manger.handle_cleanup(); if let Some(bounds) = &mut tool_data.bounding_box_manager { bounds.original_transforms.clear(); } responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); ShapeToolFsmState::Ready(tool_data.current_shape) } (_, ShapeToolMessage::WorkingColorChanged) => { responses.add(ShapeToolMessage::UpdateOptions(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; 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) { let hint_data = match self { ShapeToolFsmState::Ready(shape) => { let hint_groups = match 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(), ])], }; 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 => 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) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Crosshair }); } }