From c5045c23dc98ae2c6555a7ec29021e1a07d8208c Mon Sep 17 00:00:00 2001 From: NehharShah Date: Wed, 12 Nov 2025 10:24:49 -0500 Subject: [PATCH] feat: Add desktop screenshot export with Ctrl/Cmd+P shortcut --- Cargo.lock | 5 +- Cargo.toml | 3 + src/bevy_app/mod.rs | 7 +++ src/bevy_app/systems/mod.rs | 4 ++ src/bevy_app/systems/screenshot.rs | 99 ++++++++++++++++++++++++++++++ 5 files changed, 116 insertions(+), 2 deletions(-) create mode 100644 src/bevy_app/systems/screenshot.rs diff --git a/Cargo.lock b/Cargo.lock index 6c047d2..b9b8254 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -586,13 +586,14 @@ checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] name = "bert" -version = "0.2.2" +version = "0.2.3" dependencies = [ "bevy", "bevy-inspector-egui", "bevy_file_dialog", "bevy_picking", "bevy_prototype_lyon", + "chrono", "console_error_panic_hook", "enum-iterator", "gloo-file", @@ -619,7 +620,7 @@ dependencies = [ [[package]] name = "bert-tauri" -version = "0.2.2" +version = "0.2.3" dependencies = [ "leptos", "serde", diff --git a/Cargo.toml b/Cargo.toml index 0f0c03f..50eac01 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,9 @@ bevy-inspector-egui = "0.29.0" bevy_picking = { version = "0.15", features = ["bevy_mesh_picking_backend"] } bevy_prototype_lyon = "0.13.0" console_error_panic_hook = "0.1.7" + +[target.'cfg(not(target_arch = "wasm32"))'.dependencies] +chrono = "0.4" enum-iterator = "2.1.0" gloo-file = "0.3" js-sys = "0.3" diff --git a/src/bevy_app/mod.rs b/src/bevy_app/mod.rs index f036ed5..750f5a1 100644 --- a/src/bevy_app/mod.rs +++ b/src/bevy_app/mod.rs @@ -193,6 +193,13 @@ pub fn init_bevy_app( .in_set(AllSet), ); + // Screenshot system - Ctrl/Cmd+P (desktop only, doesn't conflict with existing shortcuts) + #[cfg(not(target_arch = "wasm32"))] + app.add_systems( + Update, + take_screenshot.run_if(input_pressed(MODIFIER).and(input_just_pressed(KeyCode::KeyP))), + ); + app.add_systems( Update, ( diff --git a/src/bevy_app/systems/mod.rs b/src/bevy_app/systems/mod.rs index 0851834..0be7b5a 100644 --- a/src/bevy_app/systems/mod.rs +++ b/src/bevy_app/systems/mod.rs @@ -111,6 +111,8 @@ mod camera; mod removal; +#[cfg(not(target_arch = "wasm32"))] +mod screenshot; mod setup; mod spatial_sync; mod subsystem; @@ -120,6 +122,8 @@ mod ui; use bevy::ecs::system::{RunSystemOnce, SystemState}; pub use camera::*; pub use removal::*; +#[cfg(not(target_arch = "wasm32"))] +pub use screenshot::*; pub use setup::*; pub use spatial_sync::*; pub use subsystem::*; diff --git a/src/bevy_app/systems/screenshot.rs b/src/bevy_app/systems/screenshot.rs new file mode 100644 index 0000000..2eb5ef5 --- /dev/null +++ b/src/bevy_app/systems/screenshot.rs @@ -0,0 +1,99 @@ +//! Screenshot capture and export functionality for BERT diagrams. +//! +//! This module provides systems for capturing screenshots of the BERT canvas +//! and saving them to disk with timestamped filenames. Screenshots are only +//! available on desktop builds (non-wasm). +//! +//! ## Key Features +//! +//! - **Desktop Only**: Gated behind `#[cfg(not(target_arch = "wasm32"))]` +//! - **Timestamped Files**: Automatic filename generation with date/time +//! - **Observer Pattern**: Uses Bevy 0.15's observer-based screenshot API +//! - **Event Integration**: Uses `SaveSuccessEvent` for user feedback +//! +//! ## Architecture +//! +//! The screenshot process uses Bevy 0.15's observer pattern: +//! +//! 1. **Request Phase**: User triggers screenshot via keyboard shortcut +//! 2. **Capture Phase**: Bevy captures screenshot and triggers observer +//! 3. **Save Phase**: Observer saves image to disk with timestamp +//! +//! This ensures the frame is fully rendered before capture, avoiding the +//! blank screenshot issue from previous implementations. + +#[cfg(not(target_arch = "wasm32"))] +use bevy::{ + prelude::*, + render::view::screenshot::{save_to_disk, Screenshot, ScreenshotCaptured}, +}; + +#[cfg(not(target_arch = "wasm32"))] +use crate::events::SaveSuccessEvent; + +/// Component to track the screenshot filename on the screenshot entity +#[cfg(not(target_arch = "wasm32"))] +#[derive(Component)] +struct ScreenshotPath(String); + +/// System that initiates screenshot capture when triggered. +/// +/// This system spawns a Screenshot component targeting the primary window +/// with an observer that saves the screenshot to disk when ready. +/// +/// # Process +/// +/// 1. Generate timestamped filename +/// 2. Spawn Screenshot entity with observer and path component +/// 3. Observer saves screenshot when capture completes +/// 4. Send success event for toast notification +/// +/// # Note +/// +/// This system uses Bevy 0.15's observer pattern which automatically handles +/// the screenshot lifecycle, including entity cleanup after capture. +#[cfg(not(target_arch = "wasm32"))] +pub fn take_screenshot(mut commands: Commands) { + let timestamp = chrono::Local::now().format("%Y-%m-%d_%H-%M-%S"); + let path = format!("bert_screenshot_{}.png", timestamp); + + info!("Initiating screenshot capture: {}", path); + + // Spawn screenshot entity with save observer and path component + commands + .spawn((Screenshot::primary_window(), ScreenshotPath(path.clone()))) + .observe(save_to_disk(path.clone())) + .observe(screenshot_saved_handler); + + info!("Screenshot scheduled for: {}", path); +} + +/// Observer function that handles screenshot completion and sends success events. +/// +/// This observer is triggered after the screenshot is captured and saved to disk. +/// It sends a `SaveSuccessEvent` to notify the user via toast notification. +/// +/// # Parameters +/// +/// - `trigger`: Contains the captured screenshot event with image data +/// - `path_query`: Query to get the screenshot path from the entity +/// - `save_events`: Event writer for sending success notifications +#[cfg(not(target_arch = "wasm32"))] +fn screenshot_saved_handler( + trigger: Trigger, + path_query: Query<&ScreenshotPath>, + mut save_events: EventWriter, +) { + let entity = trigger.entity(); + if let Ok(screenshot_path) = path_query.get(entity) { + let path = &screenshot_path.0; + info!("Screenshot saved successfully: {}", path); + + save_events.send(SaveSuccessEvent { + file_path: Some(path.clone()), + message: format!("Screenshot saved: {}", path), + }); + } +} + +