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.
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}";
}
}