2026-04-21 14:01:33 -05:00
|
|
|
use std::sync::mpsc::{self, Receiver, TryRecvError};
|
|
|
|
|
use std::thread;
|
|
|
|
|
|
2026-04-21 14:30:42 -05:00
|
|
|
use arboard::{Clipboard, ImageData};
|
2026-04-21 14:01:33 -05:00
|
|
|
use eframe::egui::{
|
2026-04-21 14:34:03 -05:00
|
|
|
self, Align2, Color32, ColorImage, Context, CornerRadius, DragValue, Event, FontId, Frame, Key,
|
|
|
|
|
Response, RichText, Sense, Slider, Stroke, StrokeKind, TextureHandle, TextureOptions, Ui, Vec2,
|
2026-04-21 14:01:33 -05:00
|
|
|
};
|
|
|
|
|
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 {
|
2026-04-22 11:59:35 -05:00
|
|
|
TerrainComposite,
|
|
|
|
|
TerrainColor,
|
2026-04-21 14:01:33 -05:00
|
|
|
Heightmap,
|
|
|
|
|
Bumpmap,
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:44:31 -05:00
|
|
|
#[derive(Clone)]
|
2026-04-22 11:51:34 -05:00
|
|
|
struct TextureSet {
|
2026-04-22 11:59:35 -05:00
|
|
|
terrain_composite: TextureHandle,
|
|
|
|
|
terrain_color: TextureHandle,
|
2026-04-21 14:01:33 -05:00
|
|
|
heightmap: TextureHandle,
|
|
|
|
|
bumpmap: TextureHandle,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
pub struct TerrainApp {
|
|
|
|
|
params: GenerationParams,
|
|
|
|
|
generation_tab: LeftTab,
|
|
|
|
|
preview_focus: PreviewFocus,
|
2026-04-21 14:35:45 -05:00
|
|
|
hovered_preview: Option<PreviewFocus>,
|
2026-04-22 11:51:34 -05:00
|
|
|
textures: Option<TextureSet>,
|
2026-04-21 14:01:33 -05:00
|
|
|
generated: Option<GeneratedImages>,
|
|
|
|
|
generation_receiver: Option<Receiver<Result<GeneratedImages, String>>>,
|
|
|
|
|
generation_in_progress: bool,
|
2026-04-21 14:49:45 -05:00
|
|
|
copy_receiver: Option<Receiver<Result<String, String>>>,
|
|
|
|
|
copy_in_progress: bool,
|
2026-04-21 14:01:33 -05:00
|
|
|
status_message: String,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl TerrainApp {
|
|
|
|
|
pub fn new(_cc: &eframe::CreationContext<'_>) -> Self {
|
|
|
|
|
Self {
|
|
|
|
|
params: GenerationParams::default(),
|
|
|
|
|
generation_tab: LeftTab::Generation,
|
2026-04-22 11:59:35 -05:00
|
|
|
preview_focus: PreviewFocus::TerrainComposite,
|
2026-04-21 14:35:45 -05:00
|
|
|
hovered_preview: None,
|
2026-04-21 14:01:33 -05:00
|
|
|
textures: None,
|
|
|
|
|
generated: None,
|
|
|
|
|
generation_receiver: None,
|
|
|
|
|
generation_in_progress: false,
|
2026-04-21 14:49:45 -05:00
|
|
|
copy_receiver: None,
|
|
|
|
|
copy_in_progress: false,
|
2026-04-21 14:01:33 -05:00
|
|
|
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!(
|
2026-04-22 11:59:35 -05:00
|
|
|
"Generated {}x{} heightmap, bumpmap, terrain color, and composite.",
|
2026-04-21 14:01:33 -05:00
|
|
|
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;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:49:45 -05:00
|
|
|
fn poll_copy(&mut self, ctx: &Context) {
|
|
|
|
|
let Some(receiver) = &self.copy_receiver else {
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
match receiver.try_recv() {
|
|
|
|
|
Ok(Ok(message)) => {
|
|
|
|
|
self.status_message = message;
|
|
|
|
|
self.copy_receiver = None;
|
|
|
|
|
self.copy_in_progress = false;
|
|
|
|
|
}
|
|
|
|
|
Ok(Err(error)) => {
|
|
|
|
|
self.status_message = error;
|
|
|
|
|
self.copy_receiver = None;
|
|
|
|
|
self.copy_in_progress = false;
|
|
|
|
|
}
|
|
|
|
|
Err(TryRecvError::Empty) => {
|
|
|
|
|
ctx.request_repaint();
|
|
|
|
|
}
|
|
|
|
|
Err(TryRecvError::Disconnected) => {
|
|
|
|
|
self.status_message = String::from("Copy worker disconnected.");
|
|
|
|
|
self.copy_receiver = None;
|
|
|
|
|
self.copy_in_progress = false;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:01:33 -05:00
|
|
|
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);
|
2026-04-22 11:59:35 -05:00
|
|
|
let terrain_composite = rgba_to_color_image(&images.terrain_composite_image);
|
2026-04-22 11:51:34 -05:00
|
|
|
let terrain_color = rgba_to_color_image(&images.terrain_color_image);
|
2026-04-21 14:01:33 -05:00
|
|
|
|
2026-04-22 11:51:34 -05:00
|
|
|
self.textures = Some(TextureSet {
|
2026-04-22 11:59:35 -05:00
|
|
|
terrain_composite: ctx.load_texture(
|
|
|
|
|
"terrain_composite_preview",
|
|
|
|
|
terrain_composite,
|
|
|
|
|
TextureOptions::default(),
|
|
|
|
|
),
|
2026-04-22 11:51:34 -05:00
|
|
|
terrain_color: ctx.load_texture(
|
|
|
|
|
"terrain_color_preview",
|
|
|
|
|
terrain_color,
|
|
|
|
|
TextureOptions::default(),
|
|
|
|
|
),
|
2026-04-22 11:59:35 -05:00
|
|
|
heightmap: ctx.load_texture("heightmap_preview", heightmap, TextureOptions::default()),
|
|
|
|
|
bumpmap: ctx.load_texture("bumpmap_preview", bumpmap, TextureOptions::default()),
|
2026-04-21 14:01:33 -05:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:30:42 -05:00
|
|
|
fn copy_preview(&mut self, preview: PreviewFocus) {
|
2026-04-21 14:49:45 -05:00
|
|
|
if self.copy_in_progress {
|
|
|
|
|
self.status_message = String::from("Copy already in progress...");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:30:42 -05:00
|
|
|
let Some(generated) = &self.generated else {
|
|
|
|
|
self.status_message = String::from("No generated image is available to copy.");
|
|
|
|
|
return;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-22 11:51:34 -05:00
|
|
|
let image = preview_image_data(generated, preview);
|
2026-04-21 14:49:45 -05:00
|
|
|
let success_message = match preview {
|
2026-04-22 11:59:35 -05:00
|
|
|
PreviewFocus::TerrainComposite => {
|
|
|
|
|
String::from("Terrain composite copied to clipboard.")
|
|
|
|
|
}
|
|
|
|
|
PreviewFocus::TerrainColor => String::from("Terrain color copied to clipboard."),
|
2026-04-21 14:49:45 -05:00
|
|
|
PreviewFocus::Heightmap => String::from("Heightmap copied to clipboard."),
|
|
|
|
|
PreviewFocus::Bumpmap => String::from("Bumpmap copied to clipboard."),
|
2026-04-21 14:30:42 -05:00
|
|
|
};
|
|
|
|
|
|
2026-04-21 14:49:45 -05:00
|
|
|
let (tx, rx) = mpsc::channel();
|
|
|
|
|
self.copy_receiver = Some(rx);
|
|
|
|
|
self.copy_in_progress = true;
|
|
|
|
|
self.status_message = match preview {
|
2026-04-22 11:59:35 -05:00
|
|
|
PreviewFocus::TerrainComposite => {
|
|
|
|
|
String::from("Copying terrain composite to clipboard...")
|
|
|
|
|
}
|
|
|
|
|
PreviewFocus::TerrainColor => String::from("Copying terrain color to clipboard..."),
|
2026-04-21 14:49:45 -05:00
|
|
|
PreviewFocus::Heightmap => String::from("Copying heightmap to clipboard..."),
|
|
|
|
|
PreviewFocus::Bumpmap => String::from("Copying bumpmap to clipboard..."),
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
thread::spawn(move || {
|
2026-04-22 11:51:34 -05:00
|
|
|
let result = copy_image_to_clipboard(&image).map(|_| success_message);
|
2026-04-21 14:49:45 -05:00
|
|
|
let _ = tx.send(result);
|
|
|
|
|
});
|
2026-04-21 14:30:42 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:44:31 -05:00
|
|
|
fn preview_context_menu(response: &Response) -> bool {
|
|
|
|
|
let mut should_copy = false;
|
2026-04-21 14:30:42 -05:00
|
|
|
response.context_menu(|ui| {
|
|
|
|
|
if ui.button("Copy").clicked() {
|
2026-04-21 14:44:31 -05:00
|
|
|
should_copy = true;
|
2026-04-21 14:30:42 -05:00
|
|
|
ui.close();
|
|
|
|
|
}
|
|
|
|
|
});
|
2026-04-21 14:44:31 -05:00
|
|
|
should_copy
|
2026-04-21 14:30:42 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:44:31 -05:00
|
|
|
fn overlay_copy_button(ui: &mut Ui, anchor_rect: egui::Rect, preview: PreviewFocus) -> bool {
|
2026-04-21 14:30:42 -05:00
|
|
|
let button_size = Vec2::new(28.0, 28.0);
|
|
|
|
|
let button_rect = Align2::RIGHT_TOP
|
|
|
|
|
.align_size_within_rect(button_size, anchor_rect.shrink2(Vec2::new(8.0, 8.0)));
|
2026-04-21 14:34:03 -05:00
|
|
|
let response = ui.interact(
|
2026-04-21 14:30:42 -05:00
|
|
|
button_rect,
|
2026-04-21 14:34:03 -05:00
|
|
|
ui.make_persistent_id(("main_copy_button", preview == PreviewFocus::Bumpmap)),
|
|
|
|
|
Sense::click(),
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
let fill = if response.hovered() {
|
|
|
|
|
Color32::from_rgba_premultiplied(70, 90, 120, 220)
|
|
|
|
|
} else {
|
|
|
|
|
Color32::from_rgba_premultiplied(40, 40, 40, 180)
|
|
|
|
|
};
|
2026-04-21 14:44:31 -05:00
|
|
|
ui.painter()
|
|
|
|
|
.rect_filled(button_rect, CornerRadius::same(6), fill);
|
2026-04-21 14:34:03 -05:00
|
|
|
ui.painter().rect_stroke(
|
|
|
|
|
button_rect,
|
|
|
|
|
CornerRadius::same(6),
|
|
|
|
|
Stroke::new(1.0, Color32::from_rgba_premultiplied(255, 255, 255, 60)),
|
|
|
|
|
StrokeKind::Outside,
|
|
|
|
|
);
|
|
|
|
|
ui.painter().text(
|
|
|
|
|
button_rect.center(),
|
|
|
|
|
Align2::CENTER_CENTER,
|
|
|
|
|
"🗍",
|
|
|
|
|
FontId::proportional(14.0),
|
|
|
|
|
Color32::WHITE,
|
2026-04-21 14:30:42 -05:00
|
|
|
);
|
|
|
|
|
|
2026-04-21 14:44:31 -05:00
|
|
|
let clicked = response.clicked();
|
2026-04-21 14:30:42 -05:00
|
|
|
response.on_hover_text("Copy");
|
2026-04-21 14:44:31 -05:00
|
|
|
let _ = preview;
|
|
|
|
|
clicked
|
2026-04-21 14:30:42 -05:00
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:01:33 -05:00
|
|
|
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| {
|
2026-04-21 14:12:02 -05:00
|
|
|
ui.heading("Advanced Erosion Terrain Generator");
|
2026-04-21 14:01:33 -05:00
|
|
|
ui.add_space(8.0);
|
|
|
|
|
|
|
|
|
|
ui.horizontal(|ui| {
|
2026-04-21 14:12:02 -05:00
|
|
|
Self::tab_button(
|
|
|
|
|
ui,
|
|
|
|
|
&mut self.generation_tab,
|
|
|
|
|
LeftTab::Generation,
|
|
|
|
|
"Generation",
|
|
|
|
|
);
|
2026-04-21 14:01:33 -05:00
|
|
|
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();
|
|
|
|
|
}
|
2026-04-21 14:49:45 -05:00
|
|
|
if self.copy_in_progress {
|
|
|
|
|
ui.add_space(4.0);
|
|
|
|
|
ui.spinner();
|
|
|
|
|
}
|
2026-04-21 14:01:33 -05:00
|
|
|
|
|
|
|
|
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);
|
2026-04-22 11:51:34 -05:00
|
|
|
labeled_int_slider(
|
|
|
|
|
ui,
|
|
|
|
|
"Snowline Height",
|
|
|
|
|
&mut self.params.snowline_height,
|
|
|
|
|
0..=255,
|
|
|
|
|
);
|
2026-04-22 13:00:52 -05:00
|
|
|
labeled_int_slider(
|
|
|
|
|
ui,
|
|
|
|
|
"Water Table Height",
|
|
|
|
|
&mut self.params.water_table_height,
|
|
|
|
|
0..=255,
|
|
|
|
|
);
|
2026-04-22 11:51:34 -05:00
|
|
|
labeled_float_slider(
|
|
|
|
|
ui,
|
|
|
|
|
"Rock Slope Angle",
|
|
|
|
|
&mut self.params.rock_slope_angle_degrees,
|
|
|
|
|
0.0..=89.0,
|
|
|
|
|
1.0,
|
|
|
|
|
);
|
2026-04-21 14:01:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn ui_right_panel(&mut self, ctx: &Context) {
|
2026-04-21 14:44:31 -05:00
|
|
|
let mut pending_copy = None;
|
|
|
|
|
let mut pending_focus = None;
|
|
|
|
|
let mut pending_hover = None;
|
|
|
|
|
let preview_focus = self.preview_focus;
|
|
|
|
|
let textures = self.textures.clone();
|
2026-04-21 14:01:33 -05:00
|
|
|
egui::CentralPanel::default().show(ctx, |ui| {
|
2026-04-21 14:44:31 -05:00
|
|
|
let Some(textures) = &textures else {
|
2026-04-21 14:01:33 -05:00
|
|
|
ui.centered_and_justified(|ui| {
|
|
|
|
|
ui.label("No generated images yet.");
|
|
|
|
|
});
|
|
|
|
|
return;
|
|
|
|
|
};
|
2026-04-22 11:59:35 -05:00
|
|
|
let terrain_composite_texture = textures.terrain_composite.clone();
|
|
|
|
|
let terrain_color_texture = textures.terrain_color.clone();
|
2026-04-21 14:30:42 -05:00
|
|
|
let heightmap_texture = textures.heightmap.clone();
|
|
|
|
|
let bumpmap_texture = textures.bumpmap.clone();
|
2026-04-21 14:01:33 -05:00
|
|
|
|
|
|
|
|
let available = ui.available_size();
|
2026-04-21 14:44:31 -05:00
|
|
|
let bottom_strip_height = (available.y * 0.24).clamp(120.0, 220.0);
|
2026-04-21 14:01:33 -05:00
|
|
|
let main_height = (available.y - bottom_strip_height - 8.0).max(100.0);
|
|
|
|
|
|
2026-04-21 14:44:31 -05:00
|
|
|
let main_texture = match preview_focus {
|
2026-04-22 11:59:35 -05:00
|
|
|
PreviewFocus::TerrainComposite => &terrain_composite_texture,
|
|
|
|
|
PreviewFocus::TerrainColor => &terrain_color_texture,
|
2026-04-21 14:30:42 -05:00
|
|
|
PreviewFocus::Heightmap => &heightmap_texture,
|
|
|
|
|
PreviewFocus::Bumpmap => &bumpmap_texture,
|
2026-04-21 14:01:33 -05:00
|
|
|
};
|
|
|
|
|
|
2026-04-21 14:30:42 -05:00
|
|
|
let main_response =
|
|
|
|
|
render_clickable_image(ui, main_texture, Vec2::new(available.x, main_height), true);
|
2026-04-21 14:35:45 -05:00
|
|
|
if main_response.hovered() {
|
2026-04-21 14:44:31 -05:00
|
|
|
pending_hover = Some(preview_focus);
|
|
|
|
|
}
|
|
|
|
|
if Self::preview_context_menu(&main_response)
|
|
|
|
|
|| Self::overlay_copy_button(ui, main_response.rect, preview_focus)
|
|
|
|
|
{
|
|
|
|
|
pending_copy = Some(preview_focus);
|
2026-04-21 14:35:45 -05:00
|
|
|
}
|
2026-04-21 14:01:33 -05:00
|
|
|
ui.add_space(8.0);
|
|
|
|
|
|
2026-04-22 11:51:34 -05:00
|
|
|
let thumb_width = (available.x * 0.26).clamp(120.0, 220.0);
|
2026-04-21 14:12:02 -05:00
|
|
|
let panel_width = (thumb_width + 32.0).min(available.x);
|
2026-04-22 11:51:34 -05:00
|
|
|
let secondary_previews = secondary_previews(preview_focus);
|
2026-04-21 14:12:02 -05:00
|
|
|
|
2026-04-21 14:01:33 -05:00
|
|
|
ui.horizontal_centered(|ui| {
|
2026-04-22 11:51:34 -05:00
|
|
|
ui.horizontal(|ui| {
|
|
|
|
|
for secondary_focus in secondary_previews {
|
|
|
|
|
let secondary_texture = match secondary_focus {
|
2026-04-22 11:59:35 -05:00
|
|
|
PreviewFocus::TerrainComposite => &terrain_composite_texture,
|
|
|
|
|
PreviewFocus::TerrainColor => &terrain_color_texture,
|
2026-04-22 11:51:34 -05:00
|
|
|
PreviewFocus::Heightmap => &heightmap_texture,
|
|
|
|
|
PreviewFocus::Bumpmap => &bumpmap_texture,
|
|
|
|
|
};
|
|
|
|
|
let secondary_label = preview_label(secondary_focus);
|
|
|
|
|
|
|
|
|
|
Frame::group(ui.style()).show(ui, |ui| {
|
|
|
|
|
ui.set_min_width(panel_width);
|
|
|
|
|
ui.set_max_width(panel_width);
|
|
|
|
|
ui.vertical_centered(|ui| {
|
|
|
|
|
ui.label(RichText::new(secondary_label).strong());
|
|
|
|
|
ui.add_space(6.0);
|
|
|
|
|
let secondary_response = render_clickable_image(
|
|
|
|
|
ui,
|
|
|
|
|
secondary_texture,
|
|
|
|
|
Vec2::new(thumb_width, bottom_strip_height - 32.0),
|
|
|
|
|
true,
|
|
|
|
|
);
|
|
|
|
|
if secondary_response.hovered() {
|
|
|
|
|
pending_hover = Some(secondary_focus);
|
|
|
|
|
}
|
|
|
|
|
if Self::preview_context_menu(&secondary_response) {
|
|
|
|
|
pending_copy = Some(secondary_focus);
|
|
|
|
|
}
|
|
|
|
|
if secondary_response.clicked() {
|
|
|
|
|
pending_focus = Some(secondary_focus);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
});
|
|
|
|
|
}
|
2026-04-21 14:12:02 -05:00
|
|
|
});
|
2026-04-21 14:01:33 -05:00
|
|
|
});
|
|
|
|
|
});
|
2026-04-21 14:44:31 -05:00
|
|
|
|
|
|
|
|
if let Some(preview) = pending_copy {
|
|
|
|
|
self.copy_preview(preview);
|
|
|
|
|
}
|
|
|
|
|
if let Some(preview) = pending_focus {
|
|
|
|
|
self.preview_focus = preview;
|
|
|
|
|
}
|
|
|
|
|
self.hovered_preview = pending_hover;
|
2026-04-21 14:01:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
impl eframe::App for TerrainApp {
|
|
|
|
|
fn update(&mut self, ctx: &Context, _frame: &mut eframe::Frame) {
|
2026-04-21 14:35:45 -05:00
|
|
|
self.hovered_preview = None;
|
|
|
|
|
self.poll_generation(ctx);
|
2026-04-21 14:49:45 -05:00
|
|
|
self.poll_copy(ctx);
|
2026-04-21 14:35:45 -05:00
|
|
|
self.ui_left_panel(ctx);
|
|
|
|
|
self.ui_right_panel(ctx);
|
|
|
|
|
|
2026-04-21 14:30:42 -05:00
|
|
|
let wants_keyboard_input = ctx.wants_keyboard_input();
|
|
|
|
|
let should_copy = ctx.input(|input| {
|
|
|
|
|
!wants_keyboard_input
|
|
|
|
|
&& input.events.iter().any(|event| match event {
|
|
|
|
|
Event::Copy => true,
|
|
|
|
|
Event::Key {
|
|
|
|
|
key: Key::C,
|
|
|
|
|
pressed: true,
|
|
|
|
|
modifiers,
|
|
|
|
|
..
|
|
|
|
|
} => modifiers.ctrl || modifiers.command,
|
|
|
|
|
_ => false,
|
|
|
|
|
})
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
if should_copy {
|
2026-04-21 14:35:45 -05:00
|
|
|
let preview = self.hovered_preview.unwrap_or(self.preview_focus);
|
|
|
|
|
self.copy_preview(preview);
|
2026-04-21 14:30:42 -05:00
|
|
|
}
|
2026-04-21 14:01:33 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:51:34 -05:00
|
|
|
fn rgba_to_color_image(image: &image::RgbaImage) -> 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 [r, g, b, a] = pixel.0;
|
|
|
|
|
pixels.push(Color32::from_rgba_unmultiplied(r, g, b, a));
|
|
|
|
|
}
|
|
|
|
|
ColorImage::new(size, pixels)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn preview_image_data(images: &GeneratedImages, preview: PreviewFocus) -> ImageData<'static> {
|
|
|
|
|
match preview {
|
2026-04-22 11:59:35 -05:00
|
|
|
PreviewFocus::TerrainComposite => {
|
|
|
|
|
rgba_image_to_clipboard_data(&images.terrain_composite_image)
|
|
|
|
|
}
|
|
|
|
|
PreviewFocus::TerrainColor => rgba_image_to_clipboard_data(&images.terrain_color_image),
|
2026-04-22 11:51:34 -05:00
|
|
|
PreviewFocus::Heightmap => gray_image_to_clipboard_data(&images.heightmap_image),
|
|
|
|
|
PreviewFocus::Bumpmap => gray_image_to_clipboard_data(&images.bumpmap_image),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn preview_label(preview: PreviewFocus) -> &'static str {
|
|
|
|
|
match preview {
|
2026-04-22 11:59:35 -05:00
|
|
|
PreviewFocus::TerrainComposite => "Composite",
|
|
|
|
|
PreviewFocus::TerrainColor => "Terrain Color",
|
2026-04-22 11:51:34 -05:00
|
|
|
PreviewFocus::Heightmap => "Heightmap",
|
|
|
|
|
PreviewFocus::Bumpmap => "Bumpmap",
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:59:35 -05:00
|
|
|
fn secondary_previews(preview: PreviewFocus) -> [PreviewFocus; 3] {
|
2026-04-22 11:51:34 -05:00
|
|
|
match preview {
|
2026-04-22 11:59:35 -05:00
|
|
|
PreviewFocus::TerrainComposite => [
|
|
|
|
|
PreviewFocus::TerrainColor,
|
|
|
|
|
PreviewFocus::Heightmap,
|
|
|
|
|
PreviewFocus::Bumpmap,
|
|
|
|
|
],
|
|
|
|
|
PreviewFocus::TerrainColor => [
|
|
|
|
|
PreviewFocus::TerrainComposite,
|
|
|
|
|
PreviewFocus::Heightmap,
|
|
|
|
|
PreviewFocus::Bumpmap,
|
|
|
|
|
],
|
|
|
|
|
PreviewFocus::Heightmap => [
|
|
|
|
|
PreviewFocus::TerrainComposite,
|
|
|
|
|
PreviewFocus::TerrainColor,
|
|
|
|
|
PreviewFocus::Bumpmap,
|
|
|
|
|
],
|
|
|
|
|
PreviewFocus::Bumpmap => [
|
|
|
|
|
PreviewFocus::TerrainComposite,
|
|
|
|
|
PreviewFocus::TerrainColor,
|
|
|
|
|
PreviewFocus::Heightmap,
|
|
|
|
|
],
|
2026-04-22 11:51:34 -05:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn labeled_int_slider<Num: egui::emath::Numeric>(
|
2026-04-21 14:12:02 -05:00
|
|
|
ui: &mut Ui,
|
|
|
|
|
label: &str,
|
2026-04-22 11:51:34 -05:00
|
|
|
value: &mut Num,
|
|
|
|
|
range: std::ops::RangeInclusive<Num>,
|
2026-04-21 14:12:02 -05:00
|
|
|
) {
|
2026-04-21 14:01:33 -05:00
|
|
|
ui.label(label);
|
2026-04-22 11:51:34 -05:00
|
|
|
ui.add(Slider::new(value, range));
|
2026-04-21 14:01:33 -05:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-21 14:12:02 -05:00
|
|
|
fn render_clickable_image(
|
|
|
|
|
ui: &mut Ui,
|
|
|
|
|
texture: &TextureHandle,
|
|
|
|
|
max_size: Vec2,
|
|
|
|
|
clickable: bool,
|
2026-04-21 14:30:42 -05:00
|
|
|
) -> Response {
|
2026-04-21 14:01:33 -05:00
|
|
|
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)
|
2026-04-21 14:12:02 -05:00
|
|
|
.sense(if clickable {
|
|
|
|
|
Sense::click()
|
|
|
|
|
} else {
|
2026-04-21 14:44:31 -05:00
|
|
|
Sense::hover()
|
2026-04-21 14:12:02 -05:00
|
|
|
});
|
2026-04-21 14:30:42 -05:00
|
|
|
let response = ui.add(image);
|
|
|
|
|
|
|
|
|
|
if response.hovered() {
|
|
|
|
|
ui.painter().rect_stroke(
|
|
|
|
|
response.rect.expand(4.0),
|
|
|
|
|
CornerRadius::same(8),
|
|
|
|
|
Stroke::new(2.0, Color32::from_rgba_premultiplied(180, 220, 255, 180)),
|
|
|
|
|
StrokeKind::Outside,
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
response
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:51:34 -05:00
|
|
|
fn gray_image_to_clipboard_data(image: &image::GrayImage) -> ImageData<'static> {
|
2026-04-21 14:30:42 -05:00
|
|
|
let mut rgba = Vec::with_capacity((image.width() * image.height() * 4) as usize);
|
|
|
|
|
for pixel in image.pixels() {
|
|
|
|
|
let gray = pixel.0[0];
|
|
|
|
|
rgba.extend_from_slice(&[gray, gray, gray, 255]);
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-22 11:51:34 -05:00
|
|
|
ImageData {
|
2026-04-21 14:30:42 -05:00
|
|
|
width: image.width() as usize,
|
|
|
|
|
height: image.height() as usize,
|
|
|
|
|
bytes: rgba.into(),
|
2026-04-22 11:51:34 -05:00
|
|
|
}
|
|
|
|
|
}
|
2026-04-21 14:30:42 -05:00
|
|
|
|
2026-04-22 11:51:34 -05:00
|
|
|
fn rgba_image_to_clipboard_data(image: &image::RgbaImage) -> ImageData<'static> {
|
|
|
|
|
let mut rgba = Vec::with_capacity((image.width() * image.height() * 4) as usize);
|
|
|
|
|
for pixel in image.pixels() {
|
|
|
|
|
rgba.extend_from_slice(&pixel.0);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
ImageData {
|
|
|
|
|
width: image.width() as usize,
|
|
|
|
|
height: image.height() as usize,
|
|
|
|
|
bytes: rgba.into(),
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fn copy_image_to_clipboard(image: &ImageData<'static>) -> Result<(), String> {
|
2026-04-21 14:30:42 -05:00
|
|
|
let mut clipboard = Clipboard::new().map_err(|error| error.to_string())?;
|
2026-04-22 11:51:34 -05:00
|
|
|
clipboard
|
|
|
|
|
.set_image(image.clone())
|
|
|
|
|
.map_err(|error| error.to_string())
|
2026-04-21 14:01:33 -05:00
|
|
|
}
|