diff --git a/editor/src/consts.rs b/editor/src/consts.rs index bbc7ac59..68e0b9a9 100644 --- a/editor/src/consts.rs +++ b/editor/src/consts.rs @@ -18,6 +18,9 @@ pub const VIEWPORT_ROTATE_SNAP_INTERVAL: f64 = 15.; pub const VIEWPORT_ZOOM_TO_FIT_PADDING_SCALE_FACTOR: f64 = 0.95; +pub const DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS: f64 = 50.; +pub const DRAG_BEYOND_VIEWPORT_SPEED_FACTOR: f64 = 0.5; + // Snapping point pub const SNAP_POINT_TOLERANCE: f64 = 5.; diff --git a/editor/src/messages/broadcast/broadcast_event.rs b/editor/src/messages/broadcast/broadcast_event.rs index b09364e2..f7224f79 100644 --- a/editor/src/messages/broadcast/broadcast_event.rs +++ b/editor/src/messages/broadcast/broadcast_event.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; #[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize, Hash)] #[impl_message(Message, BroadcastMessage, TriggerEvent)] pub enum BroadcastEvent { + AnimationFrame, CanvasTransformed, ToolAbort, SelectionChanged, diff --git a/editor/src/messages/input_mapper/default_mapping.rs b/editor/src/messages/input_mapper/default_mapping.rs index e10d8da4..5428b8c8 100644 --- a/editor/src/messages/input_mapper/default_mapping.rs +++ b/editor/src/messages/input_mapper/default_mapping.rs @@ -9,6 +9,7 @@ use crate::messages::portfolio::document::utility_types::clipboards::Clipboard; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; use crate::messages::prelude::*; use crate::messages::tool::tool_messages::brush_tool::BrushToolMessageOptionsUpdate; +use crate::messages::tool::tool_messages::select_tool::SelectToolPointerKeys; use glam::DVec2; @@ -71,7 +72,7 @@ pub fn default_mapping() -> Mapping { entry!(PointerMove; refresh_keys=[Control, Shift], action_dispatch=TransformLayerMessage::PointerMove { slow_key: Shift, snap_key: Control }), // // SelectToolMessage - entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt }), + entry!(PointerMove; refresh_keys=[Control, Alt, Shift], action_dispatch=SelectToolMessage::PointerMove(SelectToolPointerKeys { axis_align: Shift, snap_angle: Control, center: Alt, duplicate: Alt })), entry!(KeyDown(Lmb); action_dispatch=SelectToolMessage::DragStart { add_to_selection: Shift, select_deepest: Accel }), entry!(KeyUp(Lmb); action_dispatch=SelectToolMessage::DragStop { remove_from_selection: Shift }), entry!(KeyDown(Enter); action_dispatch=SelectToolMessage::Enter), diff --git a/editor/src/messages/tool/tool_messages/select_tool.rs b/editor/src/messages/tool/tool_messages/select_tool.rs index e8b8f87b..78b9c487 100644 --- a/editor/src/messages/tool/tool_messages/select_tool.rs +++ b/editor/src/messages/tool/tool_messages/select_tool.rs @@ -2,7 +2,7 @@ use super::tool_prelude::*; use crate::application::generate_uuid; -use crate::consts::{ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; +use crate::consts::{DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS, DRAG_BEYOND_VIEWPORT_SPEED_FACTOR, ROTATE_SNAP_ANGLE, SELECTION_TOLERANCE}; use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition; use crate::messages::portfolio::document::overlays::utility_types::OverlayContext; use crate::messages::portfolio::document::utility_types::document_metadata::LayerNodeIdentifier; @@ -52,6 +52,14 @@ impl fmt::Display for NestedSelectionBehavior { } } +#[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] +pub struct SelectToolPointerKeys { + pub axis_align: Key, + pub snap_angle: Key, + pub center: Key, + pub duplicate: Key, +} + #[remain::sorted] #[impl_message(Message, ToolMessage, Select)] #[derive(PartialEq, Clone, Debug, Serialize, Deserialize, specta::Type)] @@ -72,16 +80,13 @@ pub enum SelectToolMessage { }, EditLayer, Enter, - PointerMove { - axis_align: Key, - snap_angle: Key, - center: Key, - duplicate: Key, - }, + PointerMove(SelectToolPointerKeys), + PointerOutsideViewport(SelectToolPointerKeys), SelectOptions(SelectOptionsUpdate), SetPivot { position: PivotPosition, }, + ShiftViewport, } impl ToolMetadata for SelectTool { @@ -269,6 +274,7 @@ struct SelectToolData { selected_layers_count: usize, selected_layers_changed: bool, snap_candidates: Vec, + subscribed_to_animation_frame: bool, } impl SelectToolData { @@ -590,16 +596,16 @@ impl Fsm for SelectToolFsmState { state } - (SelectToolFsmState::Dragging, SelectToolMessage::PointerMove { axis_align, duplicate, .. }) => { + (SelectToolFsmState::Dragging, SelectToolMessage::PointerMove(modifier_keys)) => { tool_data.has_dragged = true; - if input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_none() { + if input.keyboard.key(modifier_keys.duplicate) && tool_data.non_duplicated_layers.is_none() { tool_data.start_duplicates(document, responses); - } else if !input.keyboard.key(duplicate) && tool_data.non_duplicated_layers.is_some() { + } else if !input.keyboard.key(modifier_keys.duplicate) && tool_data.non_duplicated_layers.is_some() { tool_data.stop_duplicates(document, responses); } - let axis_align = input.keyboard.key(axis_align); + let axis_align = input.keyboard.key(modifier_keys.axis_align); let mouse_position = axis_align_drag(axis_align, input.mouse.position, tool_data.drag_start); let total_mouse_delta_document = document.metadata.document_to_viewport.inverse().transform_vector2(mouse_position - tool_data.drag_start); @@ -644,12 +650,14 @@ impl Fsm for SelectToolFsmState { } tool_data.drag_current += mouse_delta; + setup_pointer_outside_edge_event(input.mouse.position, input.viewport_bounds.size(), tool_data, modifier_keys, responses); + SelectToolFsmState::Dragging } - (SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove { axis_align, center, .. }) => { - if let Some(bounds) = &mut tool_data.bounding_box_manager { + (SelectToolFsmState::ResizingBounds, SelectToolMessage::PointerMove(modifier_keys)) => { + if let Some(ref mut bounds) = &mut tool_data.bounding_box_manager { if let Some(movement) = &mut bounds.selected_edges { - let (center, constrain) = (input.keyboard.key(center), input.keyboard.key(axis_align)); + let (center, constrain) = (input.keyboard.key(modifier_keys.center), input.keyboard.key(modifier_keys.axis_align)); let center = center.then_some(bounds.center_of_transformation); let snap = Some(SizeSnapData { @@ -677,11 +685,13 @@ impl Fsm for SelectToolFsmState { ); selected.apply_transformation(bounds.original_bound_transform * transformation * bounds.original_bound_transform.inverse()); + + setup_pointer_outside_edge_event(input.mouse.position, input.viewport_bounds.size(), tool_data, modifier_keys, responses); } } SelectToolFsmState::ResizingBounds } - (SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove { snap_angle, .. }) => { + (SelectToolFsmState::RotatingBounds, SelectToolMessage::PointerMove(modifier_keys)) => { if let Some(bounds) = &mut tool_data.bounding_box_manager { let angle = { let start_offset = tool_data.drag_start - bounds.center_of_transformation; @@ -690,7 +700,7 @@ impl Fsm for SelectToolFsmState { start_offset.angle_between(end_offset) }; - let snapped_angle = if input.keyboard.key(snap_angle) { + let snapped_angle = if input.keyboard.key(modifier_keys.snap_angle) { let snap_resolution = ROTATE_SNAP_ANGLE.to_radians(); (angle / snap_resolution).round() * snap_resolution } else { @@ -716,20 +726,24 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::RotatingBounds } - (SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove { .. }) => { + (SelectToolFsmState::DraggingPivot, SelectToolMessage::PointerMove(modifier_keys)) => { let mouse_position = input.mouse.position; let snapped_mouse_position = mouse_position; //tool_data.snap_manager.snap_position(responses, document, mouse_position); tool_data.pivot.set_viewport_position(snapped_mouse_position, document, responses); + setup_pointer_outside_edge_event(mouse_position, input.viewport_bounds.size(), tool_data, modifier_keys, responses); + SelectToolFsmState::DraggingPivot } - (SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove { .. }) => { + (SelectToolFsmState::DrawingBox, SelectToolMessage::PointerMove(modifier_keys)) => { tool_data.drag_current = input.mouse.position; responses.add(OverlaysMessage::Draw); + setup_pointer_outside_edge_event(input.mouse.position, input.viewport_bounds.size(), tool_data, modifier_keys, responses); + SelectToolFsmState::DrawingBox } - (SelectToolFsmState::Ready, SelectToolMessage::PointerMove { .. }) => { + (SelectToolFsmState::Ready, SelectToolMessage::PointerMove(_)) => { let mut cursor = tool_data.bounding_box_manager.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true)); // Dragging the pivot overrules the other operations @@ -747,6 +761,50 @@ impl Fsm for SelectToolFsmState { SelectToolFsmState::Ready } + (SelectToolFsmState::Dragging, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => { + responses.add(SelectToolMessage::PointerMove(modifier_keys)); + + if let Some(shift) = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses) { + tool_data.drag_current += shift; + tool_data.drag_start += shift; + } + + SelectToolFsmState::Dragging + } + (SelectToolFsmState::ResizingBounds | SelectToolFsmState::DraggingPivot | SelectToolFsmState::DrawingBox, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => { + responses.add(SelectToolMessage::PointerMove(modifier_keys)); + + responses.add(SelectToolMessage::ShiftViewport); + + self + } + (SelectToolFsmState::ResizingBounds, SelectToolMessage::ShiftViewport) => { + if let Some(shift) = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses) { + if let Some(ref mut bounds) = &mut tool_data.bounding_box_manager { + bounds.center_of_transformation += shift; + bounds.original_bound_transform.translation += shift; + } + } + + self + } + (SelectToolFsmState::DraggingPivot, SelectToolMessage::ShiftViewport) => { + let _ = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses); + + self + } + (SelectToolFsmState::DrawingBox, SelectToolMessage::ShiftViewport) => { + if let Some(shift) = shift_viewport_if_mouse_beyond_edge(input.mouse.position, input.viewport_bounds.size(), responses) { + tool_data.drag_start += shift; + } + + self + } + (state, SelectToolMessage::PointerOutsideViewport(modifier_keys)) => { + unsubscribe_animation_frame(tool_data, modifier_keys, responses); + + state + } (SelectToolFsmState::Dragging, SelectToolMessage::Enter) => { let response = match input.mouse.position.distance(tool_data.drag_start) < 10. * f64::EPSILON { true => DocumentMessage::Undo, @@ -1011,3 +1069,68 @@ fn edit_layer_deepest_manipulation(layer: LayerNodeIdentifier, document_network: responses.add_front(ToolMessage::ActivateTool { tool_type: ToolType::Path }); } } + +/// Shifts the viewport when the mouse reaches the edge of the viewport. +/// +/// If the mouse was beyond any edge, it returns the amount shifted. Otherwise it returns None. +/// The shift is proportional to the distance between edge and mouse. It is also guaranteed to be integral. +fn shift_viewport_if_mouse_beyond_edge(mouse_position: DVec2, viewport_size: DVec2, responses: &mut VecDeque) -> Option { + let mouse_position = mouse_position.clamp( + DVec2::ZERO - DVec2::splat(DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS), + viewport_size + DVec2::splat(DRAG_BEYOND_VIEWPORT_MAX_OVEREXTENSION_PIXELS), + ); + let mouse_position_percent = mouse_position / viewport_size; + + let mut shift_percent = DVec2::ZERO; + + if mouse_position_percent.x < 0. { + shift_percent.x = -mouse_position_percent.x; + } else if mouse_position_percent.x > 1. { + shift_percent.x = 1. - mouse_position_percent.x; + } + + if mouse_position_percent.y < 0. { + shift_percent.y = -mouse_position_percent.y; + } else if mouse_position_percent.y > 1. { + shift_percent.y = 1. - mouse_position_percent.y; + } + + if shift_percent.x == 0. && shift_percent.y == 0. { + return None; + } + + let delta = (shift_percent * DRAG_BEYOND_VIEWPORT_SPEED_FACTOR * viewport_size).round(); + responses.add(NavigationMessage::TranslateCanvas { delta }); + Some(delta) +} + +fn setup_pointer_outside_edge_event(mouse_position: DVec2, viewport_size: DVec2, tool_data: &mut SelectToolData, modifier_keys: SelectToolPointerKeys, responses: &mut VecDeque) { + let is_pointer_outside_edge = mouse_position.x < 0. || mouse_position.x > viewport_size.x || mouse_position.y < 0. || mouse_position.y > viewport_size.y; + + match is_pointer_outside_edge { + true => subscribe_animation_frame(tool_data, modifier_keys, responses), + false => unsubscribe_animation_frame(tool_data, modifier_keys, responses), + } +} + +fn subscribe_animation_frame(tool_data: &mut SelectToolData, modifier_keys: SelectToolPointerKeys, responses: &mut VecDeque) { + if !tool_data.subscribed_to_animation_frame { + tool_data.subscribed_to_animation_frame = true; + + responses.add(BroadcastMessage::SubscribeEvent { + on: BroadcastEvent::AnimationFrame, + send: Box::new(SelectToolMessage::PointerOutsideViewport(modifier_keys).into()), + }); + } +} + +fn unsubscribe_animation_frame(tool_data: &mut SelectToolData, modifier_keys: SelectToolPointerKeys, responses: &mut VecDeque) { + if tool_data.subscribed_to_animation_frame { + tool_data.subscribed_to_animation_frame = false; + + responses.add(BroadcastMessage::UnsubscribeEvent { + on: BroadcastEvent::AnimationFrame, + message: Box::new(SelectToolMessage::PointerOutsideViewport(modifier_keys).into()), + }); + } +} diff --git a/frontend/wasm/src/editor_api.rs b/frontend/wasm/src/editor_api.rs index 750293b3..7196e525 100644 --- a/frontend/wasm/src/editor_api.rs +++ b/frontend/wasm/src/editor_api.rs @@ -239,6 +239,12 @@ impl JsEditorHandle { *g.borrow_mut() = Some(Closure::new(move || { wasm_bindgen_futures::spawn_local(poll_node_graph_evaluation()); + call_closure_with_editor_and_handle(|editor, handle| { + for message in editor.handle_message(BroadcastMessage::TriggerEvent(BroadcastEvent::AnimationFrame)) { + handle.send_frontend_message_to_js(message); + } + }); + // Schedule ourself for another requestAnimationFrame callback request_animation_frame(f.borrow().as_ref().unwrap()); }));