Camera orientation in degrees: ui.camera and the MCP screenshot's camera

`ui.view` and the MCP `screenshot` tool only turned to the named views, so
a picture sequence could only cut between them. `Orientation` (render) is a
camera orientation in degrees, {yaw, pitch, distance?}, checked as hostile
input: angles finite, distance finite and above 0, yaw wrapped, pitch
clamped to ±90, distance to 0.001–1e9. `modelcraft_engine::view::orientation`
parses it from JSON for both callers:

- control channel `ui.camera {yaw, pitch, distance?}` turns the app's camera;
- MCP `screenshot {camera}` turns it first (bridged through `ui.camera`,
  headless in the CPU render, framed unless a distance is given). `view` and
  `camera` are not given together.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NmAttHhUZTokgPeEJia8sm
This commit is contained in:
jyvikoandClaude Opus 5.5 authored and Irakli Kourtis committed 2026-10-14 00:15:50 -04:00
1 parent d84dae074d
commit 89651b707c
9 files changed
+210 -19

No files matched your search

+23
View File
@@ -184,6 +184,13 @@ pub fn handle(app: &mut ModelApp, ctx: &egui::Context, req: &ControlRequest) ->
}
ok(json!(app.cam))
}
"ui.camera" => match fieldcraft_engine::view::orientation(p) {
Ok(o) => {
app.cam.orient(&o);
ok(json!(app.cam))
}
Err(e) => err(e),
},
"ui.start" => {
let Some(id) = s("command") else { return err("missing `command`") };
match app.start_programmatic(id) {
@@ -427,6 +434,21 @@ mod tests {
assert_eq!(call(&mut app, "ui.view", json!({"view": "top"}))["ok"], true);
assert!((app.cam.pitch - std::f64::consts::FRAC_PI_2).abs() < 1e-3);
assert_eq!(call(&mut app, "ui.view", json!({"view": "sideways"}))["ok"], false);
// Any orientation in degrees; hostile numbers are refused or clamped and leave a valid camera.
let r = call(&mut app, "ui.camera", json!({"yaw": 90, "pitch": 30, "distance": 250}));
assert!((app.cam.yaw - std::f64::consts::FRAC_PI_2).abs() < 1e-9 && (app.cam.pitch - 30f64.to_radians()).abs() < 1e-9, "{r}");
assert_eq!(r["result"]["distance"], 250.0);
for hostile in [
json!({"yaw": 1}),
json!({"yaw": "x", "pitch": 0}),
json!({"yaw": 0, "pitch": 0, "distance": 0}),
json!({"yaw": 0, "pitch": 0, "spin": 1}),
] {
assert_eq!(call(&mut app, "ui.camera", hostile.clone())["ok"], false, "{hostile}");
}
assert_eq!(app.cam.distance, 250.0);
assert_eq!(call(&mut app, "ui.camera", json!({"yaw": -1e12, "pitch": 1e300, "distance": 1e300}))["ok"], true);
assert!(app.cam.is_valid() && app.cam.pitch < std::f64::consts::FRAC_PI_2);
assert_eq!(call(&mut app, "ui.inspect", json!({}))["result"]["model"]["elements"], 64 + 162);
assert_eq!(call(&mut app, "ui.screenshot", json!({})), json!({"screenshot": true}));
assert_eq!(call(&mut app, "no.method", json!({}))["ok"], false);
@@ -495,6 +517,7 @@ mod tests {
"engine.script",
"ui.set",
"ui.view",
"ui.camera",
"ui.start",
"ui.dialog",
"ui.move",