Skip to content

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

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 9 commits into from
May 18, 2025
102 changes: 64 additions & 38 deletions editor/src/messages/tool/common_functionality/shape_editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -44,46 +44,67 @@ pub enum ManipulatorAngle {
#[derive(Clone, Debug, Default)]
pub struct SelectedLayerState {
selected_points: HashSet<ManipulatorPointId>,
/// Keeps track of the current state; helps avoid unnecessary computation when called by [`ShapeState`].
ignore_handles: 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 {
pub fn selected(&self) -> impl Iterator<Item = ManipulatorPointId> + '_ {
self.selected_points.iter().copied()
}

pub fn is_selected(&self, point: ManipulatorPointId) -> bool {
self.selected_points.contains(&point)
}

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);
}

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);
}
pub fn set_handles_status(&mut self, ignore: bool) {
self.ignore_handles = ignore;
}
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 ignore_handles(&mut self, status: bool) {
if self.ignore_handles == !status {
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 clear_points(&mut self) {
if self.ignore_handles || self.ignore_anchors {

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();
}

pub fn selected_points_count(&self) -> usize {
self.selected_points.len()
}
Expand All @@ -93,8 +114,10 @@ pub type SelectedShapeState = HashMap<LayerNodeIdentifier, SelectedLayerState>;

#[derive(Debug, Default)]
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,
ignore_handles: bool,
ignore_anchors: bool,
}

#[derive(Debug)]
Expand Down Expand Up @@ -255,6 +278,10 @@ impl ClosestSegment {

// TODO Consider keeping a list of selected manipulators to minimize traversals of the layers
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>) {
// First collect all selected anchor points across all layers
let all_selected_points: Vec<(LayerNodeIdentifier, PointId)> = self
Expand Down Expand Up @@ -507,8 +534,9 @@ impl ShapeState {
} else {
// Select all connected points
while let Some(point) = selected_stack.pop() {
if !state.is_selected(ManipulatorPointId::Anchor(point)) {
state.select_point(ManipulatorPointId::Anchor(point));
let anchor_point = ManipulatorPointId::Anchor(point);
if !state.is_selected(anchor_point) {
state.select_point(anchor_point);
selected_stack.extend(vector_data.connected_points(point));
}
}
Expand Down Expand Up @@ -548,27 +576,17 @@ impl ShapeState {
}
}

pub fn mark_selected_anchors(&mut self) {
for state in self.selected_shape_state.values_mut() {
state.set_anchors_status(false);
}
}

pub fn mark_selected_handles(&mut self) {
for state in self.selected_shape_state.values_mut() {
state.set_handles_status(false);
}
}

pub fn ignore_selected_anchors(&mut self) {
pub fn update_selected_anchors_status(&mut self, status: bool) {
for state in self.selected_shape_state.values_mut() {
state.set_anchors_status(true);
self.ignore_anchors = !status;
state.ignore_anchors(status);
}
}

pub fn ignore_selected_handles(&mut self) {
pub fn update_selected_handles_status(&mut self, status: bool) {
for state in self.selected_shape_state.values_mut() {
state.set_handles_status(true);
self.ignore_handles = !status;
state.ignore_handles(status);
}
}

Expand Down Expand Up @@ -675,6 +693,10 @@ impl ShapeState {
layer: LayerNodeIdentifier,
responses: &mut VecDeque<Message>,
) -> Option<()> {
if self.is_point_ignored(point) {
return None;
}

let vector_data = network_interface.compute_modified_vector(layer)?;
let transform = network_interface.document_metadata().transform_to_document(layer).inverse();
let position = transform.transform_point2(new_position);
Expand Down Expand Up @@ -924,6 +946,10 @@ impl ShapeState {
let delta = delta_transform.inverse().transform_vector2(delta);

for &point in state.selected_points.iter() {
if self.is_point_ignored(&point) {
continue;
}

let handle = match point {
ManipulatorPointId::Anchor(point) => {
self.move_anchor(point, &vector_data, delta, layer, Some(state), responses);
Expand Down Expand Up @@ -1596,7 +1622,7 @@ impl ShapeState {
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 {
if selection_change == SelectionChange::Clear {
state.clear_points_force()
state.clear_points()
}

let vector_data = network_interface.compute_modified_vector(layer);
Expand Down
24 changes: 11 additions & 13 deletions editor/src/messages/tool/tool_messages/path_tool.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,9 @@ pub enum PathToolMessage {
},
SwapSelectedHandles,
UpdateOptions(PathOptionsUpdate),
UpdateSelectedPointsStatus {
overlay_context: OverlayContext,
},
}

#[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)]
Expand Down Expand Up @@ -989,24 +992,18 @@ impl Fsm for PathToolFsmState {
shape_editor.set_selected_layers(target_layers);

responses.add(OverlaysMessage::Draw);

responses.add(PathToolMessage::SelectedPointUpdated);
self
}
(_, PathToolMessage::Overlays(mut overlay_context)) => {
(_, PathToolMessage::UpdateSelectedPointsStatus { 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();
}

shape_editor.update_selected_anchors_status(display_anchors);
shape_editor.update_selected_handles_status(display_handles);

self
}
(_, PathToolMessage::Overlays(mut overlay_context)) => {
// TODO: find the segment ids of which the selected points are a part of

match tool_options.path_overlay_mode {
Expand Down Expand Up @@ -1133,6 +1130,7 @@ impl Fsm for PathToolFsmState {
}

responses.add(PathToolMessage::SelectedPointUpdated);
responses.add(PathToolMessage::UpdateSelectedPointsStatus { overlay_context });
self
}

Expand Down
Loading