Add Path tool support for the Tab key swapping to dragging the opposite handle (#2058)

* feat: tab alternates between handles

* fix: handle hints, remove anchor to handle switch
Added specific handle hints,
Can no longer switch to handle if just anchor is selected
typo fix

* fix: no longer deselect on esc/rclick

* feat: hides cursor when switching
A pointerlock implementation would be ideal in the future to keep the screen from panning,

* fix: tidy up dynamic tool hints
switch colinear to V

* fix: can no longer hide cursor if anchor selected
remove debug statement

* fix: clippy

* Solve some issues and remap V to C to toggle colinear

* Cleanup + change equidistant key from Shift to Alt

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
Daragh 2024-10-30 09:48:20 +00:00 committed by GitHub
parent b7ba2c3637
commit 018e9839f8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
5 changed files with 280 additions and 101 deletions

View file

@ -93,7 +93,7 @@ pub fn input_mappings() -> Mapping {
//
// SelectToolMessage
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })),
entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { add_to_selection: Shift, select_deepest: Accel }),
entry!(KeyDown(MouseLeft); action_dispatch=SelectToolMessage::DragStart { extend_selection: Shift, select_deepest: Accel }),
entry!(KeyUp(MouseLeft); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Shift }),
entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter),
entry!(DoubleClick(MouseButton::Left); action_dispatch=SelectToolMessage::EditLayer),
@ -204,19 +204,20 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
entry!(KeyDown(Delete); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { ctrl: Control, shift: Shift }),
entry!(KeyDown(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift }),
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=[Alt, Shift, Space], action_dispatch=PathToolMessage::PointerMove { alt: Alt, shift: Shift, move_anchor_and_handles: Space}),
entry!(PointerMove; refresh_keys=[KeyC, Shift, Alt, Space], action_dispatch=PathToolMessage::PointerMove { toggle_colinear: KeyC, equidistant: Alt, move_anchor_with_handles: Space}),
entry!(KeyDown(Delete); action_dispatch=PathToolMessage::Delete),
entry!(KeyDown(KeyA); modifiers=[Accel], action_dispatch=PathToolMessage::SelectAllAnchors),
entry!(KeyDown(KeyA); modifiers=[Accel, Shift], action_dispatch=PathToolMessage::DeselectAllPoints),
entry!(KeyDown(Backspace); action_dispatch=PathToolMessage::Delete),
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { equidistant: Shift }),
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { add_to_selection: Shift }),
entry!(KeyUp(MouseLeft); action_dispatch=PathToolMessage::DragStop { extend_selection: Shift }),
entry!(KeyDown(Enter); action_dispatch=PathToolMessage::Enter { extend_selection: Shift }),
entry!(DoubleClick(MouseButton::Left); action_dispatch=PathToolMessage::FlipSmoothSharp),
entry!(KeyDown(ArrowRight); action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: NUDGE_AMOUNT, delta_y: 0. }),
entry!(KeyDown(ArrowRight); modifiers=[Shift], action_dispatch=PathToolMessage::NudgeSelectedPoints { delta_x: BIG_NUDGE_AMOUNT, delta_y: 0. }),

View file

@ -4,6 +4,7 @@ use crate::messages::portfolio::document::utility_types::document_metadata::{Doc
use crate::messages::portfolio::document::utility_types::misc::{GeometrySnapSource, SnapSource};
use crate::messages::portfolio::document::utility_types::network_interface::NodeNetworkInterface;
use crate::messages::prelude::*;
use crate::messages::tool::tool_messages::path_tool::PointSelectState;
use bezier_rs::{Bezier, BezierHandles, TValue};
use graphene_core::transform::Transform;
@ -12,8 +13,9 @@ use graphene_core::vector::{ManipulatorPointId, PointId, VectorData, VectorModif
use glam::DVec2;
use graphene_std::vector::{HandleId, SegmentId};
#[derive(Debug, PartialEq, Copy, Clone)]
#[derive(Debug, PartialEq, Eq, Copy, Clone, Default)]
pub enum ManipulatorAngle {
#[default]
Colinear,
Free,
Mixed,
@ -161,9 +163,9 @@ impl ClosestSegment {
midpoint
}
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, add_to_selection: bool) {
pub fn adjusted_insert_and_select(&self, shape_editor: &mut ShapeState, responses: &mut VecDeque<Message>, extend_selection: bool) {
let id = self.adjusted_insert(responses);
shape_editor.select_anchor_point_by_id(self.layer, id, add_to_selection)
shape_editor.select_anchor_point_by_id(self.layer, id, extend_selection)
}
}
@ -221,7 +223,7 @@ impl ShapeState {
/// Select/deselect the first point within the selection threshold.
/// Returns a tuple of the points if found and the offset, or `None` otherwise.
pub fn change_point_selection(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64, add_to_selection: bool) -> Option<Option<SelectedPointsInfo>> {
pub fn change_point_selection(&mut self, network_interface: &NodeNetworkInterface, mouse_position: DVec2, select_threshold: f64, extend_selection: bool) -> Option<Option<SelectedPointsInfo>> {
if self.selected_shape_state.is_empty() {
return None;
}
@ -234,14 +236,14 @@ impl ShapeState {
let already_selected = selected_shape_state.is_selected(manipulator_point_id);
// Should we select or deselect the point?
let new_selected = if already_selected { !add_to_selection } else { true };
let new_selected = if already_selected { !extend_selection } else { true };
// Offset to snap the selected point to the cursor
let offset = mouse_position - network_interface.document_metadata().transform_to_viewport(layer).transform_point2(point_position);
// This is selecting the manipulator only for now, next to generalize to points
if new_selected {
let retain_existing_selection = add_to_selection || already_selected;
let retain_existing_selection = extend_selection || already_selected;
if !retain_existing_selection {
self.deselect_all_points();
}
@ -267,8 +269,8 @@ impl ShapeState {
None
}
pub fn select_anchor_point_by_id(&mut self, layer: LayerNodeIdentifier, id: PointId, add_to_selection: bool) {
if !add_to_selection {
pub fn select_anchor_point_by_id(&mut self, layer: LayerNodeIdentifier, id: PointId, extend_selection: bool) {
if !extend_selection {
self.deselect_all_points();
}
let point = ManipulatorPointId::Anchor(id);
@ -1060,6 +1062,71 @@ impl ShapeState {
_ => self.sorted_selected_layers(network_interface.document_metadata()).find_map(closest_seg),
}
}
pub fn get_dragging_state(&self, network_interface: &NodeNetworkInterface) -> PointSelectState {
for &layer in self.selected_shape_state.keys() {
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
for point in self.selected_points() {
if point.as_anchor().is_some() {
return PointSelectState::Anchor;
}
if point.get_handle_pair(&vector_data).is_some() {
return PointSelectState::HandleWithPair;
}
}
}
PointSelectState::HandleNoPair
}
/// Returns true if at least one handle with pair is selected
pub fn handle_with_pair_selected(&mut self, network_interface: &NodeNetworkInterface) -> bool {
for &layer in self.selected_shape_state.keys() {
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
for point in self.selected_points() {
if point.as_anchor().is_some() {
return false;
}
if point.get_handle_pair(&vector_data).is_some() {
return true;
}
}
}
false
}
/// Alternate selected handles to mirrors
pub fn alternate_selected_handles(&mut self, network_interface: &NodeNetworkInterface) {
let mut handles_to_update = Vec::new();
for &layer in self.selected_shape_state.keys() {
let Some(vector_data) = network_interface.compute_modified_vector(layer) else { continue };
for point in self.selected_points() {
if point.as_anchor().is_some() {
continue;
}
if let Some(handles) = point.get_handle_pair(&vector_data) {
// handle[0] is selected, handle[1] is opposite / mirror handle
handles_to_update.push((layer, handles[0].to_manipulator_point(), handles[1].to_manipulator_point()));
}
}
}
for (layer, handle_to_deselect, handle_to_select) in handles_to_update {
if let Some(state) = self.selected_shape_state.get_mut(&layer) {
let points = &state.selected_points;
let both_selected = points.contains(&handle_to_deselect) && points.contains(&handle_to_select);
if both_selected {
continue;
}
state.deselect_point(handle_to_deselect);
state.select_point(handle_to_select);
}
}
}
/// Selects handles and anchor connected to current handle
pub fn select_handles_and_anchor_connected_to_current_handle(&mut self, network_interface: &NodeNetworkInterface) {

View file

@ -34,10 +34,10 @@ pub enum PathToolMessage {
Delete,
DeleteAndBreakPath,
DragStop {
equidistant: Key,
extend_selection: Key,
},
Enter {
add_to_selection: Key,
extend_selection: Key,
},
Escape,
FlipSmoothSharp,
@ -48,22 +48,22 @@ pub enum PathToolMessage {
ManipulatorMakeHandlesFree,
ManipulatorMakeHandlesColinear,
MouseDown {
ctrl: Key,
shift: Key,
direct_insert_without_sliding: Key,
extend_selection: Key,
},
NudgeSelectedPoints {
delta_x: f64,
delta_y: f64,
},
PointerMove {
alt: Key,
shift: Key,
move_anchor_and_handles: Key,
equidistant: Key,
toggle_colinear: Key,
move_anchor_with_handles: Key,
},
PointerOutsideViewport {
alt: Key,
shift: Key,
move_anchor_and_handles: Key,
equidistant: Key,
toggle_colinear: Key,
move_anchor_with_handles: Key,
},
RightClick,
SelectAllAnchors,
@ -74,6 +74,7 @@ pub enum PathToolMessage {
SelectedPointYChanged {
new_y: f64,
},
SwapSelectedHandles,
}
impl ToolMetadata for PathTool {
@ -170,7 +171,19 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
fn process_message(&mut self, message: ToolMessage, responses: &mut VecDeque<Message>, tool_data: &mut ToolActionHandlerData<'a>) {
let updating_point = message == ToolMessage::Path(PathToolMessage::SelectedPointUpdated);
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true);
match message {
ToolMessage::Path(PathToolMessage::SwapSelectedHandles) => {
if tool_data.shape_editor.handle_with_pair_selected(&tool_data.document.network_interface) {
tool_data.shape_editor.alternate_selected_handles(&tool_data.document.network_interface);
responses.add(PathToolMessage::SelectedPointUpdated);
responses.add(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::None });
responses.add(OverlaysMessage::Draw);
}
}
_ => {
self.fsm_state.process_event(message, &mut self.tool_data, tool_data, &(), responses, true);
}
}
if updating_point {
self.send_layout(responses, LayoutTarget::ToolOptions);
@ -191,7 +204,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
BreakPath,
DeleteAndBreakPath,
),
PathToolFsmState::Dragging => actions!(PathToolMessageDiscriminant;
PathToolFsmState::Dragging(_) => actions!(PathToolMessageDiscriminant;
Escape,
RightClick,
FlipSmoothSharp,
@ -200,6 +213,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PathToo
Delete,
BreakPath,
DeleteAndBreakPath,
SwapSelectedHandles,
),
PathToolFsmState::DrawingBox => actions!(PathToolMessageDiscriminant;
FlipSmoothSharp,
@ -235,19 +249,32 @@ impl ToolTransition for PathTool {
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct DraggingState {
point_select_state: PointSelectState,
colinear: ManipulatorAngle,
}
#[derive(Clone, Copy, Default, Debug, PartialEq, Eq)]
pub enum PointSelectState {
HandleWithPair,
#[default]
HandleNoPair,
Anchor,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
enum PathToolFsmState {
#[default]
Ready,
Dragging,
Dragging(DraggingState),
DrawingBox,
InsertPoint,
}
enum InsertEndKind {
Abort,
Add { shift: bool },
Add { extend_selection: bool },
}
#[derive(Default)]
@ -255,7 +282,7 @@ struct PathToolData {
snap_manager: SnapManager,
drag_start_pos: DVec2,
previous_mouse_position: DVec2,
alt_debounce: bool,
toggle_colinear_debounce: bool,
opposing_handle_lengths: Option<OpposingHandleLengths>,
/// Describes information about the selected point(s), if any, across one or multiple shapes and manipulator point types (anchor or handle).
/// The available information varies depending on whether `None`, `One`, or `Multiple` points are currently selected.
@ -266,12 +293,13 @@ struct PathToolData {
auto_panning: AutoPanning,
saved_points_before_anchor_select_toggle: Vec<ManipulatorPointId>,
select_anchor_toggled: bool,
dragging_state: DraggingState,
}
impl PathToolData {
fn save_points_before_anchor_toggle(&mut self, points: Vec<ManipulatorPointId>) -> PathToolFsmState {
self.saved_points_before_anchor_select_toggle = points;
PathToolFsmState::Dragging
PathToolFsmState::Dragging(self.dragging_state)
}
fn remove_saved_points(&mut self) {
@ -308,8 +336,8 @@ impl PathToolData {
warn!("Segment was `None` before `end_insertion`")
}
Some(closed_segment) => {
if let InsertEndKind::Add { shift } = kind {
closed_segment.adjusted_insert_and_select(shape_editor, responses, shift);
if let InsertEndKind::Add { extend_selection } = kind {
closed_segment.adjusted_insert_and_select(shape_editor, responses, extend_selection);
commit_transaction = true;
}
}
@ -331,7 +359,7 @@ impl PathToolData {
document: &DocumentMessageHandler,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
add_to_selection: bool,
extend_selection: bool,
direct_insert_without_sliding: bool,
) -> PathToolFsmState {
self.double_click_handled = false;
@ -340,7 +368,7 @@ impl PathToolData {
self.drag_start_pos = input.mouse.position;
// Select the first point within the threshold (in pixels)
if let Some(selected_points) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, add_to_selection) {
if let Some(selected_points) = shape_editor.change_point_selection(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD, extend_selection) {
responses.add(DocumentMessage::StartTransaction);
if let Some(selected_points) = selected_points {
@ -348,21 +376,21 @@ impl PathToolData {
self.start_dragging_point(selected_points, input, document, shape_editor);
responses.add(OverlaysMessage::Draw);
}
PathToolFsmState::Dragging
PathToolFsmState::Dragging(self.dragging_state)
}
// We didn't find a point nearby, so now we'll try to add a point into the closest path segment
else if let Some(closed_segment) = shape_editor.upper_closest_segment(&document.network_interface, input.mouse.position, SELECTION_TOLERANCE) {
responses.add(DocumentMessage::StartTransaction);
if direct_insert_without_sliding {
self.start_insertion(responses, closed_segment);
self.end_insertion(shape_editor, responses, InsertEndKind::Add { shift: add_to_selection })
self.end_insertion(shape_editor, responses, InsertEndKind::Add { extend_selection })
} else {
self.start_insertion(responses, closed_segment)
}
}
// We didn't find a segment path, so consider selecting the nearest shape instead
else if let Some(layer) = document.click(input) {
if add_to_selection {
if extend_selection {
responses.add(NodeGraphMessage::SelectedNodesAdd { nodes: vec![layer.to_node()] });
} else {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![layer.to_node()] });
@ -372,7 +400,8 @@ impl PathToolData {
shape_editor.select_connected_anchors(document, layer, input.mouse.position);
responses.add(DocumentMessage::StartTransaction);
PathToolFsmState::Dragging
PathToolFsmState::Dragging(self.dragging_state)
}
// Start drawing a box
else {
@ -413,9 +442,9 @@ impl PathToolData {
self.previous_mouse_position = viewport_to_document.transform_point2(input.mouse.position - selected_points.offset);
}
fn update_colinear(&mut self, shift: bool, alt: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> bool {
// Check if the alt key has just been pressed
if alt && !self.alt_debounce {
fn update_colinear(&mut self, equidistant: bool, toggle_colinear: bool, shape_editor: &mut ShapeState, document: &DocumentMessageHandler, responses: &mut VecDeque<Message>) -> bool {
// Check if the toggle_colinear key has just been pressed
if toggle_colinear && !self.toggle_colinear_debounce {
self.opposing_handle_lengths = None;
let colinear = self.selection_status.angle().map_or(false, |angle| match angle {
ManipulatorAngle::Colinear => true,
@ -427,12 +456,12 @@ impl PathToolData {
} else {
shape_editor.convert_selected_manipulators_to_colinear_handles(responses, document);
}
self.alt_debounce = true;
self.toggle_colinear_debounce = true;
return true;
}
self.alt_debounce = alt;
self.toggle_colinear_debounce = toggle_colinear;
if shift && self.opposing_handle_lengths.is_none() {
if equidistant && self.opposing_handle_lengths.is_none() {
self.opposing_handle_lengths = Some(shape_editor.opposing_handle_lengths(document));
}
false
@ -481,7 +510,7 @@ impl Fsm for PathToolFsmState {
overlay_context.quad(Quad::from_box([tool_data.drag_start_pos, tool_data.previous_mouse_position]), Some(&("#".to_string() + &fill_color)));
}
Self::Dragging => {
Self::Dragging(_) => {
tool_data.snap_manager.draw_overlays(SnapData::new(document, input), &mut overlay_context);
}
Self::InsertPoint => {
@ -502,11 +531,10 @@ impl Fsm for PathToolFsmState {
}
// `Self::InsertPoint` case:
(Self::InsertPoint, PathToolMessage::MouseDown { .. } | PathToolMessage::Enter { .. }) => {
(Self::InsertPoint, PathToolMessage::MouseDown { extend_selection, .. } | PathToolMessage::Enter { extend_selection }) => {
tool_data.double_click_handled = true;
// TODO: Don't use `Key::Shift` directly, instead take it as a variable from the input mappings list like in all other places
let shift = input.keyboard.get(Key::Shift as usize);
tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { shift })
let extend_selection = input.keyboard.get(extend_selection as usize);
tool_data.end_insertion(shape_editor, responses, InsertEndKind::Add { extend_selection })
}
(Self::InsertPoint, PathToolMessage::PointerMove { .. }) => {
responses.add(OverlaysMessage::Draw);
@ -529,26 +557,56 @@ impl Fsm for PathToolFsmState {
tool_data.end_insertion(shape_editor, responses, InsertEndKind::Abort)
}
// Mouse down
(_, PathToolMessage::MouseDown { ctrl, shift }) => {
let add_to_selection = input.keyboard.get(shift as usize);
let direct_insert_without_sliding = input.keyboard.get(ctrl as usize);
tool_data.mouse_down(shape_editor, document, input, responses, add_to_selection, direct_insert_without_sliding)
(
_,
PathToolMessage::MouseDown {
direct_insert_without_sliding,
extend_selection,
},
) => {
let extend_selection = input.keyboard.get(extend_selection as usize);
let direct_insert_without_sliding = input.keyboard.get(direct_insert_without_sliding as usize);
tool_data.mouse_down(shape_editor, document, input, responses, extend_selection, direct_insert_without_sliding)
}
(PathToolFsmState::DrawingBox, PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }) => {
(
PathToolFsmState::DrawingBox,
PathToolMessage::PointerMove {
equidistant,
toggle_colinear,
move_anchor_with_handles,
},
) => {
tool_data.previous_mouse_position = input.mouse.position;
responses.add(OverlaysMessage::Draw);
// Auto-panning
let messages = [
PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(),
PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(),
PathToolMessage::PointerOutsideViewport {
equidistant,
toggle_colinear,
move_anchor_with_handles,
}
.into(),
PathToolMessage::PointerMove {
equidistant,
toggle_colinear,
move_anchor_with_handles,
}
.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
PathToolFsmState::DrawingBox
}
(PathToolFsmState::Dragging, PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }) => {
let anchor_and_handle_toggled = input.keyboard.get(move_anchor_and_handles as usize);
(
PathToolFsmState::Dragging(_),
PathToolMessage::PointerMove {
equidistant,
toggle_colinear,
move_anchor_with_handles,
},
) => {
let anchor_and_handle_toggled = input.keyboard.get(move_anchor_with_handles as usize);
let initial_press = anchor_and_handle_toggled && !tool_data.select_anchor_toggled;
let released_from_toggle = tool_data.select_anchor_toggled && !anchor_and_handle_toggled;
@ -565,63 +623,89 @@ impl Fsm for PathToolFsmState {
tool_data.remove_saved_points();
}
let alt_state = input.keyboard.get(alt as usize);
let shift_state = input.keyboard.get(shift as usize);
if !tool_data.update_colinear(shift_state, alt_state, shape_editor, document, responses) {
tool_data.drag(shift_state, shape_editor, document, input, responses);
let toggle_colinear_state = input.keyboard.get(toggle_colinear as usize);
let equidistant_state = input.keyboard.get(equidistant as usize);
if !tool_data.update_colinear(equidistant_state, toggle_colinear_state, shape_editor, document, responses) {
tool_data.drag(equidistant_state, shape_editor, document, input, responses);
}
// Auto-panning
let messages = [
PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(),
PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(),
PathToolMessage::PointerOutsideViewport {
toggle_colinear,
equidistant,
move_anchor_with_handles,
}
.into(),
PathToolMessage::PointerMove {
toggle_colinear,
equidistant,
move_anchor_with_handles,
}
.into(),
];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
PathToolFsmState::Dragging
PathToolFsmState::Dragging(tool_data.dragging_state)
}
(PathToolFsmState::DrawingBox, PathToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning
if let Some(shift) = tool_data.auto_panning.shift_viewport(input, responses) {
tool_data.drag_start_pos += shift;
if let Some(offset) = tool_data.auto_panning.shift_viewport(input, responses) {
tool_data.drag_start_pos += offset;
}
PathToolFsmState::DrawingBox
}
(PathToolFsmState::Dragging, PathToolMessage::PointerOutsideViewport { shift, .. }) => {
(PathToolFsmState::Dragging(dragging_state), PathToolMessage::PointerOutsideViewport { equidistant, .. }) => {
// Auto-panning
if tool_data.auto_panning.shift_viewport(input, responses).is_some() {
let shift_state = input.keyboard.get(shift as usize);
tool_data.drag(shift_state, shape_editor, document, input, responses);
let equidistant = input.keyboard.get(equidistant as usize);
tool_data.drag(equidistant, shape_editor, document, input, responses);
}
PathToolFsmState::Dragging
PathToolFsmState::Dragging(dragging_state)
}
(state, PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }) => {
(
state,
PathToolMessage::PointerOutsideViewport {
equidistant,
toggle_colinear,
move_anchor_with_handles,
},
) => {
// Auto-panning
let messages = [
PathToolMessage::PointerOutsideViewport { alt, shift, move_anchor_and_handles }.into(),
PathToolMessage::PointerMove { alt, shift, move_anchor_and_handles }.into(),
PathToolMessage::PointerOutsideViewport {
equidistant,
toggle_colinear,
move_anchor_with_handles,
}
.into(),
PathToolMessage::PointerMove {
equidistant,
toggle_colinear,
move_anchor_with_handles,
}
.into(),
];
tool_data.auto_panning.stop(&messages, responses);
state
}
(PathToolFsmState::DrawingBox, PathToolMessage::Enter { add_to_selection }) => {
let shift_pressed = input.keyboard.get(add_to_selection as usize);
(PathToolFsmState::DrawingBox, PathToolMessage::Enter { extend_selection }) => {
let extend_selection = input.keyboard.get(extend_selection as usize);
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
} else {
shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !shift_pressed);
shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !extend_selection);
}
responses.add(OverlaysMessage::Draw);
PathToolFsmState::Ready
}
(PathToolFsmState::Dragging, PathToolMessage::Escape | PathToolMessage::RightClick) => {
(PathToolFsmState::Dragging { .. }, PathToolMessage::Escape | PathToolMessage::RightClick) => {
responses.add(DocumentMessage::AbortTransaction);
shape_editor.deselect_all_points();
tool_data.snap_manager.cleanup(responses);
PathToolFsmState::Ready
}
@ -630,20 +714,20 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Ready
}
// Mouse up
(PathToolFsmState::DrawingBox, PathToolMessage::DragStop { equidistant }) => {
let equidistant = input.keyboard.get(equidistant as usize);
(PathToolFsmState::DrawingBox, PathToolMessage::DragStop { extend_selection }) => {
let extend_selection = input.keyboard.get(extend_selection as usize);
if tool_data.drag_start_pos == tool_data.previous_mouse_position {
responses.add(NodeGraphMessage::SelectedNodesSet { nodes: vec![] });
} else {
shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !equidistant);
shape_editor.select_all_in_quad(&document.network_interface, [tool_data.drag_start_pos, tool_data.previous_mouse_position], !extend_selection);
}
responses.add(OverlaysMessage::Draw);
responses.add(PathToolMessage::SelectedPointUpdated);
PathToolFsmState::Ready
}
(_, PathToolMessage::DragStop { equidistant }) => {
(_, PathToolMessage::DragStop { extend_selection }) => {
if tool_data.select_anchor_toggled {
shape_editor.deselect_all_points();
shape_editor.select_points_by_manipulator_id(&tool_data.saved_points_before_anchor_select_toggle);
@ -651,12 +735,12 @@ impl Fsm for PathToolFsmState {
tool_data.select_anchor_toggled = false;
}
let equidistant = input.keyboard.get(equidistant as usize);
let extend_selection = input.keyboard.get(extend_selection as usize);
let nearest_point = shape_editor.find_nearest_point_indices(&document.network_interface, input.mouse.position, SELECTION_THRESHOLD);
if let Some((layer, nearest_point)) = nearest_point {
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !equidistant {
if tool_data.drag_start_pos.distance(input.mouse.position) <= DRAG_THRESHOLD && !extend_selection {
let clicked_selected = shape_editor.selected_points().any(|&point| nearest_point == point);
if clicked_selected {
shape_editor.deselect_all_points();
@ -729,6 +813,11 @@ impl Fsm for PathToolFsmState {
PathToolFsmState::Ready
}
(_, PathToolMessage::SelectedPointUpdated) => {
let colinear = shape_editor.selected_manipulator_angles(&document.network_interface);
tool_data.dragging_state = DraggingState {
point_select_state: shape_editor.get_dragging_state(&document.network_interface),
colinear,
};
tool_data.selection_status = get_selection_status(&document.network_interface, shape_editor);
self
}
@ -767,19 +856,41 @@ impl Fsm for PathToolFsmState {
HintInfo::keys([Key::Shift], "Break Anchor").prepend_plus(),
]),
]),
PathToolFsmState::Dragging => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![
// TODO: Switch this to the "S" key. Also, make the hint dynamically say "Make Colinear" or "Make Not Colinear" based on its current state. And only
// TODO: show this hint if a handle (not an anchor) is being dragged, and disable that shortcut so it can't be pressed even with the hint not shown.
HintInfo::keys([Key::Alt], "Toggle Colinear Handles"),
// TODO: Switch this to the "Alt" key (since it's equivalent to the "From Center" modifier when drawing a line). And show this only when a handle is being dragged.
HintInfo::keys([Key::Shift], "Equidistant Handles"),
// TODO: Add "Snap 15°" modifier with the "Shift" key (only when a handle is being dragged).
// TODO: Add "Lock Angle" modifier with the "Ctrl" key (only when a handle is being dragged).
HintInfo::keys([Key::Space], "Drag anchor"),
]),
]),
PathToolFsmState::Dragging(dragging_state) => {
let colinear = dragging_state.colinear;
let mut dragging_hint_data = HintData(Vec::new());
dragging_hint_data
.0
.push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]));
let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor");
let point_select_state_hint_group = match dragging_state.point_select_state {
PointSelectState::HandleNoPair => vec![drag_anchor],
PointSelectState::HandleWithPair => {
let mut hints = vec![drag_anchor];
hints.push(HintInfo::keys([Key::Tab], "Swap Selected Handles"));
hints.push(HintInfo::keys(
[Key::KeyC],
if colinear == ManipulatorAngle::Colinear {
"Break Colinear Handles"
} else {
"Make Handles Colinear"
},
));
if colinear != ManipulatorAngle::Free {
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
}
hints
}
PointSelectState::Anchor => Vec::new(),
};
if !point_select_state_hint_group.is_empty() {
dragging_hint_data.0.push(HintGroup(point_select_state_hint_group));
}
dragging_hint_data
}
PathToolFsmState::DrawingBox => HintData(vec![
HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]),
HintGroup(vec![

View file

@ -71,7 +71,7 @@ pub enum SelectToolMessage {
Overlays(OverlayContext),
// Tool-specific messages
DragStart { add_to_selection: Key, select_deepest: Key },
DragStart { extend_selection: Key, select_deepest: Key },
DragStop { remove_from_selection: Key },
EditLayer,
Enter,
@ -517,7 +517,7 @@ impl Fsm for SelectToolFsmState {
self
}
(SelectToolFsmState::Ready { .. }, SelectToolMessage::DragStart { add_to_selection, select_deepest }) => {
(SelectToolFsmState::Ready { .. }, SelectToolMessage::DragStart { extend_selection, select_deepest }) => {
tool_data.drag_start = input.mouse.position;
tool_data.drag_current = input.mouse.position;
@ -647,7 +647,7 @@ impl Fsm for SelectToolFsmState {
else {
tool_data.layers_dragging = selected;
if !input.keyboard.key(add_to_selection) {
if !input.keyboard.key(extend_selection) {
responses.add(DocumentMessage::DeselectAllLayers);
tool_data.layers_dragging.clear();
}

View file

@ -306,7 +306,7 @@ impl ManipulatorPointId {
}
}
/// Attempt to get a pair of handles. For an anchor this is the first to handles connected. For a handle it is self and the first opposing handle.
/// Attempt to get a pair of handles. For an anchor this is the first two handles connected. For a handle it is self and the first opposing handle.
#[must_use]
pub fn get_handle_pair(self, vector_data: &VectorData) -> Option<[HandleId; 2]> {
match self {