Add a pivot widget to selected layer(s) to control the origin(s) (#772)
* Add pivot * Add dragging pivot * Cleanup * Remove tabs * Fix multiplication order * Restyle pivot * Add move cursor icon * Update pivot size * Code review tweaks * Fix alt with non-centred pivot * Comment for add one * Pivot sets layer panel origin * Tweek alt centre thing * Fix division by zero case * Add pivot dots to properties * FIx some typos Co-authored-by: Keavon Chambers <keavon@keavon.com>
This commit is contained in:
parent
0f6f3be6e7
commit
1e109dc552
|
|
@ -43,6 +43,9 @@ pub const BIG_NUDGE_AMOUNT: f64 = 10.;
|
|||
// Select tool
|
||||
pub const SELECTION_TOLERANCE: f64 = 5.;
|
||||
pub const SELECTION_DRAG_ANGLE: f64 = 90.;
|
||||
pub const PIVOT_OUTER_OUTLINE_THICKNESS: f64 = 1.;
|
||||
pub const PIVOT_OUTER: f64 = 9.;
|
||||
pub const PIVOT_INNER: f64 = 3.;
|
||||
|
||||
// Transformation cage
|
||||
pub const BOUNDS_SELECT_THRESHOLD: f64 = 10.;
|
||||
|
|
|
|||
|
|
@ -25,6 +25,7 @@ pub enum MouseCursorIcon {
|
|||
Grabbing,
|
||||
Crosshair,
|
||||
Text,
|
||||
Move,
|
||||
NSResize,
|
||||
EWResize,
|
||||
NESWResize,
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use derivative::*;
|
||||
use glam::DVec2;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use crate::messages::layout::utility_types::layout_widget::WidgetCallback;
|
||||
|
|
@ -14,7 +15,7 @@ pub struct PivotAssist {
|
|||
pub on_update: WidgetCallback<PivotAssist>,
|
||||
}
|
||||
|
||||
#[derive(Clone, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
||||
#[derive(Clone, Copy, Serialize, Deserialize, Debug, Default, PartialEq, Eq)]
|
||||
pub enum PivotPosition {
|
||||
#[default]
|
||||
None,
|
||||
|
|
@ -46,3 +47,52 @@ impl From<&str> for PivotPosition {
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<PivotPosition> for Option<DVec2> {
|
||||
fn from(input: PivotPosition) -> Self {
|
||||
match input {
|
||||
PivotPosition::None => None,
|
||||
PivotPosition::TopLeft => Some(DVec2::new(0., 0.)),
|
||||
PivotPosition::TopCenter => Some(DVec2::new(0.5, 0.)),
|
||||
PivotPosition::TopRight => Some(DVec2::new(1., 0.)),
|
||||
PivotPosition::CenterLeft => Some(DVec2::new(0., 0.5)),
|
||||
PivotPosition::Center => Some(DVec2::new(0.5, 0.5)),
|
||||
PivotPosition::CenterRight => Some(DVec2::new(1., 0.5)),
|
||||
PivotPosition::BottomLeft => Some(DVec2::new(0., 1.)),
|
||||
PivotPosition::BottomCenter => Some(DVec2::new(0.5, 1.)),
|
||||
PivotPosition::BottomRight => Some(DVec2::new(1., 1.)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<DVec2> for PivotPosition {
|
||||
fn from(input: DVec2) -> Self {
|
||||
const TOLERANCE: f64 = 1e-5_f64;
|
||||
if input.y.abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return PivotPosition::TopLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return PivotPosition::TopCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return PivotPosition::TopRight;
|
||||
}
|
||||
} else if (input.y - 0.5).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return PivotPosition::CenterLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return PivotPosition::Center;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return PivotPosition::CenterRight;
|
||||
}
|
||||
} else if (input.y - 1.).abs() < TOLERANCE {
|
||||
if input.x.abs() < TOLERANCE {
|
||||
return PivotPosition::BottomLeft;
|
||||
} else if (input.x - 0.5).abs() < TOLERANCE {
|
||||
return PivotPosition::BottomCenter;
|
||||
} else if (input.x - 1.).abs() < TOLERANCE {
|
||||
return PivotPosition::BottomRight;
|
||||
}
|
||||
}
|
||||
PivotPosition::None
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,4 +1,5 @@
|
|||
use super::utility_types::TransformOp;
|
||||
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotPosition;
|
||||
use crate::messages::portfolio::document::utility_types::misc::TargetDocument;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
|
|
@ -25,5 +26,6 @@ pub enum PropertiesPanelMessage {
|
|||
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||
ResendActiveProperties,
|
||||
SetActiveLayers { paths: Vec<Vec<LayerId>>, document: TargetDocument },
|
||||
SetPivot { new_position: PivotPosition },
|
||||
UpdateSelectedDocumentProperties,
|
||||
}
|
||||
|
|
|
|||
|
|
@ -101,6 +101,13 @@ impl<'a> MessageHandler<PropertiesPanelMessage, PropertiesPanelMessageHandlerDat
|
|||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
responses.push_back(Operation::SetTextContent { path, new_text }.into())
|
||||
}
|
||||
SetPivot { new_position } => {
|
||||
let (layer_path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||
let position: Option<glam::DVec2> = new_position.into();
|
||||
let pivot = position.unwrap().into();
|
||||
|
||||
responses.push_back(Operation::SetPivot { layer_path, pivot }.into());
|
||||
}
|
||||
CheckSelectedWasUpdated { path } => {
|
||||
if self.matches_selected(&path) {
|
||||
responses.push_back(PropertiesPanelMessage::ResendActiveProperties.into())
|
||||
|
|
|
|||
|
|
@ -1,6 +1,7 @@
|
|||
use super::utility_types::TransformOp;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist;
|
||||
use crate::messages::layout::utility_types::widgets::button_widgets::PopoverButton;
|
||||
use crate::messages::layout::utility_types::widgets::input_widgets::{ColorInput, FontInput, NumberInput, RadioEntryData, RadioInput, TextAreaInput, TextInput};
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, IconStyle, Separator, SeparatorDirection, SeparatorType, TextLabel};
|
||||
|
|
@ -80,6 +81,7 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
|||
} else {
|
||||
panic!("Artboard must have a solid fill")
|
||||
};
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(font_cache));
|
||||
|
||||
vec![LayoutGroup::Section {
|
||||
name: "Artboard".into(),
|
||||
|
|
@ -95,12 +97,12 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.transform.x()),
|
||||
value: Some(layer.transform.x() + pivot.x),
|
||||
label: "X".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
value: number_input.value.unwrap() - pivot.x,
|
||||
transform_op: TransformOp::X,
|
||||
}
|
||||
.into()
|
||||
|
|
@ -112,12 +114,12 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.transform.y()),
|
||||
value: Some(layer.transform.y() + pivot.y),
|
||||
label: "Y".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
value: number_input.value.unwrap() - pivot.y,
|
||||
transform_op: TransformOp::Y,
|
||||
}
|
||||
.into()
|
||||
|
|
@ -308,6 +310,7 @@ pub fn register_artwork_layer_properties(layer: &Layer, responses: &mut VecDeque
|
|||
}
|
||||
|
||||
fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup {
|
||||
let pivot = layer.transform.transform_vector2(layer.layerspace_pivot(font_cache));
|
||||
LayoutGroup::Section {
|
||||
name: "Transform".into(),
|
||||
layout: vec![
|
||||
|
|
@ -317,17 +320,25 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
|
|||
value: "Location".into(),
|
||||
..TextLabel::default()
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Related,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::PivotAssist(PivotAssist {
|
||||
position: layer.pivot.into(),
|
||||
on_update: WidgetCallback::new(|pivot_assist: &PivotAssist| PropertiesPanelMessage::SetPivot { new_position: pivot_assist.position }.into()),
|
||||
})),
|
||||
WidgetHolder::new(Widget::Separator(Separator {
|
||||
separator_type: SeparatorType::Unrelated,
|
||||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.transform.x()),
|
||||
value: Some(layer.transform.x() + pivot.x),
|
||||
label: "X".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
value: number_input.value.unwrap() - pivot.x,
|
||||
transform_op: TransformOp::X,
|
||||
}
|
||||
.into()
|
||||
|
|
@ -339,12 +350,12 @@ fn node_section_transform(layer: &Layer, font_cache: &FontCache) -> LayoutGroup
|
|||
direction: SeparatorDirection::Horizontal,
|
||||
})),
|
||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||
value: Some(layer.transform.y()),
|
||||
value: Some(layer.transform.y() + pivot.y),
|
||||
label: "Y".into(),
|
||||
unit: " px".into(),
|
||||
on_update: WidgetCallback::new(|number_input: &NumberInput| {
|
||||
on_update: WidgetCallback::new(move |number_input: &NumberInput| {
|
||||
PropertiesPanelMessage::ModifyTransform {
|
||||
value: number_input.value.unwrap(),
|
||||
value: number_input.value.unwrap() - pivot.y,
|
||||
transform_op: TransformOp::Y,
|
||||
}
|
||||
.into()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
pub mod overlay_renderer;
|
||||
pub mod path_outline;
|
||||
pub mod pivot;
|
||||
pub mod resize;
|
||||
pub mod shape_editor;
|
||||
pub mod snapping;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,177 @@
|
|||
//! Handler for the pivot overlay visible on the selected layer(s) whilst using the Select tool which controls the center of rotation/scale and origin of the layer.
|
||||
|
||||
use crate::application::generate_uuid;
|
||||
use crate::consts::{COLOR_ACCENT, PIVOT_INNER, PIVOT_OUTER, PIVOT_OUTER_OUTLINE_THICKNESS};
|
||||
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotPosition;
|
||||
use crate::messages::prelude::*;
|
||||
|
||||
use graphene::layers::style;
|
||||
use graphene::layers::text_layer::FontCache;
|
||||
use graphene::{LayerId, Operation};
|
||||
|
||||
use glam::{DAffine2, DVec2};
|
||||
use std::collections::VecDeque;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct Pivot {
|
||||
/// Pivot between (0,0) and (1,1)
|
||||
normalized_pivot: DVec2,
|
||||
/// Transform to get from normalized pivot to viewspace
|
||||
transform_from_normalized: DAffine2,
|
||||
/// The viewspace pivot position (if applicable)
|
||||
pivot: Option<DVec2>,
|
||||
/// A reference to the previous overlays so we can destroy them
|
||||
pivot_overlay_circles: Option<[Vec<LayerId>; 2]>,
|
||||
/// The old pivot position in the GUI, used to reduce refreshes of the document bar
|
||||
old_pivot_position: PivotPosition,
|
||||
}
|
||||
|
||||
impl Default for Pivot {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
normalized_pivot: DVec2::splat(0.5),
|
||||
transform_from_normalized: Default::default(),
|
||||
pivot: Default::default(),
|
||||
pivot_overlay_circles: Default::default(),
|
||||
old_pivot_position: PivotPosition::Center,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Pivot {
|
||||
/// Calculates the transform that gets from normalized pivot to viewspace.
|
||||
fn get_layer_pivot_transform(layer_path: &[LayerId], layer: &graphene::layers::layer_info::Layer, document: &DocumentMessageHandler, font_cache: &FontCache) -> DAffine2 {
|
||||
let [min, max] = layer.aabb_for_transform(DAffine2::IDENTITY, font_cache).unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
||||
let bounds_transform = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
|
||||
let layer_transform = document.graphene_document.multiply_transforms(layer_path).unwrap_or(DAffine2::IDENTITY);
|
||||
layer_transform * bounds_transform
|
||||
}
|
||||
|
||||
/// Recomputes the pivot position and transform.
|
||||
fn recalculate_pivot(&mut self, document: &DocumentMessageHandler, font_cache: &FontCache) {
|
||||
let mut layers = document.selected_visible_layers();
|
||||
if let Some(first) = layers.next() {
|
||||
// Add one because the first item is consumed above.
|
||||
let selected_layers_count = layers.count() + 1;
|
||||
|
||||
// If just one layer is selected we can use its inner transform
|
||||
if selected_layers_count == 1 {
|
||||
if let Ok(layer) = document.graphene_document.layer(first) {
|
||||
self.normalized_pivot = layer.pivot;
|
||||
self.transform_from_normalized = Self::get_layer_pivot_transform(first, layer, document, font_cache);
|
||||
self.pivot = Some(self.transform_from_normalized.transform_point2(layer.pivot));
|
||||
}
|
||||
} else {
|
||||
// If more than one layer is selected we use the AABB with the mean of the pivots
|
||||
let xy_summation = document
|
||||
.selected_visible_layers()
|
||||
.filter_map(|path| document.graphene_document.pivot(path, font_cache))
|
||||
.reduce(|a, b| a + b)
|
||||
.unwrap_or_default();
|
||||
|
||||
let pivot = xy_summation / selected_layers_count as f64;
|
||||
self.pivot = Some(pivot);
|
||||
let [min, max] = document.selected_visible_layers_bounding_box(font_cache).unwrap_or([DVec2::ZERO, DVec2::ONE]);
|
||||
self.normalized_pivot = (pivot - min) / (max - min);
|
||||
|
||||
self.transform_from_normalized = DAffine2::from_translation(min) * DAffine2::from_scale(max - min);
|
||||
}
|
||||
} else {
|
||||
// If no layers are selected then we revert things back to default
|
||||
self.normalized_pivot = DVec2::splat(0.5);
|
||||
self.pivot = None;
|
||||
}
|
||||
}
|
||||
|
||||
pub fn clear_overlays(&mut self, responses: &mut VecDeque<Message>) {
|
||||
if let Some(overlays) = self.pivot_overlay_circles.take() {
|
||||
for path in overlays {
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::DeleteLayer { path }.into()).into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn redraw_pivot(&mut self, responses: &mut VecDeque<Message>) {
|
||||
self.clear_overlays(responses);
|
||||
|
||||
let pivot = match self.pivot {
|
||||
Some(pivot) => pivot,
|
||||
None => return,
|
||||
};
|
||||
|
||||
let layer_paths = [vec![generate_uuid()], vec![generate_uuid()]];
|
||||
responses.push_back(
|
||||
DocumentMessage::Overlays(
|
||||
Operation::AddEllipse {
|
||||
path: layer_paths[0].clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
style: style::PathStyle::new(Some(style::Stroke::new(COLOR_ACCENT, PIVOT_OUTER_OUTLINE_THICKNESS)), style::Fill::Solid(graphene::color::Color::WHITE)),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
responses.push_back(
|
||||
DocumentMessage::Overlays(
|
||||
Operation::AddEllipse {
|
||||
path: layer_paths[1].clone(),
|
||||
transform: DAffine2::IDENTITY.to_cols_array(),
|
||||
style: style::PathStyle::new(None, style::Fill::Solid(COLOR_ACCENT)),
|
||||
insert_index: -1,
|
||||
}
|
||||
.into(),
|
||||
)
|
||||
.into(),
|
||||
);
|
||||
|
||||
self.pivot_overlay_circles = Some(layer_paths.clone());
|
||||
let [outer, inner] = layer_paths;
|
||||
|
||||
let pivot_diameter_without_outline = PIVOT_OUTER - PIVOT_OUTER_OUTLINE_THICKNESS;
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(pivot_diameter_without_outline), 0., pivot - DVec2::splat(pivot_diameter_without_outline / 2.)).to_cols_array();
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: outer, transform }.into()).into());
|
||||
|
||||
let transform = DAffine2::from_scale_angle_translation(DVec2::splat(PIVOT_INNER), 0., pivot - DVec2::splat(PIVOT_INNER / 2.)).to_cols_array();
|
||||
responses.push_back(DocumentMessage::Overlays(Operation::TransformLayerInViewport { path: inner, transform }.into()).into());
|
||||
}
|
||||
|
||||
pub fn update_pivot(&mut self, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
||||
self.recalculate_pivot(document, font_cache);
|
||||
self.redraw_pivot(responses);
|
||||
}
|
||||
|
||||
/// Answers if the pivot widget has changed (so we should refresh the tool bar at the top of the canvas).
|
||||
pub fn should_refresh_pivot_position(&mut self) -> bool {
|
||||
let new = self.to_pivot_position();
|
||||
let should_refresh = new != self.old_pivot_position;
|
||||
self.old_pivot_position = new;
|
||||
should_refresh
|
||||
}
|
||||
|
||||
pub fn to_pivot_position(&self) -> PivotPosition {
|
||||
self.normalized_pivot.into()
|
||||
}
|
||||
|
||||
/// Sets the viewport position of the pivot for all selected layers.
|
||||
pub fn set_viewport_position(&self, position: DVec2, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
||||
for layer_path in document.selected_visible_layers() {
|
||||
if let Ok(layer) = document.graphene_document.layer(layer_path) {
|
||||
let transform = Self::get_layer_pivot_transform(layer_path, layer, document, font_cache);
|
||||
let pivot = transform.inverse().transform_point2(position).into();
|
||||
let layer_path = layer_path.to_owned();
|
||||
responses.push_back(Operation::SetPivot { layer_path, pivot }.into());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the pivot using the normalized transform that is set above.
|
||||
pub fn set_normalized_position(&self, position: DVec2, document: &DocumentMessageHandler, font_cache: &FontCache, responses: &mut VecDeque<Message>) {
|
||||
self.set_viewport_position(self.transform_from_normalized.transform_point2(position), document, font_cache, responses);
|
||||
}
|
||||
|
||||
/// Answers if the pointer is currently positioned over the pivot.
|
||||
pub fn is_over(&self, mouse: DVec2) -> bool {
|
||||
self.pivot.filter(|&pivot| mouse.distance_squared(pivot) < (PIVOT_OUTER / 2.).powi(2)).is_some()
|
||||
}
|
||||
}
|
||||
|
|
@ -81,19 +81,33 @@ impl SelectedEdges {
|
|||
|
||||
let mut pivot = self.pivot_from_bounds(min, max);
|
||||
if center {
|
||||
// The below ratio is: `dragging edge / being centred`.
|
||||
// The `is_finite()` checks are in case the user is dragging the edge where the pivot is located (in which case the centering mode is ignored).
|
||||
if self.top {
|
||||
max.y = center_around.y * 2. - min.y;
|
||||
pivot.y = center_around.y;
|
||||
let ratio = (center_around.y - min.y) / (center_around.y - self.bounds[0].y);
|
||||
if ratio.is_finite() {
|
||||
max.y = center_around.y + ratio * (self.bounds[1].y - center_around.y);
|
||||
pivot.y = center_around.y;
|
||||
}
|
||||
} else if self.bottom {
|
||||
min.y = center_around.y * 2. - max.y;
|
||||
pivot.y = center_around.y;
|
||||
let ratio = (max.y - center_around.y) / (self.bounds[1].y - center_around.y);
|
||||
if ratio.is_finite() {
|
||||
min.y = center_around.y - ratio * (center_around.y - self.bounds[0].y);
|
||||
pivot.y = center_around.y;
|
||||
}
|
||||
}
|
||||
if self.left {
|
||||
max.x = center_around.x * 2. - min.x;
|
||||
pivot.x = center_around.x;
|
||||
let ratio = (center_around.x - min.x) / (center_around.x - self.bounds[0].x);
|
||||
if ratio.is_finite() {
|
||||
max.x = center_around.x + ratio * (self.bounds[1].x - center_around.x);
|
||||
pivot.x = center_around.x;
|
||||
}
|
||||
} else if self.right {
|
||||
min.x = center_around.x * 2. - max.x;
|
||||
pivot.x = center_around.x;
|
||||
let ratio = (max.x - center_around.x) / (self.bounds[1].x - center_around.x);
|
||||
if ratio.is_finite() {
|
||||
min.x = center_around.x - ratio * (center_around.x - self.bounds[0].x);
|
||||
pivot.x = center_around.x;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -115,8 +129,16 @@ impl SelectedEdges {
|
|||
}
|
||||
|
||||
/// Calculates the required scaling to resize the bounding box
|
||||
pub fn bounds_to_scale_transform(&self, size: DVec2) -> DAffine2 {
|
||||
DAffine2::from_scale(size / (self.bounds[1] - self.bounds[0]))
|
||||
pub fn bounds_to_scale_transform(&self, position: DVec2, size: DVec2) -> (DAffine2, DVec2) {
|
||||
let enlargement_factor = size / (self.bounds[1] - self.bounds[0]);
|
||||
let mut pivot = (self.bounds[0] * enlargement_factor - position) / (enlargement_factor - DVec2::splat(1.));
|
||||
if pivot.x.is_nan() {
|
||||
pivot.x = 0.;
|
||||
}
|
||||
if pivot.y.is_nan() {
|
||||
pivot.y = 0.;
|
||||
}
|
||||
(DAffine2::from_scale(enlargement_factor), pivot)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,6 +4,7 @@ use crate::messages::frontend::utility_types::MouseCursorIcon;
|
|||
use crate::messages::input_mapper::utility_types::input_keyboard::{Key, KeysGroup, MouseMotion};
|
||||
use crate::messages::input_mapper::utility_types::input_mouse::ViewportPosition;
|
||||
use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, PropertyHolder, Widget, WidgetCallback, WidgetHolder, WidgetLayout};
|
||||
use crate::messages::layout::utility_types::misc::LayoutTarget;
|
||||
use crate::messages::layout::utility_types::widgets::assist_widgets::{PivotAssist, PivotPosition};
|
||||
use crate::messages::layout::utility_types::widgets::button_widgets::{IconButton, PopoverButton};
|
||||
use crate::messages::layout::utility_types::widgets::label_widgets::{Separator, SeparatorDirection, SeparatorType};
|
||||
|
|
@ -11,6 +12,7 @@ use crate::messages::portfolio::document::utility_types::misc::{AlignAggregate,
|
|||
use crate::messages::portfolio::document::utility_types::transformation::Selected;
|
||||
use crate::messages::prelude::*;
|
||||
use crate::messages::tool::common_functionality::path_outline::*;
|
||||
use crate::messages::tool::common_functionality::pivot::Pivot;
|
||||
use crate::messages::tool::common_functionality::snapping::{self, SnapManager};
|
||||
use crate::messages::tool::common_functionality::transformation_cage::*;
|
||||
use crate::messages::tool::utility_types::{EventToMessageMap, Fsm, ToolActionHandlerData, ToolMetadata, ToolTransition, ToolType};
|
||||
|
|
@ -34,7 +36,7 @@ pub struct SelectTool {
|
|||
|
||||
#[remain::sorted]
|
||||
#[impl_message(Message, ToolMessage, Select)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Hash, Serialize, Deserialize)]
|
||||
#[derive(PartialEq, Eq, Clone, Debug, Serialize, Deserialize)]
|
||||
pub enum SelectToolMessage {
|
||||
// Standard messages
|
||||
#[remain::unsorted]
|
||||
|
|
@ -62,6 +64,9 @@ pub enum SelectToolMessage {
|
|||
center: Key,
|
||||
duplicate: Key,
|
||||
},
|
||||
SetPivot {
|
||||
position: PivotPosition,
|
||||
},
|
||||
}
|
||||
|
||||
impl ToolMetadata for SelectTool {
|
||||
|
|
@ -252,12 +257,8 @@ impl PropertyHolder for SelectTool {
|
|||
})),
|
||||
// We'd like this widget to hide and show itself whenever the transformation cage is active or inactive (i.e. when no layers are selected)
|
||||
WidgetHolder::new(Widget::PivotAssist(PivotAssist {
|
||||
position: PivotPosition::Center,
|
||||
on_update: WidgetCallback::new(|pivot_assist: &PivotAssist| {
|
||||
// TODO: Make this actually do something
|
||||
log::debug!("Changed pivot to {:?}", pivot_assist.position);
|
||||
Message::NoOp
|
||||
}),
|
||||
position: self.tool_data.pivot.to_pivot_position(),
|
||||
on_update: WidgetCallback::new(|pivot_assist: &PivotAssist| SelectToolMessage::SetPivot { position: pivot_assist.position }.into()),
|
||||
})),
|
||||
],
|
||||
}]))
|
||||
|
|
@ -278,6 +279,11 @@ impl<'a> MessageHandler<ToolMessage, ToolActionHandlerData<'a>> for SelectTool {
|
|||
|
||||
let new_state = self.fsm_state.transition(message, &mut self.tool_data, tool_data, &(), responses);
|
||||
|
||||
if self.tool_data.pivot.should_refresh_pivot_position() {
|
||||
// Notify the frontend about the updated pivot position (a bit ugly to do it here not in the fsm but that doesn't have SelectTool)
|
||||
self.register_properties(responses, LayoutTarget::ToolOptions);
|
||||
}
|
||||
|
||||
if self.fsm_state != new_state {
|
||||
self.fsm_state = new_state;
|
||||
self.fsm_state.update_hints(responses);
|
||||
|
|
@ -321,6 +327,7 @@ enum SelectToolFsmState {
|
|||
DrawingBox,
|
||||
ResizingBounds,
|
||||
RotatingBounds,
|
||||
DraggingPivot,
|
||||
}
|
||||
|
||||
impl Default for SelectToolFsmState {
|
||||
|
|
@ -340,6 +347,7 @@ struct SelectToolData {
|
|||
bounding_box_overlays: Option<BoundingBoxOverlays>,
|
||||
snap_manager: SnapManager,
|
||||
cursor: MouseCursorIcon,
|
||||
pivot: Pivot,
|
||||
}
|
||||
|
||||
impl SelectToolData {
|
||||
|
|
@ -393,6 +401,7 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
tool_data.path_outlines.update_selected(document.selected_visible_layers(), document, responses, font_cache);
|
||||
tool_data.path_outlines.intersect_test_hovered(input, document, responses, font_cache);
|
||||
tool_data.pivot.update_pivot(document, font_cache, responses);
|
||||
|
||||
self
|
||||
}
|
||||
|
|
@ -455,7 +464,12 @@ impl Fsm for SelectToolFsmState {
|
|||
// 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 let Some(selected_edges) = dragging_bounds {
|
||||
let state = if tool_data.pivot.is_over(input.mouse.position) {
|
||||
tool_data.snap_manager.start_snap(document, document.bounding_boxes(None, None, font_cache), true, true);
|
||||
tool_data.snap_manager.add_all_document_handles(document, &[], &[], &[]);
|
||||
|
||||
DraggingPivot
|
||||
} else 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;
|
||||
|
||||
|
|
@ -565,12 +579,11 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
|
||||
let (_, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, bounds.center_of_transformation, axis_align);
|
||||
let delta = movement.bounds_to_scale_transform(size);
|
||||
let (position, size) = movement.new_size(snapped_mouse_position, bounds.transform, center, bounds.center_of_transformation, axis_align);
|
||||
let (delta, mut pivot) = movement.bounds_to_scale_transform(position, size);
|
||||
|
||||
let selected = &tool_data.layers_dragging.iter().collect::<Vec<_>>();
|
||||
let pivot = if center { &mut bounds.center_of_transformation } else { &mut bounds.opposite_pivot };
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, pivot, selected, responses, &document.graphene_document);
|
||||
let mut selected = Selected::new(&mut bounds.original_transforms, &mut pivot, selected, responses, &document.graphene_document);
|
||||
|
||||
selected.update_transforms(delta);
|
||||
}
|
||||
|
|
@ -603,6 +616,13 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
RotatingBounds
|
||||
}
|
||||
(DraggingPivot, PointerMove { .. }) => {
|
||||
let mouse_position = input.mouse.position;
|
||||
let snapped_mouse_position = tool_data.snap_manager.snap_position(responses, document, mouse_position);
|
||||
tool_data.pivot.set_viewport_position(snapped_mouse_position, document, font_cache, responses);
|
||||
|
||||
DraggingPivot
|
||||
}
|
||||
(DrawingBox, PointerMove { .. }) => {
|
||||
tool_data.drag_current = input.mouse.position;
|
||||
|
||||
|
|
@ -619,7 +639,12 @@ impl Fsm for SelectToolFsmState {
|
|||
DrawingBox
|
||||
}
|
||||
(Ready, PointerMove { .. }) => {
|
||||
let cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
||||
let mut cursor = tool_data.bounding_box_overlays.as_ref().map_or(MouseCursorIcon::Default, |bounds| bounds.get_cursor(input, true));
|
||||
|
||||
// Dragging the pivot overrules the other operations
|
||||
if tool_data.pivot.is_over(input.mouse.position) {
|
||||
cursor = MouseCursorIcon::Move;
|
||||
}
|
||||
|
||||
// Generate the select outline (but not if the user is going to use the bound overlays)
|
||||
if cursor == MouseCursorIcon::Default {
|
||||
|
|
@ -660,6 +685,11 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
Ready
|
||||
}
|
||||
(DraggingPivot, DragStop) => {
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
(DrawingBox, DragStop) => {
|
||||
let quad = tool_data.selection_quad();
|
||||
responses.push_front(
|
||||
|
|
@ -684,6 +714,7 @@ impl Fsm for SelectToolFsmState {
|
|||
responses.push_back(DocumentMessage::Undo.into());
|
||||
|
||||
tool_data.path_outlines.clear_selected(responses);
|
||||
tool_data.pivot.clear_overlays(responses);
|
||||
|
||||
Ready
|
||||
}
|
||||
|
|
@ -708,6 +739,7 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
tool_data.path_outlines.clear_hovered(responses);
|
||||
tool_data.path_outlines.clear_selected(responses);
|
||||
tool_data.pivot.clear_overlays(responses);
|
||||
|
||||
tool_data.snap_manager.cleanup(responses);
|
||||
Ready
|
||||
|
|
@ -727,6 +759,12 @@ impl Fsm for SelectToolFsmState {
|
|||
|
||||
self
|
||||
}
|
||||
(_, SetPivot { position }) => {
|
||||
let pos: Option<DVec2> = position.into();
|
||||
tool_data.pivot.set_normalized_position(pos.unwrap(), document, font_cache, responses);
|
||||
|
||||
self
|
||||
}
|
||||
_ => self,
|
||||
}
|
||||
} else {
|
||||
|
|
@ -869,6 +907,7 @@ impl Fsm for SelectToolFsmState {
|
|||
label: String::from("Snap 15°"),
|
||||
plus: false,
|
||||
}])]),
|
||||
SelectToolFsmState::DraggingPivot => HintData(vec![]),
|
||||
};
|
||||
|
||||
responses.push_back(FrontendMessage::UpdateInputHints { hint_data }.into());
|
||||
|
|
|
|||
|
|
@ -195,6 +195,7 @@ const mouseCursorIconCSSNames = {
|
|||
Grabbing: "grabbing",
|
||||
Crosshair: "crosshair",
|
||||
Text: "text",
|
||||
Move: "move",
|
||||
NSResize: "ns-resize",
|
||||
EWResize: "ew-resize",
|
||||
NESWResize: "nesw-resize",
|
||||
|
|
|
|||
|
|
@ -760,7 +760,12 @@ impl Document {
|
|||
self.mark_as_dirty(&path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: path.clone() }], update_thumbnails_upstream(&path)].concat())
|
||||
}
|
||||
|
||||
Operation::SetPivot { layer_path, pivot } => {
|
||||
let layer = self.layer_mut(&layer_path).expect("Setting pivot for invalid layer");
|
||||
layer.pivot = pivot.into();
|
||||
self.mark_as_dirty(&layer_path)?;
|
||||
Some([vec![DocumentChanged, LayerChanged { path: layer_path.clone() }], update_thumbnails_upstream(&layer_path)].concat())
|
||||
}
|
||||
Operation::SetLayerTransformInViewport { path, transform } => {
|
||||
let transform = DAffine2::from_cols_array(&transform);
|
||||
self.set_transform_relative_to_viewport(&path, transform)?;
|
||||
|
|
|
|||
|
|
@ -56,6 +56,10 @@ pub enum Operation {
|
|||
blob_url: String,
|
||||
dimensions: (f64, f64),
|
||||
},
|
||||
SetPivot {
|
||||
layer_path: Vec<LayerId>,
|
||||
pivot: (f64, f64),
|
||||
},
|
||||
SetTextEditability {
|
||||
path: Vec<LayerId>,
|
||||
editable: bool,
|
||||
|
|
|
|||
Loading…
Reference in New Issue