diff --git a/.gitignore b/.gitignore index 3ca43ae..53fd210 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,6 @@ +# Custom +assets/ + # ---> Rust # Generated by Cargo # will have compiled files and executables diff --git a/Cargo.toml b/Cargo.toml new file mode 100644 index 0000000..8961b00 --- /dev/null +++ b/Cargo.toml @@ -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 diff --git a/src/2d_shapes.rs b/src/2d_shapes.rs new file mode 100644 index 0000000..7fa168f --- /dev/null +++ b/src/2d_shapes.rs @@ -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>, + mut materials: ResMut>, +) { + 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() + }); +} diff --git a/src/lib.rs b/src/lib.rs new file mode 100644 index 0000000..3bcce63 --- /dev/null +++ b/src/lib.rs @@ -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)); +} diff --git a/src/main.rs b/src/main.rs new file mode 100644 index 0000000..0bf1734 --- /dev/null +++ b/src/main.rs @@ -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(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(); +} diff --git a/src/timer.rs b/src/timer.rs new file mode 100644 index 0000000..69b9b6e --- /dev/null +++ b/src/timer.rs @@ -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::() + .add_systems(Startup, initialize_ui) + .add_systems(Update, (update_simulation_timer, debug_simulation_timer)); + } +} + +fn initialize_ui(mut commands: Commands, asset_server: Res) { + 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