-
Notifications
You must be signed in to change notification settings - Fork 26
Add MultiSelection Resource to enable selecting multiple entities. Up… #403
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
base: main
Are you sure you want to change the base?
Conversation
…dated deletion system to delete multiple entities. Disable widget upon selecting multiple entities
|
@xiyuoh Hi, please take a look and provide any thoughts/suggestions on the implementation, thank you! |
mxgrey
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks for kicking off this feature development. I've wanted a multi-select for a long time but I've been held back by how intertwined the selection mechanisms are.
I've left some recommendations for changes that I think we need to make before we can move forward with this PR. The current approach you've made is very minimal in lines of code changed and minimal in disruption, which are good qualities, but I fear it's creating a fragmented selection pipeline that will leave us with some technical debt in the long run.
| if let Some(multi_selection) = world.get_resource::<MultiSelection>() { | ||
| // If multiple entities are selected, we skip showing the Inspect plugin. | ||
| if multi_selection.0.len() > 1 { | ||
| ui.label("Using multi-selection of entities. Disabling Inspector widget."); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Instead of disabling the inspector widget, I think we should show something like a list of the objects that are currently selected.
I would recommend introducing a DeselctionWidget as a contrast to the SelectionWidget. Instead of a finger icon to indicate selecting the object, we can use an x icon to indicate de-selcting it. When the user hovers the x, it has the same hover effect as the SelectionWidget, but clicking on the x will remove the object from the list of selected items.
| mut selected: Query<&mut Selected>, | ||
| mut selection: ResMut<Selection>, | ||
| mut multi_selection: ResMut<MultiSelection>, | ||
| keyboard_input: Res<ButtonInput<KeyCode>>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should use keyboard_input directly here, for a bunch of reasons:
- This hard-codes the use of left shift as the thing that differentiates between multi-select vs single-select, but we may want to allow key remapping or other ways to toggle multi-select
- Not all interaction modes can make use of multi-select. For example it wouldn't make sense to build up a selection of multiple anchors while creating a lane.
I think what I would suggest instead is to add a field to the Select structure that indicates whether or not multi-select is allowed. Then it's up to the systems that generate the Select event to decide if multi-select is supported for the action. We can introduce a resource
#[derive(Resource)]
pub struct InspectionSettings {
pub multi_select: bool,
}which the inspector mode will use to decide whether the Select event should have multi-select enabled or not. This InspectionSettings resource can have its multi_select field toggled by a system that listens for when the left shift key goes up or down. In the future we can generalize that to allow keybindings, and downstream users can introduce their own way to toggle the setting.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think this approach is better. I was wondering what would be the best way to restrict multi-selection to say model_instances and not lanes, anchors etc. and this does the trick and is more scalable.
To summarize:
- We update the
Selectevent to have amulti_selectfield which will determine if we are able to select multiple entities:
pub struct Select {
#[deref]
pub selection_candidate: Option<SelectionCandidate>,
pub multi_select: bool,
}-
Add a system that monitors
Res<ButtonInput<KeyCode>>, where upon change in the SHIFT keyboard input, it will update theInspectionSettingsresource. -
The individual inspector functions will read the
InspectionSettingsresource to update themulti_selectfield ofSelectwhen triggering aSelectevent. -
The
selection_updateservice receives theSelectevent and reads it to determine whether to enable/disable multi-selection
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
In the midst of implementing this and there are some things Im unclear about and your advice would help:
- Where is a good place to put the
InspectionSettingsresource? I am inclined to put it incrates/rmf_site_picking/src/select/resources.rs - For checking if the SHIFT key goes up or down, I was thinking to extend deselect_on_esc to check for
.pressed(…)or.released(…)on SHIFT and then, updating theInspectionSettingsresource accordingly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Okay that might not work, because the input to deselect_on_esc is a KeyCode and not ButtonInput<KeyCode>. Im thinking that using an async service might not be the best either, and perhaps a typical Bevy system should suffice
|
|
||
| /// Used as a resource to keep track of selecting multiple entities | ||
| #[derive(Default, Debug, Clone, Deref, DerefMut, Resource)] | ||
| pub struct MultiSelection(pub HashSet<Entity>); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't think we should fragment the selection pipeline like this. Let's just update the existing Selection resource to contain a HashSet<Entity> instead of an Option<Entity>, and we can leave it to downstream systems to decide what to do with the hash set.
In the interest of generalizing Selection I think we should change it into a struct, e.g.:
pub struct Selection {
pub selected: HashSet<Entity>,
}We should get rid of the Deref and DerefMut and instead provide helper functions:
impl Selection {
pub fn get_single(&self) -> Option<Entity> {
if self.selected.len() > 1 {
return None;
}
self.selected.iter().next().copied()
}
}There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agreed. Having get_single as a method is a good idea and tidies up the use of Selection in other areas
Signed-off-by: John TGZ <[email protected]>
…shSet and update downstream services and systems. Modified Inspector Widget to list multiple selected entities.
|
Hi @mxgrey , thank you for all the suggestions. I have re-implemented the multi-selection mechanism as follows:
I have yet to implement the de-selection mechanism but would like to get some feedback on the current implementation. |
…iple entites are selected.
…in the world by enabling de-selection of entities and refactoring selection_update.
Signed-off-by: JohnTGZ <[email protected]>
mxgrey
left a comment
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks @JohnTGZ , there are a lot of great concepts being introduced in this PR.
I've left some recommendations for how I think we can tighten up the implementation.
| impl Select { | ||
| pub fn new(candidate: Option<Entity>) -> Select { | ||
| Select(candidate.map(|c| SelectionCandidate::new(c))) | ||
| pub fn new(candidate: Option<Entity>, multi_select: bool) -> Select { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think we can cut down on a lot of unnecessary changes in this PR by tweaking this API:
pub fn new(candidate: Option<Entity>) -> Self {
Self {
candidate: candidate.map(|c| SelectionCandidate::new(c)),
multi_select: false,
}
}
pub fn multi_select(mut self, on: bool) -> Self {
self.multi_select = on;
self
}This will allow us to revert the changes that were made to many of the existing uses of Select::new. For new cases where we need to include the multi-select behavior, we can do e.g.
Select::new(hovered.0)
.multi_select(self.inspection_settings.multi_select)|
|
||
| pub fn provisional(candidate: Entity) -> Select { | ||
| Select(Some(SelectionCandidate::provisional(candidate))) | ||
| pub fn provisional(candidate: Entity, multi_select: bool) -> Select { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Same as before, let's take the multi_select argument out of here and just use the .multi_select(_) builder pattern.
| app.world_mut() | ||
| .insert_resource(InspectorService(inspector_service)); | ||
| app.world_mut().insert_resource(InspectionSettings { | ||
| multi_select: false, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Small nitpick: Let's define Default for InspectionSettings, e.g.
impl Default for InspectionSettings {
fn default() -> Self {
Self {
multi_select: false,
}
}
}then we can just insert InspectionSettings::default() here.
| keyboard_input: Res<ButtonInput<KeyCode>>, | ||
| mut inspection_settings: ResMut<InspectionSettings>, | ||
| ) { | ||
| inspection_settings.multi_select = keyboard_input.pressed(KeyCode::ShiftLeft); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is going to cause InspectionSettings to be modified every single schedule frame, which is going to confuse any system that wants to react to changes in the InspectionSettings resource (such systems will be told that a change has happened on every single frame).
There are two ways we could consider for improving this:
- Check if left shift is pressed every frame and only update
InspectionSettingsif it doesn't match:
let multi_select = keyboard_input.pressed(KeyCode::ShiftLeft);
if inspection_settings.multi_select != multi_select {
inspection_settings.multi_select = multi_select;
}- React to changes in the button status
if keyboard_input.just_pressed(KeyCode::ShiftLeft) {
inspection_settings.multi_select = true;
}
if keyboard_input.just_released(KeyCode::ShiftLeft) {
inspection_settings.multi_select = false;
}I think overall approach (1) is preferable to approach (2). All of the different keyboard_input methods are O(1) so there's not much of a performance concern for either approach, but approach (2) has some very small risk of being "sticky" if there's ever a situation where pressed/released signal gets missed.
| children: Query<'w, 's, &'static Children>, | ||
| heading: Query<'w, 's, (Option<&'static Category>, Option<&'static SiteID>)>, | ||
| inspect_for_query: Query<'w, 's, &'static InspectFor>, | ||
| selection: Res<'w, Selection>, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Seems we can remove this. It isn't getting used as a system param because you call world.get_resource::<Selection>() instead, pulling the resource directly from the world instead of from this struct.
| } else { | ||
| // Only one entity can be selected, so current selections are cleared and | ||
| // a single selection candidate is added to the current selection. | ||
| selection.selected.iter().for_each(|previous_selection| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not wrong to use for_each like this, but it's not considered best practice in Rust. When a regular for-loop can suffice, you're recommended to use one:
for previously_selected in &selection.selected {
if let Ok(mut selected) = query_selected.get_mut(*previously_selected) {
selected.is_selected = false;
}
}| if let Ok(mut selected) = selected.get_mut(new_selection.candidate) { | ||
| selected.is_selected = true; | ||
| } else { | ||
| selection.selected.iter().for_each(|previous_selection| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Here as well, you should just use a regular for-loop.
|
|
||
| if let Some(previous_selection) = selection.0.take() { | ||
| if let Ok(mut selected) = selected.get_mut(previous_selection) { | ||
| selection.selected.iter().for_each(|e| { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Use a regular for-loop instead of .for_each.
| .max_height(INSTANCES_VIEWER_HEIGHT) | ||
| .show(ui, |ui| { | ||
| if self.selection.selected.is_empty() { | ||
| ui.label("Nothing selected or multiple models selected!"); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This message seems odd. With the refactor that I recommended earlier, I don't think we need to do this check or produce this label.
| if ui | ||
| .add(ImageButton::new(self.icons.trash.egui())) | ||
| .on_hover_text("Remove instance from all scenarios") | ||
| .clicked() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Notice how the selector_widget also checks for .hovered() and then emits the Hover(_) event. We should do likewise to help users know which item is being considered for removal before they click.



New feature implementation
Implemented feature
This PR implements the ability to select multiple models in the site which is briefly described in issue #337 .
User workflow:
Implementation description
One of the primary changes involves introducing the
MultiSelectionresource which is aHashSetto hold multiple entities.While multiple entities are selected by holding down the SHIFT key:
Inspectwidget is disabled.MultiSelectionresource will be updated to hold the selected entitiesGenAI declaration