From 1055a0a05fd192131cd4ce049e5a676822710d92 Mon Sep 17 00:00:00 2001 From: Henry Sloan Date: Sat, 24 Jul 2021 19:35:48 -0400 Subject: [PATCH] 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 --- .../widgets/options/ToolOptions.vue | 14 +-- core/document/src/document.rs | 2 +- .../src/document/document_message_handler.rs | 93 +++++++++++++++++++ core/editor/src/document/mod.rs | 2 +- core/editor/src/tool/tools/select.rs | 14 ++- 5 files changed, 114 insertions(+), 11 deletions(-) diff --git a/client/web/src/components/widgets/options/ToolOptions.vue b/client/web/src/components/widgets/options/ToolOptions.vue index 4bebe5d1..85f58fc3 100644 --- a/client/web/src/components/widgets/options/ToolOptions.vue +++ b/client/web/src/components/widgets/options/ToolOptions.vue @@ -38,7 +38,7 @@ interface IconButtonOption { kind: "IconButton"; icon: string; title: string; - message?: string; + message?: string | object; } interface SeparatorOption { @@ -87,15 +87,15 @@ export default defineComponent({ [ "Select", [ - { kind: "IconButton", icon: "AlignLeft", title: "Align Left" }, - { kind: "IconButton", icon: "AlignHorizontalCenter", title: "Align Horizontal Center" }, - { kind: "IconButton", icon: "AlignRight", title: "Align Right" }, + { kind: "IconButton", icon: "AlignLeft", title: "Align Left", message: { Align: ["X", "Min"] } }, + { kind: "IconButton", icon: "AlignHorizontalCenter", title: "Align Horizontal Center", message: { Align: ["X", "Center"] } }, + { kind: "IconButton", icon: "AlignRight", title: "Align Right", message: { Align: ["X", "Max"] } }, { kind: "Separator", type: SeparatorType.Unrelated }, - { kind: "IconButton", icon: "AlignTop", title: "Align Top" }, - { kind: "IconButton", icon: "AlignVerticalCenter", title: "Align Vertical Center" }, - { kind: "IconButton", icon: "AlignBottom", title: "Align Bottom" }, + { kind: "IconButton", icon: "AlignTop", title: "Align Top", message: { Align: ["Y", "Min"] } }, + { kind: "IconButton", icon: "AlignVerticalCenter", title: "Align Vertical Center", message: { Align: ["Y", "Center"] } }, + { kind: "IconButton", icon: "AlignBottom", title: "Align Bottom", message: { Align: ["Y", "Max"] } }, { kind: "Separator", type: SeparatorType.Related }, diff --git a/core/document/src/document.rs b/core/document/src/document.rs index 0f884749..f59ad0c3 100644 --- a/core/document/src/document.rs +++ b/core/document/src/document.rs @@ -416,7 +416,7 @@ impl Document { 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 } => { let layer = self.layer_mut(path).unwrap(); diff --git a/core/editor/src/document/document_message_handler.rs b/core/editor/src/document/document_message_handler.rs index b14d705c..7fd788eb 100644 --- a/core/editor/src/document/document_message_handler.rs +++ b/core/editor/src/document/document_message_handler.rs @@ -8,6 +8,7 @@ use document_core::layers::Layer; use document_core::{DocumentResponse, LayerId, Operation as DocumentOperation}; use glam::{DAffine2, DVec2}; use log::warn; +use serde::{Deserialize, Serialize}; use crate::document::Document; use std::collections::VecDeque; @@ -57,9 +58,25 @@ pub enum DocumentMessage { SetCanvasRotation(f64), NudgeSelectedLayers(f64, f64), FlipLayer(Vec, bool, bool), + AlignSelectedLayers(AlignAxis, AlignAggregate), DragLayer(Vec, DVec2), MoveSelectedLayersTo { path: Vec, insert_index: isize }, ReorderSelectedLayers(i32), // relatve_position, + SetLayerTranslation(Vec, Option, Option), +} + +#[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 for DocumentMessage { @@ -638,6 +655,69 @@ impl MessageHandler 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::() / 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) => { // TODO: Replace root transformations with functions of the transform api // and do the same with all instances of `root.transform.inverse()` in other messages @@ -652,6 +732,19 @@ impl MessageHandler for DocumentMessageHand 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()), } } diff --git a/core/editor/src/document/mod.rs b/core/editor/src/document/mod.rs index 940b4ab1..c478547a 100644 --- a/core/editor/src/document/mod.rs +++ b/core/editor/src/document/mod.rs @@ -5,4 +5,4 @@ mod document_message_handler; pub use document_file::{Document, LayerData}; #[doc(inline)] -pub use document_message_handler::{DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler}; +pub use document_message_handler::{AlignAggregate, AlignAxis, DocumentMessage, DocumentMessageDiscriminant, DocumentMessageHandler}; diff --git a/core/editor/src/tool/tools/select.rs b/core/editor/src/tool/tools/select.rs index ba0e42dc..f0b75810 100644 --- a/core/editor/src/tool/tools/select.rs +++ b/core/editor/src/tool/tools/select.rs @@ -8,7 +8,11 @@ use serde::{Deserialize, Serialize}; use crate::input::{mouse::ViewportPosition, InputPreprocessor}; 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)] pub struct Select { @@ -24,6 +28,7 @@ pub enum SelectMessage { MouseMove, Abort, + Align(AlignAxis, AlignAggregate), FlipHorizontal, FlipVertical, } @@ -120,7 +125,7 @@ impl Fsm for SelectToolFsmState { responses.push_back(make_operation(data, tool_data, transform)); } else { 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 } + (_, Align(axis, aggregate)) => { + responses.push_back(DocumentMessage::AlignSelectedLayers(axis, aggregate).into()); + + self + } (_, FlipHorizontal) => { let selected_layers = document.layer_data.iter().filter_map(|(path, data)| data.selected.then(|| path.clone())); for path in selected_layers {