Editor/Output/ArchShot.Draw.cs

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.

File Access
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 );
	}
}