From e19ac60d32d5ef703cdc4a3cb542c58ac7c93cd8 Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Fri, 11 Jul 2025 23:00:13 +0530 Subject: [PATCH 01/10] Improve path editing mode --- editor/src/consts.rs | 1 + .../messages/input_mapper/input_mappings.rs | 4 +- .../document/overlays/utility_types.rs | 32 ++- .../messages/tool/tool_messages/path_tool.rs | 217 ++++++++++++++++-- 4 files changed, 225 insertions(+), 29 deletions(-) diff --git a/editor/src/consts.rs b/editor/src/consts.rs index 58585e2dab..38d1adeecb 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -134,6 +134,7 @@ pub const SCALE_EFFECT: f64 = 0.5; // COLORS pub const COLOR_OVERLAY_BLUE: &str = "#00a8ff"; +pub const COLOR_OVERLAY_BLUE_50: &str = "rgba(0, 168, 255, 0.5)"; pub const COLOR_OVERLAY_YELLOW: &str = "#ffc848"; pub const COLOR_OVERLAY_YELLOW_DULL: &str = "#d7ba8b"; pub const COLOR_OVERLAY_GREEN: &str = "#63ce63"; diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index ad3f42a3d3..b1c248a414 100644 --- a/editor/src/messages/input_mapper/input_mappings.rs +++ b/editor/src/messages/input_mapper/input_mappings.rs @@ -211,13 +211,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 3d12ba4961..cc977d6034 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -1,7 +1,8 @@ use super::utility_functions::overlay_canvas_context; use crate::consts::{ - COLOR_OVERLAY_BLUE, 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, + 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, }; use crate::messages::prelude::Message; use bezier_rs::{Bezier, Subpath}; @@ -318,6 +319,29 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(color_stroke)); } + pub fn hover_manipulator_handle(&mut self, position: DVec2) { + 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, 8. / 2., 0., TAU).expect("Failed to draw the circle"); + + let fill = COLOR_OVERLAY_BLUE_50; + self.render_context.set_fill_style_str(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) { + let color_fill = COLOR_OVERLAY_BLUE_50; + let color_stroke = COLOR_OVERLAY_BLUE; + self.square(position, Some(8.), Some(&color_fill), Some(color_stroke)); + } + /// Transforms the canvas context to adjust for DPI scaling /// /// Overwrites all existing tranforms. This operation can be reversed with [`Self::reset_transform`]. @@ -631,11 +655,9 @@ impl OverlayContext { pub fn outline_overlay_bezier(&mut self, bezier: Bezier, transform: DAffine2) { self.start_dpi_aware_transform(); - let color = Color::from_rgb_str(COLOR_OVERLAY_BLUE.strip_prefix('#').unwrap()).unwrap().with_alpha(0.05).to_rgba_hex_srgb(); - self.render_context.begin_path(); self.bezier_command(bezier, transform, true); - self.render_context.set_stroke_style_str(&color); + self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE_50); self.render_context.set_line_width(4.); self.render_context.stroke(); diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index d54ce54274..b2a9a1841a 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -75,7 +75,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, @@ -89,6 +89,7 @@ pub enum PathToolMessage { lock_angle: Key, delete_segment: Key, break_colinear_molding: Key, + segment_editing_modifier: Key, }, PointerOutsideViewport { equidistant: Key, @@ -98,6 +99,7 @@ pub enum PathToolMessage { lock_angle: Key, delete_segment: Key, break_colinear_molding: Key, + segment_editing_modifier: Key, }, RightClick, SelectAllAnchors, @@ -116,6 +118,8 @@ pub enum PathToolMessage { UpdateSelectedPointsStatus { overlay_context: OverlayContext, }, + TogglePointEdit, + ToggleSegmentEdit, } #[derive(PartialEq, Eq, Hash, Copy, Clone, Debug, Default, serde::Serialize, serde::Deserialize, specta::Type)] @@ -237,13 +241,15 @@ impl LayoutHolder for PathTool { // TODO(Keavon): Replace with a real icon .icon("Dot") .tooltip("Point Editing Mode") - .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: input.checked }).into()) + .disabled(self.options.path_editing_mode.point_editing_mode && !self.options.path_editing_mode.segment_editing_mode) + .on_update(|_| PathToolMessage::TogglePointEdit.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()) + .disabled(self.options.path_editing_mode.segment_editing_mode && !self.options.path_editing_mode.point_editing_mode) + .on_update(|_| PathToolMessage::ToggleSegmentEdit.into()) .widget_holder(); let path_overlay_mode_widget = RadioInput::new(vec![ @@ -387,6 +393,8 @@ impl<'a> MessageHandler> for PathToo DeleteAndBreakPath, ClosePath, PointerMove, + TogglePointEdit, + ToggleSegmentEdit ), PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, @@ -398,6 +406,8 @@ impl<'a> MessageHandler> for PathToo BreakPath, DeleteAndBreakPath, SwapSelectedHandles, + TogglePointEdit, + ToggleSegmentEdit ), PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant; DoubleClick, @@ -409,6 +419,8 @@ impl<'a> MessageHandler> for PathToo DeleteAndBreakPath, Escape, RightClick, + TogglePointEdit, + ToggleSegmentEdit ), PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant; PointerMove, @@ -488,6 +500,8 @@ 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, @@ -588,7 +602,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, @@ -703,7 +717,7 @@ impl PathToolData { else if let Some(segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { responses.add(DocumentMessage::StartTransaction); - 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)); @@ -719,7 +733,26 @@ impl PathToolData { // Add to selected segments if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) { selected_shape_state.select_segment(segment_id); + + // If in segment editing mode then also select the points for which both of the connected segments are also selected + let only_segment_editing_mode = segment_editing_mode && !point_editing_mode; + + match (only_segment_editing_mode, document.network_interface.compute_modified_vector(layer)) { + (true, Some(vector_data)) => { + let mut start_connected_segments = vector_data.all_connected(segment.points()[0]); + if start_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { + selected_shape_state.select_point(ManipulatorPointId::Anchor(segment.points()[0])); + } + let mut end_connected_segments = vector_data.all_connected(segment.points()[1]); + if end_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { + selected_shape_state.select_point(ManipulatorPointId::Anchor(segment.points()[1])); + } + } + _ => {} + } } + + // TODO: If the segment connected to one of the endpoints is also selected then select that point } self.drag_start_pos = input.mouse.position; @@ -1390,7 +1423,7 @@ impl Fsm for PathToolFsmState { fn transition(self, event: ToolMessage, tool_data: &mut Self::ToolData, tool_action_data: &mut ToolActionHandlerData, tool_options: &Self::ToolOptions, responses: &mut VecDeque) -> Self { let ToolActionHandlerData { 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 }; @@ -1418,6 +1451,75 @@ impl Fsm for PathToolFsmState { self } + (_, PathToolMessage::TogglePointEdit) => { + // Clicked on the point edit mode button + let point_edit = tool_options.path_editing_mode.point_editing_mode; + let multiple_toggle = tool_data.multiple_toggle_pressed; + + if multiple_toggle && point_edit { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false })); + } else if multiple_toggle && !point_edit { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: true })); + } else { + 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)); + } + } + } + } + + self + } + (_, PathToolMessage::ToggleSegmentEdit) => { + // Clicked on the point edit mode button + let segment_edit = tool_options.path_editing_mode.segment_editing_mode; + let multiple_toggle = tool_data.multiple_toggle_pressed; + + if multiple_toggle && segment_edit { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false })); + } else if multiple_toggle && !segment_edit { + responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: true })); + } else { + 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)) => { // TODO: find the segment ids of which the selected points are a part of @@ -1483,8 +1585,24 @@ impl Fsm for PathToolFsmState { Self::Ready => { tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode); + // If there exist an underlying anchor, we show a hover overlay + + if tool_options.path_editing_mode.point_editing_mode { + if let Some((layer, manipulator_point_id)) = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { + if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { + let position = manipulator_point_id.get_position(&vector_data).expect("No position for hovered point"); + let transform = document.metadata().transform_to_viewport(layer); + let position = transform.transform_point2(position); + match manipulator_point_id { + ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position), + _ => overlay_context.hover_manipulator_handle(position), + } + } + } + } + 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); @@ -1502,6 +1620,7 @@ impl Fsm for PathToolFsmState { } } } else { + // We want this overlays also when segment_editing_mode let perp = closest_segment.calculate_perp(document); let point = closest_segment.closest_point(document.metadata(), &document.network_interface); @@ -1588,14 +1707,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(); @@ -1609,7 +1728,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, @@ -1625,6 +1744,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); @@ -1648,6 +1768,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1658,6 +1779,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -1675,6 +1797,7 @@ 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(_))); @@ -1701,9 +1824,9 @@ impl Fsm for PathToolFsmState { break_molding, tool_data.temporary_adjacent_handles_while_molding, ); - } - return PathToolFsmState::Dragging(tool_data.dragging_state); + return PathToolFsmState::Dragging(tool_data.dragging_state); + } } let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize); @@ -1760,6 +1883,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1770,6 +1894,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -1781,8 +1906,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; @@ -1817,6 +1952,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, }, ) => { // Auto-panning @@ -1829,6 +1965,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1839,6 +1976,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -2002,7 +2140,10 @@ impl Fsm for PathToolFsmState { 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()); @@ -2019,6 +2160,7 @@ 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 { let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point); @@ -2054,6 +2196,17 @@ impl Fsm for PathToolFsmState { .entry(nearest_segment.layer()) .or_default() .deselect_segment(nearest_segment.segment()); + + // If only in segement editing mode and deselected a segment then 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()); } @@ -2524,6 +2677,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 @@ -2558,26 +2712,45 @@ 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_point = shape_editor.find_nearest_point_indices(&document.network_interface, position, SELECTION_THRESHOLD).is_some(); + + let mut hint_data = if tool_data.segment.is_some() { + 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::KeyA], MouseMotion::Lmb, "Insert Point on Segment")]), + HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], 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, _) => { + } 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 Point"), HintInfo::keys([Key::Shift], "Extend").prepend_plus()]), - HintGroup(vec![HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), HintInfo::keys([Key::Control], "Lasso").prepend_plus()]), + 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")]), ] } + } else { + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::LmbDrag, "Select Area"), + HintInfo::keys([Key::Control], "Lasso").prepend_plus(), + ])] }; if at_least_one_anchor_selected { From 25d276bb900094f8c6c6f1d5fb8086c323071428 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 13 Jul 2025 02:59:56 -0700 Subject: [PATCH 02/10] Code review --- .../document/overlays/utility_types.rs | 5 +- .../messages/tool/tool_messages/path_tool.rs | 64 +++++++++---------- 2 files changed, 33 insertions(+), 36 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 8fa6abd884..02be61e726 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -327,8 +327,7 @@ impl OverlayContext { self.render_context.begin_path(); self.render_context.arc(position.x, position.y, 8. / 2., 0., TAU).expect("Failed to draw the circle"); - let fill = COLOR_OVERLAY_BLUE_50; - self.render_context.set_fill_style_str(fill); + self.render_context.set_fill_style_str(COLOR_OVERLAY_BLUE_50); self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.fill(); self.render_context.stroke(); @@ -339,7 +338,7 @@ impl OverlayContext { pub fn hover_manipulator_anchor(&mut self, position: DVec2) { let color_fill = COLOR_OVERLAY_BLUE_50; let color_stroke = COLOR_OVERLAY_BLUE; - self.square(position, Some(8.), Some(&color_fill), Some(color_stroke)); + self.square(position, Some(8.), Some(color_fill), Some(color_stroke)); } /// Transforms the canvas context to adjust for DPI scaling diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index c8c72f39b9..472dc8e9df 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -786,18 +786,18 @@ impl PathToolData { // If in segment editing mode then also select the points for which both of the connected segments are also selected let only_segment_editing_mode = segment_editing_mode && !point_editing_mode; - match (only_segment_editing_mode, document.network_interface.compute_modified_vector(layer)) { - (true, Some(vector_data)) => { - let mut start_connected_segments = vector_data.all_connected(segment.points()[0]); - if start_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { - selected_shape_state.select_point(ManipulatorPointId::Anchor(segment.points()[0])); - } - let mut end_connected_segments = vector_data.all_connected(segment.points()[1]); - if end_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { - selected_shape_state.select_point(ManipulatorPointId::Anchor(segment.points()[1])); - } + if let (true, Some(vector_data)) = (only_segment_editing_mode, document.network_interface.compute_modified_vector(layer)) { + let [segment_end1, segment_end2] = segment.points(); + + let mut start_connected_segments = vector_data.all_connected(segment_end1); + if start_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { + selected_shape_state.select_point(ManipulatorPointId::Anchor(segment_end1)); + } + + let mut end_connected_segments = vector_data.all_connected(segment_end2); + if end_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { + selected_shape_state.select_point(ManipulatorPointId::Anchor(segment_end2)); } - _ => {} } } @@ -1526,9 +1526,7 @@ impl Fsm for PathToolFsmState { 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 Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; let selected_state = shape_editor.selected_shape_state.entry(layer).or_default(); @@ -1560,9 +1558,7 @@ impl Fsm for PathToolFsmState { 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 Some(vector_data) = document.network_interface.compute_modified_vector(layer) else { continue }; let selected_state = shape_editor.selected_shape_state.entry(layer).or_default(); @@ -1649,17 +1645,19 @@ impl Fsm for PathToolFsmState { Self::Ready => { tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode); - // If there exist an underlying anchor, we show a hover overlay - + // If there exists an underlying anchor, we show a hover overlay if tool_options.path_editing_mode.point_editing_mode { if let Some((layer, manipulator_point_id)) = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { - let position = manipulator_point_id.get_position(&vector_data).expect("No position for hovered point"); - let transform = document.metadata().transform_to_viewport(layer); - let position = transform.transform_point2(position); - match manipulator_point_id { - ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position), - _ => overlay_context.hover_manipulator_handle(position), + if let Some(position) = manipulator_point_id.get_position(&vector_data) { + let transform = document.metadata().transform_to_viewport(layer); + let position = transform.transform_point2(position); + match manipulator_point_id { + ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position), + _ => overlay_context.hover_manipulator_handle(position), + } + } else { + error!("No position for hovered point"); } } } @@ -1684,7 +1682,7 @@ impl Fsm for PathToolFsmState { } } } else { - // We want this overlays also when segment_editing_mode + // 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); @@ -2307,7 +2305,7 @@ impl Fsm for PathToolFsmState { .or_default() .deselect_segment(nearest_segment.segment()); - // If only in segement editing mode and deselected a segment then deselect both of its anchors + // If in segment editing mode only, and upon deselecting a segment, then deselect both of its anchors if segment_mode && !point_mode { nearest_segment.points().iter().for_each(|point_id| { shape_editor @@ -2845,8 +2843,8 @@ fn update_dynamic_hints( // 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, "Insert Point on Segment")]), - HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::LmbDrag, "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")]), ] } else { // Hovering a segment in point editing mode @@ -2865,10 +2863,10 @@ fn update_dynamic_hints( ])] } 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()]), - HintGroup(vec![HintInfo::keys_and_mouse([Key::KeyA], MouseMotion::Lmb, "Mold Segment")]), - ] + vec![HintGroup(vec![ + HintInfo::mouse(MouseMotion::Lmb, "Select Segment"), + HintInfo::keys([Key::Shift], "Extend").prepend_plus(), + ])] } } else { vec![HintGroup(vec![ From 9ba3c77767eafb6d1d77f27473be2bae9d95a841 Mon Sep 17 00:00:00 2001 From: Keavon Chambers Date: Sun, 13 Jul 2025 03:28:25 -0700 Subject: [PATCH 03/10] Tidy up UI --- .../document/overlays/utility_types.rs | 19 +++++++++++++++---- .../messages/tool/tool_messages/path_tool.rs | 6 ++---- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index 02be61e726..f3daf4081b 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -325,9 +325,21 @@ impl OverlayContext { let position = position.round() - DVec2::splat(0.5); self.render_context.begin_path(); - self.render_context.arc(position.x, position.y, 8. / 2., 0., TAU).expect("Failed to draw the circle"); + 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"); + + self.render_context.set_fill_style_str(COLOR_OVERLAY_WHITE); self.render_context.set_stroke_style_str(COLOR_OVERLAY_BLUE); self.render_context.fill(); self.render_context.stroke(); @@ -336,9 +348,8 @@ impl OverlayContext { } pub fn hover_manipulator_anchor(&mut self, position: DVec2) { - let color_fill = COLOR_OVERLAY_BLUE_50; - let color_stroke = COLOR_OVERLAY_BLUE; - self.square(position, Some(8.), Some(color_fill), Some(color_stroke)); + self.square(position, Some(MANIPULATOR_GROUP_MARKER_SIZE + 2.), Some(COLOR_OVERLAY_BLUE_50), Some(COLOR_OVERLAY_BLUE_50)); + self.square(position, None, Some(COLOR_OVERLAY_WHITE), Some(COLOR_OVERLAY_BLUE)); } /// Transforms the canvas context to adjust for DPI scaling diff --git a/editor/src/messages/tool/tool_messages/path_tool.rs b/editor/src/messages/tool/tool_messages/path_tool.rs index 472dc8e9df..ebcbb2f51d 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -241,15 +241,13 @@ 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") - .disabled(self.options.path_editing_mode.point_editing_mode && !self.options.path_editing_mode.segment_editing_mode) + .tooltip("Point Editing Mode\n\nShift + click to select both modes") .on_update(|_| PathToolMessage::TogglePointEdit.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") - .disabled(self.options.path_editing_mode.segment_editing_mode && !self.options.path_editing_mode.point_editing_mode) + .tooltip("Segment Editing Mode\n\nShift + click to select both modes") .on_update(|_| PathToolMessage::ToggleSegmentEdit.into()) .widget_holder(); From 70a016c527e215d3eb74b2f96c4138045943a2a3 Mon Sep 17 00:00:00 2001 From: Adesh Gupta Date: Mon, 14 Jul 2025 19:50:19 +0530 Subject: [PATCH 04/10] Update path selection behaviour --- .../utility_types/widgets/input_widgets.rs | 3 + .../document/overlays/utility_types.rs | 11 +- .../tool/common_functionality/shape_editor.rs | 135 +++++++++++---- .../common_functionality/utility_functions.rs | 4 +- .../messages/tool/tool_messages/path_tool.rs | 158 ++++++++++++++---- .../widgets/inputs/CheckboxInput.svelte | 29 +++- 6 files changed, 264 insertions(+), 76 deletions(-) diff --git a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs index 893b301cd8..99d5bd5e71 100644 --- a/editor/src/messages/layout/utility_types/widgets/input_widgets.rs +++ b/editor/src/messages/layout/utility_types/widgets/input_widgets.rs @@ -16,6 +16,8 @@ pub struct CheckboxInput { pub disabled: bool, + pub frozen: bool, + pub icon: String, pub tooltip: String, @@ -41,6 +43,7 @@ impl Default for CheckboxInput { Self { checked: false, disabled: false, + frozen: false, icon: "Checkmark".into(), tooltip: Default::default(), tooltip_shortcut: Default::default(), diff --git a/editor/src/messages/portfolio/document/overlays/utility_types.rs b/editor/src/messages/portfolio/document/overlays/utility_types.rs index f3daf4081b..030eb284e5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -319,7 +319,7 @@ impl OverlayContext { self.square(position, None, Some(color_fill), Some(color_stroke)); } - pub fn hover_manipulator_handle(&mut self, position: DVec2) { + pub fn hover_manipulator_handle(&mut self, position: DVec2, selected: bool) { self.start_dpi_aware_transform(); let position = position.round() - DVec2::splat(0.5); @@ -339,7 +339,9 @@ impl OverlayContext { .arc(position.x, position.y, MANIPULATOR_GROUP_MARKER_SIZE / 2., 0., TAU) .expect("Failed to draw the circle"); - self.render_context.set_fill_style_str(COLOR_OVERLAY_WHITE); + 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(); @@ -347,9 +349,10 @@ impl OverlayContext { self.end_dpi_aware_transform(); } - pub fn hover_manipulator_anchor(&mut self, position: DVec2) { + 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)); - self.square(position, None, Some(COLOR_OVERLAY_WHITE), Some(COLOR_OVERLAY_BLUE)); + 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 diff --git a/editor/src/messages/tool/common_functionality/shape_editor.rs b/editor/src/messages/tool/common_functionality/shape_editor.rs index f98e50f079..ca8186d35a 100644 --- a/editor/src/messages/tool/common_functionality/shape_editor.rs +++ b/editor/src/messages/tool/common_functionality/shape_editor.rs @@ -301,9 +301,11 @@ impl ClosestSegment { (midpoint, segment_ids) } - pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque, extend_selection: bool) { + pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque, extend_selection: bool, point_mode: bool) { let (id, _) = self.adjusted_insert(responses); - shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection) + if point_mode { + shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection) + } } pub fn calculate_perp(&self, document: &DocumentMessageHandler) -> DVec2 { @@ -550,7 +552,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; @@ -599,7 +601,7 @@ 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() { @@ -1544,7 +1546,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; @@ -1881,20 +1883,99 @@ 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, + // Here, "selection mode" represents touched or enclosed, 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 &mut self.selected_shape_state { 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); @@ -1910,7 +1991,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) @@ -1950,10 +2031,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); } } @@ -1970,21 +2048,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); } } } @@ -2002,13 +2070,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 6ae3f130b5..10aca2a933 100644 --- a/editor/src/messages/tool/common_functionality/utility_functions.rs +++ b/editor/src/messages/tool/common_functionality/utility_functions.rs @@ -167,7 +167,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 { @@ -193,7 +193,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 ebcbb2f51d..af336191bb 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -242,12 +242,14 @@ impl LayoutHolder for PathTool { // TODO(Keavon): Replace with a real icon .icon("Dot") .tooltip("Point Editing Mode\n\nShift + click to select both modes") + .frozen(self.options.path_editing_mode.point_editing_mode && !self.options.path_editing_mode.segment_editing_mode) .on_update(|_| PathToolMessage::TogglePointEdit.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\n\nShift + click to select both modes") + .frozen(self.options.path_editing_mode.segment_editing_mode && !self.options.path_editing_mode.point_editing_mode) .on_update(|_| PathToolMessage::ToggleSegmentEdit.into()) .widget_holder(); @@ -671,7 +673,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); @@ -689,7 +691,7 @@ impl PathToolData { SELECTION_THRESHOLD, extend_selection, path_overlay_mode, - self.frontier_handles_info.clone(), + &self.frontier_handles_info, ) { selection_info = updated_selection_info; } @@ -780,23 +782,6 @@ impl PathToolData { // Add to selected segments if let Some(selected_shape_state) = shape_editor.selected_shape_state.get_mut(&layer) { selected_shape_state.select_segment(segment_id); - - // If in segment editing mode then also select the points for which both of the connected segments are also selected - let only_segment_editing_mode = segment_editing_mode && !point_editing_mode; - - if let (true, Some(vector_data)) = (only_segment_editing_mode, document.network_interface.compute_modified_vector(layer)) { - let [segment_end1, segment_end2] = segment.points(); - - let mut start_connected_segments = vector_data.all_connected(segment_end1); - if start_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { - selected_shape_state.select_point(ManipulatorPointId::Anchor(segment_end1)); - } - - let mut end_connected_segments = vector_data.all_connected(segment_end2); - if end_connected_segments.any(|handle| selected_shape_state.is_segment_selected(handle.segment) && handle.segment != segment_id) { - selected_shape_state.select_point(ManipulatorPointId::Anchor(segment_end2)); - } - } } // TODO: If the segment connected to one of the endpoints is also selected then select that point @@ -1147,7 +1132,7 @@ impl PathToolData { fn update_closest_segment(&mut self, shape_editor: &mut ShapeState, position: DVec2, document: &DocumentMessageHandler, path_overlay_mode: PathOverlayMode) { // Check if there is no point nearby 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() { self.segment = None; @@ -1510,8 +1495,13 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::TogglePointEdit) => { // 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; + } + if multiple_toggle && point_edit { responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::PointEditingMode { enabled: false })); } else if multiple_toggle && !point_edit { @@ -1535,6 +1525,9 @@ impl Fsm for PathToolFsmState { } } } + + // Deselect all of the segments + shape_editor.deselect_all_segments(); } self @@ -1542,8 +1535,14 @@ impl Fsm for PathToolFsmState { (_, PathToolMessage::ToggleSegmentEdit) => { // 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; + } + if multiple_toggle && segment_edit { responses.add(PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: false })); } else if multiple_toggle && !segment_edit { @@ -1645,14 +1644,21 @@ impl Fsm for PathToolFsmState { // If there exists an underlying anchor, we show a hover overlay if tool_options.path_editing_mode.point_editing_mode { - if let Some((layer, manipulator_point_id)) = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD) { + if let Some((layer, manipulator_point_id)) = 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, + ) { if let Some(vector_data) = document.network_interface.compute_modified_vector(layer) { if let Some(position) = manipulator_point_id.get_position(&vector_data) { 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), - _ => overlay_context.hover_manipulator_handle(position), + ManipulatorPointId::Anchor(_) => overlay_context.hover_manipulator_anchor(position, selected), + _ => overlay_context.hover_manipulator_handle(position, selected), } } else { error!("No position for hovered point"); @@ -1742,14 +1748,72 @@ impl Fsm for PathToolFsmState { }; let quad = tool_data.selection_quad(document.metadata()); - let polygon = &tool_data.lasso_polygon; + + // TODO: Calculate which points/ handles are currently in the selected region and make those have a + 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, _, _) => {} } } @@ -2119,8 +2183,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, ); } @@ -2129,8 +2194,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, ), } @@ -2208,8 +2274,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, ); } @@ -2218,8 +2285,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, ), } @@ -2239,7 +2307,7 @@ 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(); @@ -2255,7 +2323,7 @@ impl Fsm for PathToolFsmState { shape_editor.dissolve_segment(responses, segment.layer(), &vector_data, segment.segment(), segment.points()); } } else { - segment.adjusted_insert_and_select(shape_editor, responses, extend_selection); + segment.adjusted_insert_and_select(shape_editor, responses, extend_selection, point_mode); } } @@ -2327,6 +2395,22 @@ impl Fsm for PathToolFsmState { responses.add(OverlaysMessage::Draw); } } + + // If only in segment select node then 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 { @@ -2834,7 +2918,15 @@ fn update_dynamic_hints( let segment_edit = tool_options.path_editing_mode.segment_editing_mode; let point_edit = tool_options.path_editing_mode.point_editing_mode; - let hovering_point = shape_editor.find_nearest_point_indices(&document.network_interface, position, SELECTION_THRESHOLD).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 tool_data.segment.is_some() { if segment_edit { diff --git a/frontend/src/components/widgets/inputs/CheckboxInput.svelte b/frontend/src/components/widgets/inputs/CheckboxInput.svelte index f8e53eeadb..ff55336b88 100644 --- a/frontend/src/components/widgets/inputs/CheckboxInput.svelte +++ b/frontend/src/components/widgets/inputs/CheckboxInput.svelte @@ -10,6 +10,7 @@ export let checked = false; export let disabled = false; + export let frozen = true; export let icon: IconName = "Checkmark"; export let tooltip: string | undefined = undefined; export let forLabel: bigint | undefined = undefined; @@ -42,11 +43,11 @@ id={`checkbox-input-${id}`} bind:checked on:change={(_) => dispatch("checked", inputElement?.checked || false)} - {disabled} - tabindex={disabled ? -1 : 0} + disabled={disabled || frozen} + tabindex={(disabled || frozen) ? -1 : 0} bind:this={inputElement} /> -