diff --git a/rmf_site_editor/src/site/door.rs b/rmf_site_editor/src/site/door.rs index 781820a0..1135bb5f 100644 --- a/rmf_site_editor/src/site/door.rs +++ b/rmf_site_editor/src/site/door.rs @@ -15,7 +15,11 @@ * */ -use crate::{interaction::Selectable, shapes::*, site::*}; +use crate::{ + interaction::{Hovered, Selectable}, + shapes::*, + site::*, +}; use bevy::{ prelude::*, render::mesh::{Indices, PrimitiveTopology}, @@ -26,12 +30,50 @@ pub const DOOR_CUE_HEIGHT: f32 = 0.004; pub const DOOR_STOP_LINE_THICKNESS: f32 = 0.01; pub const DOOR_STOP_LINE_LENGTH: f32 = 3.0 * DEFAULT_DOOR_THICKNESS; pub const DOOR_SWEEP_THICKNESS: f32 = 0.05; +pub const DOUBLE_DOOR_GAP: f32 = 0.05; + +#[derive(Debug, Clone, Copy, PartialEq)] +pub enum DoorBodyType { + SingleSwing { body: Entity }, + DoubleSwing { left: Entity, right: Entity }, + SingleSliding { body: Entity }, + DoubleSliding { left: Entity, right: Entity }, + Model { body: Entity }, +} + +impl DoorBodyType { + pub fn from_door_type(door_type: &DoorType, entities: &Vec) -> Self { + match door_type { + DoorType::SingleSwing(_) => DoorBodyType::SingleSwing { body: entities[0] }, + DoorType::DoubleSwing(_) => DoorBodyType::DoubleSwing { + left: entities[0], + right: entities[1], + }, + DoorType::SingleSliding(_) => DoorBodyType::SingleSliding { body: entities[0] }, + DoorType::DoubleSliding(_) => DoorBodyType::DoubleSliding { + left: entities[0], + right: entities[1], + }, + DoorType::Model(_) => DoorBodyType::Model { body: entities[0] }, + } + } + + pub fn entities(&self) -> Vec { + match self { + DoorBodyType::SingleSwing { body } + | DoorBodyType::SingleSliding { body } + | DoorBodyType::Model { body } => { + vec![*body] + } + DoorBodyType::DoubleSwing { left, right } + | DoorBodyType::DoubleSliding { left, right } => vec![*left, *right], + } + } +} #[derive(Debug, Clone, Copy, Component)] pub struct DoorSegments { - // TODO(MXG): When it's time to animate the doors we should replace this - // with an enum for the different possible door types: Single/Double Swing/Sliding - pub body: Entity, + pub body: DoorBodyType, pub cue_inner: Entity, pub cue_outline: Entity, } @@ -41,7 +83,7 @@ fn make_door_visuals( edge: &Edge, anchors: &AnchorParams, kind: &DoorType, -) -> (Transform, Transform, Mesh, Mesh) { +) -> (Transform, Vec, Mesh, Mesh) { let p_start = anchors .point_in_parent_frame_of(edge.left(), Category::Door, entity) .unwrap(); @@ -55,17 +97,59 @@ fn make_door_visuals( let center = (p_start + p_end) / 2.0; let (inner, outline) = make_door_cues(length, kind); + + let get_double_door_tfs = |mid_offset: f32| -> Vec { + let left_door_length = (length - DOUBLE_DOOR_GAP) / 2.0 - mid_offset; + let right_door_length = (length - DOUBLE_DOOR_GAP) / 2.0 + mid_offset; + vec![ + Transform { + translation: Vec3::new( + 0., + (length + DOUBLE_DOOR_GAP) / 4.0 + mid_offset / 2.0, + DEFAULT_LEVEL_HEIGHT / 2.0, + ), + scale: Vec3::new( + DEFAULT_DOOR_THICKNESS, + left_door_length, + DEFAULT_LEVEL_HEIGHT, + ), + ..default() + }, + Transform { + translation: Vec3::new( + 0., + -(length + DOUBLE_DOOR_GAP) / 4.0 + mid_offset / 2.0, + DEFAULT_LEVEL_HEIGHT / 2.0, + ), + scale: Vec3::new( + DEFAULT_DOOR_THICKNESS, + right_door_length, + DEFAULT_LEVEL_HEIGHT, + ), + ..default() + }, + ] + }; + + let door_tfs = match kind { + // TODO(luca) implement model variant + DoorType::SingleSwing(_) | DoorType::SingleSliding(_) | DoorType::Model(_) => { + vec![Transform { + translation: Vec3::new(0., 0., DEFAULT_LEVEL_HEIGHT / 2.0), + scale: Vec3::new(DEFAULT_DOOR_THICKNESS, length, DEFAULT_LEVEL_HEIGHT), + ..default() + }] + } + DoorType::DoubleSwing(door) => get_double_door_tfs(door.compute_offset(length)), + DoorType::DoubleSliding(door) => get_double_door_tfs(door.compute_offset(length)), + }; ( Transform { translation: Vec3::new(center.x, center.y, 0.), rotation: Quat::from_rotation_z(yaw), ..default() }, - Transform { - translation: Vec3::new(0., 0., DEFAULT_LEVEL_HEIGHT / 2.0), - scale: Vec3::new(DEFAULT_DOOR_THICKNESS, length, DEFAULT_LEVEL_HEIGHT), - ..default() - }, + door_tfs, inner, outline, ) @@ -97,10 +181,16 @@ fn door_slide_arrows(start: f32, stop: f32) -> MeshBuffer { door_slide_arrow(start, stop, -1.0).merge_with(door_slide_arrow(start, stop, 1.0)) } -fn door_swing_arc(door_width: f32, door_count: u32, pivot_on: Side, swing: Swing) -> MeshBuffer { +fn door_swing_arc( + door_width: f32, + door_count: u32, + offset: f32, + pivot_on: Side, + swing: Swing, +) -> MeshBuffer { let pivot = pivot_on.sign() * door_width / 2.0; let pivot = Vec3::new(0.0, pivot, DOOR_CUE_HEIGHT); - let door_width = door_width / door_count as f32; + let door_width = door_width / door_count as f32 + offset; let (initial_angle, sweep) = swing.swing_on_pivot(pivot_on); flat_arc( pivot, @@ -149,11 +239,14 @@ fn make_door_cues(door_width: f32, kind: &DoorType) -> (Mesh, Mesh) { .into_mesh_and_outline() } DoorType::SingleSwing(door) => { - door_swing_arc(door_width, 1, door.pivot_on, door.swing).into_mesh_and_outline() + door_swing_arc(door_width, 1, 0.0, door.pivot_on, door.swing).into_mesh_and_outline() + } + DoorType::DoubleSwing(door) => { + let mid = door.compute_offset(door_width); + door_swing_arc(door_width, 2, -mid, Side::Left, door.swing) + .merge_with(door_swing_arc(door_width, 2, mid, Side::Right, door.swing)) + .into_mesh_and_outline() } - DoorType::DoubleSwing(door) => door_swing_arc(door_width, 2, Side::Left, door.swing) - .merge_with(door_swing_arc(door_width, 2, Side::Right, door.swing)) - .into_mesh_and_outline(), _ => { let mut mesh = Mesh::new(PrimitiveTopology::TriangleList); mesh.insert_attribute(Mesh::ATTRIBUTE_POSITION, Vec::<[f32; 3]>::new()); @@ -179,20 +272,26 @@ pub fn add_door_visuals( mut meshes: ResMut>, ) { for (e, edge, kind, visibility) in &new_doors { - let (pose_tf, shape_tf, cue_inner_mesh, cue_outline_mesh) = + let (pose_tf, door_tfs, cue_inner_mesh, cue_outline_mesh) = make_door_visuals(e, edge, &anchors, kind); let mut commands = commands.entity(e); let (body, cue_inner, cue_outline) = commands.add_children(|parent| { - let body = parent - .spawn(PbrBundle { - mesh: assets.box_mesh.clone(), - material: assets.door_body_material.clone(), - transform: shape_tf, - ..default() + let bodies = door_tfs + .iter() + .map(|tf| { + parent + .spawn(PbrBundle { + mesh: assets.box_mesh.clone(), + material: assets.door_body_material.clone(), + transform: *tf, + ..default() + }) + .insert(Selectable::new(e)) + .id() }) - .insert(Selectable::new(e)) - .id(); + .collect::>(); + let body = DoorBodyType::from_door_type(kind, &bodies); let cue_inner = parent .spawn(PbrBundle { @@ -245,6 +344,7 @@ pub fn add_door_visuals( } fn update_door_visuals( + commands: &mut Commands, entity: Entity, edge: &Edge, kind: &DoorType, @@ -253,45 +353,92 @@ fn update_door_visuals( transforms: &mut Query<&mut Transform>, mesh_handles: &mut Query<&mut Handle>, mesh_assets: &mut ResMut>, -) { - let (pose_tf, shape_tf, cue_inner_mesh, cue_outline_mesh) = + assets: &Res, +) -> Option { + let (pose_tf, door_tfs, cue_inner_mesh, cue_outline_mesh) = make_door_visuals(entity, edge, anchors, kind); let mut door_transform = transforms.get_mut(entity).unwrap(); *door_transform = pose_tf; - let mut shape_transform = transforms.get_mut(segments.body).unwrap(); - *shape_transform = shape_tf; + let mut entities = segments.body.entities(); + for (door_tf, e) in door_tfs.iter().zip(entities.iter()) { + let mut door_transform = transforms.get_mut(*e).unwrap(); + *door_transform = *door_tf; + } + for door_tf in door_tfs.iter().skip(entities.len()) { + // New doors were added, we need to spawn them + let id = commands + .spawn(PbrBundle { + mesh: assets.box_mesh.clone(), + material: assets.door_body_material.clone(), + transform: *door_tf, + ..default() + }) + .insert(Selectable::new(entity)) + .id(); + entities.push(id); + commands.entity(entity).add_child(id); + } + for e in entities.iter().skip(door_tfs.len()) { + // Doors were removed, we need to despawn them + commands.entity(*e).despawn_recursive(); + } let mut cue_inner = mesh_handles.get_mut(segments.cue_inner).unwrap(); *cue_inner = mesh_assets.add(cue_inner_mesh); let mut cue_outline = mesh_handles.get_mut(segments.cue_outline).unwrap(); *cue_outline = mesh_assets.add(cue_outline_mesh); + let new_segments = DoorBodyType::from_door_type(kind, &entities); + if new_segments != segments.body { + Some(new_segments) + } else { + None + } } pub fn update_changed_door( - doors: Query< - (Entity, &Edge, &DoorType, &DoorSegments), + mut commands: Commands, + mut doors: Query< + ( + Entity, + &Edge, + &DoorType, + &mut DoorSegments, + &mut Hovered, + ), Or<(Changed>, Changed)>, >, anchors: AnchorParams, mut transforms: Query<&mut Transform>, mut mesh_handles: Query<&mut Handle>, mut mesh_assets: ResMut>, + assets: Res, ) { - for (entity, edge, kind, segments) in &doors { - update_door_visuals( + for (entity, edge, kind, mut segments, mut hovered) in &mut doors { + let old_door_count = segments.body.entities().len(); + if let Some(new_body) = update_door_visuals( + &mut commands, entity, edge, kind, - segments, + &segments, &anchors, &mut transforms, &mut mesh_handles, &mut mesh_assets, - ); + &assets, + ) { + segments.body = new_body; + if segments.body.entities().len() > old_door_count { + // A new door was spawned, trigger hovered change detection to update the outline + // for the new mesh + hovered.set_changed(); + } + } } } pub fn update_door_for_moved_anchors( - doors: Query<(Entity, &Edge, &DoorType, &DoorSegments)>, + mut commands: Commands, + mut doors: Query<(Entity, &Edge, &DoorType, &DoorSegments)>, anchors: AnchorParams, changed_anchors: Query< &Dependents, @@ -303,19 +450,22 @@ pub fn update_door_for_moved_anchors( mut transforms: Query<&mut Transform>, mut mesh_handles: Query<&mut Handle>, mut mesh_assets: ResMut>, + assets: Res, ) { for dependents in &changed_anchors { for dependent in dependents.iter() { - if let Some((entity, edge, kind, segments)) = doors.get(*dependent).ok() { + if let Some((entity, edge, kind, mut segments)) = doors.get_mut(*dependent).ok() { update_door_visuals( + &mut commands, entity, edge, kind, - segments, + &segments, &anchors, &mut transforms, &mut mesh_handles, &mut mesh_assets, + &assets, ); } } diff --git a/rmf_site_editor/src/widgets/inspector/inspect_door.rs b/rmf_site_editor/src/widgets/inspector/inspect_door.rs index db56bcb2..de754ad0 100644 --- a/rmf_site_editor/src/widgets/inspector/inspect_door.rs +++ b/rmf_site_editor/src/widgets/inspector/inspect_door.rs @@ -48,6 +48,18 @@ impl<'a> InspectDoorType<'a> { }); }); + fn left_right_ratio_ui(ui: &mut Ui, mut ratio: &mut f32) { + ui.horizontal(|ui| { + ui.label("Left : Right"); + ui.add( + DragValue::new(ratio) + .speed(0.01) + .clamp_range(0.01..=std::f32::INFINITY), + ) + .on_hover_text("(Left Door Length)/(Right Door Length)"); + }); + }; + match &mut new_kind { DoorType::SingleSliding(door) => { ui.horizontal(|ui| { @@ -57,15 +69,7 @@ impl<'a> InspectDoorType<'a> { }); } DoorType::DoubleSliding(door) => { - ui.horizontal(|ui| { - ui.label("Left : Right"); - ui.add( - DragValue::new(&mut door.left_right_ratio) - .speed(0.01) - .clamp_range(0.0..=std::f32::INFINITY), - ) - .on_hover_text("(Left Door Length)/(Right Door Length)"); - }); + left_right_ratio_ui(ui, &mut door.left_right_ratio); } DoorType::SingleSwing(door) => { ui.horizontal(|ui| { @@ -77,6 +81,7 @@ impl<'a> InspectDoorType<'a> { } DoorType::DoubleSwing(door) => { InspectSwing::new(&mut door.swing).show(ui); + left_right_ratio_ui(ui, &mut door.left_right_ratio); } DoorType::Model(_) => { ui.label("Not yet supported"); diff --git a/rmf_site_format/src/door.rs b/rmf_site_format/src/door.rs index aaeef5bb..8cfc6656 100644 --- a/rmf_site_format/src/door.rs +++ b/rmf_site_format/src/door.rs @@ -186,12 +186,26 @@ impl From for DoorType { #[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] pub struct DoubleSwingDoor { pub swing: Swing, + /// Length of the left door divided by the length of the right door + pub left_right_ratio: f32, +} + +impl DoubleSwingDoor { + /// Get the offset from the door center of the point where the doors + /// separate. A value of 0.0 means the doors are even. A negative value + /// means the left door is smaller while a positive value means the right + /// door is smaller. + pub fn compute_offset(&self, door_width: f32) -> f32 { + let l = self.left_right_ratio * door_width / (self.left_right_ratio + 1.0); + return door_width / 2.0 - l; + } } impl Default for DoubleSwingDoor { fn default() -> Self { Self { swing: Swing::Forward(Angle::Deg(90.0)), + left_right_ratio: 1.0, } } } diff --git a/rmf_site_format/src/legacy/door.rs b/rmf_site_format/src/legacy/door.rs index 9014b8e5..afe694b3 100644 --- a/rmf_site_format/src/legacy/door.rs +++ b/rmf_site_format/src/legacy/door.rs @@ -95,6 +95,7 @@ impl Door { .into(), DoorType::DoubleSwing | DoorType::DoubleHinged => DoubleSwingDoor { swing: self.to_swing()?, + left_right_ratio: 1. / self.2.right_left_ratio.1 as f32, } .into(), DoorType::Unknown => return Err(PortingError::InvalidType(self.2.type_.1.clone())),