mirror of
https://github.com/GraphiteEditor/Graphite.git
synced 2026-09-30 18:38:13 +08:00
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 <keavon@keavon.com>
This commit is contained in:
committed by
Keavon Chambers
co-authored by
Keavon Chambers
parent
df74f6f562
commit
904f38fa1b
@@ -628,6 +628,12 @@ impl Document {
|
|||||||
}
|
}
|
||||||
Some(vec![LayerChanged { path: layer_path.clone() }])
|
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 } => {
|
Operation::SetTextEditability { path, editable } => {
|
||||||
self.layer_mut(&path)?.as_text_mut()?.editable = editable;
|
self.layer_mut(&path)?.as_text_mut()?.editable = editable;
|
||||||
self.mark_as_dirty(&path)?;
|
self.mark_as_dirty(&path)?;
|
||||||
|
|||||||
@@ -227,6 +227,9 @@ pub struct Layer {
|
|||||||
/// A transformation applied to the layer (translation, rotation, scaling, and shear).
|
/// A transformation applied to the layer (translation, rotation, scaling, and shear).
|
||||||
#[serde(with = "DAffine2Ref")]
|
#[serde(with = "DAffine2Ref")]
|
||||||
pub transform: glam::DAffine2,
|
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.
|
/// 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).
|
/// This is in local space (so the layer's transform should be applied).
|
||||||
pub pivot: DVec2,
|
pub pivot: DVec2,
|
||||||
@@ -255,6 +258,7 @@ impl Layer {
|
|||||||
name: None,
|
name: None,
|
||||||
data,
|
data,
|
||||||
transform: glam::DAffine2::from_cols_array(&transform),
|
transform: glam::DAffine2::from_cols_array(&transform),
|
||||||
|
preserve_aspect: true,
|
||||||
pivot: DVec2::splat(0.5),
|
pivot: DVec2::splat(0.5),
|
||||||
cache: String::new(),
|
cache: String::new(),
|
||||||
thumbnail_cache: String::new(),
|
thumbnail_cache: String::new(),
|
||||||
@@ -515,6 +519,7 @@ impl Clone for Layer {
|
|||||||
name: self.name.clone(),
|
name: self.name.clone(),
|
||||||
data: self.data.clone(),
|
data: self.data.clone(),
|
||||||
transform: self.transform,
|
transform: self.transform,
|
||||||
|
preserve_aspect: self.preserve_aspect,
|
||||||
pivot: self.pivot,
|
pivot: self.pivot,
|
||||||
cache: String::new(),
|
cache: String::new(),
|
||||||
thumbnail_cache: String::new(),
|
thumbnail_cache: String::new(),
|
||||||
|
|||||||
@@ -245,6 +245,10 @@ pub enum Operation {
|
|||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
name: String,
|
name: String,
|
||||||
},
|
},
|
||||||
|
SetLayerPreserveAspect {
|
||||||
|
layer_path: Vec<LayerId>,
|
||||||
|
preserve_aspect: bool,
|
||||||
|
},
|
||||||
SetLayerBlendMode {
|
SetLayerBlendMode {
|
||||||
path: Vec<LayerId>,
|
path: Vec<LayerId>,
|
||||||
blend_mode: BlendMode,
|
blend_mode: BlendMode,
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ pub enum PropertiesPanelMessage {
|
|||||||
ModifyFill { fill: Fill },
|
ModifyFill { fill: Fill },
|
||||||
ModifyFont { font_family: String, font_style: String, size: f64 },
|
ModifyFont { font_family: String, font_style: String, size: f64 },
|
||||||
ModifyName { name: String },
|
ModifyName { name: String },
|
||||||
|
ModifyPreserveAspect { preserve_aspect: bool },
|
||||||
ModifyStroke { stroke: Stroke },
|
ModifyStroke { stroke: Stroke },
|
||||||
ModifyText { new_text: String },
|
ModifyText { new_text: String },
|
||||||
ModifyTransform { value: f64, transform_op: TransformOp },
|
ModifyTransform { value: f64, transform_op: TransformOp },
|
||||||
|
|||||||
+4
@@ -107,6 +107,10 @@ impl<'a> MessageHandler<PropertiesPanelMessage, (&PersistentData, PropertiesPane
|
|||||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
||||||
responses.push_back(self.create_document_operation(Operation::SetLayerName { path, name }))
|
responses.push_back(self.create_document_operation(Operation::SetLayerName { path, name }))
|
||||||
}
|
}
|
||||||
|
ModifyPreserveAspect { preserve_aspect } => {
|
||||||
|
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 } => {
|
ModifyFill { fill } => {
|
||||||
let (path, _) = self.active_selection.clone().expect("Received update for properties panel with no active layer");
|
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 }));
|
responses.push_back(self.create_document_operation(Operation::SetLayerFill { path, fill }));
|
||||||
|
|||||||
@@ -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::misc::LayoutTarget;
|
||||||
use crate::messages::layout::utility_types::widgets::assist_widgets::PivotAssist;
|
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::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::layout::utility_types::widgets::label_widgets::{IconLabel, TextLabel};
|
||||||
use crate::messages::portfolio::utility_types::PersistentData;
|
use crate::messages::portfolio::utility_types::PersistentData;
|
||||||
use crate::messages::prelude::*;
|
use crate::messages::prelude::*;
|
||||||
@@ -20,6 +20,8 @@ use std::f64::consts::PI;
|
|||||||
use std::sync::Arc;
|
use std::sync::Arc;
|
||||||
|
|
||||||
pub fn apply_transform_operation(layer: &Layer, transform_op: TransformOp, value: f64, font_cache: &FontCache) -> [f64; 6] {
|
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 {
|
let transformation = match transform_op {
|
||||||
TransformOp::X => DAffine2::update_x,
|
TransformOp::X => DAffine2::update_x,
|
||||||
TransformOp::Y => DAffine2::update_y,
|
TransformOp::Y => DAffine2::update_y,
|
||||||
@@ -34,7 +36,24 @@ pub fn apply_transform_operation(layer: &Layer, transform_op: TransformOp, value
|
|||||||
_ => 1.,
|
_ => 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<Message>, persistent_data: &PersistentData) {
|
pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDeque<Message>, persistent_data: &PersistentData) {
|
||||||
@@ -128,9 +147,15 @@ pub fn register_artboard_layer_properties(layer: &Layer, responses: &mut VecDequ
|
|||||||
..TextLabel::default()
|
..TextLabel::default()
|
||||||
})),
|
})),
|
||||||
WidgetHolder::unrelated_separator(),
|
WidgetHolder::unrelated_separator(),
|
||||||
WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px,
|
WidgetHolder::related_separator(),
|
||||||
WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area.
|
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||||
WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
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::unrelated_separator(),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: Some(layer.bounding_transform(&persistent_data.font_cache).scale_x()),
|
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()
|
..TextLabel::default()
|
||||||
})),
|
})),
|
||||||
WidgetHolder::unrelated_separator(),
|
WidgetHolder::unrelated_separator(),
|
||||||
WidgetHolder::unrelated_separator(), // TODO: These three separators add up to 24px,
|
WidgetHolder::related_separator(),
|
||||||
WidgetHolder::unrelated_separator(), // TODO: which is the width of the Assist area.
|
WidgetHolder::new(Widget::CheckboxInput(CheckboxInput {
|
||||||
WidgetHolder::unrelated_separator(), // TODO: Remove these when we have proper entry row formatting that includes room for Assists.
|
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::unrelated_separator(),
|
||||||
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
WidgetHolder::new(Widget::NumberInput(NumberInput {
|
||||||
value: Some(layer.transform.scale_x()),
|
value: Some(layer.transform.scale_x()),
|
||||||
|
|||||||
@@ -76,7 +76,6 @@
|
|||||||
.widget-row {
|
.widget-row {
|
||||||
flex: 0 0 auto;
|
flex: 0 0 auto;
|
||||||
display: flex;
|
display: flex;
|
||||||
overflow: hidden;
|
|
||||||
min-height: 32px;
|
min-height: 32px;
|
||||||
|
|
||||||
> * {
|
> * {
|
||||||
@@ -96,6 +95,31 @@
|
|||||||
--widget-height: 16px;
|
--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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|
||||||
|
|||||||
@@ -85,6 +85,7 @@
|
|||||||
margin-bottom: 4px;
|
margin-bottom: 4px;
|
||||||
border: 1px solid var(--color-5-dullgray);
|
border: 1px solid var(--color-5-dullgray);
|
||||||
border-radius: 0 0 4px 4px;
|
border-radius: 0 0 4px 4px;
|
||||||
|
overflow: hidden;
|
||||||
|
|
||||||
.widget-row {
|
.widget-row {
|
||||||
&:first-child {
|
&:first-child {
|
||||||
|
|||||||
Reference in New Issue
Block a user