Transformation cage (#502)
* Render corners and edges of selection box * Refactor * Add drag detection * Implement the transform handles * Implement rotation by dragging <40px from bounds * Refine clustered handle behaviour * Add cursors * Add snap angle * Fix MMB drag whilst in select tool * Convert calculate_pivot into a seperate function * rename start_vec to start_offset * Fix typo * Remove Undo transaction on <10px mouse move Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
2a313471d8
commit
599a9d076b
|
|
@ -28,6 +28,10 @@ pub const SLOWING_DIVISOR: f64 = 10.;
|
|||
pub const SELECTION_TOLERANCE: f64 = 1.;
|
||||
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
||||
|
||||
// Transformation cage
|
||||
pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.;
|
||||
pub const BOUNDS_ROTATE_THRESHOLD: f64 = 40.;
|
||||
|
||||
// Path tool
|
||||
pub const VECTOR_MANIPULATOR_ANCHOR_MARKER_SIZE: f64 = 5.;
|
||||
|
||||
|
|
|
|||
|
|
@ -191,9 +191,9 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
|||
|
||||
let half_viewport = ipp.viewport_bounds.size() / 2.;
|
||||
let rotation = {
|
||||
let start_vec = self.mouse_position - half_viewport;
|
||||
let end_vec = ipp.mouse.position - half_viewport;
|
||||
start_vec.angle_between(end_vec)
|
||||
let start_offset = self.mouse_position - half_viewport;
|
||||
let end_offset = ipp.mouse.position - half_viewport;
|
||||
start_offset.angle_between(end_offset)
|
||||
};
|
||||
|
||||
responses.push_back(SetCanvasRotation { angle_radians: self.tilt + rotation }.into());
|
||||
|
|
@ -329,7 +329,6 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
|||
|
||||
fn actions(&self) -> ActionList {
|
||||
let mut common = actions!(MovementMessageDiscriminant;
|
||||
MouseMove,
|
||||
TranslateCanvasBegin,
|
||||
RotateCanvasBegin,
|
||||
ZoomCanvasBegin,
|
||||
|
|
@ -345,6 +344,7 @@ impl MessageHandler<MovementMessage, (&Document, &InputPreprocessorMessageHandle
|
|||
|
||||
if self.panning || self.tilting || self.zooming {
|
||||
let transforming = actions!(MovementMessageDiscriminant;
|
||||
MouseMove,
|
||||
TransformCanvasEnd,
|
||||
);
|
||||
common.extend(transforming);
|
||||
|
|
|
|||
|
|
@ -36,7 +36,9 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
|
|||
use TransformLayerMessage::*;
|
||||
|
||||
let (layer_metadata, document, ipp) = data;
|
||||
let mut selected = Selected::new(&mut self.original_transforms, &mut self.pivot, layer_metadata, responses, document);
|
||||
|
||||
let selected_layers = layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then(|| layer_path)).collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(&mut self.original_transforms, &mut self.pivot, &selected_layers, responses, document);
|
||||
|
||||
let mut begin_operation = |operation: TransformOperation, typing: &mut Typing, mouse_position: &mut DVec2, start_mouse: &mut DVec2| {
|
||||
if operation != TransformOperation::None {
|
||||
|
|
@ -128,10 +130,10 @@ impl MessageHandler<TransformLayerMessage, (&mut HashMap<Vec<LayerId>, LayerMeta
|
|||
TransformOperation::Rotating(rotation) => {
|
||||
let selected_pivot = selected.calculate_pivot();
|
||||
let angle = {
|
||||
let start_vec = self.mouse_position - selected_pivot;
|
||||
let end_vec = ipp.mouse.position - selected_pivot;
|
||||
let start_offset = self.mouse_position - selected_pivot;
|
||||
let end_offset = ipp.mouse.position - selected_pivot;
|
||||
|
||||
start_vec.angle_between(end_vec)
|
||||
start_offset.angle_between(end_offset)
|
||||
};
|
||||
|
||||
let change = if self.slow { angle / SLOWING_DIVISOR } else { angle };
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
use super::layer_panel::LayerMetadata;
|
||||
use crate::consts::{ROTATE_SNAP_ANGLE, SCALE_SNAP_INTERVAL};
|
||||
use crate::message_prelude::*;
|
||||
|
||||
|
|
@ -188,25 +187,18 @@ impl TransformOperation {
|
|||
}
|
||||
|
||||
pub struct Selected<'a> {
|
||||
pub selected: Vec<Vec<LayerId>>,
|
||||
pub selected: &'a [&'a Vec<LayerId>],
|
||||
pub responses: &'a mut VecDeque<Message>,
|
||||
pub document: &'a mut Document,
|
||||
pub document: &'a Document,
|
||||
pub original_transforms: &'a mut OriginalTransforms,
|
||||
pub pivot: &'a mut DVec2,
|
||||
}
|
||||
|
||||
impl<'a> Selected<'a> {
|
||||
pub fn new(
|
||||
original_transforms: &'a mut OriginalTransforms,
|
||||
pivot: &'a mut DVec2,
|
||||
layer_metadata: &'a mut HashMap<Vec<LayerId>, LayerMetadata>,
|
||||
responses: &'a mut VecDeque<Message>,
|
||||
document: &'a mut Document,
|
||||
) -> Self {
|
||||
let selected = layer_metadata.iter().filter_map(|(layer_path, data)| data.selected.then(|| layer_path.to_owned())).collect();
|
||||
for path in &selected {
|
||||
if !original_transforms.contains_key::<Vec<LayerId>>(path) {
|
||||
original_transforms.insert(path.clone(), document.layer(path).unwrap().transform);
|
||||
pub fn new(original_transforms: &'a mut OriginalTransforms, pivot: &'a mut DVec2, selected: &'a [&'a Vec<LayerId>], responses: &'a mut VecDeque<Message>, document: &'a Document) -> Self {
|
||||
for path in selected {
|
||||
if !original_transforms.contains_key(*path) {
|
||||
original_transforms.insert(path.to_vec(), document.layer(path).unwrap().transform);
|
||||
}
|
||||
}
|
||||
Self {
|
||||
|
|
@ -247,7 +239,7 @@ impl<'a> Selected<'a> {
|
|||
// 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 layer_path in Document::shallowest_unique_layers(self.selected.iter()) {
|
||||
let parent_folder_path = &layer_path[..layer_path.len() - 1];
|
||||
let original_layer_transforms = *self.original_transforms.get(layer_path).unwrap();
|
||||
let original_layer_transforms = *self.original_transforms.get(*layer_path).unwrap();
|
||||
|
||||
let to = self.document.generate_transform_across_scope(parent_folder_path, None).unwrap();
|
||||
let new = to.inverse() * transformation * to * original_layer_transforms;
|
||||
|
|
@ -266,11 +258,11 @@ impl<'a> Selected<'a> {
|
|||
}
|
||||
|
||||
pub fn revert_operation(&mut self) {
|
||||
for path in &self.selected {
|
||||
for path in self.selected {
|
||||
self.responses.push_back(
|
||||
DocumentOperation::SetLayerTransform {
|
||||
path: path.to_vec(),
|
||||
transform: (*self.original_transforms.get(path).unwrap()).to_cols_array(),
|
||||
transform: (*self.original_transforms.get(*path).unwrap()).to_cols_array(),
|
||||
}
|
||||
.into(),
|
||||
);
|
||||
|
|
|
|||
|
|
@ -15,4 +15,14 @@ pub enum MouseCursorIcon {
|
|||
Grabbing,
|
||||
Crosshair,
|
||||
Text,
|
||||
NSResize,
|
||||
EWResize,
|
||||
NESWResize,
|
||||
NWSEResize,
|
||||
}
|
||||
|
||||
impl Default for MouseCursorIcon {
|
||||
fn default() -> Self {
|
||||
Self::Default
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -29,6 +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},
|
||||
// Transform layers
|
||||
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=KeyEnter},
|
||||
entry! {action=TransformLayerMessage::ApplyTransformOperation, key_down=Lmb},
|
||||
|
|
@ -42,7 +43,7 @@ impl Default for Mapping {
|
|||
entry! {action=TransformLayerMessage::TypeDecimalPoint, key_down=KeyPeriod},
|
||||
entry! {action=TransformLayerMessage::MouseMove { slow_key: KeyShift, snap_key: KeyControl }, triggers=[KeyShift, KeyControl]},
|
||||
// Select
|
||||
entry! {action=SelectMessage::MouseMove { snap_angle: KeyShift }, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=SelectMessage::MouseMove { axis_align: KeyShift, snap_angle: KeyControl }, message=InputMapperMessage::PointerMove},
|
||||
entry! {action=SelectMessage::DragStart { add_to_selection: KeyShift }, key_down=Lmb},
|
||||
entry! {action=SelectMessage::DragStop, key_up=Lmb},
|
||||
entry! {action=SelectMessage::EditText, message=InputMapperMessage::DoubleClick},
|
||||
|
|
@ -145,7 +146,6 @@ impl Default for Mapping {
|
|||
entry! {action=TransformLayerMessage::BeginRotate, key_down=KeyR},
|
||||
entry! {action=TransformLayerMessage::BeginScale, key_down=KeyS},
|
||||
// Document movement
|
||||
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::RotateCanvasBegin, key_down=Mmb, modifiers=[KeyControl]},
|
||||
entry! {action=MovementMessage::ZoomCanvasBegin, key_down=Mmb, modifiers=[KeyShift]},
|
||||
entry! {action=MovementMessage::TranslateCanvasBegin, key_down=Mmb},
|
||||
|
|
|
|||
|
|
@ -93,18 +93,18 @@ impl SnapHandler {
|
|||
}
|
||||
}
|
||||
|
||||
/// Gets a list of snap targets for the X and Y axes 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.
|
||||
pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator<Item = &'a [LayerId]>) {
|
||||
pub fn start_snap<'a>(&mut self, document_message_handler: &DocumentMessageHandler, target_layers: impl Iterator<Item = &'a [LayerId]>, 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();
|
||||
|
||||
// Could be made into sorted Vec or a HashSet for more performant lookups.
|
||||
self.snap_targets = Some(
|
||||
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(),
|
||||
);
|
||||
self.snap_targets = Some((if snap_x { x_targets } else { Vec::new() }, if snap_y { y_targets } else { Vec::new() }));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
data.snap_handler.start_snap(document, document.visible_layers(), 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());
|
||||
|
|
|
|||
|
|
@ -155,7 +155,7 @@ impl Fsm for PenToolFsmState {
|
|||
responses.push_back(DocumentMessage::DeselectAllLayers.into());
|
||||
data.path = Some(vec![generate_uuid()]);
|
||||
|
||||
data.snap_handler.start_snap(document, document.visible_layers());
|
||||
data.snap_handler.start_snap(document, document.visible_layers(), 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);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use crate::consts::{COLOR_ACCENT, SELECTION_DRAG_ANGLE, SELECTION_TOLERANCE};
|
||||
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::document::utility_types::{AlignAggregate, AlignAxis, FlipAxis};
|
||||
use crate::document::DocumentMessageHandler;
|
||||
use crate::frontend::utility_types::MouseCursorIcon;
|
||||
|
|
@ -11,6 +12,7 @@ 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::style::{self, Fill, Stroke};
|
||||
|
|
@ -48,6 +50,7 @@ pub enum SelectMessage {
|
|||
FlipHorizontal,
|
||||
FlipVertical,
|
||||
MouseMove {
|
||||
axis_align: Key,
|
||||
snap_angle: Key,
|
||||
},
|
||||
}
|
||||
|
|
@ -233,7 +236,7 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
|||
}
|
||||
|
||||
if action == ToolMessage::UpdateCursor {
|
||||
self.fsm_state.update_cursor(responses);
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor: MouseCursorIcon::Default }.into());
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -242,7 +245,6 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
|||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
self.fsm_state.update_cursor(responses);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -250,9 +252,11 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for Select {
|
|||
use SelectToolFsmState::*;
|
||||
|
||||
match self.fsm_state {
|
||||
Ready => actions!(SelectMessageDiscriminant; DragStart, EditText),
|
||||
Ready => actions!(SelectMessageDiscriminant; DragStart, MouseMove, EditText),
|
||||
Dragging => actions!(SelectMessageDiscriminant; DragStop, MouseMove, EditText),
|
||||
DrawingBox => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditText),
|
||||
ResizingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditText),
|
||||
RotatingBounds => actions!(SelectMessageDiscriminant; DragStop, MouseMove, Abort, EditText),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -262,6 +266,8 @@ enum SelectToolFsmState {
|
|||
Ready,
|
||||
Dragging,
|
||||
DrawingBox,
|
||||
ResizingBounds,
|
||||
RotatingBounds,
|
||||
}
|
||||
|
||||
impl Default for SelectToolFsmState {
|
||||
|
|
@ -276,8 +282,9 @@ struct SelectToolData {
|
|||
drag_current: ViewportPosition,
|
||||
layers_dragging: Vec<Vec<LayerId>>, // Paths and offsets
|
||||
drag_box_overlay_layer: Option<Vec<LayerId>>,
|
||||
bounding_box_overlay_layer: Option<Vec<LayerId>>,
|
||||
bounding_box_overlays: Option<BoundingBoxOverlays>,
|
||||
snap_handler: SnapHandler,
|
||||
cursor: MouseCursorIcon,
|
||||
}
|
||||
|
||||
impl SelectToolData {
|
||||
|
|
@ -296,6 +303,63 @@ 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()];
|
||||
|
||||
|
|
@ -309,10 +373,150 @@ fn add_bounding_box(responses: &mut Vec<Message>) -> Vec<LayerId> {
|
|||
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 {
|
||||
type ToolData = SelectToolData;
|
||||
type ToolOptions = ();
|
||||
|
|
@ -334,19 +538,18 @@ impl Fsm for SelectToolFsmState {
|
|||
match (self, event) {
|
||||
(_, DocumentIsDirty) => {
|
||||
let mut buffer = Vec::new();
|
||||
let response = match (document.selected_visible_layers_bounding_box(), data.bounding_box_overlay_layer.take()) {
|
||||
(None, Some(path)) => DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into(),
|
||||
(Some([pos1, pos2]), path) => {
|
||||
let path = path.unwrap_or_else(|| add_bounding_box(&mut buffer));
|
||||
match (document.selected_visible_layers_bounding_box(), data.bounding_box_overlays.take()) {
|
||||
(None, Some(bounding_box_overlays)) => bounding_box_overlays.delete(&mut buffer),
|
||||
(Some(bounds), paths) => {
|
||||
let mut bounding_box_overlays = paths.unwrap_or_else(|| BoundingBoxOverlays::new(&mut buffer));
|
||||
|
||||
data.bounding_box_overlay_layer = Some(path.clone());
|
||||
bounding_box_overlays.bounds = bounds;
|
||||
bounding_box_overlays.transform(&mut buffer);
|
||||
|
||||
let transform = transform_from_box(pos1, pos2);
|
||||
DocumentMessage::Overlays(Operation::SetLayerTransformInViewport { path, transform }.into()).into()
|
||||
data.bounding_box_overlays = Some(bounding_box_overlays);
|
||||
}
|
||||
(_, _) => Message::NoOp,
|
||||
(_, _) => {}
|
||||
};
|
||||
responses.push_front(response);
|
||||
buffer.into_iter().rev().for_each(|message| responses.push_front(message));
|
||||
self
|
||||
}
|
||||
|
|
@ -372,18 +575,62 @@ impl Fsm for SelectToolFsmState {
|
|||
data.drag_start = input.mouse.position;
|
||||
data.drag_current = input.mouse.position;
|
||||
let mut buffer = Vec::new();
|
||||
|
||||
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
|
||||
};
|
||||
|
||||
let rotating_bounds = if let Some(bounding_box) = &mut data.bounding_box_overlays {
|
||||
bounding_box.check_rotate(input.mouse.position)
|
||||
} else {
|
||||
false
|
||||
};
|
||||
|
||||
let mut selected: Vec<_> = document.selected_visible_layers().map(|path| path.to_vec()).collect();
|
||||
let quad = data.selection_quad();
|
||||
let mut intersection = document.graphene_document.intersects_quad_root(quad);
|
||||
// If the user is dragging the bounding box bounds, go into ResizingBounds mode.
|
||||
// If the user is dragging the rotate trigger, go into RotatingBounds mode.
|
||||
// If the user clicks on a layer that is in their current selection, go into the dragging mode.
|
||||
// If the user clicks on new shape, make that layer their new selection.
|
||||
// Otherwise enter the box select mode
|
||||
let state = if selected.iter().any(|path| intersection.contains(path)) {
|
||||
let state = 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.visible_layers().filter(|layer| !selected.iter().any(|path| path == layer)), snap_x, snap_y);
|
||||
|
||||
data.layers_dragging = selected;
|
||||
|
||||
ResizingBounds
|
||||
} else if rotating_bounds {
|
||||
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||
let selected = selected.iter().collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
||||
|
||||
*selected.pivot = selected.calculate_pivot();
|
||||
}
|
||||
|
||||
data.layers_dragging = selected;
|
||||
|
||||
RotatingBounds
|
||||
} else if selected.iter().any(|path| intersection.contains(path)) {
|
||||
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)));
|
||||
.start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true);
|
||||
|
||||
Dragging
|
||||
} else {
|
||||
|
|
@ -398,7 +645,7 @@ impl Fsm for SelectToolFsmState {
|
|||
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)));
|
||||
.start_snap(document, document.visible_layers().filter(|layer| !data.layers_dragging.iter().any(|path| path == layer)), true, true);
|
||||
|
||||
Dragging
|
||||
} else {
|
||||
|
|
@ -410,11 +657,11 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
state
|
||||
}
|
||||
(Dragging, MouseMove { snap_angle }) => {
|
||||
(Dragging, MouseMove { 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(snap_angle as usize) {
|
||||
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);
|
||||
|
|
@ -440,6 +687,49 @@ impl Fsm for SelectToolFsmState {
|
|||
data.drag_current = mouse_position + closest_move;
|
||||
Dragging
|
||||
}
|
||||
(ResizingBounds, MouseMove { .. }) => {
|
||||
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||
if let Some(movement) = &mut bounds.selected_edges {
|
||||
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 selected = data.layers_dragging.iter().collect::<Vec<_>>();
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut bounds.pivot, &selected, responses, &document.graphene_document);
|
||||
|
||||
selected.update_transforms(delta);
|
||||
}
|
||||
}
|
||||
ResizingBounds
|
||||
}
|
||||
(RotatingBounds, MouseMove { snap_angle, .. }) => {
|
||||
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||
let angle = {
|
||||
let start_offset = data.drag_start - bounds.pivot;
|
||||
let end_offset = input.mouse.position - bounds.pivot;
|
||||
|
||||
start_offset.angle_between(end_offset)
|
||||
};
|
||||
|
||||
let snapped_angle = if input.keyboard.get(snap_angle as usize) {
|
||||
let snap_resolution = ROTATE_SNAP_ANGLE.to_radians();
|
||||
(angle / snap_resolution).round() * snap_resolution
|
||||
} else {
|
||||
angle
|
||||
};
|
||||
|
||||
let delta = DAffine2::from_angle(snapped_angle);
|
||||
|
||||
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);
|
||||
|
||||
selected.update_transforms(delta);
|
||||
}
|
||||
|
||||
RotatingBounds
|
||||
}
|
||||
(DrawingBox, MouseMove { .. }) => {
|
||||
data.drag_current = input.mouse.position;
|
||||
|
||||
|
|
@ -455,6 +745,16 @@ impl Fsm for SelectToolFsmState {
|
|||
);
|
||||
DrawingBox
|
||||
}
|
||||
(Ready, MouseMove { .. }) => {
|
||||
let cursor = data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input));
|
||||
|
||||
if data.cursor != cursor {
|
||||
data.cursor = cursor;
|
||||
responses.push_back(FrontendMessage::UpdateMouseCursor { cursor }.into());
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
(Dragging, DragStop) => {
|
||||
let response = match input.mouse.position.distance(data.drag_start) < 10. * f64::EPSILON {
|
||||
true => DocumentMessage::Undo,
|
||||
|
|
@ -464,6 +764,22 @@ impl Fsm for SelectToolFsmState {
|
|||
responses.push_front(response.into());
|
||||
Ready
|
||||
}
|
||||
(ResizingBounds, DragStop) => {
|
||||
data.snap_handler.cleanup(responses);
|
||||
|
||||
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
(RotatingBounds, DragStop) => {
|
||||
if let Some(bounds) = &mut data.bounding_box_overlays {
|
||||
bounds.original_transforms.clear();
|
||||
}
|
||||
|
||||
Ready
|
||||
}
|
||||
(DrawingBox, DragStop) => {
|
||||
let quad = data.selection_quad();
|
||||
responses.push_front(
|
||||
|
|
@ -484,9 +800,13 @@ impl Fsm for SelectToolFsmState {
|
|||
Ready
|
||||
}
|
||||
(_, Abort) => {
|
||||
let mut delete = |path: &mut Option<Vec<LayerId>>| path.take().map(|path| responses.push_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into()));
|
||||
delete(&mut data.drag_box_overlay_layer);
|
||||
delete(&mut data.bounding_box_overlay_layer);
|
||||
if let Some(path) = data.drag_box_overlay_layer.take() {
|
||||
responses.push_front(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into())
|
||||
};
|
||||
if let Some(bounding_box_overlays) = data.bounding_box_overlays.take() {
|
||||
bounding_box_overlays.delete(responses);
|
||||
}
|
||||
|
||||
data.snap_handler.cleanup(responses);
|
||||
Ready
|
||||
}
|
||||
|
|
@ -624,6 +944,13 @@ impl Fsm for SelectToolFsmState {
|
|||
},
|
||||
])]),
|
||||
SelectToolFsmState::DrawingBox => HintData(vec![]),
|
||||
SelectToolFsmState::ResizingBounds => HintData(vec![]),
|
||||
SelectToolFsmState::RotatingBounds => HintData(vec![HintGroup(vec![HintInfo {
|
||||
key_groups: vec![KeysGroup(vec![Key::KeyControl])],
|
||||
mouse: None,
|
||||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
}])]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
|
|
|
|||
|
|
@ -18,7 +18,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<Message>, viewport_bounds: DVec2, document: &DocumentMessageHandler, mouse_position: DVec2) {
|
||||
self.snap_handler.start_snap(document, document.visible_layers());
|
||||
self.snap_handler.start_snap(document, document.visible_layers(), true, true);
|
||||
self.drag_start = self.snap_handler.snap_position(responses, viewport_bounds, document, mouse_position);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -199,7 +199,7 @@ export class UpdateDocumentRulers extends JsMessage {
|
|||
readonly interval!: number;
|
||||
}
|
||||
|
||||
export type MouseCursorIcon = "default" | "zoom-in" | "zoom-out" | "grabbing" | "crosshair" | "text";
|
||||
export type MouseCursorIcon = "default" | "zoom-in" | "zoom-out" | "grabbing" | "crosshair" | "text" | "ns-resize" | "ew-resize" | "nesw-resize" | "nwse-resize";
|
||||
|
||||
const ToCssCursorProperty = Transform(({ value }) => {
|
||||
const cssNames: Record<string, MouseCursorIcon> = {
|
||||
|
|
@ -208,6 +208,10 @@ const ToCssCursorProperty = Transform(({ value }) => {
|
|||
Grabbing: "grabbing",
|
||||
Crosshair: "crosshair",
|
||||
Text: "text",
|
||||
NSResize: "ns-resize",
|
||||
EWResize: "ew-resize",
|
||||
NESWResize: "nesw-resize",
|
||||
NWSEResize: "nwse-resize",
|
||||
};
|
||||
|
||||
return cssNames[value] || "default";
|
||||
|
|
|
|||
Loading…
Reference in New Issue