diff --git a/editor/src/messages/input_mapper/input_mappings.rs b/editor/src/messages/input_mapper/input_mappings.rs index 02dafca170..0e3c994782 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/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 86cdd85824..030eb284e5 100644 --- a/editor/src/messages/portfolio/document/overlays/utility_types.rs +++ b/editor/src/messages/portfolio/document/overlays/utility_types.rs @@ -319,6 +319,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`]. 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 a8b10afaa9..af336191bb 100644 --- a/editor/src/messages/tool/tool_messages/path_tool.rs +++ b/editor/src/messages/tool/tool_messages/path_tool.rs @@ -76,7 +76,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, @@ -90,6 +90,7 @@ pub enum PathToolMessage { lock_angle: Key, delete_segment: Key, break_colinear_molding: Key, + segment_editing_modifier: Key, }, PointerOutsideViewport { equidistant: Key, @@ -99,6 +100,7 @@ pub enum PathToolMessage { lock_angle: Key, delete_segment: Key, break_colinear_molding: Key, + segment_editing_modifier: Key, }, RightClick, SelectAllAnchors, @@ -117,6 +119,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,14 +241,16 @@ 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") + .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") - .on_update(|input| PathToolMessage::UpdateOptions(PathOptionsUpdate::SegmentEditingMode { enabled: input.checked }).into()) + .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(); let path_overlay_mode_widget = RadioInput::new(vec![ @@ -388,6 +394,8 @@ impl<'a> MessageHandler> for Path DeleteAndBreakPath, ClosePath, PointerMove, + TogglePointEdit, + ToggleSegmentEdit ), PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant; Escape, @@ -399,6 +407,8 @@ impl<'a> MessageHandler> for Path BreakPath, DeleteAndBreakPath, SwapSelectedHandles, + TogglePointEdit, + ToggleSegmentEdit ), PathToolFsmState::Drawing { .. } => actions!(PathToolMessageDiscriminant; DoubleClick, @@ -410,6 +420,8 @@ impl<'a> MessageHandler> for Path DeleteAndBreakPath, Escape, RightClick, + TogglePointEdit, + ToggleSegmentEdit ), PathToolFsmState::SlidingPoint => actions!(PathToolMessageDiscriminant; PointerMove, @@ -489,6 +501,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, @@ -633,7 +647,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, @@ -659,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); @@ -677,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; } @@ -752,7 +766,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)); @@ -769,6 +783,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; @@ -1116,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; @@ -1447,7 +1463,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 }; @@ -1476,6 +1492,85 @@ 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 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 { + 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)); + } + } + } + + // Deselect all of the segments + shape_editor.deselect_all_segments(); + } + + self + } + (_, 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 { + 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)) => { if matches!(self, Self::Dragging(_)) { for (outline, transform) in &tool_data.ghost_outline { @@ -1547,8 +1642,33 @@ impl Fsm for PathToolFsmState { Self::Ready => { tool_data.update_closest_segment(shape_editor, input.mouse.position, document, tool_options.path_overlay_mode); + // 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_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, selected), + _ => overlay_context.hover_manipulator_handle(position, selected), + } + } else { + error!("No position for hovered point"); + } + } + } + } + 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); @@ -1566,6 +1686,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); @@ -1627,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, _, _) => {} } } @@ -1680,14 +1859,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(); @@ -1701,7 +1880,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, @@ -1717,6 +1896,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); @@ -1740,6 +1920,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1750,6 +1931,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -1767,6 +1949,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(_))); @@ -1852,6 +2035,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1862,6 +2046,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -1873,8 +2058,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; @@ -1926,6 +2121,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, }, ) => { // Auto-panning @@ -1938,6 +2134,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), PathToolMessage::PointerMove { @@ -1948,6 +2145,7 @@ impl Fsm for PathToolFsmState { lock_angle, delete_segment, break_colinear_molding, + segment_editing_modifier, } .into(), ]; @@ -1985,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, ); } @@ -1995,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, ), } @@ -2074,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, ); } @@ -2084,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, ), } @@ -2105,20 +2307,23 @@ 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); + segment.adjusted_insert_and_select(shape_editor, responses, extend_selection, point_mode); } } @@ -2129,6 +2334,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); @@ -2164,6 +2370,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, 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()); } @@ -2178,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 { @@ -2648,6 +2881,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 @@ -2682,26 +2916,53 @@ 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_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 { + // 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 { diff --git a/frontend/src/components/widgets/inputs/CheckboxInput.svelte b/frontend/src/components/widgets/inputs/CheckboxInput.svelte index f8e53eeadb..536c5cbcd9 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} /> -