1use crate::automation::command::{parse_args, schema_of, Annotations, CommandSpec, Ctx, Handler, Outcome, Phase};
6use serde::{Deserialize, Serialize};
7use serde_json::{json, Value};
8
9#[derive(Debug, Clone, Deserialize, schemars::JsonSchema)]
11#[serde(untagged)]
12pub enum Region {
13 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 pub width: u32,
35 pub height: u32,
36 pub ppp: f32,
37 pub region: [f32; 4],
39 pub png_bytes: usize,
40}
41
42pub 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
61pub 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}