Skip to content

Move layer selection logic from vue to editor #410

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

Merged
merged 24 commits into from
Dec 23, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
24 commits
Select commit Hold shift + click to select a range
1ef1987
Add vue selectLayer(layer, ctrl, shift)
otdavies Dec 17, 2021
0805314
Individual selection working, range fill next
otdavies Dec 17, 2021
fe13692
Frontend package-lock.json seems apparently needs to be pushed. Weird.
otdavies Dec 17, 2021
95858cf
Selection working with ctrl, shift from editor. Still some bugs to sq…
otdavies Dec 18, 2021
720f9bb
WIP resolving nesting folders issues
otdavies Dec 19, 2021
792a74c
Changed comparison approach, handling corner cases now
otdavies Dec 19, 2021
9f26b76
Fully working selection.
otdavies Dec 20, 2021
79ba1b1
Merge branch 'master' into move-layer-logic-to-editor
otdavies Dec 20, 2021
682bfed
Merge branch 'master' into move-layer-logic-to-editor
otdavies Dec 20, 2021
c58bfce
Reverted changes to package-lock.json
otdavies Dec 20, 2021
f13f196
Merge branch 'master' into move-layer-logic-to-editor
otdavies Dec 20, 2021
bbe008a
Removed unused code
otdavies Dec 21, 2021
f6cc0af
Merge branch 'master' into move-layer-logic-to-editor
Keavon Dec 21, 2021
6909585
Resolved ctrl click not behaving similar to windows
otdavies Dec 22, 2021
add7650
Slight comment clarification
otdavies Dec 22, 2021
708aff1
Double checked a windows behavior and corrected. Changed last_selecte…
otdavies Dec 22, 2021
645d463
Simplified if statement slightly.
otdavies Dec 22, 2021
1723f67
Made the naming clearer regarding UUIDs versus indices
otdavies Dec 22, 2021
52511fe
Clarified comments further
otdavies Dec 22, 2021
40cb3eb
Minor comment fixup
otdavies Dec 22, 2021
e3a3f5b
Implemented suggestions, clarified comments
otdavies Dec 22, 2021
e8b1db8
Resolved todo regarding clearing selection when ctrl not pressed
otdavies Dec 22, 2021
34104bd
Merge branch 'master' into move-layer-logic-to-editor
otdavies Dec 22, 2021
f55ae45
Ensure we only push responses when needed
otdavies Dec 22, 2021
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
2 changes: 1 addition & 1 deletion editor/src/communication/dispatcher.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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];
Expand Down
57 changes: 52 additions & 5 deletions editor/src/document/document_file.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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,
Expand All @@ -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),
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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();
Expand Down Expand Up @@ -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;
Expand All @@ -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 => {
Expand Down Expand Up @@ -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()),
Expand Down
2 changes: 1 addition & 1 deletion editor/src/input/input_mapper.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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},
Expand Down
72 changes: 7 additions & 65 deletions frontend/src/components/panels/LayerTree.vue
Original file line number Diff line number Diff line change
Expand Up @@ -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">
Expand Down Expand Up @@ -289,79 +289,20 @@ 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;
this.selectionRangeEndLayer = undefined;

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);

Expand All @@ -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) {
Expand Down
7 changes: 6 additions & 1 deletion frontend/wasm/src/api.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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);
}
}
Expand Down
53 changes: 51 additions & 2 deletions graphene/src/document.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use std::{
cmp::max,
collections::hash_map::DefaultHasher,
hash::{Hash, Hasher},
};
Expand Down Expand Up @@ -102,15 +103,15 @@ 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,
LayerDataType::Shape(_) => &common_prefix_of_path[..common_prefix_of_path.len() - 1],
})
}

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();
Expand All @@ -119,13 +120,61 @@ 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> {
let mut root = self.root.as_folder()?;
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);
Expand Down
4 changes: 4 additions & 0 deletions graphene/src/layers/folder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand All @@ -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
Expand Down