Editor/Floor/ArchFloorGen.cs

Editor-side utility for architectural floor generation. It groups rooms by floor/ceiling properties, computes heights and overhead planes, and produces floor/ceiling grouping keys and skin identifiers used when generating geometry.

Reflection
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchFloorGroup
{
	public string Key { get; init; }
	public List<ArchRoom> Rooms { get; init; }
	public List<List<Vector2>> Region { get; init; }
	public float WallHeight { get; init; }

	// All rooms share the key, so Lead's settings hold for the whole group.
	public ArchRoom Lead => Rooms[0];

	public int Level => Lead.Floor;
}

public static partial class ArchFloorGen
{
	// A mouth straddles the seam line, so its corners sit up to this far off it.
	const float SeamReach = 6f;

	// One answer to how tall a room's walls are: the room's own, else the kit's. Every generator,
	// extents report and affector resolves this the same way, so a room that sets its height reads
	// the same to the wall that builds it and the cut that reaches through it.
	public static float WallHeight( ArchRoom room, ArchKit kit )
	{
		return room.WallHeight > 0f ? room.WallHeight : kit.WallHeight;
	}

	// One answer to how deep a ceiling's body is, asked by the generator, the recess a bool aims into and
	// the report - a slab-thin ceiling has nothing to take a coffer out of, so it is the kit's own depth
	// rather than the floor's.
	public static float CeilingDepth( ArchKit kit ) => MathF.Max( kit.FloorThickness, kit.CeilingThickness );

	public static float CeilingDepth( ArchRoom room, ArchKit kit )
	{
		return room.CeilingDepth > 0f ? room.CeilingDepth : CeilingDepth( kit );
	}

	// The plane a room is read from below: the plate, less the body hanging under it.
	public static float CeilingSoffit( ArchRoom room, ArchKit kit )
	{
		return room.BaseHeight + WallHeight( room, kit ) - CeilingDepth( room, kit );
	}

	// A vault and a loft floor ARE the storey's ceiling and the roof owns both - they are shapes, not a
	// depth. Everything else the room carries itself, at the depth it was authored with, and the roof
	// hands its own slab over rather than building a second one an inch away.
	public static bool RoofKeepsCeiling( ArchRoofPart roof ) => roof.VaultedCeiling || roof.LoftFloor;

	// The plane anything hung in a room hangs FROM: the underside of whatever ceiling body is actually
	// overhead, and the plate itself when there is none. A beam measured off the plate instead is buried
	// in the ceiling for as deep as that ceiling is.
	public static float Overhead( ArchBuilding building, ArchRoom room, ArchKit kit )
	{
		var plate = room.BaseHeight + WallHeight( room, kit );

		if ( CeilingHeldAbove( building, room ) )
		{
			return plate - kit.FloorThickness;
		}

		return room.HasCeiling ? plate - CeilingDepth( room, kit ) : plate;
	}

	// A recess carved into the ceiling MOVES that plane up, so whatever is hung under it lands inside the
	// coffer instead of eight inches below its mouth. This is what makes the pair of gestures compose: drag
	// the zone out of the ceiling, then hang the members in the zone you just made.
	public static float Overhead( ArchPlan plan, ArchBuilding building, ArchRoom room, ArchKit kit, Vector2 at )
	{
		var soffit = Overhead( building, room, kit );

		if ( plan is null || building is null )
		{
			return soffit;
		}

		var head = room.BaseHeight + WallHeight( room, kit ) + ArchContact.Bite( kit );

		foreach ( var volume in ArchCut.Volumes( plan, room.Floor, kit, soffit, head, building.Id, ArchCutAffects.Ceilings ) )
		{
			if ( volume.Covers( at ) )
			{
				soffit = MathF.Max( soffit, volume.Ceiling.At( at ) );
			}
		}

		return soffit;
	}

	public static bool CeilingHeldAbove( ArchBuilding building, ArchRoom room )
	{
		return building.Roofs.Any( roof => roof.Level == room.Floor && RoofKeepsCeiling( roof )
			&& ArchFootprint.Overlaps( roof.Outline(), Footprint( room ) ) );
	}

	public static List<ArchFloorGroup> Groups( ArchBuilding building, ArchKit kit, ArchStyle style )
	{
		if ( building?.Rooms is null )
		{
			return new List<ArchFloorGroup>();
		}

		return Bucketed( building.Rooms, room => FloorKey( room, building, style ), kit );
	}

	// The ceiling hangs at wall height, so a stepped eave would split the floor if bucketed with it.
	static List<ArchFloorGroup> Ceilings( ArchFloorGroup floor, ArchBuilding building, ArchKit kit, ArchStyle style )
	{
		return Bucketed( floor.Rooms, room => CeilingKey( room, building, kit, style ), kit );
	}

	static List<ArchFloorGroup> Bucketed( IEnumerable<ArchRoom> rooms, Func<ArchRoom, string> key, ArchKit kit )
	{
		var groups = new List<ArchFloorGroup>();

		foreach ( var batch in rooms.GroupBy( key ) )
		{
			var group = Group( batch.ToList(), batch.Key, kit );

			if ( group is not null )
			{
				groups.Add( group );
			}
		}

		return groups;
	}

	static ArchFloorGroup Group( List<ArchRoom> candidates, string key, ArchKit kit )
	{
		// Dropped: EnsureTarget's empty room sorts first in the plan, so it must never lead the group.
		var rooms = candidates.Where( room => Footprint( room ).Count >= 3 ).ToList();

		if ( rooms.Count == 0 )
		{
			return null;
		}

		var loops = rooms.Select( Footprint ).ToList();

		// A non-rectilinear room has no grid to merge on, so it keeps its own loop and its own seam.
		var region = loops.All( ArchFootprint.IsRectilinear ) ? ArchFootprint.Union( loops ) : loops;

		return new ArchFloorGroup
		{
			Key = key,
			Rooms = rooms,
			Region = region,
			WallHeight = WallHeight( rooms[0], kit )
		};
	}

	// Anything not listed is shared, so omitting a property silently merges rooms that differ.
	static string FloorKey( ArchRoom room, ArchBuilding building, ArchStyle style )
	{
		var chain = new[] { room.Palette, building.Palette };

		return string.Join( '|',
			room.Floor,
			MathF.Round( room.BaseHeight, 2 ),
			room.HasFloor,
			room.FloorBoards,
			MathF.Round( room.FloorBoardYaw, 2 ),
			room.RaisedFoundation,
			Skin( style.Brush( ArchSurface.Floor, chain ) ),
			Skin( style.Brush( ArchSurface.Deck, chain ) ),
			Skin( style.Brush( ArchSurface.Trim, chain ) ),
			Skin( style.Brush( ArchSurface.Foundation, chain ) ) );
	}

	static string CeilingKey( ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style )
	{
		var chain = new[] { room.Palette, building.Palette };
		var wallHeight = WallHeight( room, kit );

		return string.Join( '|',
			room.Floor,
			MathF.Round( room.BaseHeight, 2 ),
			MathF.Round( wallHeight, 2 ),
			room.HasCeiling,
			MathF.Round( CeilingDepth( room, kit ), 2 ),
			Skin( style.Brush( ArchSurface.Ceiling, chain ) ),
			Skin( style.Brush( ArchSurface.Trim, chain ) ) );
	}

	// One path is one cached instance, so reference identity means "the same skin".
	static string Skin( ArchBrush brush )
	{
		var material = brush.Material is null ? 0 : RuntimeHelpers.GetHashCode( brush.Material );

		return $"{material}@{brush.TexelScale}";
	}
}