forked from 1b-agent/fieldcraft-upstream
cli, mcp: snapshot --section and --field; headless MCP screenshots on the GPU
--section x|y|z[=VALUE] hides the elements on the camera's side of a plane (at the mesh's middle by default) so the element faces inside show; --field contours a nodal field of the solved step (results.contour). The MCP server's headless screenshot draws with the viewport's pipeline on the shared device and its reply, like snapshot's, names the renderer. Refs #422 Co-Authored-By: Claude Opus 5.5 <[email protected]> Claude-Session: https://claude.ai/code/session_01UhhZMBTviDQxZvwnyP89fo
This commit is contained in:
1 parent
c9163e98ad
commit
390ddcd6cc
10 files changed
+208
-73
No files matched your search
+48
-15
@@ -9,11 +9,48 @@ use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::time::Duration;
|
||||
|
||||
use fieldcraft_engine::Session;
|
||||
use fieldcraft_engine::view::{Palette, render_png};
|
||||
use fieldcraft_engine::view::{Palette, Shown};
|
||||
use fieldcraft_gpu::Renderer;
|
||||
use fieldcraft_io::roots::Roots;
|
||||
use fieldcraft_render::{Orientation, StandardView};
|
||||
use serde_json::{Value, json};
|
||||
|
||||
/// A framed picture as PNG: the viewport's GPU pipeline on the process's shared device, or the
|
||||
/// CPU rasterizer (`FIELDCRAFT_GPU`).
|
||||
#[cfg(not(target_arch = "wasm32"))]
|
||||
fn draw(f: &fieldcraft_engine::view::Framed, w: u32, h: u32) -> Result<(Vec<u8>, Renderer), String> {
|
||||
use fieldcraft_gpu::capture;
|
||||
let policy = fieldcraft_gpu::select::Policy::from_env().map_err(|e| e.to_string())?;
|
||||
capture::png(capture::shared(), policy, &f.scene, &[&f.symbols], &f.cam, w, h).map_err(|e| e.to_string())
|
||||
}
|
||||
|
||||
/// A framed picture as PNG, drawn by the CPU rasterizer (the browser has no headless device).
|
||||
#[cfg(target_arch = "wasm32")]
|
||||
fn draw(f: &fieldcraft_engine::view::Framed, w: u32, h: u32) -> Result<(Vec<u8>, Renderer), String> {
|
||||
let png = fieldcraft_render::render_layers_png(&f.scene, &[&f.symbols], &f.cam, w as usize, h as usize).map_err(|e| format!("render: {e}"))?;
|
||||
Ok((png, Renderer::Cpu { why: None }))
|
||||
}
|
||||
|
||||
/// What drew a picture and what it shows, as JSON: `renderer` (`kind` hardware, software or cpu;
|
||||
/// the adapter, or why the CPU drew it), `results` (step, scale, contour), `undrawn_bodies` and
|
||||
/// `sampled_symbols`. The MCP `screenshot` reply and `fieldcraft-cli snapshot` print it.
|
||||
pub fn picture_report(r: &Renderer, shown: &Shown) -> Value {
|
||||
let renderer = match r {
|
||||
Renderer::Gpu { adapter, samples, .. } => json!({"kind": r.kind(), "adapter": adapter, "msaa": samples}),
|
||||
Renderer::Cpu { why } => json!({"kind": r.kind(), "why": why}),
|
||||
};
|
||||
let contour = shown
|
||||
.contour
|
||||
.as_ref()
|
||||
.map(|(field, min, max)| json!({"field": field, "min": {"node": min.0, "value": min.1}, "max": {"node": max.0, "value": max.1}}));
|
||||
json!({
|
||||
"renderer": renderer,
|
||||
"results": shown.step.as_ref().map(|step| json!({"step": step, "scale": shown.scale, "contour": contour})),
|
||||
"undrawn_bodies": shown.failed,
|
||||
"sampled_symbols": shown.sampled,
|
||||
})
|
||||
}
|
||||
|
||||
/// What a picture shows.
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub struct PictureRequest {
|
||||
@@ -110,19 +147,15 @@ impl Backend for Headless {
|
||||
}
|
||||
let w = r.width.unwrap_or(DEFAULT_SIZE.0).clamp(16, MAX_SIDE);
|
||||
let h = r.height.unwrap_or(DEFAULT_SIZE.1).clamp(16, MAX_SIDE);
|
||||
let (png, shown) = render_png(&self.session, &self.view, w as usize, h as usize, &Palette::LIGHT).map_err(|e| e.to_string())?;
|
||||
let contour = shown
|
||||
.contour
|
||||
.map(|(field, min, max)| json!({"field": field, "min": {"node": min.0, "value": min.1}, "max": {"node": max.0, "value": max.1}}));
|
||||
let info = json!({
|
||||
"source": "model",
|
||||
"view": self.view,
|
||||
"width": w,
|
||||
"height": h,
|
||||
"results": shown.step.map(|step| json!({"step": step, "scale": shown.scale, "contour": contour})),
|
||||
"undrawn_bodies": shown.failed,
|
||||
"sampled_symbols": shown.sampled,
|
||||
});
|
||||
let f = fieldcraft_engine::view::frame(&self.session, &self.view, w as usize, h as usize, &Palette::LIGHT, None);
|
||||
let (png, renderer) = draw(&f, w, h)?;
|
||||
let mut info = picture_report(&renderer, &f.shown);
|
||||
if let Some(o) = info.as_object_mut() {
|
||||
o.insert("source".into(), "model".into());
|
||||
o.insert("view".into(), json!(self.view));
|
||||
o.insert("width".into(), w.into());
|
||||
o.insert("height".into(), h.into());
|
||||
}
|
||||
Ok(Picture { png, info })
|
||||
}
|
||||
|
||||
@@ -135,7 +168,7 @@ impl Backend for Headless {
|
||||
}
|
||||
|
||||
fn describe(&self) -> String {
|
||||
"a headless in-process FieldCraft session (no window; screenshots are CPU renders)".into()
|
||||
"a headless in-process FieldCraft session (no window; screenshots drawn offscreen with the viewport's GPU pipeline, or on the CPU without a GPU)".into()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ mod backend;
|
||||
mod server;
|
||||
pub mod tools;
|
||||
|
||||
pub use backend::{Backend, Headless, Picture, PictureRequest, Remote};
|
||||
pub use backend::{Backend, Headless, Picture, PictureRequest, Remote, picture_report};
|
||||
pub use server::{MAX_BATCH_MESSAGES, MAX_LINE, PROTOCOL_VERSION, SUPPORTED_VERSIONS, Server};
|
||||
|
||||
use fieldcraft_io::roots::Roots;
|
||||
|
||||
@@ -220,6 +220,8 @@ fn screenshot_takes_a_camera_orientation() {
|
||||
let (turned, info) = picture(&mut s, json!({"camera": {"yaw": 30, "pitch": 1e9, "distance": 1e300}, "width": 64, "height": 48}));
|
||||
assert_ne!(iso, turned);
|
||||
assert_eq!(info["view"], json!({"yaw": 30.0, "pitch": 90.0, "distance": 1e9}), "{info}");
|
||||
// The reply names what drew it: the viewport's pipeline on a GPU, or the CPU rasterizer.
|
||||
assert!(matches!(info["renderer"]["kind"].as_str(), Some("hardware" | "software" | "cpu")), "{info}");
|
||||
let (again, _) = picture(&mut s, json!({"width": 64, "height": 48}));
|
||||
assert_eq!(again, turned);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user