Skip to content

Add a bounding box around selected layers in the Select tool #282

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions core/document/src/bounding_box.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
use glam::DVec2;

pub fn merge_bounding_boxes([a_min, a_max]: [DVec2; 2], [b_min, b_max]: [DVec2; 2]) -> [DVec2; 2] {
let min_x = a_min.x.min(b_min.x);
let min_y = a_min.y.min(b_min.y);
let max_x = a_max.x.max(b_max.x);
let max_y = a_max.y.max(b_max.y);
[DVec2::new(min_x, min_y), DVec2::new(max_x, max_y)]
}
28 changes: 4 additions & 24 deletions core/document/src/layers/folder.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
use glam::DVec2;

use crate::bounding_box::merge_bounding_boxes;

use crate::{DocumentError, LayerId};

use super::{style, Layer, LayerData, LayerDataTypes};
Expand Down Expand Up @@ -111,30 +113,8 @@ impl Folder {
}

pub fn bounding_box(&self, transform: glam::DAffine2) -> Option<[DVec2; 2]> {
let mut layers_non_empty_bounding_boxes = self.layers.iter().filter_map(|layer| layer.bounding_box(transform * layer.transform, layer.style)).peekable();

layers_non_empty_bounding_boxes.peek()?;

let mut x_min = f64::MAX;
let mut y_min = f64::MAX;
let mut x_max = f64::MIN;
let mut y_max = f64::MIN;

for [bounding_box_min, bounding_box_max] in layers_non_empty_bounding_boxes {
if bounding_box_min.x < x_min {
x_min = bounding_box_min.x
}
if bounding_box_min.y < y_min {
y_min = bounding_box_min.y
}
if bounding_box_max.x > x_max {
x_max = bounding_box_max.x
}
if bounding_box_max.y > y_max {
y_max = bounding_box_max.y
}
}
Some([DVec2::new(x_min, y_min), DVec2::new(x_max, y_max)])
let layers_non_empty_bounding_boxes = self.layers.iter().filter_map(|layer| layer.bounding_box(transform * layer.transform, layer.style));
layers_non_empty_bounding_boxes.reduce(merge_bounding_boxes)
}
}

Expand Down
1 change: 1 addition & 0 deletions core/document/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
//! to any application that wants to link the library for the purpose of updating GDD files by sending edit operations.
//! Optionally depends on the Renderer Core Library if rendering is required.

pub mod bounding_box;
pub mod color;
pub mod document;
pub mod intersection;
Expand Down
5 changes: 5 additions & 0 deletions core/editor/src/document/document_message_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -422,6 +422,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
for path in paths {
responses.extend(self.select_layer(&path));
}
responses.push_back(ToolMessage::SelectionUpdated.into());
// TODO: Correctly update layer panel in clear_selection instead of here
responses.extend(self.handle_folder_changed(Vec::new()));
}
Expand All @@ -430,10 +431,12 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
for path in all_layer_paths {
responses.extend(self.select_layer(&path));
}
responses.push_back(ToolMessage::SelectionUpdated.into());
}
DeselectAllLayers => {
self.clear_selection();
let children = self.active_document_mut().layer_panel(&[]).expect("The provided Path was not valid");
responses.push_back(ToolMessage::SelectionUpdated.into());
responses.push_back(FrontendMessage::ExpandFolder { path: vec![], children }.into());
}
Undo => {
Expand Down Expand Up @@ -514,6 +517,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
if self.rotating {
let half_viewport = ipp.viewport_size.as_dvec2() / 2.;
let rotation = {
responses.push_back(ToolMessage::CanvasRotated.into());
let start_vec = self.mouse_pos.as_dvec2() - half_viewport;
let end_vec = ipp.mouse.position.as_dvec2() - half_viewport;
start_vec.angle_between(end_vec)
Expand Down Expand Up @@ -590,6 +594,7 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
let layerdata = self.layerdata_mut(&[]);
layerdata.rotation = new;
self.create_document_transform_from_layerdata(&ipp.viewport_size, responses);
responses.push_back(ToolMessage::CanvasRotated.into());
responses.push_back(FrontendMessage::SetCanvasRotation { new_radians: new }.into());
}
NudgeSelectedLayers(x, y) => {
Expand Down
15 changes: 14 additions & 1 deletion core/editor/src/tool/tool_message_handler.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use crate::message_prelude::*;
use document_core::color::Color;
use document_core::Operation;

use crate::input::InputPreprocessor;
use crate::{
Expand All @@ -16,6 +17,8 @@ pub enum ToolMessage {
SelectSecondaryColor(Color),
SwapColors,
ResetColors,
CanvasRotated,
SelectionUpdated,
SetToolOptions(ToolType, ToolOptions),
#[child]
Fill(FillMessage),
Expand Down Expand Up @@ -69,8 +72,13 @@ impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessag
};
reset(tool);
reset(self.tool_state.tool_data.active_tool_type);
self.tool_state.tool_data.active_tool_type = tool;

if let ToolType::Select = tool {
responses.push_back(ToolMessage::Select(SelectMessage::Init).into());
}

self.tool_state.tool_data.active_tool_type = tool;
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(FrontendMessage::SetActiveTool { tool_name: tool.to_string() }.into())
}
SwapColors => {
Expand All @@ -87,6 +95,11 @@ impl MessageHandler<ToolMessage, (&Document, &InputPreprocessor)> for ToolMessag
SetToolOptions(tool_type, tool_options) => {
self.tool_state.document_tool_data.tool_options.insert(tool_type, tool_options);
}
CanvasRotated | SelectionUpdated => self
.tool_state
.tool_data
.active_tool_mut()
.process_action(message, (&document, &self.tool_state.document_tool_data, input), responses),
message => {
let tool_type = match message {
Fill(_) => ToolType::Fill,
Expand Down
92 changes: 76 additions & 16 deletions core/editor/src/tool/tools/select.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use document_core::bounding_box::merge_bounding_boxes;
use document_core::color::Color;
use document_core::layers::style;
use document_core::layers::style::Fill;
Expand All @@ -23,6 +24,7 @@ pub struct Select {
#[impl_message(Message, ToolMessage, Select)]
#[derive(PartialEq, Clone, Debug, Serialize, Deserialize)]
pub enum SelectMessage {
Init,
DragStart,
DragStop,
MouseMove,
Expand Down Expand Up @@ -72,9 +74,21 @@ impl Fsm for SelectToolFsmState {
let transform = document.document.root.transform;
use SelectMessage::*;
use SelectToolFsmState::*;
if let ToolMessage::Select(event) = event {
match (self, event) {
match event {
ToolMessage::CanvasRotated | ToolMessage::SelectionUpdated => {
responses.push_back(Operation::ClearWorkingFolder.into());
make_selection_bounding_box(document, responses);
self
}
ToolMessage::Select(event) => match (self, event) {
(_, Init) => {
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
make_selection_bounding_box(document, responses);
self
}
(Ready, DragStart) => {
responses.push_back(Operation::DiscardWorkingFolder.into());

data.drag_start = input.mouse.position;
data.drag_current = input.mouse.position;

Expand Down Expand Up @@ -103,18 +117,20 @@ impl Fsm for SelectToolFsmState {
.filter_map(|(path, layer_data)| {
layer_data
.selected
.then(|| (path.clone(), document.document.layer(path).unwrap().transform.translation - transformed_start))
.then(|| (path.clone(), document.document.document_layer(path).unwrap().transform.translation - transformed_start))
})
.collect();
} else {
responses.push_back(DocumentMessage::SelectLayers(vec![intersection.clone()]).into());
data.layers_dragging = vec![(intersection.clone(), document.document.layer(intersection).unwrap().transform.translation - transformed_start)]
data.layers_dragging = vec![(intersection.clone(), document.document.document_layer(intersection).unwrap().transform.translation - transformed_start)]
}
} else {
responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
data.layers_dragging = Vec::new();
}

responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
make_selection_bounding_box(document, responses);

Dragging
}
(Dragging, MouseMove) => {
Expand All @@ -124,22 +140,25 @@ impl Fsm for SelectToolFsmState {
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(make_operation(data, tool_data, transform));
} else {
responses.push_back(Operation::DiscardWorkingFolder.into());
for (path, offset) in &data.layers_dragging {
responses.push_back(DocumentMessage::DragLayer(path.clone(), *offset).into());
}

responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
make_selection_bounding_box(document, responses);
}

Dragging
}
(Dragging, DragStop) => {
data.drag_current = input.mouse.position;

if data.layers_dragging.is_empty() {
responses.push_back(Operation::ClearWorkingFolder.into());
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(Operation::DiscardWorkingFolder.into());

if data.drag_start == data.drag_current {
responses.push_back(DocumentMessage::SelectLayers(vec![]).into());
if data.layers_dragging.is_empty() {
let selection = if data.drag_start == data.drag_current {
vec![]
} else {
let (point_1, point_2) = (
DVec2::new(data.drag_start.x as f64, data.drag_start.y as f64),
Expand All @@ -153,18 +172,28 @@ impl Fsm for SelectToolFsmState {
DVec2::new(point_1.x, point_2.y),
];

responses.push_back(DocumentMessage::SelectLayers(document.document.intersects_quad_root(quad)).into());
}
document.document.intersects_quad_root(quad)
};

responses.push_back(DocumentMessage::SelectLayers(selection.clone()).into());

responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
make_paths_bounding_box(selection, document, responses);
} else {
data.layers_dragging = Vec::new();

responses.push_back(Operation::MountWorkingFolder { path: vec![] }.into());
make_selection_bounding_box(document, responses);
}

Ready
}
(Dragging, Abort) => {
responses.push_back(Operation::DiscardWorkingFolder.into());
responses.push_back(Operation::ClearWorkingFolder.into());
data.layers_dragging = Vec::new();

make_selection_bounding_box(document, responses);

Ready
}
(_, Align(axis, aggregate)) => {
Expand All @@ -189,9 +218,8 @@ impl Fsm for SelectToolFsmState {
self
}
_ => self,
}
} else {
self
},
_ => self,
}
}
}
Expand All @@ -210,3 +238,35 @@ fn make_operation(data: &SelectToolData, _tool_data: &DocumentToolData, transfor
}
.into()
}

fn make_selection_bounding_box(document: &Document, responses: &mut VecDeque<Message>) {
let selected_layers_paths = document.layer_data.iter().filter_map(|(path, layer_data)| layer_data.selected.then(|| path)).cloned().collect();
make_paths_bounding_box(selected_layers_paths, document, responses);
}

fn make_paths_bounding_box(paths: Vec<Vec<LayerId>>, document: &Document, responses: &mut VecDeque<Message>) {
let non_empty_bounding_boxes = paths.iter().filter_map(|path| {
if let Ok(some_bounding_box) = document.document.layer_axis_aligned_bounding_box(path) {
some_bounding_box
} else {
None
}
});

if let Some([min, max]) = non_empty_bounding_boxes.reduce(merge_bounding_boxes) {
let x0 = min.x - 1.0;
let y0 = min.y - 1.0;
let x1 = max.x + 1.0;
let y1 = max.y + 1.0;
let root_transform = document.document.root.transform;
responses.push_back(
Operation::AddRect {
path: vec![],
insert_index: -1,
transform: (root_transform.inverse() * glam::DAffine2::from_scale_angle_translation(DVec2::new(x1 - x0, y1 - y0), 0., DVec2::new(x0, y0))).to_cols_array(),
style: style::PathStyle::new(Some(Stroke::new(Color::from_rgb8(0x00, 0xa6, 0xfb), 1.0)), Some(Fill::none())),
}
.into(),
)
}
}