mirror of
https://github.com/bevyengine/bevy.git
synced 2026-05-06 06:06:42 -04:00
889d88cf01
# Objective - Demonstrate taking a screenshot through BRP ## Solution - Have the remote integration example take a screenshot through BRP before clicking the button - Depends on #23797 #23798 #23799 #23800 ## Testing - Run the examples, get a screenshot
229 lines
8.1 KiB
Rust
229 lines
8.1 KiB
Rust
//! An integration test that connects to a running Bevy app via the BRP,
|
|
//! finds a button's position, and sends a mouse click to press it.
|
|
//!
|
|
//! Run with the `bevy_remote` feature enabled:
|
|
//! ```bash
|
|
//! cargo run --example integration_test --features="bevy_remote"
|
|
//! ```
|
|
//! This example assumes that the `app_under_test` example is running on the same machine.
|
|
|
|
use std::{any::type_name, io::BufRead};
|
|
|
|
use anyhow::Result as AnyhowResult;
|
|
use bevy::{
|
|
platform::collections::HashMap,
|
|
remote::{
|
|
builtin_methods::{
|
|
BrpObserveParams, BrpQuery, BrpQueryFilter, BrpQueryParams, BrpSpawnEntityParams,
|
|
BrpWriteMessageParams, ComponentSelector, BRP_OBSERVE_METHOD, BRP_QUERY_METHOD,
|
|
BRP_SPAWN_ENTITY_METHOD, BRP_WRITE_MESSAGE_METHOD,
|
|
},
|
|
http::{DEFAULT_ADDR, DEFAULT_PORT},
|
|
BrpRequest,
|
|
},
|
|
render::view::screenshot::{Screenshot, ScreenshotCaptured},
|
|
ui::{widget::Button, UiGlobalTransform},
|
|
window::{Window, WindowEvent},
|
|
};
|
|
|
|
fn main() -> AnyhowResult<()> {
|
|
let url = format!("http://{DEFAULT_ADDR}:{DEFAULT_PORT}/");
|
|
|
|
// Step 1: Take a screenshot via BRP
|
|
// The window must be visible (not fully occluded) for the GPU to render content
|
|
// If the window is hidden, the screenshot will be black
|
|
println!("Spawning Screenshot entity...");
|
|
let spawn_response = brp_request(
|
|
&url,
|
|
BRP_SPAWN_ENTITY_METHOD,
|
|
1,
|
|
&BrpSpawnEntityParams {
|
|
components: HashMap::from([(
|
|
type_name::<Screenshot>().to_string(),
|
|
serde_json::json!({"Window": "Primary"}),
|
|
)]),
|
|
},
|
|
)?;
|
|
let screenshot_entity = &spawn_response["result"]["entity"];
|
|
|
|
println!("Observing ScreenshotCaptured on entity {screenshot_entity}...");
|
|
let observe_response = ureq::post(&url).send_json(BrpRequest {
|
|
method: BRP_OBSERVE_METHOD.to_string(),
|
|
id: Some(serde_json::to_value(2)?),
|
|
params: Some(serde_json::to_value(BrpObserveParams {
|
|
event: type_name::<ScreenshotCaptured>().to_string(),
|
|
entity: Some(serde_json::from_value(screenshot_entity.clone())?),
|
|
})?),
|
|
})?;
|
|
|
|
println!("Waiting for screenshot capture...");
|
|
let reader = std::io::BufReader::new(observe_response.into_body().into_reader());
|
|
for line in reader.lines() {
|
|
let line = line?;
|
|
if let Some(json_str) = line.strip_prefix("data: ") {
|
|
let response: serde_json::Value = serde_json::from_str(json_str)?;
|
|
if let Some(error) = response.get("error") {
|
|
anyhow::bail!("Observe error: {error}");
|
|
}
|
|
if let Some(result) = response.get("result") {
|
|
let events = result.as_array().expect("Expected events array");
|
|
let event = &events[0];
|
|
|
|
let image_data = &event["image"];
|
|
let width = image_data["texture_descriptor"]["size"]["width"]
|
|
.as_u64()
|
|
.unwrap();
|
|
let height = image_data["texture_descriptor"]["size"]["height"]
|
|
.as_u64()
|
|
.unwrap();
|
|
println!("Screenshot captured! Image size: {width}x{height}");
|
|
|
|
let image: bevy::image::Image = serde_json::from_value(image_data.clone())?;
|
|
let dyn_img = image
|
|
.try_into_dynamic()
|
|
.expect("Failed to convert screenshot to dynamic image");
|
|
let path = "screenshot.png";
|
|
dyn_img.to_rgb8().save(path)?;
|
|
println!("Screenshot saved to {path}");
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 2: Find the button entity, and its global transform
|
|
println!("Querying for button entity...");
|
|
let button_query = brp_request(
|
|
&url,
|
|
BRP_QUERY_METHOD,
|
|
3,
|
|
&BrpQueryParams {
|
|
data: BrpQuery {
|
|
components: vec![type_name::<UiGlobalTransform>().to_string()],
|
|
option: ComponentSelector::default(),
|
|
has: Vec::default(),
|
|
},
|
|
strict: false,
|
|
filter: BrpQueryFilter {
|
|
with: vec![type_name::<Button>().to_string()],
|
|
without: Vec::default(),
|
|
},
|
|
},
|
|
)?;
|
|
|
|
let button_result = button_query["result"]
|
|
.as_array()
|
|
.expect("Expected result array");
|
|
let button = &button_result[0];
|
|
|
|
// UiGlobalTransform wraps an Affine2, serialized as a flat array:
|
|
// [_, _, _, _, translation_x, translation_y]
|
|
// The translation gives the node's center in physical pixels.
|
|
let transform = &button["components"][type_name::<UiGlobalTransform>()];
|
|
let transform_arr = transform.as_array().expect("Expected transform array");
|
|
let phys_x = transform_arr[4].as_f64().unwrap();
|
|
let phys_y = transform_arr[5].as_f64().unwrap();
|
|
println!("Found button at physical ({phys_x}, {phys_y})");
|
|
|
|
// Step 3: Find the window entity and scale factor
|
|
println!("Querying for window entity...");
|
|
let window_query = brp_request(
|
|
&url,
|
|
BRP_QUERY_METHOD,
|
|
4,
|
|
&BrpQueryParams {
|
|
data: BrpQuery {
|
|
components: vec![type_name::<Window>().to_string()],
|
|
option: ComponentSelector::default(),
|
|
has: Vec::default(),
|
|
},
|
|
strict: false,
|
|
filter: BrpQueryFilter::default(),
|
|
},
|
|
)?;
|
|
|
|
let window_result = window_query["result"]
|
|
.as_array()
|
|
.expect("Expected result array");
|
|
let window = &window_result[0];
|
|
let window_entity = &window["entity"];
|
|
let window_data = &window["components"][type_name::<Window>()];
|
|
let scale_factor = window_data["resolution"]["scale_factor"].as_f64().unwrap();
|
|
println!("Found window entity: {window_entity}, scale_factor: {scale_factor}");
|
|
|
|
// Step 4: Convert button center from physical to logical pixels
|
|
let logical_x = phys_x / scale_factor;
|
|
let logical_y = phys_y / scale_factor;
|
|
println!("Clicking at logical position: ({logical_x}, {logical_y})");
|
|
|
|
// Step 5: Send CursorMoved via WindowEvent message
|
|
// This lets the picking system know where the pointer is.
|
|
println!("Sending CursorMoved message...");
|
|
brp_request(
|
|
&url,
|
|
BRP_WRITE_MESSAGE_METHOD,
|
|
5,
|
|
&BrpWriteMessageParams {
|
|
message: type_name::<WindowEvent>().to_string(),
|
|
value: Some(serde_json::json!({
|
|
"CursorMoved": {
|
|
"window": window_entity,
|
|
"position": [logical_x, logical_y],
|
|
"delta": null
|
|
}
|
|
})),
|
|
},
|
|
)?;
|
|
|
|
// Step 6: Send MouseButtonInput Pressed + Released via WindowEvent messages.
|
|
// The picking system needs both press and release to generate a Pointer<Click>.
|
|
println!("Sending mouse press...");
|
|
brp_request(
|
|
&url,
|
|
BRP_WRITE_MESSAGE_METHOD,
|
|
6,
|
|
&BrpWriteMessageParams {
|
|
message: type_name::<WindowEvent>().to_string(),
|
|
value: Some(serde_json::json!({
|
|
"MouseButtonInput": {
|
|
"button": "Left",
|
|
"state": "Pressed",
|
|
"window": window_entity,
|
|
}
|
|
})),
|
|
},
|
|
)?;
|
|
|
|
println!("Sending mouse release...");
|
|
brp_request(
|
|
&url,
|
|
BRP_WRITE_MESSAGE_METHOD,
|
|
7,
|
|
&BrpWriteMessageParams {
|
|
message: type_name::<WindowEvent>().to_string(),
|
|
value: Some(serde_json::json!({
|
|
"MouseButtonInput": {
|
|
"button": "Left",
|
|
"state": "Released",
|
|
"window": window_entity,
|
|
}
|
|
})),
|
|
},
|
|
)?;
|
|
|
|
Ok(())
|
|
}
|
|
|
|
fn brp_request(
|
|
url: &str,
|
|
method: &str,
|
|
id: u32,
|
|
params: &impl serde::Serialize,
|
|
) -> AnyhowResult<serde_json::Value> {
|
|
let req = BrpRequest {
|
|
method: method.to_string(),
|
|
id: Some(serde_json::to_value(id)?),
|
|
params: Some(serde_json::to_value(params)?),
|
|
};
|
|
Ok(ureq::post(url).send_json(req)?.body_mut().read_json()?)
|
|
}
|