initial commit for standalone rust program
This commit is contained in:
+331
@@ -0,0 +1,331 @@
|
||||
use std::sync::mpsc::{self, Receiver, TryRecvError};
|
||||
use std::thread;
|
||||
|
||||
use eframe::egui::{
|
||||
self, Color32, ColorImage, Context, DragValue, RichText, Sense, Slider, TextureHandle,
|
||||
TextureOptions, Ui, Vec2,
|
||||
};
|
||||
use rand::Rng;
|
||||
|
||||
use crate::generator::{self, GeneratedImages, GenerationParams};
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
enum LeftTab {
|
||||
Generation,
|
||||
Advanced,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Eq, PartialEq)]
|
||||
enum PreviewFocus {
|
||||
Heightmap,
|
||||
Bumpmap,
|
||||
}
|
||||
|
||||
struct TexturePair {
|
||||
heightmap: TextureHandle,
|
||||
bumpmap: TextureHandle,
|
||||
}
|
||||
|
||||
pub struct TerrainApp {
|
||||
params: GenerationParams,
|
||||
generation_tab: LeftTab,
|
||||
preview_focus: PreviewFocus,
|
||||
textures: Option<TexturePair>,
|
||||
generated: Option<GeneratedImages>,
|
||||
generation_receiver: Option<Receiver<Result<GeneratedImages, String>>>,
|
||||
generation_in_progress: bool,
|
||||
status_message: String,
|
||||
}
|
||||
|
||||
impl TerrainApp {
|
||||
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
||||
Self {
|
||||
params: GenerationParams::default(),
|
||||
generation_tab: LeftTab::Generation,
|
||||
preview_focus: PreviewFocus::Heightmap,
|
||||
textures: None,
|
||||
generated: None,
|
||||
generation_receiver: None,
|
||||
generation_in_progress: false,
|
||||
status_message: String::from("Adjust parameters and click Generate."),
|
||||
}
|
||||
}
|
||||
|
||||
fn start_generation(&mut self) {
|
||||
if self.generation_in_progress {
|
||||
return;
|
||||
}
|
||||
|
||||
let params = self.params.clone();
|
||||
let (tx, rx) = mpsc::channel();
|
||||
self.generation_receiver = Some(rx);
|
||||
self.generation_in_progress = true;
|
||||
self.status_message = format!(
|
||||
"Generating {}x{} terrain...",
|
||||
params.image_width, params.image_height
|
||||
);
|
||||
|
||||
thread::spawn(move || {
|
||||
let result = std::panic::catch_unwind(|| generator::generate(¶ms))
|
||||
.map_err(|_| String::from("Generation panicked unexpectedly."));
|
||||
let _ = tx.send(result);
|
||||
});
|
||||
}
|
||||
|
||||
fn poll_generation(&mut self, ctx: &Context) {
|
||||
let Some(receiver) = &self.generation_receiver else {
|
||||
return;
|
||||
};
|
||||
|
||||
match receiver.try_recv() {
|
||||
Ok(Ok(images)) => {
|
||||
self.status_message = format!(
|
||||
"Generated {}x{} heightmap and bumpmap.",
|
||||
images.width, images.height
|
||||
);
|
||||
self.install_textures(ctx, &images);
|
||||
self.generated = Some(images);
|
||||
self.generation_receiver = None;
|
||||
self.generation_in_progress = false;
|
||||
}
|
||||
Ok(Err(error)) => {
|
||||
self.status_message = error;
|
||||
self.generation_receiver = None;
|
||||
self.generation_in_progress = false;
|
||||
}
|
||||
Err(TryRecvError::Empty) => {
|
||||
ctx.request_repaint();
|
||||
}
|
||||
Err(TryRecvError::Disconnected) => {
|
||||
self.status_message = String::from("Generation worker disconnected.");
|
||||
self.generation_receiver = None;
|
||||
self.generation_in_progress = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn install_textures(&mut self, ctx: &Context, images: &GeneratedImages) {
|
||||
let heightmap = gray_to_color_image(&images.heightmap_image);
|
||||
let bumpmap = gray_to_color_image(&images.bumpmap_image);
|
||||
|
||||
self.textures = Some(TexturePair {
|
||||
heightmap: ctx.load_texture("heightmap_preview", heightmap, TextureOptions::default()),
|
||||
bumpmap: ctx.load_texture("bumpmap_preview", bumpmap, TextureOptions::default()),
|
||||
});
|
||||
}
|
||||
|
||||
fn tab_button(ui: &mut Ui, current: &mut LeftTab, tab: LeftTab, label: &str) {
|
||||
let selected = *current == tab;
|
||||
if ui.selectable_label(selected, label).clicked() {
|
||||
*current = tab;
|
||||
}
|
||||
}
|
||||
|
||||
fn ui_left_panel(&mut self, ctx: &Context) {
|
||||
egui::SidePanel::left("controls_panel")
|
||||
.resizable(true)
|
||||
.default_width(320.0)
|
||||
.min_width(280.0)
|
||||
.show(ctx, |ui| {
|
||||
ui.heading("Terrain Generator");
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.horizontal(|ui| {
|
||||
Self::tab_button(ui, &mut self.generation_tab, LeftTab::Generation, "Generation");
|
||||
Self::tab_button(ui, &mut self.generation_tab, LeftTab::Advanced, "Advanced");
|
||||
});
|
||||
|
||||
ui.separator();
|
||||
|
||||
match self.generation_tab {
|
||||
LeftTab::Generation => self.ui_generation_tab(ui),
|
||||
LeftTab::Advanced => self.ui_advanced_tab(ui),
|
||||
}
|
||||
|
||||
ui.add_space(12.0);
|
||||
let button = ui.add_sized(
|
||||
[ui.available_width(), 56.0],
|
||||
egui::Button::new(RichText::new("Generate").size(20.0)),
|
||||
);
|
||||
if button.clicked() {
|
||||
self.start_generation();
|
||||
}
|
||||
|
||||
if self.generation_in_progress {
|
||||
ui.add_space(8.0);
|
||||
ui.spinner();
|
||||
}
|
||||
|
||||
ui.add_space(8.0);
|
||||
ui.label(&self.status_message);
|
||||
});
|
||||
}
|
||||
|
||||
fn ui_generation_tab(&mut self, ui: &mut Ui) {
|
||||
labeled_int_slider(ui, "Image Width", &mut self.params.image_width, 1..=5000);
|
||||
labeled_int_slider(ui, "Image Height", &mut self.params.image_height, 1..=5000);
|
||||
labeled_float_slider(
|
||||
ui,
|
||||
"Generation Scale",
|
||||
&mut self.params.generation_scale,
|
||||
0.1..=50.0,
|
||||
0.1,
|
||||
);
|
||||
labeled_float_slider(
|
||||
ui,
|
||||
"Erosion Scale",
|
||||
&mut self.params.erosion_scale,
|
||||
0.08..=0.25,
|
||||
0.01,
|
||||
);
|
||||
|
||||
ui.label("Random Seed");
|
||||
ui.horizontal(|ui| {
|
||||
ui.add(
|
||||
DragValue::new(&mut self.params.random_seed)
|
||||
.range(0..=u64::MAX)
|
||||
.speed(1.0),
|
||||
);
|
||||
|
||||
if ui.button("Random").clicked() {
|
||||
self.params.random_seed = rand::rng().random::<u64>();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn ui_advanced_tab(&mut self, ui: &mut Ui) {
|
||||
labeled_float_slider(
|
||||
ui,
|
||||
"Strength",
|
||||
&mut self.params.erosion_strength,
|
||||
0.01..=0.10,
|
||||
0.01,
|
||||
);
|
||||
labeled_float_slider(
|
||||
ui,
|
||||
"Gully Weight",
|
||||
&mut self.params.erosion_gully_weight,
|
||||
0.0..=1.0,
|
||||
0.01,
|
||||
);
|
||||
labeled_float_slider(
|
||||
ui,
|
||||
"Detail",
|
||||
&mut self.params.erosion_detail,
|
||||
0.7..=3.0,
|
||||
0.1,
|
||||
);
|
||||
labeled_int_slider(ui, "Octaves", &mut self.params.erosion_octaves, 1..=8);
|
||||
}
|
||||
|
||||
fn ui_right_panel(&mut self, ctx: &Context) {
|
||||
egui::CentralPanel::default().show(ctx, |ui| {
|
||||
let Some(textures) = &self.textures else {
|
||||
ui.centered_and_justified(|ui| {
|
||||
ui.label("No generated images yet.");
|
||||
});
|
||||
return;
|
||||
};
|
||||
|
||||
let available = ui.available_size();
|
||||
let bottom_strip_height = (available.y * 0.24).max(120.0).min(220.0);
|
||||
let main_height = (available.y - bottom_strip_height - 8.0).max(100.0);
|
||||
|
||||
let (main_texture, bottom_texture) = match self.preview_focus {
|
||||
PreviewFocus::Heightmap => (&textures.heightmap, &textures.bumpmap),
|
||||
PreviewFocus::Bumpmap => (&textures.bumpmap, &textures.heightmap),
|
||||
};
|
||||
|
||||
render_clickable_image(ui, main_texture, Vec2::new(available.x, main_height), false);
|
||||
ui.add_space(8.0);
|
||||
|
||||
ui.horizontal_centered(|ui| {
|
||||
let thumb_width = ((available.x - 24.0) * 0.28).max(100.0).min(220.0);
|
||||
let spacer = (available.x - thumb_width * 2.0).max(0.0);
|
||||
|
||||
let main_thumb_clicked = render_clickable_image(
|
||||
ui,
|
||||
match self.preview_focus {
|
||||
PreviewFocus::Heightmap => &textures.heightmap,
|
||||
PreviewFocus::Bumpmap => &textures.bumpmap,
|
||||
},
|
||||
Vec2::new(thumb_width, bottom_strip_height),
|
||||
true,
|
||||
);
|
||||
|
||||
ui.add_space(spacer);
|
||||
|
||||
let secondary_clicked =
|
||||
render_clickable_image(ui, bottom_texture, Vec2::new(thumb_width, bottom_strip_height), true);
|
||||
|
||||
if main_thumb_clicked {
|
||||
self.preview_focus = match self.preview_focus {
|
||||
PreviewFocus::Heightmap => PreviewFocus::Heightmap,
|
||||
PreviewFocus::Bumpmap => PreviewFocus::Bumpmap,
|
||||
};
|
||||
}
|
||||
|
||||
if secondary_clicked {
|
||||
self.preview_focus = match self.preview_focus {
|
||||
PreviewFocus::Heightmap => PreviewFocus::Bumpmap,
|
||||
PreviewFocus::Bumpmap => PreviewFocus::Heightmap,
|
||||
};
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl eframe::App for TerrainApp {
|
||||
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
||||
self.poll_generation(ctx);
|
||||
self.ui_left_panel(ctx);
|
||||
self.ui_right_panel(ctx);
|
||||
}
|
||||
}
|
||||
|
||||
fn gray_to_color_image(image: &image::GrayImage) -> ColorImage {
|
||||
let size = [image.width() as usize, image.height() as usize];
|
||||
let mut pixels = Vec::with_capacity(size[0] * size[1]);
|
||||
for pixel in image.pixels() {
|
||||
let value = pixel.0[0];
|
||||
pixels.push(Color32::from_gray(value));
|
||||
}
|
||||
ColorImage::new(size, pixels)
|
||||
}
|
||||
|
||||
fn labeled_int_slider(ui: &mut Ui, label: &str, value: &mut usize, range: std::ops::RangeInclusive<usize>) {
|
||||
ui.label(label);
|
||||
let mut slider_value = *value as u32;
|
||||
if ui
|
||||
.add(Slider::new(
|
||||
&mut slider_value,
|
||||
*range.start() as u32..=*range.end() as u32,
|
||||
))
|
||||
.changed()
|
||||
{
|
||||
*value = slider_value as usize;
|
||||
}
|
||||
}
|
||||
|
||||
fn labeled_float_slider(
|
||||
ui: &mut Ui,
|
||||
label: &str,
|
||||
value: &mut f32,
|
||||
range: std::ops::RangeInclusive<f32>,
|
||||
step: f32,
|
||||
) {
|
||||
ui.label(label);
|
||||
let slider = Slider::new(value, range).step_by(step as f64);
|
||||
ui.add(slider);
|
||||
}
|
||||
|
||||
fn render_clickable_image(ui: &mut Ui, texture: &TextureHandle, max_size: Vec2, clickable: bool) -> bool {
|
||||
let image_size = texture.size_vec2();
|
||||
let scale = (max_size.x / image_size.x).min(max_size.y / image_size.y);
|
||||
let desired_size = image_size * scale.max(0.01);
|
||||
let image = egui::Image::new(texture)
|
||||
.fit_to_exact_size(desired_size)
|
||||
.sense(if clickable { Sense::click() } else { Sense::hover() });
|
||||
ui.add(image).clicked()
|
||||
}
|
||||
Reference in New Issue
Block a user