1use super::ViewerSettings;
6use bevy::light::NotShadowCaster;
7use bevy::prelude::*;
8
9#[derive(Clone, Copy, PartialEq, Eq)]
11pub enum RoadArrangement {
12 SingleSide,
14 Staggered,
16 Opposite,
18}
19
20fn determine_road_arrangement(settings: &ViewerSettings) -> RoadArrangement {
31 let road_width = settings.num_lanes as f32 * settings.lane_width;
32 let ratio = road_width / settings.mounting_height;
33
34 if ratio < 1.0 {
35 RoadArrangement::SingleSide
36 } else if ratio < 1.5 {
37 RoadArrangement::Staggered
38 } else {
39 RoadArrangement::Opposite
42 }
43}
44
45pub struct ScenePlugin;
47
48impl Plugin for ScenePlugin {
49 fn build(&self, app: &mut App) {
50 app.init_resource::<ViewerSettings>();
51 app.add_systems(
52 Startup,
53 setup_scene.run_if(resource_exists::<ViewerSettings>),
54 )
55 .add_systems(
56 Update,
57 rebuild_scene_on_change.run_if(resource_exists::<ViewerSettings>),
58 );
59 }
60}
61
62#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
64pub enum SceneType {
65 #[default]
67 Room,
68 Road,
70 Parking,
72 Outdoor,
74}
75
76impl SceneType {
77 pub fn default_dimensions(&self) -> (f32, f32, f32, f32) {
81 match self {
82 SceneType::Room => (4.0, 5.0, 2.8, 2.5),
83 SceneType::Road => (11.0, 100.0, 0.0, 8.0),
85 SceneType::Parking => (20.0, 30.0, 0.0, 6.0),
86 SceneType::Outdoor => (10.0, 15.0, 0.0, 3.0),
87 }
88 }
89}
90
91#[derive(Component)]
93pub struct SceneGeometry;
94
95fn setup_scene(
96 mut commands: Commands,
97 mut meshes: ResMut<Assets<Mesh>>,
98 mut materials: ResMut<Assets<StandardMaterial>>,
99 settings: Res<ViewerSettings>,
100) {
101 build_scene(&mut commands, &mut meshes, &mut materials, &settings);
102}
103
104fn rebuild_scene_on_change(
105 mut commands: Commands,
106 mut meshes: ResMut<Assets<Mesh>>,
107 mut materials: ResMut<Assets<StandardMaterial>>,
108 settings: Res<ViewerSettings>,
109 query: Query<Entity, With<SceneGeometry>>,
110) {
111 if !settings.is_changed() {
112 return;
113 }
114
115 for entity in query.iter() {
117 commands.entity(entity).despawn();
118 }
119
120 build_scene(&mut commands, &mut meshes, &mut materials, &settings);
122}
123
124fn build_scene(
125 commands: &mut Commands,
126 meshes: &mut ResMut<Assets<Mesh>>,
127 materials: &mut ResMut<Assets<StandardMaterial>>,
128 settings: &ViewerSettings,
129) {
130 match settings.scene_type {
131 SceneType::Room => build_room(commands, meshes, materials, settings),
132 SceneType::Road => build_road(commands, meshes, materials, settings),
133 SceneType::Parking => build_parking(commands, meshes, materials, settings),
134 SceneType::Outdoor => build_outdoor(commands, meshes, materials, settings),
135 }
136
137 commands.insert_resource(bevy::light::GlobalAmbientLight {
140 color: Color::srgb(0.9, 0.9, 1.0),
141 brightness: 50.0, affects_lightmapped_meshes: true,
143 });
144}
145
146fn build_room(
147 commands: &mut Commands,
148 meshes: &mut ResMut<Assets<Mesh>>,
149 materials: &mut ResMut<Assets<StandardMaterial>>,
150 settings: &ViewerSettings,
151) {
152 let w = settings.room_width;
153 let l = settings.room_length;
154 let h = settings.room_height;
155
156 let floor_material = materials.add(StandardMaterial {
158 base_color: Color::srgb(0.85, 0.85, 0.85),
159 perceptual_roughness: 0.8,
160 ..default()
161 });
162
163 commands.spawn((
164 Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
165 MeshMaterial3d(floor_material.clone()),
166 Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
167 SceneGeometry,
168 ));
169
170 let ceiling_material = materials.add(StandardMaterial {
172 base_color: Color::srgb(0.95, 0.95, 0.95),
173 perceptual_roughness: 0.9,
174 ..default()
175 });
176
177 commands.spawn((
178 Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
179 MeshMaterial3d(ceiling_material),
180 Transform::from_xyz(w / 2.0, h, l / 2.0)
181 .with_rotation(Quat::from_rotation_x(std::f32::consts::PI)),
182 SceneGeometry,
183 ));
184
185 let wall_material = materials.add(StandardMaterial {
187 base_color: Color::srgb(0.95, 0.95, 0.95),
188 perceptual_roughness: 0.9,
189 ..default()
190 });
191
192 commands.spawn((
194 Mesh3d(meshes.add(Plane3d::default().mesh().size(w, h))),
195 MeshMaterial3d(wall_material.clone()),
196 Transform::from_xyz(w / 2.0, h / 2.0, 0.0)
197 .with_rotation(Quat::from_rotation_x(-std::f32::consts::FRAC_PI_2)),
198 SceneGeometry,
199 ));
200
201 commands.spawn((
203 Mesh3d(meshes.add(Plane3d::default().mesh().size(w, h))),
204 MeshMaterial3d(wall_material.clone()),
205 Transform::from_xyz(w / 2.0, h / 2.0, l)
206 .with_rotation(Quat::from_rotation_x(std::f32::consts::FRAC_PI_2)),
207 SceneGeometry,
208 ));
209
210 commands.spawn((
212 Mesh3d(meshes.add(Plane3d::default().mesh().size(l, h))),
213 MeshMaterial3d(wall_material.clone()),
214 Transform::from_xyz(0.0, h / 2.0, l / 2.0)
215 .with_rotation(Quat::from_rotation_z(-std::f32::consts::FRAC_PI_2)),
216 SceneGeometry,
217 ));
218
219 commands.spawn((
221 Mesh3d(meshes.add(Plane3d::default().mesh().size(l, h))),
222 MeshMaterial3d(wall_material),
223 Transform::from_xyz(w, h / 2.0, l / 2.0)
224 .with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
225 SceneGeometry,
226 ));
227
228 spawn_pendulum_cable(commands, meshes, materials, settings, w / 2.0, l / 2.0);
230}
231
232fn build_road(
233 commands: &mut Commands,
234 meshes: &mut ResMut<Assets<Mesh>>,
235 materials: &mut ResMut<Assets<StandardMaterial>>,
236 settings: &ViewerSettings,
237) {
238 let lane_w = settings.lane_width;
240 let num_lanes = settings.num_lanes;
241 let sidewalk_w = settings.sidewalk_width;
242 let road_width = num_lanes as f32 * lane_w; let total_width = road_width + 2.0 * sidewalk_w; let road_length = settings.room_length;
245 let pole_spacing = settings.effective_pole_spacing();
246
247 let arrangement = determine_road_arrangement(settings);
249
250 let road_material = materials.add(StandardMaterial {
252 base_color: Color::srgb(0.15, 0.15, 0.15),
253 perceptual_roughness: 0.9,
254 ..default()
255 });
256
257 let sidewalk_material = materials.add(StandardMaterial {
258 base_color: Color::srgb(0.6, 0.6, 0.6),
259 perceptual_roughness: 0.8,
260 ..default()
261 });
262
263 let marking_material = materials.add(StandardMaterial {
264 base_color: Color::WHITE,
265 emissive: LinearRgba::new(0.3, 0.3, 0.3, 1.0),
266 ..default()
267 });
268
269 let yellow_marking = materials.add(StandardMaterial {
270 base_color: Color::srgb(1.0, 0.85, 0.0),
271 emissive: LinearRgba::new(0.3, 0.25, 0.0, 1.0),
272 ..default()
273 });
274
275 commands.spawn((
277 Mesh3d(meshes.add(Plane3d::default().mesh().size(road_width, road_length))),
278 MeshMaterial3d(road_material),
279 Transform::from_xyz(sidewalk_w + road_width / 2.0, 0.0, road_length / 2.0),
280 SceneGeometry,
281 ));
282
283 commands.spawn((
285 Mesh3d(meshes.add(Cuboid::new(sidewalk_w, 0.15, road_length))),
286 MeshMaterial3d(sidewalk_material.clone()),
287 Transform::from_xyz(sidewalk_w / 2.0, 0.075, road_length / 2.0),
288 SceneGeometry,
289 ));
290
291 commands.spawn((
293 Mesh3d(meshes.add(Cuboid::new(sidewalk_w, 0.15, road_length))),
294 MeshMaterial3d(sidewalk_material.clone()),
295 Transform::from_xyz(total_width - sidewalk_w / 2.0, 0.075, road_length / 2.0),
296 SceneGeometry,
297 ));
298
299 let center_x = sidewalk_w + road_width / 2.0;
301 let mut z = 1.0;
302 while z < road_length - 1.0 {
303 commands.spawn((
305 Mesh3d(meshes.add(Cuboid::new(0.12, 0.02, 3.0))),
306 MeshMaterial3d(yellow_marking.clone()),
307 Transform::from_xyz(center_x - 0.15, 0.01, z + 1.5),
308 SceneGeometry,
309 ));
310 commands.spawn((
311 Mesh3d(meshes.add(Cuboid::new(0.12, 0.02, 3.0))),
312 MeshMaterial3d(yellow_marking.clone()),
313 Transform::from_xyz(center_x + 0.15, 0.01, z + 1.5),
314 SceneGeometry,
315 ));
316 z += 4.0;
317 }
318
319 for lane_idx in 0..num_lanes {
321 if lane_idx == 0 {
322 let edge_x = sidewalk_w + 0.15;
324 commands.spawn((
325 Mesh3d(meshes.add(Cuboid::new(0.15, 0.02, road_length - 2.0))),
326 MeshMaterial3d(marking_material.clone()),
327 Transform::from_xyz(edge_x, 0.01, road_length / 2.0),
328 SceneGeometry,
329 ));
330 }
331 if lane_idx == num_lanes - 1 {
332 let edge_x = sidewalk_w + road_width - 0.15;
334 commands.spawn((
335 Mesh3d(meshes.add(Cuboid::new(0.15, 0.02, road_length - 2.0))),
336 MeshMaterial3d(marking_material.clone()),
337 Transform::from_xyz(edge_x, 0.01, road_length / 2.0),
338 SceneGeometry,
339 ));
340 }
341 }
342
343 let num_poles = ((road_length / pole_spacing).floor() as i32).max(1);
346 let actual_spacing = road_length / (num_poles as f32 + 1.0);
347
348 let middle_pole_spacing = 50.0;
350
351 match arrangement {
352 RoadArrangement::SingleSide => {
353 for i in 1..=num_poles {
355 let z = i as f32 * actual_spacing;
356 spawn_pole(
357 commands,
358 meshes,
359 materials,
360 Vec3::new(total_width - sidewalk_w / 2.0, 0.0, z),
361 settings.mounting_height,
362 );
363 }
364 }
365 RoadArrangement::Staggered => {
366 for i in 1..=num_poles {
368 let z = i as f32 * actual_spacing;
369 let x = if i % 2 == 0 {
370 sidewalk_w / 2.0 } else {
372 total_width - sidewalk_w / 2.0 };
374 spawn_pole(
375 commands,
376 meshes,
377 materials,
378 Vec3::new(x, 0.0, z),
379 settings.mounting_height,
380 );
381 }
382 }
383 RoadArrangement::Opposite => {
384 for i in 1..=num_poles {
387 let z = i as f32 * actual_spacing;
388 spawn_pole(
389 commands,
390 meshes,
391 materials,
392 Vec3::new(sidewalk_w / 2.0, 0.0, z),
393 settings.mounting_height,
394 );
395 spawn_pole(
396 commands,
397 meshes,
398 materials,
399 Vec3::new(total_width - sidewalk_w / 2.0, 0.0, z),
400 settings.mounting_height,
401 );
402 }
403
404 if road_width > 6.0 {
406 let num_middle_poles = ((road_length / middle_pole_spacing).floor() as i32).max(0);
407 for i in 1..=num_middle_poles {
408 let z = i as f32 * middle_pole_spacing;
409 spawn_dual_arm_pole(
410 commands,
411 meshes,
412 materials,
413 Vec3::new(center_x, 0.0, z),
414 settings.mounting_height,
415 );
416 }
417 }
418 }
419 }
420}
421
422fn spawn_dual_arm_pole(
424 commands: &mut Commands,
425 meshes: &mut ResMut<Assets<Mesh>>,
426 materials: &mut ResMut<Assets<StandardMaterial>>,
427 base_position: Vec3,
428 height: f32,
429) {
430 let pole_material = materials.add(StandardMaterial {
431 base_color: Color::srgb(0.3, 0.3, 0.35),
432 metallic: 0.8,
433 perceptual_roughness: 0.4,
434 ..default()
435 });
436
437 commands.spawn((
439 Mesh3d(meshes.add(Cylinder::new(0.1, height))),
440 MeshMaterial3d(pole_material.clone()),
441 Transform::from_xyz(base_position.x, height / 2.0, base_position.z),
442 SceneGeometry,
443 ));
444
445 let arm_length = 2.0;
447 commands.spawn((
448 Mesh3d(meshes.add(Cylinder::new(0.05, arm_length))),
449 MeshMaterial3d(pole_material.clone()),
450 Transform::from_xyz(
451 base_position.x - arm_length / 2.0,
452 height - 0.25,
453 base_position.z,
454 )
455 .with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
456 SceneGeometry,
457 ));
458
459 commands.spawn((
461 Mesh3d(meshes.add(Cylinder::new(0.05, arm_length))),
462 MeshMaterial3d(pole_material),
463 Transform::from_xyz(
464 base_position.x + arm_length / 2.0,
465 height - 0.25,
466 base_position.z,
467 )
468 .with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
469 SceneGeometry,
470 ));
471}
472
473fn build_parking(
474 commands: &mut Commands,
475 meshes: &mut ResMut<Assets<Mesh>>,
476 materials: &mut ResMut<Assets<StandardMaterial>>,
477 settings: &ViewerSettings,
478) {
479 let w = settings.room_width;
480 let l = settings.room_length;
481
482 let lot_material = materials.add(StandardMaterial {
484 base_color: Color::srgb(0.2, 0.2, 0.2),
485 perceptual_roughness: 0.85,
486 ..default()
487 });
488
489 commands.spawn((
490 Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
491 MeshMaterial3d(lot_material),
492 Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
493 SceneGeometry,
494 ));
495
496 let line_material = materials.add(StandardMaterial {
498 base_color: Color::WHITE,
499 emissive: LinearRgba::new(0.2, 0.2, 0.2, 1.0),
500 ..default()
501 });
502
503 let space_width = 2.5;
504 let space_length = 5.0;
505
506 let mut row = 3.0;
507 while row < l - 3.0 {
508 let mut col = space_width;
509 while col < w - 1.0 {
510 commands.spawn((
511 Mesh3d(meshes.add(Cuboid::new(0.1, 0.02, space_length))),
512 MeshMaterial3d(line_material.clone()),
513 Transform::from_xyz(col, 0.01, row),
514 SceneGeometry,
515 ));
516 col += space_width;
517 }
518 row += space_length + 1.0;
519 }
520
521 spawn_pole(
523 commands,
524 meshes,
525 materials,
526 Vec3::new(w / 2.0, 0.0, l / 2.0),
527 settings.mounting_height,
528 );
529}
530
531fn build_outdoor(
532 commands: &mut Commands,
533 meshes: &mut ResMut<Assets<Mesh>>,
534 materials: &mut ResMut<Assets<StandardMaterial>>,
535 settings: &ViewerSettings,
536) {
537 let w = settings.room_width;
538 let l = settings.room_length;
539
540 let grass_material = materials.add(StandardMaterial {
542 base_color: Color::srgb(0.15, 0.3, 0.1),
543 perceptual_roughness: 0.95,
544 ..default()
545 });
546
547 commands.spawn((
548 Mesh3d(meshes.add(Plane3d::default().mesh().size(w, l))),
549 MeshMaterial3d(grass_material),
550 Transform::from_xyz(w / 2.0, 0.0, l / 2.0),
551 SceneGeometry,
552 ));
553
554 let path_material = materials.add(StandardMaterial {
556 base_color: Color::srgb(0.5, 0.5, 0.5),
557 perceptual_roughness: 0.8,
558 ..default()
559 });
560
561 commands.spawn((
562 Mesh3d(meshes.add(Cuboid::new(1.2, 0.02, l - 2.0))),
563 MeshMaterial3d(path_material),
564 Transform::from_xyz(w / 2.0, 0.01, l / 2.0),
565 SceneGeometry,
566 ));
567
568 let bush_material = materials.add(StandardMaterial {
570 base_color: Color::srgb(0.1, 0.25, 0.05),
571 perceptual_roughness: 0.95,
572 ..default()
573 });
574
575 for (x, y, z) in [
576 (2.0, 0.4, 3.0),
577 (w - 2.0, 0.3, l - 4.0),
578 (1.5, 0.35, l - 2.0),
579 ] {
580 commands.spawn((
581 Mesh3d(meshes.add(Sphere::new(y))),
582 MeshMaterial3d(bush_material.clone()),
583 Transform::from_xyz(x, y, z),
584 SceneGeometry,
585 ));
586 }
587
588 spawn_pole(
590 commands,
591 meshes,
592 materials,
593 Vec3::new(w / 2.0, 0.0, l / 2.0),
594 settings.mounting_height,
595 );
596}
597
598fn spawn_pole(
599 commands: &mut Commands,
600 meshes: &mut ResMut<Assets<Mesh>>,
601 materials: &mut ResMut<Assets<StandardMaterial>>,
602 position: Vec3,
603 height: f32,
604) {
605 let pole_material = materials.add(StandardMaterial {
606 base_color: Color::srgb(0.4, 0.4, 0.4),
607 metallic: 0.6,
608 perceptual_roughness: 0.4,
609 ..default()
610 });
611
612 commands.spawn((
614 Mesh3d(meshes.add(Cylinder::new(0.08, height - 0.3))),
615 MeshMaterial3d(pole_material.clone()),
616 Transform::from_xyz(position.x, height / 2.0, position.z),
617 SceneGeometry,
618 NotShadowCaster,
619 ));
620
621 commands.spawn((
623 Mesh3d(meshes.add(Cylinder::new(0.05, 0.3))),
624 MeshMaterial3d(pole_material),
625 Transform::from_xyz(position.x - 0.05, height - 0.2, position.z)
626 .with_rotation(Quat::from_rotation_z(std::f32::consts::FRAC_PI_2)),
627 SceneGeometry,
628 NotShadowCaster,
629 ));
630}
631
632fn spawn_pendulum_cable(
635 commands: &mut Commands,
636 meshes: &mut ResMut<Assets<Mesh>>,
637 materials: &mut ResMut<Assets<StandardMaterial>>,
638 settings: &ViewerSettings,
639 x: f32,
640 z: f32,
641) {
642 if settings.pendulum_length <= 0.0 {
643 return;
644 }
645
646 let cable_material = materials.add(StandardMaterial {
647 base_color: Color::srgb(0.2, 0.2, 0.2),
648 metallic: 0.3,
649 perceptual_roughness: 0.6,
650 ..default()
651 });
652
653 let cable_top = settings.room_height;
655 let cable_bottom = settings.room_height - settings.pendulum_length;
656 let cable_center_y = (cable_top + cable_bottom) / 2.0;
657
658 commands.spawn((
659 Mesh3d(meshes.add(Cylinder::new(0.01, settings.pendulum_length))),
660 MeshMaterial3d(cable_material),
661 Transform::from_xyz(x, cable_center_y, z),
662 SceneGeometry,
663 NotShadowCaster,
664 ));
665}