Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand Down
7 changes: 7 additions & 0 deletions src/bevy_app/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
(
Expand Down
4 changes: 4 additions & 0 deletions src/bevy_app/systems/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,8 @@

mod camera;
mod removal;
#[cfg(not(target_arch = "wasm32"))]
mod screenshot;
mod setup;
mod spatial_sync;
mod subsystem;
Expand All @@ -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::*;
Expand Down
99 changes: 99 additions & 0 deletions src/bevy_app/systems/screenshot.rs
Original file line number Diff line number Diff line change
@@ -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<ScreenshotCaptured>,
path_query: Query<&ScreenshotPath>,
mut save_events: EventWriter<SaveSuccessEvent>,
) {
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),
});
}
}