Fix undo/redo history related issues with the Path tool (#3111)

* Fix transactions

* A little cleanup

* Fix bug in delete

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adesh Gupta 2025-09-11 10:32:45 +05:30 committed by GitHub
parent 9a32e79853
commit 09ece9424d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 57 additions and 16 deletions

View File

@ -1383,7 +1383,9 @@ impl ShapeState {
} }
/// Dissolve the selected points. /// Dissolve the selected points.
pub fn delete_selected_points(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn delete_selected_points(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, start_transaction: bool) {
let mut transaction_started = false;
for (&layer, state) in &mut self.selected_shape_state { for (&layer, state) in &mut self.selected_shape_state {
let mut missing_anchors = HashMap::new(); let mut missing_anchors = HashMap::new();
let mut deleted_anchors = HashSet::new(); let mut deleted_anchors = HashSet::new();
@ -1392,6 +1394,11 @@ impl ShapeState {
let selected_segments = &state.selected_segments; let selected_segments = &state.selected_segments;
for point in std::mem::take(&mut state.selected_points) { for point in std::mem::take(&mut state.selected_points) {
if !transaction_started && start_transaction {
responses.add(DocumentMessage::AddTransaction);
transaction_started = true;
}
match point { match point {
ManipulatorPointId::Anchor(anchor) => { ManipulatorPointId::Anchor(anchor) => {
if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector) { if let Some(handles) = Self::dissolve_anchor(anchor, responses, layer, &vector) {
@ -1488,34 +1495,51 @@ impl ShapeState {
} }
} }
pub fn delete_selected_segments(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn delete_selected_segments(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, start_transaction: bool) -> bool {
let mut transaction_started = false;
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
for (segment, _, start, end) in vector.segment_bezier_iter() { for (segment, _, start, end) in vector.segment_bezier_iter() {
if state.selected_segments.contains(&segment) { if state.selected_segments.contains(&segment) {
if start_transaction && !transaction_started {
responses.add(DocumentMessage::AddTransaction);
transaction_started = true;
}
self.dissolve_segment(responses, layer, &vector, segment, [start, end]); self.dissolve_segment(responses, layer, &vector, segment, [start, end]);
} }
} }
} }
transaction_started
} }
pub fn delete_hanging_selected_anchors(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn delete_hanging_selected_anchors(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>, start_transaction: bool) {
let mut transaction_started = false;
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
for point in &state.selected_points { for point in &state.selected_points {
if let ManipulatorPointId::Anchor(anchor) = point { if let ManipulatorPointId::Anchor(anchor) = point
if vector.all_connected(*anchor).all(|segment| state.is_segment_selected(segment.segment)) { && vector.all_connected(*anchor).all(|segment| state.is_segment_selected(segment.segment))
let modification_type = VectorModificationType::RemovePoint { id: *anchor }; {
responses.add(GraphOperationMessage::Vector { layer, modification_type }); if !transaction_started && start_transaction {
responses.add(DocumentMessage::AddTransaction);
transaction_started = true
} }
let modification_type = VectorModificationType::RemovePoint { id: *anchor };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
} }
} }
} }
} }
/// Note: this also adds a history transaction if there is some change in state.
pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
let mut transaction_started = false;
for (&layer, state) in &self.selected_shape_state { for (&layer, state) in &self.selected_shape_state {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
@ -1527,6 +1551,11 @@ impl ShapeState {
for handle in vector.all_connected(point) { for handle in vector.all_connected(point) {
// Disable the g1 continuous // Disable the g1 continuous
for &handles in &vector.colinear_manipulators { for &handles in &vector.colinear_manipulators {
if !transaction_started {
responses.add(DocumentMessage::AddTransaction);
transaction_started = true;
}
if handles.contains(&handle) { if handles.contains(&handle) {
let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false }; let modification_type = VectorModificationType::SetG1Continuous { handles, enabled: false };
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
@ -1539,6 +1568,11 @@ impl ShapeState {
continue; continue;
} }
if !transaction_started {
responses.add(DocumentMessage::AddTransaction);
transaction_started = true;
}
// Create new point // Create new point
let id = PointId::generate(); let id = PointId::generate();
let modification_type = VectorModificationType::InsertPoint { id, position: pos }; let modification_type = VectorModificationType::InsertPoint { id, position: pos };
@ -1559,13 +1593,20 @@ impl ShapeState {
} }
/// Delete point(s) and adjacent segments. /// Delete point(s) and adjacent segments.
pub fn delete_point_and_break_path(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { /// Note: this also adds a history transaction if there is some change in state, and true is returned if so.
pub fn delete_point_and_break_path(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> bool {
let mut transaction_started = false;
for (&layer, state) in &mut self.selected_shape_state { for (&layer, state) in &mut self.selected_shape_state {
let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue }; let Some(vector) = document.network_interface.compute_modified_vector(layer) else { continue };
for delete in std::mem::take(&mut state.selected_points) { for delete in std::mem::take(&mut state.selected_points) {
let Some(point) = delete.get_anchor(&vector) else { continue }; let Some(point) = delete.get_anchor(&vector) else { continue };
if !transaction_started {
responses.add(DocumentMessage::AddTransaction);
transaction_started = true;
}
// Delete point // Delete point
let modification_type = VectorModificationType::RemovePoint { id: point }; let modification_type = VectorModificationType::RemovePoint { id: point };
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
@ -1577,6 +1618,8 @@ impl ShapeState {
} }
} }
} }
transaction_started
} }
/// Disable colinear handles colinear. /// Disable colinear handles colinear.

View File

@ -2611,17 +2611,15 @@ impl Fsm for PathToolFsmState {
// Delete key // Delete key
(_, PathToolMessage::Delete) => { (_, PathToolMessage::Delete) => {
// Delete the selected points and clean up overlays // Delete the selected points and clean up overlays
responses.add(DocumentMessage::AddTransaction);
let point_mode = tool_options.path_editing_mode.point_editing_mode; let point_mode = tool_options.path_editing_mode.point_editing_mode;
let segment_mode = tool_options.path_editing_mode.segment_editing_mode; let segment_mode = tool_options.path_editing_mode.segment_editing_mode;
let only_segment_mode = segment_mode && !point_mode; let only_segment_mode = segment_mode && !point_mode;
shape_editor.delete_selected_segments(document, responses); let transaction_started = shape_editor.delete_selected_segments(document, responses, true);
if only_segment_mode { if only_segment_mode {
shape_editor.delete_hanging_selected_anchors(document, responses); shape_editor.delete_hanging_selected_anchors(document, responses, !transaction_started);
} else { } else {
shape_editor.delete_selected_points(document, responses); shape_editor.delete_selected_points(document, responses, !transaction_started);
} }
responses.add(PathToolMessage::SelectionChanged); responses.add(PathToolMessage::SelectionChanged);
@ -2841,8 +2839,8 @@ impl Fsm for PathToolFsmState {
} }
(_, PathToolMessage::DeleteSelected) => { (_, PathToolMessage::DeleteSelected) => {
// Delete the selected points and segments // Delete the selected points and segments
shape_editor.delete_point_and_break_path(document, responses); let deleted_some_point = shape_editor.delete_point_and_break_path(document, responses);
shape_editor.delete_selected_segments(document, responses); shape_editor.delete_selected_segments(document, responses, !deleted_some_point);
PathToolFsmState::Ready PathToolFsmState::Ready
} }

View File

@ -599,7 +599,7 @@ impl PenToolData {
self.g1_continuous = true; self.g1_continuous = true;
let document = snap_data.document; let document = snap_data.document;
self.next_handle_start = self.next_point; self.next_handle_start = self.next_point;
let vector = document.network_interface.compute_modified_vector(layer).unwrap(); let Some(vector) = document.network_interface.compute_modified_vector(layer) else { return };
self.update_handle_type(TargetHandle::FuturePreviewOutHandle); self.update_handle_type(TargetHandle::FuturePreviewOutHandle);
self.handle_mode = HandleMode::ColinearLocked; self.handle_mode = HandleMode::ColinearLocked;