Move layer selection logic from vue to editor (#410)

* Add vue selectLayer(layer, ctrl, shift)

* Individual selection working, range fill next

* Frontend package-lock.json seems apparently needs to be pushed. Weird.

* Selection working with ctrl, shift from editor. Still some bugs to sqaush with folder nesting.

* WIP resolving nesting folders issues

* Changed comparison approach, handling corner cases now

* Fully working selection.

* Reverted changes to package-lock.json

* Removed unused code

* Resolved ctrl click not behaving similar to windows

* Slight comment clarification

* Double checked a windows behavior and corrected. Changed last_selected name.

* Simplified if statement slightly.

* Made the naming clearer regarding UUIDs versus indices

* Clarified comments further

* Minor comment fixup

* Implemented suggestions, clarified comments

* Resolved todo regarding clearing selection when ctrl not pressed

* Ensure we only push responses when needed

Co-authored-by: Keavon Chambers <keavon@keavon.com>
Reviewed by: @TrueDoctor <3
This commit is contained in:
Oliver Davies 2021-12-22 17:45:14 -08:00 committed by Keavon Chambers
parent 30418c51f8
commit e54fedc6a5
7 changed files with 122 additions and 75 deletions

View File

@ -191,7 +191,7 @@ mod test {
const LINE_INDEX: usize = 0;
const PEN_INDEX: usize = 1;
editor.handle_message(DocumentMessage::CreateFolder(vec![]));
editor.handle_message(DocumentMessage::CreateEmptyFolder(vec![]));
let document_before_added_shapes = editor.dispatcher.documents_message_handler.active_document().graphene_document.clone();
let folder_id = document_before_added_shapes.root.as_folder().unwrap().layer_ids[FOLDER_INDEX];

View File

@ -64,6 +64,7 @@ pub struct DocumentMessageHandler {
pub saved_document_identifier: u64,
pub name: String,
pub layer_data: HashMap<Vec<LayerId>, LayerData>,
layer_range_selection_reference: Vec<LayerId>,
movement_handler: MovementMessageHandler,
transform_layer_handler: TransformLayerMessageHandler,
pub snapping_enabled: bool,
@ -78,6 +79,7 @@ impl Default for DocumentMessageHandler {
name: String::from("Untitled Document"),
saved_document_identifier: 0,
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
layer_range_selection_reference: Vec::new(),
movement_handler: MovementMessageHandler::default(),
transform_layer_handler: TransformLayerMessageHandler::default(),
snapping_enabled: true,
@ -96,12 +98,13 @@ pub enum DocumentMessage {
SetSelectedLayers(Vec<Vec<LayerId>>),
AddSelectedLayers(Vec<Vec<LayerId>>),
SelectAllLayers,
SelectLayer(Vec<LayerId>, bool, bool),
SelectionChanged,
DeselectAllLayers,
DeleteLayer(Vec<LayerId>),
DeleteSelectedLayers,
DuplicateSelectedLayers,
CreateFolder(Vec<LayerId>),
CreateEmptyFolder(Vec<LayerId>),
SetBlendModeForSelectedLayers(BlendMode),
SetOpacityForSelectedLayers(f64),
RenameLayer(Vec<LayerId>, String),
@ -312,6 +315,7 @@ impl DocumentMessageHandler {
saved_document_identifier: 0,
name,
layer_data: vec![(vec![], LayerData::new(true))].into_iter().collect(),
layer_range_selection_reference: Vec::new(),
movement_handler: MovementMessageHandler::default(),
transform_layer_handler: TransformLayerMessageHandler::default(),
snapping_enabled: true,
@ -497,14 +501,16 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
.into(),
)
}
CreateFolder(mut path) => {
CreateEmptyFolder(mut path) => {
let id = generate_uuid();
path.push(id);
self.layerdata_mut(&path).expanded = true;
responses.push_back(DocumentOperation::CreateFolder { path }.into())
}
GroupSelectedLayers => {
let common_prefix = self.graphene_document.common_prefix(self.selected_layers());
let selected_layers = self.selected_layers();
let common_prefix = self.graphene_document.common_layer_path_prefix(selected_layers);
let (_id, common_prefix) = common_prefix.split_last().unwrap_or((&0, &[]));
let mut new_folder_path = common_prefix.to_vec();
@ -568,6 +574,43 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
responses.push_back(DocumentOperation::DuplicateLayer { path }.into());
}
}
SelectLayer(selected, ctrl, shift) => {
let mut paths = vec![];
let last_selection_exists = !self.layer_range_selection_reference.is_empty();
// If we have shift pressed and a layer already selected then fill the range
if shift && last_selection_exists {
// Fill the selection range
self.layer_data
.iter()
.filter(|(target, _)| self.graphene_document.layer_is_between(&target, &selected, &self.layer_range_selection_reference))
.for_each(|(layer_path, _)| {
paths.push(layer_path.clone());
});
} else {
if ctrl {
// Toggle selection when holding ctrl
let layer = self.layerdata_mut(&selected);
layer.selected = !layer.selected;
responses.push_back(LayerChanged(selected.clone()).into());
} else {
paths.push(selected.clone());
}
// Set our last selection reference
self.layer_range_selection_reference = selected;
}
// Don't create messages for empty operations
if paths.len() > 0 {
// Add or set our selected layers
if ctrl {
responses.push_front(AddSelectedLayers(paths).into());
} else {
responses.push_front(SetSelectedLayers(paths).into());
}
}
}
SetSelectedLayers(paths) => {
self.layer_data.iter_mut().filter(|(_, layer_data)| layer_data.selected).for_each(|(path, layer_data)| {
layer_data.selected = false;
@ -593,7 +636,10 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
.collect::<Vec<_>>();
responses.push_front(SetSelectedLayers(all_layer_paths).into());
}
DeselectAllLayers => responses.push_front(SetSelectedLayers(vec![]).into()),
DeselectAllLayers => {
responses.push_front(SetSelectedLayers(vec![]).into());
self.layer_range_selection_reference.clear();
}
DocumentHistoryBackward => self.undo(responses).unwrap_or_else(|e| log::warn!("{}", e)),
DocumentHistoryForward => self.redo(responses).unwrap_or_else(|e| log::warn!("{}", e)),
Undo => {
@ -639,7 +685,8 @@ impl MessageHandler<DocumentMessage, &InputPreprocessor> for DocumentMessageHand
self.layer_data.insert(path.clone(), LayerData::new(false));
responses.push_back(LayerChanged(path.clone()).into());
if !self.graphene_document.layer(&path).unwrap().overlay {
responses.push_back(SetSelectedLayers(vec![path]).into())
self.layer_range_selection_reference = path.clone();
responses.push_back(SetSelectedLayers(vec![path]).into());
}
}
DocumentResponse::DocumentChanged => responses.push_back(RenderDocument.into()),

View File

@ -209,7 +209,7 @@ impl Default for Mapping {
entry! {action=DocumentMessage::Undo, key_down=KeyZ, modifiers=[KeyControl]},
entry! {action=DocumentMessage::DeselectAllLayers, key_down=KeyA, modifiers=[KeyControl, KeyAlt]},
entry! {action=DocumentMessage::SelectAllLayers, key_down=KeyA, modifiers=[KeyControl]},
entry! {action=DocumentMessage::CreateFolder(vec![]), key_down=KeyN, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::CreateEmptyFolder(vec![]), key_down=KeyN, modifiers=[KeyControl, KeyShift]},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyDelete},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyX},
entry! {action=DocumentMessage::DeleteSelectedLayers, key_down=KeyBackspace},

View File

@ -36,10 +36,10 @@
class="layer"
:class="{ selected: layer.layer_data.selected }"
:style="{ marginLeft: layerIndent(layer) }"
@click.shift.exact.stop="handleShiftClick(layer)"
@click.ctrl.exact.stop="handleControlClick(layer)"
@click.alt.exact.stop="handleControlClick(layer)"
@click.exact.stop="handleClick(layer)"
@click.shift.exact.stop="selectLayer(layer, false, true)"
@click.shift.ctrl.exact.stop="selectLayer(layer, true, true)"
@click.ctrl.exact.stop="selectLayer(layer, true, false)"
@click.exact.stop="selectLayer(layer, false, false)"
>
<div class="layer-thumbnail" v-html="layer.thumbnail"></div>
<div class="layer-type-icon">
@ -289,39 +289,8 @@ export default defineComponent({
async setLayerOpacity() {
this.editor.instance.set_opacity_for_selected_layers(this.opacity);
},
async handleControlClick(clickedLayer: LayerPanelEntry) {
const index = this.layers.indexOf(clickedLayer);
clickedLayer.layer_data.selected = !clickedLayer.layer_data.selected;
this.selectionRangeEndLayer = undefined;
this.selectionRangeStartLayer =
this.layers.slice(index).filter((layer) => layer.layer_data.selected)[0] ||
this.layers
.slice(0, index)
.reverse()
.filter((layer) => layer.layer_data.selected)[0];
this.sendSelectedLayers();
},
async handleShiftClick(clickedLayer: LayerPanelEntry) {
// The two paths of the range are stored in selectionRangeStartLayer and selectionRangeEndLayer
// So for a new Shift+Click, select all layers between selectionRangeStartLayer and selectionRangeEndLayer (stored in previous Shift+Click)
this.clearSelection();
this.selectionRangeEndLayer = clickedLayer;
if (!this.selectionRangeStartLayer) this.selectionRangeStartLayer = clickedLayer;
this.fillSelectionRange(this.selectionRangeStartLayer, this.selectionRangeEndLayer, true);
this.sendSelectedLayers();
},
async handleClick(clickedLayer: LayerPanelEntry) {
this.selectionRangeStartLayer = clickedLayer;
this.selectionRangeEndLayer = clickedLayer;
this.clearSelection();
clickedLayer.layer_data.selected = true;
this.sendSelectedLayers();
async selectLayer(clickedLayer: LayerPanelEntry, ctrl: boolean, shift: boolean) {
this.editor.instance.select_layer(clickedLayer.path, ctrl, shift);
},
async deselectAllLayers() {
this.selectionRangeStartLayer = undefined;
@ -329,39 +298,11 @@ export default defineComponent({
this.editor.instance.deselect_all_layers();
},
async fillSelectionRange(start: LayerPanelEntry, end: LayerPanelEntry, selected = true) {
const startIndex = this.layers.findIndex((layer) => layer.path.join() === start.path.join());
const endIndex = this.layers.findIndex((layer) => layer.path.join() === end.path.join());
const [min, max] = [startIndex, endIndex].sort();
if (min !== -1) {
for (let i = min; i <= max; i += 1) {
this.layers[i].layer_data.selected = selected;
}
}
},
async clearSelection() {
this.layers.forEach((layer) => {
layer.layer_data.selected = false;
});
},
async sendSelectedLayers() {
const paths = this.layers.filter((layer) => layer.layer_data.selected).map((layer) => layer.path);
const length = paths.reduce((acc, cur) => acc + cur.length, 0) + paths.length - 1;
const output = new BigUint64Array(length);
let i = 0;
paths.forEach((path, index) => {
output.set(path, i);
i += path.length;
if (index < paths.length) {
output[i] = (1n << 64n) - 1n;
}
i += 1;
});
this.editor.instance.select_layers(output);
},
setBlendModeForSelectedLayers() {
const selected = this.layers.filter((layer) => layer.layer_data.selected);
@ -383,6 +324,7 @@ export default defineComponent({
}
},
setOpacityForSelectedLayers() {
// todo figure out why this is here
const selected = this.layers.filter((layer) => layer.layer_data.selected);
if (selected.length < 1) {

View File

@ -322,6 +322,11 @@ impl JsEditorHandle {
self.dispatch(message);
}
pub fn select_layer(&self, paths: Vec<LayerId>, ctrl: bool, shift: bool) {
let message = DocumentMessage::SelectLayer(paths, ctrl, shift);
self.dispatch(message);
}
/// Select all layers
pub fn select_all_layers(&self) {
let message = DocumentMessage::SelectAllLayers;
@ -443,7 +448,7 @@ impl JsEditorHandle {
/// Requests the backend to add a layer to the layer list
pub fn add_folder(&self, path: Vec<LayerId>) {
let message = DocumentMessage::CreateFolder(path);
let message = DocumentMessage::CreateEmptyFolder(path);
self.dispatch(message);
}
}

View File

@ -1,4 +1,5 @@
use std::{
cmp::max,
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
@ -102,7 +103,7 @@ impl Document {
}
pub fn deepest_common_folder<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> Result<&'a [LayerId], DocumentError> {
let common_prefix_of_path = self.common_prefix(layers);
let common_prefix_of_path = self.common_layer_path_prefix(layers);
Ok(match self.layer(common_prefix_of_path)?.data {
LayerDataType::Folder(_) => common_prefix_of_path,
@ -110,7 +111,7 @@ impl Document {
})
}
pub fn common_prefix<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> &'a [LayerId] {
pub fn common_layer_path_prefix<'a>(&self, layers: impl Iterator<Item = &'a [LayerId]>) -> &'a [LayerId] {
layers
.reduce(|a, b| {
let number_of_uncommon_ids_in_a = (0..a.len()).position(|i| b.starts_with(&a[..a.len() - i])).unwrap_or_default();
@ -119,6 +120,53 @@ impl Document {
.unwrap_or_default()
}
// Determines which layer is closer to the root, if path_a return true, if path_b return false
// Answers the question: Is A closer to the root than B?
pub fn layer_closer_to_root(&self, path_a: &Vec<u64>, path_b: &Vec<u64>) -> bool {
// Convert UUIDs to indices
let indices_for_path_a = self.indices_for_path(path_a).unwrap();
let indices_for_path_b = self.indices_for_path(path_b).unwrap();
let longest = max(indices_for_path_a.len(), indices_for_path_b.len());
for i in 0..longest {
// usize::MAX becomes negative one here, sneaky. So folders are compared as [X, -1]. This is intentional.
let index_a = *indices_for_path_a.get(i).unwrap_or(&usize::MAX) as i32;
let index_b = *indices_for_path_b.get(i).unwrap_or(&usize::MAX) as i32;
// index_a == index_b -> true, this means the "2" indices being compared are within the same folder
// eg -> [2, X] == [2, X] since we are only comparing the "2" in this iteration
// Continue onto comparing the X indices.
if index_a == index_b {
continue;
}
// If index_a is smaller, index_a is closer to the root
return index_a < index_b;
}
return false;
}
// Is the target layer between a <-> b layers, inclusive
pub fn layer_is_between(&self, target: &Vec<u64>, path_a: &Vec<u64>, path_b: &Vec<u64>) -> bool {
// If the target is a nonsense path, it isn't between
if target.len() < 1 {
return false;
}
// This function is inclusive, so we consider path_a, path_b to be between themselves
if target == path_a || target == path_b {
return true;
};
// These can't both be true and be between two values
let layer_vs_a = self.layer_closer_to_root(target, path_a);
let layer_vs_b = self.layer_closer_to_root(target, path_b);
// To be inbetween you need to be above A and below B or vice versa
return layer_vs_a != layer_vs_b;
}
/// Given a path to a layer, returns a vector of the indices in the layer tree
/// These indices can be used to order a list of layers
pub fn indices_for_path(&self, path: &[LayerId]) -> Result<Vec<usize>, DocumentError> {
@ -126,6 +174,7 @@ impl Document {
let mut indices = vec![];
let (path, layer_id) = split_path(path)?;
// TODO: appears to be n^2? should we maintain a lookup table?
for id in path {
let pos = root.layer_ids.iter().position(|x| *x == *id).ok_or(DocumentError::LayerNotFound)?;
indices.push(pos);

View File

@ -41,6 +41,7 @@ impl Folder {
/// When a insertion id is provided, try to insert the layer with the given id.
/// If that id is already used, return None.
/// When no insertion id is provided, search for the next free id and insert it with that.
/// Negative values for insert_index represent distance from the end
pub fn add_layer(&mut self, layer: Layer, id: Option<LayerId>, insert_index: isize) -> Option<LayerId> {
let mut insert_index = insert_index as i128;
if insert_index < 0 {
@ -54,13 +55,16 @@ impl Folder {
if self.layer_ids.contains(&self.next_assignment_id) {
return None;
}
let id = self.next_assignment_id;
self.layers.insert(insert_index as usize, layer);
self.layer_ids.insert(insert_index as usize, id);
// Linear probing for collision avoidance
while self.layer_ids.contains(&self.next_assignment_id) {
self.next_assignment_id += 1;
}
Some(id)
} else {
None