Improve the Pen tool's colinearity and equidistance controls (#2242)

* basic implementation done now refactor

* fixed overlays refactoring need to fix colinear(update it)

* more_refactoring ,only toggle C for grs to be done(if required)

* cleanup

* cleanup

* more formatting checks

* refactoring alt fixed hints fixed

* code-review-changes

* path-tool-tab-fix

* fixed bugs

* some refactor

* fixed ctrl_snap

* added lock-overlays and fixed grs bug

* Code review

---------

Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
0SlowPoke0 2025-02-05 08:45:43 +05:30 committed by GitHub
parent 133d872a9f
commit 41ee1cf8bc
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 354 additions and 78 deletions

View File

@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath), entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Delete); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath), entry!(KeyDown(Backspace); modifiers=[Shift], action_dispatch=PathToolMessage::BreakPath),
entry!(KeyDown(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles), entry!(KeyDownNoRepeat(Tab); action_dispatch=PathToolMessage::SwapSelectedHandles),
entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control }), entry!(KeyDown(MouseLeft); action_dispatch=PathToolMessage::MouseDown { direct_insert_without_sliding: Control, extend_selection: Shift, lasso_select: Control }),
entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick), entry!(KeyDown(MouseRight); action_dispatch=PathToolMessage::RightClick),
entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape), entry!(KeyDown(Escape); action_dispatch=PathToolMessage::Escape),
@ -254,7 +254,7 @@ pub fn input_mappings() -> Mapping {
entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=ToolMessage::Path(PathToolMessage::ClosePath)), entry!(KeyDown(KeyJ); modifiers=[Accel], action_dispatch=ToolMessage::Path(PathToolMessage::ClosePath)),
// //
// PenToolMessage // PenToolMessage
entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control}), entry!(PointerMove; refresh_keys=[Control, Alt, Shift, KeyC], action_dispatch=PenToolMessage::PointerMove { snap_angle: Shift, break_handle: Alt, lock_angle: Control, colinear: KeyC }),
entry!(KeyDown(MouseLeft); action_dispatch=PenToolMessage::DragStart { append_to_selected: Shift }), entry!(KeyDown(MouseLeft); action_dispatch=PenToolMessage::DragStart { append_to_selected: Shift }),
entry!(KeyUp(MouseLeft); action_dispatch=PenToolMessage::DragStop), entry!(KeyUp(MouseLeft); action_dispatch=PenToolMessage::DragStop),
entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm), entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm),

View File

@ -1185,16 +1185,9 @@ impl Fsm for PathToolFsmState {
.push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()])); .push(HintGroup(vec![HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::keys([Key::Escape], "Cancel").prepend_slash()]));
let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor"); let drag_anchor = HintInfo::keys([Key::Space], "Drag Anchor");
let point_select_state_hint_group = match dragging_state.point_select_state { let toggle_group = match dragging_state.point_select_state {
PointSelectState::HandleNoPair => { PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => {
let mut hints = vec![drag_anchor]; let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Selected Handles")];
hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
hints
}
PointSelectState::HandleWithPair => {
let mut hints = vec![drag_anchor];
hints.push(HintInfo::keys([Key::Tab], "Swap Selected Handles"));
hints.push(HintInfo::keys( hints.push(HintInfo::keys(
[Key::KeyC], [Key::KeyC],
if colinear == ManipulatorAngle::Colinear { if colinear == ManipulatorAngle::Colinear {
@ -1203,18 +1196,40 @@ impl Fsm for PathToolFsmState {
"Make Handles Colinear" "Make Handles Colinear"
}, },
)); ));
hints
}
PointSelectState::Anchor => Vec::new(),
};
let hold_group = match dragging_state.point_select_state {
PointSelectState::HandleNoPair => {
let mut hints = vec![];
if colinear != ManipulatorAngle::Free { if colinear != ManipulatorAngle::Free {
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles")); hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
} }
hints.push(HintInfo::keys([Key::Shift], "Snap 15°")); hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
hints.push(HintInfo::keys([Key::Control], "Lock Angle")); hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
hints.push(drag_anchor);
hints
}
PointSelectState::HandleWithPair => {
let mut hints = vec![];
if colinear != ManipulatorAngle::Free {
hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
}
hints.push(HintInfo::keys([Key::Shift], "Snap 15°"));
hints.push(HintInfo::keys([Key::Control], "Lock Angle"));
hints.push(drag_anchor);
hints hints
} }
PointSelectState::Anchor => Vec::new(), PointSelectState::Anchor => Vec::new(),
}; };
if !point_select_state_hint_group.is_empty() { if !toggle_group.is_empty() {
dragging_hint_data.0.push(HintGroup(point_select_state_hint_group)); dragging_hint_data.0.push(HintGroup(toggle_group));
}
if !hold_group.is_empty() {
dragging_hint_data.0.push(HintGroup(hold_group));
} }
dragging_hint_data dragging_hint_data

View File

@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles};
use graph_craft::document::NodeId; use graph_craft::document::NodeId;
use graphene_core::vector::{PointId, VectorModificationType}; use graphene_core::vector::{PointId, VectorModificationType};
use graphene_core::Color; use graphene_core::Color;
use graphene_std::vector::{HandleId, SegmentId}; use graphene_std::vector::{HandleId, ManipulatorPointId, SegmentId, VectorData};
#[derive(Default)] #[derive(Default)]
pub struct PenTool { pub struct PenTool {
@ -56,8 +56,8 @@ pub enum PenToolMessage {
Confirm, Confirm,
DragStart { append_to_selected: Key }, DragStart { append_to_selected: Key },
DragStop, DragStop,
PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key }, PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key },
PointerOutsideViewport { snap_angle: Key, break_handle: Key, lock_angle: Key }, PointerOutsideViewport { snap_angle: Key, break_handle: Key, lock_angle: Key, colinear: Key },
Redo, Redo,
Undo, Undo,
UpdateOptions(PenOptionsUpdate), UpdateOptions(PenOptionsUpdate),
@ -71,7 +71,7 @@ pub enum PenToolMessage {
enum PenToolFsmState { enum PenToolFsmState {
#[default] #[default]
Ready, Ready,
DraggingHandle, DraggingHandle(HandleMode),
PlacingAnchor, PlacingAnchor,
GRSHandle, GRSHandle,
} }
@ -174,7 +174,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
PointerMove, PointerMove,
FinalPosition FinalPosition
), ),
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant; PenToolFsmState::DraggingHandle(_) | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
DragStart, DragStart,
DragStop, DragStop,
PointerMove, PointerMove,
@ -203,6 +203,7 @@ struct ModifierState {
snap_angle: bool, snap_angle: bool,
lock_angle: bool, lock_angle: bool,
break_handle: bool, break_handle: bool,
colinear: bool,
} }
#[derive(Clone, Debug)] #[derive(Clone, Debug)]
struct LastPoint { struct LastPoint {
@ -212,6 +213,17 @@ struct LastPoint {
handle_start: DVec2, handle_start: DVec2,
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
enum HandleMode {
/// Pressing 'C' breaks colinearity
Free,
/// Pressing 'Alt': Handle length is locked
#[default]
ColinearLocked,
/// Pressing 'Alt': Handles are equidistant
ColinearEquidistant,
}
#[derive(Clone, Debug, Default)] #[derive(Clone, Debug, Default)]
struct PenToolData { struct PenToolData {
snap_manager: SnapManager, snap_manager: SnapManager,
@ -222,6 +234,9 @@ struct PenToolData {
next_handle_start: DVec2, next_handle_start: DVec2,
g1_continuous: bool, g1_continuous: bool,
toggle_colinear_debounce: bool,
segment_end_before_bent: Option<SegmentId>,
angle: f64, angle: f64,
auto_panning: AutoPanning, auto_panning: AutoPanning,
@ -229,7 +244,11 @@ struct PenToolData {
buffering_merged_vector: bool, buffering_merged_vector: bool,
before_grs_pos: DVec2, previous_handle_start_pos: DVec2,
previous_handle_end_pos: Option<DVec2>,
alt_press: bool,
handle_mode: HandleMode,
} }
impl PenToolData { impl PenToolData {
fn latest_point(&self) -> Option<&LastPoint> { fn latest_point(&self) -> Option<&LastPoint> {
@ -265,19 +284,24 @@ impl PenToolData {
} }
/// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged. /// If the user places the anchor on top of the previous anchor, it becomes sharp and the outgoing handle may be dragged.
fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2) { fn bend_from_previous_point(&mut self, snap_data: SnapData, transform: DAffine2, layer: LayerNodeIdentifier) {
self.g1_continuous = true; self.g1_continuous = true;
let document = snap_data.document; let document = snap_data.document;
self.next_handle_start = self.next_point; self.next_handle_start = self.next_point;
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
// Break the control // Break the control
let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return }; let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return };
let transform = document.metadata().document_to_viewport * transform; let transform = document.metadata().document_to_viewport * transform;
let on_top = transform.transform_point2(self.next_point).distance_squared(transform.transform_point2(last_pos)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2); let on_top = transform.transform_point2(self.next_point).distance_squared(transform.transform_point2(last_pos)) < crate::consts::SNAP_POINT_TOLERANCE.powi(2);
if on_top { if on_top {
if let Some(point) = self.latest_point_mut() { if let Some(point) = self.latest_point_mut() {
point.in_segment = None; point.in_segment = None;
} }
self.segment_end_before_bent = vector_data.segment_domain.ids().last().copied();
self.handle_mode = HandleMode::Free;
self.handle_end = None; self.handle_end = None;
} }
} }
@ -326,6 +350,7 @@ impl PenToolData {
let points = [start, end]; let points = [start, end];
let id = SegmentId::generate(); let id = SegmentId::generate();
self.segment_end_before_bent = Some(id);
let modification_type = VectorModificationType::InsertSegment { id, points, handles }; let modification_type = VectorModificationType::InsertSegment { id, points, handles };
responses.add(GraphOperationMessage::Vector { layer, modification_type }); responses.add(GraphOperationMessage::Vector { layer, modification_type });
@ -351,19 +376,107 @@ impl PenToolData {
Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor }) Some(if close_subpath { PenToolFsmState::Ready } else { PenToolFsmState::PlacingAnchor })
} }
fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>, layer: Option<LayerNodeIdentifier>) -> Option<PenToolFsmState> {
let colinear = !self.modifiers.break_handle && self.handle_end.is_some(); let colinear = (self.handle_mode == HandleMode::ColinearEquidistant && self.modifiers.break_handle) || (self.handle_mode == HandleMode::ColinearLocked && !self.modifiers.break_handle);
let document = snap_data.document;
self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false); self.next_handle_start = self.compute_snapped_angle(snap_data, transform, colinear, mouse, Some(self.next_point), false);
if let Some(handle_end) = self.handle_end.as_mut().filter(|_| colinear) { let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) };
*handle_end = self.next_point * 2. - self.next_handle_start; let vector_data = document.network_interface.compute_modified_vector(layer)?;
self.g1_continuous = true;
} else { match self.handle_mode {
self.g1_continuous = false; HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => {
self.g1_continuous = true;
self.colinear(responses, layer, self.next_handle_start, self.next_point, &vector_data);
self.adjust_handle_length(responses, layer, &vector_data);
}
HandleMode::Free => {
self.g1_continuous = false;
}
} }
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
Some(PenToolFsmState::DraggingHandle) Some(PenToolFsmState::DraggingHandle(self.handle_mode))
}
/// Makes the opposite handle equidistant or locks its length.
fn adjust_handle_length(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) {
let Some(latest) = self.latest_point() else { return };
let anchor_pos = latest.pos;
match self.handle_mode {
HandleMode::ColinearEquidistant => self.adjust_equidistant_handle(anchor_pos, responses, layer, vector_data),
HandleMode::ColinearLocked => self.adjust_locked_length_handle(anchor_pos, responses, layer),
HandleMode::Free => {} // No adjustments needed in free mode
}
}
fn colinear(&mut self, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, handle_start: DVec2, anchor_point: DVec2, vector_data: &VectorData) {
let Some(direction) = (anchor_point - handle_start).try_normalize() else {
log::trace!("Skipping colinear adjustment: handle_start and anchor_point are too close!");
return;
};
let handle_offset = if let Some(handle_end) = self.handle_end {
(handle_end - anchor_point).length()
} else {
let Some(segment) = self.segment_end_before_bent else { return };
let end_handle = ManipulatorPointId::EndHandle(segment);
let Some(end_handle) = end_handle.get_position(vector_data) else { return };
(end_handle - anchor_point).length()
};
let new_handle_position = anchor_point + handle_offset * direction;
self.update_handle_position(new_handle_position, anchor_point, responses, layer);
}
fn adjust_equidistant_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier, vector_data: &VectorData) {
if self.modifiers.break_handle {
self.store_handle(vector_data);
self.alt_press = true;
let new_position = self.next_point * 2. - self.next_handle_start;
self.update_handle_position(new_position, anchor_pos, responses, layer);
} else {
self.restore_previous_handle(anchor_pos, responses, layer);
}
}
fn adjust_locked_length_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
if !self.modifiers.break_handle {
let new_position = self.next_point * 2. - self.next_handle_start;
self.update_handle_position(new_position, anchor_pos, responses, layer);
}
}
/// Temporarily stores the opposite handle position to revert back when Alt is released in equidistant mode.
fn store_handle(&mut self, vector_data: &VectorData) {
if !self.alt_press {
self.previous_handle_end_pos = self.handle_end.or_else(|| {
let segment = self.segment_end_before_bent?;
ManipulatorPointId::EndHandle(segment).get_position(vector_data)
});
}
}
fn restore_previous_handle(&mut self, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
if self.alt_press {
self.alt_press = false;
if let Some(previous_handle) = self.previous_handle_end_pos {
self.update_handle_position(previous_handle, anchor_pos, responses, layer);
}
self.previous_handle_end_pos = None; // Reset storage
}
}
fn update_handle_position(&mut self, new_position: DVec2, anchor_pos: DVec2, responses: &mut VecDeque<Message>, layer: LayerNodeIdentifier) {
if let Some(handle) = self.handle_end.as_mut() {
*handle = new_position;
} else {
let Some(segment) = self.segment_end_before_bent else { return };
let relative_position = new_position - anchor_pos;
let modification_type = VectorModificationType::SetEndHandle { segment, relative_position };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
} }
fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> { fn place_anchor(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, preferences: &PreferencesMessageHandler, responses: &mut VecDeque<Message>) -> Option<PenToolFsmState> {
@ -455,7 +568,7 @@ impl PenToolData {
} }
if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) { if let Some(relative) = relative.map(|layer| transform.transform_point2(layer)) {
if (relative - document_pos) != DVec2::ZERO { if (relative - document_pos) != DVec2::ZERO && (relative - document_pos).length_squared() > f64::EPSILON * 100. {
self.angle = -(relative - document_pos).angle_to(DVec2::X) self.angle = -(relative - document_pos).angle_to(DVec2::X)
} }
} }
@ -571,36 +684,67 @@ impl Fsm for PenToolFsmState {
let ToolMessage::Pen(event) = event else { return self }; let ToolMessage::Pen(event) = event else { return self };
match (self, event) { match (self, event) {
(PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => { (PenToolFsmState::PlacingAnchor | PenToolFsmState::GRSHandle, PenToolMessage::GRS { grab, rotate, scale }) => {
let Some(latest) = tool_data.latest_point_mut() else { return PenToolFsmState::PlacingAnchor };
let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor }; let Some(layer) = layer else { return PenToolFsmState::PlacingAnchor };
if latest.handle_start != latest.pos { let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::PlacingAnchor };
let viewport = document.metadata().transform_to_viewport(layer); if latest.handle_start == latest.pos {
let last_point = viewport.transform_point2(latest.pos); return PenToolFsmState::PlacingAnchor;
let handle = viewport.transform_point2(latest.handle_start);
if input.keyboard.key(grab) {
responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle });
} else if input.keyboard.key(rotate) {
responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle });
} else if input.keyboard.key(scale) {
responses.add(TransformLayerMessage::BeginScalePen { last_point, handle });
}
tool_data.before_grs_pos = latest.handle_start;
} }
let viewport = document.metadata().transform_to_viewport(layer);
let last_point = viewport.transform_point2(latest.pos);
let handle = viewport.transform_point2(latest.handle_start);
if input.keyboard.key(grab) {
responses.add(TransformLayerMessage::BeginGrabPen { last_point, handle });
} else if input.keyboard.key(rotate) {
responses.add(TransformLayerMessage::BeginRotatePen { last_point, handle });
} else if input.keyboard.key(scale) {
responses.add(TransformLayerMessage::BeginScalePen { last_point, handle });
}
tool_data.previous_handle_start_pos = latest.handle_start;
// Store the handle_end position
let segment = tool_data.segment_end_before_bent;
if let Some(segment) = segment {
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
tool_data.previous_handle_end_pos = ManipulatorPointId::EndHandle(segment).get_position(&vector_data);
}
PenToolFsmState::GRSHandle PenToolFsmState::GRSHandle
} }
(PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position: final_pos }) => { (PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position }) => {
let Some(layer) = layer else { return PenToolFsmState::GRSHandle }; let Some(layer) = layer else { return PenToolFsmState::GRSHandle };
let vector_data = document.network_interface.compute_modified_vector(layer);
let Some(vector_data) = vector_data else { return PenToolFsmState::GRSHandle };
if let Some(latest_pt) = tool_data.latest_point_mut() { if let Some(latest_pt) = tool_data.latest_point_mut() {
let layer_space_to_viewport = document.metadata().transform_to_viewport(layer); let layer_space_to_viewport = document.metadata().transform_to_viewport(layer);
let final_pos = layer_space_to_viewport.inverse().transform_point2(final_pos); let final_pos = layer_space_to_viewport.inverse().transform_point2(final_position);
latest_pt.handle_start = final_pos; latest_pt.handle_start = final_pos;
} }
// Making the end handle colinear
match tool_data.handle_mode {
HandleMode::Free => {}
HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => {
if let Some((latest, segment)) = tool_data.latest_point().zip(tool_data.segment_end_before_bent) {
let handle = ManipulatorPointId::EndHandle(segment).get_position(&vector_data);
let Some(handle) = handle else { return PenToolFsmState::GRSHandle };
let Some(direction) = (latest.pos - latest.handle_start).try_normalize() else {
log::trace!("Skipping handle adjustment: latest.pos and latest.handle_start are too close!");
return PenToolFsmState::GRSHandle;
};
let relative_distance = (handle - latest.pos).length();
let relative_position = relative_distance * direction;
let modification_type = VectorModificationType::SetEndHandle { segment, relative_position };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
}
}
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
PenToolFsmState::GRSHandle PenToolFsmState::GRSHandle
@ -614,6 +758,7 @@ impl Fsm for PenToolFsmState {
snap_angle: Key::Control, snap_angle: Key::Control,
break_handle: Key::Alt, break_handle: Key::Alt,
lock_angle: Key::Shift, lock_angle: Key::Shift,
colinear: Key::KeyC,
}); });
PenToolFsmState::PlacingAnchor PenToolFsmState::PlacingAnchor
@ -622,7 +767,9 @@ impl Fsm for PenToolFsmState {
tool_data.next_point = input.mouse.position; tool_data.next_point = input.mouse.position;
tool_data.next_handle_start = input.mouse.position; tool_data.next_handle_start = input.mouse.position;
let previous = tool_data.before_grs_pos; let Some(layer) = layer else { return PenToolFsmState::GRSHandle };
let previous = tool_data.previous_handle_start_pos;
if let Some(latest) = tool_data.latest_point_mut() { if let Some(latest) = tool_data.latest_point_mut() {
latest.handle_start = previous; latest.handle_start = previous;
} }
@ -632,8 +779,16 @@ impl Fsm for PenToolFsmState {
snap_angle: Key::Control, snap_angle: Key::Control,
break_handle: Key::Alt, break_handle: Key::Alt,
lock_angle: Key::Shift, lock_angle: Key::Shift,
colinear: Key::KeyC,
}); });
// Set the handle-end back to original position
if let Some(((latest, segment), handle_end)) = tool_data.latest_point().zip(tool_data.segment_end_before_bent).zip(tool_data.previous_handle_end_pos) {
let relative = handle_end - latest.pos;
let modification_type = VectorModificationType::SetEndHandle { segment, relative_position: relative };
responses.add(GraphOperationMessage::Vector { layer, modification_type });
}
PenToolFsmState::PlacingAnchor PenToolFsmState::PlacingAnchor
} }
(_, PenToolMessage::SelectionChanged) => { (_, PenToolMessage::SelectionChanged) => {
@ -680,9 +835,14 @@ impl Fsm for PenToolFsmState {
// Draw the line between the currently-being-placed anchor and its incoming handle (opposite the one currently being dragged out) // Draw the line between the currently-being-placed anchor and its incoming handle (opposite the one currently being dragged out)
overlay_context.line(next_anchor, handle_end, None); overlay_context.line(next_anchor, handle_end, None);
if self == PenToolFsmState::PlacingAnchor && anchor_start != handle_start && tool_data.modifiers.lock_angle {
// Draw the line between the currently-being-placed anchor and last-placed point (Lock angle bent overlays)
overlay_context.dashed_line(anchor_start, next_anchor, None, Some(4.), Some(4.), Some(0.5));
}
path_overlays(document, shape_editor, &mut overlay_context); path_overlays(document, shape_editor, &mut overlay_context);
if self == PenToolFsmState::DraggingHandle && valid(next_anchor, handle_end) { if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, handle_end) {
// Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out) // Draw the handle circle for the currently-being-dragged-out incoming handle (opposite the one currently being dragged out)
overlay_context.manipulator_handle(handle_end, false, None); overlay_context.manipulator_handle(handle_end, false, None);
} }
@ -696,12 +856,12 @@ impl Fsm for PenToolFsmState {
path_overlays(document, shape_editor, &mut overlay_context); path_overlays(document, shape_editor, &mut overlay_context);
} }
if self == PenToolFsmState::DraggingHandle && valid(next_anchor, next_handle_start) { if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) && valid(next_anchor, next_handle_start) {
// Draw the handle circle for the currently-being-dragged-out outgoing handle (the one currently being dragged out, under the user's cursor) // Draw the handle circle for the currently-being-dragged-out outgoing handle (the one currently being dragged out, under the user's cursor)
overlay_context.manipulator_handle(next_handle_start, false, None); overlay_context.manipulator_handle(next_handle_start, false, None);
} }
if self == PenToolFsmState::DraggingHandle { if self == PenToolFsmState::DraggingHandle(tool_data.handle_mode) {
// Draw the anchor square for the most recently placed anchor // Draw the anchor square for the most recently placed anchor
overlay_context.manipulator_anchor(next_anchor, false, None); overlay_context.manipulator_anchor(next_anchor, false, None);
} }
@ -720,11 +880,11 @@ impl Fsm for PenToolFsmState {
} }
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => { (PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
tool_data.handle_mode = HandleMode::Free;
tool_data.create_initial_point(document, input, responses, tool_options, input.keyboard.key(append_to_selected), preferences); tool_data.create_initial_point(document, input, responses, tool_options, input.keyboard.key(append_to_selected), preferences);
// Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle // Enter the dragging handle state while the mouse is held down, allowing the user to move the mouse and position the handle
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle(tool_data.handle_mode)
} }
(_, PenToolMessage::AddPointLayerPosition { layer, viewport }) => { (_, PenToolMessage::AddPointLayerPosition { layer, viewport }) => {
tool_data.add_point_layer_position(document, responses, layer, viewport); tool_data.add_point_layer_position(document, responses, layer, viewport);
@ -739,13 +899,15 @@ impl Fsm for PenToolFsmState {
let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position)); let point = SnapCandidatePoint::handle(document.metadata().document_to_viewport.inverse().transform_point2(input.mouse.position));
let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default()); let snapped = tool_data.snap_manager.free_snap(&SnapData::new(document, input), &point, SnapTypeConfiguration::default());
let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document); let viewport = document.metadata().document_to_viewport.transform_point2(snapped.snapped_point_document);
// Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector) // Early return if the buffer was started and this message is being run again after the buffer (so that place_anchor updates the state with the newly merged vector)
if tool_data.buffering_merged_vector { if tool_data.buffering_merged_vector {
tool_data.buffering_merged_vector = false; tool_data.buffering_merged_vector = false;
tool_data.bend_from_previous_point(SnapData::new(document, input), transform); tool_data.handle_mode = HandleMode::ColinearLocked;
tool_data.bend_from_previous_point(SnapData::new(document, input), transform, layer.unwrap());
tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses); tool_data.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses);
tool_data.buffering_merged_vector = false; tool_data.buffering_merged_vector = false;
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle(tool_data.handle_mode)
} else { } else {
if tool_data.handle_end.is_some() { if tool_data.handle_end.is_some() {
responses.add(DocumentMessage::StartTransaction); responses.add(DocumentMessage::StartTransaction);
@ -774,37 +936,80 @@ impl Fsm for PenToolFsmState {
last_point.handle_start = last_point.pos; last_point.handle_start = last_point.pos;
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
} else { } else {
log::warn!("No latest point available to modify handle_start."); log::trace!("No latest point available to modify handle_start.");
} }
self self
} }
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data (PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => tool_data
.finish_placing_handle(SnapData::new(document, input), transform, preferences, responses) .finish_placing_handle(SnapData::new(document, input), transform, preferences, responses)
.unwrap_or(PenToolFsmState::PlacingAnchor), .unwrap_or(PenToolFsmState::PlacingAnchor),
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { (
PenToolFsmState::DraggingHandle(_),
PenToolMessage::PointerMove {
snap_angle,
break_handle,
lock_angle,
colinear,
},
) => {
tool_data.modifiers = ModifierState { tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle), snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle), lock_angle: input.keyboard.key(lock_angle),
break_handle: input.keyboard.key(break_handle), break_handle: input.keyboard.key(break_handle),
colinear: input.keyboard.key(colinear),
}; };
let snap_data = SnapData::new(document, input); let snap_data = SnapData::new(document, input);
let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses).unwrap_or(PenToolFsmState::Ready); if tool_data.modifiers.colinear && !tool_data.toggle_colinear_debounce {
tool_data.handle_mode = match tool_data.handle_mode {
HandleMode::Free => HandleMode::ColinearEquidistant,
HandleMode::ColinearEquidistant | HandleMode::ColinearLocked => HandleMode::Free,
};
tool_data.toggle_colinear_debounce = true;
}
if !tool_data.modifiers.colinear {
tool_data.toggle_colinear_debounce = false;
}
let state = tool_data.drag_handle(snap_data, transform, input.mouse.position, responses, layer).unwrap_or(PenToolFsmState::Ready);
// Auto-panning // Auto-panning
let messages = [ let messages = [
PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(), PenToolMessage::PointerOutsideViewport {
PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(), snap_angle,
break_handle,
lock_angle,
colinear,
}
.into(),
PenToolMessage::PointerMove {
snap_angle,
break_handle,
lock_angle,
colinear,
}
.into(),
]; ];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
state state
} }
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }) => { (
PenToolFsmState::PlacingAnchor,
PenToolMessage::PointerMove {
snap_angle,
break_handle,
lock_angle,
colinear,
},
) => {
tool_data.alt_press = false;
tool_data.modifiers = ModifierState { tool_data.modifiers = ModifierState {
snap_angle: input.keyboard.key(snap_angle), snap_angle: input.keyboard.key(snap_angle),
lock_angle: input.keyboard.key(lock_angle), lock_angle: input.keyboard.key(lock_angle),
break_handle: input.keyboard.key(break_handle), break_handle: input.keyboard.key(break_handle),
colinear: input.keyboard.key(colinear),
}; };
let state = tool_data let state = tool_data
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses) .place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses)
@ -812,8 +1017,20 @@ impl Fsm for PenToolFsmState {
// Auto-panning // Auto-panning
let messages = [ let messages = [
PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(), PenToolMessage::PointerOutsideViewport {
PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(), snap_angle,
break_handle,
lock_angle,
colinear,
}
.into(),
PenToolMessage::PointerMove {
snap_angle,
break_handle,
lock_angle,
colinear,
}
.into(),
]; ];
tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses); tool_data.auto_panning.setup_by_mouse_position(input, &messages, responses);
@ -824,11 +1041,11 @@ impl Fsm for PenToolFsmState {
responses.add(OverlaysMessage::Draw); responses.add(OverlaysMessage::Draw);
self self
} }
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerOutsideViewport { .. }) => { (PenToolFsmState::DraggingHandle(mode), PenToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning // Auto-panning
let _ = tool_data.auto_panning.shift_viewport(input, responses); let _ = tool_data.auto_panning.shift_viewport(input, responses);
PenToolFsmState::DraggingHandle PenToolFsmState::DraggingHandle(mode)
} }
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerOutsideViewport { .. }) => { (PenToolFsmState::PlacingAnchor, PenToolMessage::PointerOutsideViewport { .. }) => {
// Auto-panning // Auto-panning
@ -836,17 +1053,37 @@ impl Fsm for PenToolFsmState {
PenToolFsmState::PlacingAnchor PenToolFsmState::PlacingAnchor
} }
(state, PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }) => { (
state,
PenToolMessage::PointerOutsideViewport {
snap_angle,
break_handle,
lock_angle,
colinear,
},
) => {
// Auto-panning // Auto-panning
let messages = [ let messages = [
PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(), PenToolMessage::PointerOutsideViewport {
PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(), snap_angle,
break_handle,
lock_angle,
colinear,
}
.into(),
PenToolMessage::PointerMove {
snap_angle,
break_handle,
lock_angle,
colinear,
}
.into(),
]; ];
tool_data.auto_panning.stop(&messages, responses); tool_data.auto_panning.stop(&messages, responses);
state state
} }
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => { (PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => {
responses.add(DocumentMessage::EndTransaction); responses.add(DocumentMessage::EndTransaction);
tool_data.handle_end = None; tool_data.handle_end = None;
tool_data.latest_points.clear(); tool_data.latest_points.clear();
@ -866,7 +1103,7 @@ impl Fsm for PenToolFsmState {
PenToolFsmState::Ready PenToolFsmState::Ready
} }
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => { (PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
if tool_data.point_index > 0 { if tool_data.point_index > 0 {
tool_data.point_index -= 1; tool_data.point_index -= 1;
tool_data tool_data
@ -906,16 +1143,40 @@ impl Fsm for PenToolFsmState {
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, "Add Sharp Point"), HintInfo::mouse(MouseMotion::LmbDrag, "Add Smooth Point")]),
HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash()]), HintGroup(vec![HintInfo::mouse(MouseMotion::Lmb, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash()]),
]), ]),
PenToolFsmState::DraggingHandle => HintData(vec![ PenToolFsmState::DraggingHandle(mode) => {
HintGroup(vec![ let mut dragging_hint_data = HintData(Vec::new());
dragging_hint_data.0.push(HintGroup(vec![
HintInfo::mouse(MouseMotion::Rmb, ""), HintInfo::mouse(MouseMotion::Rmb, ""),
HintInfo::keys([Key::Escape], "").prepend_slash(), HintInfo::keys([Key::Escape], "").prepend_slash(),
HintInfo::keys([Key::Enter], "End Path").prepend_slash(), HintInfo::keys([Key::Enter], "End Path").prepend_slash(),
]), ]));
HintGroup(vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")]),
// TODO: Only show this if the handle being dragged is colinear, so don't show this when bending from the previous point (by clicking and dragging from the previously placed anchor) let toggle_group = match mode {
HintGroup(vec![HintInfo::keys([Key::Alt], "Bend Handle")]), HandleMode::Free => {
]), vec![HintInfo::keys([Key::KeyC], "Make Handles Colinear")]
}
HandleMode::ColinearLocked | HandleMode::ColinearEquidistant => {
vec![HintInfo::keys([Key::KeyC], "Break Colinear Handles")]
}
};
let mut common_hints = vec![HintInfo::keys([Key::Shift], "Snap 15°"), HintInfo::keys([Key::Control], "Lock Angle")];
let hold_group = match mode {
HandleMode::Free => common_hints,
HandleMode::ColinearLocked => {
common_hints.push(HintInfo::keys([Key::Alt], "Non-Equidistant Handles"));
common_hints
}
HandleMode::ColinearEquidistant => {
common_hints.push(HintInfo::keys([Key::Alt], "Equidistant Handles"));
common_hints
}
};
dragging_hint_data.0.push(HintGroup(toggle_group));
dragging_hint_data.0.push(HintGroup(hold_group));
dragging_hint_data
}
}; };
responses.add(FrontendMessage::UpdateInputHints { hint_data }); responses.add(FrontendMessage::UpdateInputHints { hint_data });