Skip to content

Add multi-angle coverage and camera control for editor_screenshot#6

Merged
dsarno merged 8 commits into
mainfrom
screenshot-coverage-angles
Apr 14, 2026
Merged

Add multi-angle coverage and camera control for editor_screenshot#6
dsarno merged 8 commits into
mainfrom
screenshot-coverage-angles

Conversation

@dsarno

@dsarno dsarno commented Apr 14, 2026

Copy link
Copy Markdown
Contributor

Summary

  • coverage=true returns geometry-informed reference shots (establishing + orthographic top-down) plus AABB metadata so the AI can plan follow-up shots like a photographer
  • elevation/azimuth/fov parameters give the AI explicit camera control for closeups, back views, and telephoto details — iterate until coverage is sufficient (up to ~15 shots)
  • AABB metadata (center, size, longest ground axis) included in ALL view_target responses, not just coverage
  • Orthographic top-down uses RenderingServer.camera_set_orthogonal for a true plan view
  • Establishing angle auto-computed from target bounding box to face the longest horizontal dimension
  • SnowGroup node added to test scene for group-target testing

Still to polish

  • Tuning of establishing shot padding/FOV for different subject scales
  • Evaluate whether the AI actually iterates well in practice (prompt/docstring guidance may need refinement)
  • Consider adding a shots batch parameter to reduce round trips for multi-shot workflows

Test plan

  • ruff check — lint clean
  • pytest -v — 267 Python tests pass
  • run_tests suite=editor — 25 GDScript tests pass
  • Live: coverage=true returns 2 reference shots + AABB metadata
  • Live: elevation/azimuth/fov produce targeted follow-up shots
  • Live: iterative workflow (coverage → back view → individual closeups) works end-to-end
  • Live: regression — default view_target framing unchanged
  • Live: coverage=true without view_target silently ignored

🤖 Generated with Claude Code

The AI can now direct the editor camera like a photographer:

- coverage=true returns two reference shots (establishing perspective +
  orthographic top-down) plus AABB geometry metadata (center, size,
  longest ground axis). The establishing angle is computed from the
  target bounding box to face the longest horizontal dimension.

- elevation/azimuth/fov parameters give the AI explicit camera control
  for targeted follow-up shots: closeups, back views, telephoto details.
  The AI iterates until it has the coverage it needs (up to ~15 shots).

- AABB metadata is included in ALL view_target responses (not just
  coverage), so the AI always has geometry info for planning shots.

- Orthographic top-down uses RenderingServer.camera_set_orthogonal for
  a true plan view. Camera transform and projection are saved/restored
  after each shot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Required for the screenshot coverage PR to compile and pass CI:

- connection.gd + transport/websocket.py: increase WebSocket buffer to
  4 MB for large screenshot base64 payloads (default 64 KB was too small)
- plugin.gd: register take_screenshot, clear_logs, get_performance_monitors
  dispatchers; pass Connection to EditorHandler
- log_buffer.gd: add clear() method for clear_logs handler
- project_handler.gd + handlers/project.py + tools/project.py: run_project
  and stop_project tools
- Rename 'MCP Studio' to 'Godot AI' in doc comments
- test_project.gd: new run_project validation tests
- test_scene.gd: relax MeshInstance3D count assertion (scene has more now)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends the editor_screenshot MCP tool to support multi-angle “coverage” screenshots and explicit camera controls (elevation/azimuth/fov), returning geometry (AABB) metadata to enable iterative shot planning workflows across the Python server and Godot plugin.

Changes:

  • Add editor_screenshot MCP tool + shared handler support for coverage, view_target, and camera parameters.
  • Implement plugin-side screenshot framing (including orthographic top-down) and AABB metadata emission for view_target screenshots.
  • Expand unit/integration + Godot-side tests to cover screenshots, log clearing, project run/stop, and performance monitor retrieval.

Reviewed changes

Copilot reviewed 19 out of 19 changed files in this pull request and generated 5 comments.

Show a summary per file
File Description
src/godot_ai/tools/editor.py Adds the editor_screenshot MCP tool and documents new parameters/workflow.
src/godot_ai/handlers/editor.py Implements Python-side response shaping (single vs. coverage multi-image) and MCP content blocks.
plugin/addons/godot_ai/handlers/editor_handler.gd Adds screenshot framing logic, coverage presets, orthographic top-down, AABB metadata, monitors mapping, and log clearing.
tests/unit/test_runtime_handlers.py Extends StubClient to emulate take_screenshot responses and adds unit tests for new handlers.
tests/integration/test_mcp_tools.py Adds integration coverage for logs_clear, project_run/stop, and editor_screenshot tool round-trips.
test_project/tests/test_editor.gd Adds editor-side GDScript tests for screenshot behavior (coverage, view_target, angles, fov) and logs clear.
test_project/main.tscn Updates test scene content/groups for view_target/group-target testing.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment thread test_project/main.tscn Outdated
Comment thread plugin/addons/godot_ai/handlers/editor_handler.gd Outdated
Comment thread tests/unit/test_runtime_handlers.py Outdated
Comment thread src/godot_ai/tools/editor.py Outdated
Comment thread plugin/addons/godot_ai/handlers/editor_handler.gd Outdated
@codecov

codecov Bot commented Apr 14, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.

📢 Thoughts on this report? Let us know!

dsarno and others added 5 commits April 14, 2026 08:13
The earlier commit included interactive test artifacts (SnowGroup, _McpTest*
nodes) that referenced snowman.tscn, which isn't committed. In CI this
caused main.tscn to fail loading, cascading to ~55 test failures with
EDITOR_NOT_READY.

The new screenshot coverage tests don't require any specific scene structure
beyond a Node3D, so the original simple scene suffices.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Restores SnowGroup as a multi-subject target for screenshot coverage
testing and benchmarking. Includes snowman.tscn (reusable snowman scene)
so main.tscn has no unresolved external references.

Clean version — no _McpTest orphan nodes that would pollute tree/find tests.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
The coverage test previously picked the first Node3D child (Camera3D),
which has no visible geometry. The orthographic top-down shot ended up
empty in headless CI rendering and was silently skipped, making the
image count != 2. Target SnowGroup which has actual meshes to render.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
In headless CI, force_draw(false) produces no rendered output and
get_image() returns an empty image. The single-shot path already
errors with 'Framed viewport rendered an empty image', so all
non-coverage screenshot tests skip cleanly via the result.has('data')
gate.

The coverage path was returning {'data': {'images': []}} instead of
erroring, so its test saw 'data' and asserted 2 images, failing.
Make coverage consistent: return INTERNAL_ERROR when no shots render.

Verified locally: 179/179 tests pass in headless Godot.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- Clamp resize dimensions to 1px min in _finalize_image to avoid
  zero-dim crash on extreme aspect ratios at small max_resolution
- Fix stale comments in _compute_coverage_angles and StubClient
  that referenced a removed complementary/3-image preset
- Correct editor_screenshot docstring: clarify that the viewport
  camera is temporarily adjusted and restored (not a separate camera)

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dsarno dsarno force-pushed the screenshot-coverage-angles branch from 2114e97 to 357b7a7 Compare April 14, 2026 15:39
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@dsarno dsarno merged commit 353680a into main Apr 14, 2026
10 checks passed
@dsarno dsarno deleted the screenshot-coverage-angles branch April 14, 2026 17:50
dsarno added a commit that referenced this pull request May 5, 2026
… loop, YAGNI (#370)

* Drop SessionRegistry threading.RLock — single asyncio loop, YAGNI

Closes #350 (Audit v2 finding #6).

The RLock provided no asyncio-level mutual exclusion (it wasn't awaited)
and the only callers run inside the single asyncio WS handler — single
event loop, no thread crossings. It misled readers into assuming
thread-safety the registry doesn't deliver. If a future caller ever runs
from another thread, asyncio.Lock is the right answer for this
async-first code, not threading.RLock — so the existing lock wasn't even
the future-proof choice.

Removed:
- `from threading import RLock`
- `self._lock = RLock()`
- every `with self._lock:` block (10 sites)

Renamed `_find_matching_session_locked` → `_find_matching_session` since
the "_locked" suffix is now misleading. Updated the wait_for_session
docstring's "registry lock" reference.

Added a class docstring on SessionRegistry making the asyncio-only
invariant explicit and naming asyncio.Lock as the future-proof choice
should a thread crossing ever appear.

Tests: TestSessionRegistryNoThreadingLock pins the deletion against
accidental re-introduction (no `_lock` attribute, no `threading` /
`RLock` imported in the module). All 28 test_session_registry.py tests
plus the full 769-test pytest suite pass. Live smoke: GDScript
test_run via MCP against a real Godot 4.6.2 editor — 1157 passed, 0
failed (registry exercised end-to-end via session_manage list,
editor_state, and every test that round-trips through the WS handler).

Refs umbrella #343.

https://claude.ai/code/session_optimistic-tesla-84obZ

* Trim docstrings + drop narrating comment per /simplify

- SessionRegistry class docstring: trim 5 lines re-litigating the removed
  RLock down to 3 lines stating the asyncio-only invariant and the
  forward-looking choice (asyncio.Lock).
- TestSessionRegistryNoThreadingLock: collapse the 7-line docstring into
  one line — test names already convey the invariant.
- Drop "# Notify any waiters blocked on wait_for_session()" — the loop
  body, the to_notify name, and future.set_result(session) already say
  this. Per CLAUDE.md, avoid comments narrating WHAT.

https://claude.ai/code/session_optimistic-tesla-84obZ

* Drop prescriptive cross-thread advice from docstrings (Copilot review)

asyncio.Lock is a cooperative-scheduling primitive within a single
event loop — it does NOT make cross-thread access safe. Recommending
it as the future-proof choice for a hypothetical thread crossing was
misleading and would steer the next maintainer toward the wrong fix.

Drop the "use asyncio.Lock" prescription entirely. Class docstring now
states only the current invariant (single asyncio loop, no locking).
Test docstring trimmed to neutral framing.

If a thread crossing is ever genuinely introduced, the right answer is
to marshal through the event loop (asyncio.run_coroutine_threadsafe /
loop.call_soon_threadsafe), not to add a lock — but that decision
belongs to whoever has the actual cross-thread requirement, not to
this docstring.

https://claude.ai/code/session_optimistic-tesla-84obZ

* Cover the done-waiter skip branch in register() (Codecov patch)

The if future.done(): continue branch in register() was uncovered;
Codecov flagged it as the one missing line in the patch. Added a
regression test that pre-completes a future, sticks it in
_session_waiters, and verifies the next register() doesn't re-resolve
it. This pins the "don't double-resolve out-of-band-completed waiters"
invariant that was previously untested.

https://claude.ai/code/session_optimistic-tesla-84obZ

---------

Co-authored-by: Claude <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants