diff --git a/editor/src/communication/dispatcher.rs b/editor/src/communication/dispatcher.rs index a729f9cd46..f68d824092 100644 --- a/editor/src/communication/dispatcher.rs +++ b/editor/src/communication/dispatcher.rs @@ -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]; diff --git a/editor/src/document/document_file.rs b/editor/src/document/document_file.rs index 8b21668f75..860eb98ab6 100644 --- a/editor/src/document/document_file.rs +++ b/editor/src/document/document_file.rs @@ -64,6 +64,7 @@ pub struct DocumentMessageHandler { pub saved_document_identifier: u64, pub name: String, pub layer_data: HashMap, LayerData>, + layer_range_selection_reference: Vec, 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>), AddSelectedLayers(Vec>), SelectAllLayers, + SelectLayer(Vec, bool, bool), SelectionChanged, DeselectAllLayers, DeleteLayer(Vec), DeleteSelectedLayers, DuplicateSelectedLayers, - CreateFolder(Vec), + CreateEmptyFolder(Vec), SetBlendModeForSelectedLayers(BlendMode), SetOpacityForSelectedLayers(f64), RenameLayer(Vec, 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 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 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 for DocumentMessageHand .collect::>(); 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 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()), diff --git a/editor/src/input/input_mapper.rs b/editor/src/input/input_mapper.rs index 8487667cfe..167e7531b6 100644 --- a/editor/src/input/input_mapper.rs +++ b/editor/src/input/input_mapper.rs @@ -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}, diff --git a/frontend/src/components/panels/LayerTree.vue b/frontend/src/components/panels/LayerTree.vue index ac717e5501..cf1171b3d3 100644 --- a/frontend/src/components/panels/LayerTree.vue +++ b/frontend/src/components/panels/LayerTree.vue @@ -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)" >
@@ -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) { diff --git a/frontend/wasm/src/api.rs b/frontend/wasm/src/api.rs index 10b4f04936..7d0f876f4c 100644 --- a/frontend/wasm/src/api.rs +++ b/frontend/wasm/src/api.rs @@ -322,6 +322,11 @@ impl JsEditorHandle { self.dispatch(message); } + pub fn select_layer(&self, paths: Vec, 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) { - let message = DocumentMessage::CreateFolder(path); + let message = DocumentMessage::CreateEmptyFolder(path); self.dispatch(message); } } diff --git a/graphene/src/document.rs b/graphene/src/document.rs index 3d462e006e..472c996bcc 100644 --- a/graphene/src/document.rs +++ b/graphene/src/document.rs @@ -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) -> 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) -> &'a [LayerId] { + pub fn common_layer_path_prefix<'a>(&self, layers: impl Iterator) -> &'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, path_b: &Vec) -> 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, path_a: &Vec, path_b: &Vec) -> 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, 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); diff --git a/graphene/src/layers/folder.rs b/graphene/src/layers/folder.rs index f617c7ec76..4942fecd7d 100644 --- a/graphene/src/layers/folder.rs +++ b/graphene/src/layers/folder.rs @@ -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, insert_index: isize) -> Option { 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