use super::ViewerSettings;
use bevy::light::NotShadowCaster;
use bevy::prelude::*;
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum RoadArrangement {
SingleSide,
Staggered,
Opposite,
}
fn determine_road_arrangement(settings: &ViewerSettings) -> RoadArrangement {
let road_width = settings.num_lanes as f32 * settings.lane_width;
let ratio = road_width / settings.mounting_height;
if ratio < 1.0 {
RoadArrangement::SingleSide
} else if ratio < 1.5 {
RoadArrangement::Staggered
} else {
RoadArrangement::Opposite
}
}
pub struct ScenePlugin;
impl Plugin for ScenePlugin {
fn build(&self, app: &mut App) {
app.init_resource::<ViewerSettings>();
app.add_systems(
Startup,
setup_scene.run_if(resource_exists::<ViewerSettings>),
)
.add_systems(
Update,
rebuild_scene_on_change.run_if(resource_exists::<ViewerSettings>),
);
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum SceneType {
#[default]
Room,
Road,
Parking,
Outdoor,
}
impl SceneType {
pub fn default_dimensions(&self) -> (f32, f32, f32, f32) {
match self {
SceneType::Room => (4.0, 5.0, 2.8, 2.5),
SceneType::Road => (11.0, 100.0, 0.0, 8.0),
SceneType::Parking => (20.0, 30.0, 0.0, 6.0),
SceneType::Outdoor => (10.0, 15.0, 0.0, 3.0),
}
}
}
#[derive(Component)]
pub struct SceneGeometry;
fn setup_scene(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
settings: Res<ViewerSettings>,
) {
build_scene(&mut commands, &mut meshes, &mut materials, &settings);
}
fn rebuild_scene_on_change(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
mut materials: ResMut<Assets<StandardMaterial>>,
settings: Res<ViewerSettings>,
query: Query<Entity, With<SceneGeometry>>,
) {
if !settings.is_changed() {
return;
}
for entity in query.iter() {
commands.entity(entity).despawn();
}
build_scene(&mut commands, &mut meshes, &mut materials, &settings);
}
fn build_scene(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
settings: &ViewerSettings,
) {
match settings.scene_type {
SceneType::Room => build_room(commands, meshes, materials, settings),
SceneType::Road => build_road(commands, meshes, materials, settings),
SceneType::Parking => build_parking(commands, meshes, materials, settings),
SceneType::Outdoor => build_outdoor(commands, meshes, materials, settings),
}
commands.insert_resource(bevy::light::GlobalAmbientLight {
color: Color::srgb(0.9, 0.9, 1.0),
brightness: 50.0, affects_lightmapped_meshes: true,
});
}
fn build_room(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
settings: &ViewerSettings,
) {
let w = settings.room_width;
let l = settings.room_length;
let h = settings.room_height;
let floor_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.85, 0.85, 0.85),
perceptual_roughness: 0.8,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
MeshMaterial3d(floor_material.clone()),
Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
SceneGeometry,
));
let ceiling_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.95, 0.95, 0.95),
perceptual_roughness: 0.9,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
MeshMaterial3d(ceiling_material),
Transform::from_xyz(w / 2.0, h, l / 2.0)
.with_rotation(Quat::from_rotation_x(std::f32::consts::PI)),
SceneGeometry,
));
let wall_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.95, 0.95, 0.95),
perceptual_roughness: 0.9,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(w, h))),
MeshMaterial3d(wall_material.clone()),
Transform::from_xyz(w / 2.0, h / 2.0, 0.0)
.with_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
SceneGeometry,
));
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(w, h))),
MeshMaterial3d(wall_material.clone()),
Transform::from_xyz(w / 2.0, h / 2.0, l)
.with_rotation(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2)),
SceneGeometry,
));
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(l, h))),
MeshMaterial3d(wall_material.clone()),
Transform::from_xyz(0.0, h / 2.0, l / 2.0)
.with_rotation(Quat::from_rotation_z(-std::f32::consts::FRAC_PI_2)),
SceneGeometry,
));
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(l, h))),
MeshMaterial3d(wall_material),
Transform::from_xyz(w, h / 2.0, l / 2.0)
.with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
SceneGeometry,
));
spawn_pendulum_cable(commands, meshes, materials, settings, w / 2.0, l / 2.0);
}
fn build_road(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
settings: &ViewerSettings,
) {
let lane_w = settings.lane_width;
let num_lanes = settings.num_lanes;
let sidewalk_w = settings.sidewalk_width;
let road_width = num_lanes as f32 * lane_w; let total_width = road_width + 2.0 * sidewalk_w; let road_length = settings.room_length;
let pole_spacing = settings.effective_pole_spacing();
let arrangement = determine_road_arrangement(settings);
let road_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.15, 0.15, 0.15),
perceptual_roughness: 0.9,
..default()
});
let sidewalk_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.6, 0.6, 0.6),
perceptual_roughness: 0.8,
..default()
});
let marking_material = materials.add(StandardMaterial {
base_color: Color::WHITE,
emissive: LinearRgba::new(0.3, 0.3, 0.3, 1.0),
..default()
});
let yellow_marking = materials.add(StandardMaterial {
base_color: Color::srgb(1.0, 0.85, 0.0),
emissive: LinearRgba::new(0.3, 0.25, 0.0, 1.0),
..default()
});
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(road_width, road_length))),
MeshMaterial3d(road_material),
Transform::from_xyz(sidewalk_w + road_width / 2.0, 0.0, road_length / 2.0),
SceneGeometry,
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(sidewalk_w, 0.15, road_length))),
MeshMaterial3d(sidewalk_material.clone()),
Transform::from_xyz(sidewalk_w / 2.0, 0.075, road_length / 2.0),
SceneGeometry,
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(sidewalk_w, 0.15, road_length))),
MeshMaterial3d(sidewalk_material.clone()),
Transform::from_xyz(total_width - sidewalk_w / 2.0, 0.075, road_length / 2.0),
SceneGeometry,
));
let center_x = sidewalk_w + road_width / 2.0;
let mut z = 1.0;
while z < road_length - 1.0 {
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(0.12, 0.02, 3.0))),
MeshMaterial3d(yellow_marking.clone()),
Transform::from_xyz(center_x - 0.15, 0.01, z + 1.5),
SceneGeometry,
));
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(0.12, 0.02, 3.0))),
MeshMaterial3d(yellow_marking.clone()),
Transform::from_xyz(center_x + 0.15, 0.01, z + 1.5),
SceneGeometry,
));
z += 4.0;
}
for lane_idx in 0..num_lanes {
if lane_idx == 0 {
let edge_x = sidewalk_w + 0.15;
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(0.15, 0.02, road_length - 2.0))),
MeshMaterial3d(marking_material.clone()),
Transform::from_xyz(edge_x, 0.01, road_length / 2.0),
SceneGeometry,
));
}
if lane_idx == num_lanes - 1 {
let edge_x = sidewalk_w + road_width - 0.15;
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(0.15, 0.02, road_length - 2.0))),
MeshMaterial3d(marking_material.clone()),
Transform::from_xyz(edge_x, 0.01, road_length / 2.0),
SceneGeometry,
));
}
}
let num_poles = ((road_length / pole_spacing).floor() as i32).max(1);
let actual_spacing = road_length / (num_poles as f32 + 1.0);
let middle_pole_spacing = 50.0;
match arrangement {
RoadArrangement::SingleSide => {
for i in 1..=num_poles {
let z = i as f32 * actual_spacing;
spawn_pole(
commands,
meshes,
materials,
Vec3::new(total_width - sidewalk_w / 2.0, 0.0, z),
settings.mounting_height,
);
}
}
RoadArrangement::Staggered => {
for i in 1..=num_poles {
let z = i as f32 * actual_spacing;
let x = if i % 2 == 0 {
sidewalk_w / 2.0 } else {
total_width - sidewalk_w / 2.0 };
spawn_pole(
commands,
meshes,
materials,
Vec3::new(x, 0.0, z),
settings.mounting_height,
);
}
}
RoadArrangement::Opposite => {
for i in 1..=num_poles {
let z = i as f32 * actual_spacing;
spawn_pole(
commands,
meshes,
materials,
Vec3::new(sidewalk_w / 2.0, 0.0, z),
settings.mounting_height,
);
spawn_pole(
commands,
meshes,
materials,
Vec3::new(total_width - sidewalk_w / 2.0, 0.0, z),
settings.mounting_height,
);
}
if road_width > 6.0 {
let num_middle_poles = ((road_length / middle_pole_spacing).floor() as i32).max(0);
for i in 1..=num_middle_poles {
let z = i as f32 * middle_pole_spacing;
spawn_dual_arm_pole(
commands,
meshes,
materials,
Vec3::new(center_x, 0.0, z),
settings.mounting_height,
);
}
}
}
}
}
fn spawn_dual_arm_pole(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
base_position: Vec3,
height: f32,
) {
let pole_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.3, 0.3, 0.35),
metallic: 0.8,
perceptual_roughness: 0.4,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Cylinder::new(0.1, height))),
MeshMaterial3d(pole_material.clone()),
Transform::from_xyz(base_position.x, height / 2.0, base_position.z),
SceneGeometry,
));
let arm_length = 2.0;
commands.spawn((
Mesh3d(meshes.add(Cylinder::new(0.05, arm_length))),
MeshMaterial3d(pole_material.clone()),
Transform::from_xyz(
base_position.x - arm_length / 2.0,
height - 0.25,
base_position.z,
)
.with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
SceneGeometry,
));
commands.spawn((
Mesh3d(meshes.add(Cylinder::new(0.05, arm_length))),
MeshMaterial3d(pole_material),
Transform::from_xyz(
base_position.x + arm_length / 2.0,
height - 0.25,
base_position.z,
)
.with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
SceneGeometry,
));
}
fn build_parking(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
settings: &ViewerSettings,
) {
let w = settings.room_width;
let l = settings.room_length;
let lot_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.2, 0.2, 0.2),
perceptual_roughness: 0.85,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
MeshMaterial3d(lot_material),
Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
SceneGeometry,
));
let line_material = materials.add(StandardMaterial {
base_color: Color::WHITE,
emissive: LinearRgba::new(0.2, 0.2, 0.2, 1.0),
..default()
});
let space_width = 2.5;
let space_length = 5.0;
let mut row = 3.0;
while row < l - 3.0 {
let mut col = space_width;
while col < w - 1.0 {
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(0.1, 0.02, space_length))),
MeshMaterial3d(line_material.clone()),
Transform::from_xyz(col, 0.01, row),
SceneGeometry,
));
col += space_width;
}
row += space_length + 1.0;
}
spawn_pole(
commands,
meshes,
materials,
Vec3::new(w / 2.0, 0.0, l / 2.0),
settings.mounting_height,
);
}
fn build_outdoor(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
settings: &ViewerSettings,
) {
let w = settings.room_width;
let l = settings.room_length;
let grass_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.15, 0.3, 0.1),
perceptual_roughness: 0.95,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
MeshMaterial3d(grass_material),
Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
SceneGeometry,
));
let path_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.5, 0.5, 0.5),
perceptual_roughness: 0.8,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Cuboid::new(1.2, 0.02, l - 2.0))),
MeshMaterial3d(path_material),
Transform::from_xyz(w / 2.0, 0.01, l / 2.0),
SceneGeometry,
));
let bush_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.1, 0.25, 0.05),
perceptual_roughness: 0.95,
..default()
});
for (x, y, z) in [
(2.0, 0.4, 3.0),
(w - 2.0, 0.3, l - 4.0),
(1.5, 0.35, l - 2.0),
] {
commands.spawn((
Mesh3d(meshes.add(Sphere::new(y))),
MeshMaterial3d(bush_material.clone()),
Transform::from_xyz(x, y, z),
SceneGeometry,
));
}
spawn_pole(
commands,
meshes,
materials,
Vec3::new(w / 2.0, 0.0, l / 2.0),
settings.mounting_height,
);
}
fn spawn_pole(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
position: Vec3,
height: f32,
) {
let pole_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.4, 0.4, 0.4),
metallic: 0.6,
perceptual_roughness: 0.4,
..default()
});
commands.spawn((
Mesh3d(meshes.add(Cylinder::new(0.08, height - 0.3))),
MeshMaterial3d(pole_material.clone()),
Transform::from_xyz(position.x, height / 2.0, position.z),
SceneGeometry,
NotShadowCaster,
));
commands.spawn((
Mesh3d(meshes.add(Cylinder::new(0.05, 0.3))),
MeshMaterial3d(pole_material),
Transform::from_xyz(position.x - 0.05, height - 0.2, position.z)
.with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
SceneGeometry,
NotShadowCaster,
));
}
fn spawn_pendulum_cable(
commands: &mut Commands,
meshes: &mut ResMut<Assets<Mesh>>,
materials: &mut ResMut<Assets<StandardMaterial>>,
settings: &ViewerSettings,
x: f32,
z: f32,
) {
if settings.pendulum_length <= 0.0 {
return;
}
let cable_material = materials.add(StandardMaterial {
base_color: Color::srgb(0.2, 0.2, 0.2),
metallic: 0.3,
perceptual_roughness: 0.6,
..default()
});
let cable_top = settings.room_height;
let cable_bottom = settings.room_height - settings.pendulum_length;
let cable_center_y = (cable_top + cable_bottom) / 2.0;
commands.spawn((
Mesh3d(meshes.add(Cylinder::new(0.01, settings.pendulum_length))),
MeshMaterial3d(cable_material),
Transform::from_xyz(x, cable_center_y, z),
SceneGeometry,
NotShadowCaster,
));
}