Editor image annotation code for ArchShot. It projects world-space geometry into a bitmap, draws an orthographic grid, an outlined bounding box of the target, and a caption/legend with size, face count and orientation.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchShot
{
static void Annotate(
Bitmap bitmap, ArchTarget target, ArchAngle angle, Vector3 eye, Rotation rotation, float orthoHeight, ArchShotOptions options )
{
var halfHeight = orthoHeight * 0.5f;
var halfWidth = halfHeight * bitmap.Width / bitmap.Height;
Vector2 Project( Vector3 world )
{
var offset = world - eye;
var across = Vector3.Dot( offset, rotation.Right );
var up = Vector3.Dot( offset, rotation.Up );
return new Vector2(
bitmap.Width * (0.5f + across / (2f * halfWidth)),
bitmap.Height * (0.5f - up / (2f * halfHeight)) );
}
if ( options.Orthographic )
{
Grid( bitmap, eye, rotation, halfWidth, halfHeight, Project );
}
if ( options.Frame )
{
Outline( bitmap, target.Frame, Project );
}
Caption( bitmap, target, angle, orthoHeight, options );
}
// Drawn with this same projection, so the captioned spacing is the image spacing.
static void Grid( Bitmap bitmap, Vector3 eye, Rotation rotation, float halfWidth, float halfHeight, Func<Vector3, Vector2> project )
{
var step = Step( halfHeight * 2f );
var right = rotation.Right;
var up = rotation.Up;
bitmap.SetPen( new Color( 1f, 1f, 1f, 0.13f ), 1f );
for ( var offset = Snap( -halfWidth, step ); offset <= halfWidth; offset += step )
{
var at = eye + right * offset;
bitmap.DrawLine( project( at - up * halfHeight ), project( at + up * halfHeight ) );
}
for ( var offset = Snap( -halfHeight, step ); offset <= halfHeight; offset += step )
{
var at = eye + up * offset;
bitmap.DrawLine( project( at - right * halfWidth ), project( at + right * halfWidth ) );
}
}
static void Outline( Bitmap bitmap, BBox box, Func<Vector3, Vector2> project )
{
var corners = box.Corners.ToArray();
bitmap.SetDashedPen( new Color( 1f, 0.62f, 0.2f, 0.85f ), 1.4f, new[] { 6f, 5f } );
foreach ( var (from, to) in Edges() )
{
bitmap.DrawLine( project( corners[from] ), project( corners[to] ) );
}
}
// BBox.Corners walks bottom four then top four in the same order - three fixed rings.
static IEnumerable<(int, int)> Edges()
{
for ( var index = 0; index < 4; index++ )
{
yield return (index, (index + 1) % 4);
yield return (index + 4, (index + 1) % 4 + 4);
yield return (index, index + 4);
}
}
static void Caption( Bitmap bitmap, ArchTarget target, ArchAngle angle, float orthoHeight, ArchShotOptions options )
{
var box = target.Frame;
var scale = options.Orthographic ? $"{Step( orthoHeight ):0.#}in grid" : "perspective";
var built = target.HasGeometry
? $"built {Say( target.Built.Mins )} to {Say( target.Built.Maxs )}"
: "NO GEOMETRY - the plan has this part but nothing was generated for it";
var lines = new[]
{
$"{angle} - {target.Describe}",
$"{scale} size {box.Size.x:0.#} x {box.Size.y:0.#} x {box.Size.z:0.#} in {target.Faces} faces",
built
};
var pen = new Color( 0.05f, 0.055f, 0.065f, 0.72f );
var text = new Color( 0.92f, 0.94f, 0.96f );
bitmap.SetFill( pen );
bitmap.DrawRect( 0f, 0f, bitmap.Width, 62f );
for ( var index = 0; index < lines.Length; index++ )
{
var colour = index == 2 && !target.HasGeometry ? new Color( 1f, 0.5f, 0.45f ) : text;
bitmap.DrawText(
new TextRendering.Scope( lines[index], colour, 13f, "Consolas" ),
new Rect( 10f, 6f + index * 17f, bitmap.Width - 20f, 17f ),
TextFlag.LeftTop );
}
Legend( bitmap, angle );
}
// Without it, judging which end is short is a guess.
static void Legend( Bitmap bitmap, ArchAngle angle )
{
var (across, up) = angle switch
{
ArchAngle.Front => ("-Y", "+Z"),
ArchAngle.Back => ("+Y", "+Z"),
ArchAngle.Left => ("+X", "+Z"),
ArchAngle.Right => ("-X", "+Z"),
ArchAngle.Top => ("+Y", "-X"),
ArchAngle.Bottom => ("+Y", "+X"),
ArchAngle.Along => ("right of travel", "+Z"),
ArchAngle.AlongBack => ("left of travel", "+Z"),
ArchAngle.Across => ("along travel", "+Z"),
ArchAngle.AcrossBack => ("against travel", "+Z"),
_ => ("", "+Z")
};
if ( across.Length == 0 )
{
return;
}
bitmap.SetFill( new Color( 0.05f, 0.055f, 0.065f, 0.72f ) );
bitmap.DrawRect( 0f, bitmap.Height - 24f, bitmap.Width, 24f );
bitmap.DrawText(
new TextRendering.Scope( $"screen right = {across} screen up = {up}", new Color( 0.72f, 0.78f, 0.85f ), 12f, "Consolas" ),
new Rect( 10f, bitmap.Height - 20f, bitmap.Width - 20f, 16f ),
TextFlag.LeftTop );
}
}