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:
parent
133d872a9f
commit
41ee1cf8bc
|
|
@ -212,7 +212,7 @@ pub fn input_mappings() -> Mapping {
|
|||
entry!(KeyDown(Backspace); modifiers=[Accel], action_dispatch=PathToolMessage::DeleteAndBreakPath),
|
||||
entry!(KeyDown(Delete); 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(MouseRight); action_dispatch=PathToolMessage::RightClick),
|
||||
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)),
|
||||
//
|
||||
// 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!(KeyUp(MouseLeft); action_dispatch=PenToolMessage::DragStop),
|
||||
entry!(KeyDown(MouseRight); action_dispatch=PenToolMessage::Confirm),
|
||||
|
|
|
|||
|
|
@ -1185,16 +1185,9 @@ impl Fsm for PathToolFsmState {
|
|||
.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 => {
|
||||
let mut hints = vec![drag_anchor];
|
||||
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"));
|
||||
let toggle_group = match dragging_state.point_select_state {
|
||||
PointSelectState::HandleNoPair | PointSelectState::HandleWithPair => {
|
||||
let mut hints = vec![HintInfo::keys([Key::Tab], "Swap Selected Handles")];
|
||||
hints.push(HintInfo::keys(
|
||||
[Key::KeyC],
|
||||
if colinear == ManipulatorAngle::Colinear {
|
||||
|
|
@ -1203,18 +1196,40 @@ impl Fsm for PathToolFsmState {
|
|||
"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 {
|
||||
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
|
||||
}
|
||||
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
|
||||
}
|
||||
PointSelectState::Anchor => Vec::new(),
|
||||
};
|
||||
|
||||
if !point_select_state_hint_group.is_empty() {
|
||||
dragging_hint_data.0.push(HintGroup(point_select_state_hint_group));
|
||||
if !toggle_group.is_empty() {
|
||||
dragging_hint_data.0.push(HintGroup(toggle_group));
|
||||
}
|
||||
|
||||
if !hold_group.is_empty() {
|
||||
dragging_hint_data.0.push(HintGroup(hold_group));
|
||||
}
|
||||
|
||||
dragging_hint_data
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ use bezier_rs::{Bezier, BezierHandles};
|
|||
use graph_craft::document::NodeId;
|
||||
use graphene_core::vector::{PointId, VectorModificationType};
|
||||
use graphene_core::Color;
|
||||
use graphene_std::vector::{HandleId, SegmentId};
|
||||
use graphene_std::vector::{HandleId, ManipulatorPointId, SegmentId, VectorData};
|
||||
|
||||
#[derive(Default)]
|
||||
pub struct PenTool {
|
||||
|
|
@ -56,8 +56,8 @@ pub enum PenToolMessage {
|
|||
Confirm,
|
||||
DragStart { append_to_selected: Key },
|
||||
DragStop,
|
||||
PointerMove { snap_angle: Key, break_handle: Key, lock_angle: Key },
|
||||
PointerOutsideViewport { 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, colinear: Key },
|
||||
Redo,
|
||||
Undo,
|
||||
UpdateOptions(PenOptionsUpdate),
|
||||
|
|
@ -71,7 +71,7 @@ pub enum PenToolMessage {
|
|||
enum PenToolFsmState {
|
||||
#[default]
|
||||
Ready,
|
||||
DraggingHandle,
|
||||
DraggingHandle(HandleMode),
|
||||
PlacingAnchor,
|
||||
GRSHandle,
|
||||
}
|
||||
|
|
@ -174,7 +174,7 @@ impl<'a> MessageHandler<ToolMessage, &mut ToolActionHandlerData<'a>> for PenTool
|
|||
PointerMove,
|
||||
FinalPosition
|
||||
),
|
||||
PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
|
||||
PenToolFsmState::DraggingHandle(_) | PenToolFsmState::PlacingAnchor => actions!(PenToolMessageDiscriminant;
|
||||
DragStart,
|
||||
DragStop,
|
||||
PointerMove,
|
||||
|
|
@ -203,6 +203,7 @@ struct ModifierState {
|
|||
snap_angle: bool,
|
||||
lock_angle: bool,
|
||||
break_handle: bool,
|
||||
colinear: bool,
|
||||
}
|
||||
#[derive(Clone, Debug)]
|
||||
struct LastPoint {
|
||||
|
|
@ -212,6 +213,17 @@ struct LastPoint {
|
|||
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)]
|
||||
struct PenToolData {
|
||||
snap_manager: SnapManager,
|
||||
|
|
@ -222,6 +234,9 @@ struct PenToolData {
|
|||
next_handle_start: DVec2,
|
||||
|
||||
g1_continuous: bool,
|
||||
toggle_colinear_debounce: bool,
|
||||
|
||||
segment_end_before_bent: Option<SegmentId>,
|
||||
|
||||
angle: f64,
|
||||
auto_panning: AutoPanning,
|
||||
|
|
@ -229,7 +244,11 @@ struct PenToolData {
|
|||
|
||||
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 {
|
||||
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.
|
||||
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;
|
||||
let document = snap_data.document;
|
||||
self.next_handle_start = self.next_point;
|
||||
let vector_data = document.network_interface.compute_modified_vector(layer).unwrap();
|
||||
|
||||
// Break the control
|
||||
let Some(last_pos) = self.latest_point().map(|point| point.pos) else { return };
|
||||
|
||||
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);
|
||||
if on_top {
|
||||
if let Some(point) = self.latest_point_mut() {
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
|
@ -326,6 +350,7 @@ impl PenToolData {
|
|||
|
||||
let points = [start, end];
|
||||
let id = SegmentId::generate();
|
||||
self.segment_end_before_bent = Some(id);
|
||||
let modification_type = VectorModificationType::InsertSegment { id, points, handles };
|
||||
responses.add(GraphOperationMessage::Vector { layer, modification_type });
|
||||
|
||||
|
|
@ -351,19 +376,107 @@ impl PenToolData {
|
|||
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> {
|
||||
let colinear = !self.modifiers.break_handle && self.handle_end.is_some();
|
||||
fn drag_handle(&mut self, snap_data: SnapData, transform: DAffine2, mouse: DVec2, responses: &mut VecDeque<Message>, layer: Option<LayerNodeIdentifier>) -> Option<PenToolFsmState> {
|
||||
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);
|
||||
if let Some(handle_end) = self.handle_end.as_mut().filter(|_| colinear) {
|
||||
*handle_end = self.next_point * 2. - self.next_handle_start;
|
||||
self.g1_continuous = true;
|
||||
} else {
|
||||
self.g1_continuous = false;
|
||||
let Some(layer) = layer else { return Some(PenToolFsmState::DraggingHandle(self.handle_mode)) };
|
||||
let vector_data = document.network_interface.compute_modified_vector(layer)?;
|
||||
|
||||
match self.handle_mode {
|
||||
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);
|
||||
|
||||
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> {
|
||||
|
|
@ -455,7 +568,7 @@ impl PenToolData {
|
|||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
@ -571,36 +684,67 @@ impl Fsm for PenToolFsmState {
|
|||
let ToolMessage::Pen(event) = event else { return self };
|
||||
match (self, event) {
|
||||
(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 };
|
||||
|
||||
if latest.handle_start != latest.pos {
|
||||
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.before_grs_pos = latest.handle_start;
|
||||
let Some(latest) = tool_data.latest_point() else { return PenToolFsmState::PlacingAnchor };
|
||||
if latest.handle_start == latest.pos {
|
||||
return PenToolFsmState::PlacingAnchor;
|
||||
}
|
||||
|
||||
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, PenToolMessage::FinalPosition { final_position: final_pos }) => {
|
||||
(PenToolFsmState::GRSHandle, PenToolMessage::FinalPosition { final_position }) => {
|
||||
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() {
|
||||
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;
|
||||
}
|
||||
|
||||
// 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);
|
||||
|
||||
PenToolFsmState::GRSHandle
|
||||
|
|
@ -614,6 +758,7 @@ impl Fsm for PenToolFsmState {
|
|||
snap_angle: Key::Control,
|
||||
break_handle: Key::Alt,
|
||||
lock_angle: Key::Shift,
|
||||
colinear: Key::KeyC,
|
||||
});
|
||||
|
||||
PenToolFsmState::PlacingAnchor
|
||||
|
|
@ -622,7 +767,9 @@ impl Fsm for PenToolFsmState {
|
|||
tool_data.next_point = 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() {
|
||||
latest.handle_start = previous;
|
||||
}
|
||||
|
|
@ -632,8 +779,16 @@ impl Fsm for PenToolFsmState {
|
|||
snap_angle: Key::Control,
|
||||
break_handle: Key::Alt,
|
||||
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
|
||||
}
|
||||
(_, 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)
|
||||
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);
|
||||
|
||||
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)
|
||||
overlay_context.manipulator_handle(handle_end, false, None);
|
||||
}
|
||||
|
|
@ -696,12 +856,12 @@ impl Fsm for PenToolFsmState {
|
|||
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)
|
||||
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
|
||||
overlay_context.manipulator_anchor(next_anchor, false, None);
|
||||
}
|
||||
|
|
@ -720,11 +880,11 @@ impl Fsm for PenToolFsmState {
|
|||
}
|
||||
(PenToolFsmState::Ready, PenToolMessage::DragStart { append_to_selected }) => {
|
||||
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);
|
||||
|
||||
// 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 }) => {
|
||||
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 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);
|
||||
|
||||
// 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 {
|
||||
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.buffering_merged_vector = false;
|
||||
PenToolFsmState::DraggingHandle
|
||||
PenToolFsmState::DraggingHandle(tool_data.handle_mode)
|
||||
} else {
|
||||
if tool_data.handle_end.is_some() {
|
||||
responses.add(DocumentMessage::StartTransaction);
|
||||
|
|
@ -774,37 +936,80 @@ impl Fsm for PenToolFsmState {
|
|||
last_point.handle_start = last_point.pos;
|
||||
responses.add(OverlaysMessage::Draw);
|
||||
} else {
|
||||
log::warn!("No latest point available to modify handle_start.");
|
||||
log::trace!("No latest point available to modify handle_start.");
|
||||
}
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle, PenToolMessage::DragStop) => tool_data
|
||||
(PenToolFsmState::DraggingHandle(_), PenToolMessage::DragStop) => tool_data
|
||||
.finish_placing_handle(SnapData::new(document, input), transform, preferences, responses)
|
||||
.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 {
|
||||
snap_angle: input.keyboard.key(snap_angle),
|
||||
lock_angle: input.keyboard.key(lock_angle),
|
||||
break_handle: input.keyboard.key(break_handle),
|
||||
colinear: input.keyboard.key(colinear),
|
||||
};
|
||||
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
|
||||
let messages = [
|
||||
PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(),
|
||||
PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(),
|
||||
PenToolMessage::PointerOutsideViewport {
|
||||
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);
|
||||
|
||||
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 {
|
||||
snap_angle: input.keyboard.key(snap_angle),
|
||||
lock_angle: input.keyboard.key(lock_angle),
|
||||
break_handle: input.keyboard.key(break_handle),
|
||||
colinear: input.keyboard.key(colinear),
|
||||
};
|
||||
let state = tool_data
|
||||
.place_anchor(SnapData::new(document, input), transform, input.mouse.position, preferences, responses)
|
||||
|
|
@ -812,8 +1017,20 @@ impl Fsm for PenToolFsmState {
|
|||
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(),
|
||||
PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(),
|
||||
PenToolMessage::PointerOutsideViewport {
|
||||
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);
|
||||
|
||||
|
|
@ -824,11 +1041,11 @@ impl Fsm for PenToolFsmState {
|
|||
responses.add(OverlaysMessage::Draw);
|
||||
self
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle, PenToolMessage::PointerOutsideViewport { .. }) => {
|
||||
(PenToolFsmState::DraggingHandle(mode), PenToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
let _ = tool_data.auto_panning.shift_viewport(input, responses);
|
||||
|
||||
PenToolFsmState::DraggingHandle
|
||||
PenToolFsmState::DraggingHandle(mode)
|
||||
}
|
||||
(PenToolFsmState::PlacingAnchor, PenToolMessage::PointerOutsideViewport { .. }) => {
|
||||
// Auto-panning
|
||||
|
|
@ -836,17 +1053,37 @@ impl Fsm for PenToolFsmState {
|
|||
|
||||
PenToolFsmState::PlacingAnchor
|
||||
}
|
||||
(state, PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }) => {
|
||||
(
|
||||
state,
|
||||
PenToolMessage::PointerOutsideViewport {
|
||||
snap_angle,
|
||||
break_handle,
|
||||
lock_angle,
|
||||
colinear,
|
||||
},
|
||||
) => {
|
||||
// Auto-panning
|
||||
let messages = [
|
||||
PenToolMessage::PointerOutsideViewport { snap_angle, break_handle, lock_angle }.into(),
|
||||
PenToolMessage::PointerMove { snap_angle, break_handle, lock_angle }.into(),
|
||||
PenToolMessage::PointerOutsideViewport {
|
||||
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);
|
||||
|
||||
state
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => {
|
||||
(PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Confirm) => {
|
||||
responses.add(DocumentMessage::EndTransaction);
|
||||
tool_data.handle_end = None;
|
||||
tool_data.latest_points.clear();
|
||||
|
|
@ -866,7 +1103,7 @@ impl Fsm for PenToolFsmState {
|
|||
|
||||
PenToolFsmState::Ready
|
||||
}
|
||||
(PenToolFsmState::DraggingHandle | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
|
||||
(PenToolFsmState::DraggingHandle(..) | PenToolFsmState::PlacingAnchor, PenToolMessage::Undo) => {
|
||||
if tool_data.point_index > 0 {
|
||||
tool_data.point_index -= 1;
|
||||
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, ""), HintInfo::mouse(MouseMotion::LmbDrag, "Bend Prev. Point").prepend_slash()]),
|
||||
]),
|
||||
PenToolFsmState::DraggingHandle => HintData(vec![
|
||||
HintGroup(vec![
|
||||
PenToolFsmState::DraggingHandle(mode) => {
|
||||
let mut dragging_hint_data = HintData(Vec::new());
|
||||
dragging_hint_data.0.push(HintGroup(vec![
|
||||
HintInfo::mouse(MouseMotion::Rmb, ""),
|
||||
HintInfo::keys([Key::Escape], "").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)
|
||||
HintGroup(vec![HintInfo::keys([Key::Alt], "Bend Handle")]),
|
||||
]),
|
||||
]));
|
||||
|
||||
let toggle_group = match mode {
|
||||
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 });
|
||||
|
|
|
|||
Loading…
Reference in New Issue