diff --git a/src/exporter.rs b/src/exporter.rs index 510f008..d3ec929 100644 --- a/src/exporter.rs +++ b/src/exporter.rs @@ -2,7 +2,7 @@ use std::collections::HashSet; use std::fmt::Write as _; use std::path::PathBuf; -use image::{DynamicImage, ImageFormat, RgbaImage}; +use image::{DynamicImage, ImageFormat, RgbaImage, imageops::FilterType}; use rfd::FileDialog; use tiny_skia::{FillRule, Paint, PathBuilder, Pixmap, Rect, Stroke, StrokeDash, Transform}; @@ -105,9 +105,7 @@ pub fn export_with_dialog( } ExportFormat::Png | ExportFormat::Jpeg | ExportFormat::Webp => { let pixmap = render_pixmap(layout, settings)?; - let rgba = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec()) - .ok_or_else(|| "Failed to build image buffer".to_string())?; - let img = DynamicImage::ImageRgba8(rgba); + let img = raster_image_from_pixmap(&pixmap, settings)?; let format = match settings.export_format { ExportFormat::Png => ImageFormat::Png, ExportFormat::Jpeg => ImageFormat::Jpeg, @@ -216,6 +214,44 @@ fn render_pixmap(layout: &DungeonLayout, settings: &UiSettings) -> Result Result { + let rgba = RgbaImage::from_raw(pixmap.width(), pixmap.height(), pixmap.data().to_vec()) + .ok_or_else(|| "Failed to build image buffer".to_string())?; + let mut img = DynamicImage::ImageRgba8(rgba); + + let target = raster_target_size(settings); + if img.width() != target.0 || img.height() != target.1 { + img = img.resize_exact(target.0, target.1, FilterType::Lanczos3); + } + + Ok(img) +} + +fn raster_target_size(settings: &UiSettings) -> (u32, u32) { + let mut width = settings.export_width.clamp(1, 10_000); + let mut height = settings.export_height.clamp(1, 10_000); + + if !settings.allow_export_aspect_change { + let cols = settings.cols.max(1) as f32; + let rows = settings.rows.max(1) as f32; + let ratio = cols / rows; + height = ((width as f32) / ratio).round() as u32; + height = height.clamp(1, 10_000); + } + + if width == 0 { + width = 1; + } + if height == 0 { + height = 1; + } + + (width, height) +} + fn build_svg(layout: &DungeonLayout, settings: &UiSettings) -> String { let g = ExportGeometry::new(settings.cols, settings.rows); let wall_w = (g.cell / 5.0).max(1.0); diff --git a/src/ui.rs b/src/ui.rs index 46fd230..5b22b7a 100644 --- a/src/ui.rs +++ b/src/ui.rs @@ -47,6 +47,10 @@ impl ExportFormat { ExportFormat::Svg => ".svg", } } + + pub fn is_raster(self) -> bool { + !matches!(self, ExportFormat::Svg) + } } #[derive(Debug, Clone, Serialize, Deserialize)] @@ -67,6 +71,9 @@ pub struct UiSettings { pub allow_middle_corridor_doors: bool, pub colorblind_mode: bool, pub export_format: ExportFormat, + pub export_width: u32, + pub export_height: u32, + pub allow_export_aspect_change: bool, active_tab: Tab, } @@ -88,6 +95,9 @@ impl Default for UiSettings { allow_middle_corridor_doors: false, colorblind_mode: false, export_format: ExportFormat::Png, + export_width: 1920, + export_height: 1080, + allow_export_aspect_change: false, active_tab: Tab::Generate, } } @@ -204,6 +214,63 @@ fn draw_generate_tab(ui: &mut egui::Ui, settings: &mut UiSettings, result: &mut }); }); + if settings.export_format.is_raster() { + ui.add_space(8.0); + ui.checkbox( + &mut settings.allow_export_aspect_change, + "Allow aspect ratio change", + ); + + let mut width_changed = false; + let mut height_changed = false; + + ui.add_space(6.0); + ui.label("Resolution Width"); + ui.horizontal(|ui| { + width_changed |= ui + .add(egui::Slider::new(&mut settings.export_width, 1..=10_000).show_value(false)) + .changed(); + width_changed |= ui + .add( + egui::DragValue::new(&mut settings.export_width) + .speed(1.0) + .range(1..=10_000), + ) + .changed(); + }); + + ui.add_space(6.0); + ui.label("Resolution Height"); + ui.horizontal(|ui| { + height_changed |= ui + .add( + egui::Slider::new(&mut settings.export_height, 1..=10_000).show_value(false), + ) + .changed(); + height_changed |= ui + .add( + egui::DragValue::new(&mut settings.export_height) + .speed(1.0) + .range(1..=10_000), + ) + .changed(); + }); + + if !settings.allow_export_aspect_change { + let cols = settings.cols.max(1) as f32; + let rows = settings.rows.max(1) as f32; + let ratio = cols / rows; + + if width_changed { + let h = ((settings.export_width as f32) / ratio).round() as u32; + settings.export_height = h.clamp(1, 10_000); + } else if height_changed { + let w = ((settings.export_height as f32) * ratio).round() as u32; + settings.export_width = w.clamp(1, 10_000); + } + } + } + ui.add_space(6.0); if ui.button("Export Image").clicked() { result.export_clicked = true;