mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-17 07:18:04 +08:00
* 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
324 lines
9.1 KiB
Rust
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());
|
|
}
|
|
}
|