From 2bcc3d3bafc7c9c1a13091cd0967287fb95173ce Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Sun, 1 Jan 2023 22:02:44 +0000 Subject: [PATCH] Improve history states (#932) * Add some more history states * Fix undo whilst drawing * Paste image history * Toggle output and preview history * Code review nits * Remove extra '{' * Fix typo * Fix about.toml Co-authored-by: Keavon Chambers --- about.toml | 5 +- .../portfolio/document/document_message.rs | 1 + .../document/document_message_handler.rs | 21 +- .../document/node_graph/node_graph_message.rs | 19 +- .../node_graph/node_graph_message_handler.rs | 210 +++++++++++------- .../node_properties.rs | 8 +- .../properties_panel_message_handler.rs | 34 ++- .../portfolio/portfolio_message_handler.rs | 2 +- .../src/messages/tool/tool_message_handler.rs | 4 +- .../tool/tool_messages/gradient_tool.rs | 6 +- .../tool/tool_messages/select_tool.rs | 26 +++ .../messages/tool/tool_messages/text_tool.rs | 109 +++------ frontend/src/components/panels/NodeGraph.vue | 20 +- frontend/wasm/src/editor_api.rs | 3 + node-graph/gcore/src/ops.rs | 2 +- node-graph/node-macro/src/lib.rs | 2 +- 16 files changed, 282 insertions(+), 190 deletions(-) diff --git a/about.toml b/about.toml index 2755faa4..3c9606d9 100644 --- a/about.toml +++ b/about.toml @@ -17,9 +17,10 @@ ignore-dev-dependencies = true workarounds = ["ring"] -# https://raw.githubusercontent.com/briansmith/webpki/main/LICENSE looks like MIT to me +# https://raw.githubusercontent.com/briansmith/webpki/main/LICENSE +# is the ISC license but test code within the repo is BSD-3-Clause, but is not compiled into the crate when we use it [webpki.clarify] -license = "MIT" +license = "ISC" [[webpki.clarify.files]] path = 'LICENSE' diff --git a/editor/src/messages/portfolio/document/document_message.rs b/editor/src/messages/portfolio/document/document_message.rs index ca2d3072..d406ad3a 100644 --- a/editor/src/messages/portfolio/document/document_message.rs +++ b/editor/src/messages/portfolio/document/document_message.rs @@ -187,6 +187,7 @@ pub enum DocumentMessage { toggle_angle: bool, }, Undo, + UndoFinished, UngroupLayers { folder_path: Vec, }, diff --git a/editor/src/messages/portfolio/document/document_message_handler.rs b/editor/src/messages/portfolio/document/document_message_handler.rs index 824fbd53..240f2ff0 100644 --- a/editor/src/messages/portfolio/document/document_message_handler.rs +++ b/editor/src/messages/portfolio/document/document_message_handler.rs @@ -53,6 +53,9 @@ pub struct DocumentMessageHandler { pub document_undo_history: VecDeque, #[serde(skip)] pub document_redo_history: VecDeque, + /// Don't allow aborting transactions whilst undoing to avoid #559 + #[serde(skip)] + undo_in_progress: bool, #[serde(with = "vectorize_layer_metadata")] pub layer_metadata: HashMap, LayerMetadata>, @@ -85,6 +88,7 @@ impl Default for DocumentMessageHandler { document_undo_history: VecDeque::new(), document_redo_history: VecDeque::new(), + undo_in_progress: false, layer_metadata: vec![(vec![], LayerMetadata::new(true))].into_iter().collect(), layer_range_selection_reference: Vec::new(), @@ -190,8 +194,10 @@ impl MessageHandler { - self.undo(responses).unwrap_or_else(|e| warn!("{}", e)); - responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); + if !self.undo_in_progress { + self.undo(responses).unwrap_or_else(|e| warn!("{}", e)); + responses.extend([RenderDocument.into(), DocumentStructureChanged.into()]); + } } AddSelectedLayers { additional_layers } => { for layer_path in &additional_layers { @@ -509,7 +515,7 @@ impl MessageHandler> 1) as f64), } @@ -541,6 +547,8 @@ impl MessageHandler { + responses.push_back(DocumentMessage::StartTransaction.into()); + let path = vec![generate_uuid()]; responses.push_back( DocumentOperation::AddImage { @@ -833,12 +841,15 @@ impl MessageHandler { + self.undo_in_progress = true; responses.push_back(BroadcastEvent::ToolAbort.into()); responses.push_back(DocumentHistoryBackward.into()); responses.push_back(BroadcastEvent::DocumentIsDirty.into()); responses.push_back(RenderDocument.into()); responses.push_back(FolderChanged { affected_folder_path: vec![] }.into()); + responses.push_back(UndoFinished.into()); } + UndoFinished => self.undo_in_progress = false, UngroupLayers { folder_path } => { // Select all the children of the folder let select = self.document_legacy.folder_children_paths(&folder_path); @@ -1353,6 +1364,8 @@ impl DocumentMessageHandler { responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into()) } + responses.push_back(NodeGraphMessage::SendGraph.into()); + Ok(()) } None => Err(EditorError::NoTransactionInProgress), @@ -1391,6 +1404,8 @@ impl DocumentMessageHandler { responses.push_back(DocumentMessage::LayerChanged { affected_layer_path: layer.clone() }.into()) } + responses.push_back(NodeGraphMessage::SendGraph.into()); + Ok(()) } None => Err(EditorError::NoTransactionInProgress), diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs index e6eb0d93..12c0c1f1 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message.rs @@ -1,7 +1,8 @@ use crate::messages::prelude::*; use document_legacy::LayerId; -use graph_craft::document::{value::TaggedValue, NodeId}; +use graph_craft::document::value::TaggedValue; +use graph_craft::document::{DocumentNode, NodeId, NodeInput}; #[remain::sorted] #[impl_message(Message, DocumentMessage, NodeGraph)] @@ -43,6 +44,10 @@ pub enum NodeGraphMessage { input_index: usize, new_exposed: bool, }, + InsertNode { + node_id: NodeId, + document_node: DocumentNode, + }, MoveSelectedNodes { displacement_x: i32, displacement_y: i32, @@ -56,14 +61,20 @@ pub enum NodeGraphMessage { SelectNodes { nodes: Vec, }, + SendGraph, SetDrawing { new_drawing: bool, }, SetInputValue { - node: NodeId, + node_id: NodeId, input_index: usize, value: TaggedValue, }, + SetNodeInput { + node_id: NodeId, + input_index: usize, + input: NodeInput, + }, SetQualifiedInputValue { layer_path: Vec, node_path: Vec, @@ -74,7 +85,11 @@ pub enum NodeGraphMessage { node_id: NodeId, }, ToggleHidden, + ToggleHiddenImpl, TogglePreview { node_id: NodeId, }, + TogglePreviewImpl { + node_id: NodeId, + }, } 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 a86ecba5..cb45bee2 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 @@ -125,6 +125,16 @@ impl NodeGraphMessageHandler { }) } + /// Get the active graph_craft NodeNetwork struct + fn get_active_network<'a>(&self, document: &'a Document) -> Option<&'a graph_craft::document::NodeNetwork> { + let mut network = self.get_root_network(document); + + for segement in &self.nested_path { + network = network.and_then(|network| network.nodes.get(segement)).and_then(|node| node.implementation.get_network()); + } + network + } + /// Get the active graph_craft NodeNetwork struct fn get_active_network_mut<'a>(&self, document: &'a mut Document) -> Option<&'a mut graph_craft::document::NodeNetwork> { let mut network = self.get_root_network_mut(document); @@ -179,7 +189,7 @@ impl NodeGraphMessageHandler { /// Updates the buttons for disable and preview fn update_selection_action_buttons(&mut self, document: &mut Document, responses: &mut VecDeque) { - if let Some(network) = self.get_active_network_mut(document) { + if let Some(network) = self.get_active_network(document) { let mut widgets = Vec::new(); // Don't allow disabling input or output nodes @@ -281,7 +291,7 @@ impl NodeGraphMessageHandler { let mut nodes = Vec::new(); for (id, node) in &network.nodes { - let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else{ + let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) else { warn!("Node '{}' does not exist in library", node.name); continue }; @@ -406,7 +416,6 @@ impl MessageHandler { log::debug!("Connect primary output from node {output_node} to input of index {input_node_connector_index} on node {input_node}."); + let node_id = input_node; - let Some(network) = self.get_active_network_mut(document) else { + let Some(network) = self.get_active_network(document) else { error!("No network"); return; }; - let Some(input_node) = network.nodes.get_mut(&input_node) else { + let Some(input_node) = network.nodes.get(&input_node) else { error!("No to"); return; }; - let Some((actual_index, _)) = input_node.inputs.iter().enumerate().filter(|input|input.1.is_exposed()).nth(input_node_connector_index) else { + let Some((input_index, _)) = input_node.inputs.iter().enumerate().filter(|input|input.1.is_exposed()).nth(input_node_connector_index) else { error!("Failed to find actual index of connector indes {input_node_connector_index} on node {input_node:#?}"); return; }; - input_node.inputs[actual_index] = NodeInput::Node(output_node); - Self::send_graph(network, responses); - responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); + responses.push_back(DocumentMessage::StartTransaction.into()); + + let input = NodeInput::Node(output_node); + responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into()); + + responses.push_back(NodeGraphMessage::SendGraph.into()); } NodeGraphMessage::Copy => { - let Some(network) = self.get_active_network_mut(document) else { + let Some(network) = self.get_active_network(document) else { error!("No network"); return; }; @@ -451,12 +464,8 @@ impl MessageHandler { let node_id = node_id.unwrap_or_else(crate::application::generate_uuid); - let Some(network) = self.get_active_network_mut(document) else{ - warn!("No network"); - return; - }; - let Some(document_node_type) = document_node_types::resolve_document_node_type(&node_type) else{ + let Some(document_node_type) = document_node_types::resolve_document_node_type(&node_type) else { responses.push_back(DialogMessage::DisplayDialogError { title: "Cannot insert node".to_string(), description: format!("The document node '{node_type}' does not exist in the document node list") }.into()); return; }; @@ -481,17 +490,18 @@ impl MessageHandler { responses.push_back(NodeGraphMessage::Copy.into()); @@ -499,32 +509,26 @@ impl MessageHandler { if let Some(network) = self.get_active_network_mut(document) { - if self.remove_node(network, node_id) { - Self::send_graph(network, responses); - responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); - } + self.remove_node(network, node_id); } self.update_selected(document, responses); } NodeGraphMessage::DeleteSelectedNodes => { - if let Some(network) = self.get_active_network_mut(document) { - let mut modified = false; - for node_id in self.selected_nodes.clone() { - modified = modified || self.remove_node(network, node_id); - } - if modified { - Self::send_graph(network, responses); - responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); - } + responses.push_back(DocumentMessage::StartTransaction.into()); + + for node_id in self.selected_nodes.clone() { + responses.push_back(NodeGraphMessage::DeleteNode { node_id }.into()); } - self.update_selected(document, responses); + + responses.push_back(NodeGraphMessage::SendGraph.into()); + responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); } NodeGraphMessage::DisconnectNodes { node_id, input_index } => { - let Some(network) = self.get_active_network_mut(document) else { + let Some(network) = self.get_active_network(document) else { warn!("No network"); return; }; - let Some(node) = network.nodes.get_mut(&node_id) else { + let Some(node) = network.nodes.get(&node_id) else { warn!("Invalid node"); return; }; @@ -532,37 +536,46 @@ impl MessageHandler { - self.selected_nodes.clear(); - if let Some(network) = self.get_active_network_mut(document) { + if let Some(network) = self.get_active_network(document) { if network.nodes.get(&node).and_then(|node| node.implementation.get_network()).is_some() { self.nested_path.push(node); } } - if let Some(network) = self.get_active_network_mut(document) { + if let Some(network) = self.get_active_network(document) { Self::send_graph(network, responses); } self.collect_nested_addresses(document, responses); self.update_selected(document, responses); } NodeGraphMessage::DuplicateSelectedNodes => { - if let Some(network) = self.get_active_network_mut(document) { + if let Some(network) = self.get_active_network(document) { + responses.push_back(DocumentMessage::StartTransaction.into()); + let new_ids = &self.selected_nodes.iter().map(|&id| (id, crate::application::generate_uuid())).collect(); self.selected_nodes.clear(); // Copy the selected nodes let copied_nodes = Self::copy_nodes(network, new_ids).collect::>(); - for (new_id, mut node) in copied_nodes { + for (node_id, mut document_node) in copied_nodes { // Shift duplicated node - node.metadata.position += IVec2::splat(2); + document_node.metadata.position += IVec2::splat(2); // Add new node to the list - self.selected_nodes.push(new_id); - network.nodes.insert(new_id, node); + self.selected_nodes.push(node_id); + + // Insert new node into graph + responses.push_back(NodeGraphMessage::InsertNode { node_id, document_node }.into()); } + Self::send_graph(network, responses); self.update_selected(document, responses); } @@ -572,38 +585,48 @@ impl MessageHandler { - let Some(network) = self.get_active_network_mut(document) else{ + let Some(network) = self.get_active_network(document) else { warn!("No network"); return; }; - let Some(node) = network.nodes.get_mut(&node_id) else { + let Some(node) = network.nodes.get(&node_id) else { warn!("No node"); return; }; - if let NodeInput::Value { exposed, .. } = &mut node.inputs[input_index] { + responses.push_back(DocumentMessage::StartTransaction.into()); + + let mut input = node.inputs[input_index].clone(); + if let NodeInput::Value { exposed, .. } = &mut input { *exposed = new_exposed; } else if let Some(node_type) = document_node_types::resolve_document_node_type(&node.name) { if let NodeInput::Value { tagged_value, .. } = &node_type.inputs[input_index].default { - node.inputs[input_index] = NodeInput::Value { + input = NodeInput::Value { tagged_value: tagged_value.clone(), exposed: new_exposed, }; } } - Self::send_graph(network, responses); + responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into()); + + responses.push_back(NodeGraphMessage::SendGraph.into()); responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()); } + NodeGraphMessage::InsertNode { node_id, document_node } => { + if let Some(network) = self.get_active_network_mut(document) { + network.nodes.insert(node_id, document_node); + } + } NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y } => { - let Some(network) = self.get_active_network_mut(document) else{ + let Some(network) = self.get_active_network_mut(document) else { warn!("No network"); return; }; @@ -621,11 +644,9 @@ impl MessageHandler { - let Some(network) = self.get_active_network_mut(document) else{ + let Some(network) = self.get_active_network(document) else { warn!("No network"); return; }; @@ -660,31 +681,38 @@ impl MessageHandler = data.iter().map(|&(id, _)| (id, crate::application::generate_uuid())).collect(); - for (old_id, mut node) in data { + for (old_id, mut document_node) in data { // Shift copied node - node.metadata.position += shift; + document_node.metadata.position += shift; // Get the new, non-conflicting id - let new_id = *new_ids.get(&old_id).unwrap(); + let node_id = *new_ids.get(&old_id).unwrap(); + document_node = document_node.map_ids(Self::default_node_input, &new_ids); // Insert node into network - network.nodes.insert(new_id, node.map_ids(Self::default_node_input, &new_ids)); - - // Select the newly pasted node - self.selected_nodes.push(new_id); + responses.push_back(NodeGraphMessage::InsertNode { node_id, document_node }.into()); } - Self::send_graph(network, responses); - self.update_selected(document, responses); + let nodes = new_ids.values().copied().collect(); + responses.push_back(NodeGraphMessage::SelectNodes { nodes }.into()); + + responses.push_back(NodeGraphMessage::SendGraph.into()); } NodeGraphMessage::SelectNodes { nodes } => { self.selected_nodes = nodes; self.update_selection_action_buttons(document, responses); + self.update_selected(document, responses); responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()); } + NodeGraphMessage::SendGraph => { + if let Some(network) = self.get_active_network(document) { + Self::send_graph(network, responses); + responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); + } + } NodeGraphMessage::SetDrawing { new_drawing } => { let selected: Vec<_> = selected.collect(); // Check if we stopped drawing a node graph frame @@ -704,14 +732,13 @@ impl MessageHandler { - if let Some(network) = self.get_active_network_mut(document) { - if let Some(node) = network.nodes.get_mut(&node) { - // Extend number of inputs if not already large enough - if input_index >= node.inputs.len() { - node.inputs.extend(((node.inputs.len() - 1)..input_index).map(|_| NodeInput::Network)); - } - node.inputs[input_index] = NodeInput::Value { tagged_value: value, exposed: false }; + NodeGraphMessage::SetInputValue { node_id, input_index, value } => { + if let Some(network) = self.get_active_network(document) { + if let Some(node) = network.nodes.get(&node_id) { + responses.push_back(DocumentMessage::StartTransaction.into()); + + let input = NodeInput::Value { tagged_value: value, exposed: false }; + responses.push_back(NodeGraphMessage::SetNodeInput { node_id, input_index, input }.into()); responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into()); if node.name != "Imaginate" || input_index == 0 { responses.push_back(DocumentMessage::NodeGraphFrameGenerate.into()); @@ -719,6 +746,13 @@ impl MessageHandler { + if let Some(network) = self.get_active_network_mut(document) { + if let Some(node) = network.nodes.get_mut(&node_id) { + node.inputs[input_index] = input + } + } + } NodeGraphMessage::SetQualifiedInputValue { layer_path, node_path, @@ -750,7 +784,7 @@ impl MessageHandler { - let Some(network) = self.get_active_network_mut(document) else{ + let Some(network) = self.get_active_network_mut(document) else { warn!("No network"); return; }; @@ -794,9 +828,13 @@ impl MessageHandler { + responses.push_back(DocumentMessage::StartTransaction.into()); + responses.push_back(NodeGraphMessage::ToggleHiddenImpl.into()); + } + NodeGraphMessage::ToggleHiddenImpl => { if let Some(network) = self.get_active_network_mut(document) { // Check if any of the selected nodes are hidden if self.selected_nodes.iter().any(|id| network.disabled.contains(id)) { @@ -813,6 +851,10 @@ impl MessageHandler { + responses.push_back(DocumentMessage::StartTransaction.into()); + responses.push_back(NodeGraphMessage::TogglePreviewImpl { node_id }.into()); + } + NodeGraphMessage::TogglePreviewImpl { node_id } => { if let Some(network) = self.get_active_network_mut(document) { // Check if the node is not already if network.output != node_id { diff --git a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs index 4ee29646..157f3576 100644 --- a/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs +++ b/editor/src/messages/portfolio/document/node_graph/node_graph_message_handler/node_properties.rs @@ -21,7 +21,7 @@ pub fn string_properties(text: impl Into) -> Vec { fn update_value TaggedValue + 'static + Send + Sync>(value: F, node_id: NodeId, input_index: usize) -> WidgetCallback { WidgetCallback::new(move |input_value: &T| { NodeGraphMessage::SetInputValue { - node: node_id, + node_id, input_index, value: value(input_value), } @@ -398,13 +398,13 @@ pub fn imaginate_properties(document_node: &DocumentNode, node_id: NodeId, conte LayoutGroup::Row { widgets }.with_tooltip("Connection status to the server that computes generated images") }; - let &NodeInput::Value {tagged_value: TaggedValue::ImaginateStatus( imaginate_status),..} = status_value else{ + let &NodeInput::Value {tagged_value: TaggedValue::ImaginateStatus( imaginate_status),..} = status_value else { panic!("Invalid status input") }; - let NodeInput::Value {tagged_value: TaggedValue::RcImage( cached_data),..} = cached_value else{ + let NodeInput::Value {tagged_value: TaggedValue::RcImage( cached_data),..} = cached_value else { panic!("Invalid cached image input") }; - let &NodeInput::Value {tagged_value: TaggedValue::F64( percent_complete),..} = complete_value else{ + let &NodeInput::Value {tagged_value: TaggedValue::F64( percent_complete),..} = complete_value else { panic!("Invalid percent complete input") }; let use_base_image = if let &NodeInput::Value { diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index b59bc295..3f5ddc45 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -92,7 +92,7 @@ impl<'a> MessageHandler { let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(self.create_document_operation(Operation::ModifyFont { path, font_family, font_style, size })); + self.create_document_operation(Operation::ModifyFont { path, font_family, font_style, size }, true, responses); responses.push_back(ResendActiveProperties.into()); } ModifyTransform { value, transform_op } => { @@ -101,33 +101,34 @@ impl<'a> MessageHandler { let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(self.create_document_operation(Operation::SetLayerName { path, name })) + self.create_document_operation(Operation::SetLayerName { path, name }, true, responses); } ModifyPreserveAspect { preserve_aspect } => { let (layer_path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(self.create_document_operation(Operation::SetLayerPreserveAspect { layer_path, preserve_aspect })) + self.create_document_operation(Operation::SetLayerPreserveAspect { layer_path, preserve_aspect }, true, responses); } ModifyFill { fill } => { let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(self.create_document_operation(Operation::SetLayerFill { path, fill })); + self.create_document_operation(Operation::SetLayerFill { path, fill }, true, responses); } ModifyStroke { stroke } => { let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(self.create_document_operation(Operation::SetLayerStroke { path, stroke })) + self.create_document_operation(Operation::SetLayerStroke { path, stroke }, true, responses); } ModifyText { new_text } => { let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); - responses.push_back(Operation::SetTextContent { path, new_text }.into()) + self.create_document_operation(Operation::SetTextContent { path, new_text }, true, responses); } SetPivot { new_position } => { let (layer_path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); let position: Option = new_position.into(); let pivot = position.unwrap().into(); + responses.push_back(DocumentMessage::StartTransaction.into()); responses.push_back(Operation::SetPivot { layer_path, pivot }.into()); } CheckSelectedWasUpdated { path } => { @@ -187,11 +188,24 @@ impl PropertiesPanelMessageHandler { matches!((last_active_path_id, last_modified), (Some(active_last), Some(modified_last)) if active_last == modified_last) } - fn create_document_operation(&self, operation: Operation) -> Message { + fn create_document_operation(&self, operation: Operation, commit_history: bool, responses: &mut VecDeque) { let (_, target_document) = self.active_selection.as_ref().unwrap(); match *target_document { - TargetDocument::Artboard => ArtboardMessage::DispatchOperation(Box::new(operation)).into(), - TargetDocument::Artwork => DocumentMessage::DispatchOperation(Box::new(operation)).into(), + TargetDocument::Artboard => { + // Commit history is not respected as the artboard document is not saved in the history system. + + // Dispatch the relevant operation to the artboard document + responses.push_back(ArtboardMessage::DispatchOperation(Box::new(operation)).into()) + } + TargetDocument::Artwork => { + // Commit to history before the modification + if commit_history { + responses.push_back(DocumentMessage::StartTransaction.into()); + } + + // Dispatch the relevant operation to the main document + responses.push_back(DocumentMessage::DispatchOperation(Box::new(operation)).into()); + } } } } diff --git a/editor/src/messages/portfolio/portfolio_message_handler.rs b/editor/src/messages/portfolio/portfolio_message_handler.rs index dcdb68e9..155bd93d 100644 --- a/editor/src/messages/portfolio/portfolio_message_handler.rs +++ b/editor/src/messages/portfolio/portfolio_message_handler.rs @@ -706,7 +706,7 @@ impl PortfolioMessageHandler { let node_id = node_path[index]; inner_network.output = node_id; - let Some(new_inner) = inner_network.nodes.get_mut(&node_id).and_then(|node| node.implementation.get_network_mut()) else{ + let Some(new_inner) = inner_network.nodes.get_mut(&node_id).and_then(|node| node.implementation.get_network_mut()) else { return Err("Failed to find network".to_string()); }; inner_network = new_inner; diff --git a/editor/src/messages/tool/tool_message_handler.rs b/editor/src/messages/tool/tool_message_handler.rs index 9e4af670..8808de10 100644 --- a/editor/src/messages/tool/tool_message_handler.rs +++ b/editor/src/messages/tool/tool_message_handler.rs @@ -100,8 +100,8 @@ impl MessageHandler { - let Some(selected_gradient) = &mut tool_data.selected_gradient else{ + let Some(selected_gradient) = &mut tool_data.selected_gradient else { return self; }; diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index e886fb3c..a54be1ae 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -473,11 +473,15 @@ impl Fsm for SelectToolFsmState { // If the user clicks on new shape, make that layer their new selection. // Otherwise enter the box select mode let state = if tool_data.pivot.is_over(input.mouse.position) { + responses.push_back(DocumentMessage::StartTransaction.into()); + tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true); tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]); DraggingPivot } else if let Some(selected_edges) = dragging_bounds { + responses.push_back(DocumentMessage::StartTransaction.into()); + let snap_x = selected_edges.2 || selected_edges.3; let snap_y = selected_edges.0 || selected_edges.1; @@ -498,6 +502,8 @@ impl Fsm for SelectToolFsmState { ResizingBounds } else if rotating_bounds { + responses.push_back(DocumentMessage::StartTransaction.into()); + if let Some(bounds) = &mut tool_data.bounding_box_overlays { let selected = selected.iter().collect::>(); let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.center_of_transformation, &selected, responses, &document.document_legacy); @@ -679,6 +685,12 @@ impl Fsm for SelectToolFsmState { Ready } (ResizingBounds, DragStop) => { + let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { + true => DocumentMessage::Undo, + false => DocumentMessage::CommitTransaction, + }; + responses.push_back(response.into()); + tool_data.snap_manager.cleanup(responses); if let Some(bounds) = &mut tool_data.bounding_box_overlays { @@ -688,6 +700,12 @@ impl Fsm for SelectToolFsmState { Ready } (RotatingBounds, DragStop) => { + let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { + true => DocumentMessage::Undo, + false => DocumentMessage::CommitTransaction, + }; + responses.push_back(response.into()); + if let Some(bounds) = &mut tool_data.bounding_box_overlays { bounds.original_transforms.clear(); } @@ -695,6 +713,12 @@ impl Fsm for SelectToolFsmState { Ready } (DraggingPivot, DragStop) => { + let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { + true => DocumentMessage::Undo, + false => DocumentMessage::CommitTransaction, + }; + responses.push_back(response.into()); + tool_data.snap_manager.cleanup(responses); Ready @@ -769,6 +793,8 @@ impl Fsm for SelectToolFsmState { self } (_, SetPivot { position }) => { + responses.push_back(DocumentMessage::StartTransaction.into()); + let pos: Option = position.into(); tool_data.pivot.set_normalized_position(pos.unwrap(), document, font_cache, responses); diff --git a/editor/src/messages/tool/tool_messages/text_tool.rs b/editor/src/messages/tool/tool_messages/text_tool.rs index f55ad013..33c596b4 100644 --- a/editor/src/messages/tool/tool_messages/text_tool.rs +++ b/editor/src/messages/tool/tool_messages/text_tool.rs @@ -210,10 +210,18 @@ impl Default for TextToolFsmState { #[derive(Clone, Debug, Default)] struct TextToolData { - path: Vec, + layer_path: Vec, overlays: Vec>, } +impl TextToolData { + /// Set the editing state of the currently modifying layer + fn set_editing(&self, editable: bool, responses: &mut VecDeque) { + let path = self.layer_path.clone(); + responses.push_back(DocumentMessage::SetTextboxEditability { path, editable }.into()); + } +} + fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] { DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array() } @@ -293,55 +301,44 @@ impl Fsm for TextToolFsmState { let tolerance = DVec2::splat(SELECTION_TOLERANCE); let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]); - let new_state = if let Some(l) = document + // Check if the user has selected an existing text layer + let new_state = if let Some(clicked_text_layer_path) = document .document_legacy .intersects_quad_root(quad, font_cache) .last() .filter(|l| document.document_legacy.layer(l).map(|l| l.as_text().is_ok()).unwrap_or(false)) - // Editing existing text { if state == TextToolFsmState::Editing { - responses.push_back( - DocumentMessage::SetTextboxEditability { - path: tool_data.path.clone(), - editable: false, - } - .into(), - ); + tool_data.set_editing(false, responses); } - tool_data.path = l.clone(); + tool_data.layer_path = clicked_text_layer_path.clone(); - responses.push_back( - DocumentMessage::SetTextboxEditability { - path: tool_data.path.clone(), - editable: true, - } - .into(), - ); - responses.push_back( - DocumentMessage::SetSelectedLayers { - replacement_selected_layers: vec![tool_data.path.clone()], - } - .into(), - ); + responses.push_back(DocumentMessage::StartTransaction.into()); + + tool_data.set_editing(true, responses); + + let replacement_selected_layers = vec![tool_data.layer_path.clone()]; + responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into()); Editing } - // Creating new text + // Create new text else if state == TextToolFsmState::Ready { + responses.push_back(DocumentMessage::StartTransaction.into()); + let transform = DAffine2::from_translation(input.mouse.position).to_cols_array(); let font_size = tool_options.font_size; let font_name = tool_options.font_name.clone(); let font_style = tool_options.font_style.clone(); - tool_data.path = document.get_path_for_new_layer(); + tool_data.layer_path = document.get_path_for_new_layer(); responses.push_back( Operation::AddText { - path: tool_data.path.clone(), + path: tool_data.layer_path.clone(), transform: DAffine2::ZERO.to_cols_array(), insert_index: -1, - text: r#""#.to_string(), + text: String::new(), style: style::PathStyle::new(None, Fill::solid(global_tool_data.primary_color)), size: font_size as f64, font_name, @@ -351,37 +348,22 @@ impl Fsm for TextToolFsmState { ); responses.push_back( Operation::SetLayerTransformInViewport { - path: tool_data.path.clone(), + path: tool_data.layer_path.clone(), transform, } .into(), ); - responses.push_back( - DocumentMessage::SetTextboxEditability { - path: tool_data.path.clone(), - editable: true, - } - .into(), - ); + tool_data.set_editing(true, responses); - responses.push_back( - DocumentMessage::SetSelectedLayers { - replacement_selected_layers: vec![tool_data.path.clone()], - } - .into(), - ); + let replacement_selected_layers = vec![tool_data.layer_path.clone()]; + + responses.push_back(DocumentMessage::SetSelectedLayers { replacement_selected_layers }.into()); Editing } else { // Removing old text as editable - responses.push_back( - DocumentMessage::SetTextboxEditability { - path: tool_data.path.clone(), - editable: false, - } - .into(), - ); + tool_data.set_editing(false, responses); resize_overlays(&mut tool_data.overlays, responses, 0); @@ -392,13 +374,7 @@ impl Fsm for TextToolFsmState { } (state, Abort) => { if state == TextToolFsmState::Editing { - responses.push_back( - DocumentMessage::SetTextboxEditability { - path: tool_data.path.clone(), - editable: false, - } - .into(), - ); + tool_data.set_editing(false, responses); } resize_overlays(&mut tool_data.overlays, responses, 0); @@ -411,21 +387,10 @@ impl Fsm for TextToolFsmState { Editing } (Editing, TextChange { new_text }) => { - responses.push_back( - Operation::SetTextContent { - path: tool_data.path.clone(), - new_text, - } - .into(), - ); + let path = tool_data.layer_path.clone(); + responses.push_back(Operation::SetTextContent { path, new_text }.into()); - responses.push_back( - DocumentMessage::SetTextboxEditability { - path: tool_data.path.clone(), - editable: false, - } - .into(), - ); + tool_data.set_editing(false, responses); resize_overlays(&mut tool_data.overlays, responses, 0); @@ -433,10 +398,10 @@ impl Fsm for TextToolFsmState { } (Editing, UpdateBounds { new_text }) => { resize_overlays(&mut tool_data.overlays, responses, 1); - let text = document.document_legacy.layer(&tool_data.path).unwrap().as_text().unwrap(); + let text = document.document_legacy.layer(&tool_data.layer_path).unwrap().as_text().unwrap(); let quad = text.bounding_box(&new_text, text.load_face(font_cache)); - let transformed_quad = document.document_legacy.multiply_transforms(&tool_data.path).unwrap() * quad; + let transformed_quad = document.document_legacy.multiply_transforms(&tool_data.layer_path).unwrap() * quad; let bounds = transformed_quad.bounding_box(); let operation = Operation::SetLayerTransformInViewport { diff --git a/frontend/src/components/panels/NodeGraph.vue b/frontend/src/components/panels/NodeGraph.vue index d755c827..c4c1c45e 100644 --- a/frontend/src/components/panels/NodeGraph.vue +++ b/frontend/src/components/panels/NodeGraph.vue @@ -593,11 +593,17 @@ export default defineComponent({ // Clicked on a node if (nodeId) { + let modifiedSelected = false; + const id = BigInt(nodeId); if (e.shiftKey || e.ctrlKey) { + modifiedSelected = true; + if (this.selected.includes(id)) this.selected.splice(this.selected.lastIndexOf(id), 1); else this.selected.push(id); } else if (!this.selected.includes(id)) { + modifiedSelected = true; + this.selected = [id]; } else { this.selectIfNotDragged = id; @@ -607,15 +613,17 @@ export default defineComponent({ this.draggingNodes = { startX: e.x, startY: e.y, roundX: 0, roundY: 0 }; } - this.editor.instance.selectNodes(new BigUint64Array(this.selected)); + if (modifiedSelected) this.editor.instance.selectNodes(new BigUint64Array(this.selected)); return; } // Clicked on the graph background this.panning = true; - this.selected = []; - this.editor.instance.selectNodes(new BigUint64Array(this.selected)); + if (this.selected.length !== 0) { + this.selected = []; + this.editor.instance.selectNodes(new BigUint64Array(this.selected)); + } }, doubleClick(e: MouseEvent) { const node = (e.target as HTMLElement).closest("[data-node]") as HTMLElement | undefined; @@ -677,12 +685,14 @@ export default defineComponent({ } } else if (this.draggingNodes) { if (this.draggingNodes.startX === e.x || this.draggingNodes.startY === e.y) { - if (this.selectIfNotDragged !== undefined) { + if (this.selectIfNotDragged !== undefined && (this.selected.length !== 1 || this.selected[0] !== this.selectIfNotDragged)) { this.selected = [this.selectIfNotDragged]; this.editor.instance.selectNodes(new BigUint64Array(this.selected)); } } - this.editor.instance.moveSelectedNodes(this.draggingNodes.roundX, this.draggingNodes.roundY); + + if (this.selected.length > 0 && this.draggingNodes.roundX !== 0 && this.draggingNodes.roundY !== 0) + this.editor.instance.moveSelectedNodes(this.draggingNodes.roundX, this.draggingNodes.roundY); // Check if this node should be inserted between two other nodes if (this.selected.length === 1) { diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 94af9146..e9b93cc3 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -644,6 +644,9 @@ impl JsEditorHandle { /// Notifies the backend that the selected nodes have been moved #[wasm_bindgen(js_name = moveSelectedNodes)] pub fn move_selected_nodes(&self, displacement_x: i32, displacement_y: i32) { + let message = DocumentMessage::StartTransaction; + self.dispatch(message); + let message = NodeGraphMessage::MoveSelectedNodes { displacement_x, displacement_y }; self.dispatch(message); } diff --git a/node-graph/gcore/src/ops.rs b/node-graph/gcore/src/ops.rs index 99e42ac9..9d926fcf 100644 --- a/node-graph/gcore/src/ops.rs +++ b/node-graph/gcore/src/ops.rs @@ -59,7 +59,7 @@ pub mod dynamic { alloc::boxed::Box::new($node.eval(($(*dyn_any::downcast::<$t>($arg).unwrap()),*)) ) as Dynamic } )else* - else{ + else { panic!("Unhandled type"); // TODO: Exit neatly (although this should probably not happen) } }; diff --git a/node-graph/node-macro/src/lib.rs b/node-graph/node-macro/src/lib.rs index a8e5932c..2947b360 100644 --- a/node-graph/node-macro/src/lib.rs +++ b/node-graph/node-macro/src/lib.rs @@ -12,7 +12,7 @@ pub fn node_fn(attr: TokenStream, item: TokenStream) -> TokenStream { // Extract primary input as first argument let primary_input = function_inputs.next().expect("Primary input required - set to `()` if not needed."); - let Pat::Ident(PatIdent{ident: primary_input_ident,..} ) =&*primary_input.pat else{ + let Pat::Ident(PatIdent{ident: primary_input_ident,..} ) =&*primary_input.pat else { panic!("Expected ident as primary input."); }; let primary_input_ty = &primary_input.ty;