Massively reorganize and clean up the whole Rust codebase (#478)

* Massively reorganize and clean up the whole Rust codebase

* Additional changes during code review
This commit is contained in:
Keavon Chambers
2022-01-14 14:58:08 -08:00
parent 011c2be26d
commit f48d4e1884
85 changed files with 2515 additions and 2189 deletions
+22
View File
@@ -0,0 +1,22 @@
use crate::message_prelude::*;
use crate::viewport_tools::tool::ToolActionHandlerData;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Crop;
#[remain::sorted]
#[impl_message(Message, ToolMessage, Crop)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum CropMessage {
MouseMove,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Crop {
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
}
advertise_actions!();
}
+177
View File
@@ -0,0 +1,177 @@
use super::shared::resize::Resize;
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::layers::style;
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Ellipse {
fsm_state: EllipseToolFsmState,
data: EllipseToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Ellipse)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum EllipseMessage {
Abort,
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Ellipse {
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;
}
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);
}
}
fn actions(&self) -> ActionList {
use EllipseToolFsmState::*;
match self.fsm_state {
Ready => actions!(EllipseMessageDiscriminant; DragStart),
Drawing => actions!(EllipseMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EllipseToolFsmState {
Ready,
Drawing,
}
impl Default for EllipseToolFsmState {
fn default() -> Self {
EllipseToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EllipseToolData {
data: Resize,
}
impl Fsm for EllipseToolFsmState {
type ToolData = EllipseToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use EllipseMessage::*;
use EllipseToolFsmState::*;
if let ToolMessage::Ellipse(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(document, input.mouse.position);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddEllipse {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Drawing, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.cleanup();
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup();
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
EllipseToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Ellipse"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Constrain Circular"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
EllipseToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Constrain Circular"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
@@ -0,0 +1,129 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::MouseMotion;
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::intersection::Quad;
use graphene::layers::layer_info::LayerDataType;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Eyedropper {
fsm_state: EyedropperToolFsmState,
data: EyedropperToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Eyedropper)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum EyedropperMessage {
Abort,
LeftMouseDown,
RightMouseDown,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Eyedropper {
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;
}
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);
}
}
advertise_actions!(EyedropperMessageDiscriminant; LeftMouseDown, RightMouseDown);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum EyedropperToolFsmState {
Ready,
}
impl Default for EyedropperToolFsmState {
fn default() -> Self {
EyedropperToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct EyedropperToolData {}
impl Fsm for EyedropperToolFsmState {
type ToolData = EyedropperToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
_data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
use EyedropperMessage::*;
use EyedropperToolFsmState::*;
if let ToolMessage::Eyedropper(event) = event {
match (self, event) {
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
// TODO: Destroy this pyramid
if let Some(path) = document.graphene_document.intersects_quad_root(quad).last() {
if let Ok(layer) = document.graphene_document.layer(path) {
if let LayerDataType::Shape(shape) = &layer.data {
if let Some(fill) = shape.style.fill() {
if let Some(color) = fill.color() {
match lmb_or_rmb {
EyedropperMessage::LeftMouseDown => responses.push_back(ToolMessage::SelectPrimaryColor(color).into()),
EyedropperMessage::RightMouseDown => responses.push_back(ToolMessage::SelectSecondaryColor(color).into()),
_ => {}
}
}
}
}
}
}
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
EyedropperToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Sample to Primary"),
plus: false,
},
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Rmb),
label: String::from("Sample to Secondary"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
+123
View File
@@ -0,0 +1,123 @@
use crate::consts::SELECTION_TOLERANCE;
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::MouseMotion;
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::intersection::Quad;
use graphene::Operation;
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Fill {
fsm_state: FillToolFsmState,
data: FillToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Fill)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum FillMessage {
Abort,
LeftMouseDown,
RightMouseDown,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Fill {
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;
}
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);
}
}
advertise_actions!(FillMessageDiscriminant; LeftMouseDown, RightMouseDown);
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum FillToolFsmState {
Ready,
}
impl Default for FillToolFsmState {
fn default() -> Self {
FillToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct FillToolData {}
impl Fsm for FillToolFsmState {
type ToolData = FillToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
_data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
use FillMessage::*;
use FillToolFsmState::*;
if let ToolMessage::Fill(event) = event {
match (self, event) {
(Ready, lmb_or_rmb) if lmb_or_rmb == LeftMouseDown || lmb_or_rmb == RightMouseDown => {
let mouse_pos = input.mouse.position;
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
let quad = Quad::from_box([mouse_pos - tolerance, mouse_pos + tolerance]);
if let Some(path) = document.graphene_document.intersects_quad_root(quad).last() {
let color = match lmb_or_rmb {
LeftMouseDown => tool_data.primary_color,
RightMouseDown => tool_data.secondary_color,
Abort => unreachable!(),
};
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(Operation::SetLayerFill { path: path.to_vec(), color }.into());
responses.push_back(DocumentMessage::CommitTransaction.into());
}
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
FillToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Fill with Primary"),
plus: false,
},
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Rmb),
label: String::from("Fill with Secondary"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
+248
View File
@@ -0,0 +1,248 @@
use crate::consts::LINE_ROTATE_SNAP_ANGLE;
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::mouse::ViewportPosition;
use crate::input::InputPreprocessorMessageHandler;
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, ToolType};
use crate::viewport_tools::tool_options::ToolOptions;
use graphene::layers::style;
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Line {
fsm_state: LineToolFsmState,
data: LineToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Line)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum LineMessage {
Abort,
DragStart,
DragStop,
Redraw { center: Key, lock_angle: Key, snap_angle: Key },
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Line {
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;
}
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);
}
}
fn actions(&self) -> ActionList {
use LineToolFsmState::*;
match self.fsm_state {
Ready => actions!(LineMessageDiscriminant; DragStart),
Drawing => actions!(LineMessageDiscriminant; DragStop, Redraw, Abort),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum LineToolFsmState {
Ready,
Drawing,
}
impl Default for LineToolFsmState {
fn default() -> Self {
LineToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct LineToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
angle: f64,
weight: u32,
path: Option<Vec<LayerId>>,
snap_handler: SnapHandler,
}
impl Fsm for LineToolFsmState {
type ToolData = LineToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
use LineMessage::*;
use LineToolFsmState::*;
if let ToolMessage::Line(event) = event {
match (self, event) {
(Ready, DragStart) => {
data.snap_handler.start_snap(document, document.all_layers_sorted(), &[]);
data.drag_start = data.snap_handler.snap_position(document, input.mouse.position);
responses.push_back(DocumentMessage::StartTransaction.into());
data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.weight = match tool_data.tool_options.get(&ToolType::Line) {
Some(&ToolOptions::Line { weight }) => weight,
_ => 5,
};
responses.push_back(
Operation::AddLine {
path: data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), None),
}
.into(),
);
Drawing
}
(Drawing, Redraw { center, snap_angle, lock_angle }) => {
data.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
let values: Vec<_> = [lock_angle, snap_angle, center].iter().map(|k| input.keyboard.get(*k as usize)).collect();
responses.push_back(generate_transform(data, values[0], values[1], values[2]));
Drawing
}
(Drawing, DragStop) => {
data.drag_current = data.snap_handler.snap_position(document, input.mouse.position);
data.snap_handler.cleanup();
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
data.path = None;
Ready
}
(Drawing, Abort) => {
data.snap_handler.cleanup();
responses.push_back(DocumentMessage::AbortTransaction.into());
data.path = None;
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
LineToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Line"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Snap 15°"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Lock Angle"),
plus: true,
},
])]),
LineToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Snap 15°"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Lock Angle"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
fn generate_transform(data: &mut LineToolData, lock: bool, snap: bool, center: bool) -> Message {
let mut start = data.drag_start;
let stop = data.drag_current;
let dir = stop - start;
let mut angle = -dir.angle_between(DVec2::X);
if lock {
angle = data.angle
};
if snap {
let snap_resolution = LINE_ROTATE_SNAP_ANGLE.to_radians();
angle = (angle / snap_resolution).round() * snap_resolution;
}
data.angle = angle;
let mut scale = dir.length();
if lock {
let angle_vec = DVec2::new(angle.cos(), angle.sin());
scale = dir.dot(angle_vec);
}
if center {
start -= scale * DVec2::new(angle.cos(), angle.sin());
scale *= 2.;
}
Operation::SetLayerTransformInViewport {
path: data.path.clone().unwrap(),
transform: glam::DAffine2::from_scale_angle_translation(DVec2::new(scale, 1.), angle, start).to_cols_array(),
}
.into()
}
+12
View File
@@ -0,0 +1,12 @@
pub mod crop;
pub mod ellipse;
pub mod eyedropper;
pub mod fill;
pub mod line;
pub mod navigate;
pub mod path;
pub mod pen;
pub mod rectangle;
pub mod select;
pub mod shape;
pub mod shared;
+213
View File
@@ -0,0 +1,213 @@
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use glam::DVec2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Navigate {
fsm_state: NavigateToolFsmState,
data: NavigateToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Navigate)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum NavigateMessage {
Abort,
ClickZoom { zoom_in: bool },
MouseMove { snap_angle: Key, snap_zoom: Key },
RotateCanvasBegin,
TransformCanvasEnd,
TranslateCanvasBegin,
ZoomCanvasBegin,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
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;
}
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);
}
}
fn actions(&self) -> ActionList {
use NavigateToolFsmState::*;
match self.fsm_state {
Ready => actions!(NavigateMessageDiscriminant; TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin),
_ => actions!(NavigateMessageDiscriminant; ClickZoom, MouseMove, TransformCanvasEnd),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum NavigateToolFsmState {
Ready,
Panning,
Tilting,
Zooming,
}
impl Default for NavigateToolFsmState {
fn default() -> Self {
NavigateToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct NavigateToolData {
drag_start: DVec2,
}
impl Fsm for NavigateToolFsmState {
type ToolData = NavigateToolData;
fn transition(
self,
message: ToolMessage,
_document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
messages: &mut VecDeque<Message>,
) -> Self {
if let ToolMessage::Navigate(navigate) = message {
use NavigateMessage::*;
match navigate {
ClickZoom { zoom_in } => {
messages.push_front(MovementMessage::TransformCanvasEnd.into());
// Mouse has not moved from mousedown to mouseup
if data.drag_start == input.mouse.position {
messages.push_front(if zoom_in {
MovementMessage::IncreaseCanvasZoom { center_on_mouse: true }.into()
} else {
MovementMessage::DecreaseCanvasZoom { center_on_mouse: true }.into()
});
}
NavigateToolFsmState::Ready
}
MouseMove { snap_angle, snap_zoom } => {
messages.push_front(
MovementMessage::MouseMove {
snap_angle,
wait_for_snap_angle_release: false,
snap_zoom,
zoom_from_viewport: Some(data.drag_start),
}
.into(),
);
self
}
TranslateCanvasBegin => {
data.drag_start = input.mouse.position;
messages.push_front(MovementMessage::TranslateCanvasBegin.into());
NavigateToolFsmState::Panning
}
RotateCanvasBegin => {
data.drag_start = input.mouse.position;
messages.push_front(MovementMessage::RotateCanvasBegin.into());
NavigateToolFsmState::Tilting
}
ZoomCanvasBegin => {
data.drag_start = input.mouse.position;
messages.push_front(MovementMessage::ZoomCanvasBegin.into());
NavigateToolFsmState::Zooming
}
TransformCanvasEnd => {
messages.push_front(MovementMessage::TransformCanvasEnd.into());
NavigateToolFsmState::Ready
}
Abort => {
messages.push_front(MovementMessage::TransformCanvasEnd.into());
NavigateToolFsmState::Ready
}
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
NavigateToolFsmState::Ready => HintData(vec![
HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Zoom In"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Zoom Out"),
plus: true,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Zoom"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Snap Increments"),
plus: true,
},
]),
HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::MmbDrag),
label: String::from("Pan"),
plus: false,
}]),
HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::RmbDrag),
label: String::from("Tilt"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Snap 15°"),
plus: true,
},
]),
]),
NavigateToolFsmState::Tilting => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Snap 15°"),
plus: false,
}])]),
NavigateToolFsmState::Zooming => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Snap Increments"),
plus: false,
}])]),
_ => HintData(Vec::new()),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
+638
View File
@@ -0,0 +1,638 @@
use crate::consts::{COLOR_ACCENT, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE};
use crate::document::utility_types::{VectorManipulatorSegment, VectorManipulatorShape};
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::color::Color;
use graphene::layers::style::{self, Fill, PathStyle, Stroke};
use graphene::Operation;
use glam::{DAffine2, DVec2};
use kurbo::{BezPath, PathEl, Vec2};
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
Abort,
DocumentIsDirty,
DragStart,
DragStop,
PointerMove,
}
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;
}
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);
}
}
// 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(Clone, Debug, Default)]
struct PathToolData {
anchor_marker_pool: Vec<Vec<LayerId>>,
handle_marker_pool: Vec<Vec<LayerId>>,
anchor_handle_line_pool: Vec<Vec<LayerId>>,
shape_outline_pool: Vec<Vec<LayerId>>,
selected_shapes: Vec<VectorManipulatorShape>,
selection: PathToolSelection,
}
impl PathToolData {}
#[derive(Clone, Debug, Default)]
struct PathToolSelection {
closest_layer_path: Vec<LayerId>,
closest_shape_id: usize,
overlay_path: Vec<LayerId>,
bez_path_elements: Vec<kurbo::PathEl>,
bez_segment_id: usize,
}
impl Fsm for PathToolFsmState {
type ToolData = PathToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
if let ToolMessage::Path(event) = event {
use PathMessage::*;
use PathToolFsmState::*;
match (self, event) {
(_, DocumentIsDirty) => {
let (mut anchor_i, mut handle_i, mut line_i, mut shape_i) = (0, 0, 0, 0);
let shapes_to_draw = document.selected_visible_layers_vector_points();
// Grow the overlay pools by the shortfall, if any
let (total_anchors, total_handles, total_anchor_handle_lines) = calculate_total_overlays_per_type(&shapes_to_draw);
let total_shapes = shapes_to_draw.len();
grow_overlay_pool_entries(&mut data.shape_outline_pool, total_shapes, add_shape_outline, responses);
grow_overlay_pool_entries(&mut data.anchor_handle_line_pool, total_anchor_handle_lines, add_anchor_handle_line, responses);
grow_overlay_pool_entries(&mut data.anchor_marker_pool, total_anchors, add_anchor_marker, responses);
grow_overlay_pool_entries(&mut data.handle_marker_pool, total_handles, add_handle_marker, responses);
// Helps push values that end in approximately half, plus or minus some floating point imprecision, towards the same side of the round() function
const BIAS: f64 = 0.0001;
// Draw the overlays for each shape
for shape_to_draw in &shapes_to_draw {
let shape_layer_path = &data.shape_outline_pool[shape_i];
responses.push_back(
DocumentMessage::Overlays(
Operation::SetShapePathInViewport {
path: shape_layer_path.clone(),
bez_path: shape_to_draw.path.clone(),
transform: shape_to_draw.transform.to_cols_array(),
}
.into(),
)
.into(),
);
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerVisibility {
path: shape_layer_path.clone(),
visible: true,
}
.into(),
)
.into(),
);
shape_i += 1;
let segment = shape_manipulator_points(shape_to_draw);
// Draw the line connecting the anchor with handle for cubic and quadratic bezier segments
for anchor_handle_line in segment.anchor_handle_lines {
let marker = &data.anchor_handle_line_pool[line_i];
let line_vector = anchor_handle_line.0 - anchor_handle_line.1;
let scale = DVec2::splat(line_vector.length());
let angle = -line_vector.angle_between(DVec2::X);
let translation = (anchor_handle_line.1 + BIAS).round() + DVec2::splat(0.5);
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
line_i += 1;
}
// Draw the draggable square points on the end of every line segment or bezier curve segment
for anchor in segment.anchors {
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
let angle = 0.;
let translation = (anchor - (scale / 2.) + BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
let marker = &data.anchor_marker_pool[anchor_i];
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
anchor_i += 1;
}
// Draw the draggable handle for cubic and quadratic bezier segments
for handle in segment.handles {
let marker = &data.handle_marker_pool[handle_i];
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
let angle = 0.;
let translation = (handle - (scale / 2.) + BIAS).round();
let transform = DAffine2::from_scale_angle_translation(scale, angle, translation).to_cols_array();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path: marker.clone(), transform }.into()).into());
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker.clone(), visible: true }.into()).into());
handle_i += 1;
}
}
// Hide the remaining pooled overlays
for i in anchor_i..data.anchor_marker_pool.len() {
let marker = data.anchor_marker_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker, visible: false }.into()).into());
}
for i in handle_i..data.handle_marker_pool.len() {
let marker = data.handle_marker_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: marker, visible: false }.into()).into());
}
for i in line_i..data.anchor_handle_line_pool.len() {
let line = data.anchor_handle_line_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: line, visible: false }.into()).into());
}
for i in shape_i..data.shape_outline_pool.len() {
let shape_i = data.shape_outline_pool[i].clone();
responses.push_back(DocumentMessage::Overlays(Operation::SetLayerVisibility { path: shape_i, visible: false }.into()).into());
}
self
}
(_, DragStart) => {
// todo: DRY refactor (this arm is very similar to the (_, RedrawOverlay) arm)
let mouse_pos = input.mouse.position;
let mut points = Vec::new();
let (mut anchor_i, mut handle_i, _line_i, _shape_i) = (0, 0, 0, 0);
let shapes_to_draw = document.selected_visible_layers_vector_points();
let (total_anchors, total_handles, _total_anchor_handle_lines) = calculate_total_overlays_per_type(&shapes_to_draw);
grow_overlay_pool_entries(&mut data.anchor_marker_pool, total_anchors, add_anchor_marker, responses);
grow_overlay_pool_entries(&mut data.handle_marker_pool, total_handles, add_handle_marker, responses);
#[derive(Debug)]
enum PointType {
Anchor { anchor_i: usize, layer_path: Vec<LayerId>, shape_offset: usize },
Handle { handle_i: usize, layer_path: Vec<LayerId>, shape_offset: usize },
}
#[derive(Debug)]
struct Point {
point_type: PointType,
mouse_proximity: f64,
}
impl Point {
fn new(_position: DVec2, point_type: PointType, mouse_proximity: f64) -> Self {
Self { point_type, mouse_proximity }
}
}
// TODO simplify the following block
let select_threshold = 6.;
let select_threshold_squared = select_threshold * select_threshold;
for (shape_offset, shape_to_draw) in shapes_to_draw.iter().enumerate() {
let segment = shape_manipulator_points(shape_to_draw);
for anchor in segment.anchors {
let d2 = mouse_pos.distance_squared(anchor);
if d2 < select_threshold_squared {
points.push(Point::new(
anchor,
PointType::Anchor {
anchor_i,
layer_path: shape_to_draw.layer_path.clone(),
shape_offset,
},
d2,
));
}
anchor_i += 1;
}
for (_, handle) in segment.handles.into_iter().enumerate() {
let d2 = mouse_pos.distance_squared(handle);
if d2 < select_threshold_squared {
points.push(Point::new(
handle,
PointType::Handle {
handle_i,
layer_path: shape_to_draw.layer_path.clone(),
shape_offset,
},
d2,
));
}
handle_i += 1;
}
}
points.sort_by(|a, b| a.mouse_proximity.partial_cmp(&b.mouse_proximity).unwrap_or(std::cmp::Ordering::Equal));
let closest_point_within_click_threshold = points.first();
if let Some(point) = closest_point_within_click_threshold {
let path = match point.point_type {
PointType::Anchor {
anchor_i,
ref layer_path,
shape_offset,
} => {
data.selected_shapes = shapes_to_draw;
let shape = &data.selected_shapes[shape_offset];
let path = shape.path.clone();
let bez: Vec<PathEl> = (&path).into_iter().collect();
let transformed = shape.transform.inverse().transform_point2(input.mouse.position);
data.selection.bez_segment_id = closest_anchor(&bez, Vec2::new(transformed.x, transformed.y));
data.selection.bez_path_elements = bez;
data.selection.closest_layer_path = layer_path.clone();
data.selection.closest_shape_id = shape_offset;
data.anchor_marker_pool[anchor_i].clone()
}
PointType::Handle {
handle_i,
ref layer_path,
shape_offset,
} => {
// TODO make this work for the handles, right now just selects the anchors
data.selected_shapes = shapes_to_draw;
let shape = &data.selected_shapes[shape_offset];
let path = shape.path.clone();
let bez: Vec<PathEl> = (&path).into_iter().collect();
let transformed = shape.transform.inverse().transform_point2(input.mouse.position);
data.selection.bez_segment_id = closest_anchor(&bez, Vec2::new(transformed.x, transformed.y));
data.selection.bez_path_elements = bez;
data.selection.closest_layer_path = layer_path.clone();
data.selection.closest_shape_id = shape_offset;
data.handle_marker_pool[handle_i].clone()
}
};
data.selection.overlay_path = path;
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerFill {
path: data.selection.overlay_path.clone(),
color: COLOR_ACCENT,
}
.into(),
)
.into(),
);
Dragging
} else {
Ready
}
}
(Dragging, PointerMove) => {
let shape = &data.selected_shapes[data.selection.closest_shape_id];
let transformed = shape.transform.inverse().transform_point2(input.mouse.position);
let delta: Vec2 = Vec2::new(transformed.x, transformed.y);
let replacement = match &data.selection.bez_path_elements[data.selection.bez_segment_id] {
PathEl::MoveTo(_) => PathEl::MoveTo(delta.to_point()),
PathEl::LineTo(_) => PathEl::LineTo(delta.to_point()),
PathEl::QuadTo(a1, _) => PathEl::QuadTo(*a1, delta.to_point()),
PathEl::CurveTo(a1, a2, _) => PathEl::CurveTo(*a1, *a2, delta.to_point()),
PathEl::ClosePath => unreachable!(),
};
data.selection.bez_path_elements[data.selection.bez_segment_id] = replacement;
responses.push_back(
Operation::SetShapePathInViewport {
path: data.selection.closest_layer_path.clone(),
bez_path: data.selection.bez_path_elements.clone().into_iter().collect(),
transform: shape.transform.to_cols_array(),
}
.into(),
);
Dragging
}
(_, PointerMove) => self,
(_, DragStop) => {
let style = PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE)));
responses.push_back(
DocumentMessage::Overlays(
Operation::SetLayerStyle {
path: data.selection.overlay_path.clone(),
style,
}
.into(),
)
.into(),
);
Ready
}
(_, Abort) => {
// Destory the overlay layer pools
while let Some(layer) = data.anchor_marker_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
while let Some(layer) = data.handle_marker_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
while let Some(layer) = data.anchor_handle_line_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
while let Some(layer) = data.shape_outline_pool.pop() {
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path: layer }.into()).into());
}
Ready
}
}
} 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 (coming soon)"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Add/Remove Point"),
plus: true,
},
]),
HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Drag Selected (coming soon)"),
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![]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
struct VectorManipulatorTypes {
anchors: Vec<glam::DVec2>,
handles: Vec<glam::DVec2>,
anchor_handle_lines: Vec<(glam::DVec2, glam::DVec2)>,
}
fn shape_manipulator_points(shape: &VectorManipulatorShape) -> VectorManipulatorTypes {
// TODO: Performance can be improved by using three iterators (calling `.iter()` for each of the three) instead of a vector, achievable with some file restructuring
let initial_counts = calculate_shape_overlays_per_type(shape);
let mut result = VectorManipulatorTypes {
anchors: Vec::with_capacity(initial_counts.0),
handles: Vec::with_capacity(initial_counts.1),
anchor_handle_lines: Vec::with_capacity(initial_counts.2),
};
for (i, segment) in shape.segments.iter().enumerate() {
// An open shape needs an extra point, which is part of the first segment (when `i` is 0)
let include_start_and_end = !shape.closed && i == 0;
match segment {
VectorManipulatorSegment::Line(a1, a2) => {
result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] });
}
VectorManipulatorSegment::Quad(a1, h1, a2) => {
result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] });
result.handles.extend(vec![*h1]);
result.anchor_handle_lines.extend(vec![(*h1, *a1)]);
}
VectorManipulatorSegment::Cubic(a1, h1, h2, a2) => {
result.anchors.extend(if include_start_and_end { vec![*a1, *a2] } else { vec![*a2] });
result.handles.extend(vec![*h1, *h2]);
result.anchor_handle_lines.extend(vec![(*h1, *a1), (*h2, *a2)]);
}
};
}
result
}
fn calculate_total_overlays_per_type(shapes: &[VectorManipulatorShape]) -> (usize, usize, usize) {
shapes.iter().fold((0, 0, 0), |acc, shape| {
let counts = calculate_shape_overlays_per_type(shape);
(acc.0 + counts.0, acc.1 + counts.1, acc.2 + counts.2)
})
}
fn calculate_shape_overlays_per_type(shape: &VectorManipulatorShape) -> (usize, usize, usize) {
let (mut total_anchors, mut total_handles, mut total_anchor_handle_lines) = (0, 0, 0);
for segment in &shape.segments {
let (anchors, handles, anchor_handle_lines) = match segment {
VectorManipulatorSegment::Line(_, _) => (1, 0, 0),
VectorManipulatorSegment::Quad(_, _, _) => (1, 1, 1),
VectorManipulatorSegment::Cubic(_, _, _, _) => (1, 2, 2),
};
total_anchors += anchors;
total_handles += handles;
total_anchor_handle_lines += anchor_handle_lines;
}
// A non-closed shape does not reuse the start and end point, so there is one extra
if !shape.closed {
total_anchors += 1;
}
(total_anchors, total_handles, total_anchor_handle_lines)
}
fn grow_overlay_pool_entries<F>(pool: &mut Vec<Vec<LayerId>>, total: usize, add_overlay_function: F, responses: &mut VecDeque<Message>)
where
F: Fn(&mut VecDeque<Message>) -> Vec<LayerId>,
{
if pool.len() < total {
let additional = total - pool.len();
pool.reserve(additional);
for _ in 0..additional {
let marker = add_overlay_function(responses);
pool.push(marker);
}
}
}
fn add_anchor_marker(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayRect {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
fn add_handle_marker(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayEllipse {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
fn add_anchor_handle_line(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayLine {
path: layer_path.clone(),
transform: DAffine2::IDENTITY.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())),
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
fn add_shape_outline(responses: &mut VecDeque<Message>) -> Vec<LayerId> {
let layer_path = vec![generate_uuid()];
let operation = Operation::AddOverlayShape {
path: layer_path.clone(),
bez_path: BezPath::default(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())),
closed: false,
};
responses.push_back(DocumentMessage::Overlays(operation.into()).into());
layer_path
}
// Brute force comparison to determine which path element we want to select
fn closest_anchor(bez: &[kurbo::PathEl], pos: kurbo::Vec2) -> usize {
let mut closest: usize = 0;
let mut closest_distance: f64 = f64::MAX;
for (i, el) in bez.iter().enumerate() {
let p = match el {
kurbo::PathEl::MoveTo(p) => Some(p.to_vec2()),
kurbo::PathEl::LineTo(p) => Some(p.to_vec2()),
kurbo::PathEl::QuadTo(_, p) => Some(p.to_vec2()),
kurbo::PathEl::CurveTo(_, _, p) => Some(p.to_vec2()),
kurbo::PathEl::ClosePath => None,
};
if p.is_none() {
continue;
}
let distance_squared = (p.unwrap() - pos).hypot2();
if distance_squared < closest_distance {
closest_distance = distance_squared;
closest = i;
}
}
closest
}
+214
View File
@@ -0,0 +1,214 @@
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
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, ToolType};
use crate::viewport_tools::tool_options::ToolOptions;
use graphene::layers::style;
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Pen {
fsm_state: PenToolFsmState,
data: PenToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Pen)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum PenMessage {
Abort,
Confirm,
DragStart,
DragStop,
PointerMove,
Undo,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum PenToolFsmState {
Ready,
Drawing,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Pen {
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;
}
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);
}
}
fn actions(&self) -> ActionList {
use PenToolFsmState::*;
match self.fsm_state {
Ready => actions!(PenMessageDiscriminant; Undo, DragStart, DragStop, Confirm, Abort),
Drawing => actions!(PenMessageDiscriminant; DragStop, PointerMove, Confirm, Abort),
}
}
}
impl Default for PenToolFsmState {
fn default() -> Self {
PenToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct PenToolData {
points: Vec<DAffine2>,
next_point: DAffine2,
weight: u32,
path: Option<Vec<LayerId>>,
layer_exists: bool,
snap_handler: SnapHandler,
}
impl Fsm for PenToolFsmState {
type ToolData = PenToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
let transform = document.graphene_document.root.transform;
use PenMessage::*;
use PenToolFsmState::*;
if let ToolMessage::Pen(event) = event {
match (self, event) {
(Ready, DragStart) => {
responses.push_back(DocumentMessage::StartTransaction.into());
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.path = Some(vec![generate_uuid()]);
data.layer_exists = false;
data.snap_handler.start_snap(document, document.all_layers_sorted(), &[]);
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
data.points.push(pos);
data.next_point = pos;
data.weight = match tool_data.tool_options.get(&ToolType::Pen) {
Some(&ToolOptions::Pen { weight }) => weight,
_ => 5,
};
responses.push_back(make_operation(data, tool_data, true));
Drawing
}
(Drawing, DragStop) => {
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
if data.points.last() != Some(&pos) {
data.points.push(pos);
data.next_point = pos;
}
responses.push_back(remove_preview(data));
responses.push_back(make_operation(data, tool_data, true));
Drawing
}
(Drawing, PointerMove) => {
let snapped_position = data.snap_handler.snap_position(document, input.mouse.position);
let pos = transform.inverse() * DAffine2::from_translation(snapped_position);
data.next_point = pos;
responses.push_back(remove_preview(data));
responses.push_back(make_operation(data, tool_data, true));
Drawing
}
(Drawing, Confirm) | (Drawing, Abort) => {
if data.points.len() >= 2 {
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(remove_preview(data));
responses.push_back(make_operation(data, tool_data, false));
responses.push_back(DocumentMessage::CommitTransaction.into());
} else {
responses.push_back(DocumentMessage::AbortTransaction.into());
}
data.path = None;
data.points.clear();
data.snap_handler.cleanup();
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
PenToolFsmState::Ready => HintData(vec![HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Draw Path"),
plus: false,
}])]),
PenToolFsmState::Drawing => HintData(vec![
HintGroup(vec![HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Extend Path"),
plus: false,
}]),
HintGroup(vec![HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyEnter])],
mouse: None,
label: String::from("End Path"),
plus: false,
}]),
]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
fn remove_preview(data: &PenToolData) -> Message {
Operation::DeleteLayer { path: data.path.clone().unwrap() }.into()
}
fn make_operation(data: &PenToolData, tool_data: &DocumentToolData, show_preview: bool) -> Message {
let mut points: Vec<(f64, f64)> = data.points.iter().map(|p| (p.translation.x, p.translation.y)).collect();
if show_preview {
points.push((data.next_point.translation.x, data.next_point.translation.y))
}
Operation::AddPen {
path: data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::IDENTITY.to_cols_array(),
points,
style: style::PathStyle::new(Some(style::Stroke::new(tool_data.primary_color, data.weight as f32)), Some(style::Fill::none())),
}
.into()
}
@@ -0,0 +1,177 @@
use super::shared::resize::Resize;
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
use graphene::layers::style;
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Rectangle {
fsm_state: RectangleToolFsmState,
data: RectangleToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Rectangle)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum RectangleMessage {
Abort,
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Rectangle {
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;
}
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);
}
}
fn actions(&self) -> ActionList {
use RectangleToolFsmState::*;
match self.fsm_state {
Ready => actions!(RectangleMessageDiscriminant; DragStart),
Drawing => actions!(RectangleMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum RectangleToolFsmState {
Ready,
Drawing,
}
impl Default for RectangleToolFsmState {
fn default() -> Self {
RectangleToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct RectangleToolData {
data: Resize,
}
impl Fsm for RectangleToolFsmState {
type ToolData = RectangleToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use RectangleMessage::*;
use RectangleToolFsmState::*;
if let ToolMessage::Rectangle(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(document, input.mouse.position);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
responses.push_back(
Operation::AddRect {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Drawing, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.cleanup();
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup();
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
RectangleToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Rectangle"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Constrain Square"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
RectangleToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Constrain Square"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
+417
View File
@@ -0,0 +1,417 @@
use crate::consts::{COLOR_ACCENT, SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE};
use crate::document::utility_types::{AlignAggregate, AlignAxis, FlipAxis};
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::mouse::ViewportPosition;
use crate::input::InputPreprocessorMessageHandler;
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 graphene::intersection::Quad;
use graphene::layers::style::{self, Fill, Stroke};
use graphene::Operation;
use glam::{DAffine2, DVec2};
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Select {
fsm_state: SelectToolFsmState,
data: SelectToolData,
}
// #[remain::sorted] // https://github.com/dtolnay/remain/issues/16
#[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum SelectMessage {
// Standard messages
Abort,
DocumentIsDirty,
DragStart { add_to_selection: Key },
DragStop,
MouseMove { snap_angle: Key },
Align(AlignAxis, AlignAggregate),
FlipHorizontal,
FlipVertical,
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
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;
}
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);
}
}
fn actions(&self) -> ActionList {
use SelectToolFsmState::*;
match self.fsm_state {
Ready => actions!(SelectMessageDiscriminant; DragStart),
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove),
DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
enum SelectToolFsmState {
Ready,
Dragging,
DrawingBox,
}
impl Default for SelectToolFsmState {
fn default() -> Self {
SelectToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct SelectToolData {
drag_start: ViewportPosition,
drag_current: ViewportPosition,
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
drag_box_overlay_layer: Option<Vec<LayerId>>,
bounding_box_overlay_layer: Option<Vec<LayerId>>,
snap_handler: SnapHandler,
}
impl SelectToolData {
fn selection_quad(&self) -> Quad {
let bbox = self.selection_box();
Quad::from_box(bbox)
}
fn selection_box(&self) -> [DVec2; 2] {
if self.drag_current == self.drag_start {
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
[self.drag_start - tolerance, self.drag_start + tolerance]
} else {
[self.drag_start, self.drag_current]
}
}
}
fn add_bounding_box(responses: &mut Vec<Message>) -> Vec<LayerId> {
let path = vec![generate_uuid()];
let operation = Operation::AddOverlayRect {
path: path.clone(),
transform: DAffine2::ZERO.to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 1.0)), Some(Fill::none())),
};
responses.push(DocumentMessage::Overlays(operation.into()).into());
path
}
fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] {
DAffine2::from_scale_angle_translation(pos2 - pos1, 0., pos1).to_cols_array()
}
impl Fsm for SelectToolFsmState {
type ToolData = SelectToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
_tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
use SelectMessage::*;
use SelectToolFsmState::*;
if let ToolMessage::Select(event) = event {
match (self, event) {
(_, DocumentIsDirty) => {
let mut buffer = Vec::new();
let response = match (document.selected_visible_layers_bounding_box(), data.bounding_box_overlay_layer.take()) {
(None, Some(path)) => DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into(),
(Some([pos1, pos2]), path) => {
let path = path.unwrap_or_else(|| add_bounding_box(&mut buffer));
data.bounding_box_overlay_layer = Some(path.clone());
let half_pixel_offset = DVec2::splat(0.5);
let pos1 = pos1 + half_pixel_offset;
let pos2 = pos2 - half_pixel_offset;
let transform = transform_from_box(pos1, pos2);
DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into()
}
(_, _) => Message::NoOp,
};
responses.push_front(response);
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
self
}
(Ready, DragStart { add_to_selection }) => {
data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;
let mut buffer = Vec::new();
let mut selected: Vec<_> = document.selected_visible_layers().map(|path| path.to_vec()).collect();
let quad = data.selection_quad();
let mut intersection = document.graphene_document.intersects_quad_root(quad);
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
// If the user clicks on new shape, make that layer their new selection.
// Otherwise enter the box select mode
let state = if selected.iter().any(|path| intersection.contains(path)) {
buffer.push(DocumentMessage::StartTransaction.into());
data.layers_dragging = selected;
Dragging
} else {
if !input.keyboard.get(add_to_selection as usize) {
buffer.push(DocumentMessage::DeselectAllLayers.into());
data.layers_dragging.clear();
}
if let Some(intersection) = intersection.pop() {
selected = vec![intersection];
buffer.push(DocumentMessage::AddSelectedLayers(selected.clone()).into());
buffer.push(DocumentMessage::StartTransaction.into());
data.layers_dragging.append(&mut selected);
Dragging
} else {
data.drag_box_overlay_layer = Some(add_bounding_box(&mut buffer));
DrawingBox
}
};
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
// TODO: Probably delete this now that the overlays system has moved to a separate Graphene document? (@0hypercube)
let ignore_layers = if let Some(bounding_box) = &data.bounding_box_overlay_layer {
vec![bounding_box.clone()]
} else {
Vec::new()
};
data.snap_handler.start_snap(document, document.non_selected_layers_sorted(), &ignore_layers);
state
}
(Dragging, MouseMove { snap_angle }) => {
// TODO: This is a cheat. Break out the relevant functionality from the handler above and call it from there and here.
responses.push_front(SelectMessage::DocumentIsDirty.into());
let mouse_position = if input.keyboard.get(snap_angle as usize) {
let mouse_position = input.mouse.position - data.drag_start;
let snap_resolution = SELECTION_DRAG_ANGLE.to_radians();
let angle = -mouse_position.angle_between(DVec2::X);
let snapped_angle = (angle / snap_resolution).round() * snap_resolution;
DVec2::new(snapped_angle.cos(), snapped_angle.sin()) * mouse_position.length() + data.drag_start
} else {
input.mouse.position
};
let mouse_delta = mouse_position - data.drag_current;
let closest_move = data.snap_handler.snap_layers(document, &data.layers_dragging, mouse_delta);
for path in data.layers_dragging.iter() {
responses.push_front(
Operation::TransformLayerInViewport {
path: path.clone(),
transform: DAffine2::from_translation(mouse_delta + closest_move).to_cols_array(),
}
.into(),
);
}
data.drag_current = mouse_position + closest_move;
Dragging
}
(DrawingBox, MouseMove { snap_angle: _ }) => {
data.drag_current = input.mouse.position;
let half_pixel_offset = DVec2::splat(0.5);
let start = data.drag_start + half_pixel_offset;
let size = data.drag_current - start + half_pixel_offset;
responses.push_front(
DocumentMessage::Overlays(
Operation::SetLayerTransformInViewport {
path: data.drag_box_overlay_layer.clone().unwrap(),
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
}
.into(),
)
.into(),
);
DrawingBox
}
(Dragging, DragStop) => {
let response = match input.mouse.position.distance(data.drag_start) < 10. * f64::EPSILON {
true => DocumentMessage::Undo,
false => DocumentMessage::CommitTransaction,
};
data.snap_handler.cleanup();
responses.push_front(response.into());
Ready
}
(DrawingBox, DragStop) => {
let quad = data.selection_quad();
responses.push_front(DocumentMessage::AddSelectedLayers(document.graphene_document.intersects_quad_root(quad)).into());
responses.push_front(
DocumentMessage::Overlays(
Operation::DeleteLayer {
path: data.drag_box_overlay_layer.take().unwrap(),
}
.into(),
)
.into(),
);
Ready
}
(_, Abort) => {
let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into()));
delete(&mut data.drag_box_overlay_layer);
delete(&mut data.bounding_box_overlay_layer);
Ready
}
(_, Align(axis, aggregate)) => {
responses.push_back(DocumentMessage::AlignSelectedLayers(axis, aggregate).into());
self
}
(_, FlipHorizontal) => {
responses.push_back(DocumentMessage::FlipSelectedLayers(FlipAxis::X).into());
self
}
(_, FlipVertical) => {
responses.push_back(DocumentMessage::FlipSelectedLayers(FlipAxis::Y).into());
self
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
SelectToolFsmState::Ready => HintData(vec![
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::KeyG])],
mouse: None,
label: String::from("Grab Selected"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyR])],
mouse: None,
label: String::from("Rotate Selected"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyS])],
mouse: None,
label: String::from("Scale Selected"),
plus: false,
},
]),
HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::Lmb),
label: String::from("Select Object"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Innermost"),
plus: true,
},
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("Select Area"),
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![
KeysGroup(vec![Key::KeyArrowUp]),
KeysGroup(vec![Key::KeyArrowRight]),
KeysGroup(vec![Key::KeyArrowDown]),
KeysGroup(vec![Key::KeyArrowLeft]),
],
mouse: None,
label: String::from("Nudge Selected"),
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::KeyAlt])],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Move Duplicate"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl, Key::KeyD])],
mouse: None,
label: String::from("Duplicate"),
plus: false,
},
]),
]),
SelectToolFsmState::Dragging => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Constrain to Axis"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
mouse: None,
label: String::from("Snap to Points (coming soon)"),
plus: false,
},
])]),
SelectToolFsmState::DrawingBox => HintData(vec![]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
+185
View File
@@ -0,0 +1,185 @@
use super::shared::resize::Resize;
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::{Key, MouseMotion};
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
use crate::viewport_tools::tool_options::{ShapeType, ToolOptions};
use graphene::layers::style;
use graphene::Operation;
use glam::DAffine2;
use serde::{Deserialize, Serialize};
#[derive(Default)]
pub struct Shape {
fsm_state: ShapeToolFsmState,
data: ShapeToolData,
}
#[remain::sorted]
#[impl_message(Message, ToolMessage, Shape)]
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
pub enum ShapeMessage {
Abort,
DragStart,
DragStop,
Resize { center: Key, lock_ratio: Key },
}
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Shape {
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;
}
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);
}
}
fn actions(&self) -> ActionList {
use ShapeToolFsmState::*;
match self.fsm_state {
Ready => actions!(ShapeMessageDiscriminant; DragStart),
Drawing => actions!(ShapeMessageDiscriminant; DragStop, Abort, Resize),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum ShapeToolFsmState {
Ready,
Drawing,
}
impl Default for ShapeToolFsmState {
fn default() -> Self {
ShapeToolFsmState::Ready
}
}
#[derive(Clone, Debug, Default)]
struct ShapeToolData {
sides: u8,
data: Resize,
}
impl Fsm for ShapeToolFsmState {
type ToolData = ShapeToolData;
fn transition(
self,
event: ToolMessage,
document: &DocumentMessageHandler,
tool_data: &DocumentToolData,
data: &mut Self::ToolData,
input: &InputPreprocessorMessageHandler,
responses: &mut VecDeque<Message>,
) -> Self {
let mut shape_data = &mut data.data;
use ShapeMessage::*;
use ShapeToolFsmState::*;
if let ToolMessage::Shape(event) = event {
match (self, event) {
(Ready, DragStart) => {
shape_data.start(document, input.mouse.position);
responses.push_back(DocumentMessage::StartTransaction.into());
shape_data.path = Some(vec![generate_uuid()]);
responses.push_back(DocumentMessage::DeselectAllLayers.into());
data.sides = match tool_data.tool_options.get(&ToolType::Shape) {
Some(&ToolOptions::Shape {
shape_type: ShapeType::Polygon { vertices },
}) => vertices as u8,
_ => 6,
};
responses.push_back(
Operation::AddNgon {
path: shape_data.path.clone().unwrap(),
insert_index: -1,
transform: DAffine2::ZERO.to_cols_array(),
sides: data.sides,
style: style::PathStyle::new(None, Some(style::Fill::new(tool_data.primary_color))),
}
.into(),
);
Drawing
}
(state, Resize { center, lock_ratio }) => {
if let Some(message) = shape_data.calculate_transform(document, center, lock_ratio, input) {
responses.push_back(message);
}
state
}
(Drawing, DragStop) => {
// TODO: introduce comparison threshold when operating with canvas coordinates (https://github.com/GraphiteEditor/Graphite/issues/100)
match shape_data.drag_start == input.mouse.position {
true => responses.push_back(DocumentMessage::AbortTransaction.into()),
false => responses.push_back(DocumentMessage::CommitTransaction.into()),
}
shape_data.cleanup();
Ready
}
(Drawing, Abort) => {
responses.push_back(DocumentMessage::AbortTransaction.into());
shape_data.cleanup();
Ready
}
_ => self,
}
} else {
self
}
}
fn update_hints(&self, responses: &mut VecDeque<Message>) {
let hint_data = match self {
ShapeToolFsmState::Ready => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![],
mouse: Some(MouseMotion::LmbDrag),
label: String::from("Draw Shape"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Constrain 1:1 Aspect"),
plus: true,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: true,
},
])]),
ShapeToolFsmState::Drawing => HintData(vec![HintGroup(vec![
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
mouse: None,
label: String::from("Constrain 1:1 Aspect"),
plus: false,
},
HintInfo {
key_groups: vec![KeysGroup(vec![Key::KeyAlt])],
mouse: None,
label: String::from("From Center"),
plus: false,
},
])]),
};
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
}
}
@@ -0,0 +1 @@
pub mod resize;
@@ -0,0 +1,57 @@
use crate::document::DocumentMessageHandler;
use crate::input::keyboard::Key;
use crate::input::mouse::ViewportPosition;
use crate::input::InputPreprocessorMessageHandler;
use crate::message_prelude::*;
use crate::viewport_tools::snapping::SnapHandler;
use graphene::Operation;
use glam::{DAffine2, DVec2, Vec2Swizzles};
#[derive(Clone, Debug, Default)]
pub struct Resize {
pub drag_start: ViewportPosition,
pub path: Option<Vec<LayerId>>,
snap_handler: SnapHandler,
}
impl Resize {
/// Starts a resize, assigning the snap targets and snapping the starting position.
pub fn start(&mut self, document: &DocumentMessageHandler, mouse_position: DVec2) {
let layers = document.all_layers_sorted();
self.snap_handler.start_snap(document, layers, &[]);
self.drag_start = self.snap_handler.snap_position(document, mouse_position);
}
pub fn calculate_transform(&self, document: &DocumentMessageHandler, center: Key, lock_ratio: Key, ipp: &InputPreprocessorMessageHandler) -> Option<Message> {
if let Some(path) = &self.path {
let mut start = self.drag_start;
let stop = self.snap_handler.snap_position(document, ipp.mouse.position);
let mut size = stop - start;
if ipp.keyboard.get(lock_ratio as usize) {
size = size.abs().max(size.abs().yx()) * size.signum();
}
if ipp.keyboard.get(center as usize) {
start -= size;
size *= 2.;
}
Some(
Operation::SetLayerTransformInViewport {
path: path.to_vec(),
transform: DAffine2::from_scale_angle_translation(size, 0., start).to_cols_array(),
}
.into(),
)
} else {
None
}
}
pub fn cleanup(&mut self) {
self.snap_handler.cleanup();
self.path = None;
}
}