use crate::automation::command::{parse_args, schema_of, Annotations, CommandSpec, Ctx, Handler, Outcome, Phase};
use serde::{Deserialize, Serialize};
use serde_json::{json, Value};
#[derive(Debug, Clone, Deserialize, schemars::JsonSchema)]
#[serde(untagged)]
pub enum Region {
Named(String),
Rect { x: f32, y: f32, w: f32, h: f32 },
}
impl Default for Region {
fn default() -> Self {
Region::Named("full".into())
}
}
#[derive(Deserialize, schemars::JsonSchema)]
#[serde(deny_unknown_fields)]
pub struct ScreenshotArgs {
#[serde(default)]
pub region: Region,
}
#[derive(Serialize, schemars::JsonSchema)]
pub struct ScreenshotInfo {
pub width: u32,
pub height: u32,
pub ppp: f32,
pub region: [f32; 4],
pub png_bytes: usize,
}
pub fn region_px(region: &Region, ppp: f32, view: Option<egui::Rect>, w: u32, h: u32) -> Result<(u32, u32, u32, u32, [f32; 4]), String> {
let pts = match region {
Region::Named(n) if n == "full" => [0.0, 0.0, w as f32 / ppp, h as f32 / ppp],
Region::Named(n) if n == "viewport" => {
let r = view.ok_or("no viewport rect: the 3D view was not drawn last frame")?;
[r.min.x, r.min.y, r.width(), r.height()]
}
Region::Named(n) => return Err(format!("unknown region `{n}` (full | viewport | {{x,y,w,h}})")),
Region::Rect { x, y, w, h } => [*x, *y, *w, *h],
};
let px = |v: f32| (v * ppp).round().max(0.0) as u32;
let x0 = px(pts[0]).min(w.saturating_sub(1));
let y0 = px(pts[1]).min(h.saturating_sub(1));
let cw = px(pts[2]).clamp(1, w - x0);
let ch = px(pts[3]).clamp(1, h - y0);
Ok((x0, y0, cw, ch, pts))
}
pub fn encode_capture(image: &egui::ColorImage, ppp: f32, view: Option<egui::Rect>, region: &Region) -> Result<(Value, Vec<u8>), String> {
let (w, h) = (image.width() as u32, image.height() as u32);
let (x0, y0, cw, ch, pts) = region_px(region, ppp, view, w, h)?;
let full = image::RgbaImage::from_raw(w, h, image.as_raw().to_vec()).ok_or("frame buffer size mismatch")?;
let cropped = image::imageops::crop_imm(&full, x0, y0, cw, ch).to_image();
let mut out = std::io::Cursor::new(Vec::new());
cropped.write_to(&mut out, image::ImageFormat::Png).map_err(|e| format!("png encode: {e}"))?;
let png = out.into_inner();
let info = ScreenshotInfo { width: cw, height: ch, ppp, region: pts, png_bytes: png.len() };
Ok((serde_json::to_value(info).map_err(|e| e.to_string())?, png))
}
fn screenshot(ctx: &mut Ctx<'_>, args: Value) -> Result<Outcome, String> {
let a: ScreenshotArgs = parse_args(args)?;
let token = ctx.app.automation.next_token();
ctx.egui.send_viewport_cmd(egui::ViewportCommand::Screenshot(egui::UserData::new(token)));
Ok(Outcome::AwaitScreenshot { token, region: a.region })
}
pub static COMMANDS: &[CommandSpec] = &[
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) },
];
#[allow(dead_code)]
fn _unused(_: Value) -> Value {
json!({})
}