Add alignment of selected layers (#296)
* Add alignment of selected layers * Refactor alignment to a document message * Condense align messages into a tuple variant * Rename dimension to axis and fix redundant math * Add correct Center alignment * Add TODO comment for nested transforms * Add TODO for merging bounding boxes * Move align enums to document_message_handler * Run cargo clippy * Clean up unwraps with filter_map
This commit is contained in:
parent
52fe66b6d8
commit
1055a0a05f
|
|
@ -38,7 +38,7 @@ interface IconButtonOption {
|
||||||
kind: "IconButton";
|
kind: "IconButton";
|
||||||
icon: string;
|
icon: string;
|
||||||
title: string;
|
title: string;
|
||||||
message?: string;
|
message?: string | object;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface SeparatorOption {
|
interface SeparatorOption {
|
||||||
|
|
@ -87,15 +87,15 @@ export default defineComponent({
|
||||||
[
|
[
|
||||||
"Select",
|
"Select",
|
||||||
[
|
[
|
||||||
{ kind: "IconButton", icon: "AlignLeft", title: "Align Left" },
|
{ kind: "IconButton", icon: "AlignLeft", title: "Align Left", message: { Align: ["X", "Min"] } },
|
||||||
{ kind: "IconButton", icon: "AlignHorizontalCenter", title: "Align Horizontal Center" },
|
{ kind: "IconButton", icon: "AlignHorizontalCenter", title: "Align Horizontal Center", message: { Align: ["X", "Center"] } },
|
||||||
{ kind: "IconButton", icon: "AlignRight", title: "Align Right" },
|
{ kind: "IconButton", icon: "AlignRight", title: "Align Right", message: { Align: ["X", "Max"] } },
|
||||||
|
|
||||||
{ kind: "Separator", type: SeparatorType.Unrelated },
|
{ kind: "Separator", type: SeparatorType.Unrelated },
|
||||||
|
|
||||||
{ kind: "IconButton", icon: "AlignTop", title: "Align Top" },
|
{ kind: "IconButton", icon: "AlignTop", title: "Align Top", message: { Align: ["Y", "Min"] } },
|
||||||
{ kind: "IconButton", icon: "AlignVerticalCenter", title: "Align Vertical Center" },
|
{ kind: "IconButton", icon: "AlignVerticalCenter", title: "Align Vertical Center", message: { Align: ["Y", "Center"] } },
|
||||||
{ kind: "IconButton", icon: "AlignBottom", title: "Align Bottom" },
|
{ kind: "IconButton", icon: "AlignBottom", title: "Align Bottom", message: { Align: ["Y", "Max"] } },
|
||||||
|
|
||||||
{ kind: "Separator", type: SeparatorType.Related },
|
{ kind: "Separator", type: SeparatorType.Related },
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -416,7 +416,7 @@ impl Document {
|
||||||
|
|
||||||
let path = path.as_slice()[..path.len() - 1].to_vec();
|
let path = path.as_slice()[..path.len() - 1].to_vec();
|
||||||
|
|
||||||
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path: path.clone() }])
|
Some(vec![DocumentResponse::DocumentChanged, DocumentResponse::FolderChanged { path }])
|
||||||
}
|
}
|
||||||
Operation::FillLayer { path, color } => {
|
Operation::FillLayer { path, color } => {
|
||||||
let layer = self.layer_mut(path).unwrap();
|
let layer = self.layer_mut(path).unwrap();
|
||||||
|
|
|
||||||
|
|
@ -8,6 +8,7 @@ use document_core::layers::Layer;
|
||||||
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
|
use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation};
|
||||||
use glam::{DAffine2, DVec2};
|
use glam::{DAffine2, DVec2};
|
||||||
use log::warn;
|
use log::warn;
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::document::Document;
|
use crate::document::Document;
|
||||||
use std::collections::VecDeque;
|
use std::collections::VecDeque;
|
||||||
|
|
@ -57,9 +58,25 @@ pub enum DocumentMessage {
|
||||||
SetCanvasRotation(f64),
|
SetCanvasRotation(f64),
|
||||||
NudgeSelectedLayers(f64, f64),
|
NudgeSelectedLayers(f64, f64),
|
||||||
FlipLayer(Vec<LayerId>, bool, bool),
|
FlipLayer(Vec<LayerId>, bool, bool),
|
||||||
|
AlignSelectedLayers(AlignAxis, AlignAggregate),
|
||||||
DragLayer(Vec<LayerId>, DVec2),
|
DragLayer(Vec<LayerId>, DVec2),
|
||||||
MoveSelectedLayersTo { path: Vec<LayerId>, insert_index: isize },
|
MoveSelectedLayersTo { path: Vec<LayerId>, insert_index: isize },
|
||||||
ReorderSelectedLayers(i32), // relatve_position,
|
ReorderSelectedLayers(i32), // relatve_position,
|
||||||
|
SetLayerTranslation(Vec<LayerId>, Option<f64>, Option<f64>),
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum AlignAxis {
|
||||||
|
X,
|
||||||
|
Y,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
|
||||||
|
pub enum AlignAggregate {
|
||||||
|
Min,
|
||||||
|
Max,
|
||||||
|
Center,
|
||||||
|
Average,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<DocumentOperation> for DocumentMessage {
|
impl From<DocumentOperation> for DocumentMessage {
|
||||||
|
|
@ -638,6 +655,69 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
AlignSelectedLayers(axis, aggregate) => {
|
||||||
|
// TODO: Handle folder nested transforms with the transforms API
|
||||||
|
let selected_paths = self.selected_layers_sorted();
|
||||||
|
if selected_paths.is_empty() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let selected_layers = selected_paths.iter().filter_map(|path| {
|
||||||
|
let layer = self.active_document().document.layer(path).unwrap();
|
||||||
|
let point = {
|
||||||
|
let bounding_box = layer.bounding_box(layer.transform, layer.style)?;
|
||||||
|
match aggregate {
|
||||||
|
AlignAggregate::Min => bounding_box[0],
|
||||||
|
AlignAggregate::Max => bounding_box[1],
|
||||||
|
AlignAggregate::Center => bounding_box[0].lerp(bounding_box[1], 0.5),
|
||||||
|
AlignAggregate::Average => bounding_box[0].lerp(bounding_box[1], 0.5),
|
||||||
|
}
|
||||||
|
};
|
||||||
|
let (bounding_box_coord, translation_coord) = match axis {
|
||||||
|
AlignAxis::X => (point.x, layer.transform.translation.x),
|
||||||
|
AlignAxis::Y => (point.y, layer.transform.translation.y),
|
||||||
|
};
|
||||||
|
Some((path.clone(), bounding_box_coord, translation_coord))
|
||||||
|
});
|
||||||
|
|
||||||
|
let bounding_box_coords = selected_layers.clone().map(|(_, bounding_box_coord, _)| bounding_box_coord);
|
||||||
|
let aggregated_coord = match aggregate {
|
||||||
|
AlignAggregate::Min => bounding_box_coords.reduce(|a, b| a.min(b)).unwrap(),
|
||||||
|
AlignAggregate::Max => bounding_box_coords.reduce(|a, b| a.max(b)).unwrap(),
|
||||||
|
AlignAggregate::Center => {
|
||||||
|
// TODO: Refactor with `reduce` and `merge_bounding_boxes` once the latter is added
|
||||||
|
let bounding_boxes = selected_paths.iter().filter_map(|path| {
|
||||||
|
let layer = self.active_document().document.layer(path).unwrap();
|
||||||
|
layer.bounding_box(layer.transform, layer.style)
|
||||||
|
});
|
||||||
|
let min = bounding_boxes
|
||||||
|
.clone()
|
||||||
|
.map(|bbox| match axis {
|
||||||
|
AlignAxis::X => bbox[0].x,
|
||||||
|
AlignAxis::Y => bbox[0].y,
|
||||||
|
})
|
||||||
|
.reduce(|a, b| a.min(b))
|
||||||
|
.unwrap();
|
||||||
|
let max = bounding_boxes
|
||||||
|
.clone()
|
||||||
|
.map(|bbox| match axis {
|
||||||
|
AlignAxis::X => bbox[1].x,
|
||||||
|
AlignAxis::Y => bbox[1].y,
|
||||||
|
})
|
||||||
|
.reduce(|a, b| a.max(b))
|
||||||
|
.unwrap();
|
||||||
|
(min + max) / 2.
|
||||||
|
}
|
||||||
|
AlignAggregate::Average => bounding_box_coords.sum::<f64>() / selected_paths.len() as f64,
|
||||||
|
};
|
||||||
|
for (path, bounding_box_coord, translation_coord) in selected_layers {
|
||||||
|
let new_coord = aggregated_coord - (bounding_box_coord - translation_coord);
|
||||||
|
match axis {
|
||||||
|
AlignAxis::X => responses.push_back(DocumentMessage::SetLayerTranslation(path, Some(new_coord), None).into()),
|
||||||
|
AlignAxis::Y => responses.push_back(DocumentMessage::SetLayerTranslation(path, None, Some(new_coord)).into()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
DragLayer(path, offset) => {
|
DragLayer(path, offset) => {
|
||||||
// TODO: Replace root transformations with functions of the transform api
|
// TODO: Replace root transformations with functions of the transform api
|
||||||
// and do the same with all instances of `root.transform.inverse()` in other messages
|
// and do the same with all instances of `root.transform.inverse()` in other messages
|
||||||
|
|
@ -652,6 +732,19 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
|
||||||
responses.push_back(DocumentOperation::SetLayerTransform { path, transform }.into());
|
responses.push_back(DocumentOperation::SetLayerTransform { path, transform }.into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
SetLayerTranslation(path, x_option, y_option) => {
|
||||||
|
if let Ok(layer) = self.active_document_mut().document.layer_mut(&path) {
|
||||||
|
let mut transform = layer.transform;
|
||||||
|
transform.translation = DVec2::new(x_option.unwrap_or(transform.translation.x), y_option.unwrap_or(transform.translation.y));
|
||||||
|
responses.push_back(
|
||||||
|
DocumentOperation::SetLayerTransform {
|
||||||
|
path,
|
||||||
|
transform: transform.to_cols_array(),
|
||||||
|
}
|
||||||
|
.into(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()),
|
message => todo!("document_action_handler does not implement: {}", message.to_discriminant().global_name()),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -5,4 +5,4 @@ mod document_message_handler;
|
||||||
pub use document_file::{Document, LayerData};
|
pub use document_file::{Document, LayerData};
|
||||||
|
|
||||||
#[doc(inline)]
|
#[doc(inline)]
|
||||||
pub use document_message_handler::{DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler};
|
pub use document_message_handler::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler};
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,11 @@ use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
use crate::input::{mouse::ViewportPosition, InputPreprocessor};
|
||||||
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
use crate::tool::{DocumentToolData, Fsm, ToolActionHandlerData};
|
||||||
use crate::{consts::SELECTION_TOLERANCE, document::Document, message_prelude::*};
|
use crate::{
|
||||||
|
consts::SELECTION_TOLERANCE,
|
||||||
|
document::{AlignAggregate, AlignAxis, Document},
|
||||||
|
message_prelude::*,
|
||||||
|
};
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
pub struct Select {
|
pub struct Select {
|
||||||
|
|
@ -24,6 +28,7 @@ pub enum SelectMessage {
|
||||||
MouseMove,
|
MouseMove,
|
||||||
Abort,
|
Abort,
|
||||||
|
|
||||||
|
Align(AlignAxis, AlignAggregate),
|
||||||
FlipHorizontal,
|
FlipHorizontal,
|
||||||
FlipVertical,
|
FlipVertical,
|
||||||
}
|
}
|
||||||
|
|
@ -120,7 +125,7 @@ impl Fsm for SelectToolFsmState {
|
||||||
responses.push_back(make_operation(data, tool_data, transform));
|
responses.push_back(make_operation(data, tool_data, transform));
|
||||||
} else {
|
} else {
|
||||||
for (path, offset) in &data.layers_dragging {
|
for (path, offset) in &data.layers_dragging {
|
||||||
responses.push_back(DocumentMessage::DragLayer(path.clone(), offset.clone()).into());
|
responses.push_back(DocumentMessage::DragLayer(path.clone(), *offset).into());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -162,6 +167,11 @@ impl Fsm for SelectToolFsmState {
|
||||||
|
|
||||||
Ready
|
Ready
|
||||||
}
|
}
|
||||||
|
(_, Align(axis, aggregate)) => {
|
||||||
|
responses.push_back(DocumentMessage::AlignSelectedLayers(axis, aggregate).into());
|
||||||
|
|
||||||
|
self
|
||||||
|
}
|
||||||
(_, FlipHorizontal) => {
|
(_, FlipHorizontal) => {
|
||||||
let selected_layers = document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone()));
|
let selected_layers = document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone()));
|
||||||
for path in selected_layers {
|
for path in selected_layers {
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue