Skip to main content

brep_app/automation/
cmd_capture.rs

1//! Screen capture through egui's own screenshot path: `ViewportCommand::Screenshot`
2//! in the mutate phase, completed by the matching `Event::Screenshot` in a later
3//! frame's input phase (§5). The headless host renders directly instead and
4//! answers this command itself; the window and dial-in hosts go through here.
5use crate::automation::command::{parse_args, schema_of, Annotations, CommandSpec, Ctx, Handler, Outcome, Phase};
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8
9/// What to capture: the whole surface, the 3D viewport, or a rect in points.
10#[derive(Debug, Clone, Deserialize, schemars::JsonSchema)]
11#[serde(untagged)]
12pub enum Region {
13    /// `"full"` or `"viewport"`
14    Named(String),
15    Rect { x: f32, y: f32, w: f32, h: f32 },
16}
17
18impl Default for Region {
19    fn default() -> Self {
20        Region::Named("full".into())
21    }
22}
23
24#[derive(Deserialize, schemars::JsonSchema)]
25#[serde(deny_unknown_fields)]
26pub struct ScreenshotArgs {
27    #[serde(default)]
28    pub region: Region,
29}
30
31#[derive(Serialize, schemars::JsonSchema)]
32pub struct ScreenshotInfo {
33    /// Pixel size of the PNG carried beside this reply.
34    pub width: u32,
35    pub height: u32,
36    pub ppp: f32,
37    /// The captured rect in egui points.
38    pub region: [f32; 4],
39    pub png_bytes: usize,
40}
41
42/// Resolve a region to a pixel rect on a `w`×`h` image at `ppp`.
43pub fn region_px(region: &Region, ppp: f32, view: Option<egui::Rect>, w: u32, h: u32) -> Result<(u32, u32, u32, u32, [f32; 4]), String> {
44    let pts = match region {
45        Region::Named(n) if n == "full" => [0.0, 0.0, w as f32 / ppp, h as f32 / ppp],
46        Region::Named(n) if n == "viewport" => {
47            let r = view.ok_or("no viewport rect: the 3D view was not drawn last frame")?;
48            [r.min.x, r.min.y, r.width(), r.height()]
49        }
50        Region::Named(n) => return Err(format!("unknown region `{n}` (full | viewport | {{x,y,w,h}})")),
51        Region::Rect { x, y, w, h } => [*x, *y, *w, *h],
52    };
53    let px = |v: f32| (v * ppp).round().max(0.0) as u32;
54    let x0 = px(pts[0]).min(w.saturating_sub(1));
55    let y0 = px(pts[1]).min(h.saturating_sub(1));
56    let cw = px(pts[2]).clamp(1, w - x0);
57    let ch = px(pts[3]).clamp(1, h - y0);
58    Ok((x0, y0, cw, ch, pts))
59}
60
61/// Encode a captured frame (cropped to `region`) as PNG.
62pub fn encode_capture(image: &egui::ColorImage, ppp: f32, view: Option<egui::Rect>, region: &Region) -> Result<(Value, Vec<u8>), String> {
63    let (w, h) = (image.width() as u32, image.height() as u32);
64    let (x0, y0, cw, ch, pts) = region_px(region, ppp, view, w, h)?;
65    let full = image::RgbaImage::from_raw(w, h, image.as_raw().to_vec()).ok_or("frame buffer size mismatch")?;
66    let cropped = image::imageops::crop_imm(&full, x0, y0, cw, ch).to_image();
67    let mut out = std::io::Cursor::new(Vec::new());
68    cropped.write_to(&mut out, image::ImageFormat::Png).map_err(|e| format!("png encode: {e}"))?;
69    let png = out.into_inner();
70    let info = ScreenshotInfo { width: cw, height: ch, ppp, region: pts, png_bytes: png.len() };
71    Ok((serde_json::to_value(info).map_err(|e| e.to_string())?, png))
72}
73
74fn screenshot(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
75    let a: ScreenshotArgs = parse_args(args)?;
76    let token = ctx.app.automation.next_token();
77    ctx.egui.send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new(token)));
78    Ok(Outcome::AwaitScreenshot { token, region: a.region })
79}
80
81pub static COMMANDS: &[CommandSpec] = &[
82    CommandSpec { name: "screenshot", group: "capture", doc: "Capture the composited frame (egui chrome and the 3D view) as PNG: region `full` (default), `viewport`, or `{x,y,w,h}` in egui points. The PNG rides beside the JSON reply.", phase: Phase::Mutate, annotations: Annotations::READ, args_schema: schema_of::<ScreenshotArgs>, result_schema: schema_of::<ScreenshotInfo>, handler: Handler::App(screenshot) },
83];
84
85#[allow(dead_code)]
86fn _unused(_: Value) -> Value {
87    json!({})
88}