Initial commit
This commit is contained in:
parent
0dcc37f3b3
commit
d2fc62e5cb
|
|
@ -1,3 +1,6 @@
|
||||||
|
# Custom
|
||||||
|
assets/
|
||||||
|
|
||||||
# ---> Rust
|
# ---> Rust
|
||||||
# Generated by Cargo
|
# Generated by Cargo
|
||||||
# will have compiled files and executables
|
# will have compiled files and executables
|
||||||
|
|
|
||||||
|
|
@ -0,0 +1,18 @@
|
||||||
|
[package]
|
||||||
|
name = "metroviz"
|
||||||
|
version = "0.1.0"
|
||||||
|
edition = "2021"
|
||||||
|
|
||||||
|
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
bevy = { version = "0.11.0", features = ["dynamic_linking"] }
|
||||||
|
rand = "0.8"
|
||||||
|
rand_distr = "0.4"
|
||||||
|
rand_pcg = "0.3"
|
||||||
|
|
||||||
|
[profile.dev]
|
||||||
|
opt-level = 1
|
||||||
|
|
||||||
|
[profile.dev.package."*"]
|
||||||
|
opt-level = 3
|
||||||
|
|
@ -0,0 +1,55 @@
|
||||||
|
//! Shows how to render simple primitive shapes with a single color.
|
||||||
|
|
||||||
|
use bevy::{prelude::*, sprite::MaterialMesh2dBundle};
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
App::new()
|
||||||
|
.add_plugins(DefaultPlugins)
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
||||||
|
fn setup(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
) {
|
||||||
|
commands.spawn(Camera2dBundle::default());
|
||||||
|
|
||||||
|
// Circle
|
||||||
|
commands.spawn(MaterialMesh2dBundle {
|
||||||
|
mesh: meshes.add(shape::Circle::new(50.).into()).into(),
|
||||||
|
material: materials.add(ColorMaterial::from(Color::PURPLE)),
|
||||||
|
transform: Transform::from_translation(Vec3::new(-150., 0., 0.)),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Rectangle
|
||||||
|
commands.spawn(SpriteBundle {
|
||||||
|
sprite: Sprite {
|
||||||
|
color: Color::rgb(0.25, 0.25, 0.75),
|
||||||
|
custom_size: Some(Vec2::new(50.0, 100.0)),
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
transform: Transform::from_translation(Vec3::new(-50., 0., 0.)),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Quad
|
||||||
|
commands.spawn(MaterialMesh2dBundle {
|
||||||
|
mesh: meshes
|
||||||
|
.add(shape::Quad::new(Vec2::new(50., 100.)).into())
|
||||||
|
.into(),
|
||||||
|
material: materials.add(ColorMaterial::from(Color::LIME_GREEN)),
|
||||||
|
transform: Transform::from_translation(Vec3::new(50., 0., 0.)),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
|
||||||
|
// Hexagon
|
||||||
|
commands.spawn(MaterialMesh2dBundle {
|
||||||
|
mesh: meshes.add(shape::RegularPolygon::new(50., 6).into()).into(),
|
||||||
|
material: materials.add(ColorMaterial::from(Color::TURQUOISE)),
|
||||||
|
transform: Transform::from_translation(Vec3::new(150., 0., 0.)),
|
||||||
|
..default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,36 @@
|
||||||
|
pub mod timer;
|
||||||
|
pub mod trips;
|
||||||
|
use std::ops::{Add, AddAssign};
|
||||||
|
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
pub use timer::SimulationTimerPlugin;
|
||||||
|
pub use trips::{Trip, TripPlugin, Trips};
|
||||||
|
|
||||||
|
/// Coordinates x, y on the map.
|
||||||
|
#[derive(Clone, Copy, Debug)]
|
||||||
|
pub struct MapPosition(pub f32, pub f32);
|
||||||
|
|
||||||
|
/// Unit used to represent time in a simulation, measured in number of [TIME_UNIT] after midnight.
|
||||||
|
#[derive(Clone, Copy, Debug, Default, PartialEq, PartialOrd)]
|
||||||
|
pub struct SimulationTime(pub usize);
|
||||||
|
|
||||||
|
impl Add for SimulationTime {
|
||||||
|
type Output = SimulationTime;
|
||||||
|
fn add(self, rhs: SimulationTime) -> Self::Output {
|
||||||
|
Self(self.0 + rhs.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl AddAssign for SimulationTime {
|
||||||
|
fn add_assign(&mut self, rhs: Self) {
|
||||||
|
self.0 += rhs.0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Component)]
|
||||||
|
struct MainCamera;
|
||||||
|
|
||||||
|
pub fn setup(mut commands: Commands) {
|
||||||
|
commands.spawn((Camera2dBundle::default(), MainCamera));
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,47 @@
|
||||||
|
use bevy::prelude::*;
|
||||||
|
use metroviz::*;
|
||||||
|
use rand::Rng;
|
||||||
|
use rand_distr::{Distribution, Normal, Uniform};
|
||||||
|
use rand_pcg::Pcg64Mcg;
|
||||||
|
|
||||||
|
fn random_color<R: Rng>(rng: &mut R) -> Color {
|
||||||
|
let uniform = Uniform::new(0.0, 1.0);
|
||||||
|
let r = uniform.sample(rng);
|
||||||
|
let g = uniform.sample(rng);
|
||||||
|
let b = uniform.sample(rng);
|
||||||
|
Color::rgb(r, g, b)
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
let n = 10;
|
||||||
|
let j = 16;
|
||||||
|
let mut trips = Vec::with_capacity(n);
|
||||||
|
let mut rng = Pcg64Mcg::new(2031996);
|
||||||
|
let uniform = Uniform::new(-500.0, 500.0);
|
||||||
|
let normal = Normal::new(0.0, 3.0).unwrap();
|
||||||
|
for _ in 0..n {
|
||||||
|
let mut x = uniform.sample(&mut rng);
|
||||||
|
let mut y = uniform.sample(&mut rng);
|
||||||
|
let mut path = Vec::with_capacity(j);
|
||||||
|
for _ in 0..j {
|
||||||
|
x = x + normal.sample(&mut rng);
|
||||||
|
y = y + normal.sample(&mut rng);
|
||||||
|
path.push(MapPosition(x, y));
|
||||||
|
}
|
||||||
|
let trip = Trip {
|
||||||
|
start_time: SimulationTime(5),
|
||||||
|
end_time: SimulationTime(5 + j - 1),
|
||||||
|
path,
|
||||||
|
width: 0.3,
|
||||||
|
length: 0.6,
|
||||||
|
color: random_color(&mut rng),
|
||||||
|
};
|
||||||
|
trips.push(trip);
|
||||||
|
}
|
||||||
|
|
||||||
|
App::new()
|
||||||
|
.insert_resource(Trips(trips))
|
||||||
|
.add_plugins((DefaultPlugins, SimulationTimerPlugin, TripPlugin))
|
||||||
|
.add_systems(Startup, setup)
|
||||||
|
.run();
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,95 @@
|
||||||
|
use crate::SimulationTime;
|
||||||
|
use bevy::prelude::*;
|
||||||
|
|
||||||
|
/// Current simulation time (in [TIME_UNIT] after midnight).
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct SimulationTimer {
|
||||||
|
time: SimulationTime,
|
||||||
|
timer: Timer,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl SimulationTimer {
|
||||||
|
/// Returns `true` if the current simulation time is within the given start and end times.
|
||||||
|
pub fn covers(&self, start: SimulationTime, end: SimulationTime) -> bool {
|
||||||
|
self.time >= start && self.time <= end
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Event that triggers when the [SimulationTimer] is updated.
|
||||||
|
#[derive(Default, Event)]
|
||||||
|
pub struct SimulationTimerUpdated(pub SimulationTime);
|
||||||
|
|
||||||
|
pub struct SimulationTimerPlugin;
|
||||||
|
|
||||||
|
impl Plugin for SimulationTimerPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.insert_resource(SimulationTimer {
|
||||||
|
time: SimulationTime(0),
|
||||||
|
timer: Timer::from_seconds(1.0, TimerMode::Repeating),
|
||||||
|
})
|
||||||
|
.add_event::<SimulationTimerUpdated>()
|
||||||
|
.add_systems(Startup, initialize_ui)
|
||||||
|
.add_systems(Update, (update_simulation_timer, debug_simulation_timer));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn initialize_ui(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||||
|
commands
|
||||||
|
.spawn(NodeBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Percent(100.0),
|
||||||
|
height: Val::Px(100.0),
|
||||||
|
position_type: PositionType::Absolute,
|
||||||
|
bottom: Val::Px(0.0),
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
background_color: BackgroundColor(Color::WHITE),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent
|
||||||
|
.spawn(ButtonBundle {
|
||||||
|
style: Style {
|
||||||
|
width: Val::Px(30.0),
|
||||||
|
height: Val::Px(30.0),
|
||||||
|
border: UiRect::all(Val::Px(2.0)),
|
||||||
|
justify_content: JustifyContent::Center,
|
||||||
|
align_items: AlignItems::Center,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
border_color: BorderColor(Color::BLACK),
|
||||||
|
background_color: BackgroundColor(Color::GRAY),
|
||||||
|
..default()
|
||||||
|
})
|
||||||
|
.with_children(|parent| {
|
||||||
|
parent.spawn(TextBundle::from_section(
|
||||||
|
"\u{23f5}",
|
||||||
|
TextStyle {
|
||||||
|
font: asset_server.load("fonts/Symbola.ttf"),
|
||||||
|
font_size: 40.0,
|
||||||
|
color: Color::WHITE,
|
||||||
|
},
|
||||||
|
));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
fn update_simulation_timer(
|
||||||
|
time: Res<Time>,
|
||||||
|
mut simulation_timer: ResMut<SimulationTimer>,
|
||||||
|
mut event_writer: EventWriter<SimulationTimerUpdated>,
|
||||||
|
) {
|
||||||
|
simulation_timer.timer.tick(time.delta());
|
||||||
|
if simulation_timer.timer.just_finished() {
|
||||||
|
simulation_timer.time += SimulationTime(1);
|
||||||
|
event_writer.send(SimulationTimerUpdated(simulation_timer.time))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn debug_simulation_timer(mut timer_events: EventReader<SimulationTimerUpdated>) {
|
||||||
|
for ev in timer_events.iter() {
|
||||||
|
println!("{:?}", ev.0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -0,0 +1,136 @@
|
||||||
|
use bevy::prelude::{shape::Quad, *};
|
||||||
|
|
||||||
|
use crate::timer::SimulationTimerUpdated;
|
||||||
|
use crate::{MapPosition, SimulationTime};
|
||||||
|
use std::ops::{Deref, Index};
|
||||||
|
|
||||||
|
/// Simulation time at which the entity starts appearing.
|
||||||
|
#[derive(Clone, Copy, Debug, Component)]
|
||||||
|
struct StartTime(SimulationTime);
|
||||||
|
|
||||||
|
/// Simulation time at which the entity disappears.
|
||||||
|
#[derive(Clone, Copy, Debug, Component)]
|
||||||
|
struct EndTime(SimulationTime);
|
||||||
|
|
||||||
|
/// Index of the trip in the [Trips] resource.
|
||||||
|
#[derive(Clone, Copy, Debug, Component)]
|
||||||
|
struct TripIndex(usize);
|
||||||
|
|
||||||
|
/// Path followed by an entity.
|
||||||
|
///
|
||||||
|
/// The path is represented by the sequence of [MapPosition] traveled by the entity (one for each
|
||||||
|
/// [TIME_UNIT]). The number of [MapPosition] should be equal to the time duration of the path in
|
||||||
|
/// [TIME_UNIT].
|
||||||
|
#[derive(Component)]
|
||||||
|
struct Path(&'static [MapPosition]);
|
||||||
|
|
||||||
|
/// A vehicle or agent moving on the map.
|
||||||
|
#[derive(Bundle)]
|
||||||
|
struct TripBundle {
|
||||||
|
trip_id: TripIndex,
|
||||||
|
start_time: StartTime,
|
||||||
|
end_time: EndTime,
|
||||||
|
mesh: ColorMesh2dBundle,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub struct Trip {
|
||||||
|
pub start_time: SimulationTime,
|
||||||
|
pub end_time: SimulationTime,
|
||||||
|
pub path: Vec<MapPosition>,
|
||||||
|
pub width: f32,
|
||||||
|
pub length: f32,
|
||||||
|
pub color: Color,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Trip {
|
||||||
|
fn get_coords_at_time(&self, time: SimulationTime) -> Vec3 {
|
||||||
|
debug_assert!(
|
||||||
|
time >= self.start_time,
|
||||||
|
"{:?} < {:?}",
|
||||||
|
time,
|
||||||
|
self.start_time
|
||||||
|
);
|
||||||
|
debug_assert!(time <= self.end_time, "{:?} > {:?}", time, self.end_time);
|
||||||
|
let idx = time.0 - self.start_time.0;
|
||||||
|
let pos = self.path[idx];
|
||||||
|
Vec3::new(pos.0, pos.1, 0.0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Resource)]
|
||||||
|
pub struct Trips(pub Vec<Trip>);
|
||||||
|
|
||||||
|
impl Deref for Trips {
|
||||||
|
type Target = Vec<Trip>;
|
||||||
|
fn deref(&self) -> &Self::Target {
|
||||||
|
&self.0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Index<TripIndex> for Trips {
|
||||||
|
type Output = Trip;
|
||||||
|
fn index(&self, index: TripIndex) -> &Self::Output {
|
||||||
|
&self.0[index.0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Plugin to manage the trips.
|
||||||
|
pub struct TripPlugin;
|
||||||
|
|
||||||
|
impl Plugin for TripPlugin {
|
||||||
|
fn build(&self, app: &mut App) {
|
||||||
|
app.add_systems(Startup, spawn_trips)
|
||||||
|
.add_systems(Update, toggle_trips_visibility);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn spawn_trips(
|
||||||
|
mut commands: Commands,
|
||||||
|
mut meshes: ResMut<Assets<Mesh>>,
|
||||||
|
mut materials: ResMut<Assets<ColorMaterial>>,
|
||||||
|
trips: Res<Trips>,
|
||||||
|
) {
|
||||||
|
for (index, trip) in trips.iter().enumerate() {
|
||||||
|
commands.spawn(TripBundle {
|
||||||
|
trip_id: TripIndex(index),
|
||||||
|
start_time: StartTime(trip.start_time),
|
||||||
|
end_time: EndTime(trip.end_time),
|
||||||
|
mesh: ColorMesh2dBundle {
|
||||||
|
mesh: meshes
|
||||||
|
.add(Quad::new(Vec2::new(trip.width, trip.length)).into())
|
||||||
|
.into(),
|
||||||
|
material: materials.add(ColorMaterial::from(trip.color)),
|
||||||
|
visibility: Visibility::Hidden,
|
||||||
|
..default()
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shows / hides trips according to the current simulation time and the start / end time of the
|
||||||
|
/// trips.
|
||||||
|
fn toggle_trips_visibility(
|
||||||
|
mut events: EventReader<SimulationTimerUpdated>,
|
||||||
|
trips: Res<Trips>,
|
||||||
|
mut query: Query<(
|
||||||
|
&TripIndex,
|
||||||
|
&StartTime,
|
||||||
|
&EndTime,
|
||||||
|
&mut Visibility,
|
||||||
|
&mut Transform,
|
||||||
|
)>,
|
||||||
|
) {
|
||||||
|
if let Some(event) = events.iter().last() {
|
||||||
|
println!("hello world!");
|
||||||
|
let current_time = event.0;
|
||||||
|
for (&trip_id, start_time, end_time, mut vis, mut transform) in query.iter_mut() {
|
||||||
|
if current_time >= start_time.0 && current_time <= end_time.0 {
|
||||||
|
*vis = Visibility::Visible;
|
||||||
|
let trip = &trips[trip_id];
|
||||||
|
transform.translation = trip.get_coords_at_time(current_time);
|
||||||
|
} else {
|
||||||
|
*vis = Visibility::Hidden;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in New Issue