Editor/Floor/ArchFloorGen.Slab.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using Sandbox;

namespace Sunless.Architecture;

public static partial class ArchFloorGen {
	public static List<Vector2> Footprint( ArchRoom room ) {
		if ( room.HasFootprint ) {
			return ArchFootprint.Wind( room.Footprint.ToList() );
		}

		return ArchFootprint.Wind( FromWalls( room ) );
	}

	static List<Vector2> FromWalls( ArchRoom room ) {
		var points = new List<Vector2>();

		foreach ( var wall in room.Walls ) {
			if ( points.Count == 0 || (points[^1] - wall.Start).Length > 0.5f ) {
				points.Add( wall.Start );
			}

			points.Add( wall.End );
		}

		if ( points.Count >= 2 && (points[0] - points[^1]).Length < 0.5f ) {
			points.RemoveAt( points.Count - 1 );
		}

		return points;
	}

	public static bool Contains( IReadOnlyList<Vector2> polygon, Vector2 point ) => ArchFootprint.Contains( polygon, point );

	public static void Slab(
		ArchMesh canvas,
		IReadOnlyList<Vector2> footprint,
		IReadOnlyList<ArchFloorCutout> cutouts,
		float bottom,
		float top,
		ArchBrush brush,
		IReadOnlyList<ArchCarveVolume> recesses = null ) {
		if ( footprint.Count < 3 || top - bottom < 0.05f ) {
			return;
		}

		var shape = SlabShape( footprint, cutouts, bottom, top, recesses );

		using var welding = canvas.Welding();

		foreach ( var face in shape.Faces ) {
			canvas.Polygon( face.Points, brush );
		}
	}

	// A cutout is a hole in PLAN and takes the whole band; a recess carries its own band, so what is left
	// over or under it stays standing. Both go through the one carve, or a coffer and a stairwell would
	// disagree about what a hole is.
	public static ArchCarveShape SlabShape(
		IReadOnlyList<Vector2> footprint,
		IReadOnlyList<ArchFloorCutout> cutouts,
		float bottom,
		float top,
		IReadOnlyList<ArchCarveVolume> recesses = null ) {
		var carve = ArchCarve.Prism( footprint, bottom, top );

		foreach ( var cutout in cutouts ?? Array.Empty<ArchFloorCutout>() ) {
			carve.Less( ArchCarveVolume.Over( cutout.Outline(), bottom - 1f, top + 1f ).Breaking( cutout.Break ) );
		}

		foreach ( var recess in recesses ?? Array.Empty<ArchCarveVolume>() ) {
			carve.Less( recess );
		}

		return carve.Resolve();
	}

	readonly struct Cell {
		public Vector2 Min { get; init; }
		public Vector2 Max { get; init; }
	}

	static IEnumerable<Cell> Cells( IReadOnlyList<Vector2> footprint, IReadOnlyList<ArchFloorCutout> cutouts ) {
		var xs = new List<float>();
		var ys = new List<float>();

		foreach ( var point in footprint ) {
			xs.Add( point.x );
			ys.Add( point.y );
		}

		foreach ( var cutout in cutouts ) {
			xs.Add( cutout.Min.x );
			xs.Add( cutout.Max.x );
			ys.Add( cutout.Min.y );
			ys.Add( cutout.Max.y );

			// A turned flight's loop corners become grid lines, or its hole comes out square.
			foreach ( var point in cutout.Loop ) {
				xs.Add( point.x );
				ys.Add( point.y );
			}
		}

		var boundsMinX = footprint.Min( point => point.x );
		var boundsMaxX = footprint.Max( point => point.x );
		var boundsMinY = footprint.Min( point => point.y );
		var boundsMaxY = footprint.Max( point => point.y );

		xs = xs.Where( value => value > boundsMinX - 0.01f && value < boundsMaxX + 0.01f ).Distinct().OrderBy( value => value ).ToList();
		ys = ys.Where( value => value > boundsMinY - 0.01f && value < boundsMaxY + 0.01f ).Distinct().OrderBy( value => value ).ToList();

		var columns = xs.Count - 1;
		var rows = ys.Count - 1;
		var solid = new bool[columns * rows];

		for ( var ix = 0; ix < columns; ix++ ) {
			for ( var iy = 0; iy < rows; iy++ ) {
				var min = new Vector2( xs[ix], ys[iy] );
				var max = new Vector2( xs[ix + 1], ys[iy + 1] );

				if ( max.x - min.x < 0.05f || max.y - min.y < 0.05f ) {
					continue;
				}

				var centre = (min + max) * 0.5f;

				solid[iy * columns + ix] = ArchFootprint.Covered( new[] { footprint }, cutouts, centre );
			}
		}

		// Merged into the fewest boxes: shared faces are internal coplanar pairs.
		for ( var iy = 0; iy < rows; iy++ ) {
			for ( var ix = 0; ix < columns; ix++ ) {
				if ( !solid[iy * columns + ix] ) {
					continue;
				}

				var width = 1;

				while ( ix + width < columns && solid[iy * columns + ix + width] ) {
					width++;
				}

				var height = 1;

				while ( iy + height < rows && Spans( solid, columns, ix, width, iy + height ) ) {
					height++;
				}

				for ( var y = iy; y < iy + height; y++ ) {
					for ( var x = ix; x < ix + width; x++ ) {
						solid[y * columns + x] = false;
					}
				}

				yield return new Cell {
					Min = new Vector2( xs[ix], ys[iy] ),
					Max = new Vector2( xs[ix + width], ys[iy + height] )
				};
			}
		}
	}

	static bool Spans( bool[] solid, int columns, int ix, int width, int iy ) {
		for ( var x = ix; x < ix + width; x++ ) {
			if ( !solid[iy * columns + x] ) {
				return false;
			}
		}

		return true;
	}
}