Add panning, zooming, and info box

011cd40da8b4a8ae3324d1b0cf6f216641d44225
This commit is contained in:
Tobias Berger 2022-09-06 15:40:09 +02:00
parent 6774fcd2fa
commit 26d3e9b4a6
Signed by: toby
GPG key ID: 2D05EFAB764D6A88
7 changed files with 179 additions and 29 deletions

10
Cargo.lock generated
View file

@ -427,6 +427,15 @@ dependencies = [
"glam",
]
[[package]]
name = "bevy_pancam"
version = "0.6.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ebc698de3f4e824a67f517fe9cca35f08d5fdaab99ed8e141102c582768fc62"
dependencies = [
"bevy",
]
[[package]]
name = "bevy_pbr"
version = "0.8.1"
@ -2645,6 +2654,7 @@ name = "worlds-sim-rust"
version = "0.1.0"
dependencies = [
"bevy",
"bevy_pancam",
"save",
]

View file

@ -6,7 +6,7 @@ resolver = "2"
[features]
debug = ["save/debug"]
render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/render", "save/render"]
render = ["bevy/bevy_asset", "bevy/bevy_winit", "bevy/render", "save/render", "dep:bevy_pancam"]
default = ["render", "debug"]
[dependencies.save]
@ -15,3 +15,7 @@ path = "save"
[dependencies.bevy]
version = "0.8"
default-features = false
[dependencies.bevy_pancam]
version = "0.6.1"
optional = true

View file

@ -4,7 +4,9 @@ use std::{
fmt::{Debug, Display},
};
use bevy::{math::Vec3A, prelude::Vec2, utils::default};
// TODO: Logging doesn't seem to work here? Figure out why and fix
use bevy::{log::info, math::Vec3A, prelude::Vec2, utils::default};
use noise::{NoiseFn, Perlin, Seedable};
use rand::Rng;
@ -145,11 +147,14 @@ impl World {
}
fn generate_continents(&mut self) {
info!("Generating continents");
let mut rng = rand::thread_rng();
let width = self.width as f32;
let height = self.height as f32;
for i in 0..Self::NUM_CONTINENTS {
info!("{}/{}", i, Self::NUM_CONTINENTS);
self.continent_offsets[i as usize].x = rng
.gen_range(width * i as f32 * 2.0 / 5.0..width * (i as f32 + 2.0) * 2.0 / 5.0)
.repeat(width);
@ -159,6 +164,7 @@ impl World {
self.continent_widths[i as usize] =
rng.gen_range(Self::CONTINENT_MIN_WIDTH_FACTOR..Self::CONTINENT_MAX_WIDTH_FACTOR);
}
info!("Done generating continents");
}
fn continent_modifier(&self, x: usize, y: usize) -> f32 {
@ -195,6 +201,7 @@ impl World {
}
fn generate_altitude(&mut self) -> Result<(), CartesianError> {
info!("Generating altitude");
self.generate_continents();
const RADIUS_1: f32 = 0.5;
@ -212,6 +219,8 @@ impl World {
for y in 0..self.terrain.len() {
let alpha = (y as f32 / self.height as f32) * PI;
info!("{}/{}", y, self.terrain.len());
for x in 0..self.terrain[y].len() {
let beta = (x as f32 / self.width as f32) * TAU;
@ -250,6 +259,7 @@ impl World {
self.terrain[y][x].altitude = Self::calculate_altitude(raw_altitude);
}
}
info!("Done generating altitude");
Ok(())
}

View file

@ -67,6 +67,9 @@ impl WorldManager {
pub fn get_world(&self) -> Option<&World> {
self.world.as_ref()
}
pub fn world(&self) -> &World {
self.get_world().unwrap()
}
pub fn new_world(&mut self) -> Result<&World, WorldGenError> {
let seed = random();
@ -142,7 +145,7 @@ impl WorldManager {
let mut shade_value = 1.0;
while shade_value > altitude / World::MAX_ALTITUDE {
shade_value -= 0.1;
shade_value -= 0.05;
}
Color::rgb(shade_value, shade_value, shade_value)

View file

@ -32,7 +32,9 @@
#![warn(unused_results)]
#![warn(variant_size_differences)]
mod world_plugins;
mod plugins;
use std::fmt::Display;
use bevy::{
app::App,
@ -42,7 +44,7 @@ use bevy::{
#[cfg(feature = "render")]
use bevy::{
asset::{AssetServer, Assets},
core_pipeline::core_2d::Camera2dBundle,
core_pipeline::core_2d::{Camera2d, Camera2dBundle},
ecs::{
change_detection::ResMut,
component::Component,
@ -50,24 +52,31 @@ use bevy::{
system::{Commands, Query, Res},
},
hierarchy::BuildChildren,
prelude::Vec2,
render::{
camera::{Camera, RenderTarget},
color::Color,
render_resource::{
Extent3d, TextureDescriptor, TextureDimension, TextureFormat, TextureUsages,
},
texture::{Image, ImageSettings},
},
sprite::{Sprite, SpriteBundle},
text::Text,
transform::components::GlobalTransform,
ui::{
entity::{ButtonBundle, ImageBundle, NodeBundle, TextBundle},
entity::{ButtonBundle, NodeBundle, TextBundle},
widget::Button,
AlignItems, FocusPolicy, Interaction, JustifyContent, PositionType, Size, Style, UiColor,
UiImage, UiRect, Val,
UiRect, Val,
},
utils::default,
window::{CursorIcon, WindowDescriptor, Windows},
winit::WinitSettings,
};
use world_plugins::WorldPlugins;
#[cfg(feature = "render")]
use plugins::PanCam;
use plugins::WorldPlugins;
use save::*;
#[cfg(feature = "render")]
@ -78,17 +87,33 @@ fn refresh_world_texture(images: &mut Assets<Image>, world_manager: &WorldManage
}
#[cfg(feature = "render")]
#[derive(Component, Default)]
#[derive(Component)]
struct RainfallButton;
#[cfg(feature = "render")]
#[derive(Component, Default)]
#[derive(Component)]
struct TemperatureButton;
#[cfg(feature = "render")]
#[derive(Component, Default)]
#[derive(Component)]
struct ContoursButton;
#[cfg(feature = "render")]
#[derive(Component)]
struct InfoPanel;
#[cfg(feature = "render")]
#[derive(Default, Debug)]
struct CursorMapPosition {
x: i32,
y: i32,
}
impl Display for CursorMapPosition {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_fmt(format_args!("x: {}, y: {}", self.x, self.y))
}
}
const NORMAL_BUTTON: Color = Color::rgb(0.15, 0.15, 0.15);
const HOVERED_BUTTON: Color = Color::rgb(0.25, 0.25, 0.25);
const PRESSED_BUTTON: Color = Color::rgb(0.35, 0.60, 0.35);
@ -191,6 +216,59 @@ fn handle_contours_button(
}
}
#[cfg(feature = "render")]
fn update_cursor_map_position(
mut cursor_map_position: ResMut<'_, CursorMapPosition>,
transform: Query<'_, '_, (&Camera, &GlobalTransform), With<Camera2d>>,
windows: Res<'_, Windows>,
world_manager: Res<'_, WorldManager>,
) {
let (camera, transform) = transform.single();
let window = match camera.target {
RenderTarget::Window(window_id) => windows.get(window_id).unwrap(),
RenderTarget::Image(_) => windows.primary(),
};
if let Some(screen_position) = window.cursor_position() {
let window_size = Vec2::new(window.width(), window.height());
// GPU coordinates [-1..1]
let ndc = (screen_position / window_size) * 2.0;
// Matrix to reverse camera transform
let ndc_to_world = transform.compute_matrix() * camera.projection_matrix().inverse();
let world_position =
ndc_to_world.project_point3(ndc.extend(-1.0)).truncate() / WORLD_SCALE as f32;
cursor_map_position.x = world_position.x.round() as i32;
cursor_map_position.y = world_manager.world().height - world_position.y.round() as i32;
}
}
#[cfg(feature = "render")]
fn update_info_panel(
cursor_position: Res<'_, CursorMapPosition>,
world_manager: Res<'_, WorldManager>,
mut text: Query<'_, '_, &mut Text, With<InfoPanel>>,
) {
let world = world_manager.world();
text.single_mut().sections[0].value = if cursor_position.x >= 0
&& cursor_position.x < world.width
&& cursor_position.y >= 0
&& cursor_position.y < world.height
{
let cell = &world.terrain[cursor_position.y as usize][cursor_position.x as usize];
format!(
"Mouse position: {}\nAltitude: {}\nRainfall: {}\nTemperature: {}",
*cursor_position, cell.altitude, cell.rainfall, cell.temperature
)
} else {
format!("Mouse position: {}\nOut of bounds", *cursor_position)
};
}
#[cfg(feature = "render")]
fn generate_graphics(
mut commands: Commands<'_, '_>,
@ -198,7 +276,13 @@ fn generate_graphics(
mut world_manager: ResMut<'_, WorldManager>,
asset_server: Res<'_, AssetServer>,
) {
let world = world_manager.get_world().unwrap();
use bevy::ui::AlignSelf;
let world = world_manager.world();
let custom_sprite_size = Vec2 {
x: (WORLD_SCALE * world.width) as f32,
y: (WORLD_SCALE * world.height) as f32,
};
let image_handle = images.add(Image {
data: world_manager.world_color_bytes(),
@ -219,7 +303,17 @@ fn generate_graphics(
});
world_manager.image_handle_id = image_handle.id;
_ = commands.spawn_bundle(Camera2dBundle::default());
_ = commands
.spawn_bundle(Camera2dBundle::default())
.insert(PanCam::default());
_ = commands.spawn_bundle(SpriteBundle {
texture: images.get_handle(world_manager.image_handle_id),
sprite: Sprite {
custom_size: Some(custom_sprite_size),
..default()
},
..default()
});
_ = commands
.spawn_bundle(NodeBundle {
style: Style {
@ -230,15 +324,33 @@ fn generate_graphics(
..default()
})
.with_children(|root_node| {
_ = root_node.spawn_bundle(ImageBundle {
style: Style {
size: Size::new(Val::Percent(100.0), Val::Auto),
_ = root_node
.spawn_bundle(NodeBundle {
style: Style {
// align_items: AlignItems::FlexEnd,
align_self: AlignSelf::FlexEnd,
padding: UiRect::all(Val::Px(2.0)),
..default()
},
color: Color::rgba(1.0, 1.0, 1.0, 0.05).into(),
focus_policy: FocusPolicy::Pass,
..default()
},
image: UiImage(image_handle),
..default()
});
})
.with_children(|info_panel| {
_ = info_panel
.spawn_bundle(TextBundle {
text: Text::from_section(
"Info Panel",
bevy::text::TextStyle {
font: asset_server.load("JuliaMono.ttf"),
font_size: 15.0,
color: Color::WHITE,
},
),
..default()
})
.insert(InfoPanel);
});
_ = root_node
.spawn_bundle(NodeBundle {
style: Style {
@ -264,7 +376,7 @@ fn generate_graphics(
color: NORMAL_BUTTON.into(),
..default()
})
.insert(RainfallButton::default())
.insert(RainfallButton)
.with_children(|button| {
_ = button.spawn_bundle(TextBundle {
text: bevy::text::Text::from_section(
@ -289,7 +401,7 @@ fn generate_graphics(
color: NORMAL_BUTTON.into(),
..default()
})
.insert(TemperatureButton::default())
.insert(TemperatureButton)
.with_children(|button| {
_ = button.spawn_bundle(TextBundle {
text: bevy::text::Text::from_section(
@ -314,7 +426,7 @@ fn generate_graphics(
color: NORMAL_BUTTON.into(),
..default()
})
.insert(ContoursButton::default())
.insert(ContoursButton)
.with_children(|button| {
_ = button.spawn_bundle(TextBundle {
text: bevy::text::Text::from_section(
@ -332,6 +444,7 @@ fn generate_graphics(
});
}
const WORLD_SCALE: i32 = 3;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let mut app = App::new();
let mut manager = WorldManager::new();
@ -344,16 +457,19 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
// Use nearest-neighbor rendering for cripsier pixels
.insert_resource(ImageSettings::default_nearest())
.insert_resource(WindowDescriptor {
width: (2 * world.width) as f32,
height: (2 * world.height) as f32,
width: (WORLD_SCALE * world.width) as f32,
height: (WORLD_SCALE * world.height) as f32,
title: String::from("World-RS"),
resizable: true,
..default()
})
.insert_resource(CursorMapPosition::default())
.add_startup_system(generate_graphics)
.add_system(handle_rainfall_button)
.add_system(handle_temperature_button)
.add_system(handle_contours_button);
.add_system(handle_contours_button)
.add_system(update_cursor_map_position)
.add_system(update_info_panel);
}
#[cfg(not(feature = "render"))]
{
@ -375,7 +491,7 @@ fn main() -> Result<(), Box<dyn std::error::Error>> {
});
}
app.insert_resource(manager).add_plugins(WorldPlugins).run();
app.add_plugins(WorldPlugins).insert_resource(manager).run();
Ok(())
}

4
src/plugins/mod.rs Normal file
View file

@ -0,0 +1,4 @@
pub(crate) mod world_plugins;
pub(crate) use world_plugins::WorldPlugins;
pub(crate) use bevy_pancam::PanCam;

View file

@ -22,6 +22,8 @@ impl PluginGroup for WorldPlugins {
input::InputPlugin, render::RenderPlugin, sprite::SpritePlugin, text::TextPlugin,
transform::TransformPlugin, ui::UiPlugin, window::WindowPlugin, winit::WinitPlugin,
};
use bevy_pancam::PanCamPlugin;
_ = group
.add(TransformPlugin::default())
// hierarchy
@ -34,7 +36,8 @@ impl PluginGroup for WorldPlugins {
.add(CorePipelinePlugin::default())
.add(SpritePlugin::default())
.add(TextPlugin::default())
.add(UiPlugin::default());
.add(UiPlugin::default())
.add(PanCamPlugin::default());
}
#[cfg(not(feature = "render"))]
{