diff --git a/assets/textures/alignment.png b/assets/textures/alignment.png index 28bf6e05..818291ce 100644 Binary files a/assets/textures/alignment.png and b/assets/textures/alignment.png differ diff --git a/assets/textures/alignment.svg b/assets/textures/alignment.svg index 7fe16006..a9e9d512 100644 --- a/assets/textures/alignment.svg +++ b/assets/textures/alignment.svg @@ -9,7 +9,7 @@ id="svg5" inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" sodipodi:docname="alignment.svg" - inkscape:export-filename="/yui/projects/site/src/rmf_site/assets/textures/alignment.png" + inkscape:export-filename="alignment.png" inkscape:export-xdpi="390" inkscape:export-ydpi="390" xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape" @@ -33,9 +33,9 @@ inkscape:guide-bbox="true" inkscape:zoom="6.2217172" inkscape:cx="53.200747" - inkscape:cy="94.748762" - inkscape:window-width="3770" - inkscape:window-height="2096" + inkscape:cy="94.829125" + inkscape:window-width="3840" + inkscape:window-height="2119" inkscape:window-x="0" inkscape:window-y="0" inkscape:window-maximized="1" diff --git a/assets/textures/alpha.svg b/assets/textures/alpha.svg index c153746c..a0c09bc5 100644 --- a/assets/textures/alpha.svg +++ b/assets/textures/alpha.svg @@ -7,7 +7,7 @@ viewBox="0 0 33.866666 33.866668" version="1.1" id="svg5" - inkscape:export-filename="/yui/projects/site/src/rmf_site/assets/textures/alpha.png" + inkscape:export-filename="alpha.png" inkscape:export-xdpi="96" inkscape:export-ydpi="96" inkscape:version="1.1.2 (0a00cf5339, 2022-02-04)" @@ -32,7 +32,7 @@ inkscape:guide-bbox="true" inkscape:snap-global="true" inkscape:zoom="3.1748381" - inkscape:cx="33.230041" + inkscape:cx="33.545018" inkscape:cy="60.94799" inkscape:window-width="1521" inkscape:window-height="1082" diff --git a/assets/textures/attribution.md b/assets/textures/attribution.md index 3a6d66e2..ac9ca194 100644 --- a/assets/textures/attribution.md +++ b/assets/textures/attribution.md @@ -6,6 +6,7 @@ * [`empty.png`](https://thenounproject.com/icon/empty-194055/) * [`add.png`](https://thenounproject.com/icon/plus-1809810/) * [`search.png`](https://thenounproject.com/icon/search-3743008/) +* [`merge.png`](https://thenounproject.com/icon/merge-3402180/) * `trash.png`: @mxgrey * `selected.png`: @mxgrey * `alignment.png`: @mxgrey diff --git a/assets/textures/merge.png b/assets/textures/merge.png new file mode 100644 index 00000000..f5a9f8cf Binary files /dev/null and b/assets/textures/merge.png differ diff --git a/rmf_site_editor/Cargo.toml b/rmf_site_editor/Cargo.toml index f1f2221b..a6fdf41c 100644 --- a/rmf_site_editor/Cargo.toml +++ b/rmf_site_editor/Cargo.toml @@ -11,6 +11,10 @@ name = "librmf_site_editor" path = "src/main.rs" name = "rmf_site_editor" +[[example]] +name = "extending_site_editor" +path = "examples/extending_menu.rs" + [dependencies] bevy_egui = "0.19" bevy_mod_picking = "0.11" diff --git a/rmf_site_editor/examples/extending_menu.rs b/rmf_site_editor/examples/extending_menu.rs new file mode 100644 index 00000000..1454bb1f --- /dev/null +++ b/rmf_site_editor/examples/extending_menu.rs @@ -0,0 +1,92 @@ +use bevy::prelude::*; +use librmf_site_editor::{widgets::menu_bar::*, SiteEditor}; + +#[derive(Debug, Default)] +struct MyMenuPlugin; + +#[derive(Debug, Resource)] +struct MyMenuHandler { + unique_export: Entity, + custom_nested_menu: Entity, +} + +impl FromWorld for MyMenuHandler { + fn from_world(world: &mut World) -> Self { + // This is all it takes to register a new menu item + // We need to keep track of the entity in order to make + // sure that we can check the callback + let unique_export = world + .spawn(MenuItem::Text("My unique export".to_string())) + .id(); + + // Make it a child of the "File Menu" + let file_header = world.resource::().get(); + world + .entity_mut(file_header) + .push_children(&[unique_export]); + + // For top level menus simply spawn a menu with no parent + let menu = world + .spawn(Menu::from_title("My Awesome Menu".to_string())) + .id(); + + // We can use bevy's parent-child system to handle nesting + let sub_menu = world + .spawn(Menu::from_title("My Awesome sub menu".to_string())) + .id(); + world.entity_mut(menu).push_children(&[sub_menu]); + + // Finally we can create a custom action + let custom_nested_menu = world + .spawn(MenuItem::Text("My Awesome Action".to_string())) + .id(); + world + .entity_mut(sub_menu) + .push_children(&[custom_nested_menu]); + + // Track the entity so that we know when to handle events from it in + Self { + unique_export, + custom_nested_menu, + } + } +} + +/// Handler for unique export menu item. All one needs to do is check that you recieve +/// an event that is of the same type as the one we are supposed to +/// handle. +fn watch_unique_export_click(mut reader: EventReader, menu_handle: Res) { + for event in reader.iter() { + if event.clicked() && event.source() == menu_handle.unique_export { + println!("Doing our epic export"); + } + } +} + +/// Handler for unique export menu item. All one needs to do is check that you recieve +/// an event that is of the same type as the one we are supposed to +/// handle. +fn watch_submenu_click(mut reader: EventReader, menu_handle: Res) { + for event in reader.iter() { + if event.clicked() && event.source() == menu_handle.custom_nested_menu { + println!("Submenu clicked"); + } + } +} + +/// The actual plugin +impl Plugin for MyMenuPlugin { + fn build(&self, app: &mut App) { + app.init_resource::() + .add_system(watch_unique_export_click) + .add_system(watch_submenu_click); + } +} + +/// Lets embed site editor in our application with our own plugin +fn main() { + App::new() + .add_plugin(SiteEditor) + .add_plugin(MyMenuPlugin) + .run(); +} diff --git a/rmf_site_editor/src/interaction/select_anchor.rs b/rmf_site_editor/src/interaction/select_anchor.rs index 9b50881b..0e2a64d9 100644 --- a/rmf_site_editor/src/interaction/select_anchor.rs +++ b/rmf_site_editor/src/interaction/select_anchor.rs @@ -19,7 +19,7 @@ use crate::{ interaction::*, site::{ drawing_editor::CurrentEditDrawing, Anchor, AnchorBundle, Category, Dependents, - DrawingMarker, Original, PathBehavior, Pending, + DrawingMarker, Original, PathBehavior, Pending, TextureNeedsAssignment, }, CurrentWorkspace, }; @@ -377,6 +377,25 @@ impl EdgePlacement { }) } + fn with_extra(self: Arc, f: F) -> Arc + where + F: Fn(&mut SelectAnchorPlacementParams, Entity) + Send + Sync + 'static, + { + let mut result = match Arc::try_unwrap(self) { + Ok(r) => r, + Err(r) => (*r).clone(), + }; + let base = result.create; + result.create = Arc::new( + move |params: &mut SelectAnchorPlacementParams, edge: Edge| { + let entity = base(params, edge); + f(params, entity); + entity + }, + ); + Arc::new(result) + } + fn update_dependencies( mut anchor_selection: Option<&mut AnchorSelection>, target: Entity, @@ -812,6 +831,25 @@ impl PathPlacement { }) } + fn with_extra(self: Arc, f: F) -> Arc + where + F: Fn(&mut SelectAnchorPlacementParams, Entity) + Send + Sync + 'static, + { + let mut result = match Arc::try_unwrap(self) { + Ok(r) => r, + Err(r) => (*r).clone(), + }; + let base = result.create; + result.create = Arc::new( + move |params: &mut SelectAnchorPlacementParams, path: Path| { + let entity = base(params, path); + f(params, entity); + entity + }, + ); + Arc::new(result) + } + fn at_index(&self, index: usize) -> Arc { Arc::new(Self { index: Some(index), @@ -1188,7 +1226,14 @@ impl SelectAnchorEdgeBuilder { pub fn for_wall(self) -> SelectAnchor { SelectAnchor { target: self.for_element, - placement: EdgePlacement::new::>(self.placement), + placement: EdgePlacement::new::>(self.placement).with_extra( + |params, entity| { + params + .commands + .entity(entity) + .insert(TextureNeedsAssignment); + }, + ), continuity: self.continuity, scope: Scope::General, } @@ -1304,7 +1349,14 @@ impl SelectAnchorPathBuilder { pub fn for_floor(self) -> SelectAnchor { SelectAnchor { target: self.for_element, - placement: PathPlacement::new::>(self.placement), + placement: PathPlacement::new::>(self.placement).with_extra( + |params, entity| { + params + .commands + .entity(entity) + .insert(TextureNeedsAssignment); + }, + ), continuity: self.continuity, scope: Scope::General, } diff --git a/rmf_site_editor/src/lib.rs b/rmf_site_editor/src/lib.rs index 3aa6d49b..cbde239f 100644 --- a/rmf_site_editor/src/lib.rs +++ b/rmf_site_editor/src/lib.rs @@ -24,8 +24,8 @@ mod settings; use settings::*; mod save; use save::*; -mod widgets; -use widgets::*; +pub mod widgets; +use widgets::{menu_bar::MenuPluginManager, *}; pub mod occupancy; use occupancy::OccupancyPlugin; @@ -128,86 +128,94 @@ pub fn run(command_line_args: Vec) { } } - #[cfg(target_arch = "wasm32")] - { - app.add_plugins( - DefaultPlugins - .build() - .disable::() - .set(WindowPlugin { - window: WindowDescriptor { - title: "RMF Site Editor".to_owned(), - canvas: Some(String::from("#rmf_site_editor_canvas")), + app.add_plugin(SiteEditor); + app.run(); +} + +pub struct SiteEditor; + +impl Plugin for SiteEditor { + fn build(&self, app: &mut App) { + #[cfg(target_arch = "wasm32")] + { + app.add_plugins( + DefaultPlugins + .build() + .disable::() + .set(WindowPlugin { + window: WindowDescriptor { + title: "RMF Site Editor".to_owned(), + canvas: Some(String::from("#rmf_site_editor_canvas")), + ..default() + }, ..default() - }, - ..default() - }) - .set(ImagePlugin { - default_sampler: SamplerDescriptor { - address_mode_u: AddressMode::Repeat, - address_mode_v: AddressMode::Repeat, - address_mode_w: AddressMode::Repeat, - ..Default::default() - }, - }) - .add_after::(SiteAssetIoPlugin), - ) - .add_system_set( - SystemSet::new() - .with_run_criteria(FixedTimestep::step(0.5)) - .with_system(check_browser_window_size), - ); - } + }) + .set(ImagePlugin { + default_sampler: SamplerDescriptor { + address_mode_u: AddressMode::Repeat, + address_mode_v: AddressMode::Repeat, + address_mode_w: AddressMode::Repeat, + ..Default::default() + }, + }) + .add_after::(SiteAssetIoPlugin), + ) + .add_system_set( + SystemSet::new() + .with_run_criteria(FixedTimestep::step(0.5)) + .with_system(check_browser_window_size), + ); + } - #[cfg(not(target_arch = "wasm32"))] - { - app.add_plugins( - DefaultPlugins - .build() - .disable::() - .set(WindowPlugin { - window: WindowDescriptor { - title: "RMF Site Editor".to_owned(), - width: 1600., - height: 900., + #[cfg(not(target_arch = "wasm32"))] + { + app.add_plugins( + DefaultPlugins + .build() + .disable::() + .set(WindowPlugin { + window: WindowDescriptor { + title: "RMF Site Editor".to_owned(), + width: 1600., + height: 900., + ..default() + }, + ..default() + }) + .set(ImagePlugin { + default_sampler: SamplerDescriptor { + address_mode_u: AddressMode::Repeat, + address_mode_v: AddressMode::Repeat, + address_mode_w: AddressMode::Repeat, + ..Default::default() + }, + }) + .set(LogPlugin { + filter: "bevy_asset=error,wgpu=error".to_string(), ..default() - }, - ..default() - }) - .set(ImagePlugin { - default_sampler: SamplerDescriptor { - address_mode_u: AddressMode::Repeat, - address_mode_v: AddressMode::Repeat, - address_mode_w: AddressMode::Repeat, - ..Default::default() - }, - }) - .set(LogPlugin { - filter: "bevy_asset=error,wgpu=error".to_string(), - ..default() - }) - .add_after::(SiteAssetIoPlugin), - ); + }) + .add_after::(SiteAssetIoPlugin), + ); + } + app.init_resource::() + .add_startup_system(init_settings) + .insert_resource(DirectionalLightShadowMap { size: 2048 }) + .add_plugin(LogHistoryPlugin) + .add_plugin(AabbUpdatePlugin) + .add_plugin(EguiPlugin) + .add_plugin(KeyboardInputPlugin) + .add_plugin(SavePlugin) + .add_plugin(SdfPlugin) + .add_state(AppState::MainMenu) + .add_plugin(MainMenuPlugin) + // .add_plugin(WarehouseGeneratorPlugin) + .add_plugin(WorkcellEditorPlugin) + .add_plugin(SitePlugin) + .add_plugin(InteractionPlugin) + .add_plugin(StandardUiLayout) + .add_plugin(AnimationPlugin) + .add_plugin(OccupancyPlugin) + .add_plugin(WorkspacePlugin) + .add_plugin(MenuPluginManager); } - - app.init_resource::() - .add_startup_system(init_settings) - .insert_resource(DirectionalLightShadowMap { size: 2048 }) - .add_plugin(LogHistoryPlugin) - .add_plugin(AabbUpdatePlugin) - .add_plugin(EguiPlugin) - .add_plugin(KeyboardInputPlugin) - .add_plugin(SavePlugin) - .add_plugin(SdfPlugin) - .add_state(AppState::MainMenu) - .add_plugin(MainMenuPlugin) - // .add_plugin(WarehouseGeneratorPlugin) - .add_plugin(WorkcellEditorPlugin) - .add_plugin(SitePlugin) - .add_plugin(InteractionPlugin) - .add_plugin(StandardUiLayout) - .add_plugin(AnimationPlugin) - .add_plugin(OccupancyPlugin) - .add_plugin(WorkspacePlugin) - .run(); } diff --git a/rmf_site_editor/src/log.rs b/rmf_site_editor/src/log.rs index 492dc796..d1688c57 100644 --- a/rmf_site_editor/src/log.rs +++ b/rmf_site_editor/src/log.rs @@ -141,7 +141,7 @@ impl Default for LogHistory { { let fmt_layer = tracing_subscriber::fmt::Layer::default(); let subscriber = subscriber.with(fmt_layer); - tracing::subscriber::set_global_default(subscriber); + tracing::subscriber::set_global_default(subscriber).ok(); } #[cfg(target_arch = "wasm32")] { diff --git a/rmf_site_editor/src/save.rs b/rmf_site_editor/src/save.rs index 3f96a2af..c69a0c46 100644 --- a/rmf_site_editor/src/save.rs +++ b/rmf_site_editor/src/save.rs @@ -102,7 +102,7 @@ pub fn dispatch_save_events( if let Some(file) = default_files.get(ws_root).ok().map(|f| f.0.clone()) { file } else { - let Some(file) = FileDialog::new().save_file() else { + let Some(file) = FileDialog::new().save_file() else { continue; }; file diff --git a/rmf_site_editor/src/sdf_loader.rs b/rmf_site_editor/src/sdf_loader.rs index 8567e07a..2750b0d5 100644 --- a/rmf_site_editor/src/sdf_loader.rs +++ b/rmf_site_editor/src/sdf_loader.rs @@ -75,7 +75,7 @@ async fn load_model<'a, 'b>( match root { Ok(root) => { if let Some(model) = root.model { - let path = load_context.path().clone().to_str().unwrap().to_string(); + let path = load_context.path().to_str().unwrap().to_string(); let sdf_root = SdfRoot { model, path }; load_context.set_default_asset(LoadedAsset::new(sdf_root)); Ok(()) diff --git a/rmf_site_editor/src/shapes.rs b/rmf_site_editor/src/shapes.rs index 828de29c..a30cca9f 100644 --- a/rmf_site_editor/src/shapes.rs +++ b/rmf_site_editor/src/shapes.rs @@ -1111,14 +1111,8 @@ pub(crate) fn make_closed_path_outline(mut initial_positions: Vec<[f32; 3]>) -> let p0 = Vec3::new(p0[0], p0[1], 0.0); let p1 = Vec3::new(p1[0], p1[1], 0.0); let p2 = Vec3::new(p2[0], p2[1], 0.0); - let v0 = match (p1 - p0).try_normalize() { - Some(v) => v, - None => continue, - }; - let v1 = match (p2 - p1).try_normalize() { - Some(v) => v, - None => continue, - }; + let v0 = (p1 - p0).normalize_or_zero(); + let v1 = (p2 - p1).normalize_or_zero(); // n: normal let n = Vec3::Z; @@ -1279,7 +1273,9 @@ pub(crate) fn make_finite_grid( let mut polylines: HashMap = HashMap::new(); let mut result = { - let Some(width) = weights.values().last().copied() else { return Vec::new() }; + let Some(width) = weights.values().last().copied() else { + return Vec::new(); + }; let mut axes: Vec<(Polyline, PolylineMaterial)> = Vec::new(); for (sign, x_axis_color, y_axis_color) in [ @@ -1308,7 +1304,9 @@ pub(crate) fn make_finite_grid( for n in 1..=count { let d = n as f32 * scale; let polylines = { - let Some(weight_key) = weights.keys().rev().find(|k| n % **k == 0) else { continue }; + let Some(weight_key) = weights.keys().rev().find(|k| n % **k == 0) else { + continue; + }; polylines.entry(*weight_key).or_default() }; diff --git a/rmf_site_editor/src/site/drawing_editor/alignment.rs b/rmf_site_editor/src/site/drawing_editor/alignment.rs index a69d8cae..cc6e98cb 100644 --- a/rmf_site_editor/src/site/drawing_editor/alignment.rs +++ b/rmf_site_editor/src/site/drawing_editor/alignment.rs @@ -59,10 +59,16 @@ pub fn align_site_drawings( ) { for AlignSiteDrawings(site) in events.iter() { let mut site_variables = SiteVariables::::default(); - let Ok(children) = sites.get(*site) else { continue }; + let Ok(children) = sites.get(*site) else { + continue; + }; for child in children { - let Ok((group, point)) = params.fiducials.get(*child) else { continue }; - let Ok(anchor) = params.anchors.get(point.0) else { continue }; + let Ok((group, point)) = params.fiducials.get(*child) else { + continue; + }; + let Ok(anchor) = params.anchors.get(point.0) else { + continue; + }; let Some(group) = group.0 else { continue }; let p = anchor.translation_for_category(Category::Fiducial); site_variables.fiducials.push(FiducialVariables { @@ -72,9 +78,13 @@ pub fn align_site_drawings( } for child in children { - let Ok(level_children) = levels.get(*child) else { continue }; + let Ok(level_children) = levels.get(*child) else { + continue; + }; for level_child in level_children { - let Ok((drawing_children, pose, ppm)) = params.drawings.get(*level_child) else { continue }; + let Ok((drawing_children, pose, ppm)) = params.drawings.get(*level_child) else { + continue; + }; let mut drawing_variables = DrawingVariables::::new( Vec2::from_slice(&pose.trans).as_dvec2(), pose.rot.yaw().radians() as f64, @@ -82,7 +92,9 @@ pub fn align_site_drawings( ); for child in drawing_children { if let Ok((group, point)) = params.fiducials.get(*child) { - let Ok(anchor) = params.anchors.get(point.0) else { continue }; + let Ok(anchor) = params.anchors.get(point.0) else { + continue; + }; let Some(group) = group.0 else { continue }; let p = anchor.translation_for_category(Category::Fiducial); drawing_variables.fiducials.push(FiducialVariables { @@ -92,8 +104,12 @@ pub fn align_site_drawings( } if let Ok((edge, distance)) = params.measurements.get(*child) { - let Ok([anchor0, anchor1]) = params.anchors.get_many(edge.array()) else { continue }; - let Some(in_meters) = distance.0 else { continue }; + let Ok([anchor0, anchor1]) = params.anchors.get_many(edge.array()) else { + continue; + }; + let Some(in_meters) = distance.0 else { + continue; + }; let in_meters = in_meters as f64; let p0 = Vec2::from_slice(anchor0.translation_for_category(Category::Fiducial)); @@ -117,7 +133,9 @@ pub fn align_site_drawings( // undo operation for this set of changes. let alignments = align_site(&site_variables); for (e, alignment) in alignments { - let Ok((_, mut pose, mut ppm)) = params.drawings.get_mut(e) else { continue }; + let Ok((_, mut pose, mut ppm)) = params.drawings.get_mut(e) else { + continue; + }; pose.trans[0] = alignment.translation.x as f32; pose.trans[1] = alignment.translation.y as f32; pose.rot = diff --git a/rmf_site_editor/src/site/fiducial.rs b/rmf_site_editor/src/site/fiducial.rs index 8e143b10..73323e55 100644 --- a/rmf_site_editor/src/site/fiducial.rs +++ b/rmf_site_editor/src/site/fiducial.rs @@ -102,18 +102,26 @@ pub fn update_fiducial_usage_tracker( .iter() .chain(changed_fiducial.iter().map(|p| p.get())) { - let Ok((_, mut tracker)) = unused_fiducial_trackers.get_mut(e) else { continue }; + let Ok((_, mut tracker)) = unused_fiducial_trackers.get_mut(e) else { + continue; + }; reset_fiducial_usage(e, &mut tracker, &fiducials, &fiducial_groups, &children); } for changed_group in &changed_fiducial_groups { - let Ok((_, name, site)) = fiducial_groups.get(changed_group) else { continue }; + let Ok((_, name, site)) = fiducial_groups.get(changed_group) else { + continue; + }; for (e, mut tracker) in &mut unused_fiducial_trackers { if tracker.site == site.get() { tracker.unused.insert(changed_group, name.0.clone()); - let Ok(scope_children) = children.get(e) else { continue }; + let Ok(scope_children) = children.get(e) else { + continue; + }; for child in scope_children { - let Ok(affiliation) = fiducials.get(*child) else { continue }; + let Ok(affiliation) = fiducials.get(*child) else { + continue; + }; if let Some(group) = affiliation.0 { if changed_group == group { tracker.unused.remove(&changed_group); @@ -132,7 +140,9 @@ pub fn update_fiducial_usage_tracker( } for (changed_fiducial, parent) in &changed_fiducials { - let Ok((e, mut tracker)) = unused_fiducial_trackers.get_mut(parent.get()) else { continue }; + let Ok((e, mut tracker)) = unused_fiducial_trackers.get_mut(parent.get()) else { + continue; + }; reset_fiducial_usage(e, &mut tracker, &fiducials, &fiducial_groups, &children); } @@ -177,9 +187,13 @@ fn reset_fiducial_usage( } } - let Ok(scope_children) = children.get(entity) else { return }; + let Ok(scope_children) = children.get(entity) else { + return; + }; for child in scope_children { - let Ok(affiliation) = fiducials.get(*child) else { continue }; + let Ok(affiliation) = fiducials.get(*child) else { + continue; + }; if let Some(group) = affiliation.0 { tracker.unused.remove(&group); if let Ok((_, name, _)) = fiducial_groups.get(group) { @@ -192,6 +206,7 @@ fn reset_fiducial_usage( pub fn add_fiducial_visuals( mut commands: Commands, fiducials: Query<(Entity, &Point, Option<&Transform>), Added>, + fiducial_groups: Query, With)>, mut dependents: Query<&mut Dependents, With>, assets: Res, ) { @@ -213,6 +228,10 @@ pub fn add_fiducial_visuals( .insert(Category::Fiducial) .insert(VisualCue::outline()); } + + for e in &fiducial_groups { + commands.entity(e).insert(Category::FiducialGroup); + } } pub fn assign_orphan_fiducials_to_parent( diff --git a/rmf_site_editor/src/site/floor.rs b/rmf_site_editor/src/site/floor.rs index 17106ddf..cc966383 100644 --- a/rmf_site_editor/src/site/floor.rs +++ b/rmf_site_editor/src/site/floor.rs @@ -211,7 +211,7 @@ pub fn add_floor_visuals( ( Entity, &Path, - &Texture, + &Affiliation, Option<&RecencyRank>, Option<&LayerVisibility>, Option<&Parent>, @@ -219,14 +219,16 @@ pub fn add_floor_visuals( Added, >, anchors: AnchorParams, + textures: Query<(Option<&Handle>, &Texture)>, mut dependents: Query<&mut Dependents, With>, mut meshes: ResMut>, mut materials: ResMut>, default_floor_vis: Query<(&GlobalFloorVisibility, &RecencyRanking)>, - asset_server: Res, ) { - for (e, new_floor, texture, rank, vis, parent) in &floors { - let mesh = make_floor_mesh(e, new_floor, texture, &anchors); + for (e, new_floor, texture_source, rank, vis, parent) in &floors { + let (base_color_texture, texture) = from_texture_source(texture_source, &textures); + + let mesh = make_floor_mesh(e, new_floor, &texture, &anchors); let mut cmd = commands.entity(e); let height = floor_height(rank); let default_vis = parent @@ -234,7 +236,7 @@ pub fn add_floor_visuals( .flatten(); let (base_color, alpha_mode) = floor_transparency(vis, default_vis); let material = materials.add(StandardMaterial { - base_color_texture: Some(asset_server.load(&String::from(&texture.source))), + base_color_texture, base_color, alpha_mode, ..default() @@ -268,24 +270,10 @@ pub fn add_floor_visuals( } } -pub fn update_changed_floor( - changed_path: Query< - (Entity, &FloorSegments, &Path, &Texture), - (Changed>, With), - >, +pub fn update_changed_floor_ranks( changed_rank: Query<(Entity, &RecencyRank), Changed>>, - anchors: AnchorParams, - mut mesh_assets: ResMut>, mut transforms: Query<&mut Transform>, - mut mesh_handles: Query<&mut Handle>, ) { - for (e, segments, path, texture) in &changed_path { - if let Ok(mut mesh) = mesh_handles.get_mut(segments.mesh) { - *mesh = mesh_assets.add(make_floor_mesh(e, path, texture, &anchors)); - } - // TODO(MXG): Update texture once we support textures - } - for (e, rank) in &changed_rank { if let Ok(mut tf) = transforms.get_mut(e) { tf.translation.z = floor_height(Some(rank)); @@ -293,9 +281,10 @@ pub fn update_changed_floor( } } -pub fn update_floor_for_moved_anchors( - floors: Query<(Entity, &FloorSegments, &Path, &Texture), With>, +pub fn update_floors_for_moved_anchors( + floors: Query<(Entity, &FloorSegments, &Path, &Affiliation), With>, anchors: AnchorParams, + textures: Query<(Option<&Handle>, &Texture)>, changed_anchors: Query< &Dependents, ( @@ -308,34 +297,48 @@ pub fn update_floor_for_moved_anchors( ) { for dependents in &changed_anchors { for dependent in dependents.iter() { - if let Some((e, segments, path, texture)) = floors.get(*dependent).ok() { + if let Some((e, segments, path, texture_source)) = floors.get(*dependent).ok() { + let (_, texture) = from_texture_source(texture_source, &textures); if let Ok(mut mesh) = mesh_handles.get_mut(segments.mesh) { - *mesh = mesh_assets.add(make_floor_mesh(e, path, texture, &anchors)); + *mesh = mesh_assets.add(make_floor_mesh(e, path, &texture, &anchors)); } } } } } -pub fn update_floor_for_changed_texture( +pub fn update_floors( + floors: Query<(&FloorSegments, &Path, &Affiliation), With>, changed_floors: Query< - (Entity, &FloorSegments, &Path, &Texture), - (Changed, With), + Entity, + ( + With, + Or<(Changed>, Changed>)>, + ), + >, + changed_texture_sources: Query< + &Members, + (With, Or<(Changed>, Changed)>), >, mut meshes: ResMut>, mut materials: ResMut>, mut mesh_handles: Query<&mut Handle>, material_handles: Query<&Handle>, anchors: AnchorParams, - asset_server: Res, + textures: Query<(Option<&Handle>, &Texture)>, ) { - for (e, segment, path, texture) in &changed_floors { + for e in changed_floors.iter().chain( + changed_texture_sources + .iter() + .flat_map(|members| members.iter().cloned()), + ) { + let Ok((segment, path, texture_source)) = floors.get(e) else { continue }; + let (base_color_texture, texture) = from_texture_source(texture_source, &textures); if let Ok(mut mesh) = mesh_handles.get_mut(segment.mesh) { if let Ok(material) = material_handles.get(segment.mesh) { - *mesh = meshes.add(make_floor_mesh(e, path, texture, &anchors)); + *mesh = meshes.add(make_floor_mesh(e, path, &texture, &anchors)); if let Some(mut material) = materials.get_mut(material) { - material.base_color_texture = - Some(asset_server.load(&String::from(&texture.source))); + material.base_color_texture = base_color_texture; } } } diff --git a/rmf_site_editor/src/site/group.rs b/rmf_site_editor/src/site/group.rs new file mode 100644 index 00000000..b8197337 --- /dev/null +++ b/rmf_site_editor/src/site/group.rs @@ -0,0 +1,125 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::{ + ecs::system::{Command, EntityCommands}, + prelude::*, +}; +use rmf_site_format::{Affiliation, Group}; + +pub struct MergeGroups { + pub from_group: Entity, + pub into_group: Entity, +} + +#[derive(Component, Deref)] +pub struct Members(Vec); + +impl Members { + pub fn iter(&self) -> impl Iterator { + self.0.iter() + } +} + +#[derive(Component, Clone, Copy)] +struct LastAffiliation(Option); + +pub fn update_affiliations( + mut affiliations: Query<&mut Affiliation>, + mut merge_groups: EventReader, + deleted_groups: RemovedComponents, +) { + for merge in merge_groups.iter() { + for mut affiliation in &mut affiliations { + if affiliation.0.is_some_and(|a| a == merge.from_group) { + affiliation.0 = Some(merge.into_group); + } + } + } + + for deleted in &deleted_groups { + for mut affiliation in &mut affiliations { + if affiliation.0.is_some_and(|a| a == deleted) { + affiliation.0 = None; + } + } + } +} + +pub fn update_members_of_groups( + mut commands: Commands, + changed_affiliation: Query<(Entity, &Affiliation), Changed>>, +) { + for (e, affiliation) in &changed_affiliation { + commands.entity(e).set_membership(affiliation.0); + } +} + +struct ChangeMembership { + member: Entity, + group: Option, +} + +impl Command for ChangeMembership { + fn write(self, world: &mut World) { + let last = world + .get_entity(self.member) + .map(|e| e.get::()) + .flatten() + .cloned(); + if let Some(last) = last { + if last.0 == self.group { + // There is no effect from this change + return; + } + + if let Some(last) = last.0 { + if let Some(mut e) = world.get_entity_mut(last) { + if let Some(mut members) = e.get_mut::() { + members.0.retain(|m| *m != self.member); + } + } + } + } + + if let Some(new_group) = self.group { + if let Some(mut e) = world.get_entity_mut(new_group) { + if let Some(mut members) = e.get_mut::() { + members.0.push(self.member); + } else { + e.insert(Members(vec![self.member])); + } + } + } + + if let Some(mut e) = world.get_entity_mut(self.member) { + e.insert(LastAffiliation(self.group)); + } + } +} + +pub trait SetMembershipExt { + fn set_membership(&mut self, group: Option) -> &mut Self; +} + +impl<'w, 's, 'a> SetMembershipExt for EntityCommands<'w, 's, 'a> { + fn set_membership(&mut self, group: Option) -> &mut Self { + let member = self.id(); + self.commands().add(ChangeMembership { member, group }); + self + } +} diff --git a/rmf_site_editor/src/site/load.rs b/rmf_site_editor/src/site/load.rs index f3c7938b..21b0b11a 100644 --- a/rmf_site_editor/src/site/load.rs +++ b/rmf_site_editor/src/site/load.rs @@ -39,7 +39,35 @@ pub struct LoadSite { pub default_file: Option, } -fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format::Site) -> Entity { +#[derive(ThisError, Debug)] +#[error("The site has a broken internal reference: {broken}")] +struct LoadSiteError { + site: Entity, + broken: u32, + // TODO(@mxgrey): reintroduce Backtrack when it's supported on stable + // backtrace: Backtrace, +} + +impl LoadSiteError { + fn new(site: Entity, broken: u32) -> Self { + Self { site, broken } + } +} + +trait LoadResult { + fn for_site(self, site: Entity) -> Result; +} + +impl LoadResult for Result { + fn for_site(self, site: Entity) -> Result { + self.map_err(|broken| LoadSiteError::new(site, broken)) + } +} + +fn generate_site_entities( + commands: &mut Commands, + site_data: &rmf_site_format::Site, +) -> Result { let mut id_to_entity = HashMap::new(); let mut highest_id = 0_u32; let mut consider_id = |consider| { @@ -49,11 +77,12 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: }; let mut site_cmd = commands.spawn(SpatialBundle::INVISIBLE_IDENTITY); + let site_id = site_cmd.id(); site_cmd .insert(Category::Site) .insert(site_data.properties.clone()) .insert(WorkspaceMarker) - .with_children(|site| { + .add_children(|site| { for (anchor_id, anchor) in &site_data.anchors { let anchor_entity = site .spawn(AnchorBundle::new(anchor.clone())) @@ -69,6 +98,12 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: consider_id(*group_id); } + for (group_id, group) in &site_data.textures { + let group_entity = site.spawn(group.clone()).insert(SiteID(*group_id)).id(); + id_to_entity.insert(*group_id, group_entity); + consider_id(*group_id); + } + for (level_id, level_data) in &site_data.levels { let mut level_cmd = site.spawn(SiteID(*level_id)); @@ -76,7 +111,7 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: .insert(SpatialBundle::INVISIBLE_IDENTITY) .insert(level_data.properties.clone()) .insert(Category::Level) - .with_children(|level| { + .add_children(|level| { for (anchor_id, anchor) in &level_data.anchors { let anchor_entity = level .spawn(AnchorBundle::new(anchor.clone())) @@ -88,7 +123,7 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (door_id, door) in &level_data.doors { let door_entity = level - .spawn(door.to_ecs(&id_to_entity)) + .spawn(door.convert(&id_to_entity).for_site(site_id)?) .insert(SiteID(*door_id)) .id(); id_to_entity.insert(*door_id, door_entity); @@ -99,7 +134,7 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: level .spawn(DrawingBundle::new(drawing.properties.clone())) .insert(SiteID(*drawing_id)) - .with_children(|drawing_parent| { + .add_children(|drawing_parent| { for (anchor_id, anchor) in &drawing.anchors { let anchor_entity = drawing_parent .spawn(AnchorBundle::new(anchor.clone())) @@ -110,23 +145,32 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: } for (fiducial_id, fiducial) in &drawing.fiducials { drawing_parent - .spawn(fiducial.to_ecs(&id_to_entity)) + .spawn( + fiducial + .convert(&id_to_entity) + .for_site(site_id)?, + ) .insert(SiteID(*fiducial_id)); consider_id(*fiducial_id); } for (measurement_id, measurement) in &drawing.measurements { drawing_parent - .spawn(measurement.to_ecs(&id_to_entity)) + .spawn( + measurement + .convert(&id_to_entity) + .for_site(site_id)?, + ) .insert(SiteID(*measurement_id)); consider_id(*measurement_id); } - }); + Ok(()) + })?; consider_id(*drawing_id); } for (floor_id, floor) in &level_data.floors { level - .spawn(floor.to_ecs(&id_to_entity)) + .spawn(floor.convert(&id_to_entity).for_site(site_id)?) .insert(SiteID(*floor_id)); consider_id(*floor_id); } @@ -150,11 +194,12 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (wall_id, wall) in &level_data.walls { level - .spawn(wall.to_ecs(&id_to_entity)) + .spawn(wall.convert(&id_to_entity).for_site(site_id)?) .insert(SiteID(*wall_id)); consider_id(*wall_id); } - }); + Ok(()) + })?; // TODO(MXG): Log when a RecencyRanking fails to load correctly. let level_entity = level_cmd @@ -178,34 +223,41 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: } for (lift_id, lift_data) in &site_data.lifts { - let lift = site - .spawn(SiteID(*lift_id)) + let mut lift = site.spawn(SiteID(*lift_id)); + lift.add_children(|lift| { + let lift_entity = lift.parent_entity(); + lift.spawn(SpatialBundle::default()) + .insert(CabinAnchorGroupBundle::default()) + .with_children(|anchor_group| { + for (anchor_id, anchor) in &lift_data.cabin_anchors { + let anchor_entity = anchor_group + .spawn(AnchorBundle::new(anchor.clone())) + .insert(SiteID(*anchor_id)) + .id(); + id_to_entity.insert(*anchor_id, anchor_entity); + consider_id(*anchor_id); + } + }); + + for (door_id, door) in &lift_data.cabin_doors { + let door_entity = lift + .spawn(door.convert(&id_to_entity).for_site(site_id)?) + .insert(Dependents::single(lift_entity)) + .id(); + id_to_entity.insert(*door_id, door_entity); + consider_id(*door_id); + } + Ok(()) + })?; + + let lift = lift .insert(Category::Lift) - .with_children(|lift| { - let lift_entity = lift.parent_entity(); - lift.spawn(SpatialBundle::default()) - .insert(CabinAnchorGroupBundle::default()) - .with_children(|anchor_group| { - for (anchor_id, anchor) in &lift_data.cabin_anchors { - let anchor_entity = anchor_group - .spawn(AnchorBundle::new(anchor.clone())) - .insert(SiteID(*anchor_id)) - .id(); - id_to_entity.insert(*anchor_id, anchor_entity); - consider_id(*anchor_id); - } - }); - - for (door_id, door) in &lift_data.cabin_doors { - let door_entity = lift - .spawn(door.to_ecs(&id_to_entity)) - .insert(Dependents::single(lift_entity)) - .id(); - id_to_entity.insert(*door_id, door_entity); - consider_id(*door_id); - } - }) - .insert(lift_data.properties.to_ecs(&id_to_entity)) + .insert( + lift_data + .properties + .convert(&id_to_entity) + .for_site(site_id)?, + ) .id(); id_to_entity.insert(*lift_id, lift); consider_id(*lift_id); @@ -213,7 +265,7 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (fiducial_id, fiducial) in &site_data.fiducials { let fiducial_entity = site - .spawn(fiducial.to_ecs(&id_to_entity)) + .spawn(fiducial.convert(&id_to_entity).for_site(site_id)?) .insert(SiteID(*fiducial_id)) .id(); id_to_entity.insert(*fiducial_id, fiducial_entity); @@ -232,7 +284,7 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (lane_id, lane_data) in &site_data.navigation.guided.lanes { let lane = site - .spawn(lane_data.to_ecs(&id_to_entity)) + .spawn(lane_data.convert(&id_to_entity).for_site(site_id)?) .insert(SiteID(*lane_id)) .id(); id_to_entity.insert(*lane_id, lane); @@ -241,13 +293,15 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (location_id, location_data) in &site_data.navigation.guided.locations { let location = site - .spawn(location_data.to_ecs(&id_to_entity)) + .spawn(location_data.convert(&id_to_entity).for_site(site_id)?) .insert(SiteID(*location_id)) .id(); id_to_entity.insert(*location_id, location); consider_id(*location_id); } - }); + + Ok(()) + })?; let nav_graph_rankings = match RecencyRanking::::from_u32( &site_data.navigation.guided.ranking, @@ -266,20 +320,24 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: site_cmd .insert(nav_graph_rankings) .insert(NextSiteID(highest_id + 1)); - let site_id = site_cmd.id(); // Make the lift cabin anchors that are used by doors subordinate for (lift_id, lift_data) in &site_data.lifts { for (_, door) in &lift_data.cabin_doors { for anchor in door.reference_anchors.array() { commands - .entity(*id_to_entity.get(&anchor).unwrap()) - .insert(Subordinate(Some(*id_to_entity.get(lift_id).unwrap()))); + .entity(*id_to_entity.get(&anchor).ok_or(anchor).for_site(site_id)?) + .insert(Subordinate(Some( + *id_to_entity + .get(lift_id) + .ok_or(*lift_id) + .for_site(site_id)?, + ))); } } } - return site_id; + return Ok(site_id); } pub fn load_site( @@ -289,7 +347,18 @@ pub fn load_site( mut site_display_state: ResMut>, ) { for cmd in load_sites.iter() { - let site = generate_site_entities(&mut commands, &cmd.site); + let site = match generate_site_entities(&mut commands, &cmd.site) { + Ok(site) => site, + Err(err) => { + commands.entity(err.site).despawn_recursive(); + error!( + "Failed to load the site entities because the file had an \ + internal inconsistency:\n{err:#?}\n---\nSite Data:\n{:#?}", + &cmd.site, + ); + continue; + } + }; if let Some(path) = &cmd.default_file { commands.entity(site).insert(DefaultFile(path.clone())); } @@ -308,6 +377,8 @@ pub fn load_site( pub enum ImportNavGraphError { #[error("The site we are importing into has a broken reference")] BrokenSiteReference, + #[error("The nav graph that is being imported has a broken reference inside of it")] + BrokenInternalReference(u32), #[error("The existing site is missing a level name required by the nav graphs: {0}")] MissingLevelName(String), #[error("The existing site is missing a lift name required by the nav graphs: {0}")] @@ -507,15 +578,21 @@ fn generate_imported_nav_graphs( } for (lane_id, lane_data) in &from_site_data.navigation.guided.lanes { + let lane_data = lane_data + .convert(&id_to_entity) + .map_err(ImportNavGraphError::BrokenInternalReference)?; params.commands.entity(into_site).add_children(|site| { - let e = site.spawn(lane_data.to_ecs(&id_to_entity)).id(); + let e = site.spawn(lane_data).id(); id_to_entity.insert(*lane_id, e); }); } for (location_id, location_data) in &from_site_data.navigation.guided.locations { + let location_data = location_data + .convert(&id_to_entity) + .map_err(ImportNavGraphError::BrokenInternalReference)?; params.commands.entity(into_site).add_children(|site| { - let e = site.spawn(location_data.to_ecs(&id_to_entity)).id(); + let e = site.spawn(location_data).id(); id_to_entity.insert(*location_id, e); }); } diff --git a/rmf_site_editor/src/site/mod.rs b/rmf_site_editor/src/site/mod.rs index ab1dd0ec..a24866d0 100644 --- a/rmf_site_editor/src/site/mod.rs +++ b/rmf_site_editor/src/site/mod.rs @@ -48,6 +48,9 @@ pub use fiducial::*; pub mod floor; pub use floor::*; +pub mod group; +pub use group::*; + pub mod lane; pub use lane::*; @@ -99,6 +102,9 @@ pub use site::*; pub mod site_visualizer; pub use site_visualizer::*; +pub mod texture; +pub use texture::*; + pub mod util; pub use util::*; @@ -160,6 +166,7 @@ impl Plugin for SitePlugin { .add_event::() .add_event::() .add_event::() + .add_event::() .add_plugin(ChangePlugin::>::default()) .add_plugin(RecallPlugin::>::default()) .add_plugin(ChangePlugin::::default()) @@ -215,6 +222,17 @@ impl Plugin for SitePlugin { .with_system(update_model_tentative_formats) .with_system(update_drawing_pixels_per_meter) .with_system(update_drawing_children_to_pixel_coordinates) + .with_system(fetch_image_for_texture) + .with_system(detect_last_selected_texture::) + .with_system( + apply_last_selected_texture:: + .after(detect_last_selected_texture::), + ) + .with_system(detect_last_selected_texture::) + .with_system( + apply_last_selected_texture:: + .after(detect_last_selected_texture::), + ) .with_system(update_material_for_display_color), ) .add_system_set( @@ -253,9 +271,9 @@ impl Plugin for SitePlugin { .with_system(update_changed_door) .with_system(update_door_for_moved_anchors) .with_system(add_floor_visuals) - .with_system(update_changed_floor) - .with_system(update_floor_for_moved_anchors) - .with_system(update_floor_for_changed_texture) + .with_system(update_floors) + .with_system(update_floors_for_moved_anchors) + .with_system(update_floors) .with_system(update_floor_visibility) .with_system(update_drawing_visibility) .with_system(add_lane_visuals) @@ -292,6 +310,8 @@ impl Plugin for SitePlugin { .with_system(update_constraint_for_changed_labels) .with_system(update_changed_constraint) .with_system(update_model_scenes) + .with_system(update_affiliations) + .with_system(update_members_of_groups.after(update_affiliations)) .with_system(handle_new_sdf_roots) .with_system(update_model_scales) .with_system(make_models_selectable) @@ -301,9 +321,8 @@ impl Plugin for SitePlugin { .with_system(update_drawing_rank) .with_system(add_physical_camera_visuals) .with_system(add_wall_visual) - .with_system(update_wall_edge) - .with_system(update_wall_for_moved_anchors) - .with_system(update_wall_for_changed_texture) + .with_system(update_walls_for_moved_anchors) + .with_system(update_walls) .with_system(update_transforms_for_changed_poses) .with_system(align_site_drawings) .with_system(export_lights), diff --git a/rmf_site_editor/src/site/save.rs b/rmf_site_editor/src/site/save.rs index 8f2f80c1..37a2567d 100644 --- a/rmf_site_editor/src/site/save.rs +++ b/rmf_site_editor/src/site/save.rs @@ -65,17 +65,25 @@ pub enum SiteGenerationError { // TODO(@mxgrey): Remove this when we no longer need to de-parent drawings while // editing them. fn assemble_edited_drawing(world: &mut World) { - let Some(c) = world.get_resource::().copied() else { return }; + let Some(c) = world.get_resource::().copied() else { + return; + }; let Some(c) = c.target() else { return }; - let Some(mut level) = world.get_entity_mut(c.level) else { return }; + let Some(mut level) = world.get_entity_mut(c.level) else { + return; + }; level.push_children(&[c.drawing]); } /// Revert the drawing back to the root so it can continue to be edited. fn disassemble_edited_drawing(world: &mut World) { - let Some(c) = world.get_resource::().copied() else { return }; + let Some(c) = world.get_resource::().copied() else { + return; + }; let Some(c) = c.target() else { return }; - let Some(mut level) = world.get_entity_mut(c.level) else { return }; + let Some(mut level) = world.get_entity_mut(c.level) else { + return; + }; level.remove_children(&[c.drawing]); } @@ -111,7 +119,12 @@ fn assign_site_ids(world: &mut World, site: Entity) -> Result<(), SiteGeneration Query< Entity, ( - Or<(With, With, With)>, + Or<( + With, + With, + With, + With, + )>, Without, ), >, @@ -285,7 +298,7 @@ fn generate_levels( ( &Path, Option<&Original>>, - &Texture, + &Affiliation, &PreferredSemiTransparency, &SiteID, &Parent, @@ -330,7 +343,7 @@ fn generate_levels( ( &Edge, Option<&Original>>, - &Texture, + &Affiliation, &SiteID, &Parent, ), @@ -525,11 +538,17 @@ fn generate_levels( if let Ok((_, _, _, _, level_id, _, _, _)) = q_levels.get(parent.get()) { if let Some(level) = levels.get_mut(&level_id.0) { let anchors = get_anchor_id_path(&path)?; + let texture = if let Affiliation(Some(e)) = texture { + Affiliation(Some(get_group_id(*e)?)) + } else { + Affiliation(None) + }; + level.floors.insert( id.0, Floor { anchors, - texture: texture.clone(), + texture, preferred_semi_transparency: preferred_alpha.clone(), marker: FloorMarker, }, @@ -592,11 +611,17 @@ fn generate_levels( if let Ok((_, _, _, _, level_id, _, _, _)) = q_levels.get(parent.get()) { if let Some(level) = levels.get_mut(&level_id.0) { let anchors = get_anchor_id_edge(edge)?; + let texture = if let Affiliation(Some(e)) = texture { + Affiliation(Some(get_group_id(*e)?)) + } else { + Affiliation(None) + }; + level.walls.insert( id.0, Wall { anchors, - texture: texture.clone(), + texture, marker: WallMarker, }, ); @@ -866,6 +891,37 @@ fn generate_fiducial_groups( Ok(fiducial_groups) } +fn generate_texture_groups( + world: &mut World, + parent: Entity, +) -> Result, SiteGenerationError> { + let mut state: SystemState<( + Query<(&NameInSite, &Texture, &SiteID), With>, + Query<&Children>, + )> = SystemState::new(world); + + let (q_groups, q_children) = state.get(world); + + let Ok(children) = q_children.get(parent) else { + return Ok(BTreeMap::new()); + }; + + let mut texture_groups = BTreeMap::new(); + for child in children { + let Ok((name, texture, site_id)) = q_groups.get(*child) else { continue }; + texture_groups.insert( + site_id.0, + TextureGroup { + name: name.clone(), + texture: texture.clone(), + group: Default::default(), + }, + ); + } + + Ok(texture_groups) +} + fn generate_nav_graphs( world: &mut World, site: Entity, @@ -1054,6 +1110,7 @@ pub fn generate_site( let lifts = generate_lifts(world, site)?; let fiducials = generate_fiducials(world, site)?; let fiducial_groups = generate_fiducial_groups(world, site)?; + let textures = generate_texture_groups(world, site)?; let nav_graphs = generate_nav_graphs(world, site)?; let lanes = generate_lanes(world, site)?; let locations = generate_locations(world, site)?; @@ -1075,6 +1132,7 @@ pub fn generate_site( lifts, fiducials, fiducial_groups, + textures, navigation: Navigation { guided: Guided { graphs: nav_graphs, diff --git a/rmf_site_editor/src/site/texture.rs b/rmf_site_editor/src/site/texture.rs new file mode 100644 index 00000000..e7ee58f1 --- /dev/null +++ b/rmf_site_editor/src/site/texture.rs @@ -0,0 +1,119 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use bevy::prelude::*; +use rmf_site_format::{Affiliation, Category, Group, Texture}; + +#[derive(Component)] +pub struct TextureNeedsAssignment; + +pub fn fetch_image_for_texture( + mut commands: Commands, + mut changed_textures: Query<(Entity, Option<&mut Handle>, &Texture), Changed>, + new_textures: Query>, + asset_server: Res, +) { + for (e, image, texture) in &mut changed_textures { + if let Some(mut image) = image { + *image = asset_server.load(String::from(&texture.source)); + } else { + let image: Handle = asset_server.load(String::from(&texture.source)); + commands.entity(e).insert(image); + } + } + + for e in &new_textures { + commands.entity(e).insert(Category::TextureGroup); + } +} + +pub fn detect_last_selected_texture( + mut commands: Commands, + parents: Query<&Parent>, + mut last_selected: Query<&mut LastSelectedTexture>, + changed_affiliations: Query<&Affiliation, (Changed>, With)>, + removed_groups: RemovedComponents, +) { + if let Some(Affiliation(Some(affiliation))) = changed_affiliations.iter().last() { + let Ok(parent) = parents.get(*affiliation) else { return }; + if let Ok(mut last) = last_selected.get_mut(parent.get()) { + last.selection = Some(*affiliation); + } else { + commands.entity(parent.get()).insert(LastSelectedTexture { + selection: Some(*affiliation), + marker: std::marker::PhantomData::::default(), + }); + } + } + + for group in &removed_groups { + for mut last in &mut last_selected { + if last.selection.is_some_and(|l| l == group) { + last.selection = None; + } + } + } +} + +pub fn apply_last_selected_texture( + mut commands: Commands, + parents: Query<&Parent>, + last_selected: Query<&LastSelectedTexture>, + mut unassigned: Query< + (Entity, &mut Affiliation), + (With, With), + >, +) { + for (e, mut affiliation) in &mut unassigned { + let mut search = e; + let last = loop { + if let Ok(last) = last_selected.get(search) { + break Some(last); + } + + if let Ok(parent) = parents.get(search) { + search = parent.get(); + } else { + break None; + } + }; + if let Some(last) = last { + affiliation.0 = last.selection; + } + commands.entity(e).remove::(); + } +} + +#[derive(Component)] +pub struct LastSelectedTexture { + selection: Option, + marker: std::marker::PhantomData, +} + +// Helper function for entities that need to access their affiliated texture +// information. +pub fn from_texture_source( + texture_source: &Affiliation, + textures: &Query<(Option<&Handle>, &Texture)>, +) -> (Option>, Texture) { + texture_source + .0 + .map(|t| textures.get(t).ok()) + .flatten() + .map(|(i, t)| (i.cloned(), t.clone())) + .unwrap_or_else(|| (None, Texture::default())) +} diff --git a/rmf_site_editor/src/site/wall.rs b/rmf_site_editor/src/site/wall.rs index 2046550e..c1912c1e 100644 --- a/rmf_site_editor/src/site/wall.rs +++ b/rmf_site_editor/src/site/wall.rs @@ -57,14 +57,15 @@ fn make_wall( pub fn add_wall_visual( mut commands: Commands, - walls: Query<(Entity, &Edge, &Texture), Added>, + walls: Query<(Entity, &Edge, &Affiliation), Added>, anchors: AnchorParams, + textures: Query<(Option<&Handle>, &Texture)>, mut dependents: Query<&mut Dependents, With>, mut meshes: ResMut>, mut materials: ResMut>, - asset_server: Res, ) { - for (e, edge, texture) in &walls { + for (e, edge, texture_source) in &walls { + let (base_color_texture, texture) = from_texture_source(texture_source, &textures); let (base_color, alpha_mode) = if let Some(alpha) = texture.alpha.filter(|a| a < &1.0) { (*Color::default().set_a(alpha), AlphaMode::Blend) } else { @@ -73,9 +74,9 @@ pub fn add_wall_visual( commands .entity(e) .insert(PbrBundle { - mesh: meshes.add(make_wall(e, edge, texture, &anchors)), + mesh: meshes.add(make_wall(e, edge, &texture, &anchors)), material: materials.add(StandardMaterial { - base_color_texture: Some(asset_server.load(&String::from(&texture.source))), + base_color_texture, base_color, alpha_mode, ..default() @@ -94,22 +95,18 @@ pub fn add_wall_visual( } } -pub fn update_wall_edge( +pub fn update_walls_for_moved_anchors( mut walls: Query< - (Entity, &Edge, &Texture, &mut Handle), - (With, Changed>), + ( + Entity, + &Edge, + &Affiliation, + &mut Handle, + ), + With, >, anchors: AnchorParams, - mut meshes: ResMut>, -) { - for (e, edge, texture, mut mesh) in &mut walls { - *mesh = meshes.add(make_wall(e, edge, texture, &anchors)); - } -} - -pub fn update_wall_for_moved_anchors( - mut walls: Query<(Entity, &Edge, &Texture, &mut Handle), With>, - anchors: AnchorParams, + textures: Query<(Option<&Handle>, &Texture)>, changed_anchors: Query< &Dependents, ( @@ -121,38 +118,55 @@ pub fn update_wall_for_moved_anchors( ) { for dependents in &changed_anchors { for dependent in dependents.iter() { - if let Some((e, edge, texture, mut mesh)) = walls.get_mut(*dependent).ok() { - *mesh = meshes.add(make_wall(e, edge, texture, &anchors)); + if let Some((e, edge, texture_source, mut mesh)) = walls.get_mut(*dependent).ok() { + let (_, texture) = from_texture_source(texture_source, &textures); + *mesh = meshes.add(make_wall(e, edge, &texture, &anchors)); } } } } -pub fn update_wall_for_changed_texture( - mut changed_walls: Query< +pub fn update_walls( + mut walls: Query< ( - Entity, &Edge, - &Texture, + &Affiliation, &mut Handle, &Handle, ), - (Changed, With), + With, + >, + changed_walls: Query< + Entity, + ( + With, + Or<(Changed>, Changed>)>, + ), + >, + changed_texture_sources: Query< + &Members, + (With, Or<(Changed>, Changed)>), >, mut meshes: ResMut>, mut materials: ResMut>, anchors: AnchorParams, - asset_server: Res, + textures: Query<(Option<&Handle>, &Texture)>, ) { - for (e, edge, texture, mut mesh, material) in &mut changed_walls { - *mesh = meshes.add(make_wall(e, edge, texture, &anchors)); + for e in changed_walls.iter().chain( + changed_texture_sources + .iter() + .flat_map(|members| members.iter().cloned()), + ) { + let Ok((edge, texture_source, mut mesh, material)) = walls.get_mut(e) else { continue }; + let (base_color_texture, texture) = from_texture_source(texture_source, &textures); + *mesh = meshes.add(make_wall(e, edge, &texture, &anchors)); if let Some(mut material) = materials.get_mut(material) { let (base_color, alpha_mode) = if let Some(alpha) = texture.alpha.filter(|a| a < &1.0) { (*Color::default().set_a(alpha), AlphaMode::Blend) } else { (Color::default(), AlphaMode::Opaque) }; - material.base_color_texture = Some(asset_server.load(&String::from(&texture.source))); + material.base_color_texture = base_color_texture; material.base_color = base_color; material.alpha_mode = alpha_mode; } diff --git a/rmf_site_editor/src/site_asset_io.rs b/rmf_site_editor/src/site_asset_io.rs index 76e535e8..660777ae 100644 --- a/rmf_site_editor/src/site_asset_io.rs +++ b/rmf_site_editor/src/site_asset_io.rs @@ -166,6 +166,10 @@ impl SiteAssetIo { "textures/trash.png".to_owned(), include_bytes!("../../assets/textures/trash.png").to_vec(), ); + self.bundled_assets.insert( + "textures/merge.png".to_owned(), + include_bytes!("../../assets/textures/merge.png").to_vec(), + ); self.bundled_assets.insert( "textures/confirm.png".to_owned(), include_bytes!("../../assets/textures/confirm.png").to_vec(), diff --git a/rmf_site_editor/src/widgets/console.rs b/rmf_site_editor/src/widgets/console.rs index 2bdfa881..140da674 100644 --- a/rmf_site_editor/src/widgets/console.rs +++ b/rmf_site_editor/src/widgets/console.rs @@ -15,12 +15,8 @@ * */ -use crate::{log::*, site::*, widgets::AppEvents}; -use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::{ - egui::{self, CollapsingHeader, Color32, FontId, RichText, Ui}, - EguiContext, -}; +use crate::{log::*, widgets::AppEvents}; +use bevy_egui::egui::{self, CollapsingHeader, Color32, RichText, Ui}; pub struct ConsoleWidget<'a, 'w2, 's2> { events: &'a mut AppEvents<'w2, 's2>, @@ -139,13 +135,33 @@ fn print_log(ui: &mut egui::Ui, element: &LogHistoryElement) { LogCategory::Error => Color32::RED, LogCategory::Bevy => Color32::LIGHT_BLUE, }; + + let mut truncated = false; + let msg = if element.log.message.len() > 80 { + truncated = true; + &element.log.message[..80] + } else { + &element.log.message + }; + + let msg = if let Some(nl) = msg.find("\n") { + truncated = true; + &msg[..nl] + } else { + msg + }; + ui.label(RichText::new(element.log.category.to_string()).color(category_text_color)); // Selecting the label allows users to copy log entry to clipboard - if ui - .selectable_label(false, element.log.message.to_string()) - .clicked() - { + if ui.selectable_label(false, msg).clicked() { ui.output().copied_text = element.log.category.to_string() + &element.log.message; } + + if truncated { + ui.label(" [...]").on_hover_text( + "Some of the message is hidden. Click on it to copy the \ + full text to your clipboard.", + ); + } }); } diff --git a/rmf_site_editor/src/widgets/icons.rs b/rmf_site_editor/src/widgets/icons.rs index accc1101..362a04b8 100644 --- a/rmf_site_editor/src/widgets/icons.rs +++ b/rmf_site_editor/src/widgets/icons.rs @@ -55,6 +55,7 @@ pub struct Icons { pub edit: Icon, pub exit: Icon, pub trash: Icon, + pub merge: Icon, pub confirm: Icon, pub add: Icon, pub reject: Icon, @@ -79,6 +80,7 @@ impl FromWorld for Icons { let edit = IconBuilder::new("textures/edit.png", &asset_server); let exit = IconBuilder::new("textures/exit.png", &asset_server); let trash = IconBuilder::new("textures/trash.png", &asset_server); + let merge = IconBuilder::new("textures/merge.png", &asset_server); let confirm = IconBuilder::new("textures/confirm.png", &asset_server); let add = IconBuilder::new("textures/add.png", &asset_server); let reject = IconBuilder::new("textures/reject.png", &asset_server); @@ -104,6 +106,7 @@ impl FromWorld for Icons { edit: edit.build(&mut egui_context), exit: exit.build(&mut egui_context), trash: trash.build(&mut egui_context), + merge: merge.build(&mut egui_context), confirm: confirm.build(&mut egui_context), add: add.build(&mut egui_context), reject: reject.build(&mut egui_context), diff --git a/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs b/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs index f406ed68..0a6efe41 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs @@ -25,7 +25,7 @@ use bevy_egui::egui::{ComboBox, ImageButton, Ui}; #[derive(Resource, Default)] pub struct SearchForFiducial(pub String); -enum SearchResult { +pub(crate) enum SearchResult { Empty, Current, NoMatch, @@ -34,7 +34,7 @@ enum SearchResult { } impl SearchResult { - fn consider(&mut self, entity: Entity) { + pub(crate) fn consider(&mut self, entity: Entity) { match self { Self::NoMatch => { *self = SearchResult::Match(entity); @@ -46,7 +46,7 @@ impl SearchResult { } } - fn conflict(&mut self, text: &'static str) { + pub(crate) fn conflict(&mut self, text: &'static str) { match self { // If we already found a match then don't change the behavior Self::Match(_) | Self::Current | Self::Conflict(_) | Self::Empty => {} @@ -85,8 +85,12 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectFiducialWidget<'a, 'w1, 'w2, 's1, 's2> { } pub fn show(self, ui: &mut Ui) { - let Ok((affiliation, parent)) = self.params.fiducials.get(self.entity) else { return }; - let Ok(tracker) = self.params.usage.get(parent.get()) else { return }; + let Ok((affiliation, parent)) = self.params.fiducials.get(self.entity) else { + return; + }; + let Ok(tracker) = self.params.usage.get(parent.get()) else { + return; + }; ui.separator(); ui.label("Affiliation"); @@ -109,7 +113,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectFiducialWidget<'a, 'w1, 'w2, 's1, 's2> { let mut result = SearchResult::NoMatch; let mut any_partial_matches = false; - if *search == "" { + if search.is_empty() { // An empty string should not be used result = SearchResult::Empty; } @@ -231,14 +235,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectFiducialWidget<'a, 'w1, 'w2, 's1, 's2> { let mut new_affiliation = affiliation.clone(); ui.horizontal(|ui| { if ui - .add(ImageButton::new(self.params.icons.trash.egui(), [18., 18.])) - .on_hover_text("Remove this fiducial from its group") + .add(ImageButton::new(self.params.icons.exit.egui(), [18., 18.])) + .on_hover_text("Remove this fiducial from its current group") .clicked() { - self.events - .change_more - .affiliation - .send(Change::new(Affiliation(None), self.entity)); + new_affiliation = Affiliation(None); } ComboBox::from_id_source("fiducial_affiliation") .selected_text(selected_text) diff --git a/rmf_site_editor/src/widgets/inspector/inspect_group.rs b/rmf_site_editor/src/widgets/inspector/inspect_group.rs new file mode 100644 index 00000000..8917260f --- /dev/null +++ b/rmf_site_editor/src/widgets/inspector/inspect_group.rs @@ -0,0 +1,86 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apahe License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + site::{Affiliation, Change, DefaultFile, Group, Members, NameInSite, SiteID, Texture}, + widgets::{ + inspector::{InspectTexture, SelectionWidget}, + AppEvents, + }, + Icons, +}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{CollapsingHeader, RichText, Ui}; + +#[derive(SystemParam)] +pub struct InspectGroupParams<'w, 's> { + pub is_group: Query<'w, 's, (), With>, + pub affiliation: Query<'w, 's, &'static Affiliation>, + pub textures: Query<'w, 's, &'static Texture>, + pub members: Query<'w, 's, &'static Members>, + pub site_id: Query<'w, 's, &'static SiteID>, + pub icons: Res<'w, Icons>, +} + +pub struct InspectGroup<'a, 'w1, 'w2, 's1, 's2> { + group: Entity, + selection: Entity, + default_file: Option<&'a DefaultFile>, + params: &'a InspectGroupParams<'w1, 's1>, + events: &'a mut AppEvents<'w2, 's2>, +} + +impl<'a, 'w1, 'w2, 's1, 's2> InspectGroup<'a, 'w1, 'w2, 's1, 's2> { + pub fn new( + group: Entity, + selection: Entity, + default_file: Option<&'a DefaultFile>, + params: &'a InspectGroupParams<'w1, 's1>, + events: &'a mut AppEvents<'w2, 's2>, + ) -> Self { + Self { + group, + selection, + default_file, + params, + events, + } + } + + pub fn show(self, ui: &mut Ui) { + if let Ok(texture) = self.params.textures.get(self.group) { + ui.label(RichText::new("Texture Properties").size(18.0)); + if let Some(new_texture) = InspectTexture::new(texture, self.default_file).show(ui) { + self.events + .change_more + .texture + .send(Change::new(new_texture, self.group)); + } + ui.add_space(10.0); + } + if let Ok(members) = self.params.members.get(self.group) { + CollapsingHeader::new("Members").show(ui, |ui| { + for member in members.iter() { + let site_id = self.params.site_id.get(self.group).ok().cloned(); + SelectionWidget::new(*member, site_id, &self.params.icons, self.events) + .as_selected(self.selection == *member) + .show(ui); + } + }); + } + } +} diff --git a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs index 4c1e7ebf..4a80c9b0 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -16,37 +16,284 @@ */ use crate::{ - site::DefaultFile, - inspector::{InspectAssetSource, InspectValue}, + inspector::{InspectAssetSource, InspectValue, SearchResult}, + site::{Category, Change, DefaultFile}, widgets::egui::RichText, + AppEvents, Icons, WorkspaceMarker, +}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{ComboBox, Grid, ImageButton, Ui}; +use rmf_site_format::{ + Affiliation, FloorMarker, Group, NameInSite, RecallAssetSource, Texture, TextureGroup, + WallMarker, }; -use bevy_egui::egui::{Grid, Ui}; -use rmf_site_format::{RecallAssetSource, Texture}; -pub struct InspectTexture<'a> { - pub texture: &'a Texture, - pub default_file: Option<&'a DefaultFile>, +#[derive(Resource, Default)] +pub struct SearchForTexture(pub String); + +#[derive(SystemParam)] +pub struct InspectTextureAffiliationParams<'w, 's> { + with_texture: Query< + 'w, + 's, + (&'static Category, &'static Affiliation), + Or<(With, With)>, + >, + texture_groups: Query<'w, 's, (&'static NameInSite, &'static Texture), With>, + parents: Query<'w, 's, &'static Parent>, + sites: Query<'w, 's, &'static Children, With>, + icons: Res<'w, Icons>, } -impl<'a> InspectTexture<'a> { +pub struct InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { + entity: Entity, + default_file: Option<&'a DefaultFile>, + params: &'a InspectTextureAffiliationParams<'w1, 's1>, + events: &'a mut AppEvents<'w2, 's2>, +} + +impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { pub fn new( - texture: &'a Texture, + entity: Entity, default_file: Option<&'a DefaultFile>, + params: &'a InspectTextureAffiliationParams<'w1, 's1>, + events: &'a mut AppEvents<'w2, 's2>, ) -> Self { - Self { texture, default_file } + Self { + entity, + default_file, + params, + events, + } + } + + pub fn show(self, ui: &mut Ui) { + let Ok((category, affiliation)) = self.params.with_texture.get(self.entity) else { return }; + let mut site = self.entity; + let children = loop { + if let Ok(children) = self.params.sites.get(site) { + break children; + } + + if let Ok(parent) = self.params.parents.get(site) { + site = parent.get(); + } else { + return; + } + }; + let site = site; + + let search = &mut self.events.change_more.search_for_texture.0; + + let mut any_partial_matches = false; + let mut result = SearchResult::NoMatch; + for child in children { + let Ok((name, _)) = self.params.texture_groups.get(*child) else { continue }; + if name.0.contains(&*search) { + any_partial_matches = true; + } + + if name.0 == *search { + result.consider(*child); + } + } + let any_partial_matches = any_partial_matches; + + if search.is_empty() { + result = SearchResult::Empty; + } + + if let (SearchResult::Match(e), Some(current)) = (&result, &affiliation.0) { + if *e == *current { + result = SearchResult::Current; + } + } + + ui.separator(); + ui.label("Texture"); + ui.horizontal(|ui| { + if any_partial_matches { + if ui + .add(ImageButton::new( + self.params.icons.search.egui(), + [18., 18.], + )) + .on_hover_text("Search results for this text can be found below") + .clicked() + { + info!("Use the drop-down box to choose a texture"); + } + } else { + ui.add(ImageButton::new(self.params.icons.empty.egui(), [18., 18.])) + .on_hover_text("No search results can be found for this text"); + } + + match result { + SearchResult::Empty => { + if ui + .add(ImageButton::new( + self.params.icons.hidden.egui(), + [18., 18.], + )) + .on_hover_text("An empty string is not a good texture name") + .clicked() + { + warn!("You should not use an empty string as a texture name"); + } + } + SearchResult::Current => { + if ui + .add(ImageButton::new( + self.params.icons.selected.egui(), + [18., 18.], + )) + .on_hover_text("This is the name of the currently selected texture") + .clicked() + { + info!("This texture is already selected"); + } + } + SearchResult::NoMatch => { + if ui + .add(ImageButton::new(self.params.icons.add.egui(), [18., 18.])) + .on_hover_text(if affiliation.0.is_some() { + "Create a new copy of the current texture" + } else { + "Create a new texture" + }) + .clicked() + { + let new_texture = if let Some((_, t)) = affiliation + .0 + .map(|a| self.params.texture_groups.get(a).ok()) + .flatten() + { + t.clone() + } else { + Texture::default() + }; + + let new_texture_group = self + .events + .commands + .spawn(TextureGroup { + name: NameInSite(search.clone()), + texture: new_texture, + group: default(), + }) + .set_parent(site) + .id(); + self.events.change_more.affiliation.send(Change::new( + Affiliation(Some(new_texture_group)), + self.entity, + )); + } + } + SearchResult::Match(group) => { + if ui + .add(ImageButton::new( + self.params.icons.confirm.egui(), + [18., 18.], + )) + .on_hover_text("Select this texture") + .clicked() + { + self.events + .change_more + .affiliation + .send(Change::new(Affiliation(Some(group)), self.entity)); + } + } + SearchResult::Conflict(text) => { + if ui + .add(ImageButton::new( + self.params.icons.reject.egui(), + [18., 18.], + )) + .on_hover_text(text) + .clicked() + { + warn!("Cannot set {search} as the texture: {text}"); + } + } + } + + ui.text_edit_singleline(search) + .on_hover_text("Search for or create a new texture"); + }); + + let (current_texture_name, current_texture) = if let Some(a) = affiliation.0 { + self.params + .texture_groups + .get(a) + .ok() + .map(|(n, t)| (n.0.as_str(), Some((a, t)))) + } else { + None + } + .unwrap_or(("", None)); + + let mut new_affiliation = affiliation.clone(); + ui.horizontal(|ui| { + if ui + .add(ImageButton::new(self.params.icons.exit.egui(), [18., 18.])) + .on_hover_text(format!("Remove this texture from the {}", category.label())) + .clicked() + { + new_affiliation = Affiliation(None); + } + + ComboBox::from_id_source("texture_affiliation") + .selected_text(current_texture_name) + .show_ui(ui, |ui| { + for child in children { + if affiliation.0.is_some_and(|a| a == *child) { + continue; + } + + if let Ok((n, _)) = self.params.texture_groups.get(*child) { + if n.0.contains(&*search) { + let select_affiliation = Affiliation(Some(*child)); + ui.selectable_value(&mut new_affiliation, select_affiliation, &n.0); + } + } + } + }); + }); + + if new_affiliation != *affiliation { + self.events + .change_more + .affiliation + .send(Change::new(new_affiliation, self.entity)); + } + ui.add_space(10.0); + } +} + +pub struct InspectTexture<'a> { + texture: &'a Texture, + default_file: Option<&'a DefaultFile>, +} + +impl<'a> InspectTexture<'a> { + pub fn new(texture: &'a Texture, default_file: Option<&'a DefaultFile>) -> Self { + Self { + texture, + default_file, + } } pub fn show(self, ui: &mut Ui) -> Option { let mut new_texture = self.texture.clone(); - ui.label(RichText::new("Texture Properties").size(18.0)); // TODO(luca) recall - if let Some(new_source) = - InspectAssetSource::new( - &new_texture.source, - &RecallAssetSource::default(), - self.default_file, - ).show(ui) + if let Some(new_source) = InspectAssetSource::new( + &new_texture.source, + &RecallAssetSource::default(), + self.default_file, + ) + .show(ui) { new_texture.source = new_source; } @@ -55,7 +302,7 @@ impl<'a> InspectTexture<'a> { if let Some(width) = new_texture.width { if let Some(new_width) = InspectValue::::new(String::from("Width"), width) .clamp_range(0.001..=std::f32::MAX) - .speed(0.1) + .speed(0.01) .tooltip("Texture width in meters".to_string()) .show(ui) { @@ -66,7 +313,7 @@ impl<'a> InspectTexture<'a> { if let Some(height) = new_texture.height { if let Some(new_height) = InspectValue::::new(String::from("Height"), height) .clamp_range(0.001..=std::f32::MAX) - .speed(0.1) + .speed(0.01) .tooltip("Texture height in meters".to_string()) .show(ui) { @@ -87,11 +334,7 @@ impl<'a> InspectTexture<'a> { } }); - if new_texture.width != self.texture.width - || new_texture.height != self.texture.height - || new_texture.alpha != self.texture.alpha - || new_texture.source != self.texture.source - { + if new_texture != *self.texture { Some(new_texture) } else { None diff --git a/rmf_site_editor/src/widgets/inspector/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index a088683d..fb685518 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -36,6 +36,9 @@ pub use inspect_edge::*; pub mod inspect_fiducial; pub use inspect_fiducial::*; +pub mod inspect_group; +pub use inspect_group::*; + pub mod inspect_is_static; pub use inspect_is_static::*; @@ -96,13 +99,13 @@ use crate::{ interaction::{Selection, SpawnPreview}, site::{ AlignSiteDrawings, BeginEditDrawing, Category, Change, DefaultFile, DrawingMarker, - EdgeLabels, LayerVisibility, Original, SiteID, + EdgeLabels, LayerVisibility, Members, Original, SiteID, }, widgets::AppEvents, AppState, }; use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{Button, RichText, Ui}; +use bevy_egui::egui::{Button, CollapsingHeader, RichText, Ui}; use rmf_site_format::*; // Bevy seems to have a limit of 16 fields in a SystemParam struct, so we split @@ -120,8 +123,9 @@ pub struct InspectorParams<'w, 's> { pub mesh_primitives: Query<'w, 's, (&'static MeshPrimitive, &'static RecallMeshPrimitive)>, pub names_in_workcell: Query<'w, 's, &'static NameInWorkcell>, pub scales: Query<'w, 's, &'static Scale>, - pub textures: Query<'w, 's, &'static Texture>, pub layer: InspectorLayerParams<'w, 's>, + pub texture: InspectTextureAffiliationParams<'w, 's>, + pub groups: InspectGroupParams<'w, 's>, pub default_file: Query<'w, 's, &'static DefaultFile>, } @@ -400,15 +404,13 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { } } - if let Ok(texture) = self.params.textures.get(selection) { - if let Some(new_texture) = InspectTexture::new(texture, default_file).show(ui) { - self.events - .change_more - .texture - .send(Change::new(new_texture, selection)); - } - ui.add_space(10.0); - } + InspectTextureAffiliation::new( + selection, + default_file, + &self.params.texture, + self.events, + ) + .show(ui); if let Ok((motion, recall)) = self.params.component.motions.get(selection) { ui.label(RichText::new("Forward Motion").size(18.0)); @@ -579,6 +581,40 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { } ui.add_space(10.0); } + + if let Ok(Affiliation(Some(group))) = self.params.groups.affiliation.get(selection) { + ui.separator(); + let empty = String::new(); + let name = self + .params + .component + .names + .get(*group) + .map(|n| &n.0) + .unwrap_or(&empty); + + ui.label(RichText::new(format!("Group Properties of [{}]", name)).size(18.0)); + ui.add_space(5.0); + InspectGroup::new( + *group, + selection, + default_file, + &self.params.groups, + self.events, + ) + .show(ui); + } + + if self.params.groups.is_group.contains(selection) { + InspectGroup::new( + selection, + selection, + default_file, + &self.params.groups, + self.events, + ) + .show(ui); + } } else { ui.label("Nothing selected"); } diff --git a/rmf_site_editor/src/widgets/menu_bar.rs b/rmf_site_editor/src/widgets/menu_bar.rs index 8b5bd4c8..8f2a2a4c 100644 --- a/rmf_site_editor/src/widgets/menu_bar.rs +++ b/rmf_site_editor/src/widgets/menu_bar.rs @@ -15,17 +15,165 @@ * */ -use crate::{CreateNewWorkspace, FileEvents, LoadWorkspace, SaveWorkspace, VisibilityParameters}; +use crate::{ + CreateNewWorkspace, FileEvents, LoadWorkspace, MenuParams, SaveWorkspace, VisibilityParameters, +}; +use bevy::prelude::{ + App, Children, Component, Entity, EventWriter, FromWorld, Parent, Plugin, Query, Res, Resource, + Without, World, +}; use bevy_egui::{ - egui::{self, Button}, + egui::{self, epaint::ahash::HashSet, Button, Ui}, EguiContext, }; +/// This component represents a menu. Menus and menu items +/// can be arranged in trees using bevy's own parent-child system. +#[derive(Component)] +pub struct Menu { + text: String, +} + +impl Menu { + /// Create a new menu from the title + pub fn from_title(text: String) -> Self { + Self { text } + } + + /// Retrieve the menu name + fn get(&self) -> String { + self.text.clone() + } +} + +/// Create a new menu item +#[derive(Component)] +#[non_exhaustive] +pub enum MenuItem { + Text(String), +} + +/// This resource provides the root entity for the file menu +#[derive(Resource)] +pub struct FileMenu { + /// Map of menu items + menu_item: Entity, +} + +impl FileMenu { + pub fn get(&self) -> Entity { + return self.menu_item; + } +} + +impl FromWorld for FileMenu { + fn from_world(world: &mut World) -> Self { + let menu_item = world + .spawn(Menu { + text: "File".to_string(), + }) + .id(); + Self { menu_item } + } +} + +#[non_exhaustive] +pub enum MenuEvent { + MenuClickEvent(Entity), +} + +impl MenuEvent { + pub fn clicked(&self) -> bool { + matches!(self, Self::MenuClickEvent(_)) + } + + pub fn source(&self) -> Entity { + match self { + Self::MenuClickEvent(entity) => *entity, + } + } +} + +pub struct MenuPluginManager; + +impl Plugin for MenuPluginManager { + fn build(&self, app: &mut App) { + app.add_event::().init_resource::(); + } +} + +/// Helper function to render a submenu starting at the entity. +fn render_sub_menu( + ui: &mut Ui, + entity: &Entity, + children: &Query<&Children>, + menus: &Query<(&Menu, Entity)>, + menu_items: &Query<&MenuItem>, + extension_events: &mut EventWriter, + skip_top_label: bool, +) { + if let Ok(e) = menu_items.get(*entity) { + // Draw ui + match e { + MenuItem::Text(title) => { + if ui.add(Button::new(title)).clicked() { + extension_events.send(MenuEvent::MenuClickEvent(*entity)); + } + } + } + return; + } + + let Ok((menu, _)) = menus.get(*entity) else { + return; + }; + + if !skip_top_label { + ui.menu_button(&menu.get(), |ui| { + let Ok(child_items) = children.get(*entity) else { + return; + }; + + for child in child_items.iter() { + render_sub_menu( + ui, + child, + children, + menus, + menu_items, + extension_events, + false, + ); + } + }); + } else { + let Ok(child_items) = children.get(*entity) else { + return; + }; + + for child in child_items.iter() { + render_sub_menu( + ui, + child, + children, + menus, + menu_items, + extension_events, + false, + ); + } + } +} + pub fn top_menu_bar( egui_context: &mut EguiContext, file_events: &mut FileEvents, params: &mut VisibilityParameters, + file_menu: &Res, + top_level_components: &Query<(), Without>, + children: &Query<&Children>, + menu_params: &mut MenuParams, ) { egui::TopBottomPanel::top("top_panel").show(egui_context.ctx_mut(), |ui| { egui::menu::bar(ui, |ui| { @@ -56,6 +204,16 @@ pub fn top_menu_bar( { file_events.load_workspace.send(LoadWorkspace::Dialog); } + + render_sub_menu( + ui, + &file_menu.get(), + children, + &menu_params.menus, + &menu_params.menu_items, + &mut menu_params.extension_events, + true, + ); }); ui.menu_button("View", |ui| { if ui @@ -145,6 +303,20 @@ pub fn top_menu_bar( params.events.walls.send((!params.resources.walls.0).into()); } }); + + for (_, entity) in menu_params.menus.iter().filter(|(_, entity)| { + top_level_components.contains(*entity) && *entity != file_menu.get() + }) { + render_sub_menu( + ui, + &entity, + children, + &menu_params.menus, + &menu_params.menu_items, + &mut menu_params.extension_events, + false, + ); + } }); }); } diff --git a/rmf_site_editor/src/widgets/mod.rs b/rmf_site_editor/src/widgets/mod.rs index 4242bfe7..89c63ed4 100644 --- a/rmf_site_editor/src/widgets/mod.rs +++ b/rmf_site_editor/src/widgets/mod.rs @@ -26,8 +26,8 @@ use crate::{ site::{ AlignSiteDrawings, AssociatedGraphs, BeginEditDrawing, Change, ConsiderAssociatedGraph, ConsiderLocationTag, CurrentLevel, Delete, DrawingMarker, ExportLights, FinishEditDrawing, - GlobalDrawingVisibility, GlobalFloorVisibility, LayerVisibility, PhysicalLightToggle, - SaveNavGraphs, SiteState, Texture, ToggleLiftDoorAvailability, + GlobalDrawingVisibility, GlobalFloorVisibility, LayerVisibility, MergeGroups, + PhysicalLightToggle, SaveNavGraphs, SiteState, Texture, ToggleLiftDoorAvailability, }, AppState, CreateNewWorkspace, CurrentWorkspace, LoadWorkspace, SaveWorkspace, }; @@ -44,6 +44,9 @@ use create::*; pub mod menu_bar; use menu_bar::*; +pub mod view_groups; +use view_groups::*; + pub mod view_layers; use view_layers::*; @@ -66,7 +69,7 @@ pub mod icons; pub use icons::*; pub mod inspector; -use inspector::{InspectorParams, InspectorWidget, SearchForFiducial}; +use inspector::{InspectorParams, InspectorWidget, SearchForFiducial, SearchForTexture}; pub mod move_layer; pub use move_layer::*; @@ -102,6 +105,8 @@ impl Plugin for StandardUiLayout { .init_resource::() .init_resource::() .init_resource::() + .init_resource::() + .init_resource::() .add_system_set(SystemSet::on_enter(AppState::MainMenu).with_system(init_ui_style)) .add_system_set( SystemSet::on_update(AppState::SiteEditor) @@ -154,8 +159,10 @@ pub struct ChangeEvents<'w, 's> { pub struct MoreChangeEvents<'w, 's> { pub affiliation: EventWriter<'w, 's, Change>>, pub search_for_fiducial: ResMut<'w, SearchForFiducial>, + pub search_for_texture: ResMut<'w, SearchForTexture>, pub distance: EventWriter<'w, 's, Change>, pub texture: EventWriter<'w, 's, Change>, + pub merge_groups: EventWriter<'w, 's, MergeGroups>, } #[derive(SystemParam)] @@ -256,6 +263,13 @@ pub struct VisibilityParameters<'w, 's> { resources: VisibilityResources<'w, 's>, } +#[derive(SystemParam)] +pub struct MenuParams<'w, 's> { + menus: Query<'w, 's, (&'static Menu, Entity)>, + menu_items: Query<'w, 's, &'static MenuItem>, + extension_events: EventWriter<'w, 's, MenuEvent>, +} + /// We collect all the events into its own SystemParam because we are not /// allowed to receive more than one EventWriter of a given type per system call /// (for borrow-checker reasons). Bundling them all up into an AppEvents @@ -285,7 +299,12 @@ fn site_ui_layout( lights: LightParams, nav_graphs: NavGraphParams, layers: LayersParams, + mut groups: GroupParams, mut events: AppEvents, + file_menu: Res, + children: Query<&Children>, + top_level_components: Query<(), Without>, + mut menu_params: MenuParams, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -328,6 +347,12 @@ fn site_ui_layout( CreateWidget::new(&create_params, &mut events).show(ui); }); ui.separator(); + CollapsingHeader::new("Groups") + .default_open(false) + .show(ui, |ui| { + ViewGroups::new(&mut groups, &mut events).show(ui); + }); + ui.separator(); CollapsingHeader::new("Lights") .default_open(false) .show(ui, |ui| { @@ -350,6 +375,10 @@ fn site_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &mut menu_params, ); egui::TopBottomPanel::bottom("log_console") @@ -385,6 +414,10 @@ fn site_drawing_ui_layout( inspector_params: InspectorParams, create_params: CreateParams, mut events: AppEvents, + file_menu: Res, + children: Query<&Children>, + top_level_components: Query<(), Without>, + mut menu_params: MenuParams, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -435,6 +468,10 @@ fn site_drawing_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &mut menu_params, ); let egui_context = egui_context.ctx_mut(); @@ -458,9 +495,12 @@ fn site_drawing_ui_layout( fn site_visualizer_ui_layout( mut egui_context: ResMut, mut picking_blocker: Option>, - inspector_params: InspectorParams, mut events: AppEvents, levels: LevelParams, + file_menu: Res, + top_level_components: Query<(), Without>, + children: Query<&Children>, + mut menu_params: MenuParams, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -512,6 +552,10 @@ fn site_visualizer_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &mut menu_params, ); let egui_context = egui_context.ctx_mut(); @@ -538,6 +582,10 @@ fn workcell_ui_layout( inspector_params: InspectorParams, create_params: CreateParams, mut events: AppEvents, + file_menu: Res, + top_level_components: Query<(), Without>, + children: Query<&Children>, + mut menu_params: MenuParams, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -575,6 +623,10 @@ fn workcell_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &mut menu_params, ); let egui_context = egui_context.ctx_mut(); diff --git a/rmf_site_editor/src/widgets/view_groups.rs b/rmf_site_editor/src/widgets/view_groups.rs new file mode 100644 index 00000000..14797430 --- /dev/null +++ b/rmf_site_editor/src/widgets/view_groups.rs @@ -0,0 +1,242 @@ +/* + * Copyright (C) 2023 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * +*/ + +use crate::{ + site::{Change, FiducialMarker, Members, MergeGroups, NameInSite, SiteID, Texture}, + widgets::{inspector::SelectionWidget, AppEvents}, + Icons, +}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{Button, CollapsingHeader, ImageButton, Ui}; + +#[derive(Default, Clone, Copy)] +pub enum GroupViewMode { + #[default] + View, + SelectMergeFrom, + MergeFrom(Entity), + Delete, +} + +#[derive(Default, Resource)] +pub struct GroupViewModes { + site: Option, + textures: GroupViewMode, + fiducials: GroupViewMode, +} + +impl GroupViewModes { + pub fn reset(&mut self, site: Entity) { + *self = GroupViewModes::default(); + self.site = Some(site); + } +} + +#[derive(SystemParam)] +pub struct GroupParams<'w, 's> { + children: Query<'w, 's, &'static Children>, + textures: Query<'w, 's, (&'static NameInSite, Option<&'static SiteID>), With>, + fiducials: Query<'w, 's, (&'static NameInSite, Option<&'static SiteID>), With>, + icons: Res<'w, Icons>, + group_view_modes: ResMut<'w, GroupViewModes>, +} + +pub struct ViewGroups<'a, 'w1, 's1, 'w2, 's2> { + params: &'a mut GroupParams<'w1, 's1>, + events: &'a mut AppEvents<'w2, 's2>, +} + +impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { + pub fn new(params: &'a mut GroupParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>) -> Self { + Self { params, events } + } + + pub fn show(self, ui: &mut Ui) { + let Some(site) = self.events.request.current_workspace.root else { + return; + }; + let modes = &mut *self.params.group_view_modes; + if !modes.site.is_some_and(|s| s == site) { + modes.reset(site); + } + let Ok(children) = self.params.children.get(site) else { + return; + }; + CollapsingHeader::new("Textures").show(ui, |ui| { + Self::show_groups( + children, + &self.params.textures, + &mut modes.textures, + &self.params.icons, + self.events, + ui, + ); + }); + CollapsingHeader::new("Fiducials").show(ui, |ui| { + Self::show_groups( + children, + &self.params.fiducials, + &mut modes.fiducials, + &self.params.icons, + self.events, + ui, + ); + }); + } + + fn show_groups<'b, T: Component>( + children: impl IntoIterator, + q_groups: &Query<(&NameInSite, Option<&SiteID>), With>, + mode: &mut GroupViewMode, + icons: &Res, + events: &mut AppEvents, + ui: &mut Ui, + ) { + ui.horizontal(|ui| match mode { + GroupViewMode::View => { + if ui + .add(Button::image_and_text( + icons.merge.egui(), + [18., 18.], + "merge", + )) + .on_hover_text("Merge two groups") + .clicked() + { + info!("Select a group whose members will be merged into another group"); + *mode = GroupViewMode::SelectMergeFrom; + } + if ui + .add(Button::image_and_text( + icons.trash.egui(), + [18., 18.], + "delete", + )) + .on_hover_text("Delete a group") + .clicked() + { + info!("Deleting a group will make all its members unaffiliated"); + *mode = GroupViewMode::Delete; + } + } + GroupViewMode::MergeFrom(_) | GroupViewMode::SelectMergeFrom => { + if ui + .add(Button::image_and_text( + icons.exit.egui(), + [18., 18.], + "cancel", + )) + .on_hover_text("Cancel the merge") + .clicked() + { + *mode = GroupViewMode::View; + } + } + GroupViewMode::Delete => { + if ui + .add(Button::image_and_text( + icons.exit.egui(), + [18., 18.], + "cancel", + )) + .on_hover_text("Cancel the delete") + .clicked() + { + *mode = GroupViewMode::View; + } + } + }); + + for child in children { + let Ok((name, site_id)) = q_groups.get(*child) else { + continue; + }; + let text = site_id + .map(|s| format!("{}", s.0.clone())) + .unwrap_or_else(|| "*".to_owned()); + ui.horizontal(|ui| { + match mode.clone() { + GroupViewMode::View => { + SelectionWidget::new(*child, site_id.cloned(), &icons, events).show(ui); + } + GroupViewMode::SelectMergeFrom => { + if ui + .add(Button::image_and_text( + icons.merge.egui(), + [18., 18.], + &text, + )) + .on_hover_text("Merge the members of this group into another group") + .clicked() + { + *mode = GroupViewMode::MergeFrom(*child); + } + } + GroupViewMode::MergeFrom(merge_from) => { + if merge_from == *child { + if ui + .add(Button::image_and_text(icons.exit.egui(), [18., 18.], &text)) + .on_hover_text("Cancel merge") + .clicked() + { + *mode = GroupViewMode::View; + } + } else { + if ui + .add(Button::image_and_text( + icons.confirm.egui(), + [18., 18.], + &text, + )) + .on_hover_text("Merge into this group") + .clicked() + { + events.change_more.merge_groups.send(MergeGroups { + from_group: merge_from, + into_group: *child, + }); + *mode = GroupViewMode::View; + } + } + } + GroupViewMode::Delete => { + if ui + .add(Button::image_and_text( + icons.trash.egui(), + [18., 18.], + &text, + )) + .on_hover_text("Delete this group") + .clicked() + { + events.commands.entity(*child).despawn_recursive(); + *mode = GroupViewMode::View; + } + } + } + + let mut new_name = name.0.clone(); + if ui.text_edit_singleline(&mut new_name).changed() { + events + .change + .name + .send(Change::new(NameInSite(new_name), *child)); + } + }); + } + } +} diff --git a/rmf_site_editor/src/widgets/view_layers.rs b/rmf_site_editor/src/widgets/view_layers.rs index a7714e74..3a792a01 100644 --- a/rmf_site_editor/src/widgets/view_layers.rs +++ b/rmf_site_editor/src/widgets/view_layers.rs @@ -216,7 +216,9 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewLayers<'a, 'w1, 's1, 'w2, 's2> { as_selected = true; layer_selected = Some(*e); } - let Ok((vis, alpha)) = self.params.layer_visibility.get(*e) else { continue }; + let Ok((vis, alpha)) = self.params.layer_visibility.get(*e) else { + continue; + }; ui.horizontal(|ui| { InspectLayer::new( *e, diff --git a/rmf_site_format/Cargo.toml b/rmf_site_format/Cargo.toml index 949be05c..37187941 100644 --- a/rmf_site_format/Cargo.toml +++ b/rmf_site_format/Cargo.toml @@ -11,7 +11,7 @@ crate-type = ["rlib"] serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8.23" serde_json = "*" -ron = "0.7" +ron = "0.8" thiserror = "*" glam = "0.22" # add features=["bevy"] to a dependent Cargo.toml to get the bevy-related features diff --git a/rmf_site_format/src/alignment.rs b/rmf_site_format/src/alignment.rs index 4efca465..63adab56 100644 --- a/rmf_site_format/src/alignment.rs +++ b/rmf_site_format/src/alignment.rs @@ -373,19 +373,27 @@ fn calculate_scale_gradient( } for vars_i in AllVariables::new(u) { - let Some(fiducials_i) = fiducials.get(vars_i.level) else { continue }; + let Some(fiducials_i) = fiducials.get(vars_i.level) else { + continue; + }; for vars_j in AllVariables::except(vars_i.level, u) { - let Some(fiducials_j) = fiducials.get(vars_j.level) else { continue }; + let Some(fiducials_j) = fiducials.get(vars_j.level) else { + continue; + }; for (k, phi_ki) in fiducials_i.iter().enumerate() { let Some(phi_ki) = phi_ki else { continue }; - let Some(Some(phi_kj)) = fiducials_j.get(k) else { continue }; + let Some(Some(phi_kj)) = fiducials_j.get(k) else { + continue; + }; let f_ki = vars_i.transform(*phi_ki); let f_kj = vars_j.transform(*phi_kj); for (m, phi_mi) in fiducials_i[k + 1..].iter().enumerate() { let m = m + k + 1; let Some(phi_mi) = phi_mi else { continue }; - let Some(Some(phi_mj)) = fiducials_j.get(m) else { continue }; + let Some(Some(phi_mj)) = fiducials_j.get(m) else { + continue; + }; let f_mi = vars_i.transform(*phi_mi); let f_mj = vars_j.transform(*phi_mj); let df_i = f_ki - f_mi; @@ -446,19 +454,27 @@ fn traverse_yaws( mut f: F, ) { for vars_i in AllVariables::new(u) { - let Some(fiducials_i) = fiducials.get(vars_i.level) else { continue }; + let Some(fiducials_i) = fiducials.get(vars_i.level) else { + continue; + }; for vars_j in AllVariables::after(vars_i.level, u) { - let Some(fiducials_j) = fiducials.get(vars_j.level) else { continue }; + let Some(fiducials_j) = fiducials.get(vars_j.level) else { + continue; + }; for (k, phi_ki) in fiducials_i.iter().enumerate() { let Some(phi_ki) = phi_ki else { continue }; - let Some(Some(phi_kj)) = fiducials_j.get(k) else { continue }; + let Some(Some(phi_kj)) = fiducials_j.get(k) else { + continue; + }; let f_ki = vars_i.transform(*phi_ki); let f_kj = vars_j.transform(*phi_kj); for (m, phi_mi) in fiducials_i[k + 1..].iter().enumerate() { let m = m + k + 1; let Some(phi_mi) = phi_mi else { continue }; - let Some(Some(phi_mj)) = fiducials_j.get(m) else { continue }; + let Some(Some(phi_mj)) = fiducials_j.get(m) else { + continue; + }; let f_mi = vars_i.transform(*phi_mi); let f_mj = vars_j.transform(*phi_mj); let df_i = f_ki - f_mi; @@ -511,12 +527,18 @@ fn traverse_locations( mut f: F, ) { for vars_i in AllVariables::new(u) { - let Some(fiducials_i) = fiducials.get(vars_i.level) else { continue }; + let Some(fiducials_i) = fiducials.get(vars_i.level) else { + continue; + }; for vars_j in AllVariables::after(vars_i.level, u) { - let Some(fiducials_j) = fiducials.get(vars_j.level) else { continue }; + let Some(fiducials_j) = fiducials.get(vars_j.level) else { + continue; + }; for (k, phi_ki) in fiducials_i.iter().enumerate() { let Some(phi_ki) = phi_ki else { continue }; - let Some(Some(phi_kj)) = fiducials_j.get(k) else { continue }; + let Some(Some(phi_kj)) = fiducials_j.get(k) else { + continue; + }; let f_ki = vars_i.transform(*phi_ki); let f_kj = vars_j.transform(*phi_kj); f(vars_i.level, f_ki, vars_j.level, f_kj); diff --git a/rmf_site_format/src/category.rs b/rmf_site_format/src/category.rs index 866ee5fc..5ce12415 100644 --- a/rmf_site_format/src/category.rs +++ b/rmf_site_format/src/category.rs @@ -40,6 +40,8 @@ pub enum Category { Location, Measurement, Fiducial, + FiducialGroup, + TextureGroup, Model, Camera, Drawing, @@ -64,6 +66,8 @@ impl Category { Self::Location => "Location", Self::Measurement => "Measurement", Self::Fiducial => "Fiducial", + Self::FiducialGroup => "Fiducial Group", + Self::TextureGroup => "Texture Group", Self::Model => "Model", Self::Camera => "Camera", Self::Drawing => "Drawing", diff --git a/rmf_site_format/src/constraint.rs b/rmf_site_format/src/constraint.rs index a09a3b0d..a52e8dcf 100644 --- a/rmf_site_format/src/constraint.rs +++ b/rmf_site_format/src/constraint.rs @@ -17,8 +17,9 @@ use crate::{Edge, RefTrait}; #[cfg(feature = "bevy")] -use bevy::prelude::{Bundle, Component, Entity}; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone)] #[cfg_attr(feature = "bevy", derive(Bundle))] @@ -33,16 +34,12 @@ pub struct Constraint { #[cfg_attr(feature = "bevy", derive(Component))] pub struct ConstraintMarker; -#[cfg(feature = "bevy")] -impl Constraint { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> Constraint { - Constraint { - edge: self.edge.to_ecs(id_to_entity), +impl Constraint { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Constraint { + edge: self.edge.convert(id_map)?, marker: Default::default(), - } + }) } } diff --git a/rmf_site_format/src/door.rs b/rmf_site_format/src/door.rs index e41b7dca..aaeef5bb 100644 --- a/rmf_site_format/src/door.rs +++ b/rmf_site_format/src/door.rs @@ -19,6 +19,7 @@ use crate::*; #[cfg(feature = "bevy")] use bevy::prelude::{Bundle, Component, Entity}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; pub const DEFAULT_DOOR_THICKNESS: f32 = 0.05; @@ -384,15 +385,14 @@ impl Door { } } -#[cfg(feature = "bevy")] -impl Door { - pub fn to_ecs(&self, id_to_entity: &std::collections::HashMap) -> Door { - Door { - anchors: self.anchors.to_ecs(id_to_entity), +impl Door { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Door { + anchors: self.anchors.convert(id_map)?, name: self.name.clone(), kind: self.kind.clone(), marker: Default::default(), - } + }) } } diff --git a/rmf_site_format/src/drawing.rs b/rmf_site_format/src/drawing.rs index 14aad717..69f2ffa9 100644 --- a/rmf_site_format/src/drawing.rs +++ b/rmf_site_format/src/drawing.rs @@ -33,7 +33,12 @@ impl Default for PixelsPerMeter { #[derive(Default, Serialize, Deserialize, Debug, Clone)] pub struct Drawing { - #[serde(flatten)] + // Even though round trip flattening is supposed to work after + // https://github.com/ron-rs/ron/pull/455, it seems it currently fails + // in ron, even forcing a dependency on that commit. + // TODO(luca) investigate further, come up with a minimum example, + // open an upstream issue and link it here for reference. + // #[serde(flatten)] pub properties: DrawingProperties, #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub anchors: BTreeMap, diff --git a/rmf_site_format/src/edge.rs b/rmf_site_format/src/edge.rs index 6f59ec29..01b9d12d 100644 --- a/rmf_site_format/src/edge.rs +++ b/rmf_site_format/src/edge.rs @@ -17,8 +17,9 @@ use crate::{RefTrait, Side}; #[cfg(feature = "bevy")] -use bevy::prelude::{Component, Entity}; +use bevy::prelude::Component; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] #[serde(transparent)] @@ -106,12 +107,11 @@ impl From<[T; 2]> for Edge { } } -#[cfg(feature = "bevy")] -impl Edge { - pub fn to_ecs(&self, id_to_entity: &std::collections::HashMap) -> Edge { - Edge([ - *id_to_entity.get(&self.left()).unwrap(), - *id_to_entity.get(&self.right()).unwrap(), - ]) +impl Edge { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Edge([ + id_map.get(&self.left()).ok_or(self.left())?.clone(), + id_map.get(&self.right()).ok_or(self.right())?.clone(), + ])) } } diff --git a/rmf_site_format/src/fiducial.rs b/rmf_site_format/src/fiducial.rs index ed7c5c26..8bfc14c1 100644 --- a/rmf_site_format/src/fiducial.rs +++ b/rmf_site_format/src/fiducial.rs @@ -17,8 +17,9 @@ use crate::{Affiliation, Group, NameInSite, Point, RefTrait}; #[cfg(feature = "bevy")] -use bevy::prelude::{Bundle, Component, Entity}; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; /// Mark a point within a drawing or level to serve as a ground truth relative /// to other drawings and levels. @@ -59,17 +60,13 @@ impl FiducialGroup { #[cfg_attr(feature = "bevy", derive(Component))] pub struct FiducialMarker; -#[cfg(feature = "bevy")] -impl Fiducial { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> Fiducial { - Fiducial { - anchor: self.anchor.to_ecs(id_to_entity), - affiliation: self.affiliation.to_ecs(id_to_entity), +impl Fiducial { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Fiducial { + anchor: self.anchor.convert(id_map)?, + affiliation: self.affiliation.convert(id_map)?, marker: Default::default(), - } + }) } } diff --git a/rmf_site_format/src/floor.rs b/rmf_site_format/src/floor.rs index 21d11a6c..8ae08baa 100644 --- a/rmf_site_format/src/floor.rs +++ b/rmf_site_format/src/floor.rs @@ -17,15 +17,16 @@ use crate::*; #[cfg(feature = "bevy")] -use bevy::prelude::{Bundle, Component, Entity}; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Bundle))] pub struct Floor { pub anchors: Path, #[serde(default, skip_serializing_if = "is_default")] - pub texture: Texture, + pub texture: Affiliation, #[serde( default = "PreferredSemiTransparency::for_floor", skip_serializing_if = "PreferredSemiTransparency::is_default_for_floor" @@ -39,27 +40,14 @@ pub struct Floor { #[cfg_attr(feature = "bevy", derive(Component))] pub struct FloorMarker; -#[cfg(feature = "bevy")] -impl Floor { - pub fn to_u32(&self, anchors: Path) -> Floor { - Floor { - anchors, - texture: self.texture.clone(), +impl Floor { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Floor { + anchors: self.anchors.convert(id_map)?, + texture: self.texture.convert(id_map)?, preferred_semi_transparency: PreferredSemiTransparency::for_floor(), marker: Default::default(), - } - } -} - -#[cfg(feature = "bevy")] -impl Floor { - pub fn to_ecs(&self, id_to_entity: &std::collections::HashMap) -> Floor { - Floor { - anchors: self.anchors.to_ecs(id_to_entity), - texture: self.texture.clone(), - preferred_semi_transparency: PreferredSemiTransparency::for_floor(), - marker: Default::default(), - } + }) } } @@ -67,12 +55,7 @@ impl From> for Floor { fn from(path: Path) -> Self { Floor { anchors: path, - texture: Texture { - source: AssetSource::Remote( - "OpenRobotics/RMF_Materials/textures/blue_linoleum.png".to_owned(), - ), - ..Default::default() - }, + texture: Affiliation(None), preferred_semi_transparency: PreferredSemiTransparency::for_floor(), marker: Default::default(), } diff --git a/rmf_site_format/src/lane.rs b/rmf_site_format/src/lane.rs index a6415707..09cb272b 100644 --- a/rmf_site_format/src/lane.rs +++ b/rmf_site_format/src/lane.rs @@ -17,8 +17,9 @@ use crate::*; #[cfg(feature = "bevy")] -use bevy::prelude::{Bundle, Component, Entity}; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Bundle))] @@ -192,16 +193,15 @@ impl Recall for RecallReverseLane { } } -#[cfg(feature = "bevy")] -impl Lane { - pub fn to_ecs(&self, id_to_entity: &std::collections::HashMap) -> Lane { - Lane { - anchors: self.anchors.to_ecs(id_to_entity), +impl Lane { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Lane { + anchors: self.anchors.convert(id_map)?, forward: self.forward.clone(), reverse: self.reverse.clone(), - graphs: self.graphs.to_ecs(id_to_entity), + graphs: self.graphs.convert(id_map)?, marker: Default::default(), - } + }) } } diff --git a/rmf_site_format/src/legacy/building_map.rs b/rmf_site_format/src/legacy/building_map.rs index 4bfceb58..fe7e0d7a 100644 --- a/rmf_site_format/src/legacy/building_map.rs +++ b/rmf_site_format/src/legacy/building_map.rs @@ -1,12 +1,14 @@ -use super::{level::Level, lift::Lift, PortingError, Result}; +use super::{ + floor::FloorParameters, level::Level, lift::Lift, wall::WallProperties, PortingError, Result, +}; use crate::{ alignment::align_legacy_building, Affiliation, Anchor, Angle, AssetSource, AssociatedGraphs, - DisplayColor, Dock as SiteDock, Drawing as SiteDrawing, DrawingProperties, + Category, DisplayColor, Dock as SiteDock, Drawing as SiteDrawing, DrawingProperties, Fiducial as SiteFiducial, FiducialGroup, FiducialMarker, Guided, Lane as SiteLane, LaneMarker, Level as SiteLevel, LevelElevation, LevelProperties as SiteLevelProperties, Motion, NameInSite, NameOfSite, NavGraph, Navigation, OrientationConstraint, PixelsPerMeter, Pose, PreferredSemiTransparency, RankingsInLevel, ReverseLane, Rotation, Site, SiteProperties, - DEFAULT_NAV_GRAPH_COLORS, + Texture as SiteTexture, TextureGroup, DEFAULT_NAV_GRAPH_COLORS, }; use glam::{DAffine2, DMat3, DQuat, DVec2, DVec3, EulerRot}; use serde::{Deserialize, Serialize}; @@ -146,7 +148,9 @@ impl BuildingMap { let mut level_name_to_id = BTreeMap::new(); let mut lanes = BTreeMap::>::new(); let mut locations = BTreeMap::new(); - + let mut textures: BTreeMap = BTreeMap::new(); + let mut floor_texture_map: HashMap = HashMap::new(); + let mut wall_texture_map: HashMap = HashMap::new(); let mut lift_cabin_anchors: BTreeMap> = BTreeMap::new(); let mut building_id_to_nav_graph_id = HashMap::new(); @@ -202,6 +206,7 @@ impl BuildingMap { let mut rankings = RankingsInLevel::default(); let mut drawings = BTreeMap::new(); let mut feature_info = HashMap::new(); + let mut primary_drawing_info = None; if !level.drawing.filename.is_empty() { let primary_drawing_id = site_id.next().unwrap(); let drawing_name = Path::new(&level.drawing.filename) @@ -231,6 +236,7 @@ impl BuildingMap { pose.trans[2] as f64, DVec2::new(pose.trans[0] as f64, pose.trans[1] as f64), ); + primary_drawing_info = Some((primary_drawing_id, drawing_tf)); for fiducial in &level.fiducials { let anchor_id = site_id.next().unwrap(); @@ -426,6 +432,22 @@ impl BuildingMap { if let Some(fiducial) = drawing.fiducials.get_mut(&info.fiducial_id) { fiducial.affiliation = Affiliation(Some(fiducial_group_id)); } + // Add a level anchor to pin this feature + if let Some((primary_drawing_id, drawing_tf)) = primary_drawing_info { + if info.in_drawing == primary_drawing_id { + let anchor_tf = drawing + .anchors + .get(&info.on_anchor) + .unwrap() + .translation_for_category(Category::General); + let drawing_coords = + DVec2::new(anchor_tf[0] as f64, anchor_tf[1] as f64); + cartesian_fiducials + .entry(fiducial_group_id) + .or_default() + .push(drawing_tf.transform_point2(drawing_coords)); + } + } } } } @@ -433,7 +455,12 @@ impl BuildingMap { let mut floors = BTreeMap::new(); for floor in &level.floors { - let site_floor = floor.to_site(&vertex_to_anchor_id)?; + let site_floor = floor.to_site( + &vertex_to_anchor_id, + &mut textures, + &mut floor_texture_map, + &mut site_id, + )?; let id = site_id.next().unwrap(); floors.insert(id, site_floor); rankings.floors.push(id); @@ -456,7 +483,12 @@ impl BuildingMap { let mut walls = BTreeMap::new(); for wall in &level.walls { - let site_wall = wall.to_site(&vertex_to_anchor_id)?; + let site_wall = wall.to_site( + &vertex_to_anchor_id, + &mut textures, + &mut wall_texture_map, + &mut site_id, + )?; walls.insert(site_id.next().unwrap(), site_wall); } @@ -595,6 +627,26 @@ impl BuildingMap { }) .collect(); + let textures = textures + .into_iter() + .map(|(id, texture)| { + let name: String = (&texture.source).into(); + let name = Path::new(&name) + .file_stem() + .map(|s| s.to_str().map(|s| s.to_owned())) + .flatten() + .unwrap_or(name); + ( + id, + TextureGroup { + name: NameInSite(name), + texture, + group: Default::default(), + }, + ) + }) + .collect(); + Ok(Site { format_version: Default::default(), anchors: site_anchors, @@ -605,6 +657,7 @@ impl BuildingMap { lifts, fiducial_groups, fiducials: cartesian_fiducials, + textures, navigation: Navigation { guided: Guided { graphs: nav_graphs, diff --git a/rmf_site_format/src/legacy/floor.rs b/rmf_site_format/src/legacy/floor.rs index bb8ab3be..72b1c17d 100644 --- a/rmf_site_format/src/legacy/floor.rs +++ b/rmf_site_format/src/legacy/floor.rs @@ -1,12 +1,15 @@ use super::{rbmf::*, PortingError, Result}; use crate::{ - Angle, AssetSource, Floor as SiteFloor, FloorMarker, Path, + Affiliation, Angle, AssetSource, Floor as SiteFloor, FloorMarker, Path, PreferredSemiTransparency, Texture, }; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{ + collections::{BTreeMap, HashMap}, + ops::RangeFrom, +}; -#[derive(Deserialize, Serialize, Clone, Default)] +#[derive(Deserialize, Serialize, Clone, Default, Hash, PartialEq, Eq)] pub struct FloorParameters { pub texture_name: RbmfString, pub texture_rotation: RbmfFloat, @@ -20,7 +23,13 @@ pub struct Floor { } impl Floor { - pub fn to_site(&self, vertex_to_anchor_id: &HashMap) -> Result> { + pub fn to_site( + &self, + vertex_to_anchor_id: &HashMap, + textures: &mut BTreeMap, + texture_map: &mut HashMap, + site_id: &mut RangeFrom, + ) -> Result> { let mut anchors = Vec::new(); for v in &self.vertices { let anchor = *vertex_to_anchor_id @@ -30,28 +39,38 @@ impl Floor { anchors.push(anchor); } + let texture_site_id = *texture_map + .entry(self.parameters.clone()) + .or_insert_with(|| { + let texture = if self.parameters.texture_name.1.is_empty() { + Texture { + source: AssetSource::Remote( + "OpenRobotics/RMF_Materials/textures/blue_linoleum.png".to_owned(), + ), + ..Default::default() + } + } else { + Texture { + source: AssetSource::Remote( + "OpenRobotics/RMF_Materials/textures/".to_owned() + + &self.parameters.texture_name.1 + + ".png", + ), + rotation: Some(Angle::Deg(self.parameters.texture_rotation.1 as f32)), + width: Some(self.parameters.texture_scale.1 as f32), + height: Some(self.parameters.texture_scale.1 as f32), + ..Default::default() + } + }; + + let texture_site_id = site_id.next().unwrap(); + textures.insert(texture_site_id, texture); + texture_site_id + }); + Ok(SiteFloor { anchors: Path(anchors), - texture: if self.parameters.texture_name.1.is_empty() { - Texture { - source: AssetSource::Remote( - "OpenRobotics/RMF_Materials/textures/blue_linoleum.png".to_owned(), - ), - ..Default::default() - } - } else { - Texture { - source: AssetSource::Remote( - "OpenRobotics/RMF_Materials/textures/".to_owned() - + &self.parameters.texture_name.1 - + ".png", - ), - rotation: Some(Angle::Deg(self.parameters.texture_rotation.1 as f32)), - width: Some(self.parameters.texture_scale.1 as f32), - height: Some(self.parameters.texture_scale.1 as f32), - ..Default::default() - } - }, + texture: Affiliation(Some(texture_site_id)), preferred_semi_transparency: PreferredSemiTransparency::for_floor(), marker: FloorMarker, }) diff --git a/rmf_site_format/src/legacy/rbmf.rs b/rmf_site_format/src/legacy/rbmf.rs index 69daeafb..8032e689 100644 --- a/rmf_site_format/src/legacy/rbmf.rs +++ b/rmf_site_format/src/legacy/rbmf.rs @@ -1,10 +1,13 @@ // RBMF stands for "RMF Building Map Format" -use std::ops::{Deref, DerefMut}; +use std::{ + hash::Hash, + ops::{Deref, DerefMut}, +}; use serde::{Deserialize, Serialize}; -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Hash)] pub struct RbmfString(usize, pub String); impl From for RbmfString { @@ -31,6 +34,8 @@ impl PartialEq for RbmfString { } } +impl Eq for RbmfString {} + impl From for String { fn from(s: RbmfString) -> Self { s.1 @@ -117,6 +122,14 @@ impl PartialEq for RbmfFloat { } } +impl Hash for RbmfFloat { + fn hash(&self, state: &mut H) { + state.write_i64((self.1 * 10000.0) as i64); + } +} + +impl Eq for RbmfFloat {} + impl PartialOrd for RbmfFloat { fn partial_cmp(&self, other: &Self) -> Option { self.1.partial_cmp(&other.1) diff --git a/rmf_site_format/src/legacy/wall.rs b/rmf_site_format/src/legacy/wall.rs index 1cddb73c..aaab3f51 100644 --- a/rmf_site_format/src/legacy/wall.rs +++ b/rmf_site_format/src/legacy/wall.rs @@ -1,7 +1,10 @@ use super::{rbmf::*, PortingError, Result}; -use crate::{AssetSource, Texture, Wall as SiteWall, DEFAULT_LEVEL_HEIGHT}; +use crate::{Affiliation, AssetSource, Texture, Wall as SiteWall, DEFAULT_LEVEL_HEIGHT}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{ + collections::{BTreeMap, HashMap}, + ops::RangeFrom, +}; fn default_height() -> RbmfFloat { RbmfFloat::from(DEFAULT_LEVEL_HEIGHT as f64) @@ -15,7 +18,7 @@ fn default_scale() -> RbmfFloat { RbmfFloat::from(1.0) } -#[derive(Deserialize, Serialize, Clone)] +#[derive(Deserialize, Serialize, Clone, Hash, PartialEq, Eq)] pub struct WallProperties { pub alpha: RbmfFloat, pub texture_name: RbmfString, @@ -43,19 +46,25 @@ impl Default for WallProperties { pub struct Wall(pub usize, pub usize, pub WallProperties); impl Wall { - pub fn to_site(&self, vertex_to_anchor_id: &HashMap) -> Result> { + pub fn to_site( + &self, + vertex_to_anchor_id: &HashMap, + textures: &mut BTreeMap, + texture_map: &mut HashMap, + site_id: &mut RangeFrom, + ) -> Result> { let left_anchor = vertex_to_anchor_id .get(&self.0) .ok_or(PortingError::InvalidVertex(self.0))?; let right_anchor = vertex_to_anchor_id .get(&self.1) .ok_or(PortingError::InvalidVertex(self.1))?; - Ok(SiteWall { - anchors: [*left_anchor, *right_anchor].into(), - texture: if self.2.texture_name.is_empty() { + + let texture_site_id = *texture_map.entry(self.2.clone()).or_insert_with(|| { + let texture = if self.2.texture_name.1.is_empty() { Texture { source: AssetSource::Remote( - "OpenRobotics/RMF_Materials/textures/default.png".to_owned(), + "OpenRobotics/RMF_Materials/textures/blue_linoleum.png".to_owned(), ), ..Default::default() } @@ -66,12 +75,21 @@ impl Wall { + &self.2.texture_name.1 + ".png", ), - alpha: Some(self.2.alpha.1 as f32), + rotation: None, width: Some((self.2.texture_width.1 / self.2.texture_scale.1) as f32), height: Some((self.2.texture_height.1 / self.2.texture_scale.1) as f32), - ..Default::default() + alpha: Some(self.2.alpha.1 as f32), } - }, + }; + + let texture_site_id = site_id.next().unwrap(); + textures.insert(texture_site_id, texture); + texture_site_id + }); + + Ok(SiteWall { + anchors: [*left_anchor, *right_anchor].into(), + texture: Affiliation(Some(texture_site_id)), marker: Default::default(), }) } diff --git a/rmf_site_format/src/lift.rs b/rmf_site_format/src/lift.rs index 88e03ce6..ed62b161 100644 --- a/rmf_site_format/src/lift.rs +++ b/rmf_site_format/src/lift.rs @@ -24,7 +24,7 @@ use bevy::{ }; use glam::{Vec2, Vec3}; use serde::{Deserialize, Serialize}; -use std::collections::{BTreeMap, BTreeSet}; +use std::collections::{BTreeMap, BTreeSet, HashMap}; pub const DEFAULT_CABIN_WALL_THICKNESS: f32 = 0.1; pub const DEFAULT_CABIN_DOOR_THICKNESS: f32 = 0.05; @@ -59,18 +59,14 @@ pub struct LiftCabinDoor { pub marker: LiftCabinDoorMarker, } -#[cfg(feature = "bevy")] -impl LiftCabinDoor { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> LiftCabinDoor { - LiftCabinDoor { +impl LiftCabinDoor { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(LiftCabinDoor { kind: self.kind.clone(), - reference_anchors: self.reference_anchors.to_ecs(id_to_entity), - visits: self.visits.to_ecs(id_to_entity), + reference_anchors: self.reference_anchors.convert(id_map)?, + visits: self.visits.convert(id_map)?, marker: Default::default(), - } + }) } } @@ -85,19 +81,14 @@ impl Default for LevelVisits { } } -#[cfg(feature = "bevy")] -impl LevelVisits { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> LevelVisits { - LevelVisits( - self.0 - .iter() - .map(|level| id_to_entity.get(level).unwrap()) - .copied() - .collect(), - ) +impl LevelVisits { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + let set: Result, T> = self + .0 + .iter() + .map(|level| id_map.get(level).copied().ok_or(*level)) + .collect(); + Ok(LevelVisits(set?)) } } @@ -181,6 +172,13 @@ impl LiftCabin { None } + + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + let result = match self { + LiftCabin::Rect(cabin) => LiftCabin::Rect(cabin.convert(id_map)?), + }; + Ok(result) + } } #[derive(Clone, Debug)] @@ -390,6 +388,23 @@ impl RectangularLiftCabin { ), ]) } + + pub fn convert( + &self, + id_map: &HashMap, + ) -> Result, T> { + Ok(RectangularLiftCabin { + width: self.width, + depth: self.depth, + wall_thickness: self.wall_thickness, + gap: self.gap, + shift: self.shift, + front_door: self.front_door.map(|d| d.convert(id_map)).transpose()?, + back_door: self.back_door.map(|d| d.convert(id_map)).transpose()?, + left_door: self.left_door.map(|d| d.convert(id_map)).transpose()?, + right_door: self.right_door.map(|d| d.convert(id_map)).transpose()?, + }) + } } #[cfg(feature = "bevy")] @@ -462,23 +477,19 @@ pub struct LiftCabinDoorPlacement { pub custom_gap: Option, } -#[cfg(feature = "bevy")] -impl LiftProperties { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> LiftProperties { - LiftProperties { +impl LiftProperties { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(LiftProperties { name: self.name.clone(), - reference_anchors: self.reference_anchors.to_ecs(id_to_entity), - cabin: self.cabin.to_ecs(id_to_entity), + reference_anchors: self.reference_anchors.convert(id_map)?, + cabin: self.cabin.convert(id_map)?, is_static: self.is_static, initial_level: InitialLevel( self.initial_level - .map(|id| id_to_entity.get(&id).unwrap()) + .map(|id| id_map.get(&id).unwrap()) .copied(), ), - } + }) } } @@ -494,18 +505,6 @@ impl From> for LiftProperties { } } -#[cfg(feature = "bevy")] -impl LiftCabin { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> LiftCabin { - match self { - LiftCabin::Rect(cabin) => LiftCabin::Rect(cabin.to_ecs(id_to_entity)), - } - } -} - #[cfg(feature = "bevy")] pub type QueryLiftDoor<'w, 's> = Query< 'w, @@ -529,26 +528,6 @@ impl LiftCabin { } } -#[cfg(feature = "bevy")] -impl RectangularLiftCabin { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> RectangularLiftCabin { - RectangularLiftCabin { - width: self.width, - depth: self.depth, - wall_thickness: self.wall_thickness, - gap: self.gap, - shift: self.shift, - front_door: self.front_door.as_ref().map(|d| d.to_ecs(id_to_entity)), - back_door: self.back_door.as_ref().map(|d| d.to_ecs(id_to_entity)), - left_door: self.left_door.as_ref().map(|d| d.to_ecs(id_to_entity)), - right_door: self.right_door.as_ref().map(|d| d.to_ecs(id_to_entity)), - } - } -} - #[cfg(feature = "bevy")] impl RectangularLiftCabin { pub fn to_u32(&self, doors: &QueryLiftDoor) -> RectangularLiftCabin { @@ -566,19 +545,18 @@ impl RectangularLiftCabin { } } -#[cfg(feature = "bevy")] -impl LiftCabinDoorPlacement { - pub fn to_ecs( +impl LiftCabinDoorPlacement { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> LiftCabinDoorPlacement { - LiftCabinDoorPlacement { - door: *id_to_entity.get(&self.door).unwrap(), + id_map: &HashMap, + ) -> Result, T> { + Ok(LiftCabinDoorPlacement { + door: id_map.get(&self.door).ok_or(self.door)?.clone(), width: self.width, thickness: self.thickness, shifted: self.shifted, custom_gap: self.custom_gap, - } + }) } } diff --git a/rmf_site_format/src/location.rs b/rmf_site_format/src/location.rs index a694876c..9d3ea516 100644 --- a/rmf_site_format/src/location.rs +++ b/rmf_site_format/src/location.rs @@ -17,8 +17,9 @@ use crate::*; #[cfg(feature = "bevy")] -use bevy::prelude::{Bundle, Component, Deref, DerefMut, Entity}; +use bevy::prelude::{Bundle, Component, Deref, DerefMut}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub enum LocationTag { @@ -83,18 +84,14 @@ impl Default for LocationTags { } } -#[cfg(feature = "bevy")] -impl Location { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> Location { - Location { - anchor: Point(*id_to_entity.get(&self.anchor).unwrap()), +impl Location { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Location { + anchor: self.anchor.convert(id_map)?, tags: self.tags.clone(), name: self.name.clone(), - graphs: self.graphs.to_ecs(id_to_entity), - } + graphs: self.graphs.convert(id_map)?, + }) } } diff --git a/rmf_site_format/src/measurement.rs b/rmf_site_format/src/measurement.rs index 8aa1c5a0..16efaa21 100644 --- a/rmf_site_format/src/measurement.rs +++ b/rmf_site_format/src/measurement.rs @@ -19,6 +19,7 @@ use crate::*; #[cfg(feature = "bevy")] use bevy::prelude::{Bundle, Component, Deref, DerefMut, Entity}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Bundle))] @@ -59,18 +60,14 @@ impl Measurement { } } -#[cfg(feature = "bevy")] -impl Measurement { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> Measurement { - Measurement { - anchors: self.anchors.to_ecs(id_to_entity), +impl Measurement { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Measurement { + anchors: self.anchors.convert(id_map)?, distance: self.distance, label: self.label.clone(), marker: Default::default(), - } + }) } } diff --git a/rmf_site_format/src/misc.rs b/rmf_site_format/src/misc.rs index 5c6bc50a..a7fa86fc 100644 --- a/rmf_site_format/src/misc.rs +++ b/rmf_site_format/src/misc.rs @@ -20,6 +20,7 @@ use crate::{Recall, RefTrait}; use bevy::prelude::*; use glam::{Quat, Vec2, Vec3}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; pub const DEFAULT_LEVEL_HEIGHT: f32 = 3.0; @@ -475,12 +476,12 @@ impl Default for Affiliation { } } -#[cfg(feature = "bevy")] -impl Affiliation { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> Affiliation { - Affiliation(self.0.map(|a| *id_to_entity.get(&a).unwrap())) +impl Affiliation { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + if let Some(x) = self.0 { + Ok(Affiliation(Some(id_map.get(&x).ok_or(x)?.clone()))) + } else { + Ok(Affiliation(None)) + } } } diff --git a/rmf_site_format/src/nav_graph.rs b/rmf_site_format/src/nav_graph.rs index 37bb6cc0..d9fbbcc3 100644 --- a/rmf_site_format/src/nav_graph.rs +++ b/rmf_site_format/src/nav_graph.rs @@ -19,7 +19,7 @@ use crate::*; #[cfg(feature = "bevy")] use bevy::prelude::{Bundle, Component, Deref, DerefMut, Entity, Query, With}; use serde::{Deserialize, Serialize}; -use std::collections::BTreeSet; +use std::collections::{BTreeSet, HashMap}; pub const DEFAULT_NAV_GRAPH_COLORS: [[f32; 4]; 8] = [ [1.0, 0.5, 0.3, 1.0], @@ -112,27 +112,22 @@ impl Default for AssociatedGraphs { } } -#[cfg(feature = "bevy")] -impl AssociatedGraphs { - pub fn to_ecs( - &self, - id_to_entity: &std::collections::HashMap, - ) -> AssociatedGraphs { - match self { +impl AssociatedGraphs { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + let result = match self { Self::All => AssociatedGraphs::All, - Self::Only(set) => AssociatedGraphs::Only(Self::set_to_ecs(set, id_to_entity)), - Self::AllExcept(set) => { - AssociatedGraphs::AllExcept(Self::set_to_ecs(set, id_to_entity)) - } - } + Self::Only(set) => AssociatedGraphs::Only(Self::convert_set(set, id_map)?), + Self::AllExcept(set) => AssociatedGraphs::AllExcept(Self::convert_set(set, id_map)?), + }; + Ok(result) } - fn set_to_ecs( - set: &BTreeSet, - id_to_entity: &std::collections::HashMap, - ) -> BTreeSet { + fn convert_set( + set: &BTreeSet, + id_map: &HashMap, + ) -> Result, T> { set.iter() - .map(|g| id_to_entity.get(g).unwrap().clone()) + .map(|g| id_map.get(g).cloned().ok_or(*g)) .collect() } } diff --git a/rmf_site_format/src/path.rs b/rmf_site_format/src/path.rs index 7f1c6951..34fe2b1f 100644 --- a/rmf_site_format/src/path.rs +++ b/rmf_site_format/src/path.rs @@ -17,22 +17,22 @@ use crate::RefTrait; #[cfg(feature = "bevy")] -use bevy::prelude::{Component, Deref, DerefMut, Entity}; +use bevy::prelude::{Component, Deref, DerefMut}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[serde(transparent)] #[cfg_attr(feature = "bevy", derive(Component, Deref, DerefMut))] pub struct Path(pub Vec); -#[cfg(feature = "bevy")] -impl Path { - pub fn to_ecs(&self, id_to_entity: &std::collections::HashMap) -> Path { - Path( - self.0 - .iter() - .map(|a| *id_to_entity.get(a).unwrap()) - .collect(), - ) +impl Path { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + let path: Result, T> = self + .0 + .iter() + .map(|a| id_map.get(a).cloned().ok_or(*a)) + .collect(); + Ok(Path(path?)) } } diff --git a/rmf_site_format/src/point.rs b/rmf_site_format/src/point.rs index 8b4d1447..7dca6c74 100644 --- a/rmf_site_format/src/point.rs +++ b/rmf_site_format/src/point.rs @@ -17,8 +17,9 @@ use crate::RefTrait; #[cfg(feature = "bevy")] -use bevy::prelude::{Component, Deref, DerefMut, Entity}; +use bevy::prelude::{Component, Deref, DerefMut}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq)] #[serde(transparent)] @@ -31,9 +32,8 @@ impl From for Point { } } -#[cfg(feature = "bevy")] -impl Point { - pub fn to_ecs(&self, id_to_entity: &std::collections::HashMap) -> Point { - Point(*id_to_entity.get(&self.0).unwrap()) +impl Point { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Point(id_map.get(&self.0).ok_or(self.0)?.clone())) } } diff --git a/rmf_site_format/src/site.rs b/rmf_site_format/src/site.rs index 832cf080..102323f3 100644 --- a/rmf_site_format/src/site.rs +++ b/rmf_site_format/src/site.rs @@ -51,6 +51,9 @@ pub struct Site { /// Properties of each level #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub levels: BTreeMap, + /// The groups of textures being used in the site + #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] + pub textures: BTreeMap, /// The fiducial groups that exist in the site #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] pub fiducial_groups: BTreeMap, @@ -98,17 +101,17 @@ impl Site { ron::ser::to_string_pretty(self, style) } - pub fn from_reader(reader: R) -> ron::Result { + pub fn from_reader(reader: R) -> ron::error::SpannedResult { // TODO(MXG): Validate the parsed data, e.g. make sure anchor pairs // belong to the same level. ron::de::from_reader(reader) } - pub fn from_str<'a>(s: &'a str) -> ron::Result { + pub fn from_str<'a>(s: &'a str) -> ron::error::SpannedResult { ron::de::from_str(s) } - pub fn from_bytes<'a>(s: &'a [u8]) -> ron::Result { + pub fn from_bytes<'a>(s: &'a [u8]) -> ron::error::SpannedResult { ron::de::from_bytes(s) } } @@ -119,3 +122,17 @@ impl RefTrait for u32 {} #[cfg(feature = "bevy")] impl RefTrait for Entity {} + +#[cfg(test)] +mod tests { + use super::*; + use crate::legacy::building_map::BuildingMap; + + #[test] + fn serde_roundtrip() { + let data = std::fs::read("../assets/demo_maps/office.building.yaml").unwrap(); + let map = BuildingMap::from_bytes(&data).unwrap(); + let site_string = map.to_site().unwrap().to_string().unwrap(); + Site::from_str(&site_string).unwrap(); + } +} diff --git a/rmf_site_format/src/texture.rs b/rmf_site_format/src/texture.rs index 3fb08069..82328b11 100644 --- a/rmf_site_format/src/texture.rs +++ b/rmf_site_format/src/texture.rs @@ -17,7 +17,7 @@ use crate::*; #[cfg(feature = "bevy")] -use bevy::prelude::Component; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] @@ -33,3 +33,15 @@ pub struct Texture { #[serde(skip_serializing_if = "Option::is_none")] pub height: Option, } + +#[derive(Serialize, Deserialize, Debug, Default, Clone)] +#[cfg_attr(feature = "bevy", derive(Bundle))] +pub struct TextureGroup { + pub name: NameInSite, + // The flatten attribute currently does not work correctly for the .ron + // format, so we cannot use it for now. + // #[serde(flatten)] + pub texture: Texture, + #[serde(skip)] + pub group: Group, +} diff --git a/rmf_site_format/src/wall.rs b/rmf_site_format/src/wall.rs index 52c0134d..244066d9 100644 --- a/rmf_site_format/src/wall.rs +++ b/rmf_site_format/src/wall.rs @@ -17,15 +17,16 @@ use crate::*; #[cfg(feature = "bevy")] -use bevy::prelude::{Bundle, Component, Entity}; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; +use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Bundle))] pub struct Wall { pub anchors: Edge, #[serde(skip_serializing_if = "is_default")] - pub texture: Texture, + pub texture: Affiliation, #[serde(skip)] pub marker: WallMarker, } @@ -34,25 +35,13 @@ pub struct Wall { #[cfg_attr(feature = "bevy", derive(Component))] pub struct WallMarker; -#[cfg(feature = "bevy")] -impl Wall { - pub fn to_u32(&self, anchors: Edge) -> Wall { - Wall { - anchors, - texture: self.texture.clone(), +impl Wall { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + Ok(Wall { + anchors: self.anchors.convert(id_map)?, + texture: self.texture.convert(id_map)?, marker: Default::default(), - } - } -} - -#[cfg(feature = "bevy")] -impl Wall { - pub fn to_ecs(&self, id_to_entity: &std::collections::HashMap) -> Wall { - Wall { - anchors: self.anchors.to_ecs(id_to_entity), - texture: self.texture.clone(), - marker: Default::default(), - } + }) } } @@ -60,12 +49,7 @@ impl From> for Wall { fn from(anchors: Edge) -> Self { Self { anchors, - texture: Texture { - source: AssetSource::Remote( - "OpenRobotics/RMF_Materials/textures/default.png".to_owned(), - ), - ..Default::default() - }, + texture: Affiliation(None), marker: Default::default(), } }