From 750ef06cba0490ed3e2845648dbed1af9041cfd1 Mon Sep 17 00:00:00 2001 From: 0HyperCube <78500760+0HyperCube@users.noreply.github.com> Date: Wed, 28 Dec 2022 23:21:03 +0000 Subject: [PATCH] Add a 'Preserve Aspect Ratio' checkbox to Properties panel (#923) * Add the link button * Transform around pivot * Remove log * Fix tests * Add a hacky two-line layout for the checkbox Co-authored-by: Keavon Chambers --- document-legacy/src/document.rs | 6 +++ document-legacy/src/layers/layer_info.rs | 5 ++ document-legacy/src/operation.rs | 4 ++ .../properties_panel_message.rs | 1 + .../properties_panel_message_handler.rs | 4 ++ .../properties_panel/utility_functions.rs | 47 +++++++++++++++---- frontend/src/components/widgets/WidgetRow.vue | 26 +++++++++- .../widgets/groups/WidgetSection.vue | 1 + 8 files changed, 85 insertions(+), 9 deletions(-) diff --git a/document-legacy/src/document.rs b/document-legacy/src/document.rs index 2170df05..7749ccd7 100644 --- a/document-legacy/src/document.rs +++ b/document-legacy/src/document.rs @@ -628,6 +628,12 @@ impl Document { } Some(vec![LayerChanged { path: layer_path.clone() }]) } + Operation::SetLayerPreserveAspect { layer_path, preserve_aspect } => { + if let Ok(layer) = self.layer_mut(&layer_path) { + layer.preserve_aspect = preserve_aspect; + } + Some(vec![LayerChanged { path: layer_path.clone() }]) + } Operation::SetTextEditability { path, editable } => { self.layer_mut(&path)?.as_text_mut()?.editable = editable; self.mark_as_dirty(&path)?; diff --git a/document-legacy/src/layers/layer_info.rs b/document-legacy/src/layers/layer_info.rs index c4f731d3..51a81a47 100644 --- a/document-legacy/src/layers/layer_info.rs +++ b/document-legacy/src/layers/layer_info.rs @@ -227,6 +227,9 @@ pub struct Layer { /// A transformation applied to the layer (translation, rotation, scaling, and shear). #[serde(with = "DAffine2Ref")] pub transform: glam::DAffine2, + /// Should the aspect ratio of this layer be preserved? + #[serde(default = "return_true")] + pub preserve_aspect: bool, /// The center of transformations like rotation or scaling with the shift key. /// This is in local space (so the layer's transform should be applied). pub pivot: DVec2, @@ -255,6 +258,7 @@ impl Layer { name: None, data, transform: glam::DAffine2::from_cols_array(&transform), + preserve_aspect: true, pivot: DVec2::splat(0.5), cache: String::new(), thumbnail_cache: String::new(), @@ -515,6 +519,7 @@ impl Clone for Layer { name: self.name.clone(), data: self.data.clone(), transform: self.transform, + preserve_aspect: self.preserve_aspect, pivot: self.pivot, cache: String::new(), thumbnail_cache: String::new(), diff --git a/document-legacy/src/operation.rs b/document-legacy/src/operation.rs index 172acfb0..df6a996e 100644 --- a/document-legacy/src/operation.rs +++ b/document-legacy/src/operation.rs @@ -245,6 +245,10 @@ pub enum Operation { path: Vec, name: String, }, + SetLayerPreserveAspect { + layer_path: Vec, + preserve_aspect: bool, + }, SetLayerBlendMode { path: Vec, blend_mode: BlendMode, diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs index 3e9ee5f6..03c49485 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message.rs @@ -21,6 +21,7 @@ pub enum PropertiesPanelMessage { ModifyFill { fill: Fill }, ModifyFont { font_family: String, font_style: String, size: f64 }, ModifyName { name: String }, + ModifyPreserveAspect { preserve_aspect: bool }, ModifyStroke { stroke: Stroke }, ModifyText { new_text: String }, ModifyTransform { value: f64, transform_op: TransformOp }, diff --git a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs index 94bbaff3..b59bc295 100644 --- a/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs +++ b/editor/src/messages/portfolio/document/properties_panel/properties_panel_message_handler.rs @@ -107,6 +107,10 @@ impl<'a> MessageHandler { + let (layer_path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); + responses.push_back(self.create_document_operation(Operation::SetLayerPreserveAspect { layer_path, preserve_aspect })) + } ModifyFill { fill } => { let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer"); responses.push_back(self.create_document_operation(Operation::SetLayerFill { path, fill })); diff --git a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs index ca6eafc7..f2d0c12f 100644 --- a/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs +++ b/editor/src/messages/portfolio/document/properties_panel/utility_functions.rs @@ -4,7 +4,7 @@ use crate::messages::layout::utility_types::layout_widget::{Layout, LayoutGroup, 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::{IconButton, PopoverButton, TextButton}; -use crate::messages::layout::utility_types::widgets::input_widgets::{ColorInput, FontInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextAreaInput, TextInput}; +use crate::messages::layout::utility_types::widgets::input_widgets::{CheckboxInput, ColorInput, FontInput, NumberInput, NumberInputMode, RadioEntryData, RadioInput, TextAreaInput, TextInput}; use crate::messages::layout::utility_types::widgets::label_widgets::{IconLabel, TextLabel}; use crate::messages::portfolio::utility_types::PersistentData; use crate::messages::prelude::*; @@ -20,6 +20,8 @@ use std::f64::consts::PI; use std::sync::Arc; pub fn apply_transform_operation(layer: &Layer, transform_op: TransformOp, value: f64, font_cache: &FontCache) -> [f64; 6] { + let pivot = DAffine2::from_translation(layer.transform.transform_point2(layer.layerspace_pivot(font_cache))); + let transformation = match transform_op { TransformOp::X => DAffine2::update_x, TransformOp::Y => DAffine2::update_y, @@ -34,7 +36,24 @@ pub fn apply_transform_operation(layer: &Layer, transform_op: TransformOp, value _ => 1., }; - transformation(layer.transform, value / scale).to_cols_array() + // Apply the operation and find the delta transform + let mut delta = layer.transform.inverse() * transformation(layer.transform, value / scale); + + // Preserve aspect ratio + if matches!(transform_op, TransformOp::ScaleX | TransformOp::Width) && layer.preserve_aspect { + let scale_x = layer.transform.scale_x(); + if scale_x != 0. { + delta = DAffine2::from_scale((1., (value / scale) / scale_x).into()) * delta; + } + } else if matches!(transform_op, TransformOp::ScaleY | TransformOp::Height) && layer.preserve_aspect { + let scale_y = layer.transform.scale_y(); + if scale_y != 0. { + delta = DAffine2::from_scale(((value / scale) / scale_y, 1.).into()) * delta; + } + } + + // Transform around pivot + ((pivot * delta * pivot.inverse()) * layer.transform).to_cols_array() } pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque, persistent_data: &PersistentData) { @@ -128,9 +147,15 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ ..TextLabel::default() })), WidgetHolder::unrelated_separator(), - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::related_separator(), + WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { + checked: layer.preserve_aspect, + icon: "Link".into(), + tooltip: "Preserve Aspect Ratio".into(), + on_update: WidgetCallback::new(|input: &CheckboxInput| PropertiesPanelMessage::ModifyPreserveAspect { preserve_aspect: input.checked }.into()), + ..Default::default() + })), + WidgetHolder::related_separator(), WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()), @@ -407,9 +432,15 @@ fn node_section_transform(layer: &Layer, persistent_data: &PersistentData) -> La ..TextLabel::default() })), WidgetHolder::unrelated_separator(), - WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px, - WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area. - WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists. + WidgetHolder::related_separator(), + WidgetHolder::new(Widget::CheckboxInput(CheckboxInput { + checked: layer.preserve_aspect, + icon: "Link".into(), + tooltip: "Preserve Aspect Ratio".into(), + on_update: WidgetCallback::new(|input: &CheckboxInput| PropertiesPanelMessage::ModifyPreserveAspect { preserve_aspect: input.checked }.into()), + ..Default::default() + })), + WidgetHolder::related_separator(), WidgetHolder::unrelated_separator(), WidgetHolder::new(Widget::NumberInput(NumberInput { value: Some(layer.transform.scale_x()), diff --git a/frontend/src/components/widgets/WidgetRow.vue b/frontend/src/components/widgets/WidgetRow.vue index a681e451..27a67034 100644 --- a/frontend/src/components/widgets/WidgetRow.vue +++ b/frontend/src/components/widgets/WidgetRow.vue @@ -76,7 +76,6 @@ .widget-row { flex: 0 0 auto; display: flex; - overflow: hidden; min-height: 32px; > * { @@ -96,6 +95,31 @@ --widget-height: 16px; } } + + // TODO: Target this in a better way than using the tooltip, which will break if changed, or when localized/translated + .checkbox-input [title="Preserve Aspect Ratio"] { + margin-bottom: -32px; + position: relative; + + &::before, + &::after { + content: ""; + pointer-events: none; + position: absolute; + left: 8px; + width: 1px; + height: 16px; + background: var(--color-7-middlegray); + } + + &::before { + top: calc(-4px - 16px); + } + + &::after { + bottom: calc(-4px - 16px); + } + } } diff --git a/frontend/src/components/widgets/groups/WidgetSection.vue b/frontend/src/components/widgets/groups/WidgetSection.vue index 98c73bad..d5101cd4 100644 --- a/frontend/src/components/widgets/groups/WidgetSection.vue +++ b/frontend/src/components/widgets/groups/WidgetSection.vue @@ -85,6 +85,7 @@ margin-bottom: 4px; border: 1px solid var(--color-5-dullgray); border-radius: 0 0 4px 4px; + overflow: hidden; .widget-row { &:first-child {