Editor/Services/ArchRegion.cs

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.

File Access
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 );
		}
	}
}