Editor/Roof/ArchRoofRegionService.cs

Editor service that computes roof regions for an architectural roof part. It builds polygon loops by intersecting room footprints with the roof outline, classifies outers and holes, and returns ArchRoofRegion objects containing loops.

File Access
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public sealed class ArchRoofRegion
{
	public List<List<Vector2>> Loops { get; init; } = new();
	public List<Vector2> Outer => Loops.FirstOrDefault();
}

public sealed class ArchRoofRegionService
{
	readonly ArchBuilding building;
	readonly ArchRoofPart roof;

	public ArchRoofRegionService( ArchBuilding building, ArchRoofPart roof )
	{
		this.building = building;
		this.roof = roof;
	}

	public List<ArchRoofRegion> Resolve()
	{
		var outline = ArchFootprint.Wind( roof.Outline() );
		var rooms = ArchRegion.Footprints( building?.Rooms?.Where( room => room.Floor == roof.Level ), false );

		if ( rooms.Count == 0 )
		{
			return new List<ArchRoofRegion> { new() { Loops = new List<List<Vector2>> { outline } } };
		}

		var occupied = ArchFootprint.Union( rooms );

		// INTERSECTED with the roof's own outline, not clamped to its bounds: a box clamp fills an L's notch
		// straight back in, and a cell straddling the outline either spills the deck past it or throws away a
		// lap the author drew - a walkway's ceiling stopping short of the wall it runs into.
		var loops = ArchFootprint.Intersect( occupied, outline )
			.Where( ArchFootprint.IsSimple )
			.ToList();

		// Standing over no room at all is a FREE section - a porch canopy, whose apron is outside every room
		// by construction - and it covers its own outline, exactly as one on a building with no rooms does.
		// Returning nothing here is why a porch roof could be authored, seated and reported and still build
		// not one face: the clip threw the whole deck away and said nothing.
		if ( loops.Count == 0 )
		{
			return new List<ArchRoofRegion> { new() { Loops = new List<List<Vector2>> { outline } } };
		}
		var outers = ArchFootprint.Outer( loops ).ToList();
		var holes = loops.Where( loop => ArchFootprint.SignedArea( loop ) < 0f ).ToList();
		var regions = outers
			.Select( outer => new ArchRoofRegion
			{
				Loops = new[] { outer }
					.Concat( holes.Where( hole => ArchFloorGen.Contains( outer, hole[0] ) ) )
					.ToList()
			} )
			.ToList();

		return regions;
	}
}