Improve the Path tool's segment editing mode and make hovering manipulators react contextually (#2860)

* Improve path editing mode

* Code review

* Tidy up UI

* Update path selection behaviour

* Fix linting

* Remove frozen flag

* Code review

* Fix segment split

* Fix deleting segments

* Add requred methods in vello overlay context

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Adesh Gupta 2025-08-02 12:28:35 +05:30 committed by GitHub
parent 523132da17
commit 668acd3c30
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 555 additions and 95 deletions

View File

@ -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

View File

@ -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),

View File

@ -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.);

View File

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

View File

@ -302,9 +302,16 @@ impl ClosestSegment {
(midpoint, segment_ids)
}
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, 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<Message>, 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<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
) -> Option<Option<SelectedPointsInfo>> {
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<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
point_editing_mode: bool,
) -> Option<(bool, Option<SelectedPointsInfo>)> {
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<Message>) {
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<Message>) {
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<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
) -> 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<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
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<HashMap<SegmentId, Vec<PointId>>>,
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<LayerNodeIdentifier, HashSet<ManipulatorPointId>>, HashMap<LayerNodeIdentifier, HashSet<SegmentId>>) {
let selected_points = self.selected_points().cloned().collect::<HashSet<_>>();
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<LayerNodeIdentifier, HashSet<ManipulatorPointId>> = HashMap::new();
let mut segments_inside: HashMap<LayerNodeIdentifier, HashSet<SegmentId>> = 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<PointId> = 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)
}
}

View File

@ -175,7 +175,7 @@ pub fn is_visible_point(
manipulator_point_id: ManipulatorPointId,
vector_data: &VectorData,
path_overlay_mode: PathOverlayMode,
frontier_handles_info: Option<HashMap<SegmentId, Vec<PointId>>>,
frontier_handles_info: &Option<HashMap<SegmentId, Vec<PointId>>>,
selected_segments: Vec<SegmentId>,
selected_points: &HashSet<ManipulatorPointId>,
) -> 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;
};

View File

@ -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<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
DeleteAndBreakPath,
ClosePath,
PointerMove,
TogglePointEditing,
ToggleSegmentEditing
),
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
Escape,
@ -410,6 +416,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> for Path
BreakPath,
DeleteAndBreakPath,
SwapSelectedHandles,
TogglePointEditing,
ToggleSegmentEditing
),
PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant;
DoubleClick,
@ -421,6 +429,8 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionMessageContext<'a>> 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<ManipulatorPointId>,
select_anchor_toggled: bool,
saved_points_before_handle_drag: Vec<ManipulatorPointId>,
saved_selection_before_handle_drag: HashMap<LayerNodeIdentifier, (HashSet<ManipulatorPointId>, HashSet<SegmentId>)>,
handle_drag_toggle: bool,
saved_points_before_anchor_convert_smooth_sharp: HashSet<ManipulatorPointId>,
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::<Vec<_>>();
let mut old_selection = HashMap::new();
for (layer, state) in &shape_editor.selected_shape_state {
let selected_points = state.selected_points().collect::<HashSet<_>>();
let selected_segments = state.selected_segments().collect::<HashSet<_>>();
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::<Vec<_>>();
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::<Vec<_>>();
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();
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;
}
@ -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 {