From ac3333cd85fad244bf150516a7a74cd2c2ad292b Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Tue, 15 Jul 2025 13:43:00 +0000 Subject: [PATCH 01/21] Reorg fleet name param Signed-off-by: Xiyu Oh --- .../inspect_robot_properties.rs | 17 +++-- .../rmf_site_editor/src/widgets/tasks/mod.rs | 72 ++++++++++--------- crates/rmf_site_format/src/robot.rs | 4 ++ 3 files changed, 54 insertions(+), 39 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/inspector/inspect_model_description/inspect_robot_properties.rs b/crates/rmf_site_editor/src/widgets/inspector/inspect_model_description/inspect_robot_properties.rs index a4b0a0ac2..653c1a076 100644 --- a/crates/rmf_site_editor/src/widgets/inspector/inspect_model_description/inspect_robot_properties.rs +++ b/crates/rmf_site_editor/src/widgets/inspector/inspect_model_description/inspect_robot_properties.rs @@ -29,7 +29,7 @@ use bevy::{ ecs::{component::Mutable, hierarchy::ChildOf, system::SystemParam}, prelude::Component, }; -use bevy_egui::egui::{ComboBox, Ui}; +use bevy_egui::egui::{ComboBox, TextEdit, Ui}; use rmf_site_format::robot_properties::*; use serde_json::{Error, Value}; use smallvec::SmallVec; @@ -123,7 +123,7 @@ impl RobotPropertiesInspector { struct InspectRobotProperties<'w, 's> { model_instances: ModelPropertyQuery<'w, 's, Robot>, model_descriptions: - Query<'w, 's, &'static ModelProperty, (With, With)>, + Query<'w, 's, &'static mut ModelProperty, (With, With)>, inspect_robot_properties: Res<'w, RobotPropertiesInspector>, children: Query<'w, 's, &'static Children>, } @@ -139,7 +139,7 @@ impl<'w, 's> WidgetSystem for InspectRobotProperties<'w, 's> { state: &mut SystemState, world: &mut World, ) { - let params = state.get_mut(world); + let mut params = state.get_mut(world); let Some(description_entity) = get_selected_description_entity( selection, ¶ms.model_instances, @@ -148,9 +148,18 @@ impl<'w, 's> WidgetSystem for InspectRobotProperties<'w, 's> { return; }; // Ensure that this widget is displayed only when there is a valid Robot property - let Ok(ModelProperty(_robot)) = params.model_descriptions.get(description_entity) else { + let Ok(mut robot_model_property) = params.model_descriptions.get_mut(description_entity) + else { return; }; + + ui.horizontal(|ui| { + ui.label("Fleet:"); + TextEdit::singleline(&mut robot_model_property.0.fleet) + .desired_width(ui.available_width()) + .show(ui); + }); + ui.label("Robot Properties"); let children: Result, _> = params diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 61144cbe2..190009eee 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -479,10 +479,6 @@ fn show_task( ui.label("Requester:"); ui.label(task_request.requester().unwrap_or("None".to_string())); ui.end_row(); - - ui.label("Fleet name:"); - ui.label(task_request.fleet_name().unwrap_or("None".to_string())); - ui.end_row(); }); CollapsingHeader::new("More details") @@ -492,6 +488,12 @@ fn show_task( Grid::new("task_details_".to_owned() + &task_entity.index().to_string()) .num_columns(2) .show(ui, |ui| { + if task.is_dispatch() { + ui.label("Fleet:"); + ui.label(task_request.fleet_name().unwrap_or("None".to_string())); + ui.end_row(); + } + // TODO(@xiyuoh) Add status/queued information ui.label("Start time:"); ui.label( @@ -569,11 +571,10 @@ fn edit_task( change_task: &mut EventWriter>, update_task_modifier: &mut EventWriter>, ) { + let mut new_task = task.clone(); Grid::new("edit_task_".to_owned() + &task_entity.index().to_string()) .num_columns(2) .show(ui, |ui| { - let mut new_task = task.clone(); - // Select Request Type let mut is_robot_task_request = new_task.is_direct(); ui.label("Request Type:"); @@ -680,36 +681,33 @@ fn edit_task( *new_task_request.requester_mut() = None; } ui.end_row(); - - // Fleet name - ui.label("Fleet name:").on_hover_text( - "(Optional) The name of the fleet that should perform this task. \ - If specified, other fleets will not bid for this task.", - ); - // TODO(@xiyuoh) when available, insert combobox of registered fleets - let fleet_name = new_task_request - .fleet_name_mut() - .get_or_insert(String::new()); - ui.text_edit_singleline(fleet_name); - if fleet_name.is_empty() { - *new_task_request.fleet_name_mut() = None; - } - ui.end_row(); - - if new_task != *task { - change_task.write(Change::new(new_task, task_entity)); - } else { - } }); // More + let mut new_task_params = task_params.clone(); CollapsingHeader::new("More") .default_open(false) .show(ui, |ui| { Grid::new("edit_task_details") .num_columns(2) .show(ui, |ui| { - let mut new_task_params = task_params.clone(); + // Fleet name + if new_task.is_dispatch() { + ui.label("Fleet name:").on_hover_text( + "(Optional) The name of the fleet that should perform this task. \ + If specified, other fleets will not bid for this task.", + ); + // TODO(@xiyuoh) when available, insert combobox of registered fleets + let new_task_request = new_task.request_mut(); + let fleet_name = new_task_request + .fleet_name_mut() + .get_or_insert(String::new()); + ui.text_edit_singleline(fleet_name); + if fleet_name.is_empty() { + *new_task_request.fleet_name_mut() = None; + } + ui.end_row(); + } // Start time ui.label("Start Time:") @@ -803,14 +801,18 @@ fn edit_task( for i in remove_labels.drain(..).rev() { new_task_params.labels_mut().remove(i); } - - if new_task_params != *task_params { - update_task_modifier.write(UpdateModifier::new( - scenario, - task_entity, - UpdateTaskModifier::Modify(new_task_params), - )); - } }); }); + + if new_task != *task { + change_task.write(Change::new(new_task, task_entity)); + } + + if new_task_params != *task_params { + update_task_modifier.write(UpdateModifier::new( + scenario, + task_entity, + UpdateTaskModifier::Modify(new_task_params), + )); + } } diff --git a/crates/rmf_site_format/src/robot.rs b/crates/rmf_site_format/src/robot.rs index 2fb0cb428..9b63a95b1 100644 --- a/crates/rmf_site_format/src/robot.rs +++ b/crates/rmf_site_format/src/robot.rs @@ -23,12 +23,16 @@ use std::collections::HashMap; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component))] pub struct Robot { + // TODO(@xiyuoh) Fleet name is a string for now, we probably want some kind of + // fleet registration at some point + pub fleet: String, pub properties: HashMap, } impl Default for Robot { fn default() -> Self { Self { + fleet: String::new(), properties: HashMap::new(), } } From 7daa517546c9693f9be222b8534a1d1c31e2c350 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 16 Jul 2025 09:30:29 +0000 Subject: [PATCH 02/21] In-place task edit Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/widgets/tasks/mod.rs | 752 ++++++++++-------- 1 file changed, 402 insertions(+), 350 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 190009eee..385cbb492 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -19,7 +19,7 @@ use crate::{ count_scenarios_with_inclusion, Affiliation, Category, Change, CurrentScenario, Delete, DispatchTaskRequest, GetModifier, Group, Inclusion, Modifier, NameInSite, Pending, Robot, RobotTaskRequest, ScenarioMarker, ScenarioModifiers, Task, TaskKinds, TaskParams, - UpdateModifier, UpdateTaskModifier, + TaskRequest, UpdateModifier, UpdateTaskModifier, }, widgets::prelude::*, Icons, Tile, WidgetSystem, @@ -192,23 +192,24 @@ impl<'w, 's> ViewTasks<'w, 's> { task_entity, &self.get_inclusion_modifier, ); - if show_task( + show_task_widget( ui, + &mut self.commands, task_entity, task, current_scenario_entity, &self.get_inclusion_modifier, &self.get_params_modifier, + &mut self.change_task, &mut self.update_task_modifier, &mut self.delete, + &mut self.edit_mode, + &self.edit_task, + &mut self.task_kinds, + &self.robots, scenario_count, &self.icons, - ) { - self.edit_mode.write(EditModeEvent { - scenario: current_scenario_entity, - mode: EditMode::Edit(Some(task_entity)), - }); - } + ); } if self.tasks.is_empty() { ui.label("No tasks in this scenario"); @@ -243,51 +244,21 @@ impl<'w, 's> ViewTasks<'w, 's> { }); }); ui.separator(); - edit_task( + + show_editable_task( ui, &mut self.commands, - current_scenario_entity, task_entity, pending_task, &pending_task_params, - &self.task_kinds, + current_scenario_entity, + true, + &self.get_params_modifier, &self.robots, + &self.task_kinds, &mut self.change_task, &mut self.update_task_modifier, ); - } else { - if let Ok((_, existing_task)) = self.tasks.get_mut(task_entity) { - ui.horizontal(|ui| { - ui.label("Editing Task"); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - ui.add_enabled_ui(existing_task.is_valid(), |ui| { - if ui.button("Done").clicked() { - reset_edit = true; - } - }); - }); - }); - let Some(existing_task_params) = self - .get_params_modifier - .get(current_scenario_entity, task_entity) - .map(|modifier| (**modifier).clone()) - else { - return; - }; - ui.separator(); - edit_task( - ui, - &mut self.commands, - current_scenario_entity, - task_entity, - existing_task, - &existing_task_params, - &self.task_kinds, - &self.robots, - &mut self.change_task, - &mut self.update_task_modifier, - ); - } } } else { if ui.button("✚ Create New Task").clicked() { @@ -314,20 +285,25 @@ impl<'w, 's> ViewTasks<'w, 's> { } } -/// Displays the task data and params and returns a boolean indicating if the user -/// wishes to edit the task. -fn show_task( +/// Displays the task data and params, and allows users to enter edit mode to modify values +fn show_task_widget( ui: &mut Ui, + commands: &mut Commands, task_entity: Entity, task: &Task, scenario: Entity, get_inclusion_modifier: &GetModifier>, get_params_modifier: &GetModifier>, + change_task: &mut EventWriter>, update_task_modifier: &mut EventWriter>, delete: &mut EventWriter, + edit_mode: &mut EventWriter, + edit_task: &Res, + task_kinds: &ResMut, + robots: &Query<(Entity, &NameInSite), (With, Without)>, scenario_count: i32, icons: &Res, -) -> bool { +) { let present = get_inclusion_modifier .get(scenario, task_entity) .map(|i_modifier| **i_modifier == Inclusion::Included) @@ -337,7 +313,7 @@ fn show_task( } else { Color32::default() }; - let mut edit = false; + let in_edit_mode = edit_task.0.is_some_and(|e| e == task_entity); Frame::default() .inner_margin(4.0) .fill(color) @@ -420,14 +396,32 @@ fn show_task( )); } } - if present { - // Do not allow edit if not in current scenario + + if !in_edit_mode { + if present { + // Do not allow edit if not in current scenario + if ui + .add(ImageButton::new(icons.edit.egui())) + .on_hover_text("Edit task parameters") + .clicked() + { + edit_mode.write(EditModeEvent { + scenario: scenario, + mode: EditMode::Edit(Some(task_entity)), + }); + } + } + } else { + // Exit edit mode if ui - .add(ImageButton::new(icons.edit.egui())) - .on_hover_text("Edit task parameters") + .add(ImageButton::new(icons.confirm.egui())) + .on_hover_text("Exit edit mode") .clicked() { - edit = true; + edit_mode.write(EditModeEvent { + scenario: scenario, + mode: EditMode::Edit(None), + }); } } }); @@ -443,311 +437,160 @@ fn show_task( else { return; }; - let task_request = task.request(); - Grid::new("show_task_".to_owned() + &task_entity.index().to_string()) - .num_columns(2) - .show(ui, |ui| { - match task { - Task::Dispatch(_) => { - ui.label("Dispatch:"); - ui.label( - task_request - .fleet_name() - .unwrap_or("Unassigned".to_string()), - ); - ui.end_row(); - } - Task::Direct(_) => { - ui.label("Direct:"); - ui.label(task.fleet().to_owned() + "/" + &task.robot()); - ui.end_row(); - } - } - - ui.label("Kind:"); - ui.label(task_request.category()); - ui.end_row(); - - ui.label("Description:"); - ui.label( - task_request - .description_display() - .unwrap_or("None".to_string()), - ); - ui.end_row(); - ui.label("Requester:"); - ui.label(task_request.requester().unwrap_or("None".to_string())); - ui.end_row(); - }); - - CollapsingHeader::new("More details") - .id_salt("task_details_".to_owned() + &task_entity.index().to_string()) - .default_open(false) - .show(ui, |ui| { - Grid::new("task_details_".to_owned() + &task_entity.index().to_string()) - .num_columns(2) - .show(ui, |ui| { - if task.is_dispatch() { - ui.label("Fleet:"); - ui.label(task_request.fleet_name().unwrap_or("None".to_string())); - ui.end_row(); - } - - // TODO(@xiyuoh) Add status/queued information - ui.label("Start time:"); - ui.label( - task_params - .start_time() - .map(|rt| format!("{:?}", rt)) - .unwrap_or("None".to_string()), - ); - ui.end_row(); - - ui.label("Request time:"); - ui.label( - task_params - .request_time() - .map(|rt| format!("{:?}", rt)) - .unwrap_or("None".to_string()), - ); - ui.end_row(); - - ui.label("Priority:"); - ui.label( - task_params - .priority() - .map(|st| st.to_string()) - .unwrap_or("None".to_string()), - ); - ui.end_row(); - - ui.label("Labels:"); - ui.label(format!("{:?}", task_params.labels())); - ui.end_row(); - - // Reset task parameters to parent scenario params (if any) - if let Ok((scenario_modifiers, parent_scenario)) = - get_params_modifier.scenarios.get(scenario) - { - if scenario_modifiers - .get(&task_entity) - .is_some_and(|e| get_params_modifier.modifiers.get(*e).is_ok()) - && parent_scenario.0.is_some() - { - // Only display reset button if this task has a TaskParams modifier - // and this is not a root scenario - if ui - .button("Reset Task Params") - .on_hover_text( - "Reset task parameters to parent scenario params", - ) - .clicked() - { - update_task_modifier.write(UpdateModifier::new( - scenario, - task_entity, - UpdateTaskModifier::ResetParams, - )); - } - ui.end_row(); - } - } - }); - }); + show_editable_task( + ui, + commands, + task_entity, + task, + &task_params, + scenario, + in_edit_mode, + get_params_modifier, + robots, + task_kinds, + change_task, + update_task_modifier, + ); }); - edit } -fn edit_task( +fn show_editable_task( ui: &mut Ui, commands: &mut Commands, - scenario: Entity, task_entity: Entity, task: &Task, task_params: &TaskParams, - task_kinds: &ResMut, + scenario: Entity, + in_edit_mode: bool, + get_params_modifier: &GetModifier>, robots: &Query<(Entity, &NameInSite), (With, Without)>, + task_kinds: &ResMut, change_task: &mut EventWriter>, update_task_modifier: &mut EventWriter>, ) { let mut new_task = task.clone(); - Grid::new("edit_task_".to_owned() + &task_entity.index().to_string()) + let task_request = new_task.request(); + Grid::new("show_editable_task_".to_owned() + &task_entity.index().to_string()) .num_columns(2) .show(ui, |ui| { - // Select Request Type - let mut is_robot_task_request = new_task.is_direct(); + // Request Type ui.label("Request Type:"); - ui.horizontal(|ui| { - if ui - .selectable_label(!is_robot_task_request, "Dispatch") - .on_hover_text("Create a Dispatch Task for any robot in the site") - .clicked() - { - is_robot_task_request = false; - }; - if ui - .selectable_label(is_robot_task_request, "Direct") - .on_hover_text("Create a Direct Task for a specific robot in a fleet") - .clicked() - { - is_robot_task_request = true; - } - }); - ui.end_row(); - - // Update Request Type and show RobotTaskRequest widget - let task_request = new_task.request(); - if is_robot_task_request { - if !new_task.is_direct() { - let robot = task.robot(); - let fleet = task.fleet(); - new_task = - Task::Direct(RobotTaskRequest::new(robot, fleet, task_request.clone())); - } - if let Task::Direct(ref mut robot_task_request) = new_task { - ui.label("Fleet:"); - ui.add(TextEdit::singleline(robot_task_request.fleet_mut())); - ui.end_row(); - - ui.label("Robot:"); - let selected_robot = if robot_task_request.robot().is_empty() { - "Select Robot".to_string() - } else { - robot_task_request.robot() - }; - ComboBox::from_id_salt("select_robot_for_task") - .selected_text(selected_robot) - .show_ui(ui, |ui| { - for (_, robot) in robots.iter() { - ui.selectable_value( - robot_task_request.robot_mut(), - robot.0.clone(), - robot.0.clone(), - ); - } + if !in_edit_mode { + match task { + Task::Dispatch(_) => { + ui.horizontal(|ui| { + let _ = ui.selectable_label(true, "Dispatch"); + ui.label( + task_request + .fleet_name() + .unwrap_or("Unassigned".to_string()), + ); }); - ui.end_row(); - } else { - warn!("Unable to select Direct task!"); + } + Task::Direct(_) => { + ui.horizontal(|ui| { + let _ = ui.selectable_label(true, "Direct"); + ui.label(task.fleet().to_owned() + "/" + &task.robot()); + }); + } } } else { - if !new_task.is_dispatch() { - new_task = Task::Dispatch(DispatchTaskRequest::new(task_request.clone())); - } + edit_request_type_widget( + ui, + &mut new_task, + &task_request, + robots, + task.robot(), + task.fleet(), + ); } - // Show TaskRequest editing widget - let current_category = new_task.request().category(); - let selected_task_kind = if task_kinds.0.contains_key(¤t_category) { - current_category.clone() - } else { - "Select Kind".to_string() - }; - ui.label("Task Kind:"); - ComboBox::from_id_salt("select_task_kind") - .selected_text(selected_task_kind) - .show_ui(ui, |ui| { - for (kind, _) in task_kinds.0.iter() { - ui.selectable_value( - new_task.request_mut().category_mut(), - kind.clone(), - kind.clone(), - ); - } - }); ui.end_row(); - // Insert selected TaskKind component - let new_category = new_task.request().category(); - if new_category != current_category { - if let Some(remove_fn) = task_kinds.0.get(¤t_category).map(|(_, rm_fn)| rm_fn) - { - remove_fn(commands.entity(task_entity)); - } - if let Some(insert_fn) = task_kinds.0.get(&new_category).map(|(is_fn, _)| is_fn) { - insert_fn(commands.entity(task_entity)); - } + + // Task Kind + ui.label("Task Kind:"); + if !in_edit_mode { + ui.label(task_request.category()); + } else { + edit_task_kind_widget(ui, commands, &mut new_task, task_entity, task_kinds); } + ui.end_row(); - let new_task_request = new_task.request_mut(); + // Task Description + // Only displayed when not editing; the TaskKind widget will appear when editing + if !in_edit_mode { + ui.label("Description:"); + ui.label( + task_request + .description_display() + .unwrap_or("None".to_string()), + ); + ui.end_row(); + } // Requester ui.label("Requester:") .on_hover_text("(Optional) An identifier for the entity that requested this task"); - let requester = new_task_request - .requester_mut() - .get_or_insert(String::new()); - ui.text_edit_singleline(requester); - if requester.is_empty() { - *new_task_request.requester_mut() = None; + if !in_edit_mode { + ui.label(task_request.requester().unwrap_or("None".to_string())); + } else { + edit_requester_widget(ui, &mut new_task); } ui.end_row(); }); // More let mut new_task_params = task_params.clone(); - CollapsingHeader::new("More") + CollapsingHeader::new("More details") + .id_salt("task_details_".to_owned() + &task_entity.index().to_string()) .default_open(false) .show(ui, |ui| { - Grid::new("edit_task_details") + Grid::new("task_details_".to_owned() + &task_entity.index().to_string()) .num_columns(2) .show(ui, |ui| { // Fleet name - if new_task.is_dispatch() { - ui.label("Fleet name:").on_hover_text( - "(Optional) The name of the fleet that should perform this task. \ - If specified, other fleets will not bid for this task.", + if task.is_dispatch() { + ui.label("Fleet:").on_hover_text( + "(Optional) The name of the fleet for this robot. \ + If specified, other fleets will not bid for this task.", ); - // TODO(@xiyuoh) when available, insert combobox of registered fleets - let new_task_request = new_task.request_mut(); - let fleet_name = new_task_request - .fleet_name_mut() - .get_or_insert(String::new()); - ui.text_edit_singleline(fleet_name); - if fleet_name.is_empty() { - *new_task_request.fleet_name_mut() = None; + if !in_edit_mode { + ui.label(task_request.fleet_name().unwrap_or("None".to_string())); + } else { + edit_fleet_widget(ui, &mut new_task); } ui.end_row(); } // Start time - ui.label("Start Time:") + // TODO(@xiyuoh) Add status/queued information + ui.label("Start time:") .on_hover_text("(Optional) The earliest time that this task may start"); - let start_time = new_task_params.start_time(); - let mut has_start_time = start_time.is_some(); - ui.horizontal(|ui| { - ui.checkbox(&mut has_start_time, ""); - if has_start_time { - let new_start_time = new_task_params.start_time_mut().get_or_insert(0); - ui.add( - DragValue::new(new_start_time) - .range(0_i32..=std::i32::MAX) - .speed(1), - ); - } else if start_time.is_some() { - *new_task_params.start_time_mut() = None; - } - }); + if !in_edit_mode { + ui.label( + task_params + .start_time() + .map(|rt| format!("{:?}", rt)) + .unwrap_or("None".to_string()), + ); + } else { + edit_start_time_widget(ui, &mut new_task_params); + } ui.end_row(); // Request time - ui.label("Request Time:") + ui.label("Request time:") .on_hover_text("(Optional) The time that this request was initiated"); - let request_time = new_task_params.request_time(); - let mut has_request_time = request_time.is_some(); - ui.horizontal(|ui| { - ui.checkbox(&mut has_request_time, ""); - if has_request_time { - let new_request_time = - new_task_params.request_time_mut().get_or_insert(0); - ui.add( - DragValue::new(new_request_time) - .range(0_i32..=std::i32::MAX) - .speed(1), - ); - } else if request_time.is_some() { - *new_task_params.request_time_mut() = None; - } - }); + if !in_edit_mode { + ui.label( + task_params + .request_time() + .map(|rt| format!("{:?}", rt)) + .unwrap_or("None".to_string()), + ); + } else { + edit_request_time_widget(ui, &mut new_task_params); + } ui.end_row(); // Priority @@ -755,18 +598,17 @@ fn edit_task( "(Optional) The priority of this task. \ This must match a priority schema supported by a fleet.", ); - let priority = new_task_params.priority(); - let mut has_priority = priority.is_some(); - ui.checkbox(&mut has_priority, ""); - ui.end_row(); - if has_priority { - if priority.is_none() { - *new_task_params.priority_mut() = Some(Value::Null); - } - // TODO(@xiyuoh) Expand on this to create fleet-specific priority widgets - } else if priority.is_some() { - *new_task_params.priority_mut() = None; + if !in_edit_mode { + ui.label( + task_params + .priority() + .map(|st| st.to_string()) + .unwrap_or("None".to_string()), + ); + } else { + edit_priority_widget(ui, &mut new_task_params); } + ui.end_row(); // Labels ui.label("Labels:").on_hover_text( @@ -775,44 +617,254 @@ fn edit_task( like `app=dashboard`, in the case of a single value, it will be \ interpreted as a key-value pair with an empty string value.", ); - let mut remove_labels = Vec::new(); - let mut id: usize = 0; - for label in new_task_params.labels_mut() { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("❌").on_hover_text("Remove label").clicked() { - remove_labels.push(id.clone()); - } - ui.text_edit_singleline(label); - }); - id += 1; - ui.end_row(); - ui.label(""); + if !in_edit_mode { + ui.label(format!("{:?}", task_params.labels())); + } else { + edit_labels_widget(ui, &mut new_task_params); } - ui.with_layout(Layout::right_to_left(Align::Max), |ui| { - if ui - .button("Add label") - .on_hover_text("Insert new label") - .clicked() + ui.end_row(); + + // Reset task parameters to parent scenario params (if any) + if let Ok((scenario_modifiers, parent_scenario)) = + get_params_modifier.scenarios.get(scenario) + { + if scenario_modifiers + .get(&task_entity) + .is_some_and(|e| get_params_modifier.modifiers.get(*e).is_ok()) + && parent_scenario.0.is_some() { - new_task_params.labels_mut().push(String::new()); + // Only display reset button if this task has a TaskParams modifier + // and this is not a root scenario + if ui + .button("Reset Task Params") + .on_hover_text("Reset task parameters to parent scenario params") + .clicked() + { + update_task_modifier.write(UpdateModifier::new( + scenario, + task_entity, + UpdateTaskModifier::ResetParams, + )); + } + ui.end_row(); } - }); - ui.end_row(); - for i in remove_labels.drain(..).rev() { - new_task_params.labels_mut().remove(i); } }); }); - if new_task != *task { - change_task.write(Change::new(new_task, task_entity)); + // Trigger appropriate events if changes have been made in edit mode + if in_edit_mode { + if new_task != *task { + change_task.write(Change::new(new_task, task_entity)); + } + + if new_task_params != *task_params { + update_task_modifier.write(UpdateModifier::new( + scenario, + task_entity, + UpdateTaskModifier::Modify(new_task_params), + )); + } } +} - if new_task_params != *task_params { - update_task_modifier.write(UpdateModifier::new( - scenario, - task_entity, - UpdateTaskModifier::Modify(new_task_params), - )); +fn edit_request_type_widget( + ui: &mut Ui, + task: &mut Task, + task_request: &TaskRequest, + robots: &Query<(Entity, &NameInSite), (With, Without)>, + robot: String, + fleet: String, +) { + let mut is_robot_task_request = task.is_direct(); + ui.horizontal(|ui| { + if ui + .selectable_label(!is_robot_task_request, "Dispatch") + .on_hover_text("Create a Dispatch Task for any robot in the site") + .clicked() + { + is_robot_task_request = false; + }; + if ui + .selectable_label(is_robot_task_request, "Direct") + .on_hover_text("Create a Direct Task for a specific robot in a fleet") + .clicked() + { + is_robot_task_request = true; + } + }); + // Update Request Type and show RobotTaskRequest widget + if is_robot_task_request { + if !task.is_direct() { + *task = Task::Direct(RobotTaskRequest::new(robot, fleet, task_request.clone())); + } + if let Task::Direct(ref mut robot_task_request) = task { + ui.end_row(); + + ui.label("Fleet:"); + ui.add(TextEdit::singleline(robot_task_request.fleet_mut())); + ui.end_row(); + + ui.label("Robot:"); + let selected_robot = if robot_task_request.robot().is_empty() { + "Select Robot".to_string() + } else { + robot_task_request.robot() + }; + ComboBox::from_id_salt("select_robot_for_task") + .selected_text(selected_robot) + .show_ui(ui, |ui| { + for (_, robot) in robots.iter() { + ui.selectable_value( + robot_task_request.robot_mut(), + robot.0.clone(), + robot.0.clone(), + ); + } + }); + } else { + warn!("Unable to select Direct task!"); + } + } else { + if !task.is_dispatch() { + *task = Task::Dispatch(DispatchTaskRequest::new(task_request.clone())); + } + } +} + +fn edit_task_kind_widget( + ui: &mut Ui, + commands: &mut Commands, + task: &mut Task, + task_entity: Entity, + task_kinds: &ResMut, +) { + let current_category = task.request().category(); + let selected_task_kind = if task_kinds.0.contains_key(¤t_category) { + current_category.clone() + } else { + "Select Kind".to_string() + }; + ComboBox::from_id_salt("select_task_kind") + .selected_text(selected_task_kind) + .show_ui(ui, |ui| { + for (kind, _) in task_kinds.0.iter() { + ui.selectable_value( + task.request_mut().category_mut(), + kind.clone(), + kind.clone(), + ); + } + }); + // Insert selected TaskKind component + let new_category = task.request().category(); + if new_category != current_category { + if let Some(remove_fn) = task_kinds.0.get(¤t_category).map(|(_, rm_fn)| rm_fn) { + remove_fn(commands.entity(task_entity)); + } + if let Some(insert_fn) = task_kinds.0.get(&new_category).map(|(is_fn, _)| is_fn) { + insert_fn(commands.entity(task_entity)); + } + } +} + +fn edit_requester_widget(ui: &mut Ui, task: &mut Task) { + let new_task_request = task.request_mut(); + let requester = new_task_request + .requester_mut() + .get_or_insert(String::new()); + ui.text_edit_singleline(requester); + if requester.is_empty() { + *new_task_request.requester_mut() = None; + } +} + +fn edit_fleet_widget(ui: &mut Ui, task: &mut Task) { + // TODO(@xiyuoh) when available, insert combobox of registered fleets + let new_task_request = task.request_mut(); + let fleet_name = new_task_request + .fleet_name_mut() + .get_or_insert(String::new()); + ui.text_edit_singleline(fleet_name); + if fleet_name.is_empty() { + *new_task_request.fleet_name_mut() = None; + } +} + +fn edit_start_time_widget(ui: &mut Ui, task_params: &mut TaskParams) { + let start_time = task_params.start_time(); + let mut has_start_time = start_time.is_some(); + ui.horizontal(|ui| { + ui.checkbox(&mut has_start_time, ""); + if has_start_time { + let new_start_time = task_params.start_time_mut().get_or_insert(0); + ui.add( + DragValue::new(new_start_time) + .range(0_i32..=std::i32::MAX) + .speed(1), + ); + } else if start_time.is_some() { + *task_params.start_time_mut() = None; + } + }); +} + +fn edit_request_time_widget(ui: &mut Ui, task_params: &mut TaskParams) { + let request_time = task_params.request_time(); + let mut has_request_time = request_time.is_some(); + ui.horizontal(|ui| { + ui.checkbox(&mut has_request_time, ""); + if has_request_time { + let new_request_time = task_params.request_time_mut().get_or_insert(0); + ui.add( + DragValue::new(new_request_time) + .range(0_i32..=std::i32::MAX) + .speed(1), + ); + } else if request_time.is_some() { + *task_params.request_time_mut() = None; + } + }); +} + +fn edit_priority_widget(ui: &mut Ui, task_params: &mut TaskParams) { + let priority = task_params.priority(); + let mut has_priority = priority.is_some(); + ui.checkbox(&mut has_priority, ""); + if has_priority { + if priority.is_none() { + *task_params.priority_mut() = Some(Value::Null); + } + // TODO(@xiyuoh) Expand on this to create fleet-specific priority widgets + } else if priority.is_some() { + *task_params.priority_mut() = None; + } +} + +fn edit_labels_widget(ui: &mut Ui, task_params: &mut TaskParams) { + let mut remove_labels = Vec::new(); + let mut id: usize = 0; + for label in task_params.labels_mut() { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button("❌").on_hover_text("Remove label").clicked() { + remove_labels.push(id.clone()); + } + ui.text_edit_singleline(label); + }); + id += 1; + ui.end_row(); + ui.label(""); + } + ui.with_layout(Layout::right_to_left(Align::Max), |ui| { + if ui + .button("Add label") + .on_hover_text("Insert new label") + .clicked() + { + task_params.labels_mut().push(String::new()); + } + }); + for i in remove_labels.drain(..).rev() { + task_params.labels_mut().remove(i); } } From c4f809909286c62a13fafcc718adaa153ac22e42 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 16 Jul 2025 10:58:34 +0000 Subject: [PATCH 03/21] Move Create New Task to center window Signed-off-by: Xiyu Oh --- crates/rmf_site_editor/src/site/task.rs | 4 +- .../rmf_site_editor/src/widgets/tasks/mod.rs | 150 +++++++++++------- 2 files changed, 97 insertions(+), 57 deletions(-) diff --git a/crates/rmf_site_editor/src/site/task.rs b/crates/rmf_site_editor/src/site/task.rs index 7ba855f2a..769a4b38f 100644 --- a/crates/rmf_site_editor/src/site/task.rs +++ b/crates/rmf_site_editor/src/site/task.rs @@ -21,7 +21,7 @@ use crate::{ Pending, Property, ScenarioMarker, ScenarioModifiers, Task, TaskKind, TaskParams, UpdateModifier, UpdateProperty, }, - widgets::tasks::{EditMode, EditModeEvent, EditTask}, + widgets::tasks::{CreateTaskDialog, EditMode, EditModeEvent, EditTask}, CurrentWorkspace, }; use bevy::ecs::{hierarchy::ChildOf, system::SystemState}; @@ -194,6 +194,7 @@ pub enum UpdateTaskModifier { /// Updates the current EditTask entity based on the triggered edit mode event pub fn handle_task_edit( mut commands: Commands, + mut create_task_dialog: ResMut, mut delete: EventWriter, mut edit_mode: EventReader, mut edit_task: ResMut, @@ -208,6 +209,7 @@ pub fn handle_task_edit( commands.entity(task_entity).insert(ChildOf(site_entity)); } edit_task.0 = Some(task_entity); + create_task_dialog.visible = true; } EditMode::Edit(task_entity) => { if let Some(pending_task) = edit_task.0.filter(|e| pending_tasks.get(*e).is_ok()) { diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 385cbb492..80dc06d5f 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -21,7 +21,7 @@ use crate::{ RobotTaskRequest, ScenarioMarker, ScenarioModifiers, Task, TaskKinds, TaskParams, TaskRequest, UpdateModifier, UpdateTaskModifier, }, - widgets::prelude::*, + widgets::{prelude::*, RenderUiSet}, Icons, Tile, WidgetSystem, }; use bevy::{ @@ -31,9 +31,12 @@ use bevy::{ }, prelude::*, }; -use bevy_egui::egui::{ - Align, CollapsingHeader, Color32, ComboBox, DragValue, Frame, Grid, ImageButton, Layout, - Stroke, TextEdit, Ui, +use bevy_egui::{ + egui::{ + Align, Align2, CollapsingHeader, Color32, ComboBox, DragValue, Frame, Grid, ImageButton, + Layout, Stroke, TextEdit, Ui, Window, + }, + EguiContexts, }; use serde_json::Value; use smallvec::SmallVec; @@ -63,7 +66,9 @@ impl Plugin for MainTasksPlugin { app.init_resource::() .init_resource::() .init_resource::() - .add_event::(); + .init_resource::() + .add_event::() + .add_systems(Update, show_create_task_dialog.after(RenderUiSet)); } } @@ -79,6 +84,11 @@ impl TaskWidget { } } +#[derive(Resource, Default)] +pub struct CreateTaskDialog { + pub visible: bool, +} + impl FromWorld for TaskWidget { fn from_world(world: &mut World) -> Self { let widget = Widget::new::(world); @@ -122,7 +132,6 @@ pub struct ViewTasks<'w, 's> { get_inclusion_modifier: GetModifier<'w, 's, Modifier>, get_params_modifier: GetModifier<'w, 's, Modifier>, icons: Res<'w, Icons>, - pending_tasks: Query<'w, 's, (Entity, &'static Task, &'static TaskParams), With>, robots: Query<'w, 's, (Entity, &'static NameInSite), (With, Without)>, scenarios: Query< 'w, @@ -218,49 +227,7 @@ impl<'w, 's> ViewTasks<'w, 's> { ui.add_space(10.0); ui.separator(); - let mut reset_edit: bool = false; - - if let Some(task_entity) = self.edit_task.0 { - if let Ok((_, pending_task, pending_task_params)) = - self.pending_tasks.get_mut(task_entity) - { - ui.horizontal(|ui| { - ui.label("Creating Task"); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("Cancel").clicked() { - reset_edit = true; - } - ui.add_enabled_ui(pending_task.is_valid(), |ui| { - // TODO(@xiyuoh) Also check validity of TaskKind (e.g. GoToPlace) - if ui - .button("Add Task") - .on_hover_text("Add this task to the current scenario") - .clicked() - { - self.commands.entity(task_entity).remove::(); - reset_edit = true; - } - }); - }); - }); - ui.separator(); - - show_editable_task( - ui, - &mut self.commands, - task_entity, - pending_task, - &pending_task_params, - current_scenario_entity, - true, - &self.get_params_modifier, - &self.robots, - &self.task_kinds, - &mut self.change_task, - &mut self.update_task_modifier, - ); - } - } else { + if self.edit_task.0.is_none() { if ui.button("✚ Create New Task").clicked() { let new_task = self .commands @@ -275,13 +242,6 @@ impl<'w, 's> ViewTasks<'w, 's> { }); } } - - if reset_edit { - self.edit_mode.write(EditModeEvent { - scenario: current_scenario_entity, - mode: EditMode::Edit(None), - }); - } } } @@ -668,6 +628,84 @@ fn show_editable_task( } } +fn show_create_task_dialog( + mut commands: Commands, + mut contexts: EguiContexts, + mut create_task_dialog: ResMut, + mut change_task: EventWriter>, + mut edit_mode: EventWriter, + mut update_task_modifier: EventWriter>, + current_scenario: Res, + edit_task: Res, + get_params_modifier: GetModifier>, + pending_tasks: Query<(&Task, &TaskParams), With>, + robots: Query<(Entity, &NameInSite), (With, Without)>, + task_kinds: ResMut, +) { + if !create_task_dialog.visible { + return; + } + let Some(current_scenario_entity) = current_scenario.0 else { + return; + }; + let Some(task_entity) = edit_task.0 else { + return; + }; + let Ok((pending_task, pending_task_params)) = pending_tasks.get(task_entity) else { + return; + }; + + Window::new("Creating New Task") + .collapsible(false) + .resizable(false) + .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) + .show(contexts.ctx_mut(), |ui| { + show_editable_task( + ui, + &mut commands, + task_entity, + pending_task, + pending_task_params, + current_scenario_entity, + true, + &get_params_modifier, + &robots, + &task_kinds, + &mut change_task, + &mut update_task_modifier, + ); + ui.separator(); + + let mut reset_edit: bool = false; + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button("Cancel").clicked() { + reset_edit = true; + } + ui.add_enabled_ui(pending_task.is_valid(), |ui| { + // TODO(@xiyuoh) Also check validity of TaskKind (e.g. GoToPlace) + if ui + .button("Add Task") + .on_hover_text("Add this task to the current scenario") + .clicked() + { + commands.entity(task_entity).remove::(); + reset_edit = true; + } + }); + }); + + if reset_edit { + edit_mode.write(EditModeEvent { + scenario: current_scenario_entity, + mode: EditMode::Edit(None), + }); + create_task_dialog.visible = false; + } + + // TODO(@xiyuoh) show child widgets! + }); +} + fn edit_request_type_widget( ui: &mut Ui, task: &mut Task, From 3989422953df30ea7114ddf7116c19d4369d6826 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Tue, 22 Jul 2025 15:06:26 +0000 Subject: [PATCH 04/21] Move task panel to the left Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/widgets/creation.rs | 47 +++++- .../rmf_site_editor/src/widgets/tasks/mod.rs | 140 ++++++++++-------- 2 files changed, 126 insertions(+), 61 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/creation.rs b/crates/rmf_site_editor/src/widgets/creation.rs index 28d66777e..57c190289 100644 --- a/crates/rmf_site_editor/src/widgets/creation.rs +++ b/crates/rmf_site_editor/src/widgets/creation.rs @@ -24,7 +24,7 @@ use crate::{ }, widgets::{ AssetGalleryStatus, HeaderTilePlugin, Icons, InspectAssetSourceComponent, - InspectScaleComponent, Tile, WidgetSystem, + InspectScaleComponent, TaskWidget, Tile, WidgetSystem, }, AppState, CurrentWorkspace, }; @@ -54,6 +54,7 @@ impl Plugin for StandardCreationPlugin { DrawingCreationPlugin::default(), ModelCreationPlugin::default(), BrowseFuelTogglePlugin::default(), + InspectTasksTogglePlugin::default(), )); } } @@ -668,6 +669,50 @@ impl<'w> WidgetSystem for BrowseFuelToggle<'w> { } } +#[derive(Default)] +pub struct InspectTasksTogglePlugin {} + +impl Plugin for InspectTasksTogglePlugin { + fn build(&self, app: &mut App) { + app.add_plugins(HeaderTilePlugin::::new()); + } +} + +#[derive(SystemParam)] +pub struct InspectTasksToggle<'w> { + task_widget: Option>, + app_state: Res<'w, State>, +} + +impl<'w> WidgetSystem for InspectTasksToggle<'w> { + fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) -> () { + let mut params = state.get_mut(world); + if !matches!(params.app_state.get(), AppState::SiteEditor) { + return; + } + + let enabled = params.task_widget.is_some(); + let toggled_on = params.task_widget.as_ref().is_some_and(|panel| panel.show); + let tooltip = if !enabled { + "Task panel is not available" + } else if toggled_on { + "Close task panel" + } else { + "Open task panel" + }; + + if ui + .add_enabled(enabled, Button::new("📋").selected(toggled_on)) + .on_hover_text(tooltip) + .clicked() + { + if let Some(panel) = &mut params.task_widget { + panel.show = !panel.show; + } + } + } +} + /// Helper funtion to display the button name on hover fn button_clicked(ui: &mut Ui, icon: &str, tooltip: &str) -> bool { ui.add(Button::new(icon)).on_hover_text(tooltip).clicked() diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 80dc06d5f..ad1e70252 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -21,20 +21,17 @@ use crate::{ RobotTaskRequest, ScenarioMarker, ScenarioModifiers, Task, TaskKinds, TaskParams, TaskRequest, UpdateModifier, UpdateTaskModifier, }, - widgets::{prelude::*, RenderUiSet}, - Icons, Tile, WidgetSystem, + widgets::{prelude::*, show_panel_of_tiles, RenderUiSet}, + AppState, Icons, Tile, WidgetSystem, }; use bevy::{ - ecs::{ - hierarchy::ChildOf, - system::{SystemParam, SystemState}, - }, + ecs::system::{SystemParam, SystemState}, prelude::*, }; use bevy_egui::{ egui::{ Align, Align2, CollapsingHeader, Color32, ComboBox, DragValue, Frame, Grid, ImageButton, - Layout, Stroke, TextEdit, Ui, Window, + Layout, RichText, ScrollArea, Stroke, TextEdit, Ui, Window, }, EguiContexts, }; @@ -75,7 +72,8 @@ impl Plugin for MainTasksPlugin { /// Contains a reference to the tasks widget. #[derive(Resource)] pub struct TaskWidget { - id: Entity, + pub id: Entity, + pub show: bool, } impl TaskWidget { @@ -91,11 +89,26 @@ pub struct CreateTaskDialog { impl FromWorld for TaskWidget { fn from_world(world: &mut World) -> Self { - let widget = Widget::new::(world); - let properties_panel = world.resource::().id(); - let id = world.spawn(widget).insert(ChildOf(properties_panel)).id(); - Self { id } + let panel_widget = PanelWidget::new(tasks_panel, world); + let panel_id = world.spawn((panel_widget, PanelSide::Left)).id(); + + let main_task_widget = Widget::new::(world); + let id = world.spawn(main_task_widget).insert(ChildOf(panel_id)).id(); + + Self { id, show: false } + } +} + +fn tasks_panel(In(PanelWidgetInput { id, context }): In, world: &mut World) { + let correct_state = world + .get_resource::>() + .is_some_and(|state| matches!(state.get(), AppState::SiteEditor)); + + if !world.resource::().show || !correct_state { + return; } + + show_panel_of_tiles(In(PanelWidgetInput { id, context }), world); } /// Points to any task entity that is currently in edit mode @@ -156,27 +169,25 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { state: &mut SystemState, world: &mut World, ) { - CollapsingHeader::new("Tasks") - .default_open(true) - .show(ui, |ui| { - let mut params = state.get_mut(world); - params.show_widget(ui); - - if params.edit_task.0.is_some() { - let children: Result, _> = params - .children - .get(params.task_widget.id) - .map(|children| children.iter().collect()); - let Ok(children) = children else { - return; - }; - - for child in children { - let tile = Tile { id, panel }; - let _ = world.try_show_in(child, tile, ui); - } - } - }); + ui.label(RichText::new("Tasks").size(18.0)); + ui.add_space(10.0); + let mut params = state.get_mut(world); + params.show_widget(ui); + + if params.edit_task.0.is_some() { + let children: Result, _> = params + .children + .get(params.task_widget.id) + .map(|children| children.iter().collect()); + let Ok(children) = children else { + return; + }; + + for child in children { + let tile = Tile { id, panel }; + let _ = world.try_show_in(child, tile, ui); + } + } ui.add_space(10.0); } } @@ -187,7 +198,6 @@ impl<'w, 's> ViewTasks<'w, 's> { ui.label("No scenario selected, unable to display or create tasks."); return; }; - // View and modify tasks in current scenario Frame::default() .inner_margin(4.0) @@ -195,37 +205,47 @@ impl<'w, 's> ViewTasks<'w, 's> { .stroke(Stroke::new(1.0, Color32::GRAY)) .show(ui, |ui| { ui.set_min_width(ui.available_width()); - for (task_entity, task) in self.tasks.iter() { - let scenario_count = count_scenarios_with_inclusion( - &self.scenarios, - task_entity, - &self.get_inclusion_modifier, - ); - show_task_widget( - ui, - &mut self.commands, - task_entity, - task, - current_scenario_entity, - &self.get_inclusion_modifier, - &self.get_params_modifier, - &mut self.change_task, - &mut self.update_task_modifier, - &mut self.delete, - &mut self.edit_mode, - &self.edit_task, - &mut self.task_kinds, - &self.robots, - scenario_count, - &self.icons, - ); - } if self.tasks.is_empty() { - ui.label("No tasks in this scenario"); + ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { + ui.label("No tasks in this scenario"); + }); + } else { + let max_height = ui.available_height() / 2.0; + ScrollArea::new([true, true]) + .max_height(max_height) + .auto_shrink([false, false]) + .show(ui, |ui| { + for (task_entity, task) in self.tasks.iter() { + let scenario_count = count_scenarios_with_inclusion( + &self.scenarios, + task_entity, + &self.get_inclusion_modifier, + ); + show_task_widget( + ui, + &mut self.commands, + task_entity, + task, + current_scenario_entity, + &self.get_inclusion_modifier, + &self.get_params_modifier, + &mut self.change_task, + &mut self.update_task_modifier, + &mut self.delete, + &mut self.edit_mode, + &self.edit_task, + &mut self.task_kinds, + &self.robots, + scenario_count, + &self.icons, + ); + } + }); } }); ui.add_space(10.0); ui.separator(); + ui.add_space(10.0); if self.edit_task.0.is_none() { if ui.button("✚ Create New Task").clicked() { From 0a97f7a8ccc7db51cab2240fcb93368b4fc75611 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 23 Jul 2025 11:10:10 +0000 Subject: [PATCH 05/21] Children widget in dialog if create new task Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/widgets/tasks/mod.rs | 83 ++++++++++++++----- 1 file changed, 62 insertions(+), 21 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index ad1e70252..861e2374c 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -21,7 +21,7 @@ use crate::{ RobotTaskRequest, ScenarioMarker, ScenarioModifiers, Task, TaskKinds, TaskParams, TaskRequest, UpdateModifier, UpdateTaskModifier, }, - widgets::{prelude::*, show_panel_of_tiles, RenderUiSet}, + widgets::{prelude::*, show_panel_of_tiles}, AppState, Icons, Tile, WidgetSystem, }; use bevy::{ @@ -65,7 +65,7 @@ impl Plugin for MainTasksPlugin { .init_resource::() .init_resource::() .add_event::() - .add_systems(Update, show_create_task_dialog.after(RenderUiSet)); + .add_systems(Update, show_create_task_dialog); } } @@ -174,7 +174,12 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { let mut params = state.get_mut(world); params.show_widget(ui); - if params.edit_task.0.is_some() { + // Display children widgets if editing existing task + if params + .edit_task + .0 + .is_some_and(|e| params.tasks.get(e).is_ok()) + { let children: Result, _> = params .children .get(params.task_widget.id) @@ -649,22 +654,30 @@ fn show_editable_task( } fn show_create_task_dialog( - mut commands: Commands, - mut contexts: EguiContexts, - mut create_task_dialog: ResMut, - mut change_task: EventWriter>, - mut edit_mode: EventWriter, - mut update_task_modifier: EventWriter>, - current_scenario: Res, - edit_task: Res, - get_params_modifier: GetModifier>, - pending_tasks: Query<(&Task, &TaskParams), With>, - robots: Query<(Entity, &NameInSite), (With, Without)>, - task_kinds: ResMut, + world: &mut World, + task_state: &mut SystemState<( + Res, + Res, + Query<(&Task, &TaskParams), With>, + )>, + egui_context_state: &mut SystemState, + edit_state: &mut SystemState<( + Commands, + GetModifier>, + Query<(Entity, &NameInSite), (With, Without)>, + ResMut, + EventWriter>, + EventWriter>, + )>, + widget_state: &mut SystemState<(Query<&Children>, Res)>, + dialog_state: &mut SystemState<(ResMut, EventWriter)>, ) { + let (create_task_dialog, _) = dialog_state.get_mut(world); if !create_task_dialog.visible { return; } + + let (current_scenario, edit_task, pending_tasks) = task_state.get_mut(world); let Some(current_scenario_entity) = current_scenario.0 else { return; }; @@ -674,18 +687,29 @@ fn show_create_task_dialog( let Ok((pending_task, pending_task_params)) = pending_tasks.get(task_entity) else { return; }; + let (pending_task, pending_task_params) = (pending_task.clone(), pending_task_params.clone()); + let mut egui_context = egui_context_state.get_mut(world); + let mut ctx = egui_context.ctx_mut().clone(); Window::new("Creating New Task") .collapsible(false) .resizable(false) .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) - .show(contexts.ctx_mut(), |ui| { + .show(&mut ctx, |ui| { + let ( + mut commands, + get_params_modifier, + robots, + task_kinds, + mut change_task, + mut update_task_modifier, + ) = edit_state.get_mut(world); show_editable_task( ui, &mut commands, task_entity, - pending_task, - pending_task_params, + &pending_task, + &pending_task_params, current_scenario_entity, true, &get_params_modifier, @@ -694,8 +718,26 @@ fn show_create_task_dialog( &mut change_task, &mut update_task_modifier, ); + edit_state.apply(world); ui.separator(); + let (children, task_widget) = widget_state.get_mut(world); + let children: Result, _> = children + .get(task_widget.id) + .map(|children| children.iter().collect()); + let Ok(children) = children else { + return; + }; + + let widget_entity = task_widget.id; + for child in children { + let tile = Tile { + id: widget_entity, + panel: PanelSide::Top, // Any panel will do + }; + let _ = world.try_show_in(child, tile, ui); + } + let mut reset_edit: bool = false; ui.with_layout(Layout::right_to_left(Align::Center), |ui| { if ui.button("Cancel").clicked() { @@ -708,21 +750,20 @@ fn show_create_task_dialog( .on_hover_text("Add this task to the current scenario") .clicked() { - commands.entity(task_entity).remove::(); + world.entity_mut(task_entity).remove::(); reset_edit = true; } }); }); if reset_edit { + let (mut create_task_dialog, mut edit_mode) = dialog_state.get_mut(world); edit_mode.write(EditModeEvent { scenario: current_scenario_entity, mode: EditMode::Edit(None), }); create_task_dialog.visible = false; } - - // TODO(@xiyuoh) show child widgets! }); } From a4fb9c3099bd8c1f65aec3d0c5761e5c17c0da8e Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 23 Jul 2025 11:12:22 +0000 Subject: [PATCH 06/21] Optional serializing for some items Signed-off-by: Xiyu Oh --- crates/rmf_site_format/src/scenario.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/crates/rmf_site_format/src/scenario.rs b/crates/rmf_site_format/src/scenario.rs index 0fc3b5c26..c4c753e9d 100644 --- a/crates/rmf_site_format/src/scenario.rs +++ b/crates/rmf_site_format/src/scenario.rs @@ -48,6 +48,7 @@ pub struct InstanceModifier { pub struct TaskModifier { #[serde(default, skip_serializing_if = "is_default")] pub inclusion: Option, + #[serde(default, skip_serializing_if = "is_default")] pub params: Option, } @@ -69,7 +70,9 @@ pub struct ScenarioMarker; #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component))] pub struct Scenario { + #[serde(default, skip_serializing_if = "is_default")] pub instances: BTreeMap, + #[serde(default, skip_serializing_if = "is_default")] pub tasks: BTreeMap, #[serde(flatten)] pub properties: ScenarioBundle, From dfd30f644a56aa247df9352e1c0808a7bc48e418 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 23 Jul 2025 11:22:31 +0000 Subject: [PATCH 07/21] Infer fleet from robot Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/widgets/creation.rs | 12 +++++------ .../rmf_site_editor/src/widgets/tasks/mod.rs | 20 +++++++++++-------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/creation.rs b/crates/rmf_site_editor/src/widgets/creation.rs index 57c190289..f1d896df4 100644 --- a/crates/rmf_site_editor/src/widgets/creation.rs +++ b/crates/rmf_site_editor/src/widgets/creation.rs @@ -54,7 +54,7 @@ impl Plugin for StandardCreationPlugin { DrawingCreationPlugin::default(), ModelCreationPlugin::default(), BrowseFuelTogglePlugin::default(), - InspectTasksTogglePlugin::default(), + TaskPanelTogglePlugin::default(), )); } } @@ -670,21 +670,21 @@ impl<'w> WidgetSystem for BrowseFuelToggle<'w> { } #[derive(Default)] -pub struct InspectTasksTogglePlugin {} +pub struct TaskPanelTogglePlugin {} -impl Plugin for InspectTasksTogglePlugin { +impl Plugin for TaskPanelTogglePlugin { fn build(&self, app: &mut App) { - app.add_plugins(HeaderTilePlugin::::new()); + app.add_plugins(HeaderTilePlugin::::new()); } } #[derive(SystemParam)] -pub struct InspectTasksToggle<'w> { +pub struct TaskPanelToggle<'w> { task_widget: Option>, app_state: Res<'w, State>, } -impl<'w> WidgetSystem for InspectTasksToggle<'w> { +impl<'w> WidgetSystem for TaskPanelToggle<'w> { fn show(_: Tile, ui: &mut Ui, state: &mut SystemState, world: &mut World) -> () { let mut params = state.get_mut(world); if !matches!(params.app_state.get(), AppState::SiteEditor) { diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 861e2374c..f22d8a3fb 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -145,7 +145,7 @@ pub struct ViewTasks<'w, 's> { get_inclusion_modifier: GetModifier<'w, 's, Modifier>, get_params_modifier: GetModifier<'w, 's, Modifier>, icons: Res<'w, Icons>, - robots: Query<'w, 's, (Entity, &'static NameInSite), (With, Without)>, + robots: Query<'w, 's, (Entity, &'static NameInSite, &'static Robot), Without>, scenarios: Query< 'w, 's, @@ -285,7 +285,7 @@ fn show_task_widget( edit_mode: &mut EventWriter, edit_task: &Res, task_kinds: &ResMut, - robots: &Query<(Entity, &NameInSite), (With, Without)>, + robots: &Query<(Entity, &NameInSite, &Robot), Without>, scenario_count: i32, icons: &Res, ) { @@ -449,7 +449,7 @@ fn show_editable_task( scenario: Entity, in_edit_mode: bool, get_params_modifier: &GetModifier>, - robots: &Query<(Entity, &NameInSite), (With, Without)>, + robots: &Query<(Entity, &NameInSite, &Robot), Without>, task_kinds: &ResMut, change_task: &mut EventWriter>, update_task_modifier: &mut EventWriter>, @@ -664,7 +664,7 @@ fn show_create_task_dialog( edit_state: &mut SystemState<( Commands, GetModifier>, - Query<(Entity, &NameInSite), (With, Without)>, + Query<(Entity, &NameInSite, &Robot), Without>, ResMut, EventWriter>, EventWriter>, @@ -771,7 +771,7 @@ fn edit_request_type_widget( ui: &mut Ui, task: &mut Task, task_request: &TaskRequest, - robots: &Query<(Entity, &NameInSite), (With, Without)>, + robots: &Query<(Entity, &NameInSite, &Robot), Without>, robot: String, fleet: String, ) { @@ -813,12 +813,16 @@ fn edit_request_type_widget( ComboBox::from_id_salt("select_robot_for_task") .selected_text(selected_robot) .show_ui(ui, |ui| { - for (_, robot) in robots.iter() { + for (_, robot_name, robot) in robots.iter() { ui.selectable_value( robot_task_request.robot_mut(), - robot.0.clone(), - robot.0.clone(), + robot_name.0.clone(), + robot_name.0.clone(), ); + // Update fleet according to selected robot + if robot_task_request.robot() == robot_name.0 { + *robot_task_request.fleet_mut() = robot.fleet.clone(); + } } }); } else { From a0f90235dbbb49b33b354d3f5956f4acfa306d36 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Tue, 29 Jul 2025 07:04:04 +0000 Subject: [PATCH 08/21] Remove Grid to prevent glitch Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/widgets/tasks/mod.rs | 224 +++++++++--------- 1 file changed, 115 insertions(+), 109 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 003b82b96..2f7d25e0f 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -530,111 +530,111 @@ fn show_editable_task( .id_salt("task_details_".to_owned() + &task_entity.index().to_string()) .default_open(false) .show(ui, |ui| { - Grid::new("task_details_".to_owned() + &task_entity.index().to_string()) - .num_columns(2) - .show(ui, |ui| { - // Fleet name - if task.is_dispatch() { - ui.label("Fleet:").on_hover_text( - "(Optional) The name of the fleet for this robot. \ + // Fleet name + if task.is_dispatch() { + ui.horizontal(|ui| { + ui.label("Fleet:").on_hover_text( + "(Optional) The name of the fleet for this robot. \ If specified, other fleets will not bid for this task.", - ); - if !in_edit_mode { - ui.label(task_request.fleet_name().unwrap_or("None".to_string())); - } else { - edit_fleet_widget(ui, &mut new_task); - } - ui.end_row(); - } - - // Start time - // TODO(@xiyuoh) Add status/queued information - ui.label("Start time:") - .on_hover_text("(Optional) The earliest time that this task may start"); + ); if !in_edit_mode { - ui.label( - task_params - .start_time() - .map(|rt| format!("{:?}", rt)) - .unwrap_or("None".to_string()), - ); + ui.label(task_request.fleet_name().unwrap_or("None".to_string())); } else { - edit_start_time_widget(ui, &mut new_task_params); + edit_fleet_widget(ui, &mut new_task); } - ui.end_row(); + }); + } - // Request time - ui.label("Request time:") - .on_hover_text("(Optional) The time that this request was initiated"); - if !in_edit_mode { - ui.label( - task_params - .request_time() - .map(|rt| format!("{:?}", rt)) - .unwrap_or("None".to_string()), - ); - } else { - edit_request_time_widget(ui, &mut new_task_params); - } - ui.end_row(); + // Start time + // TODO(@xiyuoh) Add status/queued information + ui.horizontal(|ui| { + ui.label("Start time:") + .on_hover_text("(Optional) The earliest time that this task may start"); + if !in_edit_mode { + ui.label( + task_params + .start_time() + .map(|st| format!("{:?}", st)) + .unwrap_or("None".to_string()), + ); + } else { + edit_start_time_widget(ui, &mut new_task_params); + } + }); + + // Request time + ui.horizontal(|ui| { + ui.label("Request time:") + .on_hover_text("(Optional) The time that this request was initiated"); + if !in_edit_mode { + ui.label( + task_params + .request_time() + .map(|rt| format!("{:?}", rt)) + .unwrap_or("None".to_string()), + ); + } else { + edit_request_time_widget(ui, &mut new_task_params); + } + }); - // Priority - ui.label("Priority:").on_hover_text( - "(Optional) The priority of this task. \ + // Priority + ui.horizontal(|ui| { + ui.label("Priority:").on_hover_text( + "(Optional) The priority of this task. \ This must match a priority schema supported by a fleet.", + ); + if !in_edit_mode { + ui.label( + task_params + .priority() + .map(|p| p.to_string()) + .unwrap_or("None".to_string()), ); - if !in_edit_mode { - ui.label( - task_params - .priority() - .map(|st| st.to_string()) - .unwrap_or("None".to_string()), - ); - } else { - edit_priority_widget(ui, &mut new_task_params); - } - ui.end_row(); + } else { + edit_priority_widget(ui, &mut new_task_params); + } + }); - // Labels - ui.label("Labels:").on_hover_text( - "Labels to describe the purpose of the task dispatch request, \ + // Labels + ui.horizontal(|ui| { + ui.label("Labels:").on_hover_text( + "Labels to describe the purpose of the task dispatch request, \ items can be a single value like `dashboard` or a key-value pair \ like `app=dashboard`, in the case of a single value, it will be \ interpreted as a key-value pair with an empty string value.", - ); - if !in_edit_mode { - ui.label(format!("{:?}", task_params.labels())); - } else { - edit_labels_widget(ui, &mut new_task_params); - } - ui.end_row(); + ); + if !in_edit_mode { + ui.label(format!("{:?}", task_params.labels())); + } else { + edit_labels_widget(ui, &mut new_task_params); + } + }); - // Reset task parameters to parent scenario params (if any) - if let Ok((scenario_modifiers, parent_scenario)) = - get_params_modifier.scenarios.get(scenario) + // Reset task parameters to parent scenario params (if any) + if let Ok((scenario_modifiers, parent_scenario)) = + get_params_modifier.scenarios.get(scenario) + { + if scenario_modifiers + .get(&task_entity) + .is_some_and(|e| get_params_modifier.modifiers.get(*e).is_ok()) + && parent_scenario.0.is_some() + { + // Only display reset button if this task has a TaskParams modifier + // and this is not a root scenario + if ui + .button("Reset Task Params") + .on_hover_text("Reset task parameters to parent scenario params") + .clicked() { - if scenario_modifiers - .get(&task_entity) - .is_some_and(|e| get_params_modifier.modifiers.get(*e).is_ok()) - && parent_scenario.0.is_some() - { - // Only display reset button if this task has a TaskParams modifier - // and this is not a root scenario - if ui - .button("Reset Task Params") - .on_hover_text("Reset task parameters to parent scenario params") - .clicked() - { - update_task_modifier.write(UpdateModifier::new( - scenario, - task_entity, - UpdateTaskModifier::ResetParams, - )); - } - ui.end_row(); - } + update_task_modifier.write(UpdateModifier::new( + scenario, + task_entity, + UpdateTaskModifier::ResetParams, + )); } - }); + } + } }); // Trigger appropriate events if changes have been made in edit mode @@ -876,7 +876,9 @@ fn edit_requester_widget(ui: &mut Ui, task: &mut Task) { let requester = new_task_request .requester_mut() .get_or_insert(String::new()); - ui.text_edit_singleline(requester); + TextEdit::singleline(requester) + .desired_width(ui.available_width()) + .show(ui); if requester.is_empty() { *new_task_request.requester_mut() = None; } @@ -888,7 +890,9 @@ fn edit_fleet_widget(ui: &mut Ui, task: &mut Task) { let fleet_name = new_task_request .fleet_name_mut() .get_or_insert(String::new()); - ui.text_edit_singleline(fleet_name); + TextEdit::singleline(fleet_name) + .desired_width(ui.available_width()) + .show(ui); if fleet_name.is_empty() { *new_task_request.fleet_name_mut() = None; } @@ -947,25 +951,27 @@ fn edit_priority_widget(ui: &mut Ui, task_params: &mut TaskParams) { fn edit_labels_widget(ui: &mut Ui, task_params: &mut TaskParams) { let mut remove_labels = Vec::new(); let mut id: usize = 0; - for label in task_params.labels_mut() { - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { - if ui.button("❌").on_hover_text("Remove label").clicked() { - remove_labels.push(id.clone()); + ui.vertical(|ui| { + for label in task_params.labels_mut() { + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui.button("❌").on_hover_text("Remove label").clicked() { + remove_labels.push(id.clone()); + } + TextEdit::singleline(label) + .desired_width(ui.available_width()) + .show(ui); + }); + id += 1; + } + ui.with_layout(Layout::right_to_left(Align::Max), |ui| { + if ui + .button("Add label") + .on_hover_text("Insert new label") + .clicked() + { + task_params.labels_mut().push(String::new()); } - ui.text_edit_singleline(label); }); - id += 1; - ui.end_row(); - ui.label(""); - } - ui.with_layout(Layout::right_to_left(Align::Max), |ui| { - if ui - .button("Add label") - .on_hover_text("Insert new label") - .clicked() - { - task_params.labels_mut().push(String::new()); - } }); for i in remove_labels.drain(..).rev() { task_params.labels_mut().remove(i); From 49759766c0c84063f49909f94781f0e734fb3908 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Mon, 25 Aug 2025 08:38:14 +0000 Subject: [PATCH 09/21] Update test site Signed-off-by: Xiyu Oh --- assets/demo_maps/test.site.json | 1 + crates/rmf_site_format/src/robot.rs | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/assets/demo_maps/test.site.json b/assets/demo_maps/test.site.json index 9bfe9d03d..115b3feda 100644 --- a/assets/demo_maps/test.site.json +++ b/assets/demo_maps/test.site.json @@ -1013,6 +1013,7 @@ }, "robots": { "52": { + "fleet": "HospitalRobot", "properties": { "Collision": { "config": { diff --git a/crates/rmf_site_format/src/robot.rs b/crates/rmf_site_format/src/robot.rs index 0360bde9c..6b34b2c77 100644 --- a/crates/rmf_site_format/src/robot.rs +++ b/crates/rmf_site_format/src/robot.rs @@ -33,7 +33,7 @@ pub struct Robot { impl Default for Robot { fn default() -> Self { Self { - fleet: String::new(), + fleet: "".to_string(), properties: BTreeMap::new(), } } From c23dffa010e8ee30afc766df7fcf4197febedf00 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Mon, 1 Sep 2025 06:44:44 +0000 Subject: [PATCH 10/21] Check if TaskKind is valid Signed-off-by: Xiyu Oh --- crates/rmf_site_editor/src/site/task.rs | 3 ++- .../src/widgets/tasks/go_to_place.rs | 10 ++++++++ .../rmf_site_editor/src/widgets/tasks/mod.rs | 24 +++++++++++++++---- .../src/widgets/tasks/wait_for.rs | 1 + 4 files changed, 33 insertions(+), 5 deletions(-) diff --git a/crates/rmf_site_editor/src/site/task.rs b/crates/rmf_site_editor/src/site/task.rs index 62f2b657f..29ca55e5a 100644 --- a/crates/rmf_site_editor/src/site/task.rs +++ b/crates/rmf_site_editor/src/site/task.rs @@ -21,9 +21,10 @@ use std::collections::HashMap; pub type InsertTaskKindFn = fn(EntityCommands); pub type RemoveTaskKindFn = fn(EntityCommands); +pub type IsTaskValidFn = fn(Entity, &World) -> bool; #[derive(Resource)] -pub struct TaskKinds(pub HashMap); +pub struct TaskKinds(pub HashMap); impl FromWorld for TaskKinds { fn from_world(_world: &mut World) -> Self { diff --git a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs index 60c406e91..465ed6a89 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs @@ -44,6 +44,16 @@ impl Plugin for GoToPlacePlugin { |mut e_cmd| { e_cmd.remove::(); }, + |e, world| { + let Some(go_to_place) = world.entity(e).get::() else { + return false; + }; + if go_to_place.location.is_empty() { + return false; + } + // TODO(@xiyuoh) check if location is valid via entity + return true; + }, ), ); let widget = Widget::::new::(&mut app.world_mut()); diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index b5af30533..6066c0944 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -691,6 +691,13 @@ fn show_create_task_dialog( &task_kinds, &mut change_task, ); + let task_request_category = pending_task.request().category(); + let task_kind_is_valid = + if let Some((_, _, valid_fn)) = task_kinds.0.get(&task_request_category) { + Some(valid_fn.clone()) + } else { + None + }; edit_state.apply(world); ui.separator(); @@ -716,8 +723,13 @@ fn show_create_task_dialog( if ui.button("Cancel").clicked() { reset_edit = true; } - ui.add_enabled_ui(pending_task.is_valid(), |ui| { - // TODO(@xiyuoh) Also check validity of TaskKind (e.g. GoToPlace) + let task_is_valid = if let Some(task_kind_is_valid) = task_kind_is_valid { + pending_task.is_valid() && task_kind_is_valid(task_entity, world) + } else { + // If task kind valid fn cannot be retrieved, task is invalid + false + }; + ui.add_enabled_ui(task_is_valid, |ui| { if ui .button("Add Task") .on_hover_text("Add this task to the current scenario") @@ -835,10 +847,14 @@ fn edit_task_kind_widget( // Insert selected TaskKind component let new_category = task.request().category(); if new_category != current_category { - if let Some(remove_fn) = task_kinds.0.get(¤t_category).map(|(_, rm_fn)| rm_fn) { + if let Some(remove_fn) = task_kinds + .0 + .get(¤t_category) + .map(|(_, rm_fn, _)| rm_fn) + { remove_fn(commands.entity(task_entity)); } - if let Some(insert_fn) = task_kinds.0.get(&new_category).map(|(is_fn, _)| is_fn) { + if let Some(insert_fn) = task_kinds.0.get(&new_category).map(|(is_fn, _, _)| is_fn) { insert_fn(commands.entity(task_entity)); } } diff --git a/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs b/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs index ad623b8ac..f62908e36 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs @@ -44,6 +44,7 @@ impl Plugin for WaitForPlugin { |mut e_cmd| { e_cmd.remove::(); }, + |e, world| world.entity(e).get::().is_some(), ), ); let widget = Widget::::new::(&mut app.world_mut()); From f129a4a9f1e9c38eb4e8fb50ff0be7dd61fc253f Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Mon, 1 Sep 2025 07:54:32 +0000 Subject: [PATCH 11/21] In-place editing for task kind Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/widgets/tasks/mod.rs | 367 ++++++++++-------- 1 file changed, 196 insertions(+), 171 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 6066c0944..e4601dbe2 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -37,6 +37,7 @@ use bevy_egui::{ use rmf_site_egui::*; use serde_json::Value; use smallvec::SmallVec; +use std::collections::BTreeMap; pub mod go_to_place; pub use go_to_place::*; @@ -175,38 +176,18 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { ) { ui.label(RichText::new("Tasks").size(18.0)); ui.add_space(10.0); - let mut params = state.get_mut(world); - params.show_widget(ui); + let params = state.get_mut(world); - // Display children widgets if editing existing task - if params - .edit_task - .0 - .is_some_and(|e| params.tasks.get(e).is_ok()) - { - let children: Result, _> = params - .children - .get(params.task_widget.id) - .map(|children| children.iter().collect()); - let Ok(children) = children else { - return; - }; - - for child in children { - let tile = Tile { id, panel }; - let _ = world.try_show_in(child, tile, ui); - } - } - ui.add_space(10.0); - } -} - -impl<'w, 's> ViewTasks<'w, 's> { - pub fn show_widget(&mut self, ui: &mut Ui) { - let Some(current_scenario_entity) = self.current_scenario.0 else { + let Some(current_scenario_entity) = params.current_scenario.0 else { ui.label("No scenario selected, unable to display or create tasks."); return; }; + // TODO(@xiyuoh) sort tasks by request time/start time/created time + let mut tasks = BTreeMap::::new(); + for (e, task) in params.tasks.iter() { + tasks.insert(e, task.clone()); + } + // View and modify tasks in current scenario Frame::default() .inner_margin(4.0) @@ -214,7 +195,7 @@ impl<'w, 's> ViewTasks<'w, 's> { .stroke(Stroke::new(1.0, Color32::GRAY)) .show(ui, |ui| { ui.set_min_width(ui.available_width()); - if self.tasks.is_empty() { + if tasks.is_empty() { ui.with_layout(Layout::top_down_justified(Align::LEFT), |ui| { ui.label("No tasks in this scenario"); }); @@ -224,28 +205,15 @@ impl<'w, 's> ViewTasks<'w, 's> { .max_height(max_height) .auto_shrink([false, false]) .show(ui, |ui| { - for (task_entity, task) in self.tasks.iter() { - let scenario_count = count_scenarios_with_inclusion( - &self.scenarios, - task_entity, - &self.get_inclusion_modifier, - ); + for (task_entity, task) in tasks.iter() { show_task_widget( ui, - &mut self.commands, - task_entity, - task, + Tile { id, panel }, + world, + state, current_scenario_entity, - &self.get_inclusion_modifier, - &self.get_params_modifier, - &mut self.change_task, - &mut self.delete, - &mut self.edit_mode, - &self.edit_task, - &mut self.task_kinds, - &self.robots, - scenario_count, - &self.icons, + *task_entity, + task, ); } }); @@ -255,9 +223,10 @@ impl<'w, 's> ViewTasks<'w, 's> { ui.separator(); ui.add_space(10.0); - if self.edit_task.0.is_none() { + let mut params = state.get_mut(world); + if params.edit_task.0.is_none() { if ui.button("✚ Create New Task").clicked() { - let new_task = self + let new_task = params .commands .spawn(Task::default()) .insert(Category::Task) @@ -265,34 +234,28 @@ impl<'w, 's> ViewTasks<'w, 's> { .insert(Inclusion::Included) // New tasks created are included by default .insert(Pending) .id(); - self.edit_mode.write(EditModeEvent { + params.edit_mode.write(EditModeEvent { scenario: current_scenario_entity, mode: EditMode::New(new_task), }); } } + ui.add_space(10.0); } } -/// Displays the task data and params, and allows users to enter edit mode to modify values fn show_task_widget( ui: &mut Ui, - commands: &mut Commands, + Tile { id, panel }: Tile, + world: &mut World, + state: &mut SystemState, + scenario: Entity, task_entity: Entity, task: &Task, - scenario: Entity, - get_inclusion_modifier: &GetModifier>, - get_params_modifier: &GetModifier>, - change_task: &mut EventWriter>, - delete: &mut EventWriter, - edit_mode: &mut EventWriter, - edit_task: &Res, - task_kinds: &ResMut, - robots: &Query<(Entity, &NameInSite, &Robot), Without>, - scenario_count: i32, - icons: &Res, ) { - let present = get_inclusion_modifier + let params = state.get_mut(world); + let present = params + .get_inclusion_modifier .get(scenario, task_entity) .map(|i_modifier| **i_modifier == Inclusion::Included) .unwrap_or(false); @@ -301,7 +264,7 @@ fn show_task_widget( } else { Color32::default() }; - let in_edit_mode = edit_task.0.is_some_and(|e| e == task_entity); + Frame::default() .inner_margin(4.0) .fill(color) @@ -309,124 +272,186 @@ fn show_task_widget( .show(ui, |ui| { ui.set_min_width(ui.available_width()); - ui.horizontal(|ui| { - ui.label("Task ".to_owned() + &task_entity.index().to_string()) // TODO(@xiyuoh) better identifier - .on_hover_text(format!("Task is included in {} scenarios", scenario_count)); - ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + let mut params = state.get_mut(world); + + let in_edit_mode = params.edit_task.0.is_some_and(|e| e == task_entity); + let scenario_count = count_scenarios_with_inclusion( + ¶ms.scenarios, + task_entity, + ¶ms.get_inclusion_modifier, + ); + show_task_params( + ui, + &mut params.commands, + task_entity, + task, + scenario, + ¶ms.get_inclusion_modifier, + ¶ms.get_params_modifier, + &mut params.change_task, + &mut params.delete, + &mut params.edit_mode, + &mut params.task_kinds, + ¶ms.robots, + scenario_count, + ¶ms.icons, + present, + in_edit_mode, + ); + + // Display children widgets if editing existing task + if in_edit_mode { + ui.separator(); + let children: Result, _> = params + .children + .get(params.task_widget.id) + .map(|children| children.iter().collect()); + let Ok(children) = children else { + return; + }; + + for child in children { + let tile = Tile { id, panel }; + let _ = world.try_show_in(child, tile, ui); + } + } + }); +} + +/// Displays the task data and params, and allows users to enter edit mode to modify values +fn show_task_params( + ui: &mut Ui, + commands: &mut Commands, + task_entity: Entity, + task: &Task, + scenario: Entity, + get_inclusion_modifier: &GetModifier>, + get_params_modifier: &GetModifier>, + change_task: &mut EventWriter>, + delete: &mut EventWriter, + edit_mode: &mut EventWriter, + task_kinds: &ResMut, + robots: &Query<(Entity, &NameInSite, &Robot), Without>, + scenario_count: i32, + icons: &Res, + present: bool, + in_edit_mode: bool, +) { + ui.horizontal(|ui| { + ui.label("Task ".to_owned() + &task_entity.index().to_string()) // TODO(@xiyuoh) better identifier + .on_hover_text(format!("Task is included in {} scenarios", scenario_count)); + ui.with_layout(Layout::right_to_left(Align::Center), |ui| { + if ui + .add(ImageButton::new(icons.trash.egui())) + .on_hover_text("Remove task from all scenarios") + .clicked() + { + delete.write(Delete::new(task_entity)); + } + // Include/hide task + // Toggle between 3 inclusion modes: Included -> None (inherit from parent) -> Hidden + // If this is a root scenario, we won't include the None option + let inclusion_modifier = get_inclusion_modifier + .scenarios + .get(scenario) + .ok() + .and_then(|(scenario_modifiers, _)| scenario_modifiers.get(&task_entity)) + .and_then(|e| get_inclusion_modifier.modifiers.get(*e).ok()); + if let Some(inclusion_modifier) = inclusion_modifier { + // Either explicitly included or hidden + if **inclusion_modifier == Inclusion::Hidden { if ui - .add(ImageButton::new(icons.trash.egui())) - .on_hover_text("Remove task from all scenarios") + .add(ImageButton::new(icons.hide.egui())) + .on_hover_text("Task is hidden in this scenario") .clicked() { - delete.write(Delete::new(task_entity)); + commands.entity(task_entity).insert(Inclusion::Included); } - // Include/hide task - // Toggle between 3 inclusion modes: Included -> None (inherit from parent) -> Hidden - // If this is a root scenario, we won't include the None option - let inclusion_modifier = get_inclusion_modifier - .scenarios - .get(scenario) - .ok() - .and_then(|(scenario_modifiers, _)| scenario_modifiers.get(&task_entity)) - .and_then(|e| get_inclusion_modifier.modifiers.get(*e).ok()); - if let Some(inclusion_modifier) = inclusion_modifier { - // Either explicitly included or hidden - if **inclusion_modifier == Inclusion::Hidden { - if ui - .add(ImageButton::new(icons.hide.egui())) - .on_hover_text("Task is hidden in this scenario") - .clicked() - { - commands.entity(task_entity).insert(Inclusion::Included); - } - } else { - if ui - .add(ImageButton::new(icons.show.egui())) - .on_hover_text("Task is included in this scenario") - .clicked() - { - if get_inclusion_modifier - .scenarios - .get(scenario) - .is_ok_and(|(_, a)| a.0.is_some()) - { - // If parent scenario exists, clicking this button toggles to ResetInclusion - commands.trigger(UpdateModifier::::reset( - scenario, - task_entity, - )); - } else { - // Otherwise, toggle to Hidden - commands.entity(task_entity).insert(Inclusion::Hidden); - } - } - } - } else { - // Modifier is inherited - if ui - .add(ImageButton::new(icons.link.egui())) - .on_hover_text("Task inclusion is inherited in this scenario") - .clicked() + } else { + if ui + .add(ImageButton::new(icons.show.egui())) + .on_hover_text("Task is included in this scenario") + .clicked() + { + if get_inclusion_modifier + .scenarios + .get(scenario) + .is_ok_and(|(_, a)| a.0.is_some()) { + // If parent scenario exists, clicking this button toggles to ResetInclusion + commands + .trigger(UpdateModifier::::reset(scenario, task_entity)); + } else { + // Otherwise, toggle to Hidden commands.entity(task_entity).insert(Inclusion::Hidden); } } + } + } else { + // Modifier is inherited + if ui + .add(ImageButton::new(icons.link.egui())) + .on_hover_text("Task inclusion is inherited in this scenario") + .clicked() + { + commands.entity(task_entity).insert(Inclusion::Hidden); + } + } - if !in_edit_mode { - if present { - // Do not allow edit if not in current scenario - if ui - .add(ImageButton::new(icons.edit.egui())) - .on_hover_text("Edit task parameters") - .clicked() - { - edit_mode.write(EditModeEvent { - scenario: scenario, - mode: EditMode::Edit(Some(task_entity)), - }); - } - } - } else { - // Exit edit mode - if ui - .add(ImageButton::new(icons.confirm.egui())) - .on_hover_text("Exit edit mode") - .clicked() - { - edit_mode.write(EditModeEvent { - scenario: scenario, - mode: EditMode::Edit(None), - }); - } + if !in_edit_mode { + if present { + // Do not allow edit if not in current scenario + if ui + .add(ImageButton::new(icons.edit.egui())) + .on_hover_text("Edit task parameters") + .clicked() + { + edit_mode.write(EditModeEvent { + scenario: scenario, + mode: EditMode::Edit(Some(task_entity)), + }); } - }); - }); - if !present { - return; + } + } else { + // Exit edit mode + if ui + .add(ImageButton::new(icons.confirm.egui())) + .on_hover_text("Exit edit mode") + .clicked() + { + edit_mode.write(EditModeEvent { + scenario: scenario, + mode: EditMode::Edit(None), + }); + } } - ui.separator(); + }); + }); + if !present { + return; + } + ui.separator(); - let Some(task_params) = get_params_modifier - .get(scenario, task_entity) - .map(|m| (**m).clone()) - else { - return; - }; + let Some(task_params) = get_params_modifier + .get(scenario, task_entity) + .map(|m| (**m).clone()) + else { + return; + }; - show_editable_task( - ui, - commands, - task_entity, - task, - &task_params, - scenario, - in_edit_mode, - get_params_modifier, - robots, - task_kinds, - change_task, - ); - }); + show_editable_task( + ui, + commands, + task_entity, + task, + &task_params, + scenario, + in_edit_mode, + get_params_modifier, + robots, + task_kinds, + change_task, + ); } fn show_editable_task( From a2390aab543220b8a2aa2fc61ba652184dc29c7b Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Tue, 2 Sep 2025 05:49:14 +0000 Subject: [PATCH 12/21] Implement time sorting for tasks Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/widgets/tasks/mod.rs | 34 ++++++++++++++++--- crates/rmf_site_format/src/task.rs | 19 ++++++++++- 2 files changed, 47 insertions(+), 6 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index e4601dbe2..9354dec64 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -37,7 +37,6 @@ use bevy_egui::{ use rmf_site_egui::*; use serde_json::Value; use smallvec::SmallVec; -use std::collections::BTreeMap; pub mod go_to_place; pub use go_to_place::*; @@ -182,11 +181,36 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { ui.label("No scenario selected, unable to display or create tasks."); return; }; - // TODO(@xiyuoh) sort tasks by request time/start time/created time - let mut tasks = BTreeMap::::new(); + // Tasks are sorted by start time, then request time, then created time, + // depending on which fields are populated + let mut tasks = Vec::<(i32, (Entity, Task))>::new(); + let mut tasks_without_time = Vec::<(i32, (Entity, Task))>::new(); + for (e, task) in params.tasks.iter() { - tasks.insert(e, task.clone()); + if let Some(params_modifier) = + params.get_params_modifier.get(current_scenario_entity, e) + { + if let Some(start_time) = params_modifier.start_time() { + tasks.push((start_time, (e, task.clone()))); + continue; + } + if let Some(request_time) = params_modifier.request_time() { + tasks.push((request_time, (e, task.clone()))); + continue; + } + } + if let Some(created_time) = task.request().created_time() { + tasks.push((created_time, (e, task.clone()))); + continue; + } + // We should not reach here as created_time is populated by default, + // but in case it comes up as None we sort these by entity index and + // place them at the end of the task list + tasks_without_time.push((e.index() as i32, (e, task.clone()))); } + tasks.sort_by(|a, b| a.0.cmp(&b.0)); + tasks_without_time.sort_by(|a, b| a.0.cmp(&b.0)); + tasks.extend(tasks_without_time); // View and modify tasks in current scenario Frame::default() @@ -205,7 +229,7 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { .max_height(max_height) .auto_shrink([false, false]) .show(ui, |ui| { - for (task_entity, task) in tasks.iter() { + for (_, (task_entity, task)) in tasks.iter() { show_task_widget( ui, Tile { id, panel }, diff --git a/crates/rmf_site_format/src/task.rs b/crates/rmf_site_format/src/task.rs index d788161a9..2f20f1323 100644 --- a/crates/rmf_site_format/src/task.rs +++ b/crates/rmf_site_format/src/task.rs @@ -19,7 +19,10 @@ use crate::*; #[cfg(feature = "bevy")] use bevy::prelude::{Component, Reflect}; use serde::{de::DeserializeOwned, Deserialize, Serialize}; -use std::fmt; +use std::{ + fmt, + time::{SystemTime, UNIX_EPOCH}, +}; #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component))] @@ -79,6 +82,8 @@ pub struct TaskRequest { pub requester: Option, #[serde(default, skip_serializing_if = "is_default")] pub fleet_name: Option, + #[serde(default, skip_serializing_if = "is_default")] + pub created_time: Option, } impl Default for TaskRequest { @@ -89,6 +94,10 @@ impl Default for TaskRequest { description_display: None, requester: None, fleet_name: None, + created_time: SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as i32) + .ok(), } } } @@ -140,6 +149,14 @@ impl TaskRequest { pub fn fleet_name_mut(&mut self) -> &mut Option { &mut self.fleet_name } + + pub fn created_time(&self) -> Option { + self.created_time.clone() + } + + pub fn created_time_mut(&mut self) -> &mut Option { + &mut self.created_time + } } pub trait TaskRequestType { From 73dc9a3917a74603a81ae1ba9fdcb65e125f1ac0 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Tue, 2 Sep 2025 07:28:24 +0000 Subject: [PATCH 13/21] Fix pending tasks TaskParams not saved Signed-off-by: Xiyu Oh --- crates/rmf_site_editor/src/widgets/tasks/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 9354dec64..d6c3b842f 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -784,7 +784,12 @@ fn show_create_task_dialog( .on_hover_text("Add this task to the current scenario") .clicked() { - world.entity_mut(task_entity).remove::(); + // Update TaskParams modifier after removing Pending to + // for changes to take effect + world + .entity_mut(task_entity) + .remove::() + .insert(pending_task_params); reset_edit = true; } }); From 8c278e20f6c3db6544e7c098204a2bf0f38f6aca Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Tue, 2 Sep 2025 16:49:15 +0000 Subject: [PATCH 14/21] Use Point instead of String for GoToPlace locations Signed-off-by: Xiyu Oh --- .../src/mapf_rse/config_widget.rs | 2 +- .../src/mapf_rse/negotiation/mod.rs | 11 ++-- crates/rmf_site_editor/src/site/task.rs | 2 +- .../src/widgets/tasks/go_to_place.rs | 54 +++++++++++-------- .../rmf_site_editor/src/widgets/tasks/mod.rs | 1 - crates/rmf_site_format/src/task.rs | 18 ++----- 6 files changed, 46 insertions(+), 42 deletions(-) diff --git a/crates/rmf_site_editor/src/mapf_rse/config_widget.rs b/crates/rmf_site_editor/src/mapf_rse/config_widget.rs index 6aa74e861..27df28373 100644 --- a/crates/rmf_site_editor/src/mapf_rse/config_widget.rs +++ b/crates/rmf_site_editor/src/mapf_rse/config_widget.rs @@ -80,7 +80,7 @@ impl<'w, 's> MapfConfigWidget<'w, 's> { .tasks .iter() .filter(|task| { - if task.request().category() == GoToPlace::label() { + if task.request().category() == GoToPlace::::label() { true } else { false diff --git a/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs b/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs index 3ef17521e..584093f84 100644 --- a/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs +++ b/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs @@ -225,7 +225,7 @@ pub fn handle_compute_negotiation_complete( } pub fn start_compute_negotiation( - locations: Query<(&NameInSite, &Point), With>, + locations: Query<&Point, With>, anchors: Query<&GlobalTransform>, negotiation_request: EventReader, negotiation_params: Res, @@ -235,7 +235,7 @@ pub fn start_compute_negotiation( child_of: Query<&ChildOf>, robots: Query<(Entity, &NameInSite, &Pose, &Affiliation), With>, robot_descriptions: Query<(&DifferentialDrive, &CircleCollision)>, - tasks: Query<(&RobotTask, &GoToPlace)>, + tasks: Query<(&RobotTask, &GoToPlace)>, mut negotiation_task: ResMut, ) { if negotiation_request.len() == 0 { @@ -291,8 +291,11 @@ pub fn start_compute_negotiation( for (robot_entity, robot_site_name, robot_pose, robot_group) in robots.iter() { if robot_name == robot_site_name.0 { // Match location to entity - for (location_name, Point(anchor_entity)) in locations.iter() { - if location_name.0 == go_to_place.location { + for Point(anchor_entity) in locations.iter() { + if go_to_place + .location + .is_some_and(|pt| pt.0 == *anchor_entity) + { let Ok(goal_transform) = anchors.get(*anchor_entity) else { warn!("Unable to get robot's goal transform"); continue; diff --git a/crates/rmf_site_editor/src/site/task.rs b/crates/rmf_site_editor/src/site/task.rs index 29ca55e5a..a797c1434 100644 --- a/crates/rmf_site_editor/src/site/task.rs +++ b/crates/rmf_site_editor/src/site/task.rs @@ -21,7 +21,7 @@ use std::collections::HashMap; pub type InsertTaskKindFn = fn(EntityCommands); pub type RemoveTaskKindFn = fn(EntityCommands); -pub type IsTaskValidFn = fn(Entity, &World) -> bool; +pub type IsTaskValidFn = fn(Entity, &mut World) -> bool; #[derive(Resource)] pub struct TaskKinds(pub HashMap); diff --git a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs index 465ed6a89..8c781a0d5 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs @@ -16,7 +16,9 @@ */ use super::{EditTask, TaskWidget}; use crate::{ - site::{update_task_kind_component, LocationTags, NameInSite, Task, TaskKind, TaskKinds}, + site::{ + update_task_kind_component, LocationTags, NameInSite, Point, Task, TaskKind, TaskKinds, + }, widgets::prelude::*, }; use bevy::{ @@ -36,38 +38,43 @@ pub struct GoToPlacePlugin {} impl Plugin for GoToPlacePlugin { fn build(&self, app: &mut App) { app.world_mut().resource_mut::().0.insert( - GoToPlace::label(), + GoToPlace::::label(), ( |mut e_cmd| { - e_cmd.insert(GoToPlace::default()); + e_cmd.insert(GoToPlace::::default()); }, |mut e_cmd| { - e_cmd.remove::(); + e_cmd.remove::>(); }, |e, world| { - let Some(go_to_place) = world.entity(e).get::() else { + let Some(loc_entity) = world + .entity(e) + .get::>() + .and_then(|go_to_place| go_to_place.location) + .map(|pt| pt.0) + else { return false; }; - if go_to_place.location.is_empty() { - return false; - } - // TODO(@xiyuoh) check if location is valid via entity - return true; + let mut state: SystemState>> = + SystemState::new(world); + let locations = state.get(world); + + locations.get(loc_entity).is_ok() }, ), ); let widget = Widget::::new::(&mut app.world_mut()); let task_widget = app.world().resource::().get(); app.world_mut().spawn(widget).insert(ChildOf(task_widget)); - app.add_systems(PostUpdate, update_task_kind_component::); + app.add_systems(PostUpdate, update_task_kind_component::>); } } #[derive(SystemParam)] pub struct ViewGoToPlace<'w, 's> { - locations: Query<'w, 's, &'static NameInSite, With>, + locations: Query<'w, 's, (Entity, &'static NameInSite), With>, edit_task: Res<'w, EditTask>, - tasks: Query<'w, 's, (&'static mut GoToPlace, &'static mut Task)>, + tasks: Query<'w, 's, (&'static mut GoToPlace, &'static mut Task)>, } impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { @@ -84,12 +91,13 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { return; } - let selected_location_name = if go_to_place.location.is_empty() - || !params.locations.iter().any(|l| l.0 == go_to_place.location) + let selected_location_name = if let Some((_, loc_name)) = go_to_place + .location + .and_then(|pt| params.locations.get(pt.0).ok()) { - "Select Location".to_string() + loc_name.0.clone() } else { - go_to_place.location.clone() + "Select Location".to_string() }; let mut new_go_to_place = go_to_place.clone(); @@ -98,11 +106,11 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { ComboBox::from_id_salt("select_go_to_location") .selected_text(selected_location_name) .show_ui(ui, |ui| { - for location_name in params.locations.iter() { + for (loc_entity, loc_name) in params.locations.iter() { ui.selectable_value( &mut new_go_to_place.location, - location_name.0.clone(), - location_name.0.clone(), + Some(Point(loc_entity)), + loc_name.0.clone(), ); } }); @@ -113,8 +121,10 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { if let Ok(description) = serde_json::to_value(new_go_to_place.clone()) { *task.request_mut().description_mut() = description; - *task.request_mut().description_display_mut() = - Some(format!("{}", new_go_to_place.clone())); + *task.request_mut().description_display_mut() = new_go_to_place + .location + .and_then(|pt| params.locations.get(pt.0).ok()) + .map(|(_, name)| name.0.clone()); } } } diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index d6c3b842f..80ba220f8 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -1031,7 +1031,6 @@ pub fn handle_task_edit( pending_tasks: Query<&mut Task, With>, current_workspace: Res, ) { - // TODO(@xiyuoh) fix bug where the egui panel glitches when the EditTask resource is being accessed if let Some(edit) = edit_mode.read().last() { match edit.mode { EditMode::New(task_entity) => { diff --git a/crates/rmf_site_format/src/task.rs b/crates/rmf_site_format/src/task.rs index 2f20f1323..5398470d0 100644 --- a/crates/rmf_site_format/src/task.rs +++ b/crates/rmf_site_format/src/task.rs @@ -333,26 +333,18 @@ pub trait TaskKind: Component + Serialize + DeserializeOwned { // Supported Task kinds #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component, Reflect))] -pub struct GoToPlace { - pub location: String, +pub struct GoToPlace { + pub location: Option>, } -impl Default for GoToPlace { +impl Default for GoToPlace { fn default() -> Self { - Self { - location: String::new(), - } - } -} - -impl fmt::Display for GoToPlace { - fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result { - write!(f, "{}", self.location) + Self { location: None } } } #[cfg(feature = "bevy")] -impl TaskKind for GoToPlace { +impl TaskKind for GoToPlace { fn label() -> String { "Go To Place".to_string() } From 49460836f7f4c56550a765885a80091e97e394f8 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 3 Sep 2025 02:45:05 +0000 Subject: [PATCH 15/21] Task-based visual cue for GoToPlace Signed-off-by: Xiyu Oh --- .../rmf_site_editor/src/interaction/model.rs | 1 - .../src/widgets/tasks/go_to_place.rs | 29 ++++++++++++++----- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/crates/rmf_site_editor/src/interaction/model.rs b/crates/rmf_site_editor/src/interaction/model.rs index 13d7cbe81..665308c80 100644 --- a/crates/rmf_site_editor/src/interaction/model.rs +++ b/crates/rmf_site_editor/src/interaction/model.rs @@ -55,6 +55,5 @@ pub fn update_model_instance_visual_cues( } } } - // TODO(@xiyuoh) support task-based visual cues } } diff --git a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs index 8c781a0d5..427fb37fd 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs @@ -28,9 +28,10 @@ use bevy::{ }, prelude::*, }; -use bevy_egui::egui::ComboBox; +use bevy_egui::egui::{ComboBox, SelectableLabel}; use rmf_site_egui::*; use rmf_site_format::GoToPlace; +use rmf_site_picking::Hover; #[derive(Default)] pub struct GoToPlacePlugin {} @@ -75,6 +76,7 @@ pub struct ViewGoToPlace<'w, 's> { locations: Query<'w, 's, (Entity, &'static NameInSite), With>, edit_task: Res<'w, EditTask>, tasks: Query<'w, 's, (&'static mut GoToPlace, &'static mut Task)>, + hover: EventWriter<'w, Hover>, } impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { @@ -106,12 +108,25 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { ComboBox::from_id_salt("select_go_to_location") .selected_text(selected_location_name) .show_ui(ui, |ui| { - for (loc_entity, loc_name) in params.locations.iter() { - ui.selectable_value( - &mut new_go_to_place.location, - Some(Point(loc_entity)), - loc_name.0.clone(), - ); + // Sort locations alphabetically + let mut locations = params.locations.iter().fold( + Vec::<(Entity, String)>::new(), + |mut l, (e, name)| { + l.push((e, name.0.clone())); + l + }, + ); + locations.sort_by(|a, b| a.1.cmp(&b.1)); + for (loc_entity, loc_name) in locations.iter() { + let resp = ui.add(SelectableLabel::new( + new_go_to_place.location == Some(Point(*loc_entity)), + loc_name.clone(), + )); + if resp.clicked() { + new_go_to_place.location = Some(Point(*loc_entity)); + } else if resp.hovered() { + params.hover.write(Hover(Some(*loc_entity))); + } } }); }); From f67cc3b127d8f98eda81251b6d6888a674b8665c Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 3 Sep 2025 04:25:43 +0000 Subject: [PATCH 16/21] Use entities instead of name for Robot Signed-off-by: Xiyu Oh --- .../src/mapf_rse/config_widget.rs | 2 +- .../src/mapf_rse/negotiation/mod.rs | 12 +-- crates/rmf_site_editor/src/site/mod.rs | 6 +- crates/rmf_site_editor/src/site/save.rs | 38 +++++-- crates/rmf_site_editor/src/site/task.rs | 4 +- .../src/widgets/tasks/go_to_place.rs | 8 +- .../rmf_site_editor/src/widgets/tasks/mod.rs | 100 ++++++++++++------ .../src/widgets/tasks/wait_for.rs | 2 +- .../src/legacy/building_map.rs | 2 +- crates/rmf_site_format/src/site.rs | 2 +- crates/rmf_site_format/src/task.rs | 56 +++++----- 11 files changed, 145 insertions(+), 87 deletions(-) diff --git a/crates/rmf_site_editor/src/mapf_rse/config_widget.rs b/crates/rmf_site_editor/src/mapf_rse/config_widget.rs index 27df28373..daac516c2 100644 --- a/crates/rmf_site_editor/src/mapf_rse/config_widget.rs +++ b/crates/rmf_site_editor/src/mapf_rse/config_widget.rs @@ -41,7 +41,7 @@ pub struct MapfConfigWidget<'w, 's> { negotiation_task: Res<'w, NegotiationTask>, occupancy_display: ResMut<'w, OccupancyDisplay>, robots: Query<'w, 's, Entity, With>, - tasks: Query<'w, 's, &'static Task>, + tasks: Query<'w, 's, &'static Task>, } impl<'w, 's> WidgetSystem for MapfConfigWidget<'w, 's> { diff --git a/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs b/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs index 584093f84..264f39d2f 100644 --- a/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs +++ b/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs @@ -30,7 +30,7 @@ use crate::{ occupancy::{Cell, Grid}, site::{ Affiliation, CircleCollision, CurrentLevel, DifferentialDrive, GoToPlace, Group, - LocationTags, ModelMarker, NameInSite, Point, Pose, Robot, Task as RobotTask, + LocationTags, ModelMarker, Point, Pose, Robot, Task as RobotTask, }, }; use mapf::negotiation::*; @@ -233,9 +233,9 @@ pub fn start_compute_negotiation( current_level: Res, grids: Query<(Entity, &Grid)>, child_of: Query<&ChildOf>, - robots: Query<(Entity, &NameInSite, &Pose, &Affiliation), With>, + robots: Query<(Entity, &Pose, &Affiliation), With>, robot_descriptions: Query<(&DifferentialDrive, &CircleCollision)>, - tasks: Query<(&RobotTask, &GoToPlace)>, + tasks: Query<(&RobotTask, &GoToPlace)>, mut negotiation_task: ResMut, ) { if negotiation_request.len() == 0 { @@ -286,10 +286,8 @@ pub fn start_compute_negotiation( let mut agents = BTreeMap::::new(); // Only loop tasks that have specified a valid robot for (task, go_to_place) in tasks.iter() { - // Identify robot - let robot_name = task.robot(); - for (robot_entity, robot_site_name, robot_pose, robot_group) in robots.iter() { - if robot_name == robot_site_name.0 { + for (robot_entity, robot_pose, robot_group) in robots.iter() { + if task.robot().0.is_some_and(|e| robot_entity == e) { // Match location to entity for Point(anchor_entity) in locations.iter() { if go_to_place diff --git a/crates/rmf_site_editor/src/site/mod.rs b/crates/rmf_site_editor/src/site/mod.rs index c13e57dc2..067c3fc50 100644 --- a/crates/rmf_site_editor/src/site/mod.rs +++ b/crates/rmf_site_editor/src/site/mod.rs @@ -298,11 +298,11 @@ impl Plugin for SitePlugin { ChangePlugin::>::default(), ChangePlugin::>::default(), ChangePlugin::>::default(), - ChangePlugin::::default(), + ChangePlugin::>::default(), PropertyPlugin::::default(), PropertyPlugin::::default(), - PropertyPlugin::::default(), - PropertyPlugin::::default(), + PropertyPlugin::>::default(), + PropertyPlugin::>::default(), PropertyPlugin::, Robot>::default(), SlotcarSdfPlugin, MaterialPlugin::>::default(), diff --git a/crates/rmf_site_editor/src/site/save.rs b/crates/rmf_site_editor/src/site/save.rs index d17d209c1..650d9ab9e 100644 --- a/crates/rmf_site_editor/src/site/save.rs +++ b/crates/rmf_site_editor/src/site/save.rs @@ -120,7 +120,7 @@ fn assign_site_ids(world: &mut World, site: Entity) -> Result<(), SiteGeneration Query, With)>, Query, Without, Without)>, Query<(Entity, &Affiliation), With>>, - Query, Without)>, + Query>, Without)>, Query< Entity, ( @@ -1541,15 +1541,37 @@ fn generate_scenarios( fn generate_tasks( site: Entity, world: &mut World, -) -> Result, SiteGenerationError> { - let mut state: SystemState<(Query<(&SiteID, &Task), Without>, Query<&Children>)> = - SystemState::new(world); - let (tasks, children) = state.get(world); - let mut res = BTreeMap::::new(); +) -> Result>, SiteGenerationError> { + let mut state: SystemState<( + Query<(Entity, &SiteID, &Task), Without>, + Query<&SiteID, (With, Without)>, + Query<&Children>, + )> = SystemState::new(world); + let (tasks, robots, children) = state.get(world); + let mut res = BTreeMap::>::new(); if let Ok(children) = children.get(site) { for child in children.iter() { - if let Ok((site_id, task)) = tasks.get(child) { - res.insert(site_id.0, task.clone()); + if let Ok((task_entity, site_id, task)) = tasks.get(child) { + let task = match task { + Task::Dispatch(request) => Task::Dispatch(request.clone()), + Task::Direct(request) => { + let robot_entity = match request.robot.0 { + Some(e) => e, + None => return Err(SiteGenerationError::EmptyAffiliation(task_entity)), + }; + Task::Direct(RobotTaskRequest::new( + match robots.get(robot_entity) { + Ok(id) => Affiliation(Some(id.0)), + Err(_) => { + return Err(SiteGenerationError::MissingSiteID(robot_entity)) + } + }, + request.fleet.clone(), + request.request.clone(), + )) + } + }; + res.insert(site_id.0, task); } } } diff --git a/crates/rmf_site_editor/src/site/task.rs b/crates/rmf_site_editor/src/site/task.rs index a797c1434..a18e2da02 100644 --- a/crates/rmf_site_editor/src/site/task.rs +++ b/crates/rmf_site_editor/src/site/task.rs @@ -32,13 +32,13 @@ impl FromWorld for TaskKinds { } } -impl Element for Task {} +impl Element for Task {} impl StandardProperty for TaskParams {} pub fn update_task_kind_component( mut commands: Commands, - tasks: Query<(Entity, Ref, Option<&T>)>, + tasks: Query<(Entity, Ref>, Option<&T>)>, ) { for (entity, task, task_kind) in tasks.iter() { if task.is_changed() { diff --git a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs index 427fb37fd..1fa8e5f4f 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs @@ -75,7 +75,7 @@ impl Plugin for GoToPlacePlugin { pub struct ViewGoToPlace<'w, 's> { locations: Query<'w, 's, (Entity, &'static NameInSite), With>, edit_task: Res<'w, EditTask>, - tasks: Query<'w, 's, (&'static mut GoToPlace, &'static mut Task)>, + tasks: Query<'w, 's, (&'static mut GoToPlace, &'static mut Task)>, hover: EventWriter<'w, Hover>, } @@ -109,15 +109,15 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { .selected_text(selected_location_name) .show_ui(ui, |ui| { // Sort locations alphabetically - let mut locations = params.locations.iter().fold( + let mut sorted_locations = params.locations.iter().fold( Vec::<(Entity, String)>::new(), |mut l, (e, name)| { l.push((e, name.0.clone())); l }, ); - locations.sort_by(|a, b| a.1.cmp(&b.1)); - for (loc_entity, loc_name) in locations.iter() { + sorted_locations.sort_by(|a, b| a.1.cmp(&b.1)); + for (loc_entity, loc_name) in sorted_locations.iter() { let resp = ui.add(SelectableLabel::new( new_go_to_place.location == Some(Point(*loc_entity)), loc_name.clone(), diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 80ba220f8..4d376a9b1 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -30,11 +30,12 @@ use bevy::{ use bevy_egui::{ egui::{ Align, Align2, CollapsingHeader, Color32, ComboBox, DragValue, Frame, Grid, ImageButton, - Layout, RichText, ScrollArea, Stroke, TextEdit, Ui, Window, + Layout, RichText, ScrollArea, SelectableLabel, Stroke, TextEdit, Ui, Window, }, EguiContexts, }; use rmf_site_egui::*; +use rmf_site_picking::Hover; use serde_json::Value; use smallvec::SmallVec; @@ -143,13 +144,14 @@ pub struct EditModeEvent { pub struct ViewTasks<'w, 's> { children: Query<'w, 's, &'static Children>, commands: Commands<'w, 's>, - change_task: EventWriter<'w, Change>, + change_task: EventWriter<'w, Change>>, current_scenario: ResMut<'w, CurrentScenario>, delete: EventWriter<'w, Delete>, edit_mode: EventWriter<'w, EditModeEvent>, edit_task: Res<'w, EditTask>, get_inclusion_modifier: GetModifier<'w, 's, Modifier>, get_params_modifier: GetModifier<'w, 's, Modifier>, + hover: EventWriter<'w, Hover>, icons: Res<'w, Icons>, robots: Query<'w, 's, (Entity, &'static NameInSite, &'static Robot), Without>, scenarios: Query< @@ -163,7 +165,7 @@ pub struct ViewTasks<'w, 's> { >, task_kinds: ResMut<'w, TaskKinds>, task_widget: ResMut<'w, TaskWidget>, - tasks: Query<'w, 's, (Entity, &'static Task), Without>, + tasks: Query<'w, 's, (Entity, &'static Task), Without>, } impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { @@ -183,8 +185,8 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { }; // Tasks are sorted by start time, then request time, then created time, // depending on which fields are populated - let mut tasks = Vec::<(i32, (Entity, Task))>::new(); - let mut tasks_without_time = Vec::<(i32, (Entity, Task))>::new(); + let mut tasks = Vec::<(i32, (Entity, Task))>::new(); + let mut tasks_without_time = Vec::<(i32, (Entity, Task))>::new(); for (e, task) in params.tasks.iter() { if let Some(params_modifier) = @@ -252,7 +254,7 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { if ui.button("✚ Create New Task").clicked() { let new_task = params .commands - .spawn(Task::default()) + .spawn(Task::::default()) .insert(Category::Task) .insert(TaskParams::default()) .insert(Inclusion::Included) // New tasks created are included by default @@ -275,7 +277,7 @@ fn show_task_widget( state: &mut SystemState, scenario: Entity, task_entity: Entity, - task: &Task, + task: &Task, ) { let params = state.get_mut(world); let present = params @@ -321,6 +323,7 @@ fn show_task_widget( ¶ms.icons, present, in_edit_mode, + &mut params.hover, ); // Display children widgets if editing existing task @@ -347,11 +350,11 @@ fn show_task_params( ui: &mut Ui, commands: &mut Commands, task_entity: Entity, - task: &Task, + task: &Task, scenario: Entity, get_inclusion_modifier: &GetModifier>, get_params_modifier: &GetModifier>, - change_task: &mut EventWriter>, + change_task: &mut EventWriter>>, delete: &mut EventWriter, edit_mode: &mut EventWriter, task_kinds: &ResMut, @@ -360,6 +363,7 @@ fn show_task_params( icons: &Res, present: bool, in_edit_mode: bool, + hover: &mut EventWriter, ) { ui.horizontal(|ui| { ui.label("Task ".to_owned() + &task_entity.index().to_string()) // TODO(@xiyuoh) better identifier @@ -475,6 +479,7 @@ fn show_task_params( robots, task_kinds, change_task, + hover, ); } @@ -482,14 +487,15 @@ fn show_editable_task( ui: &mut Ui, commands: &mut Commands, task_entity: Entity, - task: &Task, + task: &Task, task_params: &TaskParams, scenario: Entity, in_edit_mode: bool, get_params_modifier: &GetModifier>, robots: &Query<(Entity, &NameInSite, &Robot), Without>, task_kinds: &ResMut, - change_task: &mut EventWriter>, + change_task: &mut EventWriter>>, + hover: &mut EventWriter, ) { let mut new_task = task.clone(); let task_request = new_task.request(); @@ -511,9 +517,15 @@ fn show_editable_task( }); } Task::Direct(_) => { + let robot_name = task + .robot() + .0 + .and_then(|e| robots.get(e).ok()) + .map(|(_, name, _)| name.0.clone()) + .unwrap_or("".to_string()); ui.horizontal(|ui| { let _ = ui.selectable_label(true, "Direct"); - ui.label(task.fleet().to_owned() + "/" + &task.robot()); + ui.label(task.fleet().to_owned() + "/" + &robot_name); }); } } @@ -525,6 +537,7 @@ fn show_editable_task( robots, task.robot(), task.fleet(), + hover, ); } ui.end_row(); @@ -688,7 +701,7 @@ fn show_create_task_dialog( task_state: &mut SystemState<( Res, Res, - Query<(&Task, &TaskParams), With>, + Query<(&Task, &TaskParams), With>, )>, egui_context_state: &mut SystemState, edit_state: &mut SystemState<( @@ -696,7 +709,8 @@ fn show_create_task_dialog( GetModifier>, Query<(Entity, &NameInSite, &Robot), Without>, ResMut, - EventWriter>, + EventWriter>>, + EventWriter, )>, widget_state: &mut SystemState<(Query<&Children>, Res)>, dialog_state: &mut SystemState<(ResMut, EventWriter)>, @@ -725,7 +739,7 @@ fn show_create_task_dialog( .resizable(false) .anchor(Align2::CENTER_CENTER, [0.0, 0.0]) .show(&mut ctx, |ui| { - let (mut commands, get_params_modifier, robots, task_kinds, mut change_task) = + let (mut commands, get_params_modifier, robots, task_kinds, mut change_task, mut hover) = edit_state.get_mut(world); show_editable_task( ui, @@ -739,6 +753,7 @@ fn show_create_task_dialog( &robots, &task_kinds, &mut change_task, + &mut hover, ); let task_request_category = pending_task.request().category(); let task_kind_is_valid = @@ -808,11 +823,12 @@ fn show_create_task_dialog( fn edit_request_type_widget( ui: &mut Ui, - task: &mut Task, + task: &mut Task, task_request: &TaskRequest, robots: &Query<(Entity, &NameInSite, &Robot), Without>, - robot: String, + robot: Affiliation, fleet: String, + hover: &mut EventWriter, ) { let mut is_robot_task_request = task.is_direct(); ui.horizontal(|ui| { @@ -844,22 +860,44 @@ fn edit_request_type_widget( ui.end_row(); ui.label("Robot:"); - let selected_robot = if robot_task_request.robot().is_empty() { - "Select Robot".to_string() + let selected_robot = if let Some((_, robot_name, _)) = robot_task_request + .robot() + .0 + .and_then(|e| robots.get(e).ok()) + { + robot_name.0.clone() } else { - robot_task_request.robot() + "Select Robot".to_string() }; ComboBox::from_id_salt("select_robot_for_task") .selected_text(selected_robot) .show_ui(ui, |ui| { - for (_, robot_name, robot) in robots.iter() { - ui.selectable_value( - robot_task_request.robot_mut(), - robot_name.0.clone(), - robot_name.0.clone(), - ); + // Sort robots alphabetically + let mut sorted_robots = robots.iter().fold( + Vec::<(Entity, String, Robot)>::new(), + |mut l, (e, name, r)| { + l.push((e, name.0.clone(), r.clone())); + l + }, + ); + sorted_robots.sort_by(|a, b| a.1.cmp(&b.1)); + for (robot_entity, robot_name, robot) in sorted_robots.iter() { + let requested_robot = robot_task_request.robot_mut(); + let resp = ui.add(SelectableLabel::new( + requested_robot.0.is_some_and(|e| e == *robot_entity), + robot_name.clone(), + )); + if resp.clicked() { + *requested_robot = Affiliation(Some(*robot_entity)); + } else if resp.hovered() { + hover.write(Hover(Some(*robot_entity))); + } // Update fleet according to selected robot - if robot_task_request.robot() == robot_name.0 { + if robot_task_request + .robot() + .0 + .is_some_and(|e| e == *robot_entity) + { *robot_task_request.fleet_mut() = robot.fleet.clone(); } } @@ -877,7 +915,7 @@ fn edit_request_type_widget( fn edit_task_kind_widget( ui: &mut Ui, commands: &mut Commands, - task: &mut Task, + task: &mut Task, task_entity: Entity, task_kinds: &ResMut, ) { @@ -914,7 +952,7 @@ fn edit_task_kind_widget( } } -fn edit_requester_widget(ui: &mut Ui, task: &mut Task) { +fn edit_requester_widget(ui: &mut Ui, task: &mut Task) { let new_task_request = task.request_mut(); let requester = new_task_request .requester_mut() @@ -927,7 +965,7 @@ fn edit_requester_widget(ui: &mut Ui, task: &mut Task) { } } -fn edit_fleet_widget(ui: &mut Ui, task: &mut Task) { +fn edit_fleet_widget(ui: &mut Ui, task: &mut Task) { // TODO(@xiyuoh) when available, insert combobox of registered fleets let new_task_request = task.request_mut(); let fleet_name = new_task_request @@ -1028,7 +1066,7 @@ pub fn handle_task_edit( mut delete: EventWriter, mut edit_mode: EventReader, mut edit_task: ResMut, - pending_tasks: Query<&mut Task, With>, + pending_tasks: Query<&mut Task, With>, current_workspace: Res, ) { if let Some(edit) = edit_mode.read().last() { diff --git a/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs b/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs index f62908e36..7412e64a4 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/wait_for.rs @@ -57,7 +57,7 @@ impl Plugin for WaitForPlugin { #[derive(SystemParam)] pub struct ViewWaitFor<'w, 's> { edit_task: Res<'w, EditTask>, - tasks: Query<'w, 's, (&'static mut WaitFor, &'static mut Task)>, + tasks: Query<'w, 's, (&'static mut WaitFor, &'static mut Task)>, } impl<'w, 's> WidgetSystem for ViewWaitFor<'w, 's> { diff --git a/crates/rmf_site_format/src/legacy/building_map.rs b/crates/rmf_site_format/src/legacy/building_map.rs index 5e82d90ec..bfd7e98d7 100644 --- a/crates/rmf_site_format/src/legacy/building_map.rs +++ b/crates/rmf_site_format/src/legacy/building_map.rs @@ -208,7 +208,7 @@ impl BuildingMap { let mut model_instances: BTreeMap>> = BTreeMap::new(); let mut model_description_name_map = HashMap::::new(); let mut scenarios: BTreeMap> = BTreeMap::new(); - let tasks: BTreeMap = BTreeMap::new(); // Tasks not supported in legacy + let tasks: BTreeMap> = BTreeMap::new(); // Tasks not supported in legacy let default_scenario_id = site_id.next().unwrap(); scenarios.insert(default_scenario_id, Scenario::default()); diff --git a/crates/rmf_site_format/src/site.rs b/crates/rmf_site_format/src/site.rs index fa49c135b..0635dd166 100644 --- a/crates/rmf_site_format/src/site.rs +++ b/crates/rmf_site_format/src/site.rs @@ -150,7 +150,7 @@ pub struct Site { pub model_instances: BTreeMap>>, /// Tasks available in this site #[serde(default, skip_serializing_if = "BTreeMap::is_empty")] - pub tasks: BTreeMap, + pub tasks: BTreeMap>, } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq)] diff --git a/crates/rmf_site_format/src/task.rs b/crates/rmf_site_format/src/task.rs index 5398470d0..035efaeb6 100644 --- a/crates/rmf_site_format/src/task.rs +++ b/crates/rmf_site_format/src/task.rs @@ -193,18 +193,18 @@ impl DispatchTaskRequest { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -pub struct RobotTaskRequest { - pub robot: String, +pub struct RobotTaskRequest { + pub robot: Affiliation, pub fleet: String, pub request: TaskRequest, } -impl TaskRequestType for RobotTaskRequest { +impl TaskRequestType for RobotTaskRequest { fn is_valid(&self) -> bool { if self.fleet.is_empty() { return false; } - if self.robot.is_empty() { + if self.robot.0.is_none() { return false; } self.request.is_valid() @@ -219,8 +219,8 @@ impl TaskRequestType for RobotTaskRequest { } } -impl RobotTaskRequest { - pub fn new(robot: String, fleet: String, request: TaskRequest) -> Self { +impl RobotTaskRequest { + pub fn new(robot: Affiliation, fleet: String, request: TaskRequest) -> Self { Self { robot, fleet, @@ -228,11 +228,11 @@ impl RobotTaskRequest { } } - pub fn robot(&self) -> String { + pub fn robot(&self) -> Affiliation { self.robot.clone() } - pub fn robot_mut(&mut self) -> &mut String { + pub fn robot_mut(&mut self) -> &mut Affiliation { &mut self.robot } @@ -247,79 +247,79 @@ impl RobotTaskRequest { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component))] -pub enum Task { +pub enum Task { Dispatch(DispatchTaskRequest), - Direct(RobotTaskRequest), + Direct(RobotTaskRequest), } -impl Default for Task { +impl Default for Task { fn default() -> Self { - Task::Dispatch(DispatchTaskRequest { + Task::::Dispatch(DispatchTaskRequest { request: TaskRequest::default(), }) } } -impl Task { +impl Task { pub fn is_valid(&self) -> bool { match self { - Task::Dispatch(dispatch_task_request) => dispatch_task_request.is_valid(), - Task::Direct(robot_task_request) => robot_task_request.is_valid(), + Task::::Dispatch(dispatch_task_request) => dispatch_task_request.is_valid(), + Task::::Direct(robot_task_request) => robot_task_request.is_valid(), } } pub fn is_dispatch(&self) -> bool { match self { - Task::Dispatch(_) => true, + Task::::Dispatch(_) => true, _ => false, } } pub fn is_direct(&self) -> bool { match self { - Task::Direct(_) => true, + Task::::Direct(_) => true, _ => false, } } pub fn request(&self) -> TaskRequest { match self { - Task::Dispatch(dispatch_task_request) => dispatch_task_request.request(), - Task::Direct(robot_task_request) => robot_task_request.request(), + Task::::Dispatch(dispatch_task_request) => dispatch_task_request.request(), + Task::::Direct(robot_task_request) => robot_task_request.request(), } } pub fn request_mut(&mut self) -> &mut TaskRequest { match self { - Task::Dispatch(dispatch_task_request) => dispatch_task_request.request_mut(), - Task::Direct(robot_task_request) => robot_task_request.request_mut(), + Task::::Dispatch(dispatch_task_request) => dispatch_task_request.request_mut(), + Task::::Direct(robot_task_request) => robot_task_request.request_mut(), } } - pub fn robot(&self) -> String { + pub fn robot(&self) -> Affiliation { match self { - Task::Direct(robot_task_request) => robot_task_request.robot(), - _ => "".to_string(), + Task::::Direct(robot_task_request) => robot_task_request.robot(), + _ => Affiliation(None), } } - pub fn robot_mut(&mut self) -> Option<&mut String> { + pub fn robot_mut(&mut self) -> Option<&mut Affiliation> { match self { - Task::Direct(robot_task_request) => Some(robot_task_request.robot_mut()), + Task::::Direct(robot_task_request) => Some(robot_task_request.robot_mut()), _ => None, } } pub fn fleet(&self) -> String { match self { - Task::Direct(robot_task_request) => robot_task_request.fleet(), + Task::::Direct(robot_task_request) => robot_task_request.fleet(), _ => "".to_string(), } } pub fn fleet_mut(&mut self) -> Option<&mut String> { match self { - Task::Direct(robot_task_request) => Some(robot_task_request.fleet_mut()), + Task::::Direct(robot_task_request) => Some(robot_task_request.fleet_mut()), _ => None, } } From ec9443d03438454922c75189f490b04d11bcc8eb Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 3 Sep 2025 09:52:40 +0000 Subject: [PATCH 17/21] Use timestamp as task id and fix saving Signed-off-by: Xiyu Oh --- Cargo.lock | 45 +++++++++++++ Cargo.toml | 1 + assets/demo_maps/test.site.json | 67 +++++++++++++++++-- crates/rmf_site_editor/src/site/load.rs | 3 +- .../rmf_site_editor/src/widgets/tasks/mod.rs | 9 ++- crates/rmf_site_format/Cargo.toml | 1 + crates/rmf_site_format/src/task.rs | 64 +++++++++++++----- 7 files changed, 162 insertions(+), 28 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index f7b4add9c..d9d0e54a1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -178,6 +178,12 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "fc7eb209b1518d6bb87b283c20095f5228ecda460da70b44f0802523dea6da04" +[[package]] +name = "android-tzdata" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e999941b234f3131b00bc13c22d06e8c5ff726d1b6318ac7eb276997bbb4fef0" + [[package]] name = "android_log-sys" version = "0.3.2" @@ -2091,6 +2097,20 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" +[[package]] +name = "chrono" +version = "0.4.41" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c469d952047f47f91b68d1cba3f10d63c11d73e4636f24f08daf0278abf01c4d" +dependencies = [ + "android-tzdata", + "iana-time-zone", + "js-sys", + "num-traits", + "wasm-bindgen", + "windows-link", +] + [[package]] name = "clang-sys" version = "1.8.1" @@ -3631,6 +3651,30 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" +[[package]] +name = "iana-time-zone" +version = "0.1.63" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b0c919e5debc312ad217002b8048a17b7d83f80703865bbfcfebb0458b0b27d8" +dependencies = [ + "android_system_properties", + "core-foundation-sys", + "iana-time-zone-haiku", + "js-sys", + "log", + "wasm-bindgen", + "windows-core 0.61.2", +] + +[[package]] +name = "iana-time-zone-haiku" +version = "0.1.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" +dependencies = [ + "cc", +] + [[package]] name = "icu_collections" version = "2.0.0" @@ -5750,6 +5794,7 @@ name = "rmf_site_format" version = "0.0.1" dependencies = [ "bevy", + "chrono", "float_eq", "glam", "once_cell", diff --git a/Cargo.toml b/Cargo.toml index 368bbb91e..12be0c7b2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -39,6 +39,7 @@ ron = "0.10" thiserror = "*" glam = { version = "0.29" } # Ensure that this match's bevy_math's glam before updating. uuid = { version = "1.13"} +chrono = "*" sdformat_rs = { git = "https://github.com/open-rmf/sdf_rust_experimental", rev = "514949e" } urdf-rs = { version = "0.7.3"} once_cell = "1" diff --git a/assets/demo_maps/test.site.json b/assets/demo_maps/test.site.json index 8291029cc..a3a276232 100644 --- a/assets/demo_maps/test.site.json +++ b/assets/demo_maps/test.site.json @@ -856,7 +856,8 @@ } } }, - "inclusion": "Included" + "inclusion": "Included", + "on_level": 22 }, "62": { "pose": { @@ -879,7 +880,8 @@ ] } }, - "inclusion": "Included" + "inclusion": "Included", + "on_level": 22 }, "63": { "pose": { @@ -894,7 +896,8 @@ } } }, - "inclusion": "Included" + "inclusion": "Included", + "on_level": 22 }, "122": { "pose": { @@ -994,6 +997,32 @@ } }, "inclusion": "Included" + }, + "133": { + "inclusion": "Included" + }, + "134": { + "inclusion": "Included" + } + }, + "tasks": { + "133": { + "inclusion": "Included", + "params": { + "unix_millis_earliest_start_time": null, + "unix_millis_request_time": null, + "priority": null, + "labels": [] + } + }, + "134": { + "inclusion": "Included", + "params": { + "unix_millis_earliest_start_time": null, + "unix_millis_request_time": null, + "priority": null, + "labels": [] + } } }, "name": "Default Scenario", @@ -1054,7 +1083,7 @@ }, "model_instances": { "53": { - "parent": 1, + "parent": 22, "name": "L1_robot", "pose": { "trans": [ @@ -1223,5 +1252,35 @@ }, "description": 129 } + }, + "tasks": { + "133": { + "Direct": { + "robot": 53, + "fleet": "HospitalRobot", + "request": { + "id": "task-20250903-093917", + "category": "Wait For", + "description": { + "duration": 30.0 + }, + "description_display": "30 seconds", + "created_time": 1756892357328 + } + } + }, + "134": { + "Dispatch": { + "request": { + "id": "task-20250903-093931", + "category": "Go To Place", + "description": { + "location": 4294968127 + }, + "description_display": "A", + "created_time": 1756892371119 + } + } + } } } \ No newline at end of file diff --git a/crates/rmf_site_editor/src/site/load.rs b/crates/rmf_site_editor/src/site/load.rs index e8acd2e75..cf2ccde3b 100644 --- a/crates/rmf_site_editor/src/site/load.rs +++ b/crates/rmf_site_editor/src/site/load.rs @@ -536,8 +536,9 @@ fn generate_site_entities( } for (task_id, task_data) in &site_data.tasks { + let task = task_data.convert(&id_to_entity).for_site(site_id)?; let task_entity = commands - .spawn(task_data.clone()) + .spawn(task.clone()) .insert(SiteID(*task_id)) .insert(Category::Task) .insert(ChildOf(site_id)) diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 4d376a9b1..2a9ab2d55 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -185,8 +185,8 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { }; // Tasks are sorted by start time, then request time, then created time, // depending on which fields are populated - let mut tasks = Vec::<(i32, (Entity, Task))>::new(); - let mut tasks_without_time = Vec::<(i32, (Entity, Task))>::new(); + let mut tasks = Vec::<(i64, (Entity, Task))>::new(); + let mut tasks_without_time = Vec::<(i64, (Entity, Task))>::new(); for (e, task) in params.tasks.iter() { if let Some(params_modifier) = @@ -208,7 +208,7 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { // We should not reach here as created_time is populated by default, // but in case it comes up as None we sort these by entity index and // place them at the end of the task list - tasks_without_time.push((e.index() as i32, (e, task.clone()))); + tasks_without_time.push((e.index() as i64, (e, task.clone()))); } tasks.sort_by(|a, b| a.0.cmp(&b.0)); tasks_without_time.sort_by(|a, b| a.0.cmp(&b.0)); @@ -256,7 +256,6 @@ impl<'w, 's> WidgetSystem for ViewTasks<'w, 's> { .commands .spawn(Task::::default()) .insert(Category::Task) - .insert(TaskParams::default()) .insert(Inclusion::Included) // New tasks created are included by default .insert(Pending) .id(); @@ -366,7 +365,7 @@ fn show_task_params( hover: &mut EventWriter, ) { ui.horizontal(|ui| { - ui.label("Task ".to_owned() + &task_entity.index().to_string()) // TODO(@xiyuoh) better identifier + ui.label(task.request().id().to_owned()) .on_hover_text(format!("Task is included in {} scenarios", scenario_count)); ui.with_layout(Layout::right_to_left(Align::Center), |ui| { if ui diff --git a/crates/rmf_site_format/Cargo.toml b/crates/rmf_site_format/Cargo.toml index df611d939..7fb1b8b9b 100644 --- a/crates/rmf_site_format/Cargo.toml +++ b/crates/rmf_site_format/Cargo.toml @@ -23,6 +23,7 @@ urdf-rs = { workspace = true, optional = true } # Used for lazy initialization of static variable when they are non const once_cell = {workspace = true} pathdiff = {workspace = true} +chrono = { workspace = true } [dev-dependencies] float_eq = {workspace = true} diff --git a/crates/rmf_site_format/src/task.rs b/crates/rmf_site_format/src/task.rs index 035efaeb6..cac380b5b 100644 --- a/crates/rmf_site_format/src/task.rs +++ b/crates/rmf_site_format/src/task.rs @@ -18,39 +18,38 @@ use crate::*; #[cfg(feature = "bevy")] use bevy::prelude::{Component, Reflect}; +use chrono::DateTime; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use std::{ + collections::HashMap, fmt, time::{SystemTime, UNIX_EPOCH}, }; +use uuid::Uuid; #[derive(Serialize, Deserialize, Default, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component))] pub struct TaskParams { - #[serde(default, skip_serializing_if = "is_default")] - pub unix_millis_earliest_start_time: Option, - #[serde(default, skip_serializing_if = "is_default")] - pub unix_millis_request_time: Option, - #[serde(default, skip_serializing_if = "is_default")] + pub unix_millis_earliest_start_time: Option, + pub unix_millis_request_time: Option, pub priority: Option, - #[serde(default, skip_serializing_if = "is_default")] pub labels: Vec, } impl TaskParams { - pub fn start_time(&self) -> Option { + pub fn start_time(&self) -> Option { self.unix_millis_earliest_start_time.clone() } - pub fn start_time_mut(&mut self) -> &mut Option { + pub fn start_time_mut(&mut self) -> &mut Option { &mut self.unix_millis_earliest_start_time } - pub fn request_time(&self) -> Option { + pub fn request_time(&self) -> Option { self.unix_millis_request_time.clone() } - pub fn request_time_mut(&mut self) -> &mut Option { + pub fn request_time_mut(&mut self) -> &mut Option { &mut self.unix_millis_request_time } @@ -73,6 +72,7 @@ impl TaskParams { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct TaskRequest { + pub id: String, pub category: String, #[serde(default, skip_serializing_if = "is_default")] pub description: serde_json::Value, @@ -83,21 +83,28 @@ pub struct TaskRequest { #[serde(default, skip_serializing_if = "is_default")] pub fleet_name: Option, #[serde(default, skip_serializing_if = "is_default")] - pub created_time: Option, + pub created_time: Option, } impl Default for TaskRequest { fn default() -> Self { + let created_time = SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|d| d.as_millis() as i64) + .ok(); + let id = created_time + .map(|t| std::time::Duration::from_millis(t as u64)) + .and_then(|d| DateTime::from_timestamp(d.as_secs() as i64, d.subsec_nanos())) + .map(|dt| format!("task-{}", dt.format("%Y%m%d-%H%M%S").to_string())) + .unwrap_or(format!("task-{}", Uuid::new_v4().to_string())); TaskRequest { + id, category: "".to_string(), description: serde_json::Value::Null, description_display: None, requester: None, fleet_name: None, - created_time: SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_millis() as i32) - .ok(), + created_time, } } } @@ -110,6 +117,14 @@ impl TaskRequest { true } + pub fn id(&self) -> String { + self.id.clone() + } + + pub fn id_mut(&mut self) -> &mut String { + &mut self.id + } + pub fn category(&self) -> String { self.category.clone() } @@ -150,11 +165,11 @@ impl TaskRequest { &mut self.fleet_name } - pub fn created_time(&self) -> Option { + pub fn created_time(&self) -> Option { self.created_time.clone() } - pub fn created_time_mut(&mut self) -> &mut Option { + pub fn created_time_mut(&mut self) -> &mut Option { &mut self.created_time } } @@ -246,7 +261,7 @@ impl RobotTaskRequest { } #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] -#[cfg_attr(feature = "bevy", derive(Component))] +#[cfg_attr(feature = "bevy", derive(Component), require(TaskParams))] pub enum Task { Dispatch(DispatchTaskRequest), Direct(RobotTaskRequest), @@ -260,6 +275,19 @@ impl Default for Task { } } +impl Task { + pub fn convert(&self, id_map: &HashMap) -> Result, T> { + match self { + Task::::Dispatch(request) => Ok(Task::::Dispatch(request.clone())), + Task::::Direct(request) => Ok(Task::::Direct(RobotTaskRequest { + robot: request.robot.convert(id_map)?, + fleet: request.fleet.clone(), + request: request.request.clone(), + })), + } + } +} + impl Task { pub fn is_valid(&self) -> bool { match self { From 3c43a73999078357ec7f4c4a8bee9967d898c02a Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Thu, 4 Sep 2025 03:02:20 +0000 Subject: [PATCH 18/21] Fix saving location in tasks Signed-off-by: Xiyu Oh --- assets/demo_maps/test.site.json | 40 +++++++++---------- .../src/mapf_rse/negotiation/mod.rs | 5 +-- .../src/widgets/tasks/go_to_place.rs | 39 ++++++++++++------ crates/rmf_site_format/src/task.rs | 6 ++- 4 files changed, 52 insertions(+), 38 deletions(-) diff --git a/assets/demo_maps/test.site.json b/assets/demo_maps/test.site.json index a3a276232..3eeb301ff 100644 --- a/assets/demo_maps/test.site.json +++ b/assets/demo_maps/test.site.json @@ -998,15 +998,15 @@ }, "inclusion": "Included" }, - "133": { + "138": { "inclusion": "Included" }, - "134": { + "139": { "inclusion": "Included" } }, "tasks": { - "133": { + "138": { "inclusion": "Included", "params": { "unix_millis_earliest_start_time": null, @@ -1015,7 +1015,7 @@ "labels": [] } }, - "134": { + "139": { "inclusion": "Included", "params": { "unix_millis_earliest_start_time": null, @@ -1254,31 +1254,31 @@ } }, "tasks": { - "133": { - "Direct": { - "robot": 53, - "fleet": "HospitalRobot", + "138": { + "Dispatch": { "request": { - "id": "task-20250903-093917", - "category": "Wait For", + "id": "task-20250904-030419", + "category": "Go To Place", "description": { - "duration": 30.0 + "location": 121 }, - "description_display": "30 seconds", - "created_time": 1756892357328 + "description_display": "A", + "created_time": 1756955059673 } } }, - "134": { - "Dispatch": { + "139": { + "Direct": { + "robot": 53, + "fleet": "HospitalRobot", "request": { - "id": "task-20250903-093931", - "category": "Go To Place", + "id": "task-20250904-030447", + "category": "Wait For", "description": { - "location": 4294968127 + "duration": 30.0 }, - "description_display": "A", - "created_time": 1756892371119 + "description_display": "30 seconds", + "created_time": 1756955087335 } } } diff --git a/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs b/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs index 264f39d2f..85dca6a29 100644 --- a/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs +++ b/crates/rmf_site_editor/src/mapf_rse/negotiation/mod.rs @@ -290,10 +290,7 @@ pub fn start_compute_negotiation( if task.robot().0.is_some_and(|e| robot_entity == e) { // Match location to entity for Point(anchor_entity) in locations.iter() { - if go_to_place - .location - .is_some_and(|pt| pt.0 == *anchor_entity) - { + if go_to_place.location.0.is_some_and(|e| e == *anchor_entity) { let Ok(goal_transform) = anchors.get(*anchor_entity) else { warn!("Unable to get robot's goal transform"); continue; diff --git a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs index 1fa8e5f4f..ac41b22a1 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs @@ -17,7 +17,8 @@ use super::{EditTask, TaskWidget}; use crate::{ site::{ - update_task_kind_component, LocationTags, NameInSite, Point, Task, TaskKind, TaskKinds, + update_task_kind_component, Affiliation, LocationTags, NameInSite, SiteID, Task, TaskKind, + TaskKinds, }, widgets::prelude::*, }; @@ -51,8 +52,7 @@ impl Plugin for GoToPlacePlugin { let Some(loc_entity) = world .entity(e) .get::>() - .and_then(|go_to_place| go_to_place.location) - .map(|pt| pt.0) + .and_then(|go_to_place| go_to_place.location.0) else { return false; }; @@ -73,7 +73,8 @@ impl Plugin for GoToPlacePlugin { #[derive(SystemParam)] pub struct ViewGoToPlace<'w, 's> { - locations: Query<'w, 's, (Entity, &'static NameInSite), With>, + locations: + Query<'w, 's, (Entity, &'static NameInSite, Option<&'static SiteID>), With>, edit_task: Res<'w, EditTask>, tasks: Query<'w, 's, (&'static mut GoToPlace, &'static mut Task)>, hover: EventWriter<'w, Hover>, @@ -93,9 +94,10 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { return; } - let selected_location_name = if let Some((_, loc_name)) = go_to_place + let selected_location_name = if let Some((_, loc_name, _)) = go_to_place .location - .and_then(|pt| params.locations.get(pt.0).ok()) + .0 + .and_then(|e| params.locations.get(e).ok()) { loc_name.0.clone() } else { @@ -111,7 +113,7 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { // Sort locations alphabetically let mut sorted_locations = params.locations.iter().fold( Vec::<(Entity, String)>::new(), - |mut l, (e, name)| { + |mut l, (e, name, _)| { l.push((e, name.0.clone())); l }, @@ -119,11 +121,11 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { sorted_locations.sort_by(|a, b| a.1.cmp(&b.1)); for (loc_entity, loc_name) in sorted_locations.iter() { let resp = ui.add(SelectableLabel::new( - new_go_to_place.location == Some(Point(*loc_entity)), + new_go_to_place.location == Affiliation(Some(*loc_entity)), loc_name.clone(), )); if resp.clicked() { - new_go_to_place.location = Some(Point(*loc_entity)); + new_go_to_place.location = Affiliation(Some(*loc_entity)); } else if resp.hovered() { params.hover.write(Hover(Some(*loc_entity))); } @@ -134,12 +136,25 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { if *go_to_place != new_go_to_place { *go_to_place = new_go_to_place.clone(); - if let Ok(description) = serde_json::to_value(new_go_to_place.clone()) { + // Convert Location entity to SiteID before serializing + if let Some(description) = new_go_to_place + .location + .0 + .and_then(|e| params.locations.get(e).ok()) + .and_then(|(_, _, site_id)| site_id) + .and_then(|id| { + serde_json::to_value(GoToPlace:: { + location: Affiliation(Some(**id)), + }) + .ok() + }) + { *task.request_mut().description_mut() = description; *task.request_mut().description_display_mut() = new_go_to_place .location - .and_then(|pt| params.locations.get(pt.0).ok()) - .map(|(_, name)| name.0.clone()); + .0 + .and_then(|e| params.locations.get(e).ok()) + .map(|(_, name, _)| name.0.clone()); } } } diff --git a/crates/rmf_site_format/src/task.rs b/crates/rmf_site_format/src/task.rs index cac380b5b..b54985ffd 100644 --- a/crates/rmf_site_format/src/task.rs +++ b/crates/rmf_site_format/src/task.rs @@ -362,12 +362,14 @@ pub trait TaskKind: Component + Serialize + DeserializeOwned { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] #[cfg_attr(feature = "bevy", derive(Component, Reflect))] pub struct GoToPlace { - pub location: Option>, + pub location: Affiliation, } impl Default for GoToPlace { fn default() -> Self { - Self { location: None } + Self { + location: Affiliation(None), + } } } From f79bb7dbf8c2098cd0d9d86b00813bb4a580205d Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Thu, 4 Sep 2025 08:49:57 +0000 Subject: [PATCH 19/21] Insert valid task location Signed-off-by: Xiyu Oh --- .../src/widgets/tasks/go_to_place.rs | 52 ++++++++++++++++--- 1 file changed, 45 insertions(+), 7 deletions(-) diff --git a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs index ac41b22a1..452bbf947 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs @@ -67,7 +67,8 @@ impl Plugin for GoToPlacePlugin { let widget = Widget::::new::(&mut app.world_mut()); let task_widget = app.world().resource::().get(); app.world_mut().spawn(widget).insert(ChildOf(task_widget)); - app.add_systems(PostUpdate, update_task_kind_component::>); + app.add_systems(PostUpdate, update_task_kind_component::>) + .add_observer(on_load_go_to_place); } } @@ -137,6 +138,11 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { *go_to_place = new_go_to_place.clone(); // Convert Location entity to SiteID before serializing + let location_name = new_go_to_place + .location + .0 + .and_then(|e| params.locations.get(e).ok()) + .map(|(_, name, _)| name.0.clone()); if let Some(description) = new_go_to_place .location .0 @@ -144,18 +150,50 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { .and_then(|(_, _, site_id)| site_id) .and_then(|id| { serde_json::to_value(GoToPlace:: { - location: Affiliation(Some(**id)), + location: Affiliation(Some(id.0)), }) .ok() }) { *task.request_mut().description_mut() = description; - *task.request_mut().description_display_mut() = new_go_to_place - .location - .0 - .and_then(|e| params.locations.get(e).ok()) - .map(|(_, name, _)| name.0.clone()); } + *task.request_mut().description_display_mut() = location_name; + } + } +} + +/// When loading a GoToPlace task from file, locations are stored as SiteID. +/// Since task description is serialized as JSON, we won't be able to do the +/// usual Entity <-> SiteID conversion. This observer checks that the GoToPlace +/// task/location entity loaded is valid. If not, use location name stored in +/// description display to select location entity. +fn on_load_go_to_place( + trigger: Trigger>, + mut commands: Commands, + tasks: Query<(Entity, &Task, Option<&GoToPlace>)>, + locations: Query<(Entity, &NameInSite), With>, +) { + let Ok((task_entity, task, go_to_place)) = tasks.get(trigger.target()) else { + return; + }; + if task.request().category() != GoToPlace::::label() { + return; + } + // Ignore if this is a valid location entity + if go_to_place.is_some_and(|gtp| gtp.location.0.is_some_and(|e| locations.get(e).is_ok())) { + return; + } + + // Rely on description display for location name matching + let Some(location_name) = task.request().description_display() else { + return; + }; + for (entity, name) in locations.iter() { + if location_name == *name.0 { + commands.entity(task_entity).insert(GoToPlace:: { + location: Affiliation(Some(entity)), + }); + return; } } } From 7de9d83dc1309917498d7a2596288ba281053877 Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Tue, 9 Sep 2025 20:53:51 +0000 Subject: [PATCH 20/21] Do not allow editing fleet name via task widget Signed-off-by: Xiyu Oh --- crates/rmf_site_editor/src/site/mod.rs | 1 + crates/rmf_site_editor/src/site/task.rs | 28 ++++++++++++++- .../rmf_site_editor/src/widgets/tasks/mod.rs | 36 ++++++++++++++----- 3 files changed, 55 insertions(+), 10 deletions(-) diff --git a/crates/rmf_site_editor/src/site/mod.rs b/crates/rmf_site_editor/src/site/mod.rs index 067c3fc50..8af56053c 100644 --- a/crates/rmf_site_editor/src/site/mod.rs +++ b/crates/rmf_site_editor/src/site/mod.rs @@ -457,6 +457,7 @@ impl Plugin for SitePlugin { check_for_missing_root_modifiers::, update_default_scenario, update_lane_motion_visuals, + update_direct_task_fleet, ) .run_if(AppState::in_displaying_mode()) .in_set(SiteUpdateSet::BetweenTransformAndVisibility), diff --git a/crates/rmf_site_editor/src/site/task.rs b/crates/rmf_site_editor/src/site/task.rs index a18e2da02..378803776 100644 --- a/crates/rmf_site_editor/src/site/task.rs +++ b/crates/rmf_site_editor/src/site/task.rs @@ -15,7 +15,10 @@ * */ -use crate::site::{Element, StandardProperty, Task, TaskKind, TaskParams}; +use crate::site::{ + Affiliation, Element, Group, ModelMarker, ModelProperty, Robot, StandardProperty, Task, + TaskKind, TaskParams, +}; use bevy::prelude::*; use std::collections::HashMap; @@ -54,3 +57,26 @@ pub fn update_task_kind_component( } } } + +// This systems monitors for changes in a Robot's fleet and updates relevant +// RobotTaskRequests accordingly +// TODO(@xiyuoh) This does not update fleet name for DispatchTasks, since they +// are not tagged to any robot. Convert fleet name to its own component so that +// we can track non-direct task fleet name changes too. +pub fn update_direct_task_fleet( + robots: Query<(Entity, Ref), (With, Without)>, + mut tasks: Query<&mut Task>, +) { + for (entity, robot) in robots.iter() { + if robot.is_changed() { + for mut task in tasks.iter_mut() { + if task.robot().0.is_some_and(|e| e == entity) && task.fleet() != robot.fleet { + // Update fleet name if it has changed + if let Some(fleet) = task.fleet_mut() { + *fleet = robot.fleet.clone(); + } + } + } + } + } +} diff --git a/crates/rmf_site_editor/src/widgets/tasks/mod.rs b/crates/rmf_site_editor/src/widgets/tasks/mod.rs index 2a9ab2d55..c1e971338 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/mod.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/mod.rs @@ -589,7 +589,7 @@ fn show_editable_task( if !in_edit_mode { ui.label(task_request.fleet_name().unwrap_or("None".to_string())); } else { - edit_fleet_widget(ui, &mut new_task); + edit_fleet_widget(ui, &mut new_task, robots); } }); } @@ -854,10 +854,6 @@ fn edit_request_type_widget( if let Task::Direct(ref mut robot_task_request) = task { ui.end_row(); - ui.label("Fleet:"); - ui.add(TextEdit::singleline(robot_task_request.fleet_mut())); - ui.end_row(); - ui.label("Robot:"); let selected_robot = if let Some((_, robot_name, _)) = robot_task_request .robot() @@ -901,6 +897,10 @@ fn edit_request_type_widget( } } }); + ui.end_row(); + + ui.label("Fleet:"); + ui.label(robot_task_request.fleet()); } else { warn!("Unable to select Direct task!"); } @@ -964,15 +964,33 @@ fn edit_requester_widget(ui: &mut Ui, task: &mut Task) { } } -fn edit_fleet_widget(ui: &mut Ui, task: &mut Task) { +fn edit_fleet_widget( + ui: &mut Ui, + task: &mut Task, + robots: &Query<(Entity, &NameInSite, &Robot), Without>, +) { // TODO(@xiyuoh) when available, insert combobox of registered fleets let new_task_request = task.request_mut(); let fleet_name = new_task_request .fleet_name_mut() .get_or_insert(String::new()); - TextEdit::singleline(fleet_name) - .desired_width(ui.available_width()) - .show(ui); + // Sort fleets alphabetically; only list fleets with robots + let mut sorted_fleets = robots + .iter() + .fold(Vec::::new(), |mut l, (_, _, robot)| { + if !l.contains(&robot.fleet) { + l.push(robot.fleet.clone()); + } + l + }); + sorted_fleets.sort(); + ComboBox::from_id_salt("select_fleet") + .selected_text(fleet_name.clone()) + .show_ui(ui, |ui| { + for f in sorted_fleets.iter() { + ui.selectable_value(fleet_name, f.clone(), f.clone()); + } + }); if fleet_name.is_empty() { *new_task_request.fleet_name_mut() = None; } From dec22ac6a0ce90ba95989910bc62d9aa4ee38dcc Mon Sep 17 00:00:00 2001 From: Xiyu Oh Date: Wed, 15 Oct 2025 05:33:14 +0000 Subject: [PATCH 21/21] TODO: check why negotiation loops when goal changes Signed-off-by: Xiyu Oh --- crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs index c63522676..0ab97e84a 100644 --- a/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs +++ b/crates/rmf_site_editor/src/widgets/tasks/go_to_place.rs @@ -161,6 +161,7 @@ impl<'w, 's> WidgetSystem for ViewGoToPlace<'w, 's> { *task.request_mut().description_display_mut() = location_name.clone(); // Update DebugGoal + // TODO(@xiyuoh) debug why negotiation loops if goal changes for existing task if let Some((robot_entity, debug_goal)) = task .robot() .0