Fix editor crash due to mismanaged selected points on layers (#2640)

* Add hash sets to hold ignored points in SelectedLayerState

* Fix non selected anchor dragging

* Update selected points when ignoring handles or anchors

* Refactor selected points status logic

* Refactor ignore_handles and ignore_anchors bools to ShapeState

* Add back in ignore_anchors and ignore_handles in SelectedLayerState

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
seam0s 2025-05-19 00:09:58 +03:00 committed by GitHub
parent 7a2144e31e
commit ea59f10b50
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 77 additions and 53 deletions

View file

@ -44,46 +44,67 @@ pub enum ManipulatorAngle {
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
pub struct SelectedLayerState { pub struct SelectedLayerState {
selected_points: HashSet<ManipulatorPointId>, selected_points: HashSet<ManipulatorPointId>,
/// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`].
ignore_handles: bool, ignore_handles: bool,
ignore_anchors: bool, ignore_anchors: bool,
/// Points that are selected but ignored (when their overlays are disabled) are stored here.
ignored_handle_points: HashSet<ManipulatorPointId>,
ignored_anchor_points: HashSet<ManipulatorPointId>,
} }
impl SelectedLayerState { impl SelectedLayerState {
pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ { pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
self.selected_points.iter().copied() self.selected_points.iter().copied()
} }
pub fn is_selected(&self, point: ManipulatorPointId) -> bool { pub fn is_selected(&self, point: ManipulatorPointId) -> bool {
self.selected_points.contains(&point) self.selected_points.contains(&point)
} }
pub fn select_point(&mut self, point: ManipulatorPointId) { pub fn select_point(&mut self, point: ManipulatorPointId) {
if (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors) {
return;
}
self.selected_points.insert(point); self.selected_points.insert(point);
} }
pub fn deselect_point(&mut self, point: ManipulatorPointId) { pub fn deselect_point(&mut self, point: ManipulatorPointId) {
if (point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors) {
return;
}
self.selected_points.remove(&point); self.selected_points.remove(&point);
} }
pub fn set_handles_status(&mut self, ignore: bool) {
self.ignore_handles = ignore; pub fn ignore_handles(&mut self, status: bool) {
} if self.ignore_handles == !status {
pub fn set_anchors_status(&mut self, ignore: bool) {
self.ignore_anchors = ignore;
}
pub fn clear_points_force(&mut self) {
self.selected_points.clear();
self.ignore_handles = false;
self.ignore_anchors = false;
}
pub fn clear_points(&mut self) {
if self.ignore_handles || self.ignore_anchors {
return; return;
} }
self.ignore_handles = !status;
if self.ignore_handles {
self.ignored_handle_points.extend(self.selected_points.iter().copied().filter(|point| point.as_handle().is_some()));
self.selected_points.retain(|point| !self.ignored_handle_points.contains(point));
} else {
self.selected_points.extend(self.ignored_handle_points.iter().copied());
self.ignored_handle_points.clear();
}
}
pub fn ignore_anchors(&mut self, status: bool) {
if self.ignore_anchors == !status {
return;
}
self.ignore_anchors = !status;
if self.ignore_anchors {
self.ignored_anchor_points.extend(self.selected_points.iter().copied().filter(|point| point.as_anchor().is_some()));
self.selected_points.retain(|point| !self.ignored_anchor_points.contains(point));
} else {
self.selected_points.extend(self.ignored_anchor_points.iter().copied());
self.ignored_anchor_points.clear();
}
}
pub fn clear_points(&mut self) {
self.selected_points.clear(); self.selected_points.clear();
} }
pub fn selected_points_count(&self) -> usize { pub fn selected_points_count(&self) -> usize {
self.selected_points.len() self.selected_points.len()
} }
@ -93,8 +114,10 @@ pub type SelectedShapeState = HashMap<LayerNodeIdentifier, SelectedLayerState>;
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct ShapeState { pub struct ShapeState {
// The layers we can select and edit manipulators (anchors and handles) from /// The layers we can select and edit manipulators (anchors and handles) from.
pub selected_shape_state: SelectedShapeState, pub selected_shape_state: SelectedShapeState,
ignore_handles: bool,
ignore_anchors: bool,
} }
#[derive(Debug)] #[derive(Debug)]
@ -255,6 +278,10 @@ impl ClosestSegment {
// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers // TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
impl ShapeState { impl ShapeState {
pub fn is_point_ignored(&self, point: &ManipulatorPointId) -> bool {
(point.as_handle().is_some() && self.ignore_handles) || (point.as_anchor().is_some() && self.ignore_anchors)
}
pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) { pub fn close_selected_path(&self, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) {
// First collect all selected anchor points across all layers // First collect all selected anchor points across all layers
let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self
@ -507,8 +534,9 @@ impl ShapeState {
} else { } else {
// Select all connected points // Select all connected points
while let Some(point) = selected_stack.pop() { while let Some(point) = selected_stack.pop() {
if !state.is_selected(ManipulatorPointId::Anchor(point)) { let anchor_point = ManipulatorPointId::Anchor(point);
state.select_point(ManipulatorPointId::Anchor(point)); if !state.is_selected(anchor_point) {
state.select_point(anchor_point);
selected_stack.extend(vector_data.connected_points(point)); selected_stack.extend(vector_data.connected_points(point));
} }
} }
@ -548,27 +576,17 @@ impl ShapeState {
} }
} }
pub fn mark_selected_anchors(&mut self) { pub fn update_selected_anchors_status(&mut self, status: bool) {
for state in self.selected_shape_state.values_mut() { for state in self.selected_shape_state.values_mut() {
state.set_anchors_status(false); self.ignore_anchors = !status;
state.ignore_anchors(status);
} }
} }
pub fn mark_selected_handles(&mut self) { pub fn update_selected_handles_status(&mut self, status: bool) {
for state in self.selected_shape_state.values_mut() { for state in self.selected_shape_state.values_mut() {
state.set_handles_status(false); self.ignore_handles = !status;
} state.ignore_handles(status);
}
pub fn ignore_selected_anchors(&mut self) {
for state in self.selected_shape_state.values_mut() {
state.set_anchors_status(true);
}
}
pub fn ignore_selected_handles(&mut self) {
for state in self.selected_shape_state.values_mut() {
state.set_handles_status(true);
} }
} }
@ -675,6 +693,10 @@ impl ShapeState {
layer: LayerNodeIdentifier, layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>, responses: &mut VecDeque<Message>,
) -> Option<()> { ) -> Option<()> {
if self.is_point_ignored(point) {
return None;
}
let vector_data = network_interface.compute_modified_vector(layer)?; let vector_data = network_interface.compute_modified_vector(layer)?;
let transform = network_interface.document_metadata().transform_to_document(layer).inverse(); let transform = network_interface.document_metadata().transform_to_document(layer).inverse();
let position = transform.transform_point2(new_position); let position = transform.transform_point2(new_position);
@ -924,6 +946,10 @@ impl ShapeState {
let delta = delta_transform.inverse().transform_vector2(delta); let delta = delta_transform.inverse().transform_vector2(delta);
for &point in state.selected_points.iter() { for &point in state.selected_points.iter() {
if self.is_point_ignored(&point) {
continue;
}
let handle = match point { let handle = match point {
ManipulatorPointId::Anchor(point) => { ManipulatorPointId::Anchor(point) => {
self.move_anchor(point, &vector_data, delta, layer, Some(state), responses); self.move_anchor(point, &vector_data, delta, layer, Some(state), responses);
@ -1596,7 +1622,7 @@ impl ShapeState {
pub fn select_all_in_shape(&mut self, network_interface: &NodeNetworkInterface, selection_shape: SelectionShape, selection_change: SelectionChange) { pub fn select_all_in_shape(&mut self, network_interface: &NodeNetworkInterface, selection_shape: SelectionShape, selection_change: SelectionChange) {
for (&layer, state) in &mut self.selected_shape_state { for (&layer, state) in &mut self.selected_shape_state {
if selection_change == SelectionChange::Clear { if selection_change == SelectionChange::Clear {
state.clear_points_force() state.clear_points()
} }
let vector_data = network_interface.compute_modified_vector(layer); let vector_data = network_interface.compute_modified_vector(layer);

View file

@ -100,6 +100,9 @@ pub enum PathToolMessage {
}, },
SwapSelectedHandles, SwapSelectedHandles,
UpdateOptions(PathOptionsUpdate), UpdateOptions(PathOptionsUpdate),
UpdateSelectedPointsStatus {
overlay_context: OverlayContext,
},
} }
#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)]
@ -989,24 +992,18 @@ impl Fsm for PathToolFsmState {
shape_editor.set_selected_layers(target_layers); shape_editor.set_selected_layers(target_layers);
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
self
}
(_, PathToolMessage::UpdateSelectedPointsStatus { overlay_context }) => {
let display_anchors = overlay_context.visibility_settings.anchors();
let display_handles = overlay_context.visibility_settings.handles();
shape_editor.update_selected_anchors_status(display_anchors);
shape_editor.update_selected_handles_status(display_handles);
responses.add(PathToolMessage::SelectedPointUpdated);
self self
} }
(_, PathToolMessage::Overlays(mut overlay_context)) => { (_, PathToolMessage::Overlays(mut overlay_context)) => {
let display_anchors = overlay_context.visibility_settings.anchors();
let display_handles = overlay_context.visibility_settings.handles();
if !display_handles {
shape_editor.ignore_selected_handles();
} else {
shape_editor.mark_selected_handles();
}
if !display_anchors {
shape_editor.ignore_selected_anchors();
} else {
shape_editor.mark_selected_anchors();
}
// TODO: find the segment ids of which the selected points are a part of // TODO: find the segment ids of which the selected points are a part of
match tool_options.path_overlay_mode { match tool_options.path_overlay_mode {
@ -1133,6 +1130,7 @@ impl Fsm for PathToolFsmState {
} }
responses.add(PathToolMessage::SelectedPointUpdated); responses.add(PathToolMessage::SelectedPointUpdated);
responses.add(PathToolMessage::UpdateSelectedPointsStatus { overlay_context });
self self
} }