diff --git a/rmf_site_editor/src/site/mod.rs b/rmf_site_editor/src/site/mod.rs index ced2bf19..cb85a583 100644 --- a/rmf_site_editor/src/site/mod.rs +++ b/rmf_site_editor/src/site/mod.rs @@ -119,6 +119,7 @@ pub use util::*; pub mod wall; pub use wall::*; +use yaserde::ser::Config; use crate::recency::{RecencyRank, RecencyRankingPlugin}; use crate::{AppState, RegisterIssueType}; diff --git a/rmf_site_editor/src/site/model.rs b/rmf_site_editor/src/site/model.rs index d3d64455..34c9aaa4 100644 --- a/rmf_site_editor/src/site/model.rs +++ b/rmf_site_editor/src/site/model.rs @@ -28,10 +28,11 @@ use bevy::{ }; use bevy_mod_outline::OutlineMeshExt; use rmf_site_format::{ - Affiliation, AssetSource, ModelMarker, ModelProperty, NameInSite, Pending, Pose, Scale, + Affiliation, AssetSource, DifferentialDrive, ModelMarker, ModelProperty, NameInSite, Pending, + Pose, Scale, }; use smallvec::SmallVec; -use std::any::TypeId; +use std::{any::TypeId, collections::HashMap}; #[derive(Component, Debug, Clone)] pub struct ModelScene { @@ -487,6 +488,7 @@ pub fn propagate_model_render_layers( } } +/// This system keeps model instances up to date with the properties of their affiliated descriptions pub fn update_model_instances( mut commands: Commands, model_properties: Query< diff --git a/rmf_site_editor/src/widgets/inspector/inspect_group.rs b/rmf_site_editor/src/widgets/inspector/inspect_group.rs index 335f4c88..7ea4441b 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_group.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_group.rs @@ -16,35 +16,21 @@ */ use crate::{ - site::{ - Affiliation, AssetSource, Change, DefaultFile, Group, Members, ModelProperty, NameInSite, - Scale, Texture, - }, + site::{Affiliation, Change, DefaultFile, Group, Members, ModelMarker, NameInSite, Texture}, widgets::{inspector::InspectTexture, prelude::*, Inspect, SelectorWidget}, - CurrentWorkspace, InspectAssetSourceComponent, InspectScaleComponent, + CurrentWorkspace, }; use bevy::{ecs::system::SystemParam, prelude::*}; use bevy_egui::egui::{CollapsingHeader, RichText, Ui}; -use rmf_site_format::RecallAssetSource; #[derive(SystemParam)] pub struct InspectGroup<'w, 's> { is_group: Query<'w, 's, (), With>, - affiliation: Query<'w, 's, &'static Affiliation>, + affiliation: Query<'w, 's, &'static Affiliation, Without>, names: Query<'w, 's, &'static NameInSite>, textures: Query<'w, 's, &'static Texture>, - model_descriptions: Query< - 'w, - 's, - ( - &'static ModelProperty, - &'static ModelProperty, - ), - >, members: Query<'w, 's, &'static Members>, default_file: Query<'w, 's, &'static DefaultFile>, - change_asset_source: EventWriter<'w, Change>>, - change_scale: EventWriter<'w, Change>>, change_texture: EventWriter<'w, Change>, current_workspace: Res<'w, CurrentWorkspace>, selector: SelectorWidget<'w, 's>, @@ -81,35 +67,6 @@ impl<'w, 's> InspectGroup<'w, 's> { .root .map(|e| self.default_file.get(e).ok()) .flatten(); - if let Ok((ModelProperty(current_source), ModelProperty(current_scale))) = - self.model_descriptions.get(id) - { - ui.label(RichText::new("Model Description Properties").size(18.0)); - ui.add_space(10.0); - - // Asset Source - let default_file = self - .current_workspace - .root - .map(|e| self.default_file.get(e).ok()) - .flatten(); - if let Some(new_source) = InspectAssetSourceComponent::new( - ¤t_source, - &RecallAssetSource::default(), - default_file, - ) - .show(ui) - { - self.change_asset_source - .send(Change::new(ModelProperty(new_source), id)); - } - - // Scale - if let Some(new_scale) = InspectScaleComponent::new(¤t_scale).show(ui) { - self.change_scale - .send(Change::new(ModelProperty(new_scale), id)); - } - } if let Ok(texture) = self.textures.get(id) { ui.label(RichText::new("Texture Properties").size(18.0)); if let Some(new_texture) = InspectTexture::new(texture, default_file).show(ui) { diff --git a/rmf_site_editor/src/widgets/inspector/inspect_model_description.rs b/rmf_site_editor/src/widgets/inspector/inspect_model_description.rs index 7dbfc8f6..abff9b88 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_model_description.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_model_description.rs @@ -16,27 +16,255 @@ */ use crate::{ - site::{Category, Change}, + interaction::Selection, + site::{AssetSource, Change, DefaultFile, ModelProperty, Pending}, widgets::{prelude::*, Inspect}, + CurrentWorkspace, InspectAssetSourceComponent, InspectScaleComponent, MainInspector, }; -use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{ComboBox, Ui}; -use rmf_site_format::{Affiliation, Group, ModelMarker, NameInSite}; +use rmf_site_format::{ + Affiliation, DifferentialDrive, Group, IsStatic, ModelMarker, NameInSite, RecallAssetSource, + Scale, +}; + +use bevy::{ + ecs::{component::ComponentInfo, query::WorldQuery, system::SystemParam}, + prelude::*, +}; +use bevy_egui::egui::{CollapsingHeader, ComboBox, RichText, Ui}; +use smallvec::SmallVec; +use std::{any::TypeId, collections::HashMap}; + +#[derive(Default)] +pub struct InspectModelDescriptionPlugin {} + +impl Plugin for InspectModelDescriptionPlugin { + fn build(&self, app: &mut App) { + let main_inspector = app.world.resource::().id; + let widget = Widget::new::(&mut app.world); + let id = app.world.spawn(widget).set_parent(main_inspector).id(); + app.world.insert_resource(ModelDescriptionInspector { id }); + app.world.init_resource::(); + } +} + +/// Contains a reference to the model description inspector widget. +#[derive(Resource)] +pub struct ModelDescriptionInspector { + id: Entity, +} + +impl ModelDescriptionInspector { + pub fn get(&self) -> Entity { + self.id + } +} + +#[derive(Resource)] +pub struct ModelPropertyDefault(pub ModelProperty); + +/// This resource keeps track of all the properties that can be configured for a model description. +#[derive(Resource)] +pub struct ModelPropertyNames { + pub required: HashMap, + pub optional: HashMap, +} + +impl FromWorld for ModelPropertyNames { + fn from_world(_world: &mut World) -> Self { + let mut required = HashMap::new(); + required.insert( + TypeId::of::>(), + "Asset Source".to_string(), + ); + required.insert(TypeId::of::>(), "Scale".to_string()); + required.insert( + TypeId::of::>(), + "Is Static".to_string(), + ); + + let mut optional = HashMap::new(); + optional.insert( + TypeId::of::>(), + "Differential Drive".to_string(), + ); + Self { required, optional } + } +} + +pub struct InspectModelPropertyPlugin +where + W: WidgetSystem + 'static + Send + Sync, + T: 'static + Send + Sync + Default + Clone + FromReflect + TypePath, +{ + property_name: String, + _property: ModelProperty, + _ignore: std::marker::PhantomData, +} + +impl InspectModelPropertyPlugin +where + W: WidgetSystem + 'static + Send + Sync, + T: 'static + Send + Sync + Default + Clone + FromReflect + TypePath, +{ + pub fn new(property_name: String) -> Self { + Self { + property_name: property_name, + _property: ModelProperty::::default(), + _ignore: Default::default(), + } + } +} + +impl Plugin for InspectModelPropertyPlugin +where + W: WidgetSystem + 'static + Send + Sync, + T: 'static + Send + Sync + Default + Clone + FromReflect + TypePath, +{ + fn build(&self, app: &mut App) { + app.register_type::>(); + // If type has already been loaded as required type, do not allow it to be loaded as optional + let type_id = TypeId::of::>(); + if !app + .world + .resource::() + .required + .contains_key(&type_id) + { + app.world + .resource_mut::() + .optional + .insert(type_id, self.property_name.clone()); + } + + let inspector = app.world.resource::().id; + let widget = Widget::::new::(&mut app.world); + app.world.spawn(widget).set_parent(inspector); + } +} + +/// This is the base model description inspector widget, which allows the user to dynamically +/// configure the properties associated with a model description. #[derive(SystemParam)] -pub struct InspectModelDescription<'w, 's> { - model_instances: Query< - 'w, - 's, - (&'static Category, &'static Affiliation), - (With, Without), - >, - model_descriptions: +struct InspectModelDescription<'w, 's> { + model_instances: + Query<'w, 's, &'static Affiliation, (With, Without)>, + model_descriptions: Query<'w, 's, &'static NameInSite, (With, With)>, + model_property_names: Query<'w, 's, (Entity, &'static NameInSite), (With, With)>, - change_affiliation: EventWriter<'w, Change>>, + model_properties: Res<'w, ModelPropertyNames>, + inspect_model_description: Res<'w, ModelDescriptionInspector>, + children: Query<'w, 's, &'static Children>, } impl<'w, 's> WidgetSystem for InspectModelDescription<'w, 's> { + fn show( + Inspect { + selection, + inspection, + panel, + }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + // Get description entity from within closure, since inspect_entity requires immutable reference to world + let description_entity = { + let params = state.get_mut(world); + if let Some(description_entity) = get_selected_description_entity( + selection, + ¶ms.model_instances, + ¶ms.model_descriptions, + ) { + description_entity + } else { + return; + } + }; + + let components_info: Vec = world + .inspect_entity(description_entity) + .into_iter() + .cloned() + .collect(); + let params = state.get_mut(world); + + let mut description_property_types = Vec::::new(); + for component_info in components_info { + if let Some(type_id) = component_info.type_id() { + if params.model_properties.required.contains_key(&type_id) { + description_property_types.push(type_id); + } else if params.model_properties.optional.contains_key(&type_id) { + description_property_types.push(type_id); + } + } + } + let mut available_property_types = Vec::::new(); + for (type_id, _) in params.model_properties.optional.iter() { + if !description_property_types.contains(type_id) { + available_property_types.push(*type_id); + } + } + + ui.separator(); + let description_name = params + .model_descriptions + .get(description_entity) + .map(|n| n.0.clone()) + .unwrap_or("Unnamed".to_string()); + ui.label(RichText::new(format!("Model Properties of [{}]", description_name)).size(18.0)); + + CollapsingHeader::new("Configure Properties") + .default_open(false) + .show(ui, |ui| { + ui.horizontal_wrapped(|ui| { + for type_id in description_property_types { + let property_name = params.model_properties.required.get(&type_id).unwrap(); + ui.add_enabled_ui(false, |ui| { + ui.toggle_value(&mut true, property_name); + }); + } + + for type_id in available_property_types { + let property_name = params.model_properties.optional.get(&type_id).unwrap(); + if ui.toggle_value(&mut false, property_name).clicked() { + println!("Add property: {:?}", property_name); + } + } + }); + }); + + let children: Result, _> = params + .children + .get(params.inspect_model_description.id) + .map(|children| children.iter().copied().collect()); + let Ok(children) = children else { + return; + }; + + for child in children { + let inspect = Inspect { + selection, + inspection: child, + panel, + }; + let _ = world.try_show_in(child, inspect, ui); + } + } +} + +/// When inspecting a selected instance of a model description, this widget allows the user to view +/// and change its description +#[derive(SystemParam)] +pub struct InspectSelectedModelDescription<'w, 's> { + model_instances: + Query<'w, 's, &'static Affiliation, (With, Without)>, + model_descriptions: + Query<'w, 's, (Entity, &'static NameInSite), (With, With)>, + change_affiliation: EventWriter<'w, Change>>, +} + +impl<'w, 's> WidgetSystem for InspectSelectedModelDescription<'w, 's> { fn show( Inspect { selection, .. }: Inspect, ui: &mut Ui, @@ -48,20 +276,17 @@ impl<'w, 's> WidgetSystem for InspectModelDescription<'w, 's> { } } -impl<'w, 's> InspectModelDescription<'w, 's> { +impl<'w, 's> InspectSelectedModelDescription<'w, 's> { pub fn show_widget(&mut self, id: Entity, ui: &mut Ui) { - let Ok((_, current_description_entity)) = self.model_instances.get(id) else { - return; - }; - let Ok((current_description_entity, current_description_name)) = - self.model_descriptions.get( - current_description_entity - .0 - .expect("Model instances should have valid affiliation"), - ) + let Some(current_description_entity) = + get_selected_description_entity(id, &self.model_instances, &self.model_descriptions) else { return; }; + let (current_description_entity, current_description_name) = self + .model_descriptions + .get(current_description_entity) + .unwrap(); let mut new_description_entity = current_description_entity.clone(); ui.horizontal(|ui| { @@ -80,3 +305,161 @@ impl<'w, 's> InspectModelDescription<'w, 's> { } } } + +/// Helper function to get the corresponding description entity for a given model instance entity +/// if it exists. +fn get_selected_description_entity<'w, 's, T: WorldQuery>( + selection: Entity, + model_instances: &Query< + 'w, + 's, + &'static Affiliation, + (With, Without), + >, + model_descriptions: &Query<'w, 's, T, (With, With)>, +) -> Option { + match model_descriptions.get(selection) { + Ok(_) => Some(selection), + Err(_) => match model_instances + .get(selection) + .ok() + .and_then(|affiliation| affiliation.0) + { + Some(affiliation) => { + if model_descriptions.get(affiliation).is_ok() { + Some(affiliation) + } else { + warn!("Model instance is affiliated with a non-existent description"); + None + } + } + None => None, + }, + } +} + +/// +/// Basic model properties inspector here for the time being +/// + +#[derive(SystemParam)] +pub struct InspectModelScale<'w, 's> { + model_instances: + Query<'w, 's, &'static Affiliation, (With, Without)>, + model_descriptions: + Query<'w, 's, &'static ModelProperty, (With, With)>, + change_scale: EventWriter<'w, Change>>, +} + +impl<'w, 's> WidgetSystem for InspectModelScale<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Some(description_entity) = get_selected_description_entity( + selection, + ¶ms.model_instances, + ¶ms.model_descriptions, + ) else { + return; + }; + + let Ok(ModelProperty(scale)) = params.model_descriptions.get(description_entity) else { + return; + }; + if let Some(new_scale) = InspectScaleComponent::new(scale).show(ui) { + params + .change_scale + .send(Change::new(ModelProperty(new_scale), description_entity)); + } + ui.add_space(10.0); + } +} + +#[derive(SystemParam)] +pub struct InspectModelAssetSource<'w, 's> { + model_instances: + Query<'w, 's, &'static Affiliation, (With, Without)>, + model_descriptions: + Query<'w, 's, &'static ModelProperty, (With, With)>, + change_asset_source: EventWriter<'w, Change>>, + current_workspace: Res<'w, CurrentWorkspace>, + default_file: Query<'w, 's, &'static DefaultFile>, +} + +impl<'w, 's> WidgetSystem for InspectModelAssetSource<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Some(description_entity) = get_selected_description_entity( + selection, + ¶ms.model_instances, + ¶ms.model_descriptions, + ) else { + return; + }; + + let Ok(ModelProperty(source)) = params.model_descriptions.get(description_entity) else { + return; + }; + + let default_file = params + .current_workspace + .root + .map(|e| params.default_file.get(e).ok()) + .flatten(); + + if let Some(new_source) = + InspectAssetSourceComponent::new(source, &RecallAssetSource::default(), default_file) + .show(ui) + { + params + .change_asset_source + .send(Change::new(ModelProperty(new_source), description_entity)); + } + } +} + +#[derive(SystemParam)] +pub struct InspectModelDifferentialDrive<'w, 's> { + model_instances: + Query<'w, 's, &'static Affiliation, (With, Without)>, + model_descriptions: Query< + 'w, + 's, + ( + &'static ModelProperty, + &'static RecallAssetSource, + ), + (With, With), + >, + change_asset_source: EventWriter<'w, Change>>, + current_workspace: Res<'w, CurrentWorkspace>, + default_file: Query<'w, 's, &'static DefaultFile>, +} + +impl<'w, 's> WidgetSystem for InspectModelDifferentialDrive<'w, 's> { + fn show( + Inspect { selection, .. }: Inspect, + ui: &mut Ui, + state: &mut SystemState, + world: &mut World, + ) { + let mut params = state.get_mut(world); + let Some(description_entity) = get_selected_description_entity( + selection, + ¶ms.model_instances, + ¶ms.model_descriptions, + ) else { + return; + ui.label("Differential Drive"); + }; + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_scale.rs b/rmf_site_editor/src/widgets/inspector/inspect_scale.rs index cc733065..e052abfe 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_scale.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_scale.rs @@ -25,7 +25,7 @@ use rmf_site_format::{Affiliation, Scale}; #[derive(SystemParam)] pub struct InspectScale<'w, 's> { - scales: Query<'w, 's, (&'static Scale), (Without>)>, + scales: Query<'w, 's, &'static Scale, (Without>)>, change_scale: EventWriter<'w, Change>, } diff --git a/rmf_site_editor/src/widgets/inspector/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index 46d3b17d..5a13ad4c 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -181,7 +181,7 @@ impl Plugin for StandardInspectorPlugin { app.add_plugins(MinimalInspectorPlugin::default()) .add_plugins(( InspectionPlugin::::new(), - InspectionPlugin::::new(), + InspectionPlugin::::new(), InspectionPlugin::::new(), InspectionPlugin::::new(), InspectionPlugin::::new(), @@ -210,6 +210,13 @@ impl Plugin for StandardInspectorPlugin { InspectLiftPlugin::default(), InspectionPlugin::::new(), InspectionPlugin::::new(), + InspectModelDescriptionPlugin::default(), + )) + .add_plugins(( + InspectModelPropertyPlugin::::new("Scale".to_string()), + InspectModelPropertyPlugin::::new( + "Source".to_string(), + ), )); } } diff --git a/rmf_site_format/src/legacy/model.rs b/rmf_site_format/src/legacy/model.rs index 5a0ee072..09794067 100644 --- a/rmf_site_format/src/legacy/model.rs +++ b/rmf_site_format/src/legacy/model.rs @@ -28,20 +28,6 @@ impl Model { DVec2::new(self.x, self.y) } - // pub fn to_site(&self) -> SiteModel { - // SiteModel { - // name: NameInSite(self.instance_name.clone()), - // source: AssetSource::Search(self.model_name.clone()), - // pose: Pose { - // trans: [self.x as f32, self.y as f32, self.z_offset as f32], - // rot: Rotation::Yaw(Angle::Deg(self.yaw.to_degrees() as f32)), - // }, - // is_static: IsStatic(self.static_), - // scale: Scale::default(), - // marker: ModelMarker, - // } - // } - pub fn to_site( &self, model_description_name_map: &mut HashMap, diff --git a/rmf_site_format/src/model.rs b/rmf_site_format/src/model.rs index 68f33000..ba6b0c97 100644 --- a/rmf_site_format/src/model.rs +++ b/rmf_site_format/src/model.rs @@ -20,7 +20,7 @@ use crate::*; #[cfg(feature = "bevy")] use bevy::prelude::{Bundle, Component, Reflect, ReflectComponent}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{any::TypeId, collections::HashMap}; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Bundle))] @@ -64,10 +64,12 @@ impl Default for Model { /// /// +/// Defines a property in a model description, that will be added to all instances #[derive(Serialize, Deserialize, Debug, Default, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component, Reflect))] pub struct ModelProperty(pub T); +/// Bundle with all required components for a valid model description #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Bundle))] pub struct ModelDescriptionBundle { @@ -83,6 +85,7 @@ pub struct ModelDescriptionBundle { pub marker: ModelMarker, } +/// Bundle with all required components for a valid model instance #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Bundle))] pub struct ModelInstance {