diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 6e0a3f56c1..7d88caeb40 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -7,6 +7,7 @@ use crate::messages::input_mapper::utility_types::misc::MappingEntry; use crate::messages::input_mapper::utility_types::misc::{KeyMappingEntries, Mapping}; use crate::messages::portfolio::document::node_graph::utility_types::Direction; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; +use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::prelude::*; use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate; use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; @@ -333,7 +334,7 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(KeyS); modifiers=[Accel], action_dispatch=DocumentMessage::SaveDocument), entry!(KeyDown(KeyD); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers), entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=DocumentMessage::DuplicateSelectedLayers), - entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers), + entry!(KeyDown(KeyG); modifiers=[Accel], action_dispatch=DocumentMessage::GroupSelectedLayers { group_folder_type: GroupFolderType::Layer }), entry!(KeyDown(KeyG); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::UngroupSelectedLayers), entry!(KeyDown(KeyN); modifiers=[Accel, Shift], action_dispatch=DocumentMessage::CreateEmptyFolder), entry!(KeyDown(BracketLeft); modifiers=[Alt], action_dispatch=DocumentMessage::SelectionStepBack), diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index 4408947d05..eb2d165797 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -1,4 +1,4 @@ -use super::utility_types::misc::SnappingState; +use super::utility_types::misc::{GroupFolderType, SnappingState}; use crate::messages::input_mapper::utility_types::input_keyboard::Key; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -39,9 +39,6 @@ pub enum DocumentMessage { }, ClearArtboards, ClearLayersPanel, - InsertBooleanOperation { - operation: graphene_core::vector::misc::BooleanOperation, - }, CreateEmptyFolder, DebugPrintDocument, DeleteNode { @@ -71,7 +68,9 @@ pub enum DocumentMessage { GridOptions(GridSnapping), GridOverlays(OverlayContext), GridVisibility(bool), - GroupSelectedLayers, + GroupSelectedLayers { + group_folder_type: GroupFolderType, + }, ImaginateGenerate { imaginate_node: Vec, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 75e1d971c7..ce4d7ab540 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -3,7 +3,7 @@ use super::node_graph::utility_types::Transform; use super::overlays::utility_types::Pivot; use super::utility_types::clipboards::Clipboard; use super::utility_types::error::EditorError; -use super::utility_types::misc::{SnappingOptions, SnappingState, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS}; +use super::utility_types::misc::{GroupFolderType, SnappingOptions, SnappingState, SNAP_FUNCTIONS_FOR_BOUNDING_BOXES, SNAP_FUNCTIONS_FOR_PATHS}; use super::utility_types::network_interface::{self, NodeNetworkInterface, TransactionStatus}; use super::utility_types::nodes::{CollapsedLayers, SelectedNodes}; use crate::application::{generate_uuid, GRAPHITE_GIT_COMMIT_HASH}; @@ -285,41 +285,16 @@ impl MessageHandler> for DocumentMessag layout_target: LayoutTarget::LayersPanelControlBar, }); } - DocumentMessage::InsertBooleanOperation { operation } => { - responses.add(DocumentMessage::AddTransaction); - - let Some(parent) = self.network_interface.deepest_common_ancestor(&[], false) else { - // Cancel grouping layers across different artboards - // TODO: Group each set of layers for each artboard separately - return; - }; - let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent); - - let folder_id = NodeId::new(); - let boolean_operation_layer = LayerNodeIdentifier::new_unchecked(folder_id); - responses.add(GraphOperationMessage::NewBooleanOperationLayer { - id: folder_id, - operation, - parent, - insert_index, - }); - responses.add(NodeGraphMessage::SetDisplayNameImpl { - node_id: folder_id, - alias: "Boolean Operation".to_string(), - }); - // Move all shallowest selected layers as children - responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: boolean_operation_layer }); - } DocumentMessage::CreateEmptyFolder => { + let selected_nodes = self.network_interface.selected_nodes(&[]).unwrap(); let id = NodeId::new(); let parent = self .network_interface - .deepest_common_ancestor(&self.selection_network_path, true) + .deepest_common_ancestor(&selected_nodes, &self.selection_network_path, true) .unwrap_or(LayerNodeIdentifier::ROOT_PARENT); - let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent); - + let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &self.network_interface.selected_nodes(&[]).unwrap(), parent); responses.add(DocumentMessage::AddTransaction); responses.add(GraphOperationMessage::NewCustomLayer { id, @@ -380,7 +355,7 @@ impl MessageHandler> for DocumentMessag DocumentMessage::DuplicateSelectedLayers => { let parent = self.new_layer_parent(false); let calculated_insert_index = - DocumentMessageHandler::get_calculated_insert_index(self.network_interface.document_metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent); + DocumentMessageHandler::get_calculated_insert_index(self.network_interface.document_metadata(), &self.network_interface.selected_nodes(&[]).unwrap(), parent); responses.add(DocumentMessage::AddTransaction); responses.add(PortfolioMessage::Copy { clipboard: Clipboard::Internal }); @@ -491,33 +466,53 @@ impl MessageHandler> for DocumentMessag self.snapping_state.grid_snapping = enabled; responses.add(OverlaysMessage::Draw); } - DocumentMessage::GroupSelectedLayers => { + DocumentMessage::GroupSelectedLayers { group_folder_type } => { responses.add(DocumentMessage::AddTransaction); - let Some(parent) = self.network_interface.deepest_common_ancestor(&self.selection_network_path, false) else { - // Cancel grouping layers across different artboards - // TODO: Group each set of layers for each artboard separately - return; - }; - let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), self.network_interface.selected_nodes(&[]).unwrap(), parent); + let mut parent_per_selected_nodes: HashMap> = HashMap::new(); + let artboards = LayerNodeIdentifier::ROOT_PARENT + .children(self.metadata()) + .filter(|x| self.network_interface.is_artboard(&x.to_node(), &self.selection_network_path)) + .collect::>(); + let Some(selected_nodes) = self.network_interface.selected_nodes(&[]) else { return }; - let node_id = NodeId::new(); - let new_group_node = document_node_definitions::resolve_document_node_type("Merge") - .expect("Failed to create merge node") - .default_node_template(); - responses.add(NodeGraphMessage::InsertNode { - node_id, - node_template: new_group_node, - }); - let new_group_folder = LayerNodeIdentifier::new_unchecked(node_id); - // Move the new folder to the correct position - responses.add(NodeGraphMessage::MoveLayerToStack { - layer: new_group_folder, - parent, - insert_index, - }); + // Non-artboard (infinite canvas) workflow + if artboards.is_empty() { + let Some(parent) = self.network_interface.deepest_common_ancestor(&selected_nodes, &self.selection_network_path, false) else { + return; + }; + let Some(selected_nodes) = &self.network_interface.selected_nodes(&self.selection_network_path) else { + return; + }; + let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), selected_nodes, parent); + + DocumentMessageHandler::group_layers(responses, insert_index, parent, group_folder_type); + } + // Artboard workflow + else { + for artboard in artboards { + let selected_descendants = artboard.descendants(self.metadata()).filter(|x| selected_nodes.selected_layers_contains(*x, self.metadata())); + for selected_descendant in selected_descendants { + parent_per_selected_nodes.entry(artboard).or_default().push(selected_descendant.to_node()); + } + } + + let mut new_folders: Vec = Vec::new(); - responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: new_group_folder }); + for children in parent_per_selected_nodes.into_values() { + let child_selected_nodes = SelectedNodes(children); + let Some(parent) = self.network_interface.deepest_common_ancestor(&child_selected_nodes, &self.selection_network_path, false) else { + continue; + }; + let insert_index = DocumentMessageHandler::get_calculated_insert_index(self.metadata(), &child_selected_nodes, parent); + + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: child_selected_nodes.0 }); + + new_folders.push(DocumentMessageHandler::group_layers(responses, insert_index, parent, group_folder_type)); + } + + responses.add(NodeGraphMessage::SelectedNodesSet { nodes: new_folders }); + } } DocumentMessage::ImaginateGenerate { imaginate_node } => { let random_value = generate_uuid(); @@ -1710,12 +1705,17 @@ impl DocumentMessageHandler { /// Finds the parent folder which, based on the current selections, should be the container of any newly added layers. pub fn new_layer_parent(&self, include_self: bool) -> LayerNodeIdentifier { + let Some(selected_nodes) = self.network_interface.selected_nodes(&self.selection_network_path) else { + warn!("No selected nodes found in new_layer_parent. Defaulting to ROOT_PARENT."); + return LayerNodeIdentifier::ROOT_PARENT; + }; + self.network_interface - .deepest_common_ancestor(&self.selection_network_path, include_self) + .deepest_common_ancestor(&selected_nodes, &self.selection_network_path, include_self) .unwrap_or_else(|| self.network_interface.all_artboards().iter().next().copied().unwrap_or(LayerNodeIdentifier::ROOT_PARENT)) } - pub fn get_calculated_insert_index(metadata: &DocumentMetadata, selected_nodes: SelectedNodes, parent: LayerNodeIdentifier) -> usize { + pub fn get_calculated_insert_index(metadata: &DocumentMetadata, selected_nodes: &SelectedNodes, parent: LayerNodeIdentifier) -> usize { parent .children(metadata) .enumerate() @@ -1735,6 +1735,36 @@ impl DocumentMessageHandler { .unwrap_or(0) } + pub fn group_layers(responses: &mut VecDeque, insert_index: usize, parent: LayerNodeIdentifier, group_folder_type: GroupFolderType) -> NodeId { + let folder_id = NodeId(generate_uuid()); + match group_folder_type { + GroupFolderType::Layer => responses.add(GraphOperationMessage::NewCustomLayer { + id: folder_id, + nodes: Vec::new(), + parent, + insert_index, + }), + GroupFolderType::BooleanOperation(operation) => { + responses.add(GraphOperationMessage::NewBooleanOperationLayer { + id: folder_id, + operation, + parent, + insert_index, + }); + } + }; + let new_group_folder = LayerNodeIdentifier::new_unchecked(folder_id); + // Move the new folder to the correct position + responses.add(NodeGraphMessage::MoveLayerToStack { + layer: new_group_folder, + parent, + insert_index, + }); + responses.add(DocumentMessage::MoveSelectedLayersToGroup { parent: new_group_folder }); + + folder_id + } + /// Loads all of the fonts in the document. pub fn load_layer_resources(&self, responses: &mut VecDeque) { let mut fonts = HashSet::new(); @@ -2055,7 +2085,10 @@ impl DocumentMessageHandler { IconButton::new("Folder", 24) .tooltip("Group Selected") .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers)) - .on_update(|_| DocumentMessage::GroupSelectedLayers.into()) + .on_update(|_| { + let group_folder_type = GroupFolderType::Layer; + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() + }) .disabled(!has_selection) .widget_holder(), IconButton::new("Trash", 24) diff --git a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs index 8885998273..c85f0e12c2 100644 --- a/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs +++ b/editor/src/messages/portfolio/document/graph_operation/graph_operation_message_handler.rs @@ -149,6 +149,10 @@ impl MessageHandler> for Gr let layer = modify_inputs.create_layer(id); modify_inputs.insert_boolean_data(operation, layer); network_interface.move_layer_to_stack(layer, parent, insert_index, &[]); + responses.add(NodeGraphMessage::SetDisplayNameImpl { + node_id: id, + alias: "Boolean Operation".to_string(), + }); responses.add(NodeGraphMessage::RunDocumentGraph); } GraphOperationMessage::NewCustomLayer { id, nodes, parent, insert_index } => { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs index 48f87ae75c..12d41451fc 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler.rs @@ -8,6 +8,7 @@ use crate::messages::portfolio::document::graph_operation::utility_types::Modify use crate::messages::portfolio::document::node_graph::document_node_definitions::NodePropertiesContext; use crate::messages::portfolio::document::node_graph::utility_types::{ContextMenuData, Direction, FrontendGraphDataType}; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; +use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::portfolio::document::utility_types::network_interface::{ self, InputConnector, NodeNetworkInterface, NodeTemplate, NodeTypePersistentMetadata, OutputConnector, Previewing, TypeSource, }; @@ -1815,7 +1816,10 @@ impl NodeGraphMessageHandler { IconButton::new("Folder", 24) .tooltip("Group Selected") .tooltip_shortcut(action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers)) - .on_update(|_| DocumentMessage::GroupSelectedLayers.into()) + .on_update(|_| { + let group_folder_type = GroupFolderType::Layer; + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() + }) .disabled(!has_selection) .widget_holder(), IconButton::new("Trash", 24) diff --git a/editor/src/messages/portfolio/document/utility_types/misc.rs b/editor/src/messages/portfolio/document/utility_types/misc.rs index 6db72e8e56..590bb1b34c 100644 --- a/editor/src/messages/portfolio/document/utility_types/misc.rs +++ b/editor/src/messages/portfolio/document/utility_types/misc.rs @@ -668,3 +668,9 @@ impl PTZ { self.zoom = zoom.clamp(crate::consts::VIEWPORT_ZOOM_SCALE_MIN, crate::consts::VIEWPORT_ZOOM_SCALE_MAX) } } + +#[derive(Clone, Copy, Debug, PartialEq, serde::Serialize, serde::Deserialize)] +pub enum GroupFolderType { + Layer, + BooleanOperation(graphene_std::vector::misc::BooleanOperation), +} diff --git a/editor/src/messages/portfolio/document/utility_types/network_interface.rs b/editor/src/messages/portfolio/document/utility_types/network_interface.rs index 9aea0ba1a1..da0f676762 100644 --- a/editor/src/messages/portfolio/document/utility_types/network_interface.rs +++ b/editor/src/messages/portfolio/document/utility_types/network_interface.rs @@ -1365,15 +1365,11 @@ impl NodeNetworkInterface { } /// Ancestor that is shared by all layers and that is deepest (more nested). Default may be the root. Skips selected non-folder, non-artboard layers - pub fn deepest_common_ancestor(&self, network_path: &[NodeId], include_self: bool) -> Option { + pub fn deepest_common_ancestor(&self, selected_nodes: &SelectedNodes, network_path: &[NodeId], include_self: bool) -> Option { if !network_path.is_empty() { log::error!("Currently can only get deepest common ancestor in the document network"); return None; } - let Some(selected_nodes) = self.selected_nodes(network_path) else { - log::error!("Could not get selected nodes in deepest_common_ancestor"); - return None; - }; selected_nodes .selected_layers(&self.document_metadata) .map(|layer| { diff --git a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs index b172fd81ab..b587b793b4 100644 --- a/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs +++ b/editor/src/messages/portfolio/menu_bar/menu_bar_message_handler.rs @@ -1,6 +1,7 @@ use crate::messages::input_mapper::utility_types::macros::action_keys; use crate::messages::layout::utility_types::widget_prelude::*; use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; +use crate::messages::portfolio::document::utility_types::misc::GroupFolderType; use crate::messages::prelude::*; pub struct MenuBarMessageData { @@ -226,7 +227,12 @@ impl LayoutHolder for MenuBarMessageHandler { label: "Group Selected".into(), icon: Some("Folder".into()), shortcut: action_keys!(DocumentMessageDiscriminant::GroupSelectedLayers), - action: MenuBarEntry::create_action(|_| DocumentMessage::GroupSelectedLayers.into()), + action: MenuBarEntry::create_action(|_| { + DocumentMessage::GroupSelectedLayers { + group_folder_type: GroupFolderType::Layer, + } + .into() + }), disabled: no_active_document || !has_selected_layers, ..MenuBarEntry::default() }, diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index c8fc8bf401..d41c32610a 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -6,7 +6,7 @@ use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; 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::misc::{AlignAggregate, AlignAxis, FlipAxis}; +use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate, AlignAxis, FlipAxis, GroupFolderType}; use crate::messages::portfolio::document::utility_types::network_interface::{FlowType, NodeNetworkInterface, NodeTemplate}; use crate::messages::portfolio::document::utility_types::transformation::Selected; use crate::messages::preferences::SelectionMode; @@ -158,7 +158,10 @@ impl SelectTool { IconButton::new(icon, 24) .tooltip(operation.to_string()) .disabled(selected_count == 0) - .on_update(move |_| DocumentMessage::InsertBooleanOperation { operation }.into()) + .on_update(move |_| { + let group_folder_type = GroupFolderType::BooleanOperation(operation); + DocumentMessage::GroupSelectedLayers { group_folder_type }.into() + }) .widget_holder() }) } @@ -354,7 +357,7 @@ impl SelectToolData { let nodes = document.network_interface.copy_nodes(©_ids, &[]).collect::>(); - let insert_index = DocumentMessageHandler::get_calculated_insert_index(document.metadata(), document.network_interface.selected_nodes(&[]).unwrap(), parent); + let insert_index = DocumentMessageHandler::get_calculated_insert_index(document.metadata(), &document.network_interface.selected_nodes(&[]).unwrap(), parent); let new_ids: HashMap<_, _> = nodes.iter().map(|(id, _)| (*id, NodeId::new())).collect();