Files
Graphite/editor/src/viewport_tools/tools/path.rs
Oliver Davies 45d41b1bd7 Add Pen Tool (#536)
* First steps toward pen tool, similar to spline tool currently.

* Broken WIP

* Progress, but still glitchy

* Improvements, a little farther

* Pen tool functioning as expected

* Merged master

* Fixed commit bug and overlay flashing

* Reordered import statements

TODO: Resolve issue with last segment losing its handle position on confirm
2022-02-12 02:16:31 -10:00

324 lines
9.1 KiB
Rust

use crate::consts::SELECTION_THRESHOLD;
use crate::document::DocumentMessageHandler;
use crate::frontend::utility_types::MouseCursorIcon;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::layout::widgets::PropertyHolder;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::snapping::SnapHandler;
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use crate::viewport_tools::vector_editor::shape_editor::ShapeEditor;
use graphene::intersection::Quad;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Path {
fsm_state: PathToolFsmState,
data: PathToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Path)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum PathMessage {
// Standard messages
#[remain::unsorted]
Abort,
#[remain::unsorted]
DocumentIsDirty,
#[remain::unsorted]
SelectionChanged,
// Tool-specific messages
DragStart {
add_to_selection: Key,
},
DragStop,
PointerMove {
alt_mirror_angle: Key,
shift_mirror_distance: Key,
},
}
impl PropertyHolder for Path {}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Path {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
if action == ToolMessage::UpdateHints {
self.fsm_state.update_hints(responses);
return;
}
if action == ToolMessage::UpdateCursor {
self.fsm_state.update_cursor(responses);
return;
}
let new_state = self.fsm_state.transition(action, data.0, data.1, &mut self.data, &(), data.2, responses);
if self.fsm_state != new_state {
self.fsm_state = new_state;
self.fsm_state.update_hints(responses);
self.fsm_state.update_cursor(responses);
}
}
// Different actions depending on state may be wanted:
fn actions(&self) -> ActionList {
use PathToolFsmState::*;
match self.fsm_state {
Ready => actions!(PathMessageDiscriminant; DragStart),
Dragging => actions!(PathMessageDiscriminant; DragStop, PointerMove),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PathToolFsmState {
Ready,
Dragging,
}
impl Default for PathToolFsmState {
fn default() -> Self {
PathToolFsmState::Ready
}
}
#[derive(Default)]
struct PathToolData {
shape_editor: ShapeEditor,
snap_handler: SnapHandler,
drag_start_pos: DVec2,
alt_debounce: bool,
shift_debounce: bool,
}
impl Fsm for PathToolFsmState {
type ToolData = PathToolData;
type ToolOptions = ();
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
_tool_options: &Self::ToolOptions,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
if let ToolMessage::Path(event) = event {
use PathMessage::*;
use PathToolFsmState::*;
match (self, event) {
// TODO: Capture a tool event instead of doing this?
(_, SelectionChanged) => {
// Remove any residual overlays that might exist on selection change
data.shape_editor.remove_overlays(responses);
// This currently creates new VectorManipulatorShapes for every shape, which is not ideal
// At least it is only on selection change for now
data.shape_editor.set_shapes_to_modify(document.selected_visible_layers_vector_shapes(responses));
self
}
(_, DocumentIsDirty) => {
// Update the VectorManipulatorShapes by reference so they match the kurbo data
for shape in &mut data.shape_editor.shapes_to_modify {
shape.update_shape(document, responses);
}
self
}
// Mouse down
(_, DragStart { add_to_selection }) => {
let add_to_selection = input.keyboard.get(add_to_selection as usize);
// Select the first point within the threshold (in pixels)
if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) {
responses.push_back(DocumentMessage::StartTransaction.into());
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
let snap_points = data
.shape_editor
.shapes_to_modify
.iter()
.flat_map(|shape| shape.anchors.iter().flat_map(|anchor| anchor.points[0].as_ref()))
.map(|point| point.position)
.collect();
data.snap_handler.add_snap_points(document, snap_points);
data.drag_start_pos = input.mouse.position;
Dragging
}
// We didn't find a point nearby, so consider selecting the nearest shape instead
else {
let selection_size = DVec2::new(2.0, 2.0);
// Select shapes directly under our mouse
let intersection = document
.graphene_document
.intersects_quad_root(Quad::from_box([input.mouse.position - selection_size, input.mouse.position + selection_size]));
if !intersection.is_empty() {
if add_to_selection {
responses.push_back(DocumentMessage::AddSelectedLayers { additional_layers: intersection }.into());
} else {
responses.push_back(
DocumentMessage::SetSelectedLayers {
replacement_selected_layers: intersection,
}
.into(),
);
}
} else {
// Clear the previous selection if we didn't find anything
if !input.keyboard.get(add_to_selection as usize) {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
}
}
Ready
}
}
// Dragging
(
Dragging,
PointerMove {
alt_mirror_angle,
shift_mirror_distance,
},
) => {
// Determine when alt state changes
let alt_pressed = input.keyboard.get(alt_mirror_angle as usize);
if alt_pressed != data.alt_debounce {
data.alt_debounce = alt_pressed;
// Only on alt down
if alt_pressed {
data.shape_editor.toggle_selected_mirror_angle();
}
}
// Determine when shift state changes
let shift_pressed = input.keyboard.get(shift_mirror_distance as usize);
if shift_pressed != data.shift_debounce {
data.shift_debounce = shift_pressed;
data.shape_editor.toggle_selected_mirror_distance();
}
// Move the selected points by the mouse position
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
data.shape_editor.move_selected_points(snapped_position - data.drag_start_pos, true, responses);
Dragging
}
// Mouse up
(_, DragStop) => {
data.snap_handler.cleanup(responses);
Ready
}
(_, Abort) => {
data.shape_editor.remove_overlays(responses);
Ready
}
(
_,
PointerMove {
alt_mirror_angle: _,
shift_mirror_distance: _,
},
) => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
PathToolFsmState::Ready => HintData(vec![
HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Select Point"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Grow/Shrink Selection"),
plus: true,
},
]),
HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Drag Selected"),
plus: false,
}]),
HintGroup(vec![
HintInfo {
key_groups: vec![
KeysGroup(vec![Key::KeyArrowUp]),
KeysGroup(vec![Key::KeyArrowRight]),
KeysGroup(vec![Key::KeyArrowDown]),
KeysGroup(vec![Key::KeyArrowLeft]),
],
mouse: None,
label: String::from("Nudge Selected (coming soon)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Big Increment Nudge"),
plus: true,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyG])],
mouse: None,
label: String::from("Grab Selected (coming soon)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyR])],
mouse: None,
label: String::from("Rotate Selected (coming soon)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyS])],
mouse: None,
label: String::from("Scale Selected (coming soon)"),
plus: false,
},
]),
]),
PathToolFsmState::Dragging => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("Split/Align Handles (Toggle)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Share Lengths of Aligned Handles"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
}
}