From 45edeb2a2b956a9f67224e07ca0c4a4cdc1cc4b5 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Wed, 9 Feb 2022 23:22:23 +0000 Subject: [PATCH] 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 Co-authored-by: Keavon Chambers --- editor/src/communication/dispatcher.rs | 2 +- editor/src/document/artboard_message.rs | 12 +- .../src/document/artboard_message_handler.rs | 19 +- .../src/document/document_message_handler.rs | 20 +- editor/src/document/movement_message.rs | 2 +- .../src/document/movement_message_handler.rs | 4 +- .../src/document/transform_layer_message.rs | 2 +- .../transform_layer_message_handler.rs | 4 +- editor/src/input/input_mapper.rs | 12 +- editor/src/input/input_preprocessor.rs | 6 +- .../src/input/input_preprocessor_message.rs | 6 +- .../input_preprocessor_message_handler.rs | 38 +- editor/src/misc/test_utils.rs | 6 +- editor/src/viewport_tools/snapping.rs | 17 +- editor/src/viewport_tools/tool.rs | 4 +- editor/src/viewport_tools/tools/crop.rs | 361 +++++++++++++++++- editor/src/viewport_tools/tools/line.rs | 2 +- editor/src/viewport_tools/tools/navigate.rs | 10 +- editor/src/viewport_tools/tools/path.rs | 2 +- editor/src/viewport_tools/tools/pen.rs | 2 +- editor/src/viewport_tools/tools/select.rs | 283 ++------------ editor/src/viewport_tools/tools/shared/mod.rs | 1 + .../src/viewport_tools/tools/shared/resize.rs | 2 +- .../tools/shared/transformation_cage.rs | 307 +++++++++++++++ editor/src/viewport_tools/tools/text.rs | 1 - frontend/src/components/panels/Document.vue | 2 +- frontend/wasm/src/api.rs | 14 +- graphene/src/document.rs | 6 + proc-macros/src/lib.rs | 10 +- 29 files changed, 817 insertions(+), 340 deletions(-) create mode 100644 editor/src/viewport_tools/tools/shared/transformation_cage.rs diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index 6050625f..2d776124 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -117,7 +117,7 @@ impl Dispatcher { && !(matches!( message, 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!("Hints: {:?}", self.input_mapper_message_handler.hints(self.collect_actions())); diff --git a/editor/src/document/artboard_message.rs b/editor/src/document/artboard_message.rs index 5f33e308..d816ebde 100644 --- a/editor/src/document/artboard_message.rs +++ b/editor/src/document/artboard_message.rs @@ -14,12 +14,16 @@ pub enum ArtboardMessage { // Messages AddArtboard { - top: f64, - left: f64, - height: f64, - width: f64, + id: Option, + position: (f64, f64), + size: (f64, f64), }, RenderArtboards, + ResizeArtboard { + artboard: Vec, + position: (f64, f64), + size: (f64, f64), + }, } impl From for ArtboardMessage { diff --git a/editor/src/document/artboard_message_handler.rs b/editor/src/document/artboard_message_handler.rs index f8c2ca0d..3fd2794a 100644 --- a/editor/src/document/artboard_message_handler.rs +++ b/editor/src/document/artboard_message_handler.rs @@ -5,7 +5,7 @@ use graphene::document::Document as GrapheneDocument; use graphene::layers::style::{self, Fill, ViewMode}; use graphene::Operation as DocumentOperation; -use glam::{DAffine2, DVec2}; +use glam::DAffine2; use serde::{Deserialize, Serialize}; use std::collections::VecDeque; @@ -36,8 +36,8 @@ impl MessageHandler for ArtboardMessageHandler { }, // Messages - AddArtboard { top, left, height, width } => { - let artboard_id = generate_uuid(); + AddArtboard { id, position, size } => { + let artboard_id = id.unwrap_or_else(generate_uuid); self.artboard_ids.push(artboard_id); responses.push_back( @@ -45,7 +45,7 @@ impl MessageHandler for ArtboardMessageHandler { DocumentOperation::AddRect { path: vec![artboard_id], 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))), } .into(), @@ -73,6 +73,17 @@ impl MessageHandler 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()); + } } } diff --git a/editor/src/document/document_message_handler.rs b/editor/src/document/document_message_handler.rs index e00d1877..510c7a60 100644 --- a/editor/src/document/document_message_handler.rs +++ b/editor/src/document/document_message_handler.rs @@ -42,7 +42,7 @@ pub struct DocumentMessageHandler { movement_handler: MovementMessageHandler, #[serde(skip)] overlays_message_handler: OverlaysMessageHandler, - artboard_message_handler: ArtboardMessageHandler, + pub artboard_message_handler: ArtboardMessageHandler, #[serde(skip)] transform_layer_handler: TransformLayerMessageHandler, pub overlays_visible: bool, @@ -138,6 +138,10 @@ impl DocumentMessageHandler { 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 pub fn selected_visible_layers_vector_shapes(&self, responses: &mut VecDeque) -> Vec { 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>>, ignore_artboard: Option) -> impl Iterator + '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, data: &mut Vec, path: &mut Vec) { let mut space = 0; for (id, layer) in folder.layer_ids.iter().zip(folder.layers()).rev() { diff --git a/editor/src/document/movement_message.rs b/editor/src/document/movement_message.rs index 6cbf792c..8c49c335 100644 --- a/editor/src/document/movement_message.rs +++ b/editor/src/document/movement_message.rs @@ -19,7 +19,7 @@ pub enum MovementMessage { IncreaseCanvasZoom { center_on_mouse: bool, }, - MouseMove { + PointerMove { snap_angle: Key, wait_for_snap_angle_release: bool, snap_zoom: Key, diff --git a/editor/src/document/movement_message_handler.rs b/editor/src/document/movement_message_handler.rs index 5f5695ac..e3f34c41 100644 --- a/editor/src/document/movement_message_handler.rs +++ b/editor/src/document/movement_message_handler.rs @@ -166,7 +166,7 @@ impl MessageHandler, LayerMeta } ConstrainX => self.transform_operation.constrain_axis(Axis::X, &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); let new_snap = ipp.keyboard.get(snap_key as usize); @@ -173,7 +173,7 @@ impl MessageHandler, LayerMeta if self.transform_operation != TransformOperation::None { let active = actions!(TransformLayerMessageDiscriminant; - MouseMove, + PointerMove, CancelTransformOperation, ApplyTransformOperation, TypeDigit, diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index b05e11fa..86bb85f2 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -29,7 +29,7 @@ impl Default for Mapping { let mappings = mapping![ // Higher priority than entries in sections below 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 entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter}, 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::TypeDecimalPoint, key_down=KeyComma}, 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 - 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::DragStop, key_up=Lmb}, entry! {action=SelectMessage::EditLayer, message=InputMapperMessage::DoubleClick}, entry! {action=SelectMessage::Abort, key_down=Rmb}, 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 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::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::RotateCanvasBegin, key_down=Rmb}, entry! {action=NavigateMessage::ZoomCanvasBegin, key_down=Lmb}, diff --git a/editor/src/input/input_preprocessor.rs b/editor/src/input/input_preprocessor.rs index 2bb4ba6a..1251be32 100644 --- a/editor/src/input/input_preprocessor.rs +++ b/editor/src/input/input_preprocessor.rs @@ -35,7 +35,7 @@ mod test { let editor_mouse_state = EditorMouseState::from_editor_position(4., 809.); 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(); @@ -51,7 +51,7 @@ mod test { let editor_mouse_state = EditorMouseState::new(); 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(); @@ -67,7 +67,7 @@ mod test { let editor_mouse_state = EditorMouseState::new(); 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(); diff --git a/editor/src/input/input_preprocessor_message.rs b/editor/src/input/input_preprocessor_message.rs index 53297fa8..a62ad540 100644 --- a/editor/src/input/input_preprocessor_message.rs +++ b/editor/src/input/input_preprocessor_message.rs @@ -16,8 +16,8 @@ pub enum InputPreprocessorMessage { DoubleClick { editor_mouse_state: EditorMouseState, modifier_keys: ModifierKeys }, KeyDown { 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 }, - 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 }, } diff --git a/editor/src/input/input_preprocessor_message_handler.rs b/editor/src/input/input_preprocessor_message_handler.rs index 93a4d58e..24dde1c7 100644 --- a/editor/src/input/input_preprocessor_message_handler.rs +++ b/editor/src/input/input_preprocessor_message_handler.rs @@ -67,24 +67,6 @@ impl MessageHandler for InputPreprocessorMessageHa self.keyboard.unset(key as usize); 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 } => { self.handle_modifier_keys(modifier_keys, responses); @@ -94,7 +76,25 @@ impl MessageHandler for InputPreprocessorMessageHa 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); let mouse_state = editor_mouse_state.to_mouse_state(&self.viewport_bounds); diff --git a/editor/src/misc/test_utils.rs b/editor/src/misc/test_utils.rs index 17fd6e26..acaba7e5 100644 --- a/editor/src/misc/test_utils.rs +++ b/editor/src/misc/test_utils.rs @@ -52,17 +52,17 @@ impl EditorTestUtils for Editor { let mut editor_mouse_state = EditorMouseState::new(); editor_mouse_state.editor_position = ViewportPosition::new(x, y); 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) { 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) { 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) { diff --git a/editor/src/viewport_tools/snapping.rs b/editor/src/viewport_tools/snapping.rs index af710a38..807f19f0 100644 --- a/editor/src/viewport_tools/snapping.rs +++ b/editor/src/viewport_tools/snapping.rs @@ -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.) /// 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, snap_x: bool, snap_y: bool) { + pub fn start_snap(&mut self, document_message_handler: &DocumentMessageHandler, bounding_boxes: impl Iterator, snap_x: bool, snap_y: bool) { if document_message_handler.snapping_enabled { - let (x_targets, y_targets) = target_layers - .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 (x_targets, y_targets) = bounding_boxes.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. 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, responses: &mut VecDeque, document_message_handler: &DocumentMessageHandler, - selected_layers: &[Vec], + (snap_x, snap_y): (Vec, Vec), viewport_bounds: DVec2, mouse_delta: DVec2, ) -> DVec2 { if document_message_handler.snapping_enabled { if let Some((targets_x, targets_y)) = &self.snap_targets { - let (snap_x, snap_y): (Vec, Vec) = 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 distances = targets_y.iter().flat_map(|&target| snap_y.iter().map(move |&snap| (target, target - mouse_delta.y - snap))); diff --git a/editor/src/viewport_tools/tool.rs b/editor/src/viewport_tools/tool.rs index 6e5ad473..2d5ce62c 100644 --- a/editor/src/viewport_tools/tool.rs +++ b/editor/src/viewport_tools/tool.rs @@ -187,7 +187,7 @@ pub fn standard_tool_message(tool: ToolType, message_type: StandardToolMessageTy match message_type { StandardToolMessageType::DocumentIsDirty => match tool { 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::Eyedropper => None, // Some(EyedropperMessage::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 { 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::Eyedropper => Some(EyedropperMessage::Abort.into()), ToolType::Text => Some(TextMessage::Abort.into()), diff --git a/editor/src/viewport_tools/tools/crop.rs b/editor/src/viewport_tools/tools/crop.rs index 618c7fa5..8d3f42b8 100644 --- a/editor/src/viewport_tools/tools/crop.rs +++ b/editor/src/viewport_tools/tools/crop.rs @@ -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::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}; #[derive(Default)] -pub struct Crop; +pub struct Crop { + fsm_state: CropToolFsmState, + data: CropToolData, +} #[remain::sorted] #[impl_message(Message, ToolMessage, Crop)] #[derive(PartialEq, Clone, Debug, Hash, Serialize, Deserialize)] pub enum CropMessage { // Standard messages - // #[remain::unsorted] - // Abort, + #[remain::unsorted] + Abort, + #[remain::unsorted] + DocumentIsDirty, // Tool-specific messages - MouseMove, + PointerDown, + PointerMove { + constrain_axis_or_aspect: Key, + center: Key, + }, + PointerUp, } impl<'a> MessageHandler> for Crop { fn process_action(&mut self, action: ToolMessage, data: ToolActionHandlerData<'a>, responses: &mut VecDeque) { - 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 {} + +#[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, + selected_board: Option, + 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, + ) -> 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) { + 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) { + responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into()); + } +} diff --git a/editor/src/viewport_tools/tools/line.rs b/editor/src/viewport_tools/tools/line.rs index 3c8fb019..31325640 100644 --- a/editor/src/viewport_tools/tools/line.rs +++ b/editor/src/viewport_tools/tools/line.rs @@ -155,7 +155,7 @@ impl Fsm for LineToolFsmState { if let ToolMessage::Line(event) = event { match (self, event) { (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); responses.push_back(DocumentMessage::StartTransaction.into()); diff --git a/editor/src/viewport_tools/tools/navigate.rs b/editor/src/viewport_tools/tools/navigate.rs index 021ea623..ddaa77d5 100644 --- a/editor/src/viewport_tools/tools/navigate.rs +++ b/editor/src/viewport_tools/tools/navigate.rs @@ -28,7 +28,7 @@ pub enum NavigateMessage { ClickZoom { zoom_in: bool, }, - MouseMove { + PointerMove { snap_angle: Key, snap_zoom: Key, }, @@ -66,7 +66,7 @@ impl<'a> MessageHandler> for Navigate { match self.fsm_state { 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 } => { 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 { messages.push_front(if zoom_in { MovementMessage::IncreaseCanvasZoom { center_on_mouse: true }.into() @@ -122,9 +122,9 @@ impl Fsm for NavigateToolFsmState { NavigateToolFsmState::Ready } - MouseMove { snap_angle, snap_zoom } => { + PointerMove { snap_angle, snap_zoom } => { messages.push_front( - MovementMessage::MouseMove { + MovementMessage::PointerMove { snap_angle, wait_for_snap_angle_release: false, snap_zoom, diff --git a/editor/src/viewport_tools/tools/path.rs b/editor/src/viewport_tools/tools/path.rs index eeee060e..608d1c68 100644 --- a/editor/src/viewport_tools/tools/path.rs +++ b/editor/src/viewport_tools/tools/path.rs @@ -142,7 +142,7 @@ impl Fsm for PathToolFsmState { // Select the first point within the threshold (in pixels) if data.shape_editor.select_point(input.mouse.position, SELECTION_THRESHOLD, add_to_selection, responses) { responses.push_back(DocumentMessage::StartTransaction.into()); - data.snap_handler.start_snap(document, document.visible_layers(), true, true); + data.snap_handler.start_snap(document, document.bounding_boxes(None, None), true, true); let snap_points = data .shape_editor .shapes_to_modify diff --git a/editor/src/viewport_tools/tools/pen.rs b/editor/src/viewport_tools/tools/pen.rs index b875068a..13456384 100644 --- a/editor/src/viewport_tools/tools/pen.rs +++ b/editor/src/viewport_tools/tools/pen.rs @@ -155,7 +155,7 @@ impl Fsm for PenToolFsmState { responses.push_back(DocumentMessage::DeselectAllLayers.into()); 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 pos = transform.inverse().transform_point2(snapped_position); diff --git a/editor/src/viewport_tools/tools/select.rs b/editor/src/viewport_tools/tools/select.rs index d58b1ae7..9ff5b2b3 100644 --- a/editor/src/viewport_tools/tools/select.rs +++ b/editor/src/viewport_tools/tools/select.rs @@ -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::document::transformation::{OriginalTransforms, Selected}; +use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; +use crate::document::transformation::Selected; use crate::document::utility_types::{AlignAggregate, AlignAxis, FlipAxis}; use crate::document::DocumentMessageHandler; 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::tool::{DocumentToolData, Fsm, ToolActionHandlerData, ToolType}; -use graphene::color::Color; use graphene::document::Document; use graphene::intersection::Quad; use graphene::layers::layer_info::LayerDataType; -use graphene::layers::style::{self, Fill, Stroke}; use graphene::Operation; +use super::shared::transformation_cage::*; + use glam::{DAffine2, DVec2}; use serde::{Deserialize, Serialize}; @@ -50,9 +50,10 @@ pub enum SelectMessage { EditLayer, FlipHorizontal, FlipVertical, - MouseMove { + PointerMove { axis_align: Key, snap_angle: Key, + center: Key, }, } @@ -253,11 +254,9 @@ impl<'a> MessageHandler> for Select { use SelectToolFsmState::*; match self.fsm_state { - Ready => actions!(SelectMessageDiscriminant; DragStart, MouseMove, EditLayer), - Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, EditLayer), - DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditLayer), - ResizingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditLayer), - RotatingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditLayer), + Ready => actions!(SelectMessageDiscriminant; DragStart, PointerMove, EditLayer), + Dragging => actions!(SelectMessageDiscriminant; DragStop, PointerMove, EditLayer), + _ => actions!(SelectMessageDiscriminant; DragStop, PointerMove, 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) -> Vec { - 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) -> [Vec; 8] { - const EMPTY_VEC: Vec = 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, - pub transform_handles: [Vec; 8], - pub bounds: [DVec2; 2], - pub selected_edges: Option, - pub original_transforms: OriginalTransforms, - pub pivot: DVec2, -} - -impl BoundingBoxOverlays { - #[must_use] - pub fn new(buffer: &mut Vec) -> 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) { - 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) { - 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 { type ToolData = SelectToolData; type ToolOptions = (); @@ -545,6 +330,8 @@ impl Fsm for SelectToolFsmState { let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(&mut buffer)); bounding_box_overlays.bounds = bounds; + bounding_box_overlays.transform = DAffine2::IDENTITY; + bounding_box_overlays.transform(&mut buffer); 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_y = selected_edges.0 || selected_edges.1; - data.snap_handler - .start_snap(document, document.visible_layers().filter(|layer| !selected.iter().any(|path| path == layer)), snap_x, snap_y); + data.snap_handler.start_snap(document, document.bounding_boxes(Some(&selected), None), snap_x, snap_y); data.layers_dragging = selected; @@ -632,8 +418,7 @@ impl Fsm for SelectToolFsmState { buffer.push(DocumentMessage::StartTransaction.into()); data.layers_dragging = selected; - data.snap_handler - .start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true); + data.snap_handler.start_snap(document, document.bounding_boxes(Some(&data.layers_dragging), None), true, true); Dragging } else { @@ -647,8 +432,7 @@ impl Fsm for SelectToolFsmState { buffer.push(DocumentMessage::AddSelectedLayers { additional_layers: selected.clone() }.into()); buffer.push(DocumentMessage::StartTransaction.into()); data.layers_dragging.append(&mut selected); - data.snap_handler - .start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true); + data.snap_handler.start_snap(document, document.bounding_boxes(Some(&data.layers_dragging), None), true, true); Dragging } else { @@ -660,23 +444,23 @@ impl Fsm for SelectToolFsmState { 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. responses.push_front(SelectMessage::DocumentIsDirty.into()); - let mouse_position = if input.keyboard.get(axis_align as usize) { - let mouse_position = input.mouse.position - data.drag_start; - let snap_resolution = SELECTION_DRAG_ANGLE.to_radians(); - let angle = -mouse_position.angle_between(DVec2::X); - let snapped_angle = (angle / snap_resolution).round() * snap_resolution; - DVec2::new(snapped_angle.cos(), snapped_angle.sin()) * mouse_position.length() + data.drag_start - } else { - input.mouse.position - }; + let mouse_position = axis_align_drag(input.keyboard.get(axis_align as usize), input.mouse.position, data.drag_start); 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 for path in Document::shallowest_unique_layers(data.layers_dragging.iter()) { responses.push_front( @@ -690,14 +474,17 @@ impl Fsm for SelectToolFsmState { data.drag_current = mouse_position + closest_move; Dragging } - (ResizingBounds, MouseMove { .. }) => { + (ResizingBounds, PointerMove { axis_align, center, .. }) => { if let Some(bounds) = &mut data.bounding_box_overlays { 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 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::>(); 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 } - (RotatingBounds, MouseMove { snap_angle, .. }) => { + (RotatingBounds, PointerMove { snap_angle, .. }) => { if let Some(bounds) = &mut data.bounding_box_overlays { let angle = { let start_offset = data.drag_start - bounds.pivot; @@ -733,14 +520,14 @@ impl Fsm for SelectToolFsmState { RotatingBounds } - (DrawingBox, MouseMove { .. }) => { + (DrawingBox, PointerMove { .. }) => { data.drag_current = input.mouse.position; responses.push_front( DocumentMessage::Overlays( Operation::SetLayerTransformInViewport { 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(), ) @@ -748,8 +535,8 @@ impl Fsm for SelectToolFsmState { ); DrawingBox } - (Ready, MouseMove { .. }) => { - let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input)); + (Ready, PointerMove { .. }) => { + let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true)); if data.cursor != cursor { data.cursor = cursor; diff --git a/editor/src/viewport_tools/tools/shared/mod.rs b/editor/src/viewport_tools/tools/shared/mod.rs index bade0214..b7d6c44c 100644 --- a/editor/src/viewport_tools/tools/shared/mod.rs +++ b/editor/src/viewport_tools/tools/shared/mod.rs @@ -1 +1,2 @@ pub mod resize; +pub mod transformation_cage; diff --git a/editor/src/viewport_tools/tools/shared/resize.rs b/editor/src/viewport_tools/tools/shared/resize.rs index 145ed57a..360d2a74 100644 --- a/editor/src/viewport_tools/tools/shared/resize.rs +++ b/editor/src/viewport_tools/tools/shared/resize.rs @@ -19,7 +19,7 @@ pub struct Resize { impl Resize { /// Starts a resize, assigning the snap targets and snapping the starting position. pub fn start(&mut self, responses: &mut VecDeque, 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); } diff --git a/editor/src/viewport_tools/tools/shared/transformation_cage.rs b/editor/src/viewport_tools/tools/shared/transformation_cage.rs new file mode 100644 index 00000000..e63196c5 --- /dev/null +++ b/editor/src/viewport_tools/tools/shared/transformation_cage.rs @@ -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) -> Vec { + 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) -> [Vec; 8] { + const EMPTY_VEC: Vec = 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, + pub transform_handles: [Vec; 8], + pub bounds: [DVec2; 2], + pub transform: DAffine2, + pub selected_edges: Option, + pub original_transforms: OriginalTransforms, + pub pivot: DVec2, +} + +impl BoundingBoxOverlays { + #[must_use] + pub fn new(buffer: &mut Vec) -> 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) { + 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) { + 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()), + ); + } +} diff --git a/editor/src/viewport_tools/tools/text.rs b/editor/src/viewport_tools/tools/text.rs index 983aa7ff..da0d0e8b 100644 --- a/editor/src/viewport_tools/tools/text.rs +++ b/editor/src/viewport_tools/tools/text.rs @@ -9,7 +9,6 @@ use crate::misc::{HintData, HintGroup, HintInfo, KeysGroup}; use crate::viewport_tools::tool::{DocumentToolData, Fsm, ToolActionHandlerData}; use glam::{DAffine2, DVec2}; -use graphene::color::Color; use graphene::intersection::Quad; use graphene::layers::style::{self, Fill, Stroke}; use graphene::Operation; diff --git a/frontend/src/components/panels/Document.vue b/frontend/src/components/panels/Document.vue index 46af9f43..e2e7cb14 100644 --- a/frontend/src/components/panels/Document.vue +++ b/frontend/src/components/panels/Document.vue @@ -17,7 +17,7 @@ - + diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index ac586a62..59dcb836 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -250,7 +250,7 @@ impl JsEditorHandle { 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); } @@ -271,7 +271,7 @@ impl JsEditorHandle { 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); } @@ -281,7 +281,7 @@ impl JsEditorHandle { 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); } @@ -515,8 +515,12 @@ impl JsEditorHandle { } /// 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) { - let message = ArtboardMessage::AddArtboard { top, left, height, width }; + pub fn create_artboard_and_fit_to_viewport(&self, pos_x: f64, pos_y: f64, width: f64, height: f64) { + let message = ArtboardMessage::AddArtboard { + id: None, + position: (pos_x, pos_y), + size: (width, height), + }; self.dispatch(message); let message = DocumentMessage::ZoomCanvasToFitAll; self.dispatch(message); diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 98e816d1..afde8f9b 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -299,6 +299,12 @@ impl Document { Ok(layer.data.bounding_box(transform)) } + pub fn bounding_box_and_transform(&self, path: &[LayerId]) -> Result, 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]> { let mut paths = vec![]; self.visible_layers(&mut vec![], &mut paths).ok()?; diff --git a/proc-macros/src/lib.rs b/proc-macros/src/lib.rs index ee4a6d76..e09f6542 100644 --- a/proc-macros/src/lib.rs +++ b/proc-macros/src/lib.rs @@ -240,23 +240,23 @@ pub fn derive_hint(input_item: TokenStream) -> TokenStream { /// # Example /// ```ignore /// 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")] /// 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")] /// SelectToolState::Ready /// } -/// (SelectToolState::Pending, Event::MouseMove(x,y)) => { +/// (SelectToolState::Pending, Event::PointerMove(x,y)) => { /// #[edge("Mouse Move")] /// SelectToolState::TransformSelected /// } -/// (SelectToolState::TransformSelected, Event::MouseMove(x,y)) => { +/// (SelectToolState::TransformSelected, Event::PointerMove(x,y)) => { /// #[edge("Mouse Move")] /// 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")] /// SelectToolState::Ready /// }