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:
parent
30418c51f8
commit
e54fedc6a5
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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()),
|
||||
|
|
|
|||
|
|
@ -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},
|
||||
|
|
|
|||
|
|
@ -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) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue