Editor/Services/ArchRegion.cs
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 );
}
// Rectilinear test uses the storey's own bearing, not the world axes.
public static List<List<Vector2>> Footprints( IEnumerable<ArchRoom> rooms, bool rectilinearOnly ) {
if ( rooms is null ) {
return new List<List<Vector2>>();
}
var loops = rooms.Select( ArchFloorGen.Footprint ).Where( loop => loop.Count >= 3 ).ToList();
if ( !rectilinearOnly ) {
return loops;
}
ArchFootprint.Bearing( loops, out var bearing );
return loops.Where( loop => ArchFootprint.IsRectilinear( loop, bearing ) ).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 );
}
}
}