diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 5a0d96cefb..fabb2489e7 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -106,6 +106,7 @@ pub const HIDE_HANDLE_DISTANCE: f64 = 3.; pub const HANDLE_ROTATE_SNAP_ANGLE: f64 = 15.; pub const SEGMENT_INSERTION_DISTANCE: f64 = 5.; pub const SEGMENT_OVERLAY_SIZE: f64 = 10.; +pub const SEGMENT_SELECTED_THICKNESS: f64 = 3.; pub const HANDLE_LENGTH_FACTOR: f64 = 0.5; // PEN TOOL diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index bf53a1ee3b..3c067df369 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -212,13 +212,13 @@ pub fn input_mappings() -> Mapping { entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), - entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, molding_in_segment_edit: KeyA }), + entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { extend_selection: Shift, lasso_select: Control, handle_drag_from_anchor: Alt, drag_restore_handle: Control, segment_editing_modifier: Control }), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick), entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape), entry!(KeyDown(KeyG); action_dispatch=PathToolMessage::GRS { key: KeyG }), entry!(KeyDown(KeyR); action_dispatch=PathToolMessage::GRS { key: KeyR }), entry!(KeyDown(KeyS); action_dispatch=PathToolMessage::GRS { key: KeyS }), - entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt }), + entry!(PointerMove; refresh_keys=[KeyC, Space, Control, Shift, Alt], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space, snap_angle: Shift, lock_angle: Control, delete_segment: Alt, break_colinear_molding: Alt, segment_editing_modifier: Control }), entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete), entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors), entry!(KeyDown(KeyA); modifiers=[Accel, Shift], canonical, action_dispatch=PathToolMessage::DeselectAllPoints), diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index c871921d5d..e88ad6087d 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -2,7 +2,7 @@ use super::utility_functions::overlay_canvas_context; use crate::consts::{ ARC_SWEEP_GIZMO_RADIUS, COLOR_OVERLAY_BLUE, COLOR_OVERLAY_BLUE_50, COLOR_OVERLAY_GREEN, COLOR_OVERLAY_RED, COLOR_OVERLAY_WHITE, COLOR_OVERLAY_YELLOW, COLOR_OVERLAY_YELLOW_DULL, COMPASS_ROSE_ARROW_SIZE, COMPASS_ROSE_HOVER_RING_DIAMETER, COMPASS_ROSE_MAIN_RING_DIAMETER, COMPASS_ROSE_RING_INNER_DIAMETER, DOWEL_PIN_RADIUS, MANIPULATOR_GROUP_MARKER_SIZE, - PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, + PIVOT_CROSSHAIR_LENGTH, PIVOT_CROSSHAIR_THICKNESS, PIVOT_DIAMETER, SEGMENT_SELECTED_THICKNESS, }; use crate::messages::prelude::Message; use bezier_rs::{Bezier, Subpath}; @@ -460,6 +460,42 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(color_stroke)); } + pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { + self.start_dpi_aware_transform(); + + let position = position.round() - DVec2::splat(0.5); + + self.render_context.begin_path(); + self.render_context + .arc(position.x, position.y, (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2., 0., TAU) + .expect("Failed to draw the circle"); + + self.render_context.set_fill_style_str(COLOR_OVERLAY_BLUE_50); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50); + self.render_context.fill(); + self.render_context.stroke(); + + self.render_context.begin_path(); + self.render_context + .arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU) + .expect("Failed to draw the circle"); + + let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; + + self.render_context.set_fill_style_str(color_fill); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); + self.render_context.fill(); + self.render_context.stroke(); + + self.end_dpi_aware_transform(); + } + + pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) { + self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50)); + let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; + self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); + } + /// Transforms the canvas context to adjust for DPI scaling /// /// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`]. @@ -758,7 +794,7 @@ impl OverlayContext { self.render_context.begin_path(); self.bezier_command(bezier, transform, true); self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); - self.render_context.set_line_width(4.); + self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS); self.render_context.stroke(); self.render_context.set_line_width(1.); @@ -772,7 +808,7 @@ impl OverlayContext { self.render_context.begin_path(); self.bezier_command(bezier, transform, true); self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50); - self.render_context.set_line_width(4.); + self.render_context.set_line_width(SEGMENT_SELECTED_THICKNESS); self.render_context.stroke(); self.render_context.set_line_width(1.); diff --git a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs index 4f3b612273..77b338ca57 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types_vello.rs @@ -293,12 +293,36 @@ impl OverlayContext { .stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(color.unwrap_or(COLOR_OVERLAY_BLUE)), None, &circle); } + pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { + let transform = self.get_transform(); + + let position = position.round() - DVec2::splat(0.5); + + let circle = kurbo::Circle::new((position.x, position.y), (MANIPULATOR_GROUP_MARKER_SIZE + 2.) / 2.); + + let fill = COLOR_OVERLAY_BLUE_50; + self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(fill), None, &circle); + self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE_50), None, &circle); + + let inner_circle = kurbo::Circle::new((position.x, position.y), MANIPULATOR_GROUP_MARKER_SIZE / 2.); + + let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; + self.scene.fill(peniko::Fill::NonZero, transform, Self::parse_color(color_fill), None, &circle); + self.scene.stroke(&kurbo::Stroke::new(1.0), transform, Self::parse_color(COLOR_OVERLAY_BLUE), None, &inner_circle); + } + pub fn manipulator_anchor(&mut self, position: DVec2, selected: bool, color: Option<&str>) { let color_stroke = color.unwrap_or(COLOR_OVERLAY_BLUE); let color_fill = if selected { color_stroke } else { COLOR_OVERLAY_WHITE }; self.square(position, None, Some(color_fill), Some(color_stroke)); } + pub fn hover_manipulator_anchor(&mut self, position: DVec2, selected: bool) { + self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50)); + let color_fill = if selected { COLOR_OVERLAY_BLUE } else { COLOR_OVERLAY_WHITE }; + self.square(position, None, Some(color_fill), Some(COLOR_OVERLAY_BLUE)); + } + fn get_transform(&self) -> kurbo::Affine { kurbo::Affine::scale(self.device_pixel_ratio) } diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index b45c5fcd11..b7ee07ef1d 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -302,9 +302,16 @@ impl ClosestSegment { (midpoint, segment_ids) } - pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque, extend_selection: bool) { - let (id, _) = self.adjusted_insert(responses); - shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection) + pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque, extend_selection: bool, point_mode: bool, is_segment_selected: bool) { + let (id, segments) = self.adjusted_insert(responses); + if point_mode || is_segment_selected { + shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection); + } + + if is_segment_selected { + let Some(state) = shape_editor.selected_shape_state.get_mut(&self.layer) else { return }; + segments.iter().for_each(|segment| state.select_segment(*segment)); + } } pub fn calculate_perp(&self, document: &DocumentMessageHandler) -> DVec2 { @@ -551,7 +558,7 @@ impl ShapeState { select_threshold: f64, extend_selection: bool, path_overlay_mode: PathOverlayMode, - frontier_handles_info: Option>>, + frontier_handles_info: &Option>>, ) -> Option> { if self.selected_shape_state.is_empty() { return None; @@ -600,18 +607,18 @@ impl ShapeState { mouse_position: DVec2, select_threshold: f64, path_overlay_mode: PathOverlayMode, - frontier_handles_info: Option>>, + frontier_handles_info: &Option>>, point_editing_mode: bool, ) -> Option<(bool, Option)> { if self.selected_shape_state.is_empty() { return None; } - if !point_editing_mode { - return None; - } - if let Some((layer, manipulator_point_id)) = self.find_nearest_point_indices(network_interface, mouse_position, select_threshold) { + // If not point editing mode then only handles are allowed to be dragged + if !point_editing_mode && matches!(manipulator_point_id, ManipulatorPointId::Anchor(_)) { + return None; + } let vector_data = network_interface.compute_modified_vector(layer)?; let point_position = manipulator_point_id.get_position(&vector_data)?; @@ -1483,6 +1490,23 @@ impl ShapeState { } } + pub fn delete_hanging_selected_anchors(&mut self, document: &DocumentMessageHandler, responses: &mut VecDeque) { + for (&layer, state) in &self.selected_shape_state { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { + continue; + }; + + for point in &state.selected_points { + if let ManipulatorPointId::Anchor(anchor) = point { + if vector_data.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 }); + } + } + } + } + } + pub fn break_path_at_selected_point(&self, document: &DocumentMessageHandler, responses: &mut VecDeque) { for (&layer, state) in &self.selected_shape_state { let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; @@ -1600,7 +1624,7 @@ impl ShapeState { mouse_position: DVec2, select_threshold: f64, path_overlay_mode: PathOverlayMode, - frontier_handles_info: Option>>, + frontier_handles_info: &Option>>, ) -> Option<(LayerNodeIdentifier, ManipulatorPointId)> { if self.selected_shape_state.is_empty() { return None; @@ -1968,20 +1992,91 @@ impl ShapeState { selection_shape: SelectionShape, selection_change: SelectionChange, path_overlay_mode: PathOverlayMode, - frontier_handles_info: Option>>, + frontier_handles_info: &Option>>, select_segments: bool, + select_points: bool, // Here, "selection mode" represents touched or enclosed, not to be confused with editing modes selection_mode: SelectionMode, ) { + let (points_inside, segments_inside) = self.get_inside_points_and_segments( + network_interface, + selection_shape, + path_overlay_mode, + frontier_handles_info, + select_segments, + select_points, + selection_mode, + ); + + if selection_change == SelectionChange::Clear { + self.deselect_all_points(); + self.deselect_all_segments(); + } + + for (layer, points) in points_inside { + let Some(state) = self.selected_shape_state.get_mut(&layer) else { continue }; + let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue }; + + for point in points { + match (point, selection_change) { + (_, SelectionChange::Shrink) => state.deselect_point(point), + (ManipulatorPointId::EndHandle(_) | ManipulatorPointId::PrimaryHandle(_), _) => { + let handle = point.as_handle().expect("Handle cannot be converted"); + if handle.length(&vector_data) > 0. { + state.select_point(point); + } + } + (_, _) => state.select_point(point), + } + } + } + + for (layer, segments) in segments_inside { + let Some(state) = self.selected_shape_state.get_mut(&layer) else { continue }; + match selection_change { + SelectionChange::Shrink => segments.iter().for_each(|segment| state.deselect_segment(*segment)), + _ => segments.iter().for_each(|segment| state.select_segment(*segment)), + } + + // Also select/deselect the endpoints of respective segments + let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue }; + if !select_points && select_segments { + vector_data + .segment_bezier_iter() + .filter(|(segment, _, _, _)| segments.contains(segment)) + .for_each(|(_, _, start, end)| match selection_change { + SelectionChange::Shrink => { + state.deselect_point(ManipulatorPointId::Anchor(start)); + state.deselect_point(ManipulatorPointId::Anchor(end)); + } + _ => { + state.select_point(ManipulatorPointId::Anchor(start)); + state.select_point(ManipulatorPointId::Anchor(end)); + } + }); + } + } + } + + #[allow(clippy::too_many_arguments)] + pub fn get_inside_points_and_segments( + &mut self, + network_interface: &NodeNetworkInterface, + selection_shape: SelectionShape, + path_overlay_mode: PathOverlayMode, + frontier_handles_info: &Option>>, + select_segments: bool, + select_points: bool, + // Represents if the box/lasso selection touches or encloses the targets (not to be confused with editing modes). + selection_mode: SelectionMode, + ) -> (HashMap>, HashMap>) { let selected_points = self.selected_points().cloned().collect::>(); let selected_segments = selected_segments(network_interface, self); - for (&layer, state) in &mut self.selected_shape_state { - if selection_change == SelectionChange::Clear { - state.clear_points(); - state.clear_segments(); - } + let mut points_inside: HashMap> = HashMap::new(); + let mut segments_inside: HashMap> = HashMap::new(); + for &layer in self.selected_shape_state.keys() { let vector_data = network_interface.compute_modified_vector(layer); let Some(vector_data) = vector_data else { continue }; let transform = network_interface.document_metadata().transform_to_viewport_if_feeds(layer, network_interface); @@ -1997,7 +2092,7 @@ impl ShapeState { let polygon_subpath = if let SelectionShape::Lasso(polygon) = selection_shape { if polygon.len() < 2 { - return; + return (points_inside, segments_inside); } let polygon: Subpath = Subpath::from_anchors_linear(polygon.to_vec(), true); Some(polygon) @@ -2037,10 +2132,7 @@ impl ShapeState { }; if select { - match selection_change { - SelectionChange::Shrink => state.deselect_segment(id), - _ => state.select_segment(id), - } + segments_inside.entry(layer).or_default().insert(id); } } @@ -2057,21 +2149,11 @@ impl ShapeState { .contains_point(transformed_position), }; - if select { - let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info.clone(), selected_segments.clone(), &selected_points); + if select && select_points { + let is_visible_handle = is_visible_point(id, &vector_data, path_overlay_mode, frontier_handles_info, selected_segments.clone(), &selected_points); if is_visible_handle { - match selection_change { - SelectionChange::Shrink => state.deselect_point(id), - _ => { - // Select only the handles which are of nonzero length - if let Some(handle) = id.as_handle() { - if handle.length(&vector_data) > 0. { - state.select_point(id) - } - } - } - } + points_inside.entry(layer).or_default().insert(id); } } } @@ -2089,13 +2171,12 @@ impl ShapeState { .contains_point(transformed_position), }; - if select { - match selection_change { - SelectionChange::Shrink => state.deselect_point(ManipulatorPointId::Anchor(id)), - _ => state.select_point(ManipulatorPointId::Anchor(id)), - } + if select && select_points { + points_inside.entry(layer).or_default().insert(ManipulatorPointId::Anchor(id)); } } } + + (points_inside, segments_inside) } } diff --git a/editor/src/messages/tool/common_functionality/utility_functions.rs b/editor/src/messages/tool/common_functionality/utility_functions.rs index c6707ab3c0..d3e6cd49ee 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -175,7 +175,7 @@ pub fn is_visible_point( manipulator_point_id: ManipulatorPointId, vector_data: &VectorData, path_overlay_mode: PathOverlayMode, - frontier_handles_info: Option>>, + frontier_handles_info: &Option>>, selected_segments: Vec, selected_points: &HashSet, ) -> bool { @@ -201,7 +201,7 @@ pub fn is_visible_point( warn!("No anchor for selected handle"); return false; }; - let Some(frontier_handles) = &frontier_handles_info else { + let Some(frontier_handles) = frontier_handles_info else { warn!("No frontier handles info provided"); return false; }; diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index e5efc14afe..69289847fb 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -78,7 +78,7 @@ pub enum PathToolMessage { lasso_select: Key, handle_drag_from_anchor: Key, drag_restore_handle: Key, - molding_in_segment_edit: Key, + segment_editing_modifier: Key, }, NudgeSelectedPoints { delta_x: f64, @@ -92,6 +92,7 @@ pub enum PathToolMessage { lock_angle: Key, delete_segment: Key, break_colinear_molding: Key, + segment_editing_modifier: Key, }, PointerOutsideViewport { equidistant: Key, @@ -101,6 +102,7 @@ pub enum PathToolMessage { lock_angle: Key, delete_segment: Key, break_colinear_molding: Key, + segment_editing_modifier: Key, }, RightClick, SelectAllAnchors, @@ -119,6 +121,8 @@ pub enum PathToolMessage { UpdateSelectedPointsStatus { overlay_context: OverlayContext, }, + TogglePointEditing, + ToggleSegmentEditing, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] @@ -239,14 +243,14 @@ impl LayoutHolder for PathTool { let point_editing_mode = CheckboxInput::new(self.options.path_editing_mode.point_editing_mode) // TODO(Keavon): Replace with a real icon .icon("Dot") - .tooltip("Point Editing Mode") - .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: input.checked }).into()) + .tooltip("Point Editing Mode\n\nShift + click to select both modes.") + .on_update(|_| PathToolMessage::TogglePointEditing.into()) .widget_holder(); let segment_editing_mode = CheckboxInput::new(self.options.path_editing_mode.segment_editing_mode) // TODO(Keavon): Replace with a real icon .icon("Remove") - .tooltip("Segment Editing Mode") - .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: input.checked }).into()) + .tooltip("Segment Editing Mode\n\nShift + click to select both modes.") + .on_update(|_| PathToolMessage::ToggleSegmentEditing.into()) .widget_holder(); let path_overlay_mode_widget = RadioInput::new(vec![ @@ -399,6 +403,8 @@ impl<'a> MessageHandler> for Path DeleteAndBreakPath, ClosePath, PointerMove, + TogglePointEditing, + ToggleSegmentEditing ), PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, @@ -410,6 +416,8 @@ impl<'a> MessageHandler> for Path BreakPath, DeleteAndBreakPath, SwapSelectedHandles, + TogglePointEditing, + ToggleSegmentEditing ), PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant; DoubleClick, @@ -421,6 +429,8 @@ impl<'a> MessageHandler> for Path DeleteAndBreakPath, Escape, RightClick, + TogglePointEditing, + ToggleSegmentEditing ), PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant; PointerMove, @@ -500,10 +510,12 @@ struct PathToolData { snap_cache: SnapCache, double_click_handled: bool, delete_segment_pressed: bool, + segment_editing_modifier: bool, + multiple_toggle_pressed: bool, auto_panning: AutoPanning, saved_points_before_anchor_select_toggle: Vec, select_anchor_toggled: bool, - saved_points_before_handle_drag: Vec, + saved_selection_before_handle_drag: HashMap, HashSet)>, handle_drag_toggle: bool, saved_points_before_anchor_convert_smooth_sharp: HashSet, last_click_time: u64, @@ -650,7 +662,7 @@ impl PathToolData { lasso_select: bool, handle_drag_from_anchor: bool, drag_zero_handle: bool, - molding_in_segment_edit: bool, + segment_editing_modifier: bool, path_overlay_mode: PathOverlayMode, segment_editing_mode: bool, point_editing_mode: bool, @@ -667,7 +679,13 @@ impl PathToolData { self.last_click_time = input.time; - let old_selection = shape_editor.selected_points().cloned().collect::>(); + let mut old_selection = HashMap::new(); + + for (layer, state) in &shape_editor.selected_shape_state { + let selected_points = state.selected_points().collect::>(); + let selected_segments = state.selected_segments().collect::>(); + old_selection.insert(*layer, (selected_points, selected_segments)); + } // Check if the point is already selected; if not, select the first point within the threshold (in pixels) // Don't select the points which are not shown currently in PathOverlayMode @@ -676,7 +694,7 @@ impl PathToolData { input.mouse.position, SELECTION_THRESHOLD, path_overlay_mode, - self.frontier_handles_info.clone(), + &self.frontier_handles_info, point_editing_mode, ) { responses.add(DocumentMessage::StartTransaction); @@ -694,7 +712,7 @@ impl PathToolData { SELECTION_THRESHOLD, extend_selection, path_overlay_mode, - self.frontier_handles_info.clone(), + &self.frontier_handles_info, ) { selection_info = updated_selection_info; } @@ -712,7 +730,7 @@ impl PathToolData { } } if dragging_only_handles && !self.handle_drag_toggle && !old_selection.is_empty() { - self.saved_points_before_handle_drag = old_selection; + self.saved_selection_before_handle_drag = old_selection; } if handle_drag_from_anchor { @@ -769,7 +787,7 @@ impl PathToolData { self.set_ghost_outline(shape_editor, document); - if segment_editing_mode && !molding_in_segment_edit { + if segment_editing_mode && !segment_editing_modifier { let layer = segment.layer(); let segment_id = segment.segment(); let already_selected = shape_editor.selected_shape_state.get(&layer).is_some_and(|state| state.is_segment_selected(segment_id)); @@ -786,6 +804,8 @@ impl PathToolData { if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) { selected_shape_state.select_segment(segment_id); } + + // TODO: If the segment connected to one of the endpoints is also selected then select that point } self.drag_start_pos = input.mouse.position; @@ -1130,11 +1150,13 @@ impl PathToolData { tangent_vector.try_normalize() } - fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode) { + fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode, point_editing_mode: bool) { // Check if there is no point nearby + // If the point mode is deactivated then don't override closest segment even if there is a closer point if shape_editor - .find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, self.frontier_handles_info.clone()) + .find_nearest_visible_point_indices(&document.network_interface, position, SELECTION_THRESHOLD, path_overlay_mode, &self.frontier_handles_info) .is_some() + && point_editing_mode { self.segment = None; } @@ -1464,7 +1486,7 @@ impl Fsm for PathToolFsmState { ) -> Self { let ToolActionMessageContext { document, input, shape_editor, .. } = tool_action_data; - update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options); + update_dynamic_hints(self, responses, shape_editor, document, tool_data, tool_options, input.mouse.position); let ToolMessage::Path(event) = event else { return self }; @@ -1493,8 +1515,96 @@ impl Fsm for PathToolFsmState { self } + (_, PathToolMessage::TogglePointEditing) => { + // Clicked on the point edit mode button + let point_edit = tool_options.path_editing_mode.point_editing_mode; + let segment_edit = tool_options.path_editing_mode.segment_editing_mode; + let multiple_toggle = tool_data.multiple_toggle_pressed; + + if point_edit && !segment_edit { + return self; + } + + match (multiple_toggle, point_edit) { + (true, true) => { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false })); + } + (true, false) => { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true })); + } + (_, _) => { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true })); + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false })); + + // Select all of the end points of selected segments + let selected_layers = shape_editor.selected_layers().cloned().collect::>(); + + for layer in selected_layers { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + + let selected_state = shape_editor.selected_shape_state.entry(layer).or_default(); + + for (segment, _, start, end) in vector_data.segment_bezier_iter() { + if selected_state.is_segment_selected(segment) { + selected_state.select_point(ManipulatorPointId::Anchor(start)); + selected_state.select_point(ManipulatorPointId::Anchor(end)); + } + } + } + + // Deselect all of the segments + shape_editor.deselect_all_segments(); + } + } + + self + } + (_, PathToolMessage::ToggleSegmentEditing) => { + // Clicked on the point edit mode button + let segment_edit = tool_options.path_editing_mode.segment_editing_mode; + let point_edit = tool_options.path_editing_mode.point_editing_mode; + + let multiple_toggle = tool_data.multiple_toggle_pressed; + + if segment_edit && !point_edit { + return self; + } + + match (multiple_toggle, segment_edit) { + (true, true) => { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false })); + } + (true, false) => { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true })); + } + (_, _) => { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false })); + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true })); + + // Select all the segments which have both of the ends selected + let selected_layers = shape_editor.selected_layers().cloned().collect::>(); + + for layer in selected_layers { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + + let selected_state = shape_editor.selected_shape_state.entry(layer).or_default(); + + for (segment, _, start, end) in vector_data.segment_bezier_iter() { + let first_selected = selected_state.is_point_selected(ManipulatorPointId::Anchor(start)); + let second_selected = selected_state.is_point_selected(ManipulatorPointId::Anchor(end)); + if first_selected && second_selected { + selected_state.select_segment(segment); + } + } + } + } + } + + self + } (_, PathToolMessage::Overlays(mut overlay_context)) => { - if matches!(self, Self::Dragging(_)) { + // Set this to show ghost line only if drag actually happened + if matches!(self, Self::Dragging(_)) && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { for (outline, layer) in &tool_data.ghost_outline { let transform = document.metadata().transform_to_viewport(*layer); overlay_context.outline(outline.iter(), transform, Some(COLOR_OVERLAY_GRAY)); @@ -1563,10 +1673,47 @@ impl Fsm for PathToolFsmState { match self { Self::Ready => { - tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode); + tool_data.update_closest_segment( + shape_editor, + input.mouse.position, + document, + tool_options.path_overlay_mode, + tool_options.path_editing_mode.point_editing_mode, + ); + + // If there exists an underlying anchor, we show a hover overlay + (|| { + if !tool_options.path_editing_mode.point_editing_mode { + return; + } + + let nearest_visible_point_indices = shape_editor.find_nearest_visible_point_indices( + &document.network_interface, + input.mouse.position, + SELECTION_THRESHOLD, + tool_options.path_overlay_mode, + &tool_data.frontier_handles_info, + ); + + let Some((layer, manipulator_point_id)) = nearest_visible_point_indices else { return }; + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { return }; + let Some(position) = manipulator_point_id.get_position(&vector_data) else { + error!("No position for hovered point"); + return; + }; + + let transform = document.metadata().transform_to_viewport(layer); + let position = transform.transform_point2(position); + let selected = shape_editor.selected_shape_state.entry(layer).or_default().is_point_selected(manipulator_point_id); + + match manipulator_point_id { + ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position, selected), + _ => overlay_context.hover_manipulator_handle(position, selected), + } + })(); if let Some(closest_segment) = &tool_data.segment { - if tool_options.path_editing_mode.segment_editing_mode { + if tool_options.path_editing_mode.segment_editing_mode && !tool_data.segment_editing_modifier { let transform = document.metadata().transform_to_viewport_if_feeds(closest_segment.layer(), &document.network_interface); overlay_context.outline_overlay_bezier(closest_segment.bezier(), transform); @@ -1584,6 +1731,7 @@ impl Fsm for PathToolFsmState { } } } else { + // We want this overlay also when in segment_editing_mode let perp = closest_segment.calculate_perp(document); let point = closest_segment.closest_point(document.metadata(), &document.network_interface); @@ -1645,14 +1793,70 @@ impl Fsm for PathToolFsmState { }; let quad = tool_data.selection_quad(document.metadata()); - let polygon = &tool_data.lasso_polygon; + + let select_segments = tool_options.path_editing_mode.segment_editing_mode; + let select_points = tool_options.path_editing_mode.point_editing_mode; + let (points_inside, segments_inside) = match selection_shape { + SelectionShapeType::Box => { + let previous_mouse = document.metadata().document_to_viewport.transform_point2(tool_data.previous_mouse_position); + let bbox = [tool_data.drag_start_pos, previous_mouse]; + shape_editor.get_inside_points_and_segments( + &document.network_interface, + SelectionShape::Box(bbox), + tool_options.path_overlay_mode, + &tool_data.frontier_handles_info, + select_segments, + select_points, + selection_mode, + ) + } + SelectionShapeType::Lasso => shape_editor.get_inside_points_and_segments( + &document.network_interface, + SelectionShape::Lasso(&tool_data.lasso_polygon), + tool_options.path_overlay_mode, + &tool_data.frontier_handles_info, + select_segments, + select_points, + selection_mode, + ), + }; + + for (layer, points) in points_inside { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + + for point in points { + let Some(position) = point.get_position(&vector_data) else { continue }; + + let transform = document.metadata().transform_to_viewport(layer); + let position = transform.transform_point2(position); + + let selected = shape_editor.selected_shape_state.entry(layer).or_default().is_point_selected(point); + + match point { + ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position, selected), + _ => overlay_context.hover_manipulator_handle(position, selected), + } + } + } + + for (layer, segments) in segments_inside { + let Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; + + let transform = document.metadata().transform_to_viewport_if_feeds(layer, &document.network_interface); + + for (segment, bezier, _, _) in vector_data.segment_bezier_iter() { + if segments.contains(&segment) { + overlay_context.outline_overlay_bezier(bezier, transform); + } + } + } match (selection_shape, selection_mode, tool_data.started_drawing_from_inside) { // Don't draw box if it is from inside a shape and selection just began (SelectionShapeType::Box, SelectionMode::Enclosed, false) => overlay_context.dashed_quad(quad, None, fill_color, Some(4.), Some(4.), Some(0.5)), - (SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), + (SelectionShapeType::Lasso, SelectionMode::Enclosed, _) => overlay_context.dashed_polygon(&tool_data.lasso_polygon, None, fill_color, Some(4.), Some(4.), Some(0.5)), (SelectionShapeType::Box, _, false) => overlay_context.quad(quad, None, fill_color), - (SelectionShapeType::Lasso, _, _) => overlay_context.polygon(polygon, None, fill_color), + (SelectionShapeType::Lasso, _, _) => overlay_context.polygon(&tool_data.lasso_polygon, None, fill_color), (SelectionShapeType::Box, _, _) => {} } } @@ -1698,14 +1902,14 @@ impl Fsm for PathToolFsmState { lasso_select, handle_drag_from_anchor, drag_restore_handle, - molding_in_segment_edit, + segment_editing_modifier, }, ) => { let extend_selection = input.keyboard.get(extend_selection as usize); let lasso_select = input.keyboard.get(lasso_select as usize); let handle_drag_from_anchor = input.keyboard.get(handle_drag_from_anchor as usize); let drag_zero_handle = input.keyboard.get(drag_restore_handle as usize); - let molding_in_segment_edit = input.keyboard.get(molding_in_segment_edit as usize); + let segment_editing_modifier = input.keyboard.get(segment_editing_modifier as usize); tool_data.selection_mode = None; tool_data.lasso_polygon.clear(); @@ -1719,7 +1923,7 @@ impl Fsm for PathToolFsmState { lasso_select, handle_drag_from_anchor, drag_zero_handle, - molding_in_segment_edit, + segment_editing_modifier, tool_options.path_overlay_mode, tool_options.path_editing_mode.segment_editing_mode, tool_options.path_editing_mode.point_editing_mode, @@ -1735,6 +1939,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, }, ) => { tool_data.previous_mouse_position = document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position); @@ -1758,6 +1963,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1768,6 +1974,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -1785,12 +1992,13 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, }, ) => { let selected_only_handles = !shape_editor.selected_points().any(|point| matches!(point, ManipulatorPointId::Anchor(_))); tool_data.stored_selection = None; - if !tool_data.saved_points_before_handle_drag.is_empty() && (tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) { + if !tool_data.saved_selection_before_handle_drag.is_empty() && (tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD) && (selected_only_handles) { tool_data.handle_drag_toggle = true; } @@ -1870,6 +2078,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1880,6 +2089,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -1891,8 +2101,18 @@ impl Fsm for PathToolFsmState { tool_data.slide_point(input.mouse.position, responses, &document.network_interface, shape_editor); PathToolFsmState::SlidingPoint } - (PathToolFsmState::Ready, PathToolMessage::PointerMove { delete_segment, .. }) => { + ( + PathToolFsmState::Ready, + PathToolMessage::PointerMove { + delete_segment, + segment_editing_modifier, + snap_angle, + .. + }, + ) => { tool_data.delete_segment_pressed = input.keyboard.get(delete_segment as usize); + tool_data.segment_editing_modifier = input.keyboard.get(segment_editing_modifier as usize); + tool_data.multiple_toggle_pressed = input.keyboard.get(snap_angle as usize); tool_data.saved_points_before_anchor_convert_smooth_sharp.clear(); tool_data.adjacent_anchor_offset = None; tool_data.stored_selection = None; @@ -1944,6 +2164,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, }, ) => { // Auto-panning @@ -1956,6 +2177,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1966,6 +2188,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -2003,8 +2226,9 @@ impl Fsm for PathToolFsmState { SelectionShape::Box(bbox), selection_change, tool_options.path_overlay_mode, - tool_data.frontier_handles_info.clone(), + &tool_data.frontier_handles_info, tool_options.path_editing_mode.segment_editing_mode, + tool_options.path_editing_mode.point_editing_mode, selection_mode, ); } @@ -2013,8 +2237,9 @@ impl Fsm for PathToolFsmState { SelectionShape::Lasso(&tool_data.lasso_polygon), selection_change, tool_options.path_overlay_mode, - tool_data.frontier_handles_info.clone(), + &tool_data.frontier_handles_info, tool_options.path_editing_mode.segment_editing_mode, + tool_options.path_editing_mode.point_editing_mode, selection_mode, ), } @@ -2028,9 +2253,14 @@ impl Fsm for PathToolFsmState { if tool_data.handle_drag_toggle && tool_data.drag_start_pos.distance(input.mouse.position) > DRAG_THRESHOLD { shape_editor.deselect_all_points(); shape_editor.deselect_all_segments(); - shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); - tool_data.saved_points_before_handle_drag.clear(); + for (layer, (selected_points, selected_segments)) in &tool_data.saved_selection_before_handle_drag { + let Some(state) = shape_editor.selected_shape_state.get_mut(&layer) else { continue }; + selected_points.iter().for_each(|point| state.select_point(*point)); + selected_segments.iter().for_each(|segment| state.select_segment(*segment)); + } + + tool_data.saved_selection_before_handle_drag.clear(); tool_data.handle_drag_toggle = false; } tool_data.molding_info = None; @@ -2092,8 +2322,9 @@ impl Fsm for PathToolFsmState { SelectionShape::Box(bbox), select_kind, tool_options.path_overlay_mode, - tool_data.frontier_handles_info.clone(), + &tool_data.frontier_handles_info, tool_options.path_editing_mode.segment_editing_mode, + tool_options.path_editing_mode.point_editing_mode, selection_mode, ); } @@ -2102,8 +2333,9 @@ impl Fsm for PathToolFsmState { SelectionShape::Lasso(&tool_data.lasso_polygon), select_kind, tool_options.path_overlay_mode, - tool_data.frontier_handles_info.clone(), + &tool_data.frontier_handles_info, tool_options.path_editing_mode.segment_editing_mode, + tool_options.path_editing_mode.point_editing_mode, selection_mode, ), } @@ -2123,20 +2355,33 @@ impl Fsm for PathToolFsmState { input.mouse.position, SELECTION_THRESHOLD, tool_options.path_overlay_mode, - tool_data.frontier_handles_info.clone(), + &tool_data.frontier_handles_info, ); let nearest_segment = tool_data.segment.clone(); if let Some(segment) = &mut tool_data.segment { let segment_mode = tool_options.path_editing_mode.segment_editing_mode; - if !drag_occurred && !tool_data.molding_segment && !segment_mode { + let point_mode = tool_options.path_editing_mode.point_editing_mode; + // If segment mode and the insertion modifier is pressed or it is in point editing mode + + if !drag_occurred && !tool_data.molding_segment && ((point_mode && !segment_mode) || (segment_mode && tool_data.segment_editing_modifier)) { if tool_data.delete_segment_pressed { if let Some(vector_data) = document.network_interface.compute_modified_vector(segment.layer()) { shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points()); } } else { - segment.adjusted_insert_and_select(shape_editor, responses, extend_selection); + let is_segment_selected = shape_editor + .selected_shape_state + .get(&segment.layer()) + .map_or(false, |state| state.is_segment_selected(segment.segment())); + + segment.adjusted_insert_and_select(shape_editor, responses, extend_selection, point_mode, is_segment_selected); + tool_data.segment = None; + tool_data.molding_info = None; + tool_data.molding_segment = false; + tool_data.temporary_adjacent_handles_while_molding = None; + return PathToolFsmState::Ready; } } @@ -2147,8 +2392,9 @@ impl Fsm for PathToolFsmState { } let segment_mode = tool_options.path_editing_mode.segment_editing_mode; + let point_mode = tool_options.path_editing_mode.point_editing_mode; - if let Some((layer, nearest_point)) = nearest_point { + if let (Some((layer, nearest_point)), true) = (nearest_point, point_mode) { let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point); if !drag_occurred && extend_selection { if clicked_selected && tool_data.last_clicked_point_was_selected { @@ -2182,6 +2428,17 @@ impl Fsm for PathToolFsmState { .entry(nearest_segment.layer()) .or_default() .deselect_segment(nearest_segment.segment()); + + // If in segment editing mode only, and upon deselecting a segment, we deselect both of its anchors + if segment_mode && !point_mode { + nearest_segment.points().iter().for_each(|point_id| { + shape_editor + .selected_shape_state + .entry(nearest_segment.layer()) + .or_default() + .deselect_point(ManipulatorPointId::Anchor(*point_id)); + }); + } } else { shape_editor.selected_shape_state.entry(nearest_segment.layer()).or_default().select_segment(nearest_segment.segment()); } @@ -2196,6 +2453,22 @@ impl Fsm for PathToolFsmState { responses.add(OverlaysMessage::Draw); } } + + // If only in segment select mode, we also select all of the endpoints of selected segments + let point_mode = tool_options.path_editing_mode.point_editing_mode; + if !point_mode { + let [start, end] = nearest_segment.points(); + shape_editor + .selected_shape_state + .entry(nearest_segment.layer()) + .or_default() + .select_point(ManipulatorPointId::Anchor(start)); + shape_editor + .selected_shape_state + .entry(nearest_segment.layer()) + .or_default() + .select_point(ManipulatorPointId::Anchor(end)); + } } // Deselect all points if the user clicks the filled region of the shape else if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD { @@ -2209,9 +2482,15 @@ impl Fsm for PathToolFsmState { if tool_data.handle_drag_toggle && drag_occurred { shape_editor.deselect_all_points(); - shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_handle_drag); + shape_editor.deselect_all_segments(); + + for (layer, (selected_points, selected_segments)) in &tool_data.saved_selection_before_handle_drag { + let Some(state) = shape_editor.selected_shape_state.get_mut(&layer) else { continue }; + selected_points.iter().for_each(|point| state.select_point(*point)); + selected_segments.iter().for_each(|segment| state.select_segment(*segment)); + } - tool_data.saved_points_before_handle_drag.clear(); + tool_data.saved_selection_before_handle_drag.clear(); tool_data.handle_drag_toggle = false; } @@ -2243,8 +2522,17 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::Delete) => { // Delete the selected points and clean up overlays responses.add(DocumentMessage::AddTransaction); + let point_mode = tool_options.path_editing_mode.point_editing_mode; + let segment_mode = tool_options.path_editing_mode.segment_editing_mode; + + let only_segment_mode = segment_mode && !point_mode; + shape_editor.delete_selected_segments(document, responses); - shape_editor.delete_selected_points(document, responses); + if only_segment_mode { + shape_editor.delete_hanging_selected_anchors(document, responses); + } else { + shape_editor.delete_selected_points(document, responses); + } responses.add(PathToolMessage::SelectionChanged); PathToolFsmState::Ready @@ -2695,6 +2983,7 @@ fn update_dynamic_hints( document: &DocumentMessageHandler, tool_data: &PathToolData, tool_options: &PathToolOptions, + position: DVec2, ) { // Condinting based on currently selected segment if it has any one g1 continuous handle @@ -2729,26 +3018,55 @@ fn update_dynamic_hints( drag_selected_hints.push(HintInfo::multi_keys([[Key::Control], [Key::Shift]], "Slide").prepend_plus()); } - let mut hint_data = match (tool_data.segment.is_some(), tool_options.path_editing_mode.segment_editing_mode) { - (true, true) => { + let segment_edit = tool_options.path_editing_mode.segment_editing_mode; + let point_edit = tool_options.path_editing_mode.point_editing_mode; + + let hovering_segment = tool_data.segment.is_some(); + let hovering_point = shape_editor + .find_nearest_visible_point_indices( + &document.network_interface, + position, + SELECTION_THRESHOLD, + tool_options.path_overlay_mode, + &tool_data.frontier_handles_info, + ) + .is_some(); + + let mut hint_data = if hovering_segment { + if segment_edit { + // Hovering a segment in segment editing mode vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), - HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::Lmb, "Mold Segment")]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::Lmb, "Insert Point on Segment")]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::Control], MouseMotion::LmbDrag, "Mold Segment")]), ] - } - (true, false) => { + } else { + // Hovering a segment in point editing mode vec![ HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Insert Point on Segment")]), HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Mold Segment")]), HintGroup(vec![HintInfo::keys_and_mouse([Key::Alt], MouseMotion::Lmb, "Delete Segment")]), ] } - (false, _) => { - vec![ - HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Select Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), - HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]), - ] + } else if hovering_point { + if point_edit { + // Hovering over a point in point editing mode + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::Lmb, "Select Point"), + HintInfo::keys([Key::Shift], "Extend").prepend_plus(), + ])] + } else { + // Hovering over a point in segment selection mode (will select a nearby segment) + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), + HintInfo::keys([Key::Shift], "Extend").prepend_plus(), + ])] } + } else { + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), + HintInfo::keys([Key::Control], "Lasso").prepend_plus(), + ])] }; if at_least_one_anchor_selected {