Editor utility for architectural regions. It computes room/storey footprints, grows shells, subtracts blockers, splits loops into open edges vs edges against blockers, and provides helpers like outward normal and edge struct.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public readonly struct ArchEdge
{
public Vector2 From { get; init; }
public Vector2 To { get; init; }
public Vector2 Outward { get; init; }
public Vector2 Midpoint => (From + To) * 0.5f;
public float Length => (To - From).Length;
}
// One place for "what does this storey cover" - five generators used to re-derive it themselves.
public static class ArchRegion
{
// A non-rectilinear room is left out - the cell decomposition has no grid for it.
public static List<List<Vector2>> Storey( ArchBuilding building, int level )
{
return Footprints( building?.Rooms?.Where( room => room.Floor == level ) );
}
public static List<List<Vector2>> Footprints( IEnumerable<ArchRoom> rooms )
{
return Footprints( rooms, true );
}
// The rectilinear filter guards the storey's own cell decomposition, but a roof has to cover a
// turned room too - its cells clip against the roof outline either way, so the filter is a caller
// choice rather than a second gather.
public static List<List<Vector2>> Footprints( IEnumerable<ArchRoom> rooms, bool rectilinearOnly )
{
if ( rooms is null )
{
return new List<List<Vector2>>();
}
return rooms
.Select( ArchFloorGen.Footprint )
.Where( loop => loop.Count >= 3 && (!rectilinearOnly || ArchFootprint.IsRectilinear( loop )) )
.ToList();
}
// A room's footprint is its walls' CENTRELINE - grow by half a thickness or the shell reads small.
public static List<List<Vector2>> Shell( IReadOnlyList<List<Vector2>> storey, float thickness )
{
return storey.Count == 0 ? new List<List<Vector2>>() : ArchFootprint.Grow( storey, MathF.Max( 0f, thickness ) * 0.5f );
}
public static List<List<Vector2>> Minus( IReadOnlyList<Vector2> region, IReadOnlyList<IReadOnlyList<Vector2>> blockers )
{
return ArchFootprint.Subtract( new[] { region }, blockers );
}
public static bool Covers( IReadOnlyList<IReadOnlyList<Vector2>> region, IReadOnlyList<Vector2> loop )
{
return region.Count > 0 && Minus( loop, region ).Count == 0;
}
public static List<List<Vector2>> Open( IReadOnlyList<Vector2> loop, IReadOnlyList<IReadOnlyList<Vector2>> blockers )
{
Split( loop, blockers, out var open, out _ );
return open;
}
// Both halves of the same walk - deriving them separately let a deck and its flashing disagree.
public static void Split(
IReadOnlyList<Vector2> loop,
IReadOnlyList<IReadOnlyList<Vector2>> blockers,
out List<List<Vector2>> open,
out List<ArchEdge> against )
{
open = new List<List<Vector2>>();
against = new List<ArchEdge>();
var count = loop.Count;
if ( count < 3 )
{
return;
}
var leaning = new bool[count];
for ( var index = 0; index < count; index++ )
{
var from = loop[index];
var to = loop[(index + 1) % count];
var outward = Outward( from, to );
leaning[index] = ArchProbe.Faces( blockers, from, to, outward );
if ( leaning[index] )
{
against.Add( new ArchEdge { From = from, To = to, Outward = outward } );
}
}
var first = Array.IndexOf( leaning, true );
// Nothing to lean on comes back as one closed run.
if ( first < 0 )
{
open.Add( loop.Append( loop[0] ).ToList() );
return;
}
var run = new List<Vector2>();
for ( var step = 1; step <= count; step++ )
{
var index = (first + step) % count;
if ( leaning[index] )
{
Close( open, run );
run = new List<Vector2>();
continue;
}
if ( run.Count == 0 )
{
run.Add( loop[index] );
}
run.Add( loop[(index + 1) % count] );
}
Close( open, run );
}
public static Vector2 Outward( Vector2 from, Vector2 to )
{
var along = (to - from).Normal;
return new Vector2( along.y, -along.x );
}
static void Close( List<List<Vector2>> open, List<Vector2> run )
{
if ( run.Count >= 2 )
{
open.Add( run );
}
}
}