From 3aa5fe6cfc5c3c4999f525f65c1c3be13a1825c7 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 15 Aug 2023 01:54:17 +0800 Subject: [PATCH 01/19] Introducing the concept of a texture group Signed-off-by: Michael X. Grey --- rmf_site_editor/src/lib.rs | 2 + rmf_site_editor/src/site/load.rs | 107 ++++++++++++----- rmf_site_editor/src/site/save.rs | 57 ++++++++- rmf_site_editor/src/widgets/console.rs | 8 +- rmf_site_format/src/constraint.rs | 18 +-- rmf_site_format/src/door.rs | 12 +- rmf_site_format/src/edge.rs | 19 +-- rmf_site_format/src/fiducial.rs | 18 +-- rmf_site_format/src/floor.rs | 40 ++----- rmf_site_format/src/lane.rs | 16 +-- rmf_site_format/src/legacy/building_map.rs | 48 +++++++- rmf_site_format/src/legacy/floor.rs | 33 +++-- rmf_site_format/src/legacy/rbmf.rs | 17 ++- rmf_site_format/src/legacy/wall.rs | 44 +++++-- rmf_site_format/src/lift.rs | 133 ++++++++++----------- rmf_site_format/src/location.rs | 20 ++-- rmf_site_format/src/measurement.rs | 16 +-- rmf_site_format/src/misc.rs | 16 ++- rmf_site_format/src/nav_graph.rs | 33 ++--- rmf_site_format/src/path.rs | 22 ++-- rmf_site_format/src/point.rs | 13 +- rmf_site_format/src/site.rs | 3 + rmf_site_format/src/texture.rs | 12 +- rmf_site_format/src/wall.rs | 36 ++---- 24 files changed, 454 insertions(+), 289 deletions(-) diff --git a/rmf_site_editor/src/lib.rs b/rmf_site_editor/src/lib.rs index 3aa6d49b..dd5c02e1 100644 --- a/rmf_site_editor/src/lib.rs +++ b/rmf_site_editor/src/lib.rs @@ -1,3 +1,5 @@ +#![feature(error_generic_member_access, provide_any)] + use bevy::{ log::LogPlugin, pbr::DirectionalLightShadowMap, prelude::*, render::renderer::RenderAdapterInfo, }; diff --git a/rmf_site_editor/src/site/load.rs b/rmf_site_editor/src/site/load.rs index f3c7938b..a4bcb8fc 100644 --- a/rmf_site_editor/src/site/load.rs +++ b/rmf_site_editor/src/site/load.rs @@ -19,7 +19,7 @@ use crate::{recency::RecencyRanking, site::*, Autoload, CurrentWorkspace, Worksp use bevy::{ecs::system::SystemParam, prelude::*, tasks::AsyncComputeTaskPool}; use futures_lite::future; use rmf_site_format::legacy::building_map::BuildingMap; -use std::{collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf, backtrace::Backtrace}; use thiserror::Error as ThisError; #[cfg(not(target_arch = "wasm32"))] @@ -39,7 +39,31 @@ 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, + backtrace: Backtrace, +} + +impl LoadSiteError { + fn new(site: Entity, broken: u32) -> Self { + Self { site, broken, backtrace: Backtrace::force_capture() } + } +} + +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 +73,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())) @@ -76,7 +101,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 +113,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 +124,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 +135,24 @@ 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 +176,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,10 +205,9 @@ 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)) - .insert(Category::Lift) - .with_children(|lift| { + 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()) @@ -198,14 +224,18 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (door_id, door) in &lift_data.cabin_doors { let door_entity = lift - .spawn(door.to_ecs(&id_to_entity)) + .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); } - }) - .insert(lift_data.properties.to_ecs(&id_to_entity)) + Ok(()) + })?; + + let lift = lift + .insert(Category::Lift) + .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 +243,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 +262,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 +271,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 +298,19 @@ 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 +320,17 @@ 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: {err:#?}", + ); + continue; + } + }; if let Some(path) = &cmd.default_file { commands.entity(site).insert(DefaultFile(path.clone())); } @@ -308,6 +349,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 +550,19 @@ 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/save.rs b/rmf_site_editor/src/site/save.rs index 8f2f80c1..bdbc744c 100644 --- a/rmf_site_editor/src/site/save.rs +++ b/rmf_site_editor/src/site/save.rs @@ -111,7 +111,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 +290,7 @@ fn generate_levels( ( &Path, Option<&Original>>, - &Texture, + &Affiliation, &PreferredSemiTransparency, &SiteID, &Parent, @@ -330,7 +335,7 @@ fn generate_levels( ( &Edge, Option<&Original>>, - &Texture, + &Affiliation, &SiteID, &Parent, ), @@ -525,11 +530,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 +603,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 +883,34 @@ 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 +1099,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 +1121,7 @@ pub fn generate_site( lifts, fiducials, fiducial_groups, + textures, navigation: Navigation { guided: Guided { graphs: nav_graphs, diff --git a/rmf_site_editor/src/widgets/console.rs b/rmf_site_editor/src/widgets/console.rs index 2bdfa881..cc9d42e0 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>, diff --git a/rmf_site_format/src/constraint.rs b/rmf_site_format/src/constraint.rs index a09a3b0d..8d0d99e6 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,15 @@ pub struct Constraint { #[cfg_attr(feature = "bevy", derive(Component))] pub struct ConstraintMarker; -#[cfg(feature = "bevy")] -impl Constraint { - pub fn to_ecs( +impl Constraint { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> Constraint { - Constraint { - edge: self.edge.to_ecs(id_to_entity), + 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/edge.rs b/rmf_site_format/src/edge.rs index 6f59ec29..4dc8f2ba 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,14 @@ 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..6bb01cc6 100644 --- a/rmf_site_format/src/fiducial.rs +++ b/rmf_site_format/src/fiducial.rs @@ -19,6 +19,7 @@ use crate::{Affiliation, Group, NameInSite, Point, RefTrait}; #[cfg(feature = "bevy")] use bevy::prelude::{Bundle, Component, Entity}; 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,16 @@ impl FiducialGroup { #[cfg_attr(feature = "bevy", derive(Component))] pub struct FiducialMarker; -#[cfg(feature = "bevy")] -impl Fiducial { - pub fn to_ecs( +impl Fiducial { + pub fn convert( &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), + 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..5a0d8c93 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,17 @@ 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 +58,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..f9ff4161 100644 --- a/rmf_site_format/src/legacy/building_map.rs +++ b/rmf_site_format/src/legacy/building_map.rs @@ -1,4 +1,7 @@ -use super::{level::Level, lift::Lift, PortingError, Result}; +use super::{ + level::Level, lift::Lift, floor::FloorParameters, PortingError, Result, + wall::WallProperties, +}; use crate::{ alignment::align_legacy_building, Affiliation, Anchor, Angle, AssetSource, AssociatedGraphs, DisplayColor, Dock as SiteDock, Drawing as SiteDrawing, DrawingProperties, @@ -6,7 +9,7 @@ use crate::{ 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, + DEFAULT_NAV_GRAPH_COLORS, Texture as SiteTexture, TextureGroup, }; use glam::{DAffine2, DMat3, DQuat, DVec2, DVec3, EulerRot}; use serde::{Deserialize, Serialize}; @@ -146,7 +149,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(); @@ -433,7 +438,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 +466,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 +610,28 @@ 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 +642,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..de2d9e01 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, - PreferredSemiTransparency, Texture, + PreferredSemiTransparency, Texture, Affiliation, }; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{ + collections::{HashMap, BTreeMap}, + 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,9 +39,8 @@ impl Floor { anchors.push(anchor); } - Ok(SiteFloor { - anchors: Path(anchors), - texture: if self.parameters.texture_name.1.is_empty() { + 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(), @@ -51,7 +59,16 @@ impl Floor { 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: 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..a6b9be68 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::{ + ops::{Deref, DerefMut}, + hash::Hash, +}; 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..476d2b9e 100644 --- a/rmf_site_format/src/legacy/wall.rs +++ b/rmf_site_format/src/legacy/wall.rs @@ -1,7 +1,12 @@ use super::{rbmf::*, PortingError, Result}; -use crate::{AssetSource, Texture, Wall as SiteWall, DEFAULT_LEVEL_HEIGHT}; +use crate::{ + AssetSource, Texture, Wall as SiteWall, Affiliation, DEFAULT_LEVEL_HEIGHT, +}; use serde::{Deserialize, Serialize}; -use std::collections::HashMap; +use std::{ + collections::{HashMap, BTreeMap}, + ops::RangeFrom, +}; fn default_height() -> RbmfFloat { RbmfFloat::from(DEFAULT_LEVEL_HEIGHT as f64) @@ -15,7 +20,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 +48,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 +77,21 @@ impl Wall { + &self.2.texture_name.1 + ".png", ), + 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), alpha: Some(self.2.alpha.1 as f32), - 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() } - }, + }; + + 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..d07a71c3 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,17 @@ pub struct LiftCabinDoor { pub marker: LiftCabinDoorMarker, } -#[cfg(feature = "bevy")] -impl LiftCabinDoor { - pub fn to_ecs( +impl LiftCabinDoor { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> LiftCabinDoor { - LiftCabinDoor { + 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 +84,16 @@ impl Default for LevelVisits { } } -#[cfg(feature = "bevy")] -impl LevelVisits { - pub fn to_ecs( +impl LevelVisits { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> LevelVisits { - LevelVisits( - self.0 - .iter() - .map(|level| id_to_entity.get(level).unwrap()) - .copied() - .collect(), - ) + 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 +177,16 @@ 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 +396,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 +485,22 @@ pub struct LiftCabinDoorPlacement { pub custom_gap: Option, } -#[cfg(feature = "bevy")] -impl LiftProperties { - pub fn to_ecs( +impl LiftProperties { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> LiftProperties { - LiftProperties { + 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 +516,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 +539,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 +556,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..73b8e7bb 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,17 @@ impl Default for LocationTags { } } -#[cfg(feature = "bevy")] -impl Location { - pub fn to_ecs( +impl Location { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> Location { - Location { - anchor: Point(*id_to_entity.get(&self.anchor).unwrap()), + 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..4a100644 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,17 @@ impl Measurement { } } -#[cfg(feature = "bevy")] -impl Measurement { - pub fn to_ecs( +impl Measurement { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> Measurement { - Measurement { - anchors: self.anchors.to_ecs(id_to_entity), + 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..be43ca75 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,15 @@ impl Default for Affiliation { } } -#[cfg(feature = "bevy")] -impl Affiliation { - pub fn to_ecs( +impl Affiliation { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> Affiliation { - Affiliation(self.0.map(|a| *id_to_entity.get(&a).unwrap())) + 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..61e0778c 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,28 @@ impl Default for AssociatedGraphs { } } -#[cfg(feature = "bevy")] -impl AssociatedGraphs { - pub fn to_ecs( +impl AssociatedGraphs { + pub fn convert( &self, - id_to_entity: &std::collections::HashMap, - ) -> AssociatedGraphs { - match 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::Only(set) => AssociatedGraphs::Only(Self::convert_set(set, id_map)?), Self::AllExcept(set) => { - AssociatedGraphs::AllExcept(Self::set_to_ecs(set, id_to_entity)) + AssociatedGraphs::AllExcept(Self::convert_set(set, id_map)?) } - } + }; + Ok(result) } - fn set_to_ecs( - set: &BTreeSet, - id_to_entity: &std::collections::HashMap, - ) -> BTreeSet { - set.iter() - .map(|g| id_to_entity.get(g).unwrap().clone()) + fn convert_set( + set: &BTreeSet, + id_map: &HashMap, + ) -> Result, T> { + set + .iter() + .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..586fe6ca 100644 --- a/rmf_site_format/src/path.rs +++ b/rmf_site_format/src/path.rs @@ -17,22 +17,24 @@ 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..18a2fcd1 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,11 @@ 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..f8f02184 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, diff --git a/rmf_site_format/src/texture.rs b/rmf_site_format/src/texture.rs index 3fb08069..cfc8b297 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::{Component, Bundle}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] @@ -33,3 +33,13 @@ 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, + #[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(), } } From 839d2e95e5032025c7aed83fd6d1c41f94c53b92 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Tue, 15 Aug 2023 15:40:46 +0800 Subject: [PATCH 02/19] Make texture groups functional -- need to add edit widget Signed-off-by: Michael X. Grey --- rmf_site_editor/src/log.rs | 2 +- rmf_site_editor/src/site/floor.rs | 81 ++++++++++++++--------- rmf_site_editor/src/site/group.rs | 89 ++++++++++++++++++++++++++ rmf_site_editor/src/site/load.rs | 9 ++- rmf_site_editor/src/site/mod.rs | 19 ++++-- rmf_site_editor/src/site/texture.rs | 47 ++++++++++++++ rmf_site_editor/src/site/wall.rs | 79 ++++++++++++++--------- rmf_site_editor/src/widgets/console.rs | 30 +++++++-- 8 files changed, 281 insertions(+), 75 deletions(-) create mode 100644 rmf_site_editor/src/site/group.rs create mode 100644 rmf_site_editor/src/site/texture.rs 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/site/floor.rs b/rmf_site_editor/src/site/floor.rs index 17106ddf..6ded6064 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,62 @@ 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..cab42a6d --- /dev/null +++ b/rmf_site_editor/src/site/group.rs @@ -0,0 +1,89 @@ +/* + * 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 rmf_site_format::{Affiliation, Group}; +use bevy::{ + ecs::system::{Command, EntityCommands}, + prelude::*, +}; + +#[derive(Component, Deref)] +pub struct Members(Vec); + +#[derive(Component, Clone, Copy)] +struct LastAffiliation(Option); + +pub fn update_members_of_groups( + mut commands: Commands, + mut changed_affiliation: Query<(Entity, &Affiliation), Changed>>, +) { + for (e, affiliation) in &mut 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 a4bcb8fc..af95d313 100644 --- a/rmf_site_editor/src/site/load.rs +++ b/rmf_site_editor/src/site/load.rs @@ -94,6 +94,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)); @@ -326,7 +332,8 @@ pub fn load_site( commands.entity(err.site).despawn_recursive(); error!( "Failed to load the site entities because the file had an \ - internal inconsistency: {err:#?}", + internal inconsistency:\n{err:#?}\n---\nSite Data:\n{:#?}", + &cmd.site, ); continue; } diff --git a/rmf_site_editor/src/site/mod.rs b/rmf_site_editor/src/site/mod.rs index ab1dd0ec..1b9cd986 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::*; @@ -215,6 +221,7 @@ 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(update_material_for_display_color), ) .add_system_set( @@ -253,9 +260,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 +299,7 @@ impl Plugin for SitePlugin { .with_system(update_constraint_for_changed_labels) .with_system(update_changed_constraint) .with_system(update_model_scenes) + .with_system(update_members_of_groups) .with_system(handle_new_sdf_roots) .with_system(update_model_scales) .with_system(make_models_selectable) @@ -301,9 +309,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/texture.rs b/rmf_site_editor/src/site/texture.rs new file mode 100644 index 00000000..1d54ba96 --- /dev/null +++ b/rmf_site_editor/src/site/texture.rs @@ -0,0 +1,47 @@ +/* + * 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 rmf_site_format::{Texture, Affiliation}; +use bevy::prelude::*; + +pub fn fetch_image_for_texture( + mut commands: Commands, + mut changed_textures: Query<(Entity, Option<&mut Handle>, &Texture), Changed>, + asset_server: Res, +) { + for (e, mut 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); + } + } +} + +// 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..d0ecace9 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,10 @@ pub fn add_wall_visual( } } -pub fn update_wall_edge( - mut walls: Query< - (Entity, &Edge, &Texture, &mut Handle), - (With, Changed>), - >, - 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>, +pub fn update_walls_for_moved_anchors( + mut walls: Query<(Entity, &Edge, &Affiliation, &mut Handle), With>, anchors: AnchorParams, + textures: Query<(Option<&Handle>, &Texture)>, changed_anchors: Query< &Dependents, ( @@ -121,38 +110,66 @@ 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/widgets/console.rs b/rmf_site_editor/src/widgets/console.rs index cc9d42e0..fc469a80 100644 --- a/rmf_site_editor/src/widgets/console.rs +++ b/rmf_site_editor/src/widgets/console.rs @@ -135,13 +135,35 @@ 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." + ); + } }); } From 591444ca3631dafa6aafc75a690c9ba91c212404 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Wed, 16 Aug 2023 00:23:03 +0800 Subject: [PATCH 03/19] Texture edit widget -- WIP Signed-off-by: Michael X. Grey --- .../src/widgets/inspector/inspect_texture.rs | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs index 4c1e7ebf..681dea26 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -17,15 +17,70 @@ use crate::{ site::DefaultFile, + AppEvents, inspector::{InspectAssetSource, InspectValue}, widgets::egui::RichText, + WorkspaceMarker, }; +use bevy::prelude::*; use bevy_egui::egui::{Grid, Ui}; -use rmf_site_format::{RecallAssetSource, Texture}; +use rmf_site_format::{ + RecallAssetSource, Texture, NameInSite, Group, Affiliation, FloorMarker, + WallMarker, +}; + +pub struct InspectTextureAffiliationParams<'w, 's> { + with_texture: Query< + 'w, + 's, + &'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>, +} + +pub struct InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { + entity: Entity, + 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( + entity: Entity, + params: &'a InspectTextureAffiliationParams<'w1, 's1>, + events: &'a mut AppEvents<'w2, 's2>, + ) -> Self { + Self { entity, params, events } + } + + pub fn show(self, ui: &mut Ui) { + let Ok(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; + } + }; + + ui.separator(); + ui.label("Texture"); + + + } +} pub struct InspectTexture<'a> { - pub texture: &'a Texture, - pub default_file: Option<&'a DefaultFile>, + texture: &'a Texture, + default_file: Option<&'a DefaultFile>, } impl<'a> InspectTexture<'a> { From 84965a3066f9840d57f665ce401c3eb86ad075b2 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Wed, 16 Aug 2023 18:12:07 +0800 Subject: [PATCH 04/19] Allow textures to be edited Signed-off-by: Michael X. Grey --- .../src/widgets/inspector/inspect_fiducial.rs | 17 +- .../src/widgets/inspector/inspect_texture.rs | 228 ++++++++++++++++-- rmf_site_editor/src/widgets/inspector/mod.rs | 9 + rmf_site_editor/src/widgets/menu_bar.rs | 4 + rmf_site_editor/src/widgets/mod.rs | 6 +- 5 files changed, 235 insertions(+), 29 deletions(-) diff --git a/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs b/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs index f406ed68..f3baa63c 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 => {} @@ -109,7 +109,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 +231,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_texture.rs b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs index 681dea26..5c5c1ce9 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -16,33 +16,41 @@ */ use crate::{ - site::DefaultFile, - AppEvents, - inspector::{InspectAssetSource, InspectValue}, + site::{DefaultFile, Change, Category}, + inspector::{InspectAssetSource, InspectValue, SearchResult}, widgets::egui::RichText, - WorkspaceMarker, + WorkspaceMarker, Icons, AppEvents, }; -use bevy::prelude::*; -use bevy_egui::egui::{Grid, Ui}; +use bevy::{ + prelude::*, + ecs::system::SystemParam, +}; +use bevy_egui::egui::{Grid, Ui, ImageButton, ComboBox}; use rmf_site_format::{ RecallAssetSource, Texture, NameInSite, Group, Affiliation, FloorMarker, - WallMarker, + WallMarker, TextureGroup, }; +#[derive(Resource, Default)] +pub struct SearchForTexture(pub String); + +#[derive(SystemParam)] pub struct InspectTextureAffiliationParams<'w, 's> { with_texture: Query< 'w, 's, - &'static Affiliation, + (&'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>, } 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>, } @@ -50,14 +58,15 @@ pub struct InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { pub fn new( entity: Entity, + default_file: Option<&'a DefaultFile>, params: &'a InspectTextureAffiliationParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>, ) -> Self { - Self { entity, params, events } + Self { entity, default_file, params, events } } pub fn show(self, ui: &mut Ui) { - let Ok(affiliation) = self.params.with_texture.get(self.entity) else { return }; + 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) { @@ -70,11 +79,199 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { 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)); + } + + if let Some((group, texture)) = current_texture { + ui.add_space(5.0); + ui.label(RichText::new(format!("Properties of [{current_texture_name}]")).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, group)) + } + } + ui.add_space(10.0); } } @@ -94,7 +291,6 @@ impl<'a> InspectTexture<'a> { 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( @@ -110,7 +306,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) { @@ -121,7 +317,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) { @@ -142,11 +338,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..c7decaa5 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -122,6 +122,7 @@ pub struct InspectorParams<'w, 's> { 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 default_file: Query<'w, 's, &'static DefaultFile>, } @@ -400,7 +401,15 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { } } + InspectTextureAffiliation::new( + selection, + default_file, + &self.params.texture, + self.events, + ).show(ui); + if let Ok(texture) = self.params.textures.get(selection) { + ui.label(RichText::new("Texture Properties").size(18.0)); if let Some(new_texture) = InspectTexture::new(texture, default_file).show(ui) { self.events .change_more diff --git a/rmf_site_editor/src/widgets/menu_bar.rs b/rmf_site_editor/src/widgets/menu_bar.rs index 8b5bd4c8..4e824139 100644 --- a/rmf_site_editor/src/widgets/menu_bar.rs +++ b/rmf_site_editor/src/widgets/menu_bar.rs @@ -49,6 +49,10 @@ pub fn top_menu_bar( { file_events.save.send(SaveWorkspace::new().to_dialog()); } + ui.menu_button("Text expansion", |ui| { + ui.label("Oh look"); + ui.label("It worked"); + }); } if ui .add(Button::new("Open").shortcut_text("Ctrl+O")) diff --git a/rmf_site_editor/src/widgets/mod.rs b/rmf_site_editor/src/widgets/mod.rs index 4242bfe7..1495ef51 100644 --- a/rmf_site_editor/src/widgets/mod.rs +++ b/rmf_site_editor/src/widgets/mod.rs @@ -66,7 +66,9 @@ 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 +104,7 @@ impl Plugin for StandardUiLayout { .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,6 +157,7 @@ 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>, } From afde07249a140b581f0ec0db32388081a0aa3268 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Wed, 16 Aug 2023 21:23:35 +0800 Subject: [PATCH 05/19] Display group information Signed-off-by: Michael X. Grey --- rmf_site_editor/src/sdf_loader.rs | 2 +- rmf_site_editor/src/site/group.rs | 8 ++- .../src/widgets/inspector/inspect_texture.rs | 13 ----- rmf_site_editor/src/widgets/inspector/mod.rs | 57 +++++++++++++++++-- 4 files changed, 61 insertions(+), 19 deletions(-) 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/site/group.rs b/rmf_site_editor/src/site/group.rs index cab42a6d..485404f1 100644 --- a/rmf_site_editor/src/site/group.rs +++ b/rmf_site_editor/src/site/group.rs @@ -15,7 +15,7 @@ * */ -use rmf_site_format::{Affiliation, Group}; +use rmf_site_format::Affiliation; use bevy::{ ecs::system::{Command, EntityCommands}, prelude::*, @@ -24,6 +24,12 @@ use bevy::{ #[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); diff --git a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs index 5c5c1ce9..30ec4341 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -258,19 +258,6 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { .affiliation .send(Change::new(new_affiliation, self.entity)); } - - if let Some((group, texture)) = current_texture { - ui.add_space(5.0); - ui.label(RichText::new(format!("Properties of [{current_texture_name}]")).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, group)) - } - } ui.add_space(10.0); } } diff --git a/rmf_site_editor/src/widgets/inspector/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index c7decaa5..d26b3984 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -96,13 +96,13 @@ use crate::{ interaction::{Selection, SpawnPreview}, site::{ AlignSiteDrawings, BeginEditDrawing, Category, Change, DefaultFile, DrawingMarker, - EdgeLabels, LayerVisibility, Original, SiteID, + EdgeLabels, LayerVisibility, Original, SiteID, Members, }, widgets::AppEvents, AppState, }; use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{Button, RichText, Ui}; +use bevy_egui::egui::{Button, RichText, Ui, CollapsingHeader}; use rmf_site_format::*; // Bevy seems to have a limit of 16 fields in a SystemParam struct, so we split @@ -120,9 +120,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>, } @@ -158,6 +158,13 @@ pub struct InspectorComponentParams<'w, 's> { pub previewable: Query<'w, 's, &'static PreviewableMarker>, } +#[derive(SystemParam)] +pub struct InspectGroupParams<'w, 's> { + pub affiliation: Query<'w, 's, &'static Affiliation>, + pub textures: Query<'w, 's, &'static Texture>, + pub members: Query<'w, 's, &'static Members>, +} + #[derive(SystemParam)] pub struct InspectDrawingParams<'w, 's> { pub distance: Query<'w, 's, &'static Distance>, @@ -408,7 +415,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { self.events, ).show(ui); - if let Ok(texture) = self.params.textures.get(selection) { + if let Ok(texture) = self.params.groups.textures.get(selection) { ui.label(RichText::new("Texture Properties").size(18.0)); if let Some(new_texture) = InspectTexture::new(texture, default_file).show(ui) { self.events @@ -588,6 +595,48 @@ 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(); + ui.label(RichText::new("Group").size(18.0)); + if let Ok(name) = self.params.component.names.get(*group) { + ui.horizontal(|ui| { + ui.label("Name "); + let mut new_name = name.0.clone(); + if ui.text_edit_singleline(&mut new_name).changed() { + self.events.change.name.send( + Change::new(NameInSite(new_name), *group) + ); + } + }); + } + if let Ok(texture) = self.params.groups.textures.get(*group) { + ui.label(RichText::new("Texture Properties").size(18.0)); + 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); + } + if let Ok(members) = self.params.groups.members.get(*group) { + CollapsingHeader::new("Members").show(ui, |ui| { + for member in members.iter() { + let site_id = self.params + .anchor_params.site_id.get(*group).ok().cloned(); + SelectionWidget::new( + *member, + site_id, + &self.params.anchor_params.icons, + self.events, + ) + .as_selected(*member == selection) + .show(ui); + } + }); + } + } } else { ui.label("Nothing selected"); } From f5aaa3822c7d1d1a5ea8668bde062a69d42114ca Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 00:37:23 +0800 Subject: [PATCH 06/19] Allow groups to be edited Signed-off-by: Michael X. Grey --- assets/textures/alignment.png | Bin 34039 -> 24312 bytes assets/textures/alignment.svg | 8 +- assets/textures/alpha.svg | 4 +- assets/textures/attribution.md | 1 + assets/textures/merge.png | Bin 0 -> 1370 bytes rmf_site_editor/src/site/fiducial.rs | 5 + rmf_site_editor/src/site/group.rs | 33 ++- rmf_site_editor/src/site/mod.rs | 4 +- rmf_site_editor/src/site/texture.rs | 9 +- rmf_site_editor/src/site_asset_io.rs | 4 + rmf_site_editor/src/widgets/icons.rs | 3 + .../src/widgets/inspector/inspect_group.rs | 95 ++++++++ rmf_site_editor/src/widgets/inspector/mod.rs | 83 +++---- rmf_site_editor/src/widgets/mod.rs | 14 +- rmf_site_editor/src/widgets/view_groups.rs | 214 ++++++++++++++++++ rmf_site_format/src/category.rs | 4 + 16 files changed, 412 insertions(+), 69 deletions(-) create mode 100644 assets/textures/merge.png create mode 100644 rmf_site_editor/src/widgets/inspector/inspect_group.rs create mode 100644 rmf_site_editor/src/widgets/view_groups.rs diff --git a/assets/textures/alignment.png b/assets/textures/alignment.png index 28bf6e05df12b8efed17338e97d7956ea50478e5..818291ce4e3c48ae8bc2b9347d2336509a155c8d 100644 GIT binary patch literal 24312 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 v+TM0h(i_$M+RqhSCqDDZXh_ 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, Option<&Transform>), Added>, + fiducial_groups: Query, With)>, mut dependents: Query<&mut Dependents, With>, assets: Res, ) { @@ -213,6 +214,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/group.rs b/rmf_site_editor/src/site/group.rs index 485404f1..3946dc8a 100644 --- a/rmf_site_editor/src/site/group.rs +++ b/rmf_site_editor/src/site/group.rs @@ -15,12 +15,17 @@ * */ -use rmf_site_format::Affiliation; +use rmf_site_format::{Affiliation, Group}; use bevy::{ ecs::system::{Command, EntityCommands}, prelude::*, }; +pub struct MergeGroups { + pub from_group: Entity, + pub into_group: Entity, +} + #[derive(Component, Deref)] pub struct Members(Vec); @@ -33,11 +38,33 @@ impl Members { #[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, - mut changed_affiliation: Query<(Entity, &Affiliation), Changed>>, + changed_affiliation: Query<(Entity, &Affiliation), Changed>>, ) { - for (e, affiliation) in &mut changed_affiliation { + for (e, affiliation) in &changed_affiliation { commands.entity(e).set_membership(affiliation.0); } } diff --git a/rmf_site_editor/src/site/mod.rs b/rmf_site_editor/src/site/mod.rs index 1b9cd986..72dd956d 100644 --- a/rmf_site_editor/src/site/mod.rs +++ b/rmf_site_editor/src/site/mod.rs @@ -166,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()) @@ -299,7 +300,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_members_of_groups) + .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) diff --git a/rmf_site_editor/src/site/texture.rs b/rmf_site_editor/src/site/texture.rs index 1d54ba96..5d4ebda3 100644 --- a/rmf_site_editor/src/site/texture.rs +++ b/rmf_site_editor/src/site/texture.rs @@ -15,15 +15,16 @@ * */ -use rmf_site_format::{Texture, Affiliation}; +use rmf_site_format::{Texture, Affiliation, Category}; use bevy::prelude::*; 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, mut image, texture) in &mut changed_textures { + for (e, image, texture) in &mut changed_textures { if let Some(mut image) = image { *image = asset_server.load(String::from(&texture.source)); } else { @@ -31,6 +32,10 @@ pub fn fetch_image_for_texture( commands.entity(e).insert(image); } } + + for e in &new_textures { + commands.entity(e).insert(Category::TextureGroup); + } } // Helper function for entities that need to access their affiliated texture 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/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_group.rs b/rmf_site_editor/src/widgets/inspector/inspect_group.rs new file mode 100644 index 00000000..191ac6a7 --- /dev/null +++ b/rmf_site_editor/src/widgets/inspector/inspect_group.rs @@ -0,0 +1,95 @@ +/* + * 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::{ + Texture, Members, Affiliation, Change, NameInSite, DefaultFile, SiteID, + Group, + }, + widgets::{ + AppEvents, + inspector::{InspectTexture, SelectionWidget}, + }, + Icons, +}; +use bevy::{ + prelude::*, + ecs::system::SystemParam, +}; +use bevy_egui::egui::{Ui, RichText, CollapsingHeader}; + + +#[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/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index d26b3984..43f23b1c 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::*; @@ -158,13 +161,6 @@ pub struct InspectorComponentParams<'w, 's> { pub previewable: Query<'w, 's, &'static PreviewableMarker>, } -#[derive(SystemParam)] -pub struct InspectGroupParams<'w, 's> { - pub affiliation: Query<'w, 's, &'static Affiliation>, - pub textures: Query<'w, 's, &'static Texture>, - pub members: Query<'w, 's, &'static Members>, -} - #[derive(SystemParam)] pub struct InspectDrawingParams<'w, 's> { pub distance: Query<'w, 's, &'static Distance>, @@ -415,17 +411,6 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { self.events, ).show(ui); - if let Ok(texture) = self.params.groups.textures.get(selection) { - ui.label(RichText::new("Texture Properties").size(18.0)); - 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); - } - if let Ok((motion, recall)) = self.params.component.motions.get(selection) { ui.label(RichText::new("Forward Motion").size(18.0)); if let Some(new_motion) = InspectMotionWidget::new(motion, recall).show(ui) { @@ -598,44 +583,30 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { if let Ok(Affiliation(Some(group))) = self.params.groups.affiliation.get(selection) { ui.separator(); - ui.label(RichText::new("Group").size(18.0)); - if let Ok(name) = self.params.component.names.get(*group) { - ui.horizontal(|ui| { - ui.label("Name "); - let mut new_name = name.0.clone(); - if ui.text_edit_singleline(&mut new_name).changed() { - self.events.change.name.send( - Change::new(NameInSite(new_name), *group) - ); - } - }); - } - if let Ok(texture) = self.params.groups.textures.get(*group) { - ui.label(RichText::new("Texture Properties").size(18.0)); - 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); - } - if let Ok(members) = self.params.groups.members.get(*group) { - CollapsingHeader::new("Members").show(ui, |ui| { - for member in members.iter() { - let site_id = self.params - .anchor_params.site_id.get(*group).ok().cloned(); - SelectionWidget::new( - *member, - site_id, - &self.params.anchor_params.icons, - self.events, - ) - .as_selected(*member == selection) - .show(ui); - } - }); - } + 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/mod.rs b/rmf_site_editor/src/widgets/mod.rs index 1495ef51..d6b2da37 100644 --- a/rmf_site_editor/src/widgets/mod.rs +++ b/rmf_site_editor/src/widgets/mod.rs @@ -27,7 +27,7 @@ use crate::{ AlignSiteDrawings, AssociatedGraphs, BeginEditDrawing, Change, ConsiderAssociatedGraph, ConsiderLocationTag, CurrentLevel, Delete, DrawingMarker, ExportLights, FinishEditDrawing, GlobalDrawingVisibility, GlobalFloorVisibility, LayerVisibility, PhysicalLightToggle, - SaveNavGraphs, SiteState, Texture, ToggleLiftDoorAvailability, + SaveNavGraphs, SiteState, Texture, ToggleLiftDoorAvailability, MergeGroups, }, 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::*; @@ -105,6 +108,7 @@ impl Plugin for StandardUiLayout { .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) @@ -160,6 +164,7 @@ pub struct MoreChangeEvents<'w, 's> { 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)] @@ -289,6 +294,7 @@ fn site_ui_layout( lights: LightParams, nav_graphs: NavGraphParams, layers: LayersParams, + mut groups: GroupParams, mut events: AppEvents, ) { egui::SidePanel::right("right_panel") @@ -332,6 +338,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| { 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..a0da626a --- /dev/null +++ b/rmf_site_editor/src/widgets/view_groups.rs @@ -0,0 +1,214 @@ +/* + * 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::{ + NameInSite, SiteID, Members, Texture, FiducialMarker, MergeGroups, + Change, + }, + widgets::{ + AppEvents, + inspector::SelectionWidget, + }, + Icons, +}; +use bevy::{ + prelude::*, + ecs::system::SystemParam, +}; +use bevy_egui::egui::{Ui, CollapsingHeader, ImageButton, Button}; + +#[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_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", From 3a28ec276428f588c7de157f9c7c768f9aa47253 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 12:53:27 +0800 Subject: [PATCH 07/19] Automatically assign new walls and floors the last texture that the user selected Signed-off-by: Michael X. Grey --- .../src/interaction/select_anchor.rs | 46 ++++++++++++- rmf_site_editor/src/site/mod.rs | 8 +++ rmf_site_editor/src/site/texture.rs | 67 ++++++++++++++++++- 3 files changed, 117 insertions(+), 4 deletions(-) diff --git a/rmf_site_editor/src/interaction/select_anchor.rs b/rmf_site_editor/src/interaction/select_anchor.rs index 9b50881b..75ec49b5 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,23 @@ 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 +829,23 @@ 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 +1222,10 @@ 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 +1341,10 @@ 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/site/mod.rs b/rmf_site_editor/src/site/mod.rs index 1b9cd986..66a76287 100644 --- a/rmf_site_editor/src/site/mod.rs +++ b/rmf_site_editor/src/site/mod.rs @@ -222,6 +222,14 @@ impl Plugin for SitePlugin { .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( diff --git a/rmf_site_editor/src/site/texture.rs b/rmf_site_editor/src/site/texture.rs index 1d54ba96..8b04fb7e 100644 --- a/rmf_site_editor/src/site/texture.rs +++ b/rmf_site_editor/src/site/texture.rs @@ -15,9 +15,12 @@ * */ -use rmf_site_format::{Texture, Affiliation}; +use rmf_site_format::{Texture, Affiliation, FloorMarker, WallMarker, Group}; use bevy::prelude::*; +#[derive(Component)] +pub struct TextureNeedsAssignment; + pub fn fetch_image_for_texture( mut commands: Commands, mut changed_textures: Query<(Entity, Option<&mut Handle>, &Texture), Changed>, @@ -33,6 +36,68 @@ pub fn fetch_image_for_texture( } } +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( From e546cad391641a5f53a0a6273891b554289636bc Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 13:03:02 +0800 Subject: [PATCH 08/19] Fix style Signed-off-by: Michael X. Grey --- .../src/interaction/select_anchor.rs | 48 +++--- rmf_site_editor/src/site/floor.rs | 30 +--- rmf_site_editor/src/site/group.rs | 9 +- rmf_site_editor/src/site/load.rs | 96 +++++++----- rmf_site_editor/src/site/mod.rs | 10 +- rmf_site_editor/src/site/save.rs | 13 +- rmf_site_editor/src/site/texture.rs | 24 +-- rmf_site_editor/src/site/wall.rs | 33 ++-- rmf_site_editor/src/widgets/console.rs | 10 +- .../src/widgets/inspector/inspect_texture.rs | 141 +++++++++--------- rmf_site_editor/src/widgets/inspector/mod.rs | 3 +- rmf_site_editor/src/widgets/mod.rs | 4 +- rmf_site_format/src/constraint.rs | 5 +- rmf_site_format/src/edge.rs | 5 +- rmf_site_format/src/fiducial.rs | 5 +- rmf_site_format/src/floor.rs | 5 +- rmf_site_format/src/legacy/building_map.rs | 39 +++-- rmf_site_format/src/legacy/floor.rs | 58 +++---- rmf_site_format/src/legacy/rbmf.rs | 6 +- rmf_site_format/src/legacy/wall.rs | 10 +- rmf_site_format/src/lift.rs | 23 +-- rmf_site_format/src/location.rs | 5 +- rmf_site_format/src/measurement.rs | 5 +- rmf_site_format/src/misc.rs | 5 +- rmf_site_format/src/nav_graph.rs | 12 +- rmf_site_format/src/path.rs | 8 +- rmf_site_format/src/point.rs | 5 +- rmf_site_format/src/texture.rs | 2 +- 28 files changed, 304 insertions(+), 315 deletions(-) diff --git a/rmf_site_editor/src/interaction/select_anchor.rs b/rmf_site_editor/src/interaction/select_anchor.rs index 75ec49b5..0e2a64d9 100644 --- a/rmf_site_editor/src/interaction/select_anchor.rs +++ b/rmf_site_editor/src/interaction/select_anchor.rs @@ -386,11 +386,13 @@ impl EdgePlacement { 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 - }); + result.create = Arc::new( + move |params: &mut SelectAnchorPlacementParams, edge: Edge| { + let entity = base(params, edge); + f(params, entity); + entity + }, + ); Arc::new(result) } @@ -838,11 +840,13 @@ impl PathPlacement { 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 - }); + result.create = Arc::new( + move |params: &mut SelectAnchorPlacementParams, path: Path| { + let entity = base(params, path); + f(params, entity); + entity + }, + ); Arc::new(result) } @@ -1222,10 +1226,14 @@ impl SelectAnchorEdgeBuilder { pub fn for_wall(self) -> SelectAnchor { SelectAnchor { target: self.for_element, - placement: EdgePlacement::new::>(self.placement) - .with_extra(|params, entity| { - params.commands.entity(entity).insert(TextureNeedsAssignment); - }), + placement: EdgePlacement::new::>(self.placement).with_extra( + |params, entity| { + params + .commands + .entity(entity) + .insert(TextureNeedsAssignment); + }, + ), continuity: self.continuity, scope: Scope::General, } @@ -1341,10 +1349,14 @@ impl SelectAnchorPathBuilder { pub fn for_floor(self) -> SelectAnchor { SelectAnchor { target: self.for_element, - placement: PathPlacement::new::>(self.placement) - .with_extra(|params, entity| { - params.commands.entity(entity).insert(TextureNeedsAssignment); - }), + 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/site/floor.rs b/rmf_site_editor/src/site/floor.rs index 6ded6064..cc966383 100644 --- a/rmf_site_editor/src/site/floor.rs +++ b/rmf_site_editor/src/site/floor.rs @@ -308,29 +308,17 @@ pub fn update_floors_for_moved_anchors( } pub fn update_floors( - floors: Query< - (&FloorSegments, &Path, &Affiliation), - With, - >, + floors: Query<(&FloorSegments, &Path, &Affiliation), With>, changed_floors: Query< Entity, ( With, - Or<( - Changed>, - Changed>, - )>, - ) + Or<(Changed>, Changed>)>, + ), >, changed_texture_sources: Query< &Members, - ( - With, - Or<( - Changed>, - Changed, - )>, - ) + (With, Or<(Changed>, Changed)>), >, mut meshes: ResMut>, mut materials: ResMut>, @@ -339,13 +327,11 @@ pub fn update_floors( anchors: AnchorParams, textures: Query<(Option<&Handle>, &Texture)>, ) { - for e in changed_floors.iter() - .chain( - changed_texture_sources + for e in changed_floors.iter().chain( + changed_texture_sources .iter() - .flat_map(|members| members.iter().cloned()) - ) - { + .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) { diff --git a/rmf_site_editor/src/site/group.rs b/rmf_site_editor/src/site/group.rs index cab42a6d..3fd8507e 100644 --- a/rmf_site_editor/src/site/group.rs +++ b/rmf_site_editor/src/site/group.rs @@ -15,11 +15,11 @@ * */ -use rmf_site_format::{Affiliation, Group}; use bevy::{ ecs::system::{Command, EntityCommands}, prelude::*, }; +use rmf_site_format::{Affiliation, Group}; #[derive(Component, Deref)] pub struct Members(Vec); @@ -43,8 +43,11 @@ struct ChangeMembership { impl Command for ChangeMembership { fn write(self, world: &mut World) { - let last = world.get_entity(self.member) - .map(|e| e.get::()).flatten().cloned(); + 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 diff --git a/rmf_site_editor/src/site/load.rs b/rmf_site_editor/src/site/load.rs index af95d313..6f7a5e38 100644 --- a/rmf_site_editor/src/site/load.rs +++ b/rmf_site_editor/src/site/load.rs @@ -19,7 +19,7 @@ use crate::{recency::RecencyRanking, site::*, Autoload, CurrentWorkspace, Worksp use bevy::{ecs::system::SystemParam, prelude::*, tasks::AsyncComputeTaskPool}; use futures_lite::future; use rmf_site_format::legacy::building_map::BuildingMap; -use std::{collections::HashMap, path::PathBuf, backtrace::Backtrace}; +use std::{backtrace::Backtrace, collections::HashMap, path::PathBuf}; use thiserror::Error as ThisError; #[cfg(not(target_arch = "wasm32"))] @@ -49,7 +49,11 @@ struct LoadSiteError { impl LoadSiteError { fn new(site: Entity, broken: u32) -> Self { - Self { site, broken, backtrace: Backtrace::force_capture() } + Self { + site, + broken, + backtrace: Backtrace::force_capture(), + } } } @@ -63,7 +67,10 @@ impl LoadResult for Result { } } -fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format::Site) -> Result { +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| { @@ -141,13 +148,21 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: } for (fiducial_id, fiducial) in &drawing.fiducials { drawing_parent - .spawn(fiducial.convert(&id_to_entity).for_site(site_id)?) + .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.convert(&id_to_entity).for_site(site_id)?) + .spawn( + measurement + .convert(&id_to_entity) + .for_site(site_id)?, + ) .insert(SiteID(*measurement_id)); consider_id(*measurement_id); } @@ -212,36 +227,40 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (lift_id, lift_data) in &site_data.lifts { 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(()) - })?; + 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) - .insert(lift_data.properties.convert(&id_to_entity).for_site(site_id)?) + .insert( + lift_data + .properties + .convert(&id_to_entity) + .for_site(site_id)?, + ) .id(); id_to_entity.insert(*lift_id, lift); consider_id(*lift_id); @@ -311,7 +330,12 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for anchor in door.reference_anchors.array() { commands .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)?))); + .insert(Subordinate(Some( + *id_to_entity + .get(lift_id) + .ok_or(*lift_id) + .for_site(site_id)?, + ))); } } } @@ -557,7 +581,8 @@ 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) + 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).id(); @@ -566,7 +591,8 @@ fn generate_imported_nav_graphs( } for (location_id, location_data) in &from_site_data.navigation.guided.locations { - let location_data = location_data.convert(&id_to_entity) + 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).id(); diff --git a/rmf_site_editor/src/site/mod.rs b/rmf_site_editor/src/site/mod.rs index 66a76287..ed37edc9 100644 --- a/rmf_site_editor/src/site/mod.rs +++ b/rmf_site_editor/src/site/mod.rs @@ -223,12 +223,14 @@ impl Plugin for SitePlugin { .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( + 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( + apply_last_selected_texture:: + .after(detect_last_selected_texture::), ) .with_system(update_material_for_display_color), ) diff --git a/rmf_site_editor/src/site/save.rs b/rmf_site_editor/src/site/save.rs index bdbc744c..e76afab1 100644 --- a/rmf_site_editor/src/site/save.rs +++ b/rmf_site_editor/src/site/save.rs @@ -901,11 +901,14 @@ fn generate_texture_groups( 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(), - }); + texture_groups.insert( + site_id.0, + TextureGroup { + name: name.clone(), + texture: texture.clone(), + group: Default::default(), + }, + ); } Ok(texture_groups) diff --git a/rmf_site_editor/src/site/texture.rs b/rmf_site_editor/src/site/texture.rs index 8b04fb7e..589a4436 100644 --- a/rmf_site_editor/src/site/texture.rs +++ b/rmf_site_editor/src/site/texture.rs @@ -15,8 +15,8 @@ * */ -use rmf_site_format::{Texture, Affiliation, FloorMarker, WallMarker, Group}; use bevy::prelude::*; +use rmf_site_format::{Affiliation, FloorMarker, Group, Texture, WallMarker}; #[derive(Component)] pub struct TextureNeedsAssignment; @@ -40,9 +40,7 @@ pub fn detect_last_selected_texture( mut commands: Commands, parents: Query<&Parent>, mut last_selected: Query<&mut LastSelectedTexture>, - changed_affiliations: Query< - &Affiliation, (Changed>, With) - >, + changed_affiliations: Query<&Affiliation, (Changed>, With)>, removed_groups: RemovedComponents, ) { if let Some(Affiliation(Some(affiliation))) = changed_affiliations.iter().last() { @@ -70,7 +68,10 @@ pub fn apply_last_selected_texture( mut commands: Commands, parents: Query<&Parent>, last_selected: Query<&LastSelectedTexture>, - mut unassigned: Query<(Entity, &mut Affiliation), (With, With)>, + mut unassigned: Query< + (Entity, &mut Affiliation), + (With, With), + >, ) { for (e, mut affiliation) in &mut unassigned { let mut search = e; @@ -93,7 +94,7 @@ pub fn apply_last_selected_texture( } #[derive(Component)] -pub struct LastSelectedTexture{ +pub struct LastSelectedTexture { selection: Option, marker: std::marker::PhantomData, } @@ -104,9 +105,10 @@ 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())) + 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 d0ecace9..c1912c1e 100644 --- a/rmf_site_editor/src/site/wall.rs +++ b/rmf_site_editor/src/site/wall.rs @@ -96,7 +96,15 @@ pub fn add_wall_visual( } pub fn update_walls_for_moved_anchors( - mut walls: Query<(Entity, &Edge, &Affiliation, &mut Handle), With>, + mut walls: Query< + ( + Entity, + &Edge, + &Affiliation, + &mut Handle, + ), + With, + >, anchors: AnchorParams, textures: Query<(Option<&Handle>, &Texture)>, changed_anchors: Query< @@ -132,34 +140,23 @@ pub fn update_walls( Entity, ( With, - Or<( - Changed>, - Changed>, - )>, + Or<(Changed>, Changed>)>, ), >, changed_texture_sources: Query< &Members, - ( - With, - Or<( - Changed>, - Changed, - )>, - ) + (With, Or<(Changed>, Changed)>), >, mut meshes: ResMut>, mut materials: ResMut>, anchors: AnchorParams, textures: Query<(Option<&Handle>, &Texture)>, ) { - for e in changed_walls.iter() - .chain( - changed_texture_sources + for e in changed_walls.iter().chain( + changed_texture_sources .iter() - .flat_map(|members| members.iter().cloned()) - ) - { + .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)); diff --git a/rmf_site_editor/src/widgets/console.rs b/rmf_site_editor/src/widgets/console.rs index fc469a80..140da674 100644 --- a/rmf_site_editor/src/widgets/console.rs +++ b/rmf_site_editor/src/widgets/console.rs @@ -158,12 +158,10 @@ fn print_log(ui: &mut egui::Ui, element: &LogHistoryElement) { } if truncated { - ui - .label(" [...]") - .on_hover_text( - "Some of the message is hidden. Click on it to copy the \ - full text to your clipboard." - ); + 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/inspector/inspect_texture.rs b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs index 5c5c1ce9..713d921c 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -16,19 +16,16 @@ */ use crate::{ - site::{DefaultFile, Change, Category}, inspector::{InspectAssetSource, InspectValue, SearchResult}, + site::{Category, Change, DefaultFile}, widgets::egui::RichText, - WorkspaceMarker, Icons, AppEvents, + AppEvents, Icons, WorkspaceMarker, }; -use bevy::{ - prelude::*, - ecs::system::SystemParam, -}; -use bevy_egui::egui::{Grid, Ui, ImageButton, ComboBox}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{ComboBox, Grid, ImageButton, Ui}; use rmf_site_format::{ - RecallAssetSource, Texture, NameInSite, Group, Affiliation, FloorMarker, - WallMarker, TextureGroup, + Affiliation, FloorMarker, Group, NameInSite, RecallAssetSource, Texture, TextureGroup, + WallMarker, }; #[derive(Resource, Default)] @@ -62,7 +59,12 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { params: &'a InspectTextureAffiliationParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>, ) -> Self { - Self { entity, default_file, params, events } + Self { + entity, + default_file, + params, + events, + } } pub fn show(self, ui: &mut Ui) { @@ -111,10 +113,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { ui.label("Texture"); ui.horizontal(|ui| { if any_partial_matches { - if ui.add(ImageButton::new( - self.params.icons.search.egui(), - [18., 18.], - )) + 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() { @@ -127,10 +130,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { match result { SearchResult::Empty => { - if ui.add(ImageButton::new( - self.params.icons.hidden.egui(), - [18., 18.], - )) + 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() { @@ -138,10 +142,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } } SearchResult::Current => { - if ui.add(ImageButton::new( - self.params.icons.selected.egui(), - [18., 18.], - )) + 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() { @@ -149,22 +154,20 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } } 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" - } - ) + 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() { + let new_texture = if let Some((_, t)) = affiliation + .0 + .map(|a| self.params.texture_groups.get(a).ok()) + .flatten() + { t.clone() } else { Texture::default() @@ -180,17 +183,18 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }) .set_parent(site) .id(); - self.events - .change_more - .affiliation - .send(Change::new(Affiliation(Some(new_texture_group)), self.entity)); + 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.], - )) + if ui + .add(ImageButton::new( + self.params.icons.confirm.egui(), + [18., 18.], + )) .on_hover_text("Select this texture") .clicked() { @@ -201,10 +205,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } } SearchResult::Conflict(text) => { - if ui.add(ImageButton::new( - self.params.icons.reject.egui(), - [18., 18.], - )) + if ui + .add(ImageButton::new( + self.params.icons.reject.egui(), + [18., 18.], + )) .on_hover_text(text) .clicked() { @@ -218,16 +223,20 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }); 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))) - ) + self.params + .texture_groups + .get(a) + .ok() + .map(|(n, t)| (n.0.as_str(), Some((a, t)))) } else { None - }.unwrap_or(("", 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.])) + if ui + .add(ImageButton::new(self.params.icons.exit.egui(), [18., 18.])) .on_hover_text(format!("Remove this texture from the {}", category.label())) .clicked() { @@ -262,9 +271,7 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { if let Some((group, texture)) = current_texture { ui.add_space(5.0); ui.label(RichText::new(format!("Properties of [{current_texture_name}]")).size(18.0)); - if let Some(new_texture) = InspectTexture::new( - texture, self.default_file - ).show(ui) { + if let Some(new_texture) = InspectTexture::new(texture, self.default_file).show(ui) { self.events .change_more .texture @@ -281,23 +288,23 @@ pub struct InspectTexture<'a> { } impl<'a> InspectTexture<'a> { - pub fn new( - texture: &'a Texture, - default_file: Option<&'a DefaultFile>, - ) -> Self { - Self { texture, default_file } + 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(); // 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; } diff --git a/rmf_site_editor/src/widgets/inspector/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index c7decaa5..d785e472 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -406,7 +406,8 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { default_file, &self.params.texture, self.events, - ).show(ui); + ) + .show(ui); if let Ok(texture) = self.params.textures.get(selection) { ui.label(RichText::new("Texture Properties").size(18.0)); diff --git a/rmf_site_editor/src/widgets/mod.rs b/rmf_site_editor/src/widgets/mod.rs index 1495ef51..2810b5e0 100644 --- a/rmf_site_editor/src/widgets/mod.rs +++ b/rmf_site_editor/src/widgets/mod.rs @@ -66,9 +66,7 @@ pub mod icons; pub use icons::*; pub mod inspector; -use inspector::{ - InspectorParams, InspectorWidget, SearchForFiducial, SearchForTexture, -}; +use inspector::{InspectorParams, InspectorWidget, SearchForFiducial, SearchForTexture}; pub mod move_layer; pub use move_layer::*; diff --git a/rmf_site_format/src/constraint.rs b/rmf_site_format/src/constraint.rs index 8d0d99e6..a52e8dcf 100644 --- a/rmf_site_format/src/constraint.rs +++ b/rmf_site_format/src/constraint.rs @@ -35,10 +35,7 @@ pub struct Constraint { pub struct ConstraintMarker; impl Constraint { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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/edge.rs b/rmf_site_format/src/edge.rs index 4dc8f2ba..01b9d12d 100644 --- a/rmf_site_format/src/edge.rs +++ b/rmf_site_format/src/edge.rs @@ -108,10 +108,7 @@ impl From<[T; 2]> for Edge { } impl Edge { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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 6bb01cc6..2468dbe7 100644 --- a/rmf_site_format/src/fiducial.rs +++ b/rmf_site_format/src/fiducial.rs @@ -61,10 +61,7 @@ impl FiducialGroup { pub struct FiducialMarker; impl Fiducial { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Fiducial { anchor: self.anchor.convert(id_map)?, affiliation: self.affiliation.convert(id_map)?, diff --git a/rmf_site_format/src/floor.rs b/rmf_site_format/src/floor.rs index 5a0d8c93..8ae08baa 100644 --- a/rmf_site_format/src/floor.rs +++ b/rmf_site_format/src/floor.rs @@ -41,10 +41,7 @@ pub struct Floor { pub struct FloorMarker; impl Floor { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Floor { anchors: self.anchors.convert(id_map)?, texture: self.texture.convert(id_map)?, diff --git a/rmf_site_format/src/legacy/building_map.rs b/rmf_site_format/src/legacy/building_map.rs index f9ff4161..4a4bb273 100644 --- a/rmf_site_format/src/legacy/building_map.rs +++ b/rmf_site_format/src/legacy/building_map.rs @@ -1,6 +1,5 @@ use super::{ - level::Level, lift::Lift, floor::FloorParameters, PortingError, Result, - wall::WallProperties, + floor::FloorParameters, level::Level, lift::Lift, wall::WallProperties, PortingError, Result, }; use crate::{ alignment::align_legacy_building, Affiliation, Anchor, Angle, AssetSource, AssociatedGraphs, @@ -9,7 +8,7 @@ use crate::{ 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, + Texture as SiteTexture, TextureGroup, DEFAULT_NAV_GRAPH_COLORS, }; use glam::{DAffine2, DMat3, DQuat, DVec2, DVec3, EulerRot}; use serde::{Deserialize, Serialize}; @@ -612,24 +611,22 @@ impl BuildingMap { 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(), - } - ) - } - ) + .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 { diff --git a/rmf_site_format/src/legacy/floor.rs b/rmf_site_format/src/legacy/floor.rs index de2d9e01..72b1c17d 100644 --- a/rmf_site_format/src/legacy/floor.rs +++ b/rmf_site_format/src/legacy/floor.rs @@ -1,11 +1,11 @@ use super::{rbmf::*, PortingError, Result}; use crate::{ - Angle, AssetSource, Floor as SiteFloor, FloorMarker, Path, - PreferredSemiTransparency, Texture, Affiliation, + Affiliation, Angle, AssetSource, Floor as SiteFloor, FloorMarker, Path, + PreferredSemiTransparency, Texture, }; use serde::{Deserialize, Serialize}; use std::{ - collections::{HashMap, BTreeMap}, + collections::{BTreeMap, HashMap}, ops::RangeFrom, }; @@ -39,32 +39,34 @@ 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 = *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 - }); + let texture_site_id = site_id.next().unwrap(); + textures.insert(texture_site_id, texture); + texture_site_id + }); Ok(SiteFloor { anchors: Path(anchors), diff --git a/rmf_site_format/src/legacy/rbmf.rs b/rmf_site_format/src/legacy/rbmf.rs index a6b9be68..8032e689 100644 --- a/rmf_site_format/src/legacy/rbmf.rs +++ b/rmf_site_format/src/legacy/rbmf.rs @@ -1,8 +1,8 @@ // RBMF stands for "RMF Building Map Format" use std::{ - ops::{Deref, DerefMut}, hash::Hash, + ops::{Deref, DerefMut}, }; use serde::{Deserialize, Serialize}; @@ -34,7 +34,7 @@ impl PartialEq for RbmfString { } } -impl Eq for RbmfString { } +impl Eq for RbmfString {} impl From for String { fn from(s: RbmfString) -> Self { @@ -128,7 +128,7 @@ impl Hash for RbmfFloat { } } -impl Eq for RbmfFloat { } +impl Eq for RbmfFloat {} impl PartialOrd for RbmfFloat { fn partial_cmp(&self, other: &Self) -> Option { diff --git a/rmf_site_format/src/legacy/wall.rs b/rmf_site_format/src/legacy/wall.rs index 476d2b9e..aaab3f51 100644 --- a/rmf_site_format/src/legacy/wall.rs +++ b/rmf_site_format/src/legacy/wall.rs @@ -1,10 +1,8 @@ use super::{rbmf::*, PortingError, Result}; -use crate::{ - AssetSource, Texture, Wall as SiteWall, Affiliation, DEFAULT_LEVEL_HEIGHT, -}; +use crate::{Affiliation, AssetSource, Texture, Wall as SiteWall, DEFAULT_LEVEL_HEIGHT}; use serde::{Deserialize, Serialize}; use std::{ - collections::{HashMap, BTreeMap}, + collections::{BTreeMap, HashMap}, ops::RangeFrom, }; @@ -78,8 +76,8 @@ impl Wall { + ".png", ), 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), + 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), alpha: Some(self.2.alpha.1 as f32), } }; diff --git a/rmf_site_format/src/lift.rs b/rmf_site_format/src/lift.rs index d07a71c3..ed62b161 100644 --- a/rmf_site_format/src/lift.rs +++ b/rmf_site_format/src/lift.rs @@ -60,10 +60,7 @@ pub struct LiftCabinDoor { } impl LiftCabinDoor { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(LiftCabinDoor { kind: self.kind.clone(), reference_anchors: self.reference_anchors.convert(id_map)?, @@ -85,11 +82,9 @@ impl Default for LevelVisits { } impl LevelVisits { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { - let set: Result, T> = self.0 + 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(); @@ -178,10 +173,7 @@ impl LiftCabin { None } - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { let result = match self { LiftCabin::Rect(cabin) => LiftCabin::Rect(cabin.convert(id_map)?), }; @@ -486,10 +478,7 @@ pub struct LiftCabinDoorPlacement { } impl LiftProperties { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(LiftProperties { name: self.name.clone(), reference_anchors: self.reference_anchors.convert(id_map)?, diff --git a/rmf_site_format/src/location.rs b/rmf_site_format/src/location.rs index 73b8e7bb..9d3ea516 100644 --- a/rmf_site_format/src/location.rs +++ b/rmf_site_format/src/location.rs @@ -85,10 +85,7 @@ impl Default for LocationTags { } impl Location { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Location { anchor: self.anchor.convert(id_map)?, tags: self.tags.clone(), diff --git a/rmf_site_format/src/measurement.rs b/rmf_site_format/src/measurement.rs index 4a100644..16efaa21 100644 --- a/rmf_site_format/src/measurement.rs +++ b/rmf_site_format/src/measurement.rs @@ -61,10 +61,7 @@ impl Measurement { } impl Measurement { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Measurement { anchors: self.anchors.convert(id_map)?, distance: self.distance, diff --git a/rmf_site_format/src/misc.rs b/rmf_site_format/src/misc.rs index be43ca75..a7fa86fc 100644 --- a/rmf_site_format/src/misc.rs +++ b/rmf_site_format/src/misc.rs @@ -477,10 +477,7 @@ impl Default for Affiliation { } impl Affiliation { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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 { diff --git a/rmf_site_format/src/nav_graph.rs b/rmf_site_format/src/nav_graph.rs index 61e0778c..d9fbbcc3 100644 --- a/rmf_site_format/src/nav_graph.rs +++ b/rmf_site_format/src/nav_graph.rs @@ -113,16 +113,11 @@ impl Default for AssociatedGraphs { } impl AssociatedGraphs { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { let result = match self { Self::All => AssociatedGraphs::All, Self::Only(set) => AssociatedGraphs::Only(Self::convert_set(set, id_map)?), - Self::AllExcept(set) => { - AssociatedGraphs::AllExcept(Self::convert_set(set, id_map)?) - } + Self::AllExcept(set) => AssociatedGraphs::AllExcept(Self::convert_set(set, id_map)?), }; Ok(result) } @@ -131,8 +126,7 @@ impl AssociatedGraphs { set: &BTreeSet, id_map: &HashMap, ) -> Result, T> { - set - .iter() + set.iter() .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 586fe6ca..34fe2b1f 100644 --- a/rmf_site_format/src/path.rs +++ b/rmf_site_format/src/path.rs @@ -27,11 +27,9 @@ use std::collections::HashMap; pub struct Path(pub Vec); impl Path { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { - let path: Result, T> = self.0 + 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(); diff --git a/rmf_site_format/src/point.rs b/rmf_site_format/src/point.rs index 18a2fcd1..7dca6c74 100644 --- a/rmf_site_format/src/point.rs +++ b/rmf_site_format/src/point.rs @@ -33,10 +33,7 @@ impl From for Point { } impl Point { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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/texture.rs b/rmf_site_format/src/texture.rs index cfc8b297..d0705b1b 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, Bundle}; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] From 6d24f0eff6bcc944be4bbbba8e8bddd3a2a7aa1b Mon Sep 17 00:00:00 2001 From: Luca Della Vedova Date: Thu, 17 Aug 2023 13:07:29 +0800 Subject: [PATCH 09/19] Fix panic when creating new floor (#148) Signed-off-by: Luca Della Vedova --- rmf_site_editor/src/shapes.rs | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/rmf_site_editor/src/shapes.rs b/rmf_site_editor/src/shapes.rs index a960b2e6..3268ff9f 100644 --- a/rmf_site_editor/src/shapes.rs +++ b/rmf_site_editor/src/shapes.rs @@ -1107,14 +1107,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; From b7fc019797684a3e89982429f08cde2b687e3149 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 13:48:34 +0800 Subject: [PATCH 10/19] Lock in the version of thiserror Signed-off-by: Michael X. Grey --- rmf_site_editor/Cargo.toml | 4 +++- rmf_site_format/Cargo.toml | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/rmf_site_editor/Cargo.toml b/rmf_site_editor/Cargo.toml index f1f2221b..a4487880 100644 --- a/rmf_site_editor/Cargo.toml +++ b/rmf_site_editor/Cargo.toml @@ -34,7 +34,9 @@ bevy_utils = "0.9" dirs = "4.0" thread_local = "*" lyon = "1" -thiserror = "*" +# TODO(@mxgrey): A newer version of thiserror introduced a CI error. Remove this +# inequality requirement when that error can be resolved. +thiserror = "<=1.0.40" rmf_site_format = { path = "../rmf_site_format", features = ["bevy"] } itertools = "*" bitfield = "*" diff --git a/rmf_site_format/Cargo.toml b/rmf_site_format/Cargo.toml index 949be05c..56a96f91 100644 --- a/rmf_site_format/Cargo.toml +++ b/rmf_site_format/Cargo.toml @@ -12,7 +12,9 @@ serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8.23" serde_json = "*" ron = "0.7" -thiserror = "*" +# TODO(@mxgrey): A newer version of thiserror introduced a CI error. Remove this +# inequality requirement when that error can be resolved. +thiserror = "<=1.0.40" glam = "0.22" # add features=["bevy"] to a dependent Cargo.toml to get the bevy-related features bevy = { version = "0.9", optional = true } From 25f95dd8eb0d5a2bf1d2bc81f79b2084a92725af Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 14:09:56 +0800 Subject: [PATCH 11/19] Get rid of nightly features Signed-off-by: Michael X. Grey --- rmf_site_editor/Cargo.toml | 4 +--- rmf_site_editor/src/lib.rs | 2 -- rmf_site_editor/src/site/load.rs | 6 +++--- rmf_site_format/Cargo.toml | 4 +--- 4 files changed, 5 insertions(+), 11 deletions(-) diff --git a/rmf_site_editor/Cargo.toml b/rmf_site_editor/Cargo.toml index a4487880..f1f2221b 100644 --- a/rmf_site_editor/Cargo.toml +++ b/rmf_site_editor/Cargo.toml @@ -34,9 +34,7 @@ bevy_utils = "0.9" dirs = "4.0" thread_local = "*" lyon = "1" -# TODO(@mxgrey): A newer version of thiserror introduced a CI error. Remove this -# inequality requirement when that error can be resolved. -thiserror = "<=1.0.40" +thiserror = "*" rmf_site_format = { path = "../rmf_site_format", features = ["bevy"] } itertools = "*" bitfield = "*" diff --git a/rmf_site_editor/src/lib.rs b/rmf_site_editor/src/lib.rs index dd5c02e1..3aa6d49b 100644 --- a/rmf_site_editor/src/lib.rs +++ b/rmf_site_editor/src/lib.rs @@ -1,5 +1,3 @@ -#![feature(error_generic_member_access, provide_any)] - use bevy::{ log::LogPlugin, pbr::DirectionalLightShadowMap, prelude::*, render::renderer::RenderAdapterInfo, }; diff --git a/rmf_site_editor/src/site/load.rs b/rmf_site_editor/src/site/load.rs index 6f7a5e38..74a39c4f 100644 --- a/rmf_site_editor/src/site/load.rs +++ b/rmf_site_editor/src/site/load.rs @@ -19,7 +19,7 @@ use crate::{recency::RecencyRanking, site::*, Autoload, CurrentWorkspace, Worksp use bevy::{ecs::system::SystemParam, prelude::*, tasks::AsyncComputeTaskPool}; use futures_lite::future; use rmf_site_format::legacy::building_map::BuildingMap; -use std::{backtrace::Backtrace, collections::HashMap, path::PathBuf}; +use std::{collections::HashMap, path::PathBuf}; use thiserror::Error as ThisError; #[cfg(not(target_arch = "wasm32"))] @@ -44,7 +44,8 @@ pub struct LoadSite { struct LoadSiteError { site: Entity, broken: u32, - backtrace: Backtrace, + // TODO(@mxgrey): reintroduce Backtrack when it's supported on stable + // backtrace: Backtrace, } impl LoadSiteError { @@ -52,7 +53,6 @@ impl LoadSiteError { Self { site, broken, - backtrace: Backtrace::force_capture(), } } } diff --git a/rmf_site_format/Cargo.toml b/rmf_site_format/Cargo.toml index 56a96f91..949be05c 100644 --- a/rmf_site_format/Cargo.toml +++ b/rmf_site_format/Cargo.toml @@ -12,9 +12,7 @@ serde = { version = "1.0", features = ["derive"] } serde_yaml = "0.8.23" serde_json = "*" ron = "0.7" -# TODO(@mxgrey): A newer version of thiserror introduced a CI error. Remove this -# inequality requirement when that error can be resolved. -thiserror = "<=1.0.40" +thiserror = "*" glam = "0.22" # add features=["bevy"] to a dependent Cargo.toml to get the bevy-related features bevy = { version = "0.9", optional = true } From 473351e865f2746039cef57f61f5a311863f3787 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 14:11:10 +0800 Subject: [PATCH 12/19] Fix style Signed-off-by: Michael X. Grey --- rmf_site_editor/src/site/load.rs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/rmf_site_editor/src/site/load.rs b/rmf_site_editor/src/site/load.rs index 74a39c4f..21b0b11a 100644 --- a/rmf_site_editor/src/site/load.rs +++ b/rmf_site_editor/src/site/load.rs @@ -50,10 +50,7 @@ struct LoadSiteError { impl LoadSiteError { fn new(site: Entity, broken: u32) -> Self { - Self { - site, - broken, - } + Self { site, broken } } } From 0ef3d83d732abd5d8e45688eacdb5a25639c53b1 Mon Sep 17 00:00:00 2001 From: Luca Della Vedova Date: Thu, 17 Aug 2023 14:15:37 +0800 Subject: [PATCH 13/19] Add Cartesian fiducials to reference drawing features (#164) Signed-off-by: Luca Della Vedova Signed-off-by: Michael X. Grey Co-authored-by: Michael X. Grey --- rmf_site_format/src/legacy/building_map.rs | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/rmf_site_format/src/legacy/building_map.rs b/rmf_site_format/src/legacy/building_map.rs index 4bfceb58..77618f54 100644 --- a/rmf_site_format/src/legacy/building_map.rs +++ b/rmf_site_format/src/legacy/building_map.rs @@ -1,7 +1,7 @@ use super::{level::Level, lift::Lift, 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, @@ -202,6 +202,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 +232,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 +428,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)); + } + } } } } From e0e4897a488f8e79feb6abbfbe2a29dd2f93ccd3 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 17:38:32 +0800 Subject: [PATCH 14/19] Fix style Signed-off-by: Michael X. Grey --- rmf_site_editor/src/save.rs | 2 +- rmf_site_editor/src/shapes.rs | 8 +- .../src/site/drawing_editor/alignment.rs | 36 +++- rmf_site_editor/src/site/fiducial.rs | 28 ++- rmf_site_editor/src/site/floor.rs | 34 ++-- rmf_site_editor/src/site/group.rs | 9 +- rmf_site_editor/src/site/load.rs | 96 +++++++---- rmf_site_editor/src/site/save.rs | 41 +++-- rmf_site_editor/src/site/texture.rs | 13 +- rmf_site_editor/src/site/wall.rs | 37 ++-- rmf_site_editor/src/widgets/console.rs | 10 +- .../src/widgets/inspector/inspect_fiducial.rs | 8 +- .../src/widgets/inspector/inspect_group.rs | 41 ++--- .../src/widgets/inspector/inspect_texture.rs | 145 +++++++++------- rmf_site_editor/src/widgets/inspector/mod.rs | 23 ++- rmf_site_editor/src/widgets/mod.rs | 8 +- rmf_site_editor/src/widgets/view_groups.rs | 162 ++++++++++-------- rmf_site_editor/src/widgets/view_layers.rs | 4 +- rmf_site_format/src/alignment.rs | 44 +++-- rmf_site_format/src/constraint.rs | 5 +- rmf_site_format/src/edge.rs | 5 +- rmf_site_format/src/fiducial.rs | 5 +- rmf_site_format/src/floor.rs | 5 +- rmf_site_format/src/legacy/building_map.rs | 39 ++--- rmf_site_format/src/legacy/floor.rs | 58 ++++--- rmf_site_format/src/legacy/rbmf.rs | 6 +- rmf_site_format/src/legacy/wall.rs | 10 +- rmf_site_format/src/lift.rs | 23 +-- rmf_site_format/src/location.rs | 5 +- rmf_site_format/src/measurement.rs | 5 +- rmf_site_format/src/misc.rs | 5 +- rmf_site_format/src/nav_graph.rs | 12 +- rmf_site_format/src/path.rs | 8 +- rmf_site_format/src/point.rs | 5 +- rmf_site_format/src/texture.rs | 2 +- 35 files changed, 517 insertions(+), 430 deletions(-) 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/shapes.rs b/rmf_site_editor/src/shapes.rs index 828de29c..b92dd013 100644 --- a/rmf_site_editor/src/shapes.rs +++ b/rmf_site_editor/src/shapes.rs @@ -1279,7 +1279,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 +1310,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 e5926ad8..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) { diff --git a/rmf_site_editor/src/site/floor.rs b/rmf_site_editor/src/site/floor.rs index 6ded6064..1c09d367 100644 --- a/rmf_site_editor/src/site/floor.rs +++ b/rmf_site_editor/src/site/floor.rs @@ -308,29 +308,17 @@ pub fn update_floors_for_moved_anchors( } pub fn update_floors( - floors: Query< - (&FloorSegments, &Path, &Affiliation), - With, - >, + floors: Query<(&FloorSegments, &Path, &Affiliation), With>, changed_floors: Query< Entity, ( With, - Or<( - Changed>, - Changed>, - )>, - ) + Or<(Changed>, Changed>)>, + ), >, changed_texture_sources: Query< &Members, - ( - With, - Or<( - Changed>, - Changed, - )>, - ) + (With, Or<(Changed>, Changed)>), >, mut meshes: ResMut>, mut materials: ResMut>, @@ -339,14 +327,14 @@ pub fn update_floors( anchors: AnchorParams, textures: Query<(Option<&Handle>, &Texture)>, ) { - for e in changed_floors.iter() - .chain( - changed_texture_sources + 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 }; + .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) { diff --git a/rmf_site_editor/src/site/group.rs b/rmf_site_editor/src/site/group.rs index 3946dc8a..b8197337 100644 --- a/rmf_site_editor/src/site/group.rs +++ b/rmf_site_editor/src/site/group.rs @@ -15,11 +15,11 @@ * */ -use rmf_site_format::{Affiliation, Group}; use bevy::{ ecs::system::{Command, EntityCommands}, prelude::*, }; +use rmf_site_format::{Affiliation, Group}; pub struct MergeGroups { pub from_group: Entity, @@ -76,8 +76,11 @@ struct ChangeMembership { impl Command for ChangeMembership { fn write(self, world: &mut World) { - let last = world.get_entity(self.member) - .map(|e| e.get::()).flatten().cloned(); + 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 diff --git a/rmf_site_editor/src/site/load.rs b/rmf_site_editor/src/site/load.rs index af95d313..6f7a5e38 100644 --- a/rmf_site_editor/src/site/load.rs +++ b/rmf_site_editor/src/site/load.rs @@ -19,7 +19,7 @@ use crate::{recency::RecencyRanking, site::*, Autoload, CurrentWorkspace, Worksp use bevy::{ecs::system::SystemParam, prelude::*, tasks::AsyncComputeTaskPool}; use futures_lite::future; use rmf_site_format::legacy::building_map::BuildingMap; -use std::{collections::HashMap, path::PathBuf, backtrace::Backtrace}; +use std::{backtrace::Backtrace, collections::HashMap, path::PathBuf}; use thiserror::Error as ThisError; #[cfg(not(target_arch = "wasm32"))] @@ -49,7 +49,11 @@ struct LoadSiteError { impl LoadSiteError { fn new(site: Entity, broken: u32) -> Self { - Self { site, broken, backtrace: Backtrace::force_capture() } + Self { + site, + broken, + backtrace: Backtrace::force_capture(), + } } } @@ -63,7 +67,10 @@ impl LoadResult for Result { } } -fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format::Site) -> Result { +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| { @@ -141,13 +148,21 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: } for (fiducial_id, fiducial) in &drawing.fiducials { drawing_parent - .spawn(fiducial.convert(&id_to_entity).for_site(site_id)?) + .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.convert(&id_to_entity).for_site(site_id)?) + .spawn( + measurement + .convert(&id_to_entity) + .for_site(site_id)?, + ) .insert(SiteID(*measurement_id)); consider_id(*measurement_id); } @@ -212,36 +227,40 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for (lift_id, lift_data) in &site_data.lifts { 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(()) - })?; + 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) - .insert(lift_data.properties.convert(&id_to_entity).for_site(site_id)?) + .insert( + lift_data + .properties + .convert(&id_to_entity) + .for_site(site_id)?, + ) .id(); id_to_entity.insert(*lift_id, lift); consider_id(*lift_id); @@ -311,7 +330,12 @@ fn generate_site_entities(commands: &mut Commands, site_data: &rmf_site_format:: for anchor in door.reference_anchors.array() { commands .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)?))); + .insert(Subordinate(Some( + *id_to_entity + .get(lift_id) + .ok_or(*lift_id) + .for_site(site_id)?, + ))); } } } @@ -557,7 +581,8 @@ 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) + 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).id(); @@ -566,7 +591,8 @@ fn generate_imported_nav_graphs( } for (location_id, location_data) in &from_site_data.navigation.guided.locations { - let location_data = location_data.convert(&id_to_entity) + 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).id(); diff --git a/rmf_site_editor/src/site/save.rs b/rmf_site_editor/src/site/save.rs index bdbc744c..b7895194 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]); } @@ -830,7 +838,9 @@ fn generate_fiducials( let mut fiducials = BTreeMap::new(); for child in children { - let Ok((point, affiliation, site_id)) = q_fiducials.get(*child) else { continue }; + let Ok((point, affiliation, site_id)) = q_fiducials.get(*child) else { + continue; + }; let anchor = q_anchor_ids .get(point.0) .map_err(|_| SiteGenerationError::BrokenAnchorReference(point.0))? @@ -876,7 +886,9 @@ fn generate_fiducial_groups( let mut fiducial_groups = BTreeMap::new(); for child in children { - let Ok((name, site_id)) = q_groups.get(*child) else { continue }; + let Ok((name, site_id)) = q_groups.get(*child) else { + continue; + }; fiducial_groups.insert(site_id.0, FiducialGroup::new(name.clone())); } @@ -900,12 +912,17 @@ fn generate_texture_groups( 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(), - }); + 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) diff --git a/rmf_site_editor/src/site/texture.rs b/rmf_site_editor/src/site/texture.rs index 5d4ebda3..5ec31052 100644 --- a/rmf_site_editor/src/site/texture.rs +++ b/rmf_site_editor/src/site/texture.rs @@ -15,8 +15,8 @@ * */ -use rmf_site_format::{Texture, Affiliation, Category}; use bevy::prelude::*; +use rmf_site_format::{Affiliation, Category, Texture}; pub fn fetch_image_for_texture( mut commands: Commands, @@ -44,9 +44,10 @@ 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())) + 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 d0ecace9..11947b2c 100644 --- a/rmf_site_editor/src/site/wall.rs +++ b/rmf_site_editor/src/site/wall.rs @@ -96,7 +96,15 @@ pub fn add_wall_visual( } pub fn update_walls_for_moved_anchors( - mut walls: Query<(Entity, &Edge, &Affiliation, &mut Handle), With>, + mut walls: Query< + ( + Entity, + &Edge, + &Affiliation, + &mut Handle, + ), + With, + >, anchors: AnchorParams, textures: Query<(Option<&Handle>, &Texture)>, changed_anchors: Query< @@ -132,35 +140,26 @@ pub fn update_walls( Entity, ( With, - Or<( - Changed>, - Changed>, - )>, + Or<(Changed>, Changed>)>, ), >, changed_texture_sources: Query< &Members, - ( - With, - Or<( - Changed>, - Changed, - )>, - ) + (With, Or<(Changed>, Changed)>), >, mut meshes: ResMut>, mut materials: ResMut>, anchors: AnchorParams, textures: Query<(Option<&Handle>, &Texture)>, ) { - for e in changed_walls.iter() - .chain( - changed_texture_sources + 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 }; + .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) { diff --git a/rmf_site_editor/src/widgets/console.rs b/rmf_site_editor/src/widgets/console.rs index fc469a80..140da674 100644 --- a/rmf_site_editor/src/widgets/console.rs +++ b/rmf_site_editor/src/widgets/console.rs @@ -158,12 +158,10 @@ fn print_log(ui: &mut egui::Ui, element: &LogHistoryElement) { } if truncated { - ui - .label(" [...]") - .on_hover_text( - "Some of the message is hidden. Click on it to copy the \ - full text to your clipboard." - ); + 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/inspector/inspect_fiducial.rs b/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs index f3baa63c..0a6efe41 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_fiducial.rs @@ -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"); diff --git a/rmf_site_editor/src/widgets/inspector/inspect_group.rs b/rmf_site_editor/src/widgets/inspector/inspect_group.rs index 191ac6a7..8917260f 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_group.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_group.rs @@ -16,22 +16,15 @@ */ use crate::{ - site::{ - Texture, Members, Affiliation, Change, NameInSite, DefaultFile, SiteID, - Group, - }, + site::{Affiliation, Change, DefaultFile, Group, Members, NameInSite, SiteID, Texture}, widgets::{ - AppEvents, inspector::{InspectTexture, SelectionWidget}, + AppEvents, }, Icons, }; -use bevy::{ - prelude::*, - ecs::system::SystemParam, -}; -use bevy_egui::egui::{Ui, RichText, CollapsingHeader}; - +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{CollapsingHeader, RichText, Ui}; #[derive(SystemParam)] pub struct InspectGroupParams<'w, 's> { @@ -59,15 +52,19 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectGroup<'a, 'w1, 'w2, 's1, 's2> { params: &'a InspectGroupParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>, ) -> Self { - Self { group, selection, default_file, params, events } + 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) { + if let Some(new_texture) = InspectTexture::new(texture, self.default_file).show(ui) { self.events .change_more .texture @@ -78,16 +75,10 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectGroup<'a, 'w1, 'w2, 's1, 's2> { 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); + 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 30ec4341..1cc11bc6 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_texture.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_texture.rs @@ -16,19 +16,16 @@ */ use crate::{ - site::{DefaultFile, Change, Category}, inspector::{InspectAssetSource, InspectValue, SearchResult}, + site::{Category, Change, DefaultFile}, widgets::egui::RichText, - WorkspaceMarker, Icons, AppEvents, + AppEvents, Icons, WorkspaceMarker, }; -use bevy::{ - prelude::*, - ecs::system::SystemParam, -}; -use bevy_egui::egui::{Grid, Ui, ImageButton, ComboBox}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{ComboBox, Grid, ImageButton, Ui}; use rmf_site_format::{ - RecallAssetSource, Texture, NameInSite, Group, Affiliation, FloorMarker, - WallMarker, TextureGroup, + Affiliation, FloorMarker, Group, NameInSite, RecallAssetSource, Texture, TextureGroup, + WallMarker, }; #[derive(Resource, Default)] @@ -62,11 +59,18 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { params: &'a InspectTextureAffiliationParams<'w1, 's1>, events: &'a mut AppEvents<'w2, 's2>, ) -> Self { - Self { entity, default_file, params, events } + 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 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) { @@ -86,7 +90,9 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { 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 }; + let Ok((name, _)) = self.params.texture_groups.get(*child) else { + continue; + }; if name.0.contains(&*search) { any_partial_matches = true; } @@ -111,10 +117,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { ui.label("Texture"); ui.horizontal(|ui| { if any_partial_matches { - if ui.add(ImageButton::new( - self.params.icons.search.egui(), - [18., 18.], - )) + 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() { @@ -127,10 +134,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { match result { SearchResult::Empty => { - if ui.add(ImageButton::new( - self.params.icons.hidden.egui(), - [18., 18.], - )) + 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() { @@ -138,10 +146,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } } SearchResult::Current => { - if ui.add(ImageButton::new( - self.params.icons.selected.egui(), - [18., 18.], - )) + 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() { @@ -149,22 +158,20 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } } 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" - } - ) + 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() { + let new_texture = if let Some((_, t)) = affiliation + .0 + .map(|a| self.params.texture_groups.get(a).ok()) + .flatten() + { t.clone() } else { Texture::default() @@ -180,17 +187,18 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }) .set_parent(site) .id(); - self.events - .change_more - .affiliation - .send(Change::new(Affiliation(Some(new_texture_group)), self.entity)); + 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.], - )) + if ui + .add(ImageButton::new( + self.params.icons.confirm.egui(), + [18., 18.], + )) .on_hover_text("Select this texture") .clicked() { @@ -201,10 +209,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { } } SearchResult::Conflict(text) => { - if ui.add(ImageButton::new( - self.params.icons.reject.egui(), - [18., 18.], - )) + if ui + .add(ImageButton::new( + self.params.icons.reject.egui(), + [18., 18.], + )) .on_hover_text(text) .clicked() { @@ -218,16 +227,20 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectTextureAffiliation<'a, 'w1, 'w2, 's1, 's2> { }); 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))) - ) + self.params + .texture_groups + .get(a) + .ok() + .map(|(n, t)| (n.0.as_str(), Some((a, t)))) } else { None - }.unwrap_or(("", 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.])) + if ui + .add(ImageButton::new(self.params.icons.exit.egui(), [18., 18.])) .on_hover_text(format!("Remove this texture from the {}", category.label())) .clicked() { @@ -268,23 +281,23 @@ pub struct InspectTexture<'a> { } impl<'a> InspectTexture<'a> { - pub fn new( - texture: &'a Texture, - default_file: Option<&'a DefaultFile>, - ) -> Self { - Self { texture, default_file } + 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(); // 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; } diff --git a/rmf_site_editor/src/widgets/inspector/mod.rs b/rmf_site_editor/src/widgets/inspector/mod.rs index 43f23b1c..fb685518 100644 --- a/rmf_site_editor/src/widgets/inspector/mod.rs +++ b/rmf_site_editor/src/widgets/inspector/mod.rs @@ -99,13 +99,13 @@ use crate::{ interaction::{Selection, SpawnPreview}, site::{ AlignSiteDrawings, BeginEditDrawing, Category, Change, DefaultFile, DrawingMarker, - EdgeLabels, LayerVisibility, Original, SiteID, Members, + EdgeLabels, LayerVisibility, Members, Original, SiteID, }, widgets::AppEvents, AppState, }; use bevy::{ecs::system::SystemParam, prelude::*}; -use bevy_egui::egui::{Button, RichText, Ui, CollapsingHeader}; +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 @@ -409,7 +409,8 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { default_file, &self.params.texture, self.events, - ).show(ui); + ) + .show(ui); if let Ok((motion, recall)) = self.params.component.motions.get(selection) { ui.label(RichText::new("Forward Motion").size(18.0)); @@ -584,7 +585,11 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { 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) + let name = self + .params + .component + .names + .get(*group) .map(|n| &n.0) .unwrap_or(&empty); @@ -595,8 +600,9 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { selection, default_file, &self.params.groups, - self.events - ).show(ui); + self.events, + ) + .show(ui); } if self.params.groups.is_group.contains(selection) { @@ -605,8 +611,9 @@ impl<'a, 'w1, 'w2, 's1, 's2> InspectorWidget<'a, 'w1, 'w2, 's1, 's2> { selection, default_file, &self.params.groups, - self.events - ).show(ui); + self.events, + ) + .show(ui); } } else { ui.label("Nothing selected"); diff --git a/rmf_site_editor/src/widgets/mod.rs b/rmf_site_editor/src/widgets/mod.rs index d6b2da37..cfee8caf 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, MergeGroups, + GlobalDrawingVisibility, GlobalFloorVisibility, LayerVisibility, MergeGroups, + PhysicalLightToggle, SaveNavGraphs, SiteState, Texture, ToggleLiftDoorAvailability, }, AppState, CreateNewWorkspace, CurrentWorkspace, LoadWorkspace, SaveWorkspace, }; @@ -69,9 +69,7 @@ pub mod icons; pub use icons::*; pub mod inspector; -use inspector::{ - InspectorParams, InspectorWidget, SearchForFiducial, SearchForTexture, -}; +use inspector::{InspectorParams, InspectorWidget, SearchForFiducial, SearchForTexture}; pub mod move_layer; pub use move_layer::*; diff --git a/rmf_site_editor/src/widgets/view_groups.rs b/rmf_site_editor/src/widgets/view_groups.rs index a0da626a..14797430 100644 --- a/rmf_site_editor/src/widgets/view_groups.rs +++ b/rmf_site_editor/src/widgets/view_groups.rs @@ -16,21 +16,12 @@ */ use crate::{ - site::{ - NameInSite, SiteID, Members, Texture, FiducialMarker, MergeGroups, - Change, - }, - widgets::{ - AppEvents, - inspector::SelectionWidget, - }, + site::{Change, FiducialMarker, Members, MergeGroups, NameInSite, SiteID, Texture}, + widgets::{inspector::SelectionWidget, AppEvents}, Icons, }; -use bevy::{ - prelude::*, - ecs::system::SystemParam, -}; -use bevy_egui::egui::{Ui, CollapsingHeader, ImageButton, Button}; +use bevy::{ecs::system::SystemParam, prelude::*}; +use bevy_egui::egui::{Button, CollapsingHeader, ImageButton, Ui}; #[derive(Default, Clone, Copy)] pub enum GroupViewMode { @@ -70,20 +61,21 @@ pub struct ViewGroups<'a, 'w1, 's1, '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 { + 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 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 }; + let Ok(children) = self.params.children.get(site) else { + return; + }; CollapsingHeader::new("Textures").show(ui, |ui| { Self::show_groups( children, @@ -101,59 +93,80 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { &mut modes.fiducials, &self.params.icons, self.events, - ui + ui, ); }); } fn show_groups<'b, T: Component>( - children: impl IntoIterator, + 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; - } + 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; } - 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; - } + 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::Delete => { - if ui.add(Button::image_and_text(icons.exit.egui(), [18., 18.], "cancel")) - .on_hover_text("Cancel the delete") - .clicked() - { - *mode = GroupViewMode::View; - } + } + 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())) + 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() { @@ -161,7 +174,12 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { 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)) + 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() { @@ -170,29 +188,38 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { } GroupViewMode::MergeFrom(merge_from) => { if merge_from == *child { - if ui.add(Button::image_and_text(icons.exit.egui(), [18., 18.], &text)) + 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)) + 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, - } - ); + 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)) + if ui + .add(Button::image_and_text( + icons.trash.egui(), + [18., 18.], + &text, + )) .on_hover_text("Delete this group") .clicked() { @@ -204,9 +231,10 @@ impl<'a, 'w1, 's1, 'w2, 's2> ViewGroups<'a, 'w1, 's1, 'w2, 's2> { 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) - ); + 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/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/constraint.rs b/rmf_site_format/src/constraint.rs index 8d0d99e6..a52e8dcf 100644 --- a/rmf_site_format/src/constraint.rs +++ b/rmf_site_format/src/constraint.rs @@ -35,10 +35,7 @@ pub struct Constraint { pub struct ConstraintMarker; impl Constraint { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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/edge.rs b/rmf_site_format/src/edge.rs index 4dc8f2ba..01b9d12d 100644 --- a/rmf_site_format/src/edge.rs +++ b/rmf_site_format/src/edge.rs @@ -108,10 +108,7 @@ impl From<[T; 2]> for Edge { } impl Edge { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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 6bb01cc6..2468dbe7 100644 --- a/rmf_site_format/src/fiducial.rs +++ b/rmf_site_format/src/fiducial.rs @@ -61,10 +61,7 @@ impl FiducialGroup { pub struct FiducialMarker; impl Fiducial { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Fiducial { anchor: self.anchor.convert(id_map)?, affiliation: self.affiliation.convert(id_map)?, diff --git a/rmf_site_format/src/floor.rs b/rmf_site_format/src/floor.rs index 5a0d8c93..8ae08baa 100644 --- a/rmf_site_format/src/floor.rs +++ b/rmf_site_format/src/floor.rs @@ -41,10 +41,7 @@ pub struct Floor { pub struct FloorMarker; impl Floor { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Floor { anchors: self.anchors.convert(id_map)?, texture: self.texture.convert(id_map)?, diff --git a/rmf_site_format/src/legacy/building_map.rs b/rmf_site_format/src/legacy/building_map.rs index f9ff4161..4a4bb273 100644 --- a/rmf_site_format/src/legacy/building_map.rs +++ b/rmf_site_format/src/legacy/building_map.rs @@ -1,6 +1,5 @@ use super::{ - level::Level, lift::Lift, floor::FloorParameters, PortingError, Result, - wall::WallProperties, + floor::FloorParameters, level::Level, lift::Lift, wall::WallProperties, PortingError, Result, }; use crate::{ alignment::align_legacy_building, Affiliation, Anchor, Angle, AssetSource, AssociatedGraphs, @@ -9,7 +8,7 @@ use crate::{ 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, + Texture as SiteTexture, TextureGroup, DEFAULT_NAV_GRAPH_COLORS, }; use glam::{DAffine2, DMat3, DQuat, DVec2, DVec3, EulerRot}; use serde::{Deserialize, Serialize}; @@ -612,24 +611,22 @@ impl BuildingMap { 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(), - } - ) - } - ) + .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 { diff --git a/rmf_site_format/src/legacy/floor.rs b/rmf_site_format/src/legacy/floor.rs index de2d9e01..72b1c17d 100644 --- a/rmf_site_format/src/legacy/floor.rs +++ b/rmf_site_format/src/legacy/floor.rs @@ -1,11 +1,11 @@ use super::{rbmf::*, PortingError, Result}; use crate::{ - Angle, AssetSource, Floor as SiteFloor, FloorMarker, Path, - PreferredSemiTransparency, Texture, Affiliation, + Affiliation, Angle, AssetSource, Floor as SiteFloor, FloorMarker, Path, + PreferredSemiTransparency, Texture, }; use serde::{Deserialize, Serialize}; use std::{ - collections::{HashMap, BTreeMap}, + collections::{BTreeMap, HashMap}, ops::RangeFrom, }; @@ -39,32 +39,34 @@ 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 = *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 - }); + let texture_site_id = site_id.next().unwrap(); + textures.insert(texture_site_id, texture); + texture_site_id + }); Ok(SiteFloor { anchors: Path(anchors), diff --git a/rmf_site_format/src/legacy/rbmf.rs b/rmf_site_format/src/legacy/rbmf.rs index a6b9be68..8032e689 100644 --- a/rmf_site_format/src/legacy/rbmf.rs +++ b/rmf_site_format/src/legacy/rbmf.rs @@ -1,8 +1,8 @@ // RBMF stands for "RMF Building Map Format" use std::{ - ops::{Deref, DerefMut}, hash::Hash, + ops::{Deref, DerefMut}, }; use serde::{Deserialize, Serialize}; @@ -34,7 +34,7 @@ impl PartialEq for RbmfString { } } -impl Eq for RbmfString { } +impl Eq for RbmfString {} impl From for String { fn from(s: RbmfString) -> Self { @@ -128,7 +128,7 @@ impl Hash for RbmfFloat { } } -impl Eq for RbmfFloat { } +impl Eq for RbmfFloat {} impl PartialOrd for RbmfFloat { fn partial_cmp(&self, other: &Self) -> Option { diff --git a/rmf_site_format/src/legacy/wall.rs b/rmf_site_format/src/legacy/wall.rs index 476d2b9e..aaab3f51 100644 --- a/rmf_site_format/src/legacy/wall.rs +++ b/rmf_site_format/src/legacy/wall.rs @@ -1,10 +1,8 @@ use super::{rbmf::*, PortingError, Result}; -use crate::{ - AssetSource, Texture, Wall as SiteWall, Affiliation, DEFAULT_LEVEL_HEIGHT, -}; +use crate::{Affiliation, AssetSource, Texture, Wall as SiteWall, DEFAULT_LEVEL_HEIGHT}; use serde::{Deserialize, Serialize}; use std::{ - collections::{HashMap, BTreeMap}, + collections::{BTreeMap, HashMap}, ops::RangeFrom, }; @@ -78,8 +76,8 @@ impl Wall { + ".png", ), 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), + 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), alpha: Some(self.2.alpha.1 as f32), } }; diff --git a/rmf_site_format/src/lift.rs b/rmf_site_format/src/lift.rs index d07a71c3..ed62b161 100644 --- a/rmf_site_format/src/lift.rs +++ b/rmf_site_format/src/lift.rs @@ -60,10 +60,7 @@ pub struct LiftCabinDoor { } impl LiftCabinDoor { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(LiftCabinDoor { kind: self.kind.clone(), reference_anchors: self.reference_anchors.convert(id_map)?, @@ -85,11 +82,9 @@ impl Default for LevelVisits { } impl LevelVisits { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { - let set: Result, T> = self.0 + 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(); @@ -178,10 +173,7 @@ impl LiftCabin { None } - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { let result = match self { LiftCabin::Rect(cabin) => LiftCabin::Rect(cabin.convert(id_map)?), }; @@ -486,10 +478,7 @@ pub struct LiftCabinDoorPlacement { } impl LiftProperties { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(LiftProperties { name: self.name.clone(), reference_anchors: self.reference_anchors.convert(id_map)?, diff --git a/rmf_site_format/src/location.rs b/rmf_site_format/src/location.rs index 73b8e7bb..9d3ea516 100644 --- a/rmf_site_format/src/location.rs +++ b/rmf_site_format/src/location.rs @@ -85,10 +85,7 @@ impl Default for LocationTags { } impl Location { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Location { anchor: self.anchor.convert(id_map)?, tags: self.tags.clone(), diff --git a/rmf_site_format/src/measurement.rs b/rmf_site_format/src/measurement.rs index 4a100644..16efaa21 100644 --- a/rmf_site_format/src/measurement.rs +++ b/rmf_site_format/src/measurement.rs @@ -61,10 +61,7 @@ impl Measurement { } impl Measurement { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { Ok(Measurement { anchors: self.anchors.convert(id_map)?, distance: self.distance, diff --git a/rmf_site_format/src/misc.rs b/rmf_site_format/src/misc.rs index be43ca75..a7fa86fc 100644 --- a/rmf_site_format/src/misc.rs +++ b/rmf_site_format/src/misc.rs @@ -477,10 +477,7 @@ impl Default for Affiliation { } impl Affiliation { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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 { diff --git a/rmf_site_format/src/nav_graph.rs b/rmf_site_format/src/nav_graph.rs index 61e0778c..d9fbbcc3 100644 --- a/rmf_site_format/src/nav_graph.rs +++ b/rmf_site_format/src/nav_graph.rs @@ -113,16 +113,11 @@ impl Default for AssociatedGraphs { } impl AssociatedGraphs { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { let result = match self { Self::All => AssociatedGraphs::All, Self::Only(set) => AssociatedGraphs::Only(Self::convert_set(set, id_map)?), - Self::AllExcept(set) => { - AssociatedGraphs::AllExcept(Self::convert_set(set, id_map)?) - } + Self::AllExcept(set) => AssociatedGraphs::AllExcept(Self::convert_set(set, id_map)?), }; Ok(result) } @@ -131,8 +126,7 @@ impl AssociatedGraphs { set: &BTreeSet, id_map: &HashMap, ) -> Result, T> { - set - .iter() + set.iter() .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 586fe6ca..34fe2b1f 100644 --- a/rmf_site_format/src/path.rs +++ b/rmf_site_format/src/path.rs @@ -27,11 +27,9 @@ use std::collections::HashMap; pub struct Path(pub Vec); impl Path { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { - let path: Result, T> = self.0 + 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(); diff --git a/rmf_site_format/src/point.rs b/rmf_site_format/src/point.rs index 18a2fcd1..7dca6c74 100644 --- a/rmf_site_format/src/point.rs +++ b/rmf_site_format/src/point.rs @@ -33,10 +33,7 @@ impl From for Point { } impl Point { - pub fn convert( - &self, - id_map: &HashMap, - ) -> Result, T> { + 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/texture.rs b/rmf_site_format/src/texture.rs index cfc8b297..d0705b1b 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, Bundle}; +use bevy::prelude::{Bundle, Component}; use serde::{Deserialize, Serialize}; #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] From 2bebd9cfe53e8307a1c6a1e1197beb65ad4aa998 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Thu, 17 Aug 2023 17:52:50 +0800 Subject: [PATCH 15/19] Remove test code Signed-off-by: Michael X. Grey --- rmf_site_editor/src/widgets/menu_bar.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rmf_site_editor/src/widgets/menu_bar.rs b/rmf_site_editor/src/widgets/menu_bar.rs index 4e824139..8b5bd4c8 100644 --- a/rmf_site_editor/src/widgets/menu_bar.rs +++ b/rmf_site_editor/src/widgets/menu_bar.rs @@ -49,10 +49,6 @@ pub fn top_menu_bar( { file_events.save.send(SaveWorkspace::new().to_dialog()); } - ui.menu_button("Text expansion", |ui| { - ui.label("Oh look"); - ui.label("It worked"); - }); } if ui .add(Button::new("Open").shortcut_text("Ctrl+O")) From 120528af03d7ecb7227faa4aead0e373a46a1363 Mon Sep 17 00:00:00 2001 From: Arjo Chakravarty Date: Fri, 18 Aug 2023 15:47:30 +0800 Subject: [PATCH 16/19] Make Site Editor itself a `Plugin` (#162) Signed-off-by: Arjo Chakravarty --- rmf_site_editor/Cargo.toml | 4 + rmf_site_editor/examples/extending_menu.rs | 92 +++++++++++ rmf_site_editor/src/lib.rs | 128 ++++++++------- rmf_site_editor/src/widgets/menu_bar.rs | 174 ++++++++++++++++++++- rmf_site_editor/src/widgets/mod.rs | 48 ++++++ 5 files changed, 385 insertions(+), 61 deletions(-) create mode 100644 rmf_site_editor/examples/extending_menu.rs 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/lib.rs b/rmf_site_editor/src/lib.rs index 03f69d96..19bdf0ad 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; @@ -126,66 +126,74 @@ 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() - }) - .add_after::(SiteAssetIoPlugin), - ) - .add_system_set( - SystemSet::new() - .with_run_criteria(FixedTimestep::step(0.5)) - .with_system(check_browser_window_size), - ); - } + }) + .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() - }, - ..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/widgets/menu_bar.rs b/rmf_site_editor/src/widgets/menu_bar.rs index 8b5bd4c8..5d746582 100644 --- a/rmf_site_editor/src/widgets/menu_bar.rs +++ b/rmf_site_editor/src/widgets/menu_bar.rs @@ -17,15 +17,163 @@ use crate::{CreateNewWorkspace, FileEvents, LoadWorkspace, 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>, + menus: &Query<(&Menu, Entity)>, + menu_items: &Query<&MenuItem>, + extension_events: &mut EventWriter, ) { 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, + menus, + menu_items, + 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 menus.iter().filter(|(_, entity)| { + top_level_components.contains(*entity) && *entity != file_menu.get() + }) { + render_sub_menu( + ui, + &entity, + children, + menus, + menu_items, + extension_events, + false, + ); + } }); }); } diff --git a/rmf_site_editor/src/widgets/mod.rs b/rmf_site_editor/src/widgets/mod.rs index 2253370e..f1118ec4 100644 --- a/rmf_site_editor/src/widgets/mod.rs +++ b/rmf_site_editor/src/widgets/mod.rs @@ -286,6 +286,12 @@ fn site_ui_layout( nav_graphs: NavGraphParams, layers: LayersParams, mut events: AppEvents, + file_menu: Res, + children: Query<&Children>, + top_level_components: Query<(), Without>, + menus: Query<(&Menu, Entity)>, + menu_items: Query<&MenuItem>, + mut extension_events: EventWriter, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -350,6 +356,12 @@ fn site_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &menus, + &menu_items, + &mut extension_events, ); egui::TopBottomPanel::bottom("log_console") @@ -385,6 +397,12 @@ 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>, + menus: Query<(&Menu, Entity)>, + menu_items: Query<&MenuItem>, + mut extension_events: EventWriter, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -435,6 +453,12 @@ fn site_drawing_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &menus, + &menu_items, + &mut extension_events, ); let egui_context = egui_context.ctx_mut(); @@ -461,6 +485,12 @@ fn site_visualizer_ui_layout( inspector_params: InspectorParams, mut events: AppEvents, levels: LevelParams, + file_menu: Res, + top_level_components: Query<(), Without>, + children: Query<&Children>, + menus: Query<(&Menu, Entity)>, + menu_items: Query<&MenuItem>, + mut extension_events: EventWriter, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -512,6 +542,12 @@ fn site_visualizer_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &menus, + &menu_items, + &mut extension_events, ); let egui_context = egui_context.ctx_mut(); @@ -538,6 +574,12 @@ fn workcell_ui_layout( inspector_params: InspectorParams, create_params: CreateParams, mut events: AppEvents, + mut extension_events: EventWriter, + file_menu: Res, + top_level_components: Query<(), Without>, + children: Query<&Children>, + menus: Query<(&Menu, Entity)>, + menu_items: Query<&MenuItem>, ) { egui::SidePanel::right("right_panel") .resizable(true) @@ -575,6 +617,12 @@ fn workcell_ui_layout( &mut egui_context, &mut events.file_events, &mut events.visibility_parameters, + &file_menu, + &top_level_components, + &children, + &menus, + &menu_items, + &mut extension_events, ); let egui_context = egui_context.ctx_mut(); From 3360e0c471bdcab8cc1950782d47e25060d8f583 Mon Sep 17 00:00:00 2001 From: Luca Della Vedova Date: Fri, 18 Aug 2023 16:30:18 +0800 Subject: [PATCH 17/19] Workaround flatten issue and add roundtrip unit test to fix broken project save / load (#166) Signed-off-by: Luca Della Vedova --- rmf_site_format/Cargo.toml | 2 +- rmf_site_format/src/drawing.rs | 7 ++++++- rmf_site_format/src/site.rs | 20 +++++++++++++++++--- 3 files changed, 24 insertions(+), 5 deletions(-) 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/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/site.rs b/rmf_site_format/src/site.rs index 832cf080..e717aaac 100644 --- a/rmf_site_format/src/site.rs +++ b/rmf_site_format/src/site.rs @@ -98,17 +98,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 +119,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(); + } +} From 06926a9c735bcaf6f408335836259a14793788d8 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Fri, 18 Aug 2023 17:15:10 +0800 Subject: [PATCH 18/19] Avoid use of flatten attribute Signed-off-by: Michael X. Grey --- rmf_site_format/src/texture.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/rmf_site_format/src/texture.rs b/rmf_site_format/src/texture.rs index d0705b1b..82328b11 100644 --- a/rmf_site_format/src/texture.rs +++ b/rmf_site_format/src/texture.rs @@ -38,7 +38,9 @@ pub struct Texture { #[cfg_attr(feature = "bevy", derive(Bundle))] pub struct TextureGroup { pub name: NameInSite, - #[serde(flatten)] + // 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, From c5a881050384cfc589d205081574f9fd6ecfa574 Mon Sep 17 00:00:00 2001 From: "Michael X. Grey" Date: Fri, 18 Aug 2023 17:15:55 +0800 Subject: [PATCH 19/19] Fix style Signed-off-by: Michael X. Grey --- rmf_site_editor/src/lib.rs | 8 ++++---- rmf_site_editor/src/widgets/menu_bar.rs | 3 +-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/rmf_site_editor/src/lib.rs b/rmf_site_editor/src/lib.rs index dc9eb345..cbde239f 100644 --- a/rmf_site_editor/src/lib.rs +++ b/rmf_site_editor/src/lib.rs @@ -150,14 +150,14 @@ impl Plugin for SiteEditor { }, ..default() }) - .set(ImagePlugin { - default_sampler: SamplerDescriptor { + .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( diff --git a/rmf_site_editor/src/widgets/menu_bar.rs b/rmf_site_editor/src/widgets/menu_bar.rs index d47c1f70..8f2a2a4c 100644 --- a/rmf_site_editor/src/widgets/menu_bar.rs +++ b/rmf_site_editor/src/widgets/menu_bar.rs @@ -16,8 +16,7 @@ */ use crate::{ - CreateNewWorkspace, FileEvents, LoadWorkspace, SaveWorkspace, - VisibilityParameters, MenuParams, + CreateNewWorkspace, FileEvents, LoadWorkspace, MenuParams, SaveWorkspace, VisibilityParameters, }; use bevy::prelude::{