Editor/UI/ArchSlotPreview.cs

Editor utility that renders preview pixmaps for architectural surface slots. It composes simple geometry into a Scene, sets up lights and camera, renders to a Pixmap, and caches results per palette and surface binding.

File AccessExternal Download
using System;
using System.Collections.Generic;
using Editor;
using Sandbox;

namespace Sunless.Architecture;

public static class ArchSlotPreview
{
	const float Metre = 39.37f;
	const float WallHeight = 3f * Metre;
	const float WallThickness = 9f;
	const float Span = 4f * Metre;
	const float Overhang = 30f;

	static readonly Dictionary<string, Pixmap> cache = new();
	static readonly HashSet<string> failed = new();

	public static void Invalidate()
	{
		cache.Clear();
		failed.Clear();
	}

	public static Pixmap For( string palette, ArchSurface slot, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, int size = 256 )
	{
		if ( bindings is null || !bindings.TryGetValue( slot, out var subject ) )
		{
			return null;
		}

		var key = $"{palette}|{slot}|{subject.Name}";

		if ( cache.TryGetValue( key, out var hit ) )
		{
			return hit;
		}

		if ( failed.Contains( key ) )
		{
			return null;
		}

		var pixmap = Render( slot, bindings, size );

		if ( pixmap is null )
		{
			failed.Add( key );
			return null;
		}

		cache[key] = pixmap;
		return pixmap;
	}

	static Pixmap Render( ArchSurface slot, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, int size )
	{
		Scene scene = null;

		try
		{
			scene = new Scene();

			using ( scene.Push() )
			{
				var subject = Compose( scene, slot, bindings );

				if ( subject is null )
				{
					return null;
				}

				var camera = Frame( scene, slot, subject );
				var pixmap = new Pixmap( size, size );

				return camera.RenderToPixmap( pixmap ) ? pixmap : null;
			}
		}
		catch ( Exception exception )
		{
			Log.Warning( $"Architecture: could not preview {slot}: {exception.Message}" );
			return null;
		}
		finally
		{
			scene?.Destroy();
		}
	}

	static ModelRenderer Compose( Scene scene, ArchSurface slot, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings )
	{
		Ground( scene, Brush( bindings, ArchSurface.Floor ) );

		// Stair shots look along the flight; a shell wall would block the camera.
		var stairs = slot is ArchSurface.StairTread or ArchSurface.StairRiser;

		var wallBrush = Brush( bindings, ArchSurface.WallExterior );
		var wall = stairs ? null : Piece( scene, "Wall", canvas => canvas.Box(
			new Vector3( -WallThickness, -Span, 0 ),
			new Vector3( 0, Span, WallHeight ), wallBrush ) );

		return slot switch
		{
			ArchSurface.WallExterior => wall,

			ArchSurface.WallInterior or ArchSurface.Reveal => Piece( scene, "Interior", canvas => canvas.Box(
				new Vector3( 0, -Span, 0 ),
				new Vector3( WallThickness, Span, WallHeight ), Brush( bindings, slot ) ) ),

			ArchSurface.Floor => Ground( scene, Brush( bindings, ArchSurface.Floor ) ),

			ArchSurface.Ceiling => Piece( scene, "Ceiling", canvas => canvas.Box(
				new Vector3( 0, -Span, WallHeight - 5 ),
				new Vector3( Span, Span, WallHeight ), Brush( bindings, slot ) ) ),

			// Wound so the pitch faces UP; reversed, it culls to black.
			ArchSurface.Roof => Piece( scene, "Roof", canvas => canvas.Quad(
				new Vector3( -Overhang, -Span, WallHeight ),
				new Vector3( Span * 0.7f, -Span, WallHeight + Span * 0.5f ),
				new Vector3( Span * 0.7f, Span, WallHeight + Span * 0.5f ),
				new Vector3( -Overhang, Span, WallHeight ), Brush( bindings, slot ) ) ),

			ArchSurface.RoofEdge => Piece( scene, "Fascia", canvas => canvas.Box(
				new Vector3( -Overhang - 4, -Span, WallHeight - 10 ),
				new Vector3( -Overhang, Span, WallHeight + 4 ), Brush( bindings, slot ) ) ),

			ArchSurface.Soffit => Piece( scene, "Soffit", canvas => canvas.Box(
				new Vector3( -Overhang, -Span, WallHeight - 6 ),
				new Vector3( 0, Span, WallHeight - 2 ), Brush( bindings, slot ) ) ),

			ArchSurface.Gutter => Gutter( scene, Brush( bindings, slot ) ),

			ArchSurface.Trim => Piece( scene, "Trim", canvas => canvas.Box(
				new Vector3( -WallThickness - 3, -Span, WallHeight * 0.55f ),
				new Vector3( -WallThickness, Span, WallHeight * 0.55f + 14 ), Brush( bindings, slot ) ) ),

			ArchSurface.Baseboard => Piece( scene, "Baseboard", canvas => canvas.Box(
				new Vector3( 0, -Span, 0 ),
				new Vector3( 3, Span, 11 ), Brush( bindings, slot ) ) ),

			ArchSurface.Pillar => Piece( scene, "Pillar", canvas => canvas.Box(
				new Vector3( -34, -14, 0 ),
				new Vector3( -20, 14, WallHeight ), Brush( bindings, slot ) ) ),

			ArchSurface.StairTread => Steps( scene, bindings, true ),
			ArchSurface.StairRiser => Steps( scene, bindings, false ),

			_ => wall
		};
	}

	static ModelRenderer Gutter( Scene scene, ArchBrush brush )
	{
		Piece( scene, "Fascia", canvas => canvas.Box(
			new Vector3( -Overhang - 4, -Span, WallHeight - 10 ),
			new Vector3( -Overhang, Span, WallHeight + 4 ), brush ) );

		return Piece( scene, "Gutter", canvas =>
		{
			var front = -Overhang - 14f;
			var back = -Overhang - 4f;
			var top = WallHeight - 2f;
			var floor = WallHeight - 9f;

			canvas.Box( new Vector3( front, -Span, floor ), new Vector3( front + 2, Span, top ), brush );
			canvas.Box( new Vector3( front, -Span, floor ), new Vector3( back, Span, floor + 2 ), brush );
			canvas.Box( new Vector3( back - 2, -Span, floor ), new Vector3( back, Span, top ), brush );
		} );
	}

	static ModelRenderer Steps( Scene scene, Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, bool outlineTread )
	{
		var treadBrush = Brush( bindings, ArchSurface.StairTread );
		var riserBrush = Brush( bindings, ArchSurface.StairRiser );

		const float rise = 7f;
		const float going = 11f;

		var treads = Piece( scene, "Treads", canvas =>
		{
			for ( var step = 0; step < 5; step++ )
			{
				var z = (step + 1) * rise;
				var x = step * going;

				canvas.Box( new Vector3( x, -Span * 0.4f, z - 2 ), new Vector3( x + going + 2, Span * 0.4f, z ), treadBrush );
			}
		} );

		var risers = Piece( scene, "Risers", canvas =>
		{
			for ( var step = 0; step < 5; step++ )
			{
				var z = (step + 1) * rise;
				var x = step * going;

				canvas.Box( new Vector3( x, -Span * 0.4f, z - rise ), new Vector3( x + 2, Span * 0.4f, z - 2 ), riserBrush );
			}
		} );

		return outlineTread ? treads : risers;
	}

	static ModelRenderer Ground( Scene scene, ArchBrush brush )
	{
		return Piece( scene, "Ground", canvas => canvas.Box(
			new Vector3( -Span, -Span, -4 ),
			new Vector3( Span, Span, 0 ), brush ) );
	}

	static ModelRenderer Piece( Scene scene, string name, Action<ArchMesh> build )
	{
		var canvas = new ArchMesh( Transform.Zero );
		build( canvas );

		if ( canvas.IsEmpty )
		{
			return null;
		}

		var node = new GameObject( true, name );
		node.SetParent( scene );

		var renderer = node.Components.GetOrCreate<ModelRenderer>();
		renderer.Model = canvas.Finish().Rebuild();
		renderer.Tint = Color.White;

		return renderer;
	}

	static ArchBrush Brush( Dictionary<ArchSurface, ArchMapPalette.Surface> bindings, ArchSurface slot )
	{
		var material = ArchBrush.Missing;

		if ( bindings.TryGetValue( slot, out var surface ) )
		{
			material = Material.Load( surface.MaterialPath ) ?? ArchBrush.Missing;
		}

		return new ArchBrush { Material = material, TexelScale = ArchMesh.TexelScale };
	}

	static CameraComponent Frame( Scene scene, ArchSurface slot, Renderer subject )
	{
		var sun = new GameObject( true, "Sun" );
		sun.SetParent( scene );
		sun.WorldRotation = Rotation.From( -52, 135, 0 );

		var light = sun.Components.GetOrCreate<DirectionalLight>();
		light.LightColor = Color.White * 4.2f;
		light.SkyColor = new Color( 0.42f, 0.47f, 0.58f ) * 3.2f;
		light.Shadows = false;

		var fill = new GameObject( true, "Fill" );
		fill.SetParent( scene );
		fill.WorldRotation = Rotation.From( -14, -40, 0 );

		var fillLight = fill.Components.GetOrCreate<DirectionalLight>();
		fillLight.LightColor = Color.White * 1.5f;
		fillLight.SkyColor = Color.Black;
		fillLight.Shadows = false;

		var node = new GameObject( true, "Camera" );
		node.SetParent( scene );

		var camera = node.Components.GetOrCreate<CameraComponent>();
		camera.BackgroundColor = new Color( 0.07f, 0.075f, 0.085f );
		camera.FieldOfView = 42;
		camera.ZNear = 1;
		camera.ZFar = 5000;

		var (target, offset) = Shot( slot );

		node.WorldPosition = target + offset;
		node.WorldRotation = Rotation.LookAt( (target - node.WorldPosition).Normal );

		node.Components.GetOrCreate<Highlight>();

		var outline = subject.Components.GetOrCreate<HighlightOutline>();
		outline.OverrideTargets = true;
		outline.Targets = new List<Renderer> { subject };
		outline.Color = Color.White;
		outline.ObscuredColor = Color.White.WithAlpha( 0.4f );
		outline.InsideColor = Color.Transparent;
		outline.InsideObscuredColor = Color.Transparent;
		outline.Width = 6f;

		return camera;
	}

	static (Vector3 Target, Vector3 Offset) Shot( ArchSurface slot )
	{
		var eave = new Vector3( -Overhang, 0, WallHeight );

		return slot switch
		{
			ArchSurface.Roof => (eave + new Vector3( 20, 0, 20 ), new Vector3( -150, -120, 90 )),
			ArchSurface.RoofEdge or ArchSurface.Soffit => (eave, new Vector3( -78, -70, -26 )),
			ArchSurface.Gutter => (eave + new Vector3( -9, 0, -4 ), new Vector3( -62, -58, -18 )),
			ArchSurface.Trim => (new Vector3( -WallThickness, 0, WallHeight * 0.58f ), new Vector3( -74, -62, 26 )),
			ArchSurface.Baseboard => (new Vector3( 2, 0, 8 ), new Vector3( 66, -54, 34 )),
			ArchSurface.Pillar => (new Vector3( -27, 0, WallHeight * 0.5f ), new Vector3( -104, -86, 44 )),
			ArchSurface.Floor => (Vector3.Zero, new Vector3( 96, -96, 96 )),
			ArchSurface.Ceiling => (new Vector3( Span * 0.4f, 0, WallHeight - 4 ), new Vector3( 92, -78, -44 )),
			ArchSurface.WallInterior or ArchSurface.Reveal => (new Vector3( WallThickness, 0, WallHeight * 0.5f ), new Vector3( 128, -96, 34 )),
			ArchSurface.StairTread or ArchSurface.StairRiser => (new Vector3( 28, 0, 20 ), new Vector3( -66, -84, 58 )),
			_ => (new Vector3( -WallThickness, 0, WallHeight * 0.5f ), new Vector3( -132, -104, 40 ))
		};
	}
}