diff --git a/Cargo.lock b/Cargo.lock index f5c566d3..63821fe2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -764,6 +764,22 @@ dependencies = [ "smallvec", ] +[[package]] +name = "bevy_impulse" +version = "0.0.1" +source = "git+https://github.com/open-rmf/bevy_impulse?branch=main#ee7858aa7ff0a875980f48272fbb7fc889792cc2" +dependencies = [ + "anyhow", + "arrayvec", + "backtrace", + "bevy", + "crossbeam", + "futures", + "itertools 0.11.0", + "smallvec", + "thiserror", +] + [[package]] name = "bevy_infinite_grid" version = "0.10.0" @@ -1819,6 +1835,19 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" +[[package]] +name = "crossbeam" +version = "0.8.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1137cd7e7fc0fb5d3c5a8678be38ec56e819125d8d7907411fe24ccb943faca8" +dependencies = [ + "crossbeam-channel", + "crossbeam-deque", + "crossbeam-epoch", + "crossbeam-queue", + "crossbeam-utils", +] + [[package]] name = "crossbeam-channel" version = "0.5.12" @@ -1847,6 +1876,15 @@ dependencies = [ "crossbeam-utils", ] +[[package]] +name = "crossbeam-queue" +version = "0.3.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df0346b5d5e76ac2fe4e327c5fd1118d6be7c51dfb18f9b7922923f287471e35" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -2288,12 +2326,48 @@ dependencies = [ "percent-encoding", ] +[[package]] +name = "futures" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "645c6916888f6cb6350d2550b80fb63e734897a8498abe35cfb732b6487804b0" +dependencies = [ + "futures-channel", + "futures-core", + "futures-executor", + "futures-io", + "futures-sink", + "futures-task", + "futures-util", +] + +[[package]] +name = "futures-channel" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "eac8f7d7865dcb88bd4373ab671c8cf4508703796caa2b1985a9ca867b3fcb78" +dependencies = [ + "futures-core", + "futures-sink", +] + [[package]] name = "futures-core" version = "0.3.30" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfc6580bb841c5a68e9ef15c77ccc837b40a7504914d52e47b8b0e9bbda25a1d" +[[package]] +name = "futures-executor" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a576fc72ae164fca6b9db127eaa9a9dda0d61316034f33a0a0d4eda41f02b01d" +dependencies = [ + "futures-core", + "futures-task", + "futures-util", +] + [[package]] name = "futures-io" version = "0.3.30" @@ -2328,6 +2402,47 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "futures-macro" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "87750cf4b7a4c0625b1529e4c543c2182106e4dedc60a2a6455e00d212c489ac" +dependencies = [ + "proc-macro2", + "quote", + "syn 2.0.52", +] + +[[package]] +name = "futures-sink" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9fb8e00e87438d937621c1c6269e53f536c14d3fbd6a042bb24879e57d474fb5" + +[[package]] +name = "futures-task" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "38d84fa142264698cdce1a9f9172cf383a0c82de1bddcf3092901442c4097004" + +[[package]] +name = "futures-util" +version = "0.3.30" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d6401deb83407ab3da39eba7e33987a73c3df0c82b4bb5813ee871c19c41d48" +dependencies = [ + "futures-channel", + "futures-core", + "futures-io", + "futures-macro", + "futures-sink", + "futures-task", + "memchr", + "pin-project-lite", + "pin-utils", + "slab", +] + [[package]] name = "gdk-pixbuf-sys" version = "0.18.0" @@ -3897,6 +4012,12 @@ version = "0.2.13" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8afb450f006bf6385ca15ef45d71d2288452bc3683ce2e2cacc0d18e4be60b58" +[[package]] +name = "pin-utils" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b870d8c151b6f2fb93e84a13146138f05d02ed11c7e7c54f8826aaaf7c9f184" + [[package]] name = "piper" version = "0.2.1" @@ -4204,6 +4325,7 @@ dependencies = [ "bevy", "bevy_egui", "bevy_gltf_export", + "bevy_impulse", "bevy_infinite_grid", "bevy_mod_outline", "bevy_mod_raycast", diff --git a/rmf_site_editor/Cargo.toml b/rmf_site_editor/Cargo.toml index 371dd17a..446fb222 100644 --- a/rmf_site_editor/Cargo.toml +++ b/rmf_site_editor/Cargo.toml @@ -22,6 +22,7 @@ bevy_mod_outline = "0.6" # PR merged after 0.10 but not released yet, bump to 0.10.1 once merged bevy_infinite_grid = { git = "https://github.com/ForesightMiningSoftwareCorporation/bevy_infinite_grid", rev = "86018dd" } bevy_gltf_export = { git = "https://github.com/luca-della-vedova/bevy_gltf_export", branch = "luca/transform_api"} +bevy_impulse = { git = "https://github.com/open-rmf/bevy_impulse", branch = "main"} bevy_polyline = "0.8.1" bevy_stl = "0.12" bevy_obj = { version = "0.12.1", features = ["scene"] } diff --git a/rmf_site_editor/src/lib.rs b/rmf_site_editor/src/lib.rs index 5537b60a..d0d18cd4 100644 --- a/rmf_site_editor/src/lib.rs +++ b/rmf_site_editor/src/lib.rs @@ -230,6 +230,7 @@ impl Plugin for SiteEditor { OccupancyPlugin, WorkspacePlugin, IssuePlugin, + bevy_impulse::ImpulsePlugin::default(), )); if self.headless_export.is_none() { diff --git a/rmf_site_editor/src/workspace.rs b/rmf_site_editor/src/workspace.rs index 57cae9cb..27175bfa 100644 --- a/rmf_site_editor/src/workspace.rs +++ b/rmf_site_editor/src/workspace.rs @@ -16,6 +16,7 @@ */ use bevy::{prelude::*, tasks::AsyncComputeTaskPool}; +use bevy_impulse::*; use rfd::AsyncFileDialog; use std::path::PathBuf; @@ -99,21 +100,6 @@ pub struct CurrentWorkspace { pub struct LoadWorkspaceFile(pub Option, pub WorkspaceData); -/// Using channels instead of events to allow usage in wasm since, unlike event writers, they can -/// be cloned and moved into async functions therefore don't have lifetime issues -#[derive(Debug, Resource)] -pub struct LoadWorkspaceChannels { - pub sender: Sender, - pub receiver: Receiver, -} - -impl Default for LoadWorkspaceChannels { - fn default() -> Self { - let (sender, receiver) = crossbeam_channel::unbounded(); - Self { sender, receiver } - } -} - #[derive(Event)] pub struct SaveWorkspace { /// If specified workspace will be saved to requested file, otherwise the default file @@ -219,14 +205,13 @@ impl Plugin for WorkspacePlugin { .init_resource::() .init_resource::() .init_resource::() - .init_resource::() + .init_resource::() .add_systems( Update, ( dispatch_new_workspace_events, - workspace_file_load_complete, sync_workspace_visibility, - dispatch_load_workspace_events, + dispatch_load_workspace_workflows, workspace_file_save_complete, ), ); @@ -264,208 +249,267 @@ pub fn dispatch_new_workspace_events( } } -pub fn dispatch_load_workspace_events( - load_channels: Res, - mut load_workspace: EventReader, -) { - if let Some(cmd) = load_workspace.read().last() { - match cmd { - LoadWorkspace::Dialog => { - let sender = load_channels.sender.clone(); - AsyncComputeTaskPool::get() - .spawn(async move { - if let Some(file) = AsyncFileDialog::new().pick_file().await { - let data = file.read().await; - #[cfg(not(target_arch = "wasm32"))] - let file = file.path().to_path_buf(); - #[cfg(target_arch = "wasm32")] - let file = PathBuf::from(file.file_name()); - if let Some(data) = WorkspaceData::new(&file, data) { - sender - .send(LoadWorkspaceFile(Some(file), data)) - .expect("Failed sending file event"); - } - } - }) - .detach(); - } - LoadWorkspace::BlankFromDialog => { - let sender = load_channels.sender.clone(); - #[cfg(not(target_arch = "wasm32"))] - { - AsyncComputeTaskPool::get() - .spawn(async move { - if let Some(file) = AsyncFileDialog::new().save_file().await { - let file = file.path().to_path_buf(); - let name = file - .file_stem() - .map(|s| s.to_str().map(|s| s.to_owned())) - .flatten() - .unwrap_or_else(|| "blank".to_owned()); - let data = WorkspaceData::LoadSite(LoadSite::blank_L1( - name, - Some(file.clone()), - )); - let _ = sender.send(LoadWorkspaceFile(Some(file), data)); - } - }) - .detach(); - } - #[cfg(target_arch = "wasm32")] - { - let data = - WorkspaceData::LoadSite(LoadSite::blank_L1("blank".to_owned(), None)); - sender.send(LoadWorkspaceFile(None, data)); - } - } - LoadWorkspace::Path(path) => { - if let Ok(data) = std::fs::read(&path) { - if let Some(data) = WorkspaceData::new(path, data) { - load_channels - .sender - .send(LoadWorkspaceFile(Some(path.clone()), data)) - .expect("Failed sending load event"); - } - } else { - warn!("Unable to read file [{path:?}] so it cannot be loaded"); - } - } - LoadWorkspace::Data(data) => { - load_channels - .sender - .send(LoadWorkspaceFile(None, data.clone())) - .expect("Failed sending load event"); - } - } - } -} - -/// Handles the file opening events -fn workspace_file_load_complete( +/// Service that takes workspace data and loads a site / workcell, as well as transition state. +pub fn process_load_workspace_files( + In(BlockingService { request, .. }): BlockingServiceInput>, mut app_state: ResMut>, mut interaction_state: ResMut>, mut load_site: EventWriter, mut load_workcell: EventWriter, - load_channels: Res, ) { - if let Ok(result) = load_channels.receiver.try_recv() { - let LoadWorkspaceFile(default_file, data) = result; - match data { - WorkspaceData::LegacyBuilding(data) => { - info!("Opening legacy building map file"); - match BuildingMap::from_bytes(&data) { - Ok(building) => { - match building.to_site() { - Ok(site) => { - // Switch state - app_state.set(AppState::SiteEditor); - load_site.send(LoadSite { - site, - focus: true, - default_file, - }); - interaction_state.set(InteractionState::Enable); - } - Err(err) => { - error!("Failed converting to site {:?}", err); - } + let Some(LoadWorkspaceFile(default_file, data)) = request else { + return; + }; + match data { + WorkspaceData::LegacyBuilding(data) => { + info!("Opening legacy building map file"); + match BuildingMap::from_bytes(&data) { + Ok(building) => { + match building.to_site() { + Ok(site) => { + // Switch state + app_state.set(AppState::SiteEditor); + load_site.send(LoadSite { + site, + focus: true, + default_file, + }); + interaction_state.set(InteractionState::Enable); + } + Err(err) => { + error!("Failed converting to site {:?}", err); } - } - Err(err) => { - error!("Failed loading legacy building {:?}", err); } } + Err(err) => { + error!("Failed loading legacy building {:?}", err); + } } - WorkspaceData::RonSite(data) => { - info!("Opening site file"); - match Site::from_bytes_ron(&data) { - Ok(site) => { - // Switch state - app_state.set(AppState::SiteEditor); - load_site.send(LoadSite { - site, - focus: true, - default_file, - }); - interaction_state.set(InteractionState::Enable); - } - Err(err) => { - error!("Failed loading site {:?}", err); - } + } + WorkspaceData::RonSite(data) => { + info!("Opening site file"); + match Site::from_bytes_ron(&data) { + Ok(site) => { + // Switch state + app_state.set(AppState::SiteEditor); + load_site.send(LoadSite { + site, + focus: true, + default_file, + }); + interaction_state.set(InteractionState::Enable); + } + Err(err) => { + error!("Failed loading site {:?}", err); } } - WorkspaceData::JsonSite(data) => { - info!("Opening site file"); - match Site::from_bytes_json(&data) { - Ok(site) => { - // Switch state - app_state.set(AppState::SiteEditor); - load_site.send(LoadSite { - site, - focus: true, - default_file, - }); - interaction_state.set(InteractionState::Enable); - } - Err(err) => { - error!("Failed loading site {:?}", err); - } + } + WorkspaceData::JsonSite(data) => { + info!("Opening site file"); + match Site::from_bytes_json(&data) { + Ok(site) => { + // Switch state + app_state.set(AppState::SiteEditor); + load_site.send(LoadSite { + site, + focus: true, + default_file, + }); + interaction_state.set(InteractionState::Enable); + } + Err(err) => { + error!("Failed loading site {:?}", err); } } - WorkspaceData::Workcell(data) => { - info!("Opening workcell file"); - match Workcell::from_bytes(&data) { - Ok(workcell) => { - // Switch state - app_state.set(AppState::WorkcellEditor); - load_workcell.send(LoadWorkcell { - workcell, - focus: true, - default_file, - }); - interaction_state.set(InteractionState::Enable); - } - Err(err) => { - error!("Failed loading workcell {:?}", err); + } + WorkspaceData::Workcell(data) => { + info!("Opening workcell file"); + match Workcell::from_bytes(&data) { + Ok(workcell) => { + // Switch state + app_state.set(AppState::WorkcellEditor); + load_workcell.send(LoadWorkcell { + workcell, + focus: true, + default_file, + }); + interaction_state.set(InteractionState::Enable); + } + Err(err) => { + error!("Failed loading workcell {:?}", err); + } + } + } + WorkspaceData::WorkcellUrdf(data) => { + info!("Importing urdf workcell"); + let Ok(utf) = std::str::from_utf8(&data) else { + error!("Failed converting urdf bytes to string"); + return; + }; + match urdf_rs::read_from_string(utf) { + Ok(urdf) => { + // TODO(luca) make this function return a result and this a match statement + match Workcell::from_urdf(&urdf) { + Ok(workcell) => { + // Switch state + app_state.set(AppState::WorkcellEditor); + load_workcell.send(LoadWorkcell { + workcell, + focus: true, + default_file, + }); + interaction_state.set(InteractionState::Enable); + } + Err(err) => { + error!("Failed converting urdf to workcell {:?}", err); + } } } + Err(err) => { + error!("Failed loading urdf workcell {:?}", err); + } } - WorkspaceData::WorkcellUrdf(data) => { - info!("Importing urdf workcell"); - let Ok(utf) = std::str::from_utf8(&data) else { - error!("Failed converting urdf bytes to string"); - return; - }; - match urdf_rs::read_from_string(utf) { - Ok(urdf) => { - // TODO(luca) make this function return a result and this a match statement - match Workcell::from_urdf(&urdf) { - Ok(workcell) => { - // Switch state - app_state.set(AppState::WorkcellEditor); - load_workcell.send(LoadWorkcell { - workcell, - focus: true, - default_file, - }); - interaction_state.set(InteractionState::Enable); - } - Err(err) => { - error!("Failed converting urdf to workcell {:?}", err); - } + } + WorkspaceData::LoadSite(site) => { + app_state.set(AppState::SiteEditor); + load_site.send(site); + interaction_state.set(InteractionState::Enable); + } + } +} + +#[derive(Resource)] +/// Services that deal with workspace loading +pub struct WorkspaceLoadingServices { + /// Service that spawns an open file dialog and loads a site accordingly. + pub load_workspace_from_dialog: Service<(), (), ()>, + /// Service that spawns a save file dialog then creates a site with an empty level. + pub create_empty_workspace_from_dialog: Service<(), (), ()>, + /// Loads the workspace at the requested path + pub load_workspace_from_path: Service, + /// Loads the workspace from the requested data + pub load_workspace_from_data: Service, +} + +impl FromWorld for WorkspaceLoadingServices { + fn from_world(world: &mut World) -> Self { + let process_load_files = world.spawn_service(process_load_workspace_files); + // Spawn all the services + let load_workspace_from_dialog = world.spawn_workflow(|scope, builder| { + scope + .input + .chain(builder) + .map_async(|_| async move { + if let Some(file) = AsyncFileDialog::new().pick_file().await { + let data = file.read().await; + #[cfg(not(target_arch = "wasm32"))] + let file = file.path().to_path_buf(); + #[cfg(target_arch = "wasm32")] + let file = PathBuf::from(file.file_name()); + let data = WorkspaceData::new(&file, data)?; + return Some(LoadWorkspaceFile(Some(file), data)); + } + None + }) + .then(process_load_files) + .connect(scope.terminate) + }); + + let create_empty_workspace_from_dialog = world.spawn_workflow(|scope, builder| { + scope + .input + .chain(builder) + .map_async(|_| async move { + #[cfg(not(target_arch = "wasm32"))] + { + if let Some(file) = AsyncFileDialog::new().save_file().await { + let file = file.path().to_path_buf(); + let name = file + .file_stem() + .map(|s| s.to_str().map(|s| s.to_owned())) + .flatten() + .unwrap_or_else(|| "blank".to_owned()); + let data = WorkspaceData::LoadSite(LoadSite::blank_L1( + name, + Some(file.clone()), + )); + return Some(LoadWorkspaceFile(Some(file), data)); } + None } - Err(err) => { - error!("Failed loading urdf workcell {:?}", err); + #[cfg(target_arch = "wasm32")] + { + let data = + WorkspaceData::LoadSite(LoadSite::blank_L1("blank".to_owned(), None)); + Some(LoadWorkspaceFile(None, data)) } - } + }) + .then(process_load_files) + .connect(scope.terminate) + }); + + let load_workspace_from_path = world.spawn_workflow(|scope, builder| { + scope + .input + .chain(builder) + .map_async(|path| async move { + let Some(data) = std::fs::read(&path) + .ok() + .and_then(|data| WorkspaceData::new(&path, data)) + else { + warn!("Unable to read file [{path:?}] so it cannot be loaded"); + return None; + }; + Some(LoadWorkspaceFile(Some(path.clone()), data)) + }) + .then(process_load_files) + .connect(scope.terminate) + }); + + let load_workspace_from_data = world.spawn_workflow(|scope, builder| { + scope + .input + .chain(builder) + .map_block(|data| Some(LoadWorkspaceFile(None, data))) + .then(process_load_files) + .connect(scope.terminate) + }); + + Self { + load_workspace_from_dialog, + create_empty_workspace_from_dialog, + load_workspace_from_path, + load_workspace_from_data, + } + } +} + +impl WorkspaceLoadingServices { + /// Given a `LoadWorkspace` event, dispatches the corresponding workflow to process it + pub fn dispatch_workflow_for_request(&self, commands: &mut Commands, request: &LoadWorkspace) { + match request { + LoadWorkspace::Dialog => commands.request((), self.load_workspace_from_dialog), + LoadWorkspace::BlankFromDialog => { + commands.request((), self.create_empty_workspace_from_dialog) } - WorkspaceData::LoadSite(site) => { - app_state.set(AppState::SiteEditor); - load_site.send(site); - interaction_state.set(InteractionState::Enable); + LoadWorkspace::Path(path) => { + commands.request(path.clone(), self.load_workspace_from_path) + } + LoadWorkspace::Data(data) => { + commands.request(data.clone(), self.load_workspace_from_data) } } + .detach(); + } +} + +pub fn dispatch_load_workspace_workflows( + mut commands: Commands, + mut load_workspace: EventReader, + workspace_service: Res, +) { + if let Some(cmd) = load_workspace.read().last() { + workspace_service.dispatch_workflow_for_request(&mut commands, cmd); } }