Editor/Porch/ArchOverhangGen.cs

Editor-side geometry generator for porch overhangs. Computes unsupported overhang regions, creates soffit slabs and places supporting posts based on building, room, kit and style data.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

// Nothing under it carries it, so it needs a boxed underside of its own.
public static class ArchOverhangGen
{
	const float Walkable = 24f;

	public static void Build( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style )
	{
		new ArchOverhangGenerationService( room, building, kit, style ).Create( canvas );
	}

	// Walkable depth needs carrying: posts land on open-air edges, corners first, then evenly.
	internal static void Posts( ArchMesh canvas, List<List<Vector2>> region, ArchRoom room, ArchBuilding building, ArchKit kit, float top, ArchBrush brush )
	{
		if ( ArchFootprint.Shrink( region, Walkable * 0.5f ).Count == 0 )
		{
			return;
		}

		var support = new ArchOverhangSupportService( room, building, kit );
		var spacing = MathF.Max( 32f, kit.PorchPostSize * 8f );

		foreach ( var loop in ArchFootprint.Outer( region ) )
		{
			var open = ArchRegion.Open( loop, support.Carried ).Select( run => ArchRunPath.Of( run, top ) );

			foreach ( var at in ArchBays.Posts( ArchBays.Over( open, spacing, MathF.Max( 6f, kit.PorchPostSize * 2f ) ) ) )
			{
				ArchPorchGen.Stand( canvas, at.Point, kit, support.GradeAt( at.Point ), top, brush );
			}
		}
	}

	// The line below grows by half a wall first, or the footprint reads unsupported.
	public static List<List<Vector2>> Unsupported( ArchRoom room, ArchBuilding building, ArchKit kit )
	{
		return new ArchOverhangSupportService( room, building, kit ).Resolve();
	}

}

sealed class ArchOverhangGenerationService
{
	readonly ArchRoom room;
	readonly ArchBuilding building;
	readonly ArchKit kit;
	readonly ArchStyle style;

	public ArchOverhangGenerationService( ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style )
	{
		this.room = room;
		this.building = building;
		this.kit = kit;
		this.style = style;
	}

	public void Create( ArchMesh canvas )
	{
		var support = new ArchOverhangSupportService( room, building, kit );
		var region = support.Resolve();

		if ( region.Count == 0 )
		{
			return;
		}

		var chain = new[] { room.Palette, building.Palette };
		var depth = MathF.Max( 3f, kit.FasciaHeight );
		var top = room.BaseHeight - kit.FloorThickness;

		foreach ( var loop in ArchFootprint.Outer( region ) )
		{
			ArchFloorGen.Slab( canvas, loop, null, top - depth, top, style.Brush( ArchSurface.Soffit, chain ) );
		}

		using ( canvas.Part( ArchPieces.Posts ) )
		{
			ArchOverhangGen.Posts( canvas, region, room, building, kit, top - depth, style.Brush( ArchSurface.Pillar, chain ) );
		}
	}
}

sealed class ArchOverhangSupportService
{
	readonly ArchRoom room;
	readonly ArchBuilding building;
	readonly ArchKit kit;
	readonly List<List<Vector2>> carried;

	public ArchOverhangSupportService( ArchRoom room, ArchBuilding building, ArchKit kit )
	{
		this.room = room;
		this.building = building;
		this.kit = kit;
		carried = room is null || building is null || room.Floor <= 0
			? new List<List<Vector2>>()
			: ArchRegion.Shell( ArchRegion.Storey( building, room.Floor - 1 ), kit.WallThickness );
	}

	public List<List<Vector2>> Resolve()
	{
		var none = new List<List<Vector2>>();

		if ( room is null || building is null || room.Floor <= 0 || room.Spans )
		{
			return none;
		}

		var footprint = ArchFloorGen.Footprint( room );

		if ( footprint.Count < 3 || !ArchFootprint.IsRectilinear( footprint ) || carried.Count == 0 )
		{
			return none;
		}

		return ArchRegion.Minus( footprint, carried );
	}

	public float GradeAt( Vector2 point )
	{
		var lower = building.Rooms
			.Where( other => other.Floor == room.Floor - 1 )
			.Where( other => ArchFootprint.Encloses( ArchRegion.Shell( ArchRegion.Footprints( new[] { other } ), kit.WallThickness ), point ) )
			.OrderByDescending( other => other.BaseHeight )
			.FirstOrDefault();

		return lower?.BaseHeight ?? building.Rooms.Where( other => other.Floor == room.Floor - 1 ).Select( other => other.BaseHeight ).DefaultIfEmpty( kit.GroundClearance ).Min();
	}

	public IReadOnlyList<List<Vector2>> Carried => carried;
}