diff --git a/lightningbeam-ui/lightningbeam-core/src/export.rs b/lightningbeam-ui/lightningbeam-core/src/export.rs index 21b9d46..6e62d50 100644 --- a/lightningbeam-ui/lightningbeam-core/src/export.rs +++ b/lightningbeam-ui/lightningbeam-core/src/export.rs @@ -524,11 +524,14 @@ pub struct ImageExportSettings { /// When false, the image is composited onto an opaque background before encoding. /// Only meaningful for formats that support alpha (PNG, WebP). pub allow_transparency: bool, + /// How the document is fit into the output frame when aspect ratios differ (default Letterbox). + #[serde(default)] + pub fit: ExportFitMode, } impl Default for ImageExportSettings { fn default() -> Self { - Self { format: ImageFormat::Png, time: 0.0, width: None, height: None, quality: 90, allow_transparency: false } + Self { format: ImageFormat::Png, time: 0.0, width: None, height: None, quality: 90, allow_transparency: false, fit: ExportFitMode::Letterbox } } } diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs index 65e4f2b..ec716d7 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/dialog.rs @@ -378,6 +378,19 @@ impl ExportDialog { if changed_h { self.image_settings.height = if h == 0 { None } else { Some(h) }; } ui.weak("(0 = document size)"); }); + + // Fit mode — how the document maps into the output frame when aspect ratios differ. + ui.horizontal(|ui| { + use lightningbeam_core::export::ExportFitMode; + ui.label("Fit:"); + egui::ComboBox::from_id_salt("image_fit_mode") + .selected_text(self.image_settings.fit.name()) + .show_ui(ui, |ui| { + ui.selectable_value(&mut self.image_settings.fit, ExportFitMode::Letterbox, ExportFitMode::Letterbox.name()); + ui.selectable_value(&mut self.image_settings.fit, ExportFitMode::Crop, ExportFitMode::Crop.name()); + ui.selectable_value(&mut self.image_settings.fit, ExportFitMode::Stretch, ExportFitMode::Stretch.name()); + }); + }); } /// Render advanced audio settings (sample rate, channels, bit depth, bitrate, time range) diff --git a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs index 9c62936..7b7a7fc 100644 --- a/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs +++ b/lightningbeam-ui/lightningbeam-editor/src/export/mod.rs @@ -598,6 +598,7 @@ impl ExportOrchestrator { // ── First call: render the frame to the GPU output texture ──────── let w = state.width; let h = state.height; + let fit = state.settings.fit; if state.gpu_resources.is_none() { state.gpu_resources = Some(video_exporter::ExportGpuResources::new(device, w, h)); @@ -632,8 +633,7 @@ impl ExportOrchestrator { state.settings.allow_transparency, raster_store, true, // image export composites on the shared device - // Image export renders at the document's own size, so the fit transform is identity. - lightningbeam_core::export::ExportFitMode::Letterbox, + fit, )?; queue.submit(Some(encoder.finish()));