Editor/Roof/ArchRoofRegionService.cs
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;
	}
}