Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic tui #11

Merged
merged 4 commits into from
Jan 12, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 5 additions & 2 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ members = [
]

default-members = [
"crates/yahs-ui"
"crates/yahs-ui",
"crates/yahs-cli"
]

[workspace.package]
Expand All @@ -23,7 +24,9 @@ license-file = "LICENSE-APACHE"
bevy = { version = "0.15", default-features = false, features = [
"bevy_state", "multi_threaded"
] }
avian3d = "0.2"
avian3d = { version = "0.2", default-features = false, features = [

] }

# Compile with Performance Optimizations:
# https://bevyengine.org/learn/quick-start/getting-started/setup/#compile-with-performance-optimizations
Expand Down
12 changes: 11 additions & 1 deletion crates/yahs-cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,21 @@ path = "src/main.rs"

[dependencies]
yahs = { path = "../yahs" }
bevy = { workspace = true, default-features = false }
bevy = { workspace = true, default-features = false, features = [
"bevy_asset",
"bevy_state",
] }
bevy_ratatui = "0.7"
ratatui = "0.29"
crossterm = "0.28.1"
color-eyre = "0.6.3"
avian3d = { workspace = true, default-features = false, features = [
"f32",
"parry-f32",
"default-collider",
"collider-from-mesh",
"parallel"
] }

[features]
default = []
Expand Down
30 changes: 30 additions & 0 deletions crates/yahs-cli/src/colors.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
use ratatui::style::Color;

#[derive(Debug, Clone, Copy)]
pub enum TuiColor {
// UI Elements
Title,
Status,
Controls,

// Forces (matching yahs-ui colors)
Weight,
Buoyancy,
Drag,
}

impl TuiColor {
pub fn color(&self) -> Color {
match self {
// UI Elements
TuiColor::Title => Color::Cyan,
TuiColor::Status => Color::Yellow,
TuiColor::Controls => Color::Green,

// Forces - matching yahs-ui/src/forces/body.rs colors
TuiColor::Weight => Color::Red,
TuiColor::Buoyancy => Color::Blue,
TuiColor::Drag => Color::Green,
}
}
}
45 changes: 45 additions & 0 deletions crates/yahs-cli/src/controls.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
use bevy::prelude::*;
use bevy_ratatui::event::KeyEvent;
use yahs::prelude::*;

pub struct ControlsPlugin;

impl Plugin for ControlsPlugin {
fn build(&self, app: &mut App) {
app.add_systems(Update, keyboard_input_system);
}
}

pub fn keyboard_input_system(
mut events: EventReader<KeyEvent>,
mut exit: EventWriter<AppExit>,
mut next_state: ResMut<NextState<SimState>>,
mut time_options: ResMut<TimeScaleOptions>,
state: Res<State<SimState>>,
) {
use crossterm::event::KeyCode;
for event in events.read() {
match event.code {
KeyCode::Char('q') | KeyCode::Esc => {
exit.send_default();
}
KeyCode::Char(' ') => match state.get() {
SimState::Running => next_state.set(SimState::Stopped),
SimState::Stopped => next_state.set(SimState::Running),
_ => {}
},
KeyCode::Char('r') => {
time_options.reset();
}
KeyCode::Left => {
time_options.multiplier =
(time_options.multiplier / 2.0).max(time_options.min_multiplier);
}
KeyCode::Right => {
time_options.multiplier =
(time_options.multiplier * 2.0).min(time_options.max_multiplier);
}
_ => {}
}
}
}
63 changes: 63 additions & 0 deletions crates/yahs-cli/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
mod colors;
mod controls;
mod ui;

use avian3d::prelude::*;
use bevy::prelude::*;
use bevy_ratatui::terminal::RatatuiContext;
use ratatui::prelude::*;
use yahs::prelude::*;

pub struct TuiPlugin;

impl Plugin for TuiPlugin {
fn build(&self, app: &mut App) {
app.add_plugins(controls::ControlsPlugin)
.add_systems(Startup, spawn_balloon)
.add_systems(Update, draw_tui.pipe(bevy_ratatui::error::exit_on_error));
}
}

fn draw_tui(
mut context: ResMut<RatatuiContext>,
time: Res<Time<Physics>>,
state: Res<State<SimState>>,
time_options: Res<TimeScaleOptions>,
balloons: Query<
(
&Name,
&Transform,
&Weight,
&Buoyancy,
&yahs::prelude::Drag,
&IdealGas,
),
With<Balloon>,
>,
) -> color_eyre::Result<()> {
context.draw(|frame| {
let area = frame.area();

let chunks = Layout::default()
.direction(Direction::Vertical)
.constraints([
Constraint::Length(3), // Title
Constraint::Length(3), // Sim Status
Constraint::Min(10), // Balloon Data
Constraint::Length(3), // Controls
])
.split(area);

ui::draw_title(frame, chunks[0]);
ui::draw_status(
frame,
chunks[1],
state.get(),
time.elapsed_secs(),
&time_options,
);
ui::draw_balloon_data(frame, chunks[2], &balloons.iter().collect::<Vec<_>>());
ui::draw_controls(frame, chunks[3]);
})?;
Ok(())
}
44 changes: 15 additions & 29 deletions crates/yahs-cli/src/main.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,22 @@
use bevy::{
app::{AppExit, ScheduleRunnerPlugin},
prelude::*,
window::{WindowPlugin, ExitCondition},
};
use bevy_ratatui::{
error::exit_on_error, event::KeyEvent, terminal::RatatuiContext, RatatuiPlugins,
};
use bevy_ratatui::RatatuiPlugins;
use yahs::prelude::SimulatorPlugins;
use yahs_cli::TuiPlugin;

fn main() {
let wait_duration = std::time::Duration::from_secs_f64(1. / 60.); // 60 FPS
App::new()
.add_plugins(RatatuiPlugins::default())
.add_plugins(ScheduleRunnerPlugin::run_loop(wait_duration))
.add_systems(PreUpdate, keyboard_input_system)
.add_systems(Update, hello_world.pipe(exit_on_error))
.add_plugins((
DefaultPlugins.set(WindowPlugin {
primary_window: None,
close_when_requested: false,
exit_condition: ExitCondition::DontExit, // exit in controls.rs
}),
RatatuiPlugins::default(),
SimulatorPlugins,
TuiPlugin,
))
.run();
}

fn hello_world(mut context: ResMut<RatatuiContext>) -> color_eyre::Result<()> {
context.draw(|frame| {
let text = ratatui::text::Text::raw("hello world\nPress 'q' to Quit");
frame.render_widget(text, frame.area())
})?;
Ok(())
}

fn keyboard_input_system(mut events: EventReader<KeyEvent>, mut exit: EventWriter<AppExit>) {
use crossterm::event::KeyCode;
for event in events.read() {
match event.code {
KeyCode::Char('q') | KeyCode::Esc => {
exit.send_default();
}
_ => {}
}
}
}
78 changes: 78 additions & 0 deletions crates/yahs-cli/src/ui.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
use ratatui::{
prelude::Alignment,
widgets::{Block, Borders, Paragraph},
text::{Line, Text},
style::Style,
Frame,
};
use yahs::prelude::*;
use crate::colors::TuiColor;

pub fn draw_title(frame: &mut Frame<'_>, area: ratatui::layout::Rect) {
let title = Paragraph::new("yet another hab simulator 🎈")
.style(Style::default().fg(TuiColor::Title.color()))
.alignment(Alignment::Center)
.block(Block::default().borders(Borders::ALL));
frame.render_widget(title, area);
}

pub fn draw_status(
frame: &mut Frame<'_>,
area: ratatui::layout::Rect,
state: &SimState,
time: f32,
time_options: &TimeScaleOptions,
) {
let status = format!(
"state: {:?} | time: {:.2}s | speed: {}x",
state,
time,
time_options.multiplier,
);
let status = Paragraph::new(status)
.style(Style::default().fg(TuiColor::Status.color()))
.block(Block::default().borders(Borders::ALL));
frame.render_widget(status, area);
}

pub fn draw_balloon_data(
frame: &mut Frame<'_>,
area: ratatui::layout::Rect,
balloons: &[(
&bevy::prelude::Name,
&bevy::prelude::Transform,
&Weight,
&Buoyancy,
&Drag,
&IdealGas,
)],
) {
let mut balloon_data = Vec::new();
for (name, transform, weight, buoyancy, drag, gas) in balloons {
balloon_data.extend_from_slice(&[
format!("{}", name.as_str()),
format!("position: {:?} m", transform.translation),
format!("gas density: {:.2} kg/m³", gas.density.kg_per_m3()),
format!("volume: {:.2} m³", gas.volume().m3()),
Line::from(format!("weight: {:.2} N", weight.force()))
.style(Style::default().fg(TuiColor::Weight.color())).to_string(),
Line::from(format!("buoyancy: {:.2} N", buoyancy.force()))
.style(Style::default().fg(TuiColor::Buoyancy.color())).to_string(),
Line::from(format!("drag: {:.2} N", drag.force()))
.style(Style::default().fg(TuiColor::Drag.color())).to_string(),
]);
}

let balloon_text = Paragraph::new(Text::from(balloon_data.join("\n")))
.block(Block::default().borders(Borders::ALL).title("balloon status"));
frame.render_widget(balloon_text, area);
}

pub fn draw_controls(frame: &mut Frame<'_>, area: ratatui::layout::Rect) {
let controls = Paragraph::new(
"q: quit | space: pause/play | r: reset | ←/→: speed"
)
.style(Style::default().fg(TuiColor::Controls.color()))
.block(Block::default().borders(Borders::ALL));
frame.render_widget(controls, area);
}
2 changes: 2 additions & 0 deletions crates/yahs-ui/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description = "UI for the YAHS simulator"
edition = "2021"
authors = { workspace = true }
license = { workspace = true }
default-run = "yahs-ui"

[[bin]]
name = "yahs-ui"
Expand All @@ -25,6 +26,7 @@ bevy = { workspace = true, default-features = false, features = [
"default_font",
"png",
"tonemapping_luts",
"wayland",
] }
avian3d = { workspace = true }
bevy-inspector-egui = { version = "0.28", optional = true, features = [
Expand Down
14 changes: 14 additions & 0 deletions crates/yahs-ui/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,17 @@
3D rendered simulation viewport.

Unstable on platforms that don't support Vulkan.

## Usage

Run with debug tools enabled:

```
cargo run --release --features dev
```

Run with entity inspector enabled:

```
cargo run --release --features inspect
```
2 changes: 2 additions & 0 deletions crates/yahs-ui/src/colors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ pub enum ColorPalette {
BrightOrange,
VividYellow,
LivelyGreen,
DirtBrown,
BrightBlue,
BoldPurple,
DarkBase,
Expand All @@ -28,6 +29,7 @@ impl ColorPalette {
ColorPalette::BrightOrange => Color::Srgba(Srgba::hex("#FF6F00").unwrap()),
ColorPalette::VividYellow => Color::Srgba(Srgba::hex("#FFD600").unwrap()),
ColorPalette::LivelyGreen => Color::Srgba(Srgba::hex("#AEEA00").unwrap()),
ColorPalette::DirtBrown => Color::Srgba(Srgba::hex("#795548").unwrap()),
ColorPalette::BrightBlue => Color::Srgba(Srgba::hex("#00B0FF").unwrap()),
ColorPalette::BoldPurple => Color::Srgba(Srgba::hex("#D5006D").unwrap()),
ColorPalette::DarkBase => Color::Srgba(Srgba::hex("#212121").unwrap()),
Expand Down
Loading
Loading