Editor utility that captures a screenshot orbiting a target scene object. It finds the target by GUID or name, computes an orbit camera position from pitch/yaw/radius (or presets), spawns a temporary camera in the active scene, asks the editor runtime to take a screenshot, writes a PNG into .sbox/captures, then returns metadata about the capture.
#nullable enable
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.Json;
using System.Text.Json.Nodes;
using Sandbox;
namespace Editor.Mcp
{
internal static class OrbitSnapshotDriver
{
private static Scene? ResolveScene()
{
if ( Game.ActiveScene != null ) return Game.ActiveScene;
var session = EditorRuntime.GetStaticProperty( "Editor.SceneEditorSession", "Active" );
if ( session != null )
{
return EditorRuntime.GetMember( session, "Scene" ) as Scene;
}
return null;
}
public static ToolOutcome<object> CaptureOrbit(
string target,
float? radius = null,
float pitch = 25f,
float yaw = 45f,
string? preset = null,
int resolution = 1024,
string? fileName = null,
bool includeUi = false )
{
var scene = ResolveScene();
if ( scene == null )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Unavailable, "orbit.no_scene", "No active scene loaded." );
}
var go = scene.Directory.FindByGuid( Guid.TryParse( target, out var guid ) ? guid : Guid.Empty )
?? scene.GetAllObjects( false ).FirstOrDefault( o => string.Equals( o.Name, target, StringComparison.OrdinalIgnoreCase ) );
if ( go == null )
{
return ToolOutcome<object>.Failure( null, ToolStatus.Failed, "orbit.not_found", $"Target '{target}' not found." );
}
// Compute target center and bounding radius
var center = go.WorldPosition;
var modelRenderer = go.GetComponent<ModelRenderer>();
var boundsRadius = 100f;
if ( modelRenderer != null && modelRenderer.Model != null )
{
var modelBounds = modelRenderer.Model.Bounds;
center = go.WorldPosition + modelBounds.Center;
boundsRadius = Math.Max( modelBounds.Size.Length * 0.75f, 60f );
}
var actualRadius = (radius.HasValue && radius.Value > 0) ? radius.Value : boundsRadius * 1.5f;
var actualPitch = pitch;
var actualYaw = yaw;
if ( !string.IsNullOrWhiteSpace( preset ) )
{
switch ( preset.ToLowerInvariant().Trim() )
{
case "front": actualPitch = 0f; actualYaw = 0f; break;
case "back": actualPitch = 0f; actualYaw = 180f; break;
case "left": actualPitch = 0f; actualYaw = 90f; break;
case "right": actualPitch = 0f; actualYaw = -90f; break;
case "top": actualPitch = 88f; actualYaw = 0f; break;
case "isometric": actualPitch = 30f; actualYaw = 45f; break;
case "cinematic": actualPitch = 15f; actualYaw = 35f; break;
}
}
var pitchRad = (float)(actualPitch * Math.PI / 180.0);
var yawRad = (float)(actualYaw * Math.PI / 180.0);
var offsetX = actualRadius * (float)Math.Cos( pitchRad ) * (float)Math.Sin( yawRad );
var offsetY = actualRadius * (float)Math.Cos( pitchRad ) * (float)Math.Cos( yawRad );
var offsetZ = actualRadius * (float)Math.Sin( pitchRad );
var camPos = center + new Vector3( offsetX, offsetY, offsetZ );
var camRot = Rotation.LookAt( center - camPos );
// Spawn temporary camera attached directly to active scene
var camGo = scene.CreateObject( true );
camGo.Name = "__temporary_orbit_camera__";
camGo.WorldPosition = camPos;
camGo.WorldRotation = camRot;
var cam = camGo.Components.Create<CameraComponent>();
var boundedRes = Math.Clamp( resolution, 128, 4096 );
var safeName = go.Name.Replace( " ", "_" ).ToLowerInvariant();
var outFileName = string.IsNullOrWhiteSpace( fileName ) ? $"orbit_{safeName}_{actualPitch:0}_{actualYaw:0}.png" : fileName.Trim();
var artifactDir = Path.Combine( ".sbox", "captures" );
Directory.CreateDirectory( artifactDir );
var artifactPath = Path.Combine( artifactDir, outFileName );
object? screenshotResult = null;
byte[]? imageBytes = null;
string? invokeError = null;
try
{
screenshotResult = EditorRuntime.InvokeStatic( "Editor.Mcp.SceneTools", "CameraScreenshot", cam.Id.ToString(), boundedRes, boundedRes, includeUi );
if ( screenshotResult is Sandbox.Bitmap bmp )
{
imageBytes = bmp.ToPng();
File.WriteAllBytes( artifactPath, imageBytes );
}
else if ( screenshotResult != null )
{
var json = JsonSerializer.Serialize( screenshotResult );
var node = JsonNode.Parse( json );
var base64Data = (node as JsonArray)?[0]?["data"]?.GetValue<string>() ?? (node as JsonObject)?["data"]?.GetValue<string>();
if ( !string.IsNullOrEmpty( base64Data ) )
{
imageBytes = Convert.FromBase64String( base64Data );
File.WriteAllBytes( artifactPath, imageBytes );
}
}
}
catch ( Exception ex )
{
invokeError = ex.ToString();
}
finally
{
camGo.Destroy();
}
return ToolOutcome<object>.Ready( new
{
Target = go.Name,
Id = go.Id.ToString(),
TargetCenter = center,
CameraPosition = camPos,
CameraAngles = camRot.Angles(),
Radius = actualRadius,
Pitch = actualPitch,
Yaw = actualYaw,
Preset = preset,
Resolution = boundedRes,
ArtifactPath = artifactPath,
Captured = imageBytes != null,
FileSizeBytes = imageBytes?.Length ?? 0,
RawResultType = screenshotResult?.GetType().FullName,
} );
}
}
}