Implement the Crop Tool for artboard resizing (#519)
* Extract transformation cage to a seperate file * Hook up tool * Implement resize * Draw artboards * centre and constrain * Bounding box is rotated * Fix transform handle positions for artboard * Drag layers * Snapping * Fix imports * Cleanup * Remove allocation from bounding_boxes * Round artboard size and position * Hints * Fix rotated transform cage * Code review changes * Hints changes Co-authored-by: Dennis <dennis@kobert.dev> Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
29485001e9
commit
45edeb2a2b
|
|
@ -117,7 +117,7 @@ impl Dispatcher {
|
||||||
&& !(matches!(
|
&& !(matches!(
|
||||||
message,
|
message,
|
||||||
InputPreprocessor(_) | Frontend(FrontendMessage::UpdateCanvasZoom { .. }) | Frontend(FrontendMessage::UpdateCanvasRotation { .. })
|
InputPreprocessor(_) | Frontend(FrontendMessage::UpdateCanvasZoom { .. }) | Frontend(FrontendMessage::UpdateCanvasRotation { .. })
|
||||||
) || MessageDiscriminant::from(message).local_name().ends_with("MouseMove"))
|
) || MessageDiscriminant::from(message).local_name().ends_with("PointerMove"))
|
||||||
{
|
{
|
||||||
log::trace!("Message: {:?}", message);
|
log::trace!("Message: {:?}", message);
|
||||||
// log::trace!("Hints: {:?}", self.input_mapper_message_handler.hints(self.collect_actions()));
|
// log::trace!("Hints: {:?}", self.input_mapper_message_handler.hints(self.collect_actions()));
|
||||||
|
|
|
||||||
|
|
@ -14,12 +14,16 @@ pub enum ArtboardMessage {
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
AddArtboard {
|
AddArtboard {
|
||||||
top: f64,
|
id: Option<LayerId>,
|
||||||
left: f64,
|
position: (f64, f64),
|
||||||
height: f64,
|
size: (f64, f64),
|
||||||
width: f64,
|
|
||||||
},
|
},
|
||||||
RenderArtboards,
|
RenderArtboards,
|
||||||
|
ResizeArtboard {
|
||||||
|
artboard: Vec<LayerId>,
|
||||||
|
position: (f64, f64),
|
||||||
|
size: (f64, f64),
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DocumentOperation> for ArtboardMessage {
|
impl From<DocumentOperation> for ArtboardMessage {
|
||||||
|
|
|
||||||
|
|
@ -5,7 +5,7 @@ use graphene::document::Document as GrapheneDocument;
|
||||||
use graphene::layers::style::{self, Fill, ViewMode};
|
use graphene::layers::style::{self, Fill, ViewMode};
|
||||||
use graphene::Operation as DocumentOperation;
|
use graphene::Operation as DocumentOperation;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::DAffine2;
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
||||||
|
|
@ -36,8 +36,8 @@ impl MessageHandler<ArtboardMessage, ()> for ArtboardMessageHandler {
|
||||||
},
|
},
|
||||||
|
|
||||||
// Messages
|
// Messages
|
||||||
AddArtboard { top, left, height, width } => {
|
AddArtboard { id, position, size } => {
|
||||||
let artboard_id = generate_uuid();
|
let artboard_id = id.unwrap_or_else(generate_uuid);
|
||||||
self.artboard_ids.push(artboard_id);
|
self.artboard_ids.push(artboard_id);
|
||||||
|
|
||||||
responses.push_back(
|
responses.push_back(
|
||||||
|
|
@ -45,7 +45,7 @@ impl MessageHandler<ArtboardMessage, ()> for ArtboardMessageHandler {
|
||||||
DocumentOperation::AddRect {
|
DocumentOperation::AddRect {
|
||||||
path: vec![artboard_id],
|
path: vec![artboard_id],
|
||||||
insert_index: -1,
|
insert_index: -1,
|
||||||
transform: DAffine2::from_scale_angle_translation(DVec2::new(height, width), 0., DVec2::new(top, left)).to_cols_array(),
|
transform: DAffine2::from_scale_angle_translation(size.into(), 0., position.into()).to_cols_array(),
|
||||||
style: style::PathStyle::new(None, Some(Fill::new(Color::WHITE))),
|
style: style::PathStyle::new(None, Some(Fill::new(Color::WHITE))),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
|
|
@ -73,6 +73,17 @@ impl MessageHandler<ArtboardMessage, ()> for ArtboardMessageHandler {
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
ResizeArtboard { artboard, position, size } => {
|
||||||
|
responses.push_back(
|
||||||
|
ArtboardMessage::DispatchOperation(Box::new(DocumentOperation::SetLayerTransform {
|
||||||
|
path: artboard,
|
||||||
|
transform: DAffine2::from_scale_angle_translation(size.into(), 0., position.into()).to_cols_array(),
|
||||||
|
}))
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
responses.push_back(DocumentMessage::RenderDocument.into());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -42,7 +42,7 @@ pub struct DocumentMessageHandler {
|
||||||
movement_handler: MovementMessageHandler,
|
movement_handler: MovementMessageHandler,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
overlays_message_handler: OverlaysMessageHandler,
|
overlays_message_handler: OverlaysMessageHandler,
|
||||||
artboard_message_handler: ArtboardMessageHandler,
|
pub artboard_message_handler: ArtboardMessageHandler,
|
||||||
#[serde(skip)]
|
#[serde(skip)]
|
||||||
transform_layer_handler: TransformLayerMessageHandler,
|
transform_layer_handler: TransformLayerMessageHandler,
|
||||||
pub overlays_visible: bool,
|
pub overlays_visible: bool,
|
||||||
|
|
@ -138,6 +138,10 @@ impl DocumentMessageHandler {
|
||||||
self.graphene_document.combined_viewport_bounding_box(paths)
|
self.graphene_document.combined_viewport_bounding_box(paths)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn artboard_bounding_box_and_transform(&self, path: &[LayerId]) -> Option<([DVec2; 2], DAffine2)> {
|
||||||
|
self.artboard_message_handler.artboards_graphene_document.bounding_box_and_transform(path).unwrap_or(None)
|
||||||
|
}
|
||||||
|
|
||||||
/// Create a new vector shape representation with the underlying kurbo data, VectorManipulatorShape
|
/// Create a new vector shape representation with the underlying kurbo data, VectorManipulatorShape
|
||||||
pub fn selected_visible_layers_vector_shapes(&self, responses: &mut VecDeque<Message>) -> Vec<VectorShape> {
|
pub fn selected_visible_layers_vector_shapes(&self, responses: &mut VecDeque<Message>) -> Vec<VectorShape> {
|
||||||
let shapes = self.selected_layers().filter_map(|path_to_shape| {
|
let shapes = self.selected_layers().filter_map(|path_to_shape| {
|
||||||
|
|
@ -203,6 +207,20 @@ impl DocumentMessageHandler {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns the bounding boxes for all visible layers and artboards, optionally excluding any paths.
|
||||||
|
pub fn bounding_boxes<'a>(&'a self, ignore_document: Option<&'a Vec<Vec<LayerId>>>, ignore_artboard: Option<LayerId>) -> impl Iterator<Item = [DVec2; 2]> + 'a {
|
||||||
|
self.visible_layers()
|
||||||
|
.filter(move |path| ignore_document.map_or(true, |ignore_document| !ignore_document.iter().any(|ig| ig.as_slice() == *path)))
|
||||||
|
.filter_map(|path| self.graphene_document.viewport_bounding_box(path).ok()?)
|
||||||
|
.chain(
|
||||||
|
self.artboard_message_handler
|
||||||
|
.artboard_ids
|
||||||
|
.iter()
|
||||||
|
.filter(move |&&id| Some(id) != ignore_artboard)
|
||||||
|
.filter_map(|&path| self.artboard_message_handler.artboards_graphene_document.viewport_bounding_box(&[path]).ok()?),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn serialize_structure(&self, folder: &Folder, structure: &mut Vec<u64>, data: &mut Vec<LayerId>, path: &mut Vec<LayerId>) {
|
fn serialize_structure(&self, folder: &Folder, structure: &mut Vec<u64>, data: &mut Vec<LayerId>, path: &mut Vec<LayerId>) {
|
||||||
let mut space = 0;
|
let mut space = 0;
|
||||||
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()).rev() {
|
for (id, layer) in folder.layer_ids.iter().zip(folder.layers()).rev() {
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub enum MovementMessage {
|
||||||
IncreaseCanvasZoom {
|
IncreaseCanvasZoom {
|
||||||
center_on_mouse: bool,
|
center_on_mouse: bool,
|
||||||
},
|
},
|
||||||
MouseMove {
|
PointerMove {
|
||||||
snap_angle: Key,
|
snap_angle: Key,
|
||||||
wait_for_snap_angle_release: bool,
|
wait_for_snap_angle_release: bool,
|
||||||
snap_zoom: Key,
|
snap_zoom: Key,
|
||||||
|
|
|
||||||
|
|
@ -166,7 +166,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||||
}
|
}
|
||||||
responses.push_back(SetCanvasZoom { zoom_factor: new_scale }.into());
|
responses.push_back(SetCanvasZoom { zoom_factor: new_scale }.into());
|
||||||
}
|
}
|
||||||
MouseMove {
|
PointerMove {
|
||||||
snap_angle,
|
snap_angle,
|
||||||
wait_for_snap_angle_release,
|
wait_for_snap_angle_release,
|
||||||
snap_zoom,
|
snap_zoom,
|
||||||
|
|
@ -344,7 +344,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
||||||
|
|
||||||
if self.panning || self.tilting || self.zooming {
|
if self.panning || self.tilting || self.zooming {
|
||||||
let transforming = actions!(MovementMessageDiscriminant;
|
let transforming = actions!(MovementMessageDiscriminant;
|
||||||
MouseMove,
|
PointerMove,
|
||||||
TransformCanvasEnd,
|
TransformCanvasEnd,
|
||||||
);
|
);
|
||||||
common.extend(transforming);
|
common.extend(transforming);
|
||||||
|
|
|
||||||
|
|
@ -14,7 +14,7 @@ pub enum TransformLayerMessage {
|
||||||
CancelTransformOperation,
|
CancelTransformOperation,
|
||||||
ConstrainX,
|
ConstrainX,
|
||||||
ConstrainY,
|
ConstrainY,
|
||||||
MouseMove { slow_key: Key, snap_key: Key },
|
PointerMove { slow_key: Key, snap_key: Key },
|
||||||
TypeBackspace,
|
TypeBackspace,
|
||||||
TypeDecimalPoint,
|
TypeDecimalPoint,
|
||||||
TypeDigit { digit: u8 },
|
TypeDigit { digit: u8 },
|
||||||
|
|
|
||||||
|
|
@ -108,7 +108,7 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
|
||||||
}
|
}
|
||||||
ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),
|
ConstrainX => self.transform_operation.constrain_axis(Axis::X, &mut selected, self.snap),
|
||||||
ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap),
|
ConstrainY => self.transform_operation.constrain_axis(Axis::Y, &mut selected, self.snap),
|
||||||
MouseMove { slow_key, snap_key } => {
|
PointerMove { slow_key, snap_key } => {
|
||||||
self.slow = ipp.keyboard.get(slow_key as usize);
|
self.slow = ipp.keyboard.get(slow_key as usize);
|
||||||
|
|
||||||
let new_snap = ipp.keyboard.get(snap_key as usize);
|
let new_snap = ipp.keyboard.get(snap_key as usize);
|
||||||
|
|
@ -173,7 +173,7 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
|
||||||
|
|
||||||
if self.transform_operation != TransformOperation::None {
|
if self.transform_operation != TransformOperation::None {
|
||||||
let active = actions!(TransformLayerMessageDiscriminant;
|
let active = actions!(TransformLayerMessageDiscriminant;
|
||||||
MouseMove,
|
PointerMove,
|
||||||
CancelTransformOperation,
|
CancelTransformOperation,
|
||||||
ApplyTransformOperation,
|
ApplyTransformOperation,
|
||||||
TypeDigit,
|
TypeDigit,
|
||||||
|
|
|
||||||
|
|
@ -29,7 +29,7 @@ impl Default for Mapping {
|
||||||
let mappings = mapping![
|
let mappings = mapping![
|
||||||
// Higher priority than entries in sections below
|
// Higher priority than entries in sections below
|
||||||
entry! {action=PortfolioMessage::Paste { clipboard: Clipboard::User }, key_down=KeyV, modifiers=[KeyControl]},
|
entry! {action=PortfolioMessage::Paste { clipboard: Clipboard::User }, key_down=KeyV, modifiers=[KeyControl]},
|
||||||
entry! {action=MovementMessage::MouseMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, message=InputMapperMessage::PointerMove},
|
entry! {action=MovementMessage::PointerMove { snap_angle: KeyControl, wait_for_snap_angle_release: true, snap_zoom: KeyControl, zoom_from_viewport: None }, message=InputMapperMessage::PointerMove},
|
||||||
// Transform layers
|
// Transform layers
|
||||||
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter},
|
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter},
|
||||||
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=Lmb},
|
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=Lmb},
|
||||||
|
|
@ -41,18 +41,22 @@ impl Default for Mapping {
|
||||||
entry! {action=TransformLayerMessage::TypeNegate, key_down=KeyMinus},
|
entry! {action=TransformLayerMessage::TypeNegate, key_down=KeyMinus},
|
||||||
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyComma},
|
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyComma},
|
||||||
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod},
|
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod},
|
||||||
entry! {action=TransformLayerMessage::MouseMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]},
|
entry! {action=TransformLayerMessage::PointerMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]},
|
||||||
// Select
|
// Select
|
||||||
entry! {action=SelectMessage::MouseMove { axis_align: KeyShift, snap_angle: KeyControl }, message=InputMapperMessage::PointerMove},
|
entry! {action=SelectMessage::PointerMove { axis_align: KeyShift, snap_angle: KeyControl, center: KeyAlt }, message=InputMapperMessage::PointerMove},
|
||||||
entry! {action=SelectMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb},
|
entry! {action=SelectMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb},
|
||||||
entry! {action=SelectMessage::DragStop, key_up=Lmb},
|
entry! {action=SelectMessage::DragStop, key_up=Lmb},
|
||||||
entry! {action=SelectMessage::EditLayer, message=InputMapperMessage::DoubleClick},
|
entry! {action=SelectMessage::EditLayer, message=InputMapperMessage::DoubleClick},
|
||||||
entry! {action=SelectMessage::Abort, key_down=Rmb},
|
entry! {action=SelectMessage::Abort, key_down=Rmb},
|
||||||
entry! {action=SelectMessage::Abort, key_down=KeyEscape},
|
entry! {action=SelectMessage::Abort, key_down=KeyEscape},
|
||||||
|
// Crop
|
||||||
|
entry! {action=CropMessage::PointerDown, key_down=Lmb},
|
||||||
|
entry! {action=CropMessage::PointerMove { constrain_axis_or_aspect: KeyShift, center: KeyAlt }, message=InputMapperMessage::PointerMove},
|
||||||
|
entry! {action=CropMessage::PointerUp, key_up=Lmb},
|
||||||
// Navigate
|
// Navigate
|
||||||
entry! {action=NavigateMessage::ClickZoom { zoom_in: false }, key_up=Lmb, modifiers=[KeyShift]},
|
entry! {action=NavigateMessage::ClickZoom { zoom_in: false }, key_up=Lmb, modifiers=[KeyShift]},
|
||||||
entry! {action=NavigateMessage::ClickZoom { zoom_in: true }, key_up=Lmb},
|
entry! {action=NavigateMessage::ClickZoom { zoom_in: true }, key_up=Lmb},
|
||||||
entry! {action=NavigateMessage::MouseMove { snap_angle: KeyControl, snap_zoom: KeyControl }, message=InputMapperMessage::PointerMove},
|
entry! {action=NavigateMessage::PointerMove { snap_angle: KeyControl, snap_zoom: KeyControl }, message=InputMapperMessage::PointerMove},
|
||||||
entry! {action=NavigateMessage::TranslateCanvasBegin, key_down=Mmb},
|
entry! {action=NavigateMessage::TranslateCanvasBegin, key_down=Mmb},
|
||||||
entry! {action=NavigateMessage::RotateCanvasBegin, key_down=Rmb},
|
entry! {action=NavigateMessage::RotateCanvasBegin, key_down=Rmb},
|
||||||
entry! {action=NavigateMessage::ZoomCanvasBegin, key_down=Lmb},
|
entry! {action=NavigateMessage::ZoomCanvasBegin, key_down=Lmb},
|
||||||
|
|
|
||||||
|
|
@ -35,7 +35,7 @@ mod test {
|
||||||
|
|
||||||
let editor_mouse_state = EditorMouseState::from_editor_position(4., 809.);
|
let editor_mouse_state = EditorMouseState::from_editor_position(4., 809.);
|
||||||
let modifier_keys = ModifierKeys::ALT;
|
let modifier_keys = ModifierKeys::ALT;
|
||||||
let message = InputPreprocessorMessage::MouseMove { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys };
|
||||||
|
|
||||||
let mut responses = VecDeque::new();
|
let mut responses = VecDeque::new();
|
||||||
|
|
||||||
|
|
@ -51,7 +51,7 @@ mod test {
|
||||||
|
|
||||||
let editor_mouse_state = EditorMouseState::new();
|
let editor_mouse_state = EditorMouseState::new();
|
||||||
let modifier_keys = ModifierKeys::CONTROL;
|
let modifier_keys = ModifierKeys::CONTROL;
|
||||||
let message = InputPreprocessorMessage::MouseDown { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys };
|
||||||
|
|
||||||
let mut responses = VecDeque::new();
|
let mut responses = VecDeque::new();
|
||||||
|
|
||||||
|
|
@ -67,7 +67,7 @@ mod test {
|
||||||
|
|
||||||
let editor_mouse_state = EditorMouseState::new();
|
let editor_mouse_state = EditorMouseState::new();
|
||||||
let modifier_keys = ModifierKeys::SHIFT;
|
let modifier_keys = ModifierKeys::SHIFT;
|
||||||
let message = InputPreprocessorMessage::MouseUp { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys };
|
||||||
|
|
||||||
let mut responses = VecDeque::new();
|
let mut responses = VecDeque::new();
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -16,8 +16,8 @@ pub enum InputPreprocessorMessage {
|
||||||
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
KeyDown { key: Key, modifier_keys: ModifierKeys },
|
KeyDown { key: Key, modifier_keys: ModifierKeys },
|
||||||
KeyUp { key: Key, modifier_keys: ModifierKeys },
|
KeyUp { key: Key, modifier_keys: ModifierKeys },
|
||||||
MouseDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
|
||||||
MouseMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
|
||||||
MouseScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
MouseScroll { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
MouseUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
PointerDown { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
|
PointerMove { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
|
PointerUp { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys },
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -67,24 +67,6 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHa
|
||||||
self.keyboard.unset(key as usize);
|
self.keyboard.unset(key as usize);
|
||||||
responses.push_back(InputMapperMessage::KeyUp(key).into());
|
responses.push_back(InputMapperMessage::KeyUp(key).into());
|
||||||
}
|
}
|
||||||
InputPreprocessorMessage::MouseDown { editor_mouse_state, modifier_keys } => {
|
|
||||||
self.handle_modifier_keys(modifier_keys, responses);
|
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
|
||||||
self.mouse.position = mouse_state.position;
|
|
||||||
|
|
||||||
if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Pressed) {
|
|
||||||
responses.push_back(message);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
InputPreprocessorMessage::MouseMove { editor_mouse_state, modifier_keys } => {
|
|
||||||
self.handle_modifier_keys(modifier_keys, responses);
|
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
|
||||||
self.mouse.position = mouse_state.position;
|
|
||||||
|
|
||||||
responses.push_back(InputMapperMessage::PointerMove.into());
|
|
||||||
}
|
|
||||||
InputPreprocessorMessage::MouseScroll { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::MouseScroll { editor_mouse_state, modifier_keys } => {
|
||||||
self.handle_modifier_keys(modifier_keys, responses);
|
self.handle_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
|
|
@ -94,7 +76,25 @@ impl MessageHandler<InputPreprocessorMessage, ()> for InputPreprocessorMessageHa
|
||||||
|
|
||||||
responses.push_back(InputMapperMessage::MouseScroll.into());
|
responses.push_back(InputMapperMessage::MouseScroll.into());
|
||||||
}
|
}
|
||||||
InputPreprocessorMessage::MouseUp { editor_mouse_state, modifier_keys } => {
|
InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys } => {
|
||||||
|
self.handle_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
|
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
||||||
|
self.mouse.position = mouse_state.position;
|
||||||
|
|
||||||
|
if let Some(message) = self.translate_mouse_event(mouse_state, KeyPosition::Pressed) {
|
||||||
|
responses.push_back(message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys } => {
|
||||||
|
self.handle_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
|
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
||||||
|
self.mouse.position = mouse_state.position;
|
||||||
|
|
||||||
|
responses.push_back(InputMapperMessage::PointerMove.into());
|
||||||
|
}
|
||||||
|
InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys } => {
|
||||||
self.handle_modifier_keys(modifier_keys, responses);
|
self.handle_modifier_keys(modifier_keys, responses);
|
||||||
|
|
||||||
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds);
|
||||||
|
|
|
||||||
|
|
@ -52,17 +52,17 @@ impl EditorTestUtils for Editor {
|
||||||
let mut editor_mouse_state = EditorMouseState::new();
|
let mut editor_mouse_state = EditorMouseState::new();
|
||||||
editor_mouse_state.editor_position = ViewportPosition::new(x, y);
|
editor_mouse_state.editor_position = ViewportPosition::new(x, y);
|
||||||
let modifier_keys = ModifierKeys::default();
|
let modifier_keys = ModifierKeys::default();
|
||||||
self.input(InputPreprocessorMessage::MouseMove { editor_mouse_state, modifier_keys });
|
self.input(InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mousedown(&mut self, editor_mouse_state: EditorMouseState) {
|
fn mousedown(&mut self, editor_mouse_state: EditorMouseState) {
|
||||||
let modifier_keys = ModifierKeys::default();
|
let modifier_keys = ModifierKeys::default();
|
||||||
self.input(InputPreprocessorMessage::MouseDown { editor_mouse_state, modifier_keys });
|
self.input(InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn mouseup(&mut self, editor_mouse_state: EditorMouseState) {
|
fn mouseup(&mut self, editor_mouse_state: EditorMouseState) {
|
||||||
let modifier_keys = ModifierKeys::default();
|
let modifier_keys = ModifierKeys::default();
|
||||||
self.handle_message(InputPreprocessorMessage::MouseUp { editor_mouse_state, modifier_keys });
|
self.handle_message(InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys });
|
||||||
}
|
}
|
||||||
|
|
||||||
fn lmb_mousedown(&mut self, x: f64, y: f64) {
|
fn lmb_mousedown(&mut self, x: f64, y: f64) {
|
||||||
|
|
|
||||||
|
|
@ -95,13 +95,9 @@ impl SnapHandler {
|
||||||
|
|
||||||
/// Gets a list of snap targets for the X and Y axes (if specified) in Viewport coords for the target layers (usually all layers or all non-selected layers.)
|
/// Gets a list of snap targets for the X and Y axes (if specified) in Viewport coords for the target layers (usually all layers or all non-selected layers.)
|
||||||
/// This should be called at the start of a drag.
|
/// This should be called at the start of a drag.
|
||||||
pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator<Item = &'a [LayerId]>, snap_x: bool, snap_y: bool) {
|
pub fn start_snap(&mut self, document_message_handler: &DocumentMessageHandler, bounding_boxes: impl Iterator<Item = [DVec2; 2]>, snap_x: bool, snap_y: bool) {
|
||||||
if document_message_handler.snapping_enabled {
|
if document_message_handler.snapping_enabled {
|
||||||
let (x_targets, y_targets) = target_layers
|
let (x_targets, y_targets) = bounding_boxes.flat_map(|[bound1, bound2]| [bound1, bound2, ((bound1 + bound2) / 2.)]).map(|vec| vec.into()).unzip();
|
||||||
.filter_map(|path| document_message_handler.graphene_document.viewport_bounding_box(path).ok()?)
|
|
||||||
.flat_map(|[bound1, bound2]| [bound1, bound2, ((bound1 + bound2) / 2.)])
|
|
||||||
.map(|vec| vec.into())
|
|
||||||
.unzip();
|
|
||||||
|
|
||||||
// Could be made into sorted Vec or a HashSet for more performant lookups.
|
// Could be made into sorted Vec or a HashSet for more performant lookups.
|
||||||
self.snap_targets = Some((if snap_x { x_targets } else { Vec::new() }, if snap_y { y_targets } else { Vec::new() }));
|
self.snap_targets = Some((if snap_x { x_targets } else { Vec::new() }, if snap_y { y_targets } else { Vec::new() }));
|
||||||
|
|
@ -127,19 +123,12 @@ impl SnapHandler {
|
||||||
&mut self,
|
&mut self,
|
||||||
responses: &mut VecDeque<Message>,
|
responses: &mut VecDeque<Message>,
|
||||||
document_message_handler: &DocumentMessageHandler,
|
document_message_handler: &DocumentMessageHandler,
|
||||||
selected_layers: &[Vec<LayerId>],
|
(snap_x, snap_y): (Vec<f64>, Vec<f64>),
|
||||||
viewport_bounds: DVec2,
|
viewport_bounds: DVec2,
|
||||||
mouse_delta: DVec2,
|
mouse_delta: DVec2,
|
||||||
) -> DVec2 {
|
) -> DVec2 {
|
||||||
if document_message_handler.snapping_enabled {
|
if document_message_handler.snapping_enabled {
|
||||||
if let Some((targets_x, targets_y)) = &self.snap_targets {
|
if let Some((targets_x, targets_y)) = &self.snap_targets {
|
||||||
let (snap_x, snap_y): (Vec<f64>, Vec<f64>) = selected_layers
|
|
||||||
.iter()
|
|
||||||
.filter_map(|path| document_message_handler.graphene_document.viewport_bounding_box(path).ok()?)
|
|
||||||
.flat_map(|[bound1, bound2]| [bound1, bound2, (bound1 + bound2) / 2.])
|
|
||||||
.map(|vec| vec.into())
|
|
||||||
.unzip();
|
|
||||||
|
|
||||||
let positions = targets_x.iter().flat_map(|&target| snap_x.iter().map(move |&snap| (target, target - mouse_delta.x - snap)));
|
let positions = targets_x.iter().flat_map(|&target| snap_x.iter().map(move |&snap| (target, target - mouse_delta.x - snap)));
|
||||||
let distances = targets_y.iter().flat_map(|&target| snap_y.iter().map(move |&snap| (target, target - mouse_delta.y - snap)));
|
let distances = targets_y.iter().flat_map(|&target| snap_y.iter().map(move |&snap| (target, target - mouse_delta.y - snap)));
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -187,7 +187,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy
|
||||||
match message_type {
|
match message_type {
|
||||||
StandardToolMessageType::DocumentIsDirty => match tool {
|
StandardToolMessageType::DocumentIsDirty => match tool {
|
||||||
ToolType::Select => Some(SelectMessage::DocumentIsDirty.into()),
|
ToolType::Select => Some(SelectMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Crop => None, // Some(CropMessage::DocumentIsDirty.into()),
|
ToolType::Crop => Some(CropMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Navigate => None, // Some(NavigateMessage::DocumentIsDirty.into()),
|
ToolType::Navigate => None, // Some(NavigateMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Eyedropper => None, // Some(EyedropperMessage::DocumentIsDirty.into()),
|
ToolType::Eyedropper => None, // Some(EyedropperMessage::DocumentIsDirty.into()),
|
||||||
ToolType::Text => Some(TextMessage::DocumentIsDirty.into()),
|
ToolType::Text => Some(TextMessage::DocumentIsDirty.into()),
|
||||||
|
|
@ -210,7 +210,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy
|
||||||
},
|
},
|
||||||
StandardToolMessageType::Abort => match tool {
|
StandardToolMessageType::Abort => match tool {
|
||||||
ToolType::Select => Some(SelectMessage::Abort.into()),
|
ToolType::Select => Some(SelectMessage::Abort.into()),
|
||||||
// ToolType::Crop => Some(CropMessage::Abort.into()),
|
ToolType::Crop => Some(CropMessage::Abort.into()),
|
||||||
ToolType::Navigate => Some(NavigateMessage::Abort.into()),
|
ToolType::Navigate => Some(NavigateMessage::Abort.into()),
|
||||||
ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()),
|
ToolType::Eyedropper => Some(EyedropperMessage::Abort.into()),
|
||||||
ToolType::Text => Some(TextMessage::Abort.into()),
|
ToolType::Text => Some(TextMessage::Abort.into()),
|
||||||
|
|
|
||||||
|
|
@ -1,30 +1,377 @@
|
||||||
|
use crate::consts::SELECTION_TOLERANCE;
|
||||||
|
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::layout::widgets::PropertyHolder;
|
||||||
use crate::message_prelude::*;
|
use crate::message_prelude::*;
|
||||||
use crate::viewport_tools::tool::ToolActionHandlerData;
|
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 super::shared::transformation_cage::*;
|
||||||
|
|
||||||
|
use glam::{DVec2, Vec2Swizzles};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Crop;
|
pub struct Crop {
|
||||||
|
fsm_state: CropToolFsmState,
|
||||||
|
data: CropToolData,
|
||||||
|
}
|
||||||
|
|
||||||
#[remain::sorted]
|
#[remain::sorted]
|
||||||
#[impl_message(Message, ToolMessage, Crop)]
|
#[impl_message(Message, ToolMessage, Crop)]
|
||||||
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
#[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||||
pub enum CropMessage {
|
pub enum CropMessage {
|
||||||
// Standard messages
|
// Standard messages
|
||||||
// #[remain::unsorted]
|
#[remain::unsorted]
|
||||||
// Abort,
|
Abort,
|
||||||
|
#[remain::unsorted]
|
||||||
|
DocumentIsDirty,
|
||||||
|
|
||||||
// Tool-specific messages
|
// Tool-specific messages
|
||||||
MouseMove,
|
PointerDown,
|
||||||
|
PointerMove {
|
||||||
|
constrain_axis_or_aspect: Key,
|
||||||
|
center: Key,
|
||||||
|
},
|
||||||
|
PointerUp,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Crop {
|
impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Crop {
|
||||||
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque<Message>) {
|
||||||
todo!("{}::handle_input {:?} {:?} {:?} ", module_path!(), action, data, responses);
|
if action == ToolMessage::UpdateHints {
|
||||||
|
self.fsm_state.update_hints(responses);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if action == ToolMessage::UpdateCursor {
|
||||||
|
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||||
|
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!();
|
advertise_actions!(CropMessageDiscriminant; PointerDown, PointerUp, PointerMove, Abort);
|
||||||
}
|
}
|
||||||
|
|
||||||
impl PropertyHolder for Crop {}
|
impl PropertyHolder for Crop {}
|
||||||
|
|
||||||
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
||||||
|
enum CropToolFsmState {
|
||||||
|
Ready,
|
||||||
|
Drawing,
|
||||||
|
ResizingBounds,
|
||||||
|
Dragging,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Default for CropToolFsmState {
|
||||||
|
fn default() -> Self {
|
||||||
|
CropToolFsmState::Ready
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
struct CropToolData {
|
||||||
|
bounding_box_overlays: Option<BoundingBoxOverlays>,
|
||||||
|
selected_board: Option<LayerId>,
|
||||||
|
snap_handler: SnapHandler,
|
||||||
|
cursor: MouseCursorIcon,
|
||||||
|
drag_start: DVec2,
|
||||||
|
drag_current: DVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fsm for CropToolFsmState {
|
||||||
|
type ToolData = CropToolData;
|
||||||
|
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::Crop(event) = event {
|
||||||
|
match (self, event) {
|
||||||
|
(CropToolFsmState::Ready | CropToolFsmState::ResizingBounds | CropToolFsmState::Dragging, CropMessage::DocumentIsDirty) => {
|
||||||
|
let mut buffer = Vec::new();
|
||||||
|
match (
|
||||||
|
data.selected_board.map(|path| document.artboard_bounding_box_and_transform(&[path])).unwrap_or(None),
|
||||||
|
data.bounding_box_overlays.take(),
|
||||||
|
) {
|
||||||
|
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(&mut buffer),
|
||||||
|
(Some((bounds, transform)), paths) => {
|
||||||
|
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(&mut buffer));
|
||||||
|
|
||||||
|
bounding_box_overlays.bounds = bounds;
|
||||||
|
bounding_box_overlays.transform = transform;
|
||||||
|
|
||||||
|
bounding_box_overlays.transform(&mut buffer);
|
||||||
|
|
||||||
|
data.bounding_box_overlays = Some(bounding_box_overlays);
|
||||||
|
|
||||||
|
responses.push_back(OverlaysMessage::Rerender.into());
|
||||||
|
}
|
||||||
|
_ => {}
|
||||||
|
};
|
||||||
|
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
||||||
|
self
|
||||||
|
}
|
||||||
|
(CropToolFsmState::Ready, CropMessage::PointerDown) => {
|
||||||
|
data.drag_start = input.mouse.position;
|
||||||
|
data.drag_current = input.mouse.position;
|
||||||
|
|
||||||
|
let dragging_bounds = if let Some(bounding_box) = &mut data.bounding_box_overlays {
|
||||||
|
let edges = bounding_box.check_selected_edges(input.mouse.position);
|
||||||
|
|
||||||
|
bounding_box.selected_edges = edges.map(|(top, bottom, left, right)| {
|
||||||
|
let edges = SelectedEdges::new(top, bottom, left, right, bounding_box.bounds);
|
||||||
|
bounding_box.pivot = edges.calculate_pivot();
|
||||||
|
edges
|
||||||
|
});
|
||||||
|
|
||||||
|
edges
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(selected_edges) = dragging_bounds {
|
||||||
|
let snap_x = selected_edges.2 || selected_edges.3;
|
||||||
|
let snap_y = selected_edges.0 || selected_edges.1;
|
||||||
|
|
||||||
|
data.snap_handler
|
||||||
|
.start_snap(document, document.bounding_boxes(None, Some(data.selected_board.unwrap())), snap_x, snap_y);
|
||||||
|
|
||||||
|
CropToolFsmState::ResizingBounds
|
||||||
|
} else {
|
||||||
|
let tolerance = DVec2::splat(SELECTION_TOLERANCE);
|
||||||
|
let quad = Quad::from_box([input.mouse.position - tolerance, input.mouse.position + tolerance]);
|
||||||
|
let intersection = document.artboard_message_handler.artboards_graphene_document.intersects_quad_root(quad);
|
||||||
|
|
||||||
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
|
if let Some(intersection) = intersection.last() {
|
||||||
|
data.selected_board = Some(intersection[0]);
|
||||||
|
|
||||||
|
data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(intersection[0])), true, true);
|
||||||
|
|
||||||
|
CropToolFsmState::Dragging
|
||||||
|
} else {
|
||||||
|
let id = generate_uuid();
|
||||||
|
data.selected_board = Some(id);
|
||||||
|
|
||||||
|
data.snap_handler.start_snap(document, document.bounding_boxes(None, Some(id)), true, true);
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
ArtboardMessage::AddArtboard {
|
||||||
|
id: Some(id),
|
||||||
|
position: (0., 0.),
|
||||||
|
size: (0., 0.),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
CropToolFsmState::Drawing
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(CropToolFsmState::ResizingBounds, CropMessage::PointerMove { constrain_axis_or_aspect, center }) => {
|
||||||
|
if let Some(bounds) = &data.bounding_box_overlays {
|
||||||
|
if let Some(movement) = &bounds.selected_edges {
|
||||||
|
let from_center = input.keyboard.get(center as usize);
|
||||||
|
let constrain_square = input.keyboard.get(constrain_axis_or_aspect as usize);
|
||||||
|
|
||||||
|
let mouse_position = input.mouse.position;
|
||||||
|
let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position);
|
||||||
|
|
||||||
|
let [position, size] = movement.new_size(snapped_mouse_position, bounds.transform, from_center, constrain_square);
|
||||||
|
let position = movement.center_position(position, size, from_center);
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
ArtboardMessage::ResizeArtboard {
|
||||||
|
artboard: vec![data.selected_board.unwrap()],
|
||||||
|
position: position.round().into(),
|
||||||
|
size: size.round().into(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
CropToolFsmState::ResizingBounds
|
||||||
|
}
|
||||||
|
(CropToolFsmState::Dragging, CropMessage::PointerMove { constrain_axis_or_aspect, .. }) => {
|
||||||
|
if let Some(bounds) = &data.bounding_box_overlays {
|
||||||
|
let axis_align = input.keyboard.get(constrain_axis_or_aspect as usize);
|
||||||
|
|
||||||
|
let mouse_position = axis_align_drag(axis_align, input.mouse.position, data.drag_start);
|
||||||
|
let mouse_delta = mouse_position - data.drag_current;
|
||||||
|
|
||||||
|
let snap = bounds.evaluate_transform_handle_positions().iter().map(|v| (v.x, v.y)).unzip();
|
||||||
|
let closest_move = data.snap_handler.snap_layers(responses, document, snap, input.viewport_bounds.size(), mouse_delta);
|
||||||
|
|
||||||
|
let size = bounds.bounds[1] - bounds.bounds[0];
|
||||||
|
|
||||||
|
let position = bounds.bounds[0] + bounds.transform.inverse().transform_vector2(mouse_position - data.drag_current + closest_move);
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
ArtboardMessage::ResizeArtboard {
|
||||||
|
artboard: vec![data.selected_board.unwrap()],
|
||||||
|
position: position.round().into(),
|
||||||
|
size: size.round().into(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
|
|
||||||
|
data.drag_current = mouse_position + closest_move;
|
||||||
|
}
|
||||||
|
CropToolFsmState::Dragging
|
||||||
|
}
|
||||||
|
(CropToolFsmState::Drawing, CropMessage::PointerMove { constrain_axis_or_aspect, center }) => {
|
||||||
|
let mouse_position = input.mouse.position;
|
||||||
|
let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position);
|
||||||
|
|
||||||
|
let root_transform = document.graphene_document.root.transform.inverse();
|
||||||
|
|
||||||
|
let mut start = data.drag_start;
|
||||||
|
let mut size = snapped_mouse_position - start;
|
||||||
|
// Constrain axis
|
||||||
|
if input.keyboard.get(constrain_axis_or_aspect as usize) {
|
||||||
|
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||||
|
}
|
||||||
|
// From center
|
||||||
|
if input.keyboard.get(center as usize) {
|
||||||
|
start -= size;
|
||||||
|
size *= 2.;
|
||||||
|
}
|
||||||
|
|
||||||
|
let start = root_transform.transform_point2(start);
|
||||||
|
let size = root_transform.transform_vector2(size);
|
||||||
|
|
||||||
|
responses.push_back(
|
||||||
|
ArtboardMessage::ResizeArtboard {
|
||||||
|
artboard: vec![data.selected_board.unwrap()],
|
||||||
|
position: start.round().into(),
|
||||||
|
size: size.round().into(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
|
||||||
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
|
|
||||||
|
CropToolFsmState::Drawing
|
||||||
|
}
|
||||||
|
(CropToolFsmState::Ready, CropMessage::PointerMove { .. }) => {
|
||||||
|
let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, false));
|
||||||
|
|
||||||
|
if data.cursor != cursor {
|
||||||
|
data.cursor = cursor;
|
||||||
|
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
||||||
|
}
|
||||||
|
|
||||||
|
CropToolFsmState::Ready
|
||||||
|
}
|
||||||
|
(CropToolFsmState::ResizingBounds, CropMessage::PointerUp) => {
|
||||||
|
data.snap_handler.cleanup(responses);
|
||||||
|
|
||||||
|
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||||
|
bounds.original_transforms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
CropToolFsmState::Ready
|
||||||
|
}
|
||||||
|
(CropToolFsmState::Drawing, CropMessage::PointerUp) => {
|
||||||
|
data.snap_handler.cleanup(responses);
|
||||||
|
|
||||||
|
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||||
|
bounds.original_transforms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
responses.push_back(ToolMessage::DocumentIsDirty.into());
|
||||||
|
|
||||||
|
CropToolFsmState::Ready
|
||||||
|
}
|
||||||
|
(CropToolFsmState::Dragging, CropMessage::PointerUp) => {
|
||||||
|
data.snap_handler.cleanup(responses);
|
||||||
|
|
||||||
|
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||||
|
bounds.original_transforms.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
CropToolFsmState::Ready
|
||||||
|
}
|
||||||
|
(_, CropMessage::Abort) => {
|
||||||
|
if let Some(bounding_box_overlays) = data.bounding_box_overlays.take() {
|
||||||
|
bounding_box_overlays.delete(responses);
|
||||||
|
}
|
||||||
|
|
||||||
|
data.snap_handler.cleanup(responses);
|
||||||
|
CropToolFsmState::Ready
|
||||||
|
}
|
||||||
|
_ => self,
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_hints(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
let hint_data = match self {
|
||||||
|
CropToolFsmState::Ready => HintData(vec![
|
||||||
|
HintGroup(vec![HintInfo {
|
||||||
|
key_groups: vec![],
|
||||||
|
mouse: Some(MouseMotion::LmbDrag),
|
||||||
|
label: String::from("Draw Artboard"),
|
||||||
|
plus: false,
|
||||||
|
}]),
|
||||||
|
HintGroup(vec![HintInfo {
|
||||||
|
key_groups: vec![],
|
||||||
|
mouse: Some(MouseMotion::LmbDrag),
|
||||||
|
label: String::from("Move Artboard"),
|
||||||
|
plus: false,
|
||||||
|
}]),
|
||||||
|
]),
|
||||||
|
CropToolFsmState::Dragging => HintData(vec![HintGroup(vec![HintInfo {
|
||||||
|
key_groups: vec![KeysGroup(vec![Key::KeyShift])],
|
||||||
|
mouse: None,
|
||||||
|
label: String::from("Constrain to Axis"),
|
||||||
|
plus: false,
|
||||||
|
}])]),
|
||||||
|
CropToolFsmState::Drawing | CropToolFsmState::ResizingBounds => 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());
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_cursor(&self, responses: &mut VecDeque<Message>) {
|
||||||
|
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ impl Fsm for LineToolFsmState {
|
||||||
if let ToolMessage::Line(event) = event {
|
if let ToolMessage::Line(event) = event {
|
||||||
match (self, event) {
|
match (self, event) {
|
||||||
(Ready, DragStart) => {
|
(Ready, DragStart) => {
|
||||||
data.snap_handler.start_snap(document, document.visible_layers(), true, true);
|
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
||||||
data.drag_start = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
data.drag_start = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
||||||
|
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
|
|
|
||||||
|
|
@ -28,7 +28,7 @@ pub enum NavigateMessage {
|
||||||
ClickZoom {
|
ClickZoom {
|
||||||
zoom_in: bool,
|
zoom_in: bool,
|
||||||
},
|
},
|
||||||
MouseMove {
|
PointerMove {
|
||||||
snap_angle: Key,
|
snap_angle: Key,
|
||||||
snap_zoom: Key,
|
snap_zoom: Key,
|
||||||
},
|
},
|
||||||
|
|
@ -66,7 +66,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Navigate {
|
||||||
|
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(NavigateMessageDiscriminant; TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin),
|
Ready => actions!(NavigateMessageDiscriminant; TranslateCanvasBegin, RotateCanvasBegin, ZoomCanvasBegin),
|
||||||
_ => actions!(NavigateMessageDiscriminant; ClickZoom, MouseMove, TransformCanvasEnd),
|
_ => actions!(NavigateMessageDiscriminant; ClickZoom, PointerMove, TransformCanvasEnd),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -111,7 +111,7 @@ impl Fsm for NavigateToolFsmState {
|
||||||
ClickZoom { zoom_in } => {
|
ClickZoom { zoom_in } => {
|
||||||
messages.push_front(MovementMessage::TransformCanvasEnd.into());
|
messages.push_front(MovementMessage::TransformCanvasEnd.into());
|
||||||
|
|
||||||
// Mouse has not moved from mousedown to mouseup
|
// Mouse has not moved from pointerdown to pointerup
|
||||||
if data.drag_start == input.mouse.position {
|
if data.drag_start == input.mouse.position {
|
||||||
messages.push_front(if zoom_in {
|
messages.push_front(if zoom_in {
|
||||||
MovementMessage::IncreaseCanvasZoom { center_on_mouse: true }.into()
|
MovementMessage::IncreaseCanvasZoom { center_on_mouse: true }.into()
|
||||||
|
|
@ -122,9 +122,9 @@ impl Fsm for NavigateToolFsmState {
|
||||||
|
|
||||||
NavigateToolFsmState::Ready
|
NavigateToolFsmState::Ready
|
||||||
}
|
}
|
||||||
MouseMove { snap_angle, snap_zoom } => {
|
PointerMove { snap_angle, snap_zoom } => {
|
||||||
messages.push_front(
|
messages.push_front(
|
||||||
MovementMessage::MouseMove {
|
MovementMessage::PointerMove {
|
||||||
snap_angle,
|
snap_angle,
|
||||||
wait_for_snap_angle_release: false,
|
wait_for_snap_angle_release: false,
|
||||||
snap_zoom,
|
snap_zoom,
|
||||||
|
|
|
||||||
|
|
@ -142,7 +142,7 @@ impl Fsm for PathToolFsmState {
|
||||||
// Select the first point within the threshold (in pixels)
|
// Select the first point within the threshold (in pixels)
|
||||||
if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) {
|
if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) {
|
||||||
responses.push_back(DocumentMessage::StartTransaction.into());
|
responses.push_back(DocumentMessage::StartTransaction.into());
|
||||||
data.snap_handler.start_snap(document, document.visible_layers(), true, true);
|
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
||||||
let snap_points = data
|
let snap_points = data
|
||||||
.shape_editor
|
.shape_editor
|
||||||
.shapes_to_modify
|
.shapes_to_modify
|
||||||
|
|
|
||||||
|
|
@ -155,7 +155,7 @@ impl Fsm for PenToolFsmState {
|
||||||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||||
data.path = Some(document.get_path_for_new_layer());
|
data.path = Some(document.get_path_for_new_layer());
|
||||||
|
|
||||||
data.snap_handler.start_snap(document, document.visible_layers(), true, true);
|
data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
||||||
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
let snapped_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, input.mouse.position);
|
||||||
|
|
||||||
let pos = transform.inverse().transform_point2(snapped_position);
|
let pos = transform.inverse().transform_point2(snapped_position);
|
||||||
|
|
|
||||||
|
|
@ -1,5 +1,5 @@
|
||||||
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_ACCENT, ROTATE_SNAP_ANGLE, SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE};
|
use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE};
|
||||||
use crate::document::transformation::{OriginalTransforms, Selected};
|
use crate::document::transformation::Selected;
|
||||||
use crate::document::utility_types::{AlignAggregate, AlignAxis, FlipAxis};
|
use crate::document::utility_types::{AlignAggregate, AlignAxis, FlipAxis};
|
||||||
use crate::document::DocumentMessageHandler;
|
use crate::document::DocumentMessageHandler;
|
||||||
use crate::frontend::utility_types::MouseCursorIcon;
|
use crate::frontend::utility_types::MouseCursorIcon;
|
||||||
|
|
@ -12,13 +12,13 @@ use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::viewport_tools::snapping::SnapHandler;
|
use crate::viewport_tools::snapping::SnapHandler;
|
||||||
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
|
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType};
|
||||||
|
|
||||||
use graphene::color::Color;
|
|
||||||
use graphene::document::Document;
|
use graphene::document::Document;
|
||||||
use graphene::intersection::Quad;
|
use graphene::intersection::Quad;
|
||||||
use graphene::layers::layer_info::LayerDataType;
|
use graphene::layers::layer_info::LayerDataType;
|
||||||
use graphene::layers::style::{self, Fill, Stroke};
|
|
||||||
use graphene::Operation;
|
use graphene::Operation;
|
||||||
|
|
||||||
|
use super::shared::transformation_cage::*;
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
|
|
@ -50,9 +50,10 @@ pub enum SelectMessage {
|
||||||
EditLayer,
|
EditLayer,
|
||||||
FlipHorizontal,
|
FlipHorizontal,
|
||||||
FlipVertical,
|
FlipVertical,
|
||||||
MouseMove {
|
PointerMove {
|
||||||
axis_align: Key,
|
axis_align: Key,
|
||||||
snap_angle: Key,
|
snap_angle: Key,
|
||||||
|
center: Key,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -253,11 +254,9 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
||||||
use SelectToolFsmState::*;
|
use SelectToolFsmState::*;
|
||||||
|
|
||||||
match self.fsm_state {
|
match self.fsm_state {
|
||||||
Ready => actions!(SelectMessageDiscriminant; DragStart, MouseMove, EditLayer),
|
Ready => actions!(SelectMessageDiscriminant; DragStart, PointerMove, EditLayer),
|
||||||
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, EditLayer),
|
Dragging => actions!(SelectMessageDiscriminant; DragStop, PointerMove, EditLayer),
|
||||||
DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditLayer),
|
_ => actions!(SelectMessageDiscriminant; DragStop, PointerMove, Abort, EditLayer),
|
||||||
ResizingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditLayer),
|
|
||||||
RotatingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditLayer),
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
@ -304,220 +303,6 @@ impl SelectToolData {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Handles the selected edges whilst dragging the layer bounds
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
struct SelectedEdges {
|
|
||||||
bounds: [DVec2; 2],
|
|
||||||
top: bool,
|
|
||||||
bottom: bool,
|
|
||||||
left: bool,
|
|
||||||
right: bool,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl SelectedEdges {
|
|
||||||
fn new(top: bool, bottom: bool, left: bool, right: bool, bounds: [DVec2; 2]) -> Self {
|
|
||||||
Self { top, bottom, left, right, bounds }
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculate the pivot for the operation (the opposite point to the edge dragged)
|
|
||||||
fn calculate_pivot(&self) -> DVec2 {
|
|
||||||
let min = self.bounds[0];
|
|
||||||
let max = self.bounds[1];
|
|
||||||
|
|
||||||
let x = if self.left {
|
|
||||||
max.x
|
|
||||||
} else if self.right {
|
|
||||||
min.x
|
|
||||||
} else {
|
|
||||||
(min.x + max.x) / 2.
|
|
||||||
};
|
|
||||||
|
|
||||||
let y = if self.top {
|
|
||||||
max.y
|
|
||||||
} else if self.bottom {
|
|
||||||
min.y
|
|
||||||
} else {
|
|
||||||
(min.y + max.y) / 2.
|
|
||||||
};
|
|
||||||
|
|
||||||
DVec2::new(x, y)
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Calculates the required scaling to resize the bounding box
|
|
||||||
fn pos_to_scale_transform(&self, mouse: DVec2) -> DAffine2 {
|
|
||||||
let mut min = self.bounds[0];
|
|
||||||
let mut max = self.bounds[1];
|
|
||||||
if self.top {
|
|
||||||
min.y = mouse.y;
|
|
||||||
} else if self.bottom {
|
|
||||||
max.y = mouse.y;
|
|
||||||
}
|
|
||||||
if self.left {
|
|
||||||
min.x = mouse.x
|
|
||||||
} else if self.right {
|
|
||||||
max.x = mouse.x;
|
|
||||||
}
|
|
||||||
DAffine2::from_scale((max - min) / (self.bounds[1] - self.bounds[0]))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
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)), None),
|
|
||||||
};
|
|
||||||
responses.push(DocumentMessage::Overlays(operation.into()).into());
|
|
||||||
|
|
||||||
path
|
|
||||||
}
|
|
||||||
|
|
||||||
fn evaluate_transform_handle_positions((left, top): (f64, f64), (right, bottom): (f64, f64)) -> [DVec2; 8] {
|
|
||||||
[
|
|
||||||
DVec2::new(left, top),
|
|
||||||
DVec2::new(left, (top + bottom) / 2.),
|
|
||||||
DVec2::new(left, bottom),
|
|
||||||
DVec2::new((left + right) / 2., top),
|
|
||||||
DVec2::new((left + right) / 2., bottom),
|
|
||||||
DVec2::new(right, top),
|
|
||||||
DVec2::new(right, (top + bottom) / 2.),
|
|
||||||
DVec2::new(right, bottom),
|
|
||||||
]
|
|
||||||
}
|
|
||||||
|
|
||||||
fn add_transform_handles(responses: &mut Vec<Message>) -> [Vec<LayerId>; 8] {
|
|
||||||
const EMPTY_VEC: Vec<LayerId> = Vec::new();
|
|
||||||
let mut transform_handle_paths = [EMPTY_VEC; 8];
|
|
||||||
|
|
||||||
for item in &mut transform_handle_paths {
|
|
||||||
let current_path = vec![generate_uuid()];
|
|
||||||
|
|
||||||
let operation = Operation::AddOverlayRect {
|
|
||||||
path: current_path.clone(),
|
|
||||||
transform: DAffine2::ZERO.to_cols_array(),
|
|
||||||
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
|
|
||||||
};
|
|
||||||
responses.push(DocumentMessage::Overlays(operation.into()).into());
|
|
||||||
|
|
||||||
*item = current_path;
|
|
||||||
}
|
|
||||||
|
|
||||||
transform_handle_paths
|
|
||||||
}
|
|
||||||
|
|
||||||
fn transform_from_box(pos1: DVec2, pos2: DVec2) -> [f64; 6] {
|
|
||||||
DAffine2::from_scale_angle_translation((pos2 - pos1).round(), 0., pos1.round() - DVec2::splat(0.5)).to_cols_array()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Contains info on the overlays for the bounding box and transform handles
|
|
||||||
#[derive(Clone, Debug, Default)]
|
|
||||||
struct BoundingBoxOverlays {
|
|
||||||
pub bounding_box: Vec<LayerId>,
|
|
||||||
pub transform_handles: [Vec<LayerId>; 8],
|
|
||||||
pub bounds: [DVec2; 2],
|
|
||||||
pub selected_edges: Option<SelectedEdges>,
|
|
||||||
pub original_transforms: OriginalTransforms,
|
|
||||||
pub pivot: DVec2,
|
|
||||||
}
|
|
||||||
|
|
||||||
impl BoundingBoxOverlays {
|
|
||||||
#[must_use]
|
|
||||||
pub fn new(buffer: &mut Vec<Message>) -> Self {
|
|
||||||
Self {
|
|
||||||
bounding_box: add_bounding_box(buffer),
|
|
||||||
transform_handles: add_transform_handles(buffer),
|
|
||||||
..Default::default()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Update the position of the bounding box and transform handles
|
|
||||||
pub fn transform(&mut self, buffer: &mut Vec<Message>) {
|
|
||||||
let transform = transform_from_box(self.bounds[0], self.bounds[1]);
|
|
||||||
let path = self.bounding_box.clone();
|
|
||||||
buffer.push(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
|
|
||||||
|
|
||||||
// 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;
|
|
||||||
|
|
||||||
for (position, path) in evaluate_transform_handle_positions(self.bounds[0].into(), self.bounds[1].into())
|
|
||||||
.into_iter()
|
|
||||||
.zip(&self.transform_handles)
|
|
||||||
{
|
|
||||||
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
|
|
||||||
let translation = (position - (scale / 2.) - 0.5 + BIAS).round();
|
|
||||||
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation).to_cols_array();
|
|
||||||
let path = path.clone();
|
|
||||||
buffer.push(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the user has selected the edge for dragging (returns which edge in order top, bottom, left, right)
|
|
||||||
pub fn check_selected_edges(&self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> {
|
|
||||||
let min = self.bounds[0].min(self.bounds[1]);
|
|
||||||
let max = self.bounds[0].max(self.bounds[1]);
|
|
||||||
if min.x - cursor.x < BOUNDS_SELECT_THRESHOLD && min.y - cursor.y < BOUNDS_SELECT_THRESHOLD && cursor.x - max.x < BOUNDS_SELECT_THRESHOLD && cursor.y - max.y < BOUNDS_SELECT_THRESHOLD {
|
|
||||||
let mut top = (cursor.y - min.y).abs() < BOUNDS_SELECT_THRESHOLD;
|
|
||||||
let mut bottom = (max.y - cursor.y).abs() < BOUNDS_SELECT_THRESHOLD;
|
|
||||||
let mut left = (cursor.x - min.x).abs() < BOUNDS_SELECT_THRESHOLD;
|
|
||||||
let mut right = (max.x - cursor.x).abs() < BOUNDS_SELECT_THRESHOLD;
|
|
||||||
if cursor.y - min.y + max.y - cursor.y < BOUNDS_SELECT_THRESHOLD * 2. && (left || right) {
|
|
||||||
top = false;
|
|
||||||
bottom = false;
|
|
||||||
}
|
|
||||||
if cursor.x - min.x + max.x - cursor.x < BOUNDS_SELECT_THRESHOLD * 2. && (top || bottom) {
|
|
||||||
left = false;
|
|
||||||
right = false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if top || bottom || left || right {
|
|
||||||
return Some((top, bottom, left, right));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
None
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Check if the user is rotating with the bounds
|
|
||||||
pub fn check_rotate(&self, cursor: DVec2) -> bool {
|
|
||||||
let min = self.bounds[0].min(self.bounds[1]);
|
|
||||||
let max = self.bounds[0].max(self.bounds[1]);
|
|
||||||
|
|
||||||
let outside_bounds = (min.x > cursor.x || cursor.x > max.x) || (min.y > cursor.y || cursor.y > max.y);
|
|
||||||
let inside_extended_bounds =
|
|
||||||
min.x - cursor.x < BOUNDS_ROTATE_THRESHOLD && min.y - cursor.y < BOUNDS_ROTATE_THRESHOLD && cursor.x - max.x < BOUNDS_ROTATE_THRESHOLD && cursor.y - max.y < BOUNDS_ROTATE_THRESHOLD;
|
|
||||||
|
|
||||||
outside_bounds & inside_extended_bounds
|
|
||||||
}
|
|
||||||
|
|
||||||
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler) -> MouseCursorIcon {
|
|
||||||
if let Some(directions) = self.check_selected_edges(input.mouse.position) {
|
|
||||||
match directions {
|
|
||||||
(true, false, false, false) | (false, true, false, false) => MouseCursorIcon::NSResize,
|
|
||||||
(false, false, true, false) | (false, false, false, true) => MouseCursorIcon::EWResize,
|
|
||||||
(true, false, true, false) | (false, true, false, true) => MouseCursorIcon::NWSEResize,
|
|
||||||
(true, false, false, true) | (false, true, true, false) => MouseCursorIcon::NESWResize,
|
|
||||||
_ => MouseCursorIcon::Default,
|
|
||||||
}
|
|
||||||
} else if self.check_rotate(input.mouse.position) {
|
|
||||||
MouseCursorIcon::Grabbing
|
|
||||||
} else {
|
|
||||||
MouseCursorIcon::Default
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Removes the overlays
|
|
||||||
pub fn delete(self, buffer: &mut impl Extend<Message>) {
|
|
||||||
buffer.extend([DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()).into()]);
|
|
||||||
buffer.extend(
|
|
||||||
self.transform_handles
|
|
||||||
.iter()
|
|
||||||
.map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
impl Fsm for SelectToolFsmState {
|
impl Fsm for SelectToolFsmState {
|
||||||
type ToolData = SelectToolData;
|
type ToolData = SelectToolData;
|
||||||
type ToolOptions = ();
|
type ToolOptions = ();
|
||||||
|
|
@ -545,6 +330,8 @@ impl Fsm for SelectToolFsmState {
|
||||||
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(&mut buffer));
|
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(&mut buffer));
|
||||||
|
|
||||||
bounding_box_overlays.bounds = bounds;
|
bounding_box_overlays.bounds = bounds;
|
||||||
|
bounding_box_overlays.transform = DAffine2::IDENTITY;
|
||||||
|
|
||||||
bounding_box_overlays.transform(&mut buffer);
|
bounding_box_overlays.transform(&mut buffer);
|
||||||
|
|
||||||
data.bounding_box_overlays = Some(bounding_box_overlays);
|
data.bounding_box_overlays = Some(bounding_box_overlays);
|
||||||
|
|
@ -611,8 +398,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
let snap_x = selected_edges.2 || selected_edges.3;
|
let snap_x = selected_edges.2 || selected_edges.3;
|
||||||
let snap_y = selected_edges.0 || selected_edges.1;
|
let snap_y = selected_edges.0 || selected_edges.1;
|
||||||
|
|
||||||
data.snap_handler
|
data.snap_handler.start_snap(document, document.bounding_boxes(Some(&selected), None), snap_x, snap_y);
|
||||||
.start_snap(document, document.visible_layers().filter(|layer| !selected.iter().any(|path| path == layer)), snap_x, snap_y);
|
|
||||||
|
|
||||||
data.layers_dragging = selected;
|
data.layers_dragging = selected;
|
||||||
|
|
||||||
|
|
@ -632,8 +418,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
buffer.push(DocumentMessage::StartTransaction.into());
|
buffer.push(DocumentMessage::StartTransaction.into());
|
||||||
data.layers_dragging = selected;
|
data.layers_dragging = selected;
|
||||||
|
|
||||||
data.snap_handler
|
data.snap_handler.start_snap(document, document.bounding_boxes(Some(&data.layers_dragging), None), true, true);
|
||||||
.start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true);
|
|
||||||
|
|
||||||
Dragging
|
Dragging
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -647,8 +432,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
buffer.push(DocumentMessage::AddSelectedLayers { additional_layers: selected.clone() }.into());
|
buffer.push(DocumentMessage::AddSelectedLayers { additional_layers: selected.clone() }.into());
|
||||||
buffer.push(DocumentMessage::StartTransaction.into());
|
buffer.push(DocumentMessage::StartTransaction.into());
|
||||||
data.layers_dragging.append(&mut selected);
|
data.layers_dragging.append(&mut selected);
|
||||||
data.snap_handler
|
data.snap_handler.start_snap(document, document.bounding_boxes(Some(&data.layers_dragging), None), true, true);
|
||||||
.start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true);
|
|
||||||
|
|
||||||
Dragging
|
Dragging
|
||||||
} else {
|
} else {
|
||||||
|
|
@ -660,23 +444,23 @@ impl Fsm for SelectToolFsmState {
|
||||||
|
|
||||||
state
|
state
|
||||||
}
|
}
|
||||||
(Dragging, MouseMove { axis_align, .. }) => {
|
(Dragging, PointerMove { axis_align, .. }) => {
|
||||||
// TODO: This is a cheat. Break out the relevant functionality from the handler above and call it from there and here.
|
// 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());
|
responses.push_front(SelectMessage::DocumentIsDirty.into());
|
||||||
|
|
||||||
let mouse_position = if input.keyboard.get(axis_align as usize) {
|
let mouse_position = axis_align_drag(input.keyboard.get(axis_align as usize), input.mouse.position, data.drag_start);
|
||||||
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 mouse_delta = mouse_position - data.drag_current;
|
||||||
|
|
||||||
let closest_move = data.snap_handler.snap_layers(responses, document, &data.layers_dragging, input.viewport_bounds.size(), mouse_delta);
|
let snap = data
|
||||||
|
.layers_dragging
|
||||||
|
.iter()
|
||||||
|
.filter_map(|path| document.graphene_document.viewport_bounding_box(path).ok()?)
|
||||||
|
.flat_map(|[bound1, bound2]| [bound1, bound2, (bound1 + bound2) / 2.])
|
||||||
|
.map(|vec| vec.into())
|
||||||
|
.unzip();
|
||||||
|
|
||||||
|
let closest_move = data.snap_handler.snap_layers(responses, document, snap, input.viewport_bounds.size(), mouse_delta);
|
||||||
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
|
// TODO: Cache the result of `shallowest_unique_layers` to avoid this heavy computation every frame of movement, see https://github.com/GraphiteEditor/Graphite/pull/481
|
||||||
for path in Document::shallowest_unique_layers(data.layers_dragging.iter()) {
|
for path in Document::shallowest_unique_layers(data.layers_dragging.iter()) {
|
||||||
responses.push_front(
|
responses.push_front(
|
||||||
|
|
@ -690,14 +474,17 @@ impl Fsm for SelectToolFsmState {
|
||||||
data.drag_current = mouse_position + closest_move;
|
data.drag_current = mouse_position + closest_move;
|
||||||
Dragging
|
Dragging
|
||||||
}
|
}
|
||||||
(ResizingBounds, MouseMove { .. }) => {
|
(ResizingBounds, PointerMove { axis_align, center, .. }) => {
|
||||||
if let Some(bounds) = &mut data.bounding_box_overlays {
|
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||||
if let Some(movement) = &mut bounds.selected_edges {
|
if let Some(movement) = &mut bounds.selected_edges {
|
||||||
|
let (center, axis_align) = (input.keyboard.get(center as usize), input.keyboard.get(axis_align as usize));
|
||||||
|
|
||||||
let mouse_position = input.mouse.position;
|
let mouse_position = input.mouse.position;
|
||||||
|
|
||||||
let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position);
|
let snapped_mouse_position = data.snap_handler.snap_position(responses, input.viewport_bounds.size(), document, mouse_position);
|
||||||
|
|
||||||
let delta = movement.pos_to_scale_transform(snapped_mouse_position);
|
let [_position, size] = movement.new_size(snapped_mouse_position, bounds.transform, center, axis_align);
|
||||||
|
let delta = movement.bounds_to_scale_transform(center, size);
|
||||||
|
|
||||||
let selected = data.layers_dragging.iter().collect::<Vec<_>>();
|
let selected = data.layers_dragging.iter().collect::<Vec<_>>();
|
||||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
||||||
|
|
@ -707,7 +494,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
}
|
}
|
||||||
ResizingBounds
|
ResizingBounds
|
||||||
}
|
}
|
||||||
(RotatingBounds, MouseMove { snap_angle, .. }) => {
|
(RotatingBounds, PointerMove { snap_angle, .. }) => {
|
||||||
if let Some(bounds) = &mut data.bounding_box_overlays {
|
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||||
let angle = {
|
let angle = {
|
||||||
let start_offset = data.drag_start - bounds.pivot;
|
let start_offset = data.drag_start - bounds.pivot;
|
||||||
|
|
@ -733,14 +520,14 @@ impl Fsm for SelectToolFsmState {
|
||||||
|
|
||||||
RotatingBounds
|
RotatingBounds
|
||||||
}
|
}
|
||||||
(DrawingBox, MouseMove { .. }) => {
|
(DrawingBox, PointerMove { .. }) => {
|
||||||
data.drag_current = input.mouse.position;
|
data.drag_current = input.mouse.position;
|
||||||
|
|
||||||
responses.push_front(
|
responses.push_front(
|
||||||
DocumentMessage::Overlays(
|
DocumentMessage::Overlays(
|
||||||
Operation::SetLayerTransformInViewport {
|
Operation::SetLayerTransformInViewport {
|
||||||
path: data.drag_box_overlay_layer.clone().unwrap(),
|
path: data.drag_box_overlay_layer.clone().unwrap(),
|
||||||
transform: transform_from_box(data.drag_start, data.drag_current),
|
transform: transform_from_box(data.drag_start, data.drag_current, DAffine2::IDENTITY).to_cols_array(),
|
||||||
}
|
}
|
||||||
.into(),
|
.into(),
|
||||||
)
|
)
|
||||||
|
|
@ -748,8 +535,8 @@ impl Fsm for SelectToolFsmState {
|
||||||
);
|
);
|
||||||
DrawingBox
|
DrawingBox
|
||||||
}
|
}
|
||||||
(Ready, MouseMove { .. }) => {
|
(Ready, PointerMove { .. }) => {
|
||||||
let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input));
|
let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
||||||
|
|
||||||
if data.cursor != cursor {
|
if data.cursor != cursor {
|
||||||
data.cursor = cursor;
|
data.cursor = cursor;
|
||||||
|
|
|
||||||
|
|
@ -1 +1,2 @@
|
||||||
pub mod resize;
|
pub mod resize;
|
||||||
|
pub mod transformation_cage;
|
||||||
|
|
|
||||||
|
|
@ -19,7 +19,7 @@ pub struct Resize {
|
||||||
impl Resize {
|
impl Resize {
|
||||||
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
/// Starts a resize, assigning the snap targets and snapping the starting position.
|
||||||
pub fn start(&mut self, responses: &mut VecDeque<Message>, viewport_bounds: DVec2, document: &DocumentMessageHandler, mouse_position: DVec2) {
|
pub fn start(&mut self, responses: &mut VecDeque<Message>, viewport_bounds: DVec2, document: &DocumentMessageHandler, mouse_position: DVec2) {
|
||||||
self.snap_handler.start_snap(document, document.visible_layers(), true, true);
|
self.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true);
|
||||||
self.drag_start = self.snap_handler.snap_position(responses, viewport_bounds, document, mouse_position);
|
self.drag_start = self.snap_handler.snap_position(responses, viewport_bounds, document, mouse_position);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,307 @@
|
||||||
|
use crate::consts::{BOUNDS_ROTATE_THRESHOLD, BOUNDS_SELECT_THRESHOLD, COLOR_ACCENT, SELECTION_DRAG_ANGLE, VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE};
|
||||||
|
use crate::document::transformation::OriginalTransforms;
|
||||||
|
use crate::frontend::utility_types::MouseCursorIcon;
|
||||||
|
use crate::input::InputPreprocessorMessageHandler;
|
||||||
|
use crate::message_prelude::*;
|
||||||
|
|
||||||
|
use graphene::color::Color;
|
||||||
|
use graphene::layers::style::{self, Fill, Stroke};
|
||||||
|
use graphene::Operation;
|
||||||
|
|
||||||
|
use glam::{DAffine2, DVec2, Vec2Swizzles};
|
||||||
|
|
||||||
|
/// Contains the edges that are being dragged along with the origional bounds
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct SelectedEdges {
|
||||||
|
bounds: [DVec2; 2],
|
||||||
|
top: bool,
|
||||||
|
bottom: bool,
|
||||||
|
left: bool,
|
||||||
|
right: bool,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SelectedEdges {
|
||||||
|
pub fn new(top: bool, bottom: bool, left: bool, right: bool, bounds: [DVec2; 2]) -> Self {
|
||||||
|
Self { top, bottom, left, right, bounds }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculate the pivot for the operation (the opposite point to the edge dragged)
|
||||||
|
pub fn calculate_pivot(&self) -> DVec2 {
|
||||||
|
let min = self.bounds[0];
|
||||||
|
let max = self.bounds[1];
|
||||||
|
|
||||||
|
let x = if self.left {
|
||||||
|
max.x
|
||||||
|
} else if self.right {
|
||||||
|
min.x
|
||||||
|
} else {
|
||||||
|
(min.x + max.x) / 2.
|
||||||
|
};
|
||||||
|
|
||||||
|
let y = if self.top {
|
||||||
|
max.y
|
||||||
|
} else if self.bottom {
|
||||||
|
min.y
|
||||||
|
} else {
|
||||||
|
(min.y + max.y) / 2.
|
||||||
|
};
|
||||||
|
|
||||||
|
DVec2::new(x, y)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Computes the new bounds with the given mouse move and modifier keys
|
||||||
|
pub fn new_size(&self, mouse: DVec2, transform: DAffine2, center: bool, constrain: bool) -> [DVec2; 2] {
|
||||||
|
let mouse = transform.inverse().transform_point2(mouse);
|
||||||
|
|
||||||
|
let mut min = self.bounds[0];
|
||||||
|
let mut max = self.bounds[1];
|
||||||
|
if self.top {
|
||||||
|
min.y = mouse.y;
|
||||||
|
} else if self.bottom {
|
||||||
|
max.y = mouse.y;
|
||||||
|
}
|
||||||
|
if self.left {
|
||||||
|
min.x = mouse.x
|
||||||
|
} else if self.right {
|
||||||
|
max.x = mouse.x;
|
||||||
|
}
|
||||||
|
|
||||||
|
let mut size = max - min;
|
||||||
|
if constrain && ((self.top || self.bottom) && (self.left || self.right)) {
|
||||||
|
size = size.abs().max(size.abs().yx()) * size.signum();
|
||||||
|
}
|
||||||
|
if center {
|
||||||
|
if self.left || self.right {
|
||||||
|
size.x *= 2.;
|
||||||
|
}
|
||||||
|
|
||||||
|
if self.bottom || self.top {
|
||||||
|
size.y *= 2.;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[min, size]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Offsets the transformation pivot in order to scale from the center
|
||||||
|
fn offset_pivot(&self, center: bool, size: DVec2) -> DVec2 {
|
||||||
|
let mut offset = DVec2::ZERO;
|
||||||
|
|
||||||
|
if center && self.right {
|
||||||
|
offset.x -= size.x / 2.;
|
||||||
|
}
|
||||||
|
if center && self.left {
|
||||||
|
offset.x += size.x / 2.;
|
||||||
|
}
|
||||||
|
if center && self.bottom {
|
||||||
|
offset.y -= size.y / 2.;
|
||||||
|
}
|
||||||
|
if center && self.top {
|
||||||
|
offset.y += size.y / 2.;
|
||||||
|
}
|
||||||
|
offset
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Moves the position to account for centring (only necessary with absolute transforms - e.g. with artboards)
|
||||||
|
pub fn center_position(&self, mut position: DVec2, size: DVec2, center: bool) -> DVec2 {
|
||||||
|
if center && self.right {
|
||||||
|
position.x -= size.x / 2.;
|
||||||
|
}
|
||||||
|
if center && self.bottom {
|
||||||
|
position.y -= size.y / 2.;
|
||||||
|
}
|
||||||
|
|
||||||
|
position
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculates the required scaling to resize the bounding box
|
||||||
|
pub fn bounds_to_scale_transform(&self, center: bool, size: DVec2) -> DAffine2 {
|
||||||
|
DAffine2::from_translation(self.offset_pivot(center, size)) * DAffine2::from_scale(size / (self.bounds[1] - self.bounds[0]))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Create a viewport relative bounding box overlay with no transform handles
|
||||||
|
pub 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)), None),
|
||||||
|
};
|
||||||
|
responses.push(DocumentMessage::Overlays(operation.into()).into());
|
||||||
|
|
||||||
|
path
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Add the transform handle overlay
|
||||||
|
fn add_transform_handles(responses: &mut Vec<Message>) -> [Vec<LayerId>; 8] {
|
||||||
|
const EMPTY_VEC: Vec<LayerId> = Vec::new();
|
||||||
|
let mut transform_handle_paths = [EMPTY_VEC; 8];
|
||||||
|
|
||||||
|
for item in &mut transform_handle_paths {
|
||||||
|
let current_path = vec![generate_uuid()];
|
||||||
|
|
||||||
|
let operation = Operation::AddOverlayRect {
|
||||||
|
path: current_path.clone(),
|
||||||
|
transform: DAffine2::ZERO.to_cols_array(),
|
||||||
|
style: style::PathStyle::new(Some(Stroke::new(COLOR_ACCENT, 2.0)), Some(Fill::new(Color::WHITE))),
|
||||||
|
};
|
||||||
|
responses.push(DocumentMessage::Overlays(operation.into()).into());
|
||||||
|
|
||||||
|
*item = current_path;
|
||||||
|
}
|
||||||
|
|
||||||
|
transform_handle_paths
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Converts a bounding box to a rounded transform (with translation and scale)
|
||||||
|
pub fn transform_from_box(pos1: DVec2, pos2: DVec2, transform: DAffine2) -> DAffine2 {
|
||||||
|
let inverse = transform.inverse();
|
||||||
|
transform
|
||||||
|
* DAffine2::from_scale_angle_translation(
|
||||||
|
inverse.transform_vector2(transform.transform_vector2(pos2 - pos1).round()),
|
||||||
|
0.,
|
||||||
|
inverse.transform_point2(transform.transform_point2(pos1).round() - DVec2::splat(0.5)),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Aligns the mouse position to the closest axis
|
||||||
|
pub fn axis_align_drag(axis_align: bool, position: DVec2, start: DVec2) -> DVec2 {
|
||||||
|
if axis_align {
|
||||||
|
let mouse_position = position - 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() + start
|
||||||
|
} else {
|
||||||
|
position
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Contains info on the overlays for the bounding box and transform handles
|
||||||
|
#[derive(Clone, Debug, Default)]
|
||||||
|
pub struct BoundingBoxOverlays {
|
||||||
|
pub bounding_box: Vec<LayerId>,
|
||||||
|
pub transform_handles: [Vec<LayerId>; 8],
|
||||||
|
pub bounds: [DVec2; 2],
|
||||||
|
pub transform: DAffine2,
|
||||||
|
pub selected_edges: Option<SelectedEdges>,
|
||||||
|
pub original_transforms: OriginalTransforms,
|
||||||
|
pub pivot: DVec2,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl BoundingBoxOverlays {
|
||||||
|
#[must_use]
|
||||||
|
pub fn new(buffer: &mut Vec<Message>) -> Self {
|
||||||
|
Self {
|
||||||
|
bounding_box: add_bounding_box(buffer),
|
||||||
|
transform_handles: add_transform_handles(buffer),
|
||||||
|
..Default::default()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Calculats the transformed handle positions based on the bounding box and the transform
|
||||||
|
pub fn evaluate_transform_handle_positions(&self) -> [DVec2; 8] {
|
||||||
|
let (left, top): (f64, f64) = self.bounds[0].into();
|
||||||
|
let (right, bottom): (f64, f64) = self.bounds[1].into();
|
||||||
|
[
|
||||||
|
self.transform.transform_point2(DVec2::new(left, top)),
|
||||||
|
self.transform.transform_point2(DVec2::new(left, (top + bottom) / 2.)),
|
||||||
|
self.transform.transform_point2(DVec2::new(left, bottom)),
|
||||||
|
self.transform.transform_point2(DVec2::new((left + right) / 2., top)),
|
||||||
|
self.transform.transform_point2(DVec2::new((left + right) / 2., bottom)),
|
||||||
|
self.transform.transform_point2(DVec2::new(right, top)),
|
||||||
|
self.transform.transform_point2(DVec2::new(right, (top + bottom) / 2.)),
|
||||||
|
self.transform.transform_point2(DVec2::new(right, bottom)),
|
||||||
|
]
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Update the position of the bounding box and transform handles
|
||||||
|
pub fn transform(&mut self, buffer: &mut Vec<Message>) {
|
||||||
|
let transform = transform_from_box(self.bounds[0], self.bounds[1], self.transform).to_cols_array();
|
||||||
|
let path = self.bounding_box.clone();
|
||||||
|
buffer.push(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
|
||||||
|
for (position, path) in self.evaluate_transform_handle_positions().into_iter().zip(&self.transform_handles) {
|
||||||
|
let scale = DVec2::splat(VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE);
|
||||||
|
let translation = (position - (scale / 2.) - 0.5 + BIAS).round();
|
||||||
|
let transform = DAffine2::from_scale_angle_translation(scale, 0., translation).to_cols_array();
|
||||||
|
let path = path.clone();
|
||||||
|
buffer.push(DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the user has selected the edge for dragging (returns which edge in order top, bottom, left, right)
|
||||||
|
pub fn check_selected_edges(&self, cursor: DVec2) -> Option<(bool, bool, bool, bool)> {
|
||||||
|
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||||
|
let select_threshold = self.transform.inverse().transform_vector2(DVec2::new(0., BOUNDS_SELECT_THRESHOLD)).length();
|
||||||
|
|
||||||
|
let min = self.bounds[0].min(self.bounds[1]);
|
||||||
|
let max = self.bounds[0].max(self.bounds[1]);
|
||||||
|
if min.x - cursor.x < select_threshold && min.y - cursor.y < select_threshold && cursor.x - max.x < select_threshold && cursor.y - max.y < select_threshold {
|
||||||
|
let mut top = (cursor.y - min.y).abs() < select_threshold;
|
||||||
|
let mut bottom = (max.y - cursor.y).abs() < select_threshold;
|
||||||
|
let mut left = (cursor.x - min.x).abs() < select_threshold;
|
||||||
|
let mut right = (max.x - cursor.x).abs() < select_threshold;
|
||||||
|
if cursor.y - min.y + max.y - cursor.y < select_threshold * 2. && (left || right) {
|
||||||
|
top = false;
|
||||||
|
bottom = false;
|
||||||
|
}
|
||||||
|
if cursor.x - min.x + max.x - cursor.x < select_threshold * 2. && (top || bottom) {
|
||||||
|
left = false;
|
||||||
|
right = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if top || bottom || left || right {
|
||||||
|
return Some((top, bottom, left, right));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
None
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Check if the user is rotating with the bounds
|
||||||
|
pub fn check_rotate(&self, cursor: DVec2) -> bool {
|
||||||
|
let cursor = self.transform.inverse().transform_point2(cursor);
|
||||||
|
let rotate_threshold = self.transform.inverse().transform_vector2(DVec2::new(0., BOUNDS_ROTATE_THRESHOLD)).length();
|
||||||
|
|
||||||
|
let min = self.bounds[0].min(self.bounds[1]);
|
||||||
|
let max = self.bounds[0].max(self.bounds[1]);
|
||||||
|
|
||||||
|
let outside_bounds = (min.x > cursor.x || cursor.x > max.x) || (min.y > cursor.y || cursor.y > max.y);
|
||||||
|
let inside_extended_bounds = min.x - cursor.x < rotate_threshold && min.y - cursor.y < rotate_threshold && cursor.x - max.x < rotate_threshold && cursor.y - max.y < rotate_threshold;
|
||||||
|
|
||||||
|
outside_bounds & inside_extended_bounds
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Gets the required mouse cursor to show resizing bounds or optionally rotation
|
||||||
|
pub fn get_cursor(&self, input: &InputPreprocessorMessageHandler, rotate: bool) -> MouseCursorIcon {
|
||||||
|
if let Some(directions) = self.check_selected_edges(input.mouse.position) {
|
||||||
|
match directions {
|
||||||
|
(true, false, false, false) | (false, true, false, false) => MouseCursorIcon::NSResize,
|
||||||
|
(false, false, true, false) | (false, false, false, true) => MouseCursorIcon::EWResize,
|
||||||
|
(true, false, true, false) | (false, true, false, true) => MouseCursorIcon::NWSEResize,
|
||||||
|
(true, false, false, true) | (false, true, true, false) => MouseCursorIcon::NESWResize,
|
||||||
|
_ => MouseCursorIcon::Default,
|
||||||
|
}
|
||||||
|
} else if rotate && self.check_rotate(input.mouse.position) {
|
||||||
|
MouseCursorIcon::Grabbing
|
||||||
|
} else {
|
||||||
|
MouseCursorIcon::Default
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Removes the overlays
|
||||||
|
pub fn delete(self, buffer: &mut impl Extend<Message>) {
|
||||||
|
buffer.extend([DocumentMessage::Overlays(Operation::DeleteLayer { path: self.bounding_box }.into()).into()]);
|
||||||
|
buffer.extend(
|
||||||
|
self.transform_handles
|
||||||
|
.iter()
|
||||||
|
.map(|path| DocumentMessage::Overlays(Operation::DeleteLayer { path: path.clone() }.into()).into()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -9,7 +9,6 @@ use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup};
|
||||||
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
|
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use graphene::color::Color;
|
|
||||||
use graphene::intersection::Quad;
|
use graphene::intersection::Quad;
|
||||||
use graphene::layers::style::{self, Fill, Stroke};
|
use graphene::layers::style::{self, Fill, Stroke};
|
||||||
use graphene::Operation;
|
use graphene::Operation;
|
||||||
|
|
|
||||||
|
|
@ -17,7 +17,7 @@
|
||||||
<LayoutCol class="shelf">
|
<LayoutCol class="shelf">
|
||||||
<LayoutCol class="tools" :scrollableY="true">
|
<LayoutCol class="tools" :scrollableY="true">
|
||||||
<ShelfItemInput icon="LayoutSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" />
|
<ShelfItemInput icon="LayoutSelectTool" title="Select Tool (V)" :active="activeTool === 'Select'" :action="() => selectTool('Select')" />
|
||||||
<ShelfItemInput icon="LayoutCropTool" title="Crop Tool" :active="activeTool === 'Crop'" :action="() => (dialog.comingSoon(289), false) && selectTool('Crop')" />
|
<ShelfItemInput icon="LayoutCropTool" title="Crop Tool" :active="activeTool === 'Crop'" :action="() => selectTool('Crop')" />
|
||||||
<ShelfItemInput icon="LayoutNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => selectTool('Navigate')" />
|
<ShelfItemInput icon="LayoutNavigateTool" title="Navigate Tool (Z)" :active="activeTool === 'Navigate'" :action="() => selectTool('Navigate')" />
|
||||||
<ShelfItemInput icon="LayoutEyedropperTool" title="Eyedropper Tool (I)" :active="activeTool === 'Eyedropper'" :action="() => selectTool('Eyedropper')" />
|
<ShelfItemInput icon="LayoutEyedropperTool" title="Eyedropper Tool (I)" :active="activeTool === 'Eyedropper'" :action="() => selectTool('Eyedropper')" />
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -250,7 +250,7 @@ impl JsEditorHandle {
|
||||||
|
|
||||||
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
||||||
|
|
||||||
let message = InputPreprocessorMessage::MouseMove { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerMove { editor_mouse_state, modifier_keys };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -271,7 +271,7 @@ impl JsEditorHandle {
|
||||||
|
|
||||||
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
||||||
|
|
||||||
let message = InputPreprocessorMessage::MouseDown { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerDown { editor_mouse_state, modifier_keys };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -281,7 +281,7 @@ impl JsEditorHandle {
|
||||||
|
|
||||||
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
let modifier_keys = ModifierKeys::from_bits(modifiers).expect("Invalid modifier keys");
|
||||||
|
|
||||||
let message = InputPreprocessorMessage::MouseUp { editor_mouse_state, modifier_keys };
|
let message = InputPreprocessorMessage::PointerUp { editor_mouse_state, modifier_keys };
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -515,8 +515,12 @@ impl JsEditorHandle {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Creates an artboard at a specified point with a width and height
|
/// Creates an artboard at a specified point with a width and height
|
||||||
pub fn create_artboard_and_fit_to_viewport(&self, top: f64, left: f64, height: f64, width: f64) {
|
pub fn create_artboard_and_fit_to_viewport(&self, pos_x: f64, pos_y: f64, width: f64, height: f64) {
|
||||||
let message = ArtboardMessage::AddArtboard { top, left, height, width };
|
let message = ArtboardMessage::AddArtboard {
|
||||||
|
id: None,
|
||||||
|
position: (pos_x, pos_y),
|
||||||
|
size: (width, height),
|
||||||
|
};
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
let message = DocumentMessage::ZoomCanvasToFitAll;
|
let message = DocumentMessage::ZoomCanvasToFitAll;
|
||||||
self.dispatch(message);
|
self.dispatch(message);
|
||||||
|
|
|
||||||
|
|
@ -299,6 +299,12 @@ impl Document {
|
||||||
Ok(layer.data.bounding_box(transform))
|
Ok(layer.data.bounding_box(transform))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub fn bounding_box_and_transform(&self, path: &[LayerId]) -> Result<Option<([DVec2; 2], DAffine2)>, DocumentError> {
|
||||||
|
let layer = self.layer(path)?;
|
||||||
|
let transform = self.multiply_transforms(&path[..path.len() - 1])?;
|
||||||
|
Ok(layer.data.bounding_box(layer.transform).map(|bounds| (bounds, transform)))
|
||||||
|
}
|
||||||
|
|
||||||
pub fn visible_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
|
pub fn visible_layers_bounding_box(&self) -> Option<[DVec2; 2]> {
|
||||||
let mut paths = vec![];
|
let mut paths = vec![];
|
||||||
self.visible_layers(&mut vec![], &mut paths).ok()?;
|
self.visible_layers(&mut vec![], &mut paths).ok()?;
|
||||||
|
|
|
||||||
|
|
@ -240,23 +240,23 @@ pub fn derive_hint(input_item: TokenStream) -> TokenStream {
|
||||||
/// # Example
|
/// # Example
|
||||||
/// ```ignore
|
/// ```ignore
|
||||||
/// match (example_tool_state, event) {
|
/// match (example_tool_state, event) {
|
||||||
/// (ToolState::Ready, Event::MouseDown(mouse_state)) if *mouse_state == MouseState::Left => {
|
/// (ToolState::Ready, Event::PointerDown(mouse_state)) if *mouse_state == MouseState::Left => {
|
||||||
/// #[edge("LMB Down")]
|
/// #[edge("LMB Down")]
|
||||||
/// ToolState::Pending
|
/// ToolState::Pending
|
||||||
/// }
|
/// }
|
||||||
/// (SelectToolState::Pending, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
|
/// (SelectToolState::Pending, Event::PointerUp(mouse_state)) if *mouse_state == MouseState::Left => {
|
||||||
/// #[edge("LMB Up: Select Object")]
|
/// #[edge("LMB Up: Select Object")]
|
||||||
/// SelectToolState::Ready
|
/// SelectToolState::Ready
|
||||||
/// }
|
/// }
|
||||||
/// (SelectToolState::Pending, Event::MouseMove(x,y)) => {
|
/// (SelectToolState::Pending, Event::PointerMove(x,y)) => {
|
||||||
/// #[edge("Mouse Move")]
|
/// #[edge("Mouse Move")]
|
||||||
/// SelectToolState::TransformSelected
|
/// SelectToolState::TransformSelected
|
||||||
/// }
|
/// }
|
||||||
/// (SelectToolState::TransformSelected, Event::MouseMove(x,y)) => {
|
/// (SelectToolState::TransformSelected, Event::PointerMove(x,y)) => {
|
||||||
/// #[edge("Mouse Move")]
|
/// #[edge("Mouse Move")]
|
||||||
/// SelectToolState::TransformSelected
|
/// SelectToolState::TransformSelected
|
||||||
/// }
|
/// }
|
||||||
/// (SelectToolState::TransformSelected, Event::MouseUp(mouse_state)) if *mouse_state == MouseState::Left => {
|
/// (SelectToolState::TransformSelected, Event::PointerUp(mouse_state)) if *mouse_state == MouseState::Left => {
|
||||||
/// #[edge("LMB Up")]
|
/// #[edge("LMB Up")]
|
||||||
/// SelectToolState::Ready
|
/// SelectToolState::Ready
|
||||||
/// }
|
/// }
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue