Initial commit

This commit is contained in:
LucasJavaudin 2025-07-28 14:58:29 +02:00
parent 0dcc37f3b3
commit d2fc62e5cb
7 changed files with 390 additions and 0 deletions

3
.gitignore vendored
View File

@ -1,3 +1,6 @@
# Custom
assets/
# ---> Rust
# Generated by Cargo
# will have compiled files and executables

18
Cargo.toml Normal file
View File

@ -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

55
src/2d_shapes.rs Normal file
View File

@ -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()
});
}

36
src/lib.rs Normal file
View File

@ -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));
}

47
src/main.rs Normal file
View File

@ -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();
}

95
src/timer.rs Normal file
View File

@ -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);
}
}

136
src/trips.rs Normal file
View File

@ -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;
}
}
}
}