added export for images

This commit is contained in:
grimsace
2026-04-22 13:46:19 -05:00
parent 20c4871971
commit b9810bf873
3 changed files with 233 additions and 21 deletions
Generated
+73 -14
View File
@@ -124,6 +124,7 @@ dependencies = [
"image", "image",
"rand", "rand",
"rayon", "rayon",
"rfd",
] ]
[[package]] [[package]]
@@ -461,6 +462,15 @@ dependencies = [
"objc2 0.5.2", "objc2 0.5.2",
] ]
[[package]]
name = "block2"
version = "0.6.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cdeb9d870516001442e364c5220d3574d2da8dc765554b4a617230d33fa58ef5"
dependencies = [
"objc2 0.6.4",
]
[[package]] [[package]]
name = "blocking" name = "blocking"
version = "1.6.2" version = "1.6.2"
@@ -755,6 +765,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38" checksum = "1e0e367e4e7da84520dedcac1901e4da967309406d1e51017ae1abfb97adbd38"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2 0.6.2",
"libc",
"objc2 0.6.4", "objc2 0.6.4",
] ]
@@ -1536,10 +1548,23 @@ checksum = "85ab80394333c02fe689eaf900ab500fbd0c2213da414687ebf995a65d5a6104"
dependencies = [ dependencies = [
"bytemuck", "bytemuck",
"byteorder-lite", "byteorder-lite",
"image-webp",
"moxcms", "moxcms",
"num-traits", "num-traits",
"png", "png",
"tiff", "tiff",
"zune-core",
"zune-jpeg",
]
[[package]]
name = "image-webp"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "525e9ff3e1a4be2fbea1fdf0e98686a6d98b4d8f937e1bf7402245af1909e8c3"
dependencies = [
"byteorder-lite",
"quick-error",
] ]
[[package]] [[package]]
@@ -1939,7 +1964,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff" checksum = "e4e89ad9e3d7d297152b17d39ed92cd50ca8063a89a9fa569046d41568891eff"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-data", "objc2-core-data",
@@ -1955,6 +1980,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c" checksum = "d49e936b501e5c5bf01fda3a9452ff86dc3ea98ad5f283e1455153142d97518c"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2 0.6.2",
"objc2 0.6.4", "objc2 0.6.4",
"objc2-core-foundation", "objc2-core-foundation",
"objc2-core-graphics", "objc2-core-graphics",
@@ -1968,7 +1994,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009" checksum = "74dd3b56391c7a0596a295029734d3c1c5e7e510a4cb30245f8221ccea96b009"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -1980,7 +2006,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889" checksum = "a5ff520e9c33812fd374d8deecef01d4a840e7b41862d849513de77e44aa4889"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -1992,7 +2018,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef" checksum = "617fbf49e071c178c0b24c080767db52958f716d9eabdf0890523aeae54773ef"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2027,7 +2053,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80" checksum = "55260963a527c99f1819c4f8e3b47fe04f9650694ef348ffd2227e8196d34c80"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-metal", "objc2-metal",
@@ -2039,7 +2065,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781" checksum = "000cfee34e683244f284252ee206a27953279d370e309649dc3ee317b37e5781"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-contacts", "objc2-contacts",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -2058,7 +2084,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8" checksum = "0ee638a5da3799329310ad4cfa62fbf045d5f56e3ef5ba4149e7452dcf89d5a8"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"dispatch", "dispatch",
"libc", "libc",
"objc2 0.5.2", "objc2 0.5.2",
@@ -2092,7 +2118,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398" checksum = "a1a1ae721c5e35be65f01a03b6d2ac13a54cb4fa70d8a5da293d7b0020261398"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-app-kit 0.2.2", "objc2-app-kit 0.2.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -2105,7 +2131,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6" checksum = "dd0cba1276f6023976a406a14ffa85e1fdd19df6b0f737b063b95f6c8c7aadd6"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2117,7 +2143,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a" checksum = "e42bee7bff906b14b167da2bac5efe6b6a07e6f7c0a21a7308d40c960242dc7a"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
"objc2-metal", "objc2-metal",
@@ -2140,7 +2166,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f" checksum = "b8bb46798b20cd6b91cbd113524c490f1686f4c4e8f49502431415f3512e2b6f"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-cloud-kit", "objc2-cloud-kit",
"objc2-core-data", "objc2-core-data",
@@ -2160,7 +2186,7 @@ version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe" checksum = "44fa5f9748dbfe1ca6c0b79ad20725a11eca7c2218bceb4b005cb1be26273bfe"
dependencies = [ dependencies = [
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
] ]
@@ -2172,7 +2198,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3" checksum = "76cfcbf642358e8689af64cee815d139339f3ed8ad05103ed5eaf73db8d84cb3"
dependencies = [ dependencies = [
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"objc2 0.5.2", "objc2 0.5.2",
"objc2-core-location", "objc2-core-location",
"objc2-foundation 0.2.2", "objc2-foundation 0.2.2",
@@ -2339,6 +2365,12 @@ dependencies = [
"windows-sys 0.61.2", "windows-sys 0.61.2",
] ]
[[package]]
name = "pollster"
version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "2f3a9f18d041e6d0e102a0a46750538147e5e8992d3b4873aaafee2520b00ce3"
[[package]] [[package]]
name = "portable-atomic" name = "portable-atomic"
version = "1.13.1" version = "1.13.1"
@@ -2558,6 +2590,33 @@ version = "1.1.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832" checksum = "19b30a45b0cd0bcca8037f3d0dc3421eaf95327a17cad11964fb8179b4fc4832"
[[package]]
name = "rfd"
version = "0.17.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "20dafead71c16a34e1ff357ddefc8afc11e7d51d6d2b9fbd07eaa48e3e540220"
dependencies = [
"block2 0.6.2",
"dispatch2",
"js-sys",
"libc",
"log",
"objc2 0.6.4",
"objc2-app-kit 0.3.2",
"objc2-core-foundation",
"objc2-foundation 0.3.2",
"percent-encoding",
"pollster",
"raw-window-handle",
"wasm-bindgen",
"wasm-bindgen-futures",
"wayland-backend",
"wayland-client",
"wayland-protocols",
"web-sys",
"windows-sys 0.61.2",
]
[[package]] [[package]]
name = "rustc-hash" name = "rustc-hash"
version = "1.1.0" version = "1.1.0"
@@ -3953,7 +4012,7 @@ dependencies = [
"android-activity", "android-activity",
"atomic-waker", "atomic-waker",
"bitflags 2.11.1", "bitflags 2.11.1",
"block2", "block2 0.5.1",
"bytemuck", "bytemuck",
"calloop 0.13.0", "calloop 0.13.0",
"cfg_aliases", "cfg_aliases",
+2 -1
View File
@@ -7,9 +7,10 @@ edition = "2024"
arboard = "3.6" arboard = "3.6"
eframe = "0.33" eframe = "0.33"
egui = "0.33" egui = "0.33"
image = { version = "0.25", default-features = false, features = ["png"] } image = { version = "0.25", default-features = false, features = ["jpeg", "png", "webp"] }
rand = "0.9" rand = "0.9"
rayon = "1.10" rayon = "1.10"
rfd = "0.17.2"
[profile.release] [profile.release]
opt-level = "z" opt-level = "z"
+158 -6
View File
@@ -43,6 +43,8 @@ pub struct TerrainApp {
generation_in_progress: bool, generation_in_progress: bool,
copy_receiver: Option<Receiver<Result<String, String>>>, copy_receiver: Option<Receiver<Result<String, String>>>,
copy_in_progress: bool, copy_in_progress: bool,
export_receiver: Option<Receiver<Result<String, String>>>,
export_in_progress: bool,
status_message: String, status_message: String,
} }
@@ -59,6 +61,8 @@ impl TerrainApp {
generation_in_progress: false, generation_in_progress: false,
copy_receiver: None, copy_receiver: None,
copy_in_progress: false, copy_in_progress: false,
export_receiver: None,
export_in_progress: false,
status_message: String::from("Adjust parameters and click Generate."), status_message: String::from("Adjust parameters and click Generate."),
} }
} }
@@ -143,6 +147,94 @@ impl TerrainApp {
} }
} }
fn poll_export(&mut self, ctx: &Context) {
let Some(receiver) = &self.export_receiver else {
return;
};
match receiver.try_recv() {
Ok(Ok(message)) => {
self.status_message = message;
self.export_receiver = None;
self.export_in_progress = false;
}
Ok(Err(error)) => {
self.status_message = error;
self.export_receiver = None;
self.export_in_progress = false;
}
Err(TryRecvError::Empty) => {
ctx.request_repaint();
}
Err(TryRecvError::Disconnected) => {
self.status_message = String::from("Export worker disconnected.");
self.export_receiver = None;
self.export_in_progress = false;
}
}
}
fn start_export(&mut self, preview: PreviewFocus) {
if self.export_in_progress {
self.status_message = String::from("Export already in progress...");
return;
}
let Some(generated) = &self.generated else {
self.status_message = String::from("No generated image is available to export.");
return;
};
let label = preview_label(preview).to_lowercase().replace(' ', "_");
let default_name = format!("{}.png", label);
let files = rfd::FileDialog::new()
.add_filter("PNG Image", &["png"])
.add_filter("JPEG Image", &["jpg", "jpeg"])
.add_filter("WebP Image", &["webp"])
.set_file_name(default_name)
.save_file();
let Some(path) = files else {
return;
};
let generated = generated.clone();
let (tx, rx) = mpsc::channel();
self.export_receiver = Some(rx);
self.export_in_progress = true;
self.status_message = format!("Exporting to {}...", path.display());
thread::spawn(move || {
let result = (|| -> Result<String, String> {
let format = match path.extension().and_then(|s| s.to_str()) {
Some("jpg") | Some("jpeg") => image::ImageFormat::Jpeg,
Some("webp") => image::ImageFormat::WebP,
_ => image::ImageFormat::Png,
};
match preview {
PreviewFocus::TerrainComposite => generated
.terrain_composite_image
.save_with_format(&path, format),
PreviewFocus::TerrainColor => generated
.terrain_color_image
.save_with_format(&path, format),
PreviewFocus::Heightmap => {
generated.heightmap_image.save_with_format(&path, format)
}
PreviewFocus::Bumpmap => {
generated.bumpmap_image.save_with_format(&path, format)
}
}
.map_err(|e| format!("Failed to save image: {}", e))?;
Ok(format!("Successfully exported to {}.", path.display()))
})();
let _ = tx.send(result);
});
}
fn install_textures(&mut self, ctx: &Context, images: &GeneratedImages) { fn install_textures(&mut self, ctx: &Context, images: &GeneratedImages) {
let heightmap = gray_to_color_image(&images.heightmap_image); let heightmap = gray_to_color_image(&images.heightmap_image);
let bumpmap = gray_to_color_image(&images.bumpmap_image); let bumpmap = gray_to_color_image(&images.bumpmap_image);
@@ -204,15 +296,20 @@ impl TerrainApp {
}); });
} }
fn preview_context_menu(response: &Response) -> bool { fn preview_context_menu(response: &Response) -> (bool, bool) {
let mut should_copy = false; let mut should_copy = false;
let mut should_export = false;
response.context_menu(|ui| { response.context_menu(|ui| {
if ui.button("Copy").clicked() { if ui.button("Copy").clicked() {
should_copy = true; should_copy = true;
ui.close(); ui.close();
} }
if ui.button("Export").clicked() {
should_export = true;
ui.close();
}
}); });
should_copy (should_copy, should_export)
} }
fn overlay_copy_button(ui: &mut Ui, anchor_rect: egui::Rect, preview: PreviewFocus) -> bool { fn overlay_copy_button(ui: &mut Ui, anchor_rect: egui::Rect, preview: PreviewFocus) -> bool {
@@ -252,6 +349,43 @@ impl TerrainApp {
clicked clicked
} }
fn overlay_export_button(ui: &mut Ui, anchor_rect: egui::Rect, preview: PreviewFocus) -> bool {
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 + 32.0, 8.0)));
let response = ui.interact(
button_rect,
ui.make_persistent_id(("main_export_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)
};
ui.painter()
.rect_filled(button_rect, CornerRadius::same(6), fill);
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,
);
let clicked = response.clicked();
response.on_hover_text("Export");
let _ = preview;
clicked
}
fn tab_button(ui: &mut Ui, current: &mut LeftTab, tab: LeftTab, label: &str) { fn tab_button(ui: &mut Ui, current: &mut LeftTab, tab: LeftTab, label: &str) {
let selected = *current == tab; let selected = *current == tab;
if ui.selectable_label(selected, label).clicked() { if ui.selectable_label(selected, label).clicked() {
@@ -302,6 +436,10 @@ impl TerrainApp {
ui.add_space(4.0); ui.add_space(4.0);
ui.spinner(); ui.spinner();
} }
if self.export_in_progress {
ui.add_space(4.0);
ui.spinner();
}
ui.add_space(8.0); ui.add_space(8.0);
ui.label(&self.status_message); ui.label(&self.status_message);
@@ -386,6 +524,7 @@ impl TerrainApp {
fn ui_right_panel(&mut self, ctx: &Context) { fn ui_right_panel(&mut self, ctx: &Context) {
let mut pending_copy = None; let mut pending_copy = None;
let mut pending_export = None;
let mut pending_focus = None; let mut pending_focus = None;
let mut pending_hover = None; let mut pending_hover = None;
let preview_focus = self.preview_focus; let preview_focus = self.preview_focus;
@@ -418,11 +557,15 @@ impl TerrainApp {
if main_response.hovered() { if main_response.hovered() {
pending_hover = Some(preview_focus); pending_hover = Some(preview_focus);
} }
if Self::preview_context_menu(&main_response)
|| Self::overlay_copy_button(ui, main_response.rect, preview_focus) let (context_copy, context_export) = Self::preview_context_menu(&main_response);
{ if context_copy || Self::overlay_copy_button(ui, main_response.rect, preview_focus) {
pending_copy = Some(preview_focus); pending_copy = Some(preview_focus);
} }
if context_export || Self::overlay_export_button(ui, main_response.rect, preview_focus)
{
pending_export = Some(preview_focus);
}
ui.add_space(8.0); ui.add_space(8.0);
let thumb_width = (available.x * 0.26).clamp(120.0, 220.0); let thumb_width = (available.x * 0.26).clamp(120.0, 220.0);
@@ -455,9 +598,14 @@ impl TerrainApp {
if secondary_response.hovered() { if secondary_response.hovered() {
pending_hover = Some(secondary_focus); pending_hover = Some(secondary_focus);
} }
if Self::preview_context_menu(&secondary_response) { let (context_copy, context_export) =
Self::preview_context_menu(&secondary_response);
if context_copy {
pending_copy = Some(secondary_focus); pending_copy = Some(secondary_focus);
} }
if context_export {
pending_export = Some(secondary_focus);
}
if secondary_response.clicked() { if secondary_response.clicked() {
pending_focus = Some(secondary_focus); pending_focus = Some(secondary_focus);
} }
@@ -471,6 +619,9 @@ impl TerrainApp {
if let Some(preview) = pending_copy { if let Some(preview) = pending_copy {
self.copy_preview(preview); self.copy_preview(preview);
} }
if let Some(preview) = pending_export {
self.start_export(preview);
}
if let Some(preview) = pending_focus { if let Some(preview) = pending_focus {
self.preview_focus = preview; self.preview_focus = preview;
} }
@@ -483,6 +634,7 @@ impl eframe::App for TerrainApp {
self.hovered_preview = None; self.hovered_preview = None;
self.poll_generation(ctx); self.poll_generation(ctx);
self.poll_copy(ctx); self.poll_copy(ctx);
self.poll_export(ctx);
self.ui_left_panel(ctx); self.ui_left_panel(ctx);
self.ui_right_panel(ctx); self.ui_right_panel(ctx);