#![allow(clippy::too_many_arguments)] use super::tool_prelude::*; use crate::application::generate_uuid; use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; 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::misc::{AlignAggregate, AlignAxis, FlipAxis}; use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::tool::common_functionality::graph_modification_utils::is_layer_fed_by_node_of_name; use crate::messages::tool::common_functionality::pivot::Pivot; use crate::messages::tool::common_functionality::snapping::{self, SnapCandidatePoint, SnapConstraint, SnapData, SnapManager, SnappedPoint}; use crate::messages::tool::common_functionality::transformation_cage::*; use graph_craft::document::{NodeId, NodeNetwork}; use graphene_core::renderer::Quad; use std::fmt; #[derive(Default)] pub struct SelectTool { fsm_state: SelectToolFsmState, tool_data: SelectToolData, } #[allow(dead_code)] #[derive(Default)] pub struct SelectOptions { nested_selection_behavior: NestedSelectionBehavior, } #[remain::sorted] #[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize, specta::Type)] pub enum SelectOptionsUpdate { NestedSelectionBehavior(NestedSelectionBehavior), } #[derive(Default, PartialEq, Eq, Clone, Copy, Debug, Hash, Serialize, Deserialize, specta::Type)] pub enum NestedSelectionBehavior { #[default] Deepest, Shallowest, } impl fmt::Display for NestedSelectionBehavior { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { NestedSelectionBehavior::Deepest => write!(f, "Deep Select"), NestedSelectionBehavior::Shallowest => write!(f, "Shallow Select"), } } } #[remain::sorted] #[impl_message(Message, ToolMessage, Select)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] pub enum SelectToolMessage { // Standard messages #[remain::unsorted] Abort, #[remain::unsorted] Overlays(OverlayContext), // Tool-specific messages DragStart { add_to_selection: Key, select_deepest: Key, }, DragStop { remove_from_selection: Key, }, EditLayer, Enter, PointerMove { axis_align: Key, snap_angle: Key, center: Key, duplicate: Key, }, SelectOptions(SelectOptionsUpdate), SetPivot { position: PivotPosition, }, } impl ToolMetadata for SelectTool { fn icon_name(&self) -> String { "GeneralSelectTool".into() } fn tooltip(&self) -> String { "Select Tool".into() } fn tool_type(&self) -> crate::messages::tool::utility_types::ToolType { ToolType::Select } } impl SelectTool { fn deep_selection_widget(&self) -> WidgetHolder { let layer_selection_behavior_entries = [NestedSelectionBehavior::Deepest, NestedSelectionBehavior::Shallowest] .iter() .map(|mode| { MenuListEntry::new(mode.to_string()) .value(mode.to_string()) .on_update(move |_| SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(*mode)).into()) }) .collect(); DropdownInput::new(vec![layer_selection_behavior_entries]) .selected_index(Some((self.tool_data.nested_selection_behavior == NestedSelectionBehavior::Shallowest) as u32)) .tooltip("Choose if clicking nested layers directly selects the deepest, or selects the shallowest and deepens by double clicking") .widget_holder() } fn pivot_widget(&self, disabled: bool) -> WidgetHolder { PivotInput::new(self.tool_data.pivot.to_pivot_position()) .on_update(|pivot_input: &PivotInput| SelectToolMessage::SetPivot { position: pivot_input.position }.into()) .disabled(disabled) .widget_holder() } fn alignment_widgets(&self, disabled: bool) -> impl Iterator { [AlignAxis::X, AlignAxis::Y] .into_iter() .flat_map(|axis| [(axis, AlignAggregate::Min), (axis, AlignAggregate::Center), (axis, AlignAggregate::Max)]) .map(move |(axis, aggregate)| { let (icon, tooltip) = match (axis, aggregate) { (AlignAxis::X, AlignAggregate::Min) => ("AlignLeft", "Align Left"), (AlignAxis::X, AlignAggregate::Center) => ("AlignHorizontalCenter", "Align Horizontal Center"), (AlignAxis::X, AlignAggregate::Max) => ("AlignRight", "Align Right"), (AlignAxis::Y, AlignAggregate::Min) => ("AlignTop", "Align Top"), (AlignAxis::Y, AlignAggregate::Center) => ("AlignVerticalCenter", "Align Vertical Center"), (AlignAxis::Y, AlignAggregate::Max) => ("AlignBottom", "Align Bottom"), }; IconButton::new(icon, 24) .tooltip(tooltip) .on_update(move |_| DocumentMessage::AlignSelectedLayers { axis, aggregate }.into()) .disabled(disabled) .widget_holder() }) } fn flip_widgets(&self, disabled: bool) -> impl Iterator { [(FlipAxis::X, "Horizontal"), (FlipAxis::Y, "Vertical")].into_iter().map(move |(flip_axis, name)| { IconButton::new("Flip".to_string() + name, 24) .tooltip("Flip ".to_string() + name) .on_update(move |_| DocumentMessage::FlipSelectedLayers { flip_axis }.into()) .disabled(disabled) .widget_holder() }) } fn boolean_widgets(&self) -> impl Iterator { ["Union", "Subtract Front", "Subtract Back", "Intersect", "Difference"].into_iter().map(|name| { IconButton::new(format!("Boolean{}", name.replace(' ', "")), 24) .tooltip(format!("Boolean {name} (coming soon)")) .on_update(|_| DialogMessage::RequestComingSoonDialog { issue: Some(1091) }.into()) .widget_holder() }) } } impl LayoutHolder for SelectTool { fn layout(&self) -> Layout { let mut widgets = Vec::new(); // Select mode (Deep/Shallow) widgets.push(self.deep_selection_widget()); // Pivot widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.push(self.pivot_widget(self.tool_data.selected_layers_count == 0)); // Align let disabled = self.tool_data.selected_layers_count < 2; widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.extend(self.alignment_widgets(disabled)); widgets.push(PopoverButton::new("Align", "Coming soon").disabled(disabled).widget_holder()); // Flip let disabled = self.tool_data.selected_layers_count == 0; widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.extend(self.flip_widgets(disabled)); // Boolean if self.tool_data.selected_layers_count >= 2 { widgets.push(Separator::new(SeparatorType::Unrelated).widget_holder()); widgets.extend(self.boolean_widgets()); } Layout::WidgetLayout(WidgetLayout::new(vec![LayoutGroup::Row { widgets }])) } } impl<'a> MessageHandler> for SelectTool { fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque, tool_data: &mut ToolActionHandlerData<'a>) { if let ToolMessage::Select(SelectToolMessage::SelectOptions(SelectOptionsUpdate::NestedSelectionBehavior(nested_selection_behavior))) = message { self.tool_data.nested_selection_behavior = nested_selection_behavior; responses.add(ToolMessage::UpdateHints); } self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, false); if self.tool_data.pivot.should_refresh_pivot_position() || self.tool_data.selected_layers_changed { // Send the layout containing the updated pivot position (a bit ugly to do it here not in the fsm but that doesn't have SelectTool) self.send_layout(responses, LayoutTarget::ToolOptions); self.tool_data.selected_layers_changed = false; } } fn actions(&self) -> ActionList { use SelectToolFsmState::*; match self.fsm_state { Ready => actions!(SelectToolMessageDiscriminant; DragStart, PointerMove, Abort, EditLayer, Enter, ), _ => actions!(SelectToolMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer, Enter, ), } } } impl ToolTransition for SelectTool { fn event_to_message_map(&self) -> EventToMessageMap { EventToMessageMap { tool_abort: Some(SelectToolMessage::Abort.into()), overlay_provider: Some(|overlay_context| SelectToolMessage::Overlays(overlay_context).into()), ..Default::default() } } } #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)] enum SelectToolFsmState { #[default] Ready, Dragging, DrawingBox, ResizingBounds, RotatingBounds, DraggingPivot, } #[derive(Clone, Debug, Default)] struct SelectToolData { drag_start: ViewportPosition, drag_current: ViewportPosition, layers_dragging: Vec, layer_selected_on_start: Option, select_single_layer: Option, has_dragged: bool, non_duplicated_layers: Option>, bounding_box_manager: Option, snap_manager: SnapManager, cursor: MouseCursorIcon, pivot: Pivot, nested_selection_behavior: NestedSelectionBehavior, selected_layers_count: usize, selected_layers_changed: bool, snap_candidates: Vec, } impl SelectToolData { 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); } } } fn selection_quad(&self) -> Quad { let bbox = self.selection_box(); Quad::from_box(bbox) } fn selection_box(&self) -> [DVec2; 2] { if self.drag_current == self.drag_start { let tolerance = DVec2::splat(SELECTION_TOLERANCE); [self.drag_start - tolerance, self.drag_start + tolerance] } else { [self.drag_start, self.drag_current] } } /// Duplicates the currently dragging layers. Called when Alt is pressed and the layers have not yet been duplicated. fn start_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { self.non_duplicated_layers = Some(self.layers_dragging.clone()); let mut new_dragging = Vec::new(); for layer_ancestors in document.metadata().shallowest_unique_layers(self.layers_dragging.iter().copied().rev()) { let Some(layer) = layer_ancestors.last().copied() else { continue }; let Some(parent) = layer.parent(&document.metadata) else { continue }; // Copy the layer let node = layer.to_node(); let Some(node) = document.network().nodes.get(&node).and_then(|node| node.inputs.first()).and_then(|input| input.as_node()) else { continue; }; let nodes = NodeGraphMessageHandler::copy_nodes( document.network(), &document .network() .upstream_flow_back_from_nodes(vec![node], false) .enumerate() .map(|(index, (_, node_id))| (node_id, NodeId(index as u64))) .collect(), ) .collect(); // Moves the layer back to its starting position. responses.add(GraphOperationMessage::TransformChange { layer, transform: DAffine2::from_translation(self.drag_start - self.drag_current), transform_in: TransformIn::Viewport, skip_rerender: true, }); let id = NodeId(generate_uuid()); let insert_index = -1; let layer = LayerNodeIdentifier::new_unchecked(id); responses.add(GraphOperationMessage::NewCustomLayer { id, nodes, parent, insert_index }); new_dragging.push(layer); } let nodes = new_dragging.iter().map(|layer| layer.to_node()).collect(); responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); self.layers_dragging = new_dragging; } /// Removes the duplicated layers. Called when Alt is released and the layers have previously been duplicated. fn stop_duplicates(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { let Some(original) = self.non_duplicated_layers.take() else { return; }; // Delete the duplicated layers for layer_ancestors in document.metadata().shallowest_unique_layers(self.layers_dragging.iter().copied()) { responses.add(GraphOperationMessage::DeleteLayer { id: layer_ancestors.last().unwrap().to_node(), }); } for &layer in &original { responses.add(GraphOperationMessage::TransformChange { layer, transform: DAffine2::from_translation(self.drag_current - self.drag_start), transform_in: TransformIn::Viewport, skip_rerender: true, }); } let nodes = original.iter().map(|layer| layer.to_node()).collect(); responses.add(NodeGraphMessage::SelectedNodesSet { nodes }); self.layers_dragging = original; } } impl Fsm for SelectToolFsmState { type ToolData = SelectToolData; type ToolOptions = (); fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, _tool_options: &(), responses: &mut VecDeque) -> Self { let ToolActionHandlerData { document, input, .. } = tool_action_data; let ToolMessage::Select(event) = event else { return self; }; match (self, event) { (_, SelectToolMessage::Overlays(mut overlay_context)) => { tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context); let selected_layers_count = document.selected_nodes.selected_layers(document.metadata()).count(); tool_data.selected_layers_changed = selected_layers_count != tool_data.selected_layers_count; tool_data.selected_layers_count = selected_layers_count; // Outline selected layers for layer in document.selected_nodes.selected_visible_layers(document.network(), document.metadata()) { overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); } // Get the layer the user is hovering over let click = document.click(input.mouse.position, &document.network); let not_selected_click = click.filter(|&hovered_layer| !document.selected_nodes.selected_layers_contains(hovered_layer, document.metadata())); if let Some(layer) = not_selected_click { overlay_context.outline(document.metadata().layer_outline(layer), document.metadata().transform_to_viewport(layer)); } // Update bounds let transform = document .selected_nodes .selected_visible_layers(document.network(), document.metadata()) .next() .map(|layer| document.metadata().transform_to_viewport(layer)); let transform = transform.unwrap_or(DAffine2::IDENTITY); let bounds = document .selected_nodes .selected_visible_layers(document.network(), document.metadata()) .filter_map(|layer| { document .metadata() .bounding_box_with_transform(layer, transform.inverse() * document.metadata().transform_to_viewport(layer)) }) .reduce(graphene_core::renderer::Quad::combine_bounds); if let Some(bounds) = bounds { let bounding_box_manager = tool_data.bounding_box_manager.get_or_insert(BoundingBoxManager::default()); bounding_box_manager.bounds = bounds; bounding_box_manager.transform = transform; bounding_box_manager.render_overlays(&mut overlay_context); } else { tool_data.bounding_box_manager.take(); } // Update pivot tool_data.pivot.update_pivot(document, &mut overlay_context); // Update dragging box if self == Self::DrawingBox { overlay_context.quad(Quad::from_box([tool_data.drag_start, tool_data.drag_current])); } self } (_, SelectToolMessage::EditLayer) => { // Edit the clicked layer if let Some(intersect) = document.click(input.mouse.position, &document.network) { match tool_data.nested_selection_behavior { NestedSelectionBehavior::Shallowest => edit_layer_shallowest_manipulation(document, intersect, responses), NestedSelectionBehavior::Deepest => edit_layer_deepest_manipulation(intersect, &document.network, responses), } } self } (SelectToolFsmState::Ready, SelectToolMessage::DragStart { add_to_selection, select_deepest }) => { tool_data.drag_start = input.mouse.position; tool_data.drag_current = input.mouse.position; let dragging_bounds = tool_data.bounding_box_manager.as_mut().and_then(|bounding_box| { let edges = bounding_box.check_selected_edges(input.mouse.position); bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| { let selected_edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds); bounding_box.opposite_pivot = selected_edges.calculate_pivot(); selected_edges }); edges }); let rotating_bounds = tool_data .bounding_box_manager .as_ref() .map(|bounding_box| bounding_box.check_rotate(input.mouse.position)) .unwrap_or_default(); let mut selected: Vec<_> = document.selected_nodes.selected_visible_layers(document.network(), document.metadata()).collect(); let intersection = document.click(input.mouse.position, &document.network); // If the user is dragging the bounding box bounds, go into ResizingBounds mode. // If the user is dragging the rotate trigger, go into RotatingBounds mode. // If the user clicks on a layer that is in their current selection, go into the dragging mode. // If the user clicks on new shape, make that layer their new selection. // Otherwise enter the box select mode let state = // Dragging the pivot if tool_data.pivot.is_over(input.mouse.position) { responses.add(DocumentMessage::StartTransaction); //tool_data.snap_manager.start_snap(document, input, document.bounding_boxes(), true, true); //tool_data.snap_manager.add_all_document_handles(document, input, &[], &[], &[]); SelectToolFsmState::DraggingPivot } // Dragging one (or two, forming a corner) of the transform cage bounding box edges else if let Some(_selected_edges) = dragging_bounds { responses.add(DocumentMessage::StartTransaction); tool_data.layers_dragging = selected; if let Some(bounds) = &mut tool_data.bounding_box_manager { bounds.original_bound_transform = bounds.transform; tool_data.layers_dragging.retain(|layer| document.network.nodes.contains_key(&layer.to_node())); let mut selected = Selected::new( &mut bounds.original_transforms, &mut bounds.center_of_transformation, &tool_data.layers_dragging, responses, &document.network, &document.metadata, None, &ToolType::Select, ); bounds.center_of_transformation = selected.mean_average_of_pivots(); } tool_data.get_snap_candidates(document, input); SelectToolFsmState::ResizingBounds } // Dragging near the transform cage bounding box to rotate it else if rotating_bounds { responses.add(DocumentMessage::StartTransaction); if let Some(bounds) = &mut tool_data.bounding_box_manager { tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node())); let mut selected = Selected::new( &mut bounds.original_transforms, &mut bounds.center_of_transformation, &selected, responses, &document.network, &document.metadata, None, &ToolType::Select, ); bounds.center_of_transformation = selected.mean_average_of_pivots(); } tool_data.layers_dragging = selected; SelectToolFsmState::RotatingBounds } // Dragging the selected layers around to transform them else if intersection.is_some_and(|intersection| selected.iter().any(|selected_layer| intersection.starts_with(*selected_layer, document.metadata()))) { responses.add(DocumentMessage::StartTransaction); if tool_data.nested_selection_behavior == NestedSelectionBehavior::Deepest { tool_data.select_single_layer = intersection; } else { tool_data.select_single_layer = intersection.and_then(|intersection| intersection.ancestors(&document.metadata).find(|ancestor| selected.contains(ancestor))); } tool_data.layers_dragging = selected; tool_data.get_snap_candidates(document, input); SelectToolFsmState::Dragging } // Dragging a selection box else { tool_data.layers_dragging = selected; if !input.keyboard.key(add_to_selection) { responses.add(DocumentMessage::DeselectAllLayers); tool_data.layers_dragging.clear(); } if let Some(intersection) = intersection { responses.add(DocumentMessage::StartTransaction); tool_data.layer_selected_on_start = Some(intersection); selected = vec![intersection]; match tool_data.nested_selection_behavior { NestedSelectionBehavior::Shallowest if !input.keyboard.key(select_deepest) => drag_shallowest_manipulation(responses, selected, tool_data, document), _ => drag_deepest_manipulation(responses, selected, tool_data), } tool_data.get_snap_candidates(document, input); SelectToolFsmState::Dragging } else { // Deselect all layers if using shallowest selection behavior // Necessary since for shallowest mode, we need to know the current selected layers to determine the next if tool_data.nested_selection_behavior == NestedSelectionBehavior::Shallowest { responses.add(DocumentMessage::DeselectAllLayers); tool_data.layers_dragging.clear(); } SelectToolFsmState::DrawingBox } }; tool_data.non_duplicated_layers = None; state } (SelectToolFsmState::Dragging, SelectToolMessage::PointerMove { axis_align, duplicate, .. }) => { tool_data.has_dragged = true; if input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_none() { tool_data.start_duplicates(document, responses); } else if !input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_some() { tool_data.stop_duplicates(document, responses); } let axis_align = input.keyboard.key(axis_align); let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start); let total_mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_start); // Ignore the non duplicated layers if the current layers have not spawned yet. let layers_exist = tool_data.layers_dragging.iter().all(|&layer| document.metadata().click_target(layer).is_some()); let ignore = tool_data.non_duplicated_layers.as_ref().filter(|_| !layers_exist).unwrap_or(&tool_data.layers_dragging); let snap_data = SnapData::ignore(document, input, ignore); let mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_current); let mut offset = mouse_delta_document; let mut best_snap = SnappedPoint::infinite_snap(document.metadata.document_to_viewport.inverse().transform_point2(mouse_position)); for point in &mut tool_data.snap_candidates { point.document_point += total_mouse_delta_document; let snapped = if axis_align { let constraint = SnapConstraint::Line { origin: point.document_point, direction: total_mouse_delta_document.normalize(), }; tool_data.snap_manager.constrained_snap(&snap_data, &point, constraint, None) } else { tool_data.snap_manager.free_snap(&snap_data, &point, None, false) }; if best_snap.other_snap_better(&snapped) { offset = snapped.snapped_point_document - point.document_point + mouse_delta_document; best_snap = snapped; } point.document_point -= total_mouse_delta_document; } tool_data.snap_manager.update_indicator(best_snap); let mouse_delta = document.metadata.document_to_viewport.transform_vector2(offset); // TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481 for layer_ancestors in document.metadata().shallowest_unique_layers(tool_data.layers_dragging.iter().copied()) { responses.add_front(GraphOperationMessage::TransformChange { layer: *layer_ancestors.last().unwrap(), transform: DAffine2::from_translation(mouse_delta), transform_in: TransformIn::Viewport, skip_rerender: false, }); } tool_data.drag_current += mouse_delta; SelectToolFsmState::Dragging } (SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { axis_align, center, .. }) => { if let Some(bounds) = &mut tool_data.bounding_box_manager { if let Some(movement) = &mut bounds.selected_edges { let (center, constrain) = (input.keyboard.key(center), input.keyboard.key(axis_align)); let center = center.then_some(bounds.center_of_transformation); let snap = Some(SizeSnapData { manager: &mut tool_data.snap_manager, points: &mut tool_data.snap_candidates, snap_data: SnapData::ignore(document, input, &tool_data.layers_dragging), }); let (position, size) = movement.new_size(input.mouse.position, bounds.original_bound_transform, center, constrain, snap); let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size); let pivot_transform = DAffine2::from_translation(pivot); let transformation = pivot_transform * delta * pivot_transform.inverse(); tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node())); let selected = &tool_data.layers_dragging; let mut selected = Selected::new( &mut bounds.original_transforms, &mut pivot, selected, responses, &document.network, &document.metadata, None, &ToolType::Select, ); selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse()); } } SelectToolFsmState::ResizingBounds } (SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { snap_angle, .. }) => { if let Some(bounds) = &mut tool_data.bounding_box_manager { let angle = { let start_offset = tool_data.drag_start - bounds.center_of_transformation; let end_offset = input.mouse.position - bounds.center_of_transformation; start_offset.angle_between(end_offset) }; let snapped_angle = if input.keyboard.key(snap_angle) { let snap_resolution = ROTATE_SNAP_ANGLE.to_radians(); (angle / snap_resolution).round() * snap_resolution } else { angle }; let delta = DAffine2::from_angle(snapped_angle); tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node())); let mut selected = Selected::new( &mut bounds.original_transforms, &mut bounds.center_of_transformation, &tool_data.layers_dragging, responses, &document.network, &document.metadata, None, &ToolType::Select, ); selected.update_transforms(delta); } SelectToolFsmState::RotatingBounds } (SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { .. }) => { let mouse_position = input.mouse.position; let snapped_mouse_position = mouse_position; //tool_data.snap_manager.snap_position(responses, document, mouse_position); tool_data.pivot.set_viewport_position(snapped_mouse_position, document, responses); SelectToolFsmState::DraggingPivot } (SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove { .. }) => { tool_data.drag_current = input.mouse.position; responses.add(OverlaysMessage::Draw); SelectToolFsmState::DrawingBox } (SelectToolFsmState::Ready, SelectToolMessage::PointerMove { .. }) => { let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true)); // Dragging the pivot overrules the other operations if tool_data.pivot.is_over(input.mouse.position) { cursor = MouseCursorIcon::Move; } // Generate the hover outline responses.add(OverlaysMessage::Draw); if tool_data.cursor != cursor { tool_data.cursor = cursor; responses.add(FrontendMessage::UpdateMouseCursor { cursor }); } SelectToolFsmState::Ready } (SelectToolFsmState::Dragging, SelectToolMessage::Enter) => { let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { true => DocumentMessage::Undo, false => DocumentMessage::CommitTransaction, }; tool_data.snap_manager.cleanup(responses); responses.add_front(response); SelectToolFsmState::Ready } (SelectToolFsmState::Dragging, SelectToolMessage::DragStop { remove_from_selection }) => { // Deselect layer if not snap dragging if !tool_data.has_dragged && input.keyboard.key(remove_from_selection) && tool_data.layer_selected_on_start.is_none() { let quad = tool_data.selection_quad(); let intersection = document.intersect_quad(quad, &document.network); if let Some(path) = intersection.last() { let replacement_selected_layers: Vec<_> = document .selected_nodes .selected_layers(document.metadata()) .filter(|&layer| !path.starts_with(layer, document.metadata())) .collect(); tool_data.layers_dragging.clear(); tool_data.layers_dragging.extend(replacement_selected_layers.iter()); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: replacement_selected_layers.iter().map(|layer| layer.to_node()).collect(), }); } } else if let Some(selecting_layer) = tool_data.select_single_layer.take() { if !tool_data.has_dragged { responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![selecting_layer.to_node()], }); } } tool_data.has_dragged = false; tool_data.layer_selected_on_start = None; responses.add(DocumentMessage::CommitTransaction); tool_data.snap_manager.cleanup(responses); tool_data.select_single_layer = None; SelectToolFsmState::Ready } (SelectToolFsmState::ResizingBounds, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => { let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { true => DocumentMessage::Undo, false => DocumentMessage::CommitTransaction, }; responses.add(response); tool_data.snap_manager.cleanup(responses); if let Some(bounds) = &mut tool_data.bounding_box_manager { bounds.original_transforms.clear(); } SelectToolFsmState::Ready } (SelectToolFsmState::RotatingBounds, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => { let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { true => DocumentMessage::Undo, false => DocumentMessage::CommitTransaction, }; responses.add(response); if let Some(bounds) = &mut tool_data.bounding_box_manager { bounds.original_transforms.clear(); } SelectToolFsmState::Ready } (SelectToolFsmState::DraggingPivot, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => { let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { true => DocumentMessage::Undo, false => DocumentMessage::CommitTransaction, }; responses.add(response); tool_data.snap_manager.cleanup(responses); SelectToolFsmState::Ready } (SelectToolFsmState::DrawingBox, SelectToolMessage::DragStop { .. } | SelectToolMessage::Enter) => { let quad = tool_data.selection_quad(); let new_selected: HashSet<_> = document.intersect_quad(quad, &document.network).collect(); let current_selected: HashSet<_> = document.selected_nodes.selected_layers(document.metadata()).collect(); if new_selected != current_selected { tool_data.layers_dragging = new_selected.into_iter().collect(); responses.add(DocumentMessage::StartTransaction); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(), }); } responses.add(OverlaysMessage::Draw); SelectToolFsmState::Ready } (SelectToolFsmState::Ready, SelectToolMessage::Enter) => { let mut selected_layers = document.selected_nodes.selected_layers(document.metadata()); if let Some(layer) = selected_layers.next() { // Check that only one layer is selected if selected_layers.next().is_none() && is_layer_fed_by_node_of_name(layer, &document.network, "Text") { responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }); responses.add(TextToolMessage::EditSelected); } } SelectToolFsmState::Ready } (SelectToolFsmState::Dragging, SelectToolMessage::Abort) => { tool_data.snap_manager.cleanup(responses); responses.add(DocumentMessage::Undo); responses.add(OverlaysMessage::Draw); SelectToolFsmState::Ready } (_, SelectToolMessage::Abort) => { tool_data.layers_dragging.retain(|layer| document.network().nodes.contains_key(&layer.to_node())); if let Some(mut bounding_box_overlays) = tool_data.bounding_box_manager.take() { let mut selected = Selected::new( &mut bounding_box_overlays.original_transforms, &mut bounding_box_overlays.opposite_pivot, &tool_data.layers_dragging, responses, &document.network, &document.metadata, None, &ToolType::Select, ); selected.revert_operation(); } responses.add(OverlaysMessage::Draw); tool_data.snap_manager.cleanup(responses); SelectToolFsmState::Ready } (_, SelectToolMessage::SetPivot { position }) => { responses.add(DocumentMessage::StartTransaction); let pos: Option = position.into(); tool_data.pivot.set_normalized_position(pos.unwrap(), document, responses); self } _ => self, } } fn standard_tool_messages(&self, message: &ToolMessage, responses: &mut VecDeque, tool_data: &mut Self::ToolData) -> bool { // Check for standard hits or cursor events match message { ToolMessage::UpdateHints => { let hint_data = HintData(vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Drag Selected")]), HintGroup(vec![HintInfo::keys([Key::KeyG, Key::KeyR, Key::KeyS], "Grab/Rotate/Scale Selected")]), HintGroup({ let mut hints = vec![HintInfo::mouse(MouseMotion::Lmb, "Select Object"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus()]; if tool_data.nested_selection_behavior == NestedSelectionBehavior::Shallowest { hints.extend([HintInfo::keys([Key::Accel], "Deepest").prepend_plus(), HintInfo::mouse(MouseMotion::LmbDouble, "Deepen Selection")]); } hints }), HintGroup(vec![ HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Shift], "Extend Selection").prepend_plus(), ]), HintGroup(vec![ HintInfo::arrow_keys("Nudge Selected"), HintInfo::keys([Key::Shift], "10x").prepend_plus(), HintInfo::keys([Key::Alt], "Resize Corner").prepend_plus(), HintInfo::keys([Key::Control], "Opp. Corner").prepend_plus(), ]), HintGroup(vec![ HintInfo::keys_and_mouse([Key::Alt], MouseMotion::LmbDrag, "Move Duplicate"), HintInfo::keys([Key::Control, Key::KeyD], "Duplicate").add_mac_keys([Key::Command, Key::KeyD]), ]), ]); responses.add(FrontendMessage::UpdateInputHints { hint_data }); self.update_hints(responses); true } ToolMessage::UpdateCursor => { self.update_cursor(responses); true } _ => false, } } fn update_hints(&self, _responses: &mut VecDeque) {} fn update_cursor(&self, responses: &mut VecDeque) { responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }); } } fn not_artboard<'a>(document: &'a DocumentMessageHandler) -> impl Fn(&LayerNodeIdentifier) -> bool + 'a { |&layer| !document.metadata.is_artboard(layer) } fn drag_shallowest_manipulation(responses: &mut VecDeque, selected: Vec, tool_data: &mut SelectToolData, document: &DocumentMessageHandler) { for layer in selected { let ancestor = layer .ancestors(document.metadata()) .filter(not_artboard(document)) .find(|&ancestor| document.selected_nodes.selected_layers_contains(ancestor, document.metadata())); let new_selected = ancestor.unwrap_or_else(|| { layer .ancestors(document.metadata()) .take_while(|&layer| layer != LayerNodeIdentifier::ROOT) .filter(not_artboard(document)) .last() .unwrap_or(layer) }); tool_data.layers_dragging.retain(|layer| !layer.ancestors(document.metadata()).any(|ancestor| ancestor == new_selected)); tool_data.layers_dragging.push(new_selected); } responses.add(NodeGraphMessage::SelectedNodesSet { nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(), }); } fn drag_deepest_manipulation(responses: &mut VecDeque, mut selected: Vec, tool_data: &mut SelectToolData) { tool_data.layers_dragging.append(&mut selected); responses.add(NodeGraphMessage::SelectedNodesSet { nodes: tool_data.layers_dragging.iter().map(|layer| layer.to_node()).collect(), }); } fn edit_layer_shallowest_manipulation(document: &DocumentMessageHandler, layer: LayerNodeIdentifier, responses: &mut VecDeque) { if document.selected_nodes.selected_layers_contains(layer, document.metadata()) { responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }); return; } let Some(new_selected) = layer.ancestors(document.metadata()).filter(not_artboard(document)).find(|ancestor| { ancestor .parent(document.metadata()) .is_some_and(|parent| document.selected_nodes.selected_layers_contains(parent, document.metadata())) }) else { return; }; responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![new_selected.to_node()] }); } fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document_network: &NodeNetwork, responses: &mut VecDeque) { if is_layer_fed_by_node_of_name(layer, document_network, "Text") { responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Text }); responses.add(TextToolMessage::EditSelected); } else if is_layer_fed_by_node_of_name(layer, document_network, "Shape") { responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }); } }