Editor/InteriorLayoutBuilder/RoomLayoutGeometryBuilder.Floors.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace ReusableRoomLayout;

public sealed partial class RoomLayoutGeometryBuilder
{
	private const float ThresholdSurfaceOffset = 0.15f;
	private const float FloorSideProbeOffset = 0.01f;

	private static void BuildFloorGeometry(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		IReadOnlyCollection<RoomLayoutFloorSlab> floorSlabs )
	{
		if ( floorSlabs.Count == 0 )
		{
			return;
		}

		var surfaceCells = BuildFloorSurfaceCells( floorSlabs );
		foreach ( var group in surfaceCells.GroupBy( cell => new MaterialSurfaceKey( cell.MaterialPath ?? "", cell.TextureWorldSize ) ) )
		{
			CreateFloorSurface( scene, parent, settings, group.Key.MaterialPath, group.Key.TextureWorldSize, group.ToArray(), surfaceCells );
		}

		foreach ( var slab in floorSlabs )
		{
			CreateFloorCollider( scene, parent, slab, settings.FloorThickness );
		}
	}

	private static void BuildThresholdGeometry(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		IReadOnlyCollection<RoomLayoutThresholdSlab> thresholdSlabs )
	{
		if ( thresholdSlabs.Count == 0 )
		{
			return;
		}

		foreach ( var group in thresholdSlabs.GroupBy( slab => new MaterialSurfaceKey( slab.MaterialPath ?? "", slab.TextureWorldSize ) ) )
		{
			CreateThresholdSurface( scene, parent, settings, group.Key.MaterialPath, group.Key.TextureWorldSize, group.ToArray() );
		}
	}

	private static void BuildRoofGeometry(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		IReadOnlyCollection<RoomLayoutFloorSlab> roofSlabs )
	{
		if ( roofSlabs.Count == 0 )
		{
			return;
		}

		var surfaceCells = BuildFloorSurfaceCells( roofSlabs, preferFirstSlab: true );
		foreach ( var group in surfaceCells.GroupBy( cell => new MaterialSurfaceKey( cell.MaterialPath ?? "", cell.TextureWorldSize ) ) )
		{
			CreateRoofSurface( scene, parent, settings, group.Key.MaterialPath, group.Key.TextureWorldSize, group.ToArray(), surfaceCells );
		}

		foreach ( var slab in roofSlabs )
		{
			CreateRoofCollider( scene, parent, slab, settings );
		}
	}

	private static void CreateThresholdSurface(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		string materialPath,
		float textureWorldSize,
		IReadOnlyCollection<RoomLayoutThresholdSlab> thresholdSlabs )
	{
		var gameObject = scene.CreateObject( true );
		gameObject.Name = "Combined Threshold Surface";
		gameObject.SetParent( parent, false );

		var meshComponent = gameObject.Components.Create<MeshComponent>( false );
		meshComponent.Mesh = BuildThresholdSurfaceMesh( thresholdSlabs, textureWorldSize, MaterialFor( settings, RoomLayoutSurface.Threshold, materialPath ) );
		meshComponent.Color = Color.White;
		meshComponent.SmoothingAngle = 0.0f;
		meshComponent.Enabled = true;
		meshComponent.RebuildMesh();
	}

	private static void CreateFloorSurface(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		string materialPath,
		float textureWorldSize,
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> surfaceCells,
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> allSurfaceCells )
	{
		var gameObject = scene.CreateObject( true );
		gameObject.Name = "Combined Floor Surface";
		gameObject.SetParent( parent, false );

		var meshComponent = gameObject.Components.Create<MeshComponent>( false );
		meshComponent.Mesh = BuildFloorSurfaceMesh( surfaceCells, allSurfaceCells, textureWorldSize, settings.FloorThickness, MaterialFor( settings, RoomLayoutSurface.Floor, materialPath ) );
		meshComponent.Color = Color.White;
		meshComponent.SmoothingAngle = 0.0f;
		meshComponent.Enabled = true;
		meshComponent.RebuildMesh();
	}

	private static void CreateRoofSurface(
		Scene scene,
		GameObject parent,
		RoomLayoutSettings settings,
		string materialPath,
		float textureWorldSize,
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> surfaceCells,
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> allSurfaceCells )
	{
		var gameObject = scene.CreateObject( true );
		gameObject.Name = "Combined Roof Surface";
		gameObject.SetParent( parent, false );

		var meshComponent = gameObject.Components.Create<MeshComponent>( false );
		meshComponent.Mesh = BuildRoofSurfaceMesh(
			surfaceCells,
			allSurfaceCells,
			textureWorldSize,
			RoofBottomZ( settings ),
			settings.RoofThickness,
			MaterialFor( settings, RoomLayoutSurface.Roof, materialPath ) );
		meshComponent.Color = Color.White;
		meshComponent.SmoothingAngle = 0.0f;
		meshComponent.Enabled = true;
		meshComponent.RebuildMesh();
	}

	private static PolygonMesh BuildFloorSurfaceMesh(
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> surfaceCells,
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> allSurfaceCells,
		float textureWorldSize,
		float floorThickness,
		Material material )
	{
		var mesh = new PolygonMesh();
		var vertices = new Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle>();
		var textureSize = MathF.Max( 1.0f, textureWorldSize );
		var bottomZ = -MathF.Max( 0.5f, floorThickness );

		foreach ( var cell in surfaceCells )
		{
			var rect = cell.Rect;
			AddFloorTopFace(
				mesh,
				vertices,
				material,
				textureSize,
				new Vector2( rect.X, rect.Y ),
				new Vector2( rect.X + rect.Width, rect.Y + rect.Height ) );
			AddFloorBottomFace(
				mesh,
				vertices,
				material,
				textureSize,
				new Vector2( rect.X, rect.Y ),
				new Vector2( rect.X + rect.Width, rect.Y + rect.Height ),
				bottomZ );

			AddFloorSideFaces(
				mesh,
				vertices,
				material,
				textureSize,
				rect,
				bottomZ,
				0.0f,
				allSurfaceCells );
		}

		mesh.ComputeFaceTextureParametersFromCoordinates();
		mesh.SetSmoothingAngle( 0.0f );
		return mesh;
	}

	private static PolygonMesh BuildRoofSurfaceMesh(
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> surfaceCells,
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> allSurfaceCells,
		float textureWorldSize,
		float roofBottomZ,
		float roofThickness,
		Material material )
	{
		var mesh = new PolygonMesh();
		var vertices = new Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle>();
		var textureSize = MathF.Max( 1.0f, textureWorldSize );
		var bottomZ = MathF.Max( 0.0f, roofBottomZ );
		var topZ = bottomZ + MathF.Max( 0.5f, roofThickness );

		foreach ( var cell in surfaceCells )
		{
			var rect = cell.Rect;
			AddFloorBottomFace(
				mesh,
				vertices,
				material,
				textureSize,
				new Vector2( rect.X, rect.Y ),
				new Vector2( rect.X + rect.Width, rect.Y + rect.Height ),
				bottomZ );
			AddFloorTopFace(
				mesh,
				vertices,
				material,
				textureSize,
				new Vector2( rect.X, rect.Y ),
				new Vector2( rect.X + rect.Width, rect.Y + rect.Height ),
				topZ );

			AddFloorSideFaces(
				mesh,
				vertices,
				material,
				textureSize,
				rect,
				bottomZ,
				topZ,
				allSurfaceCells );
		}

		mesh.ComputeFaceTextureParametersFromCoordinates();
		mesh.SetSmoothingAngle( 0.0f );
		return mesh;
	}

	private static PolygonMesh BuildThresholdSurfaceMesh(
		IReadOnlyCollection<RoomLayoutThresholdSlab> thresholdSlabs,
		float textureWorldSize,
		Material material )
	{
		var mesh = new PolygonMesh();
		var vertices = new Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle>();
		var textureSize = MathF.Max( 1.0f, textureWorldSize );

		foreach ( var slab in thresholdSlabs )
		{
			var rect = slab.Rect;
			AddFloorTopFace(
				mesh,
				vertices,
				material,
				textureSize,
				new Vector2( rect.X, rect.Y ),
				new Vector2( rect.X + rect.Width, rect.Y + rect.Height ),
				ThresholdSurfaceOffset );
		}

		mesh.ComputeFaceTextureParametersFromCoordinates();
		mesh.SetSmoothingAngle( 0.0f );
		return mesh;
	}

	private static void AddFloorTopFace(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		Vector2 min,
		Vector2 max,
		float z = 0.0f )
	{
		var faceVertices = new[]
		{
			FloorVertex( mesh, vertices, min.x, min.y, z ),
			FloorVertex( mesh, vertices, max.x, min.y, z ),
			FloorVertex( mesh, vertices, max.x, max.y, z ),
			FloorVertex( mesh, vertices, min.x, max.y, z )
		};
		var face = mesh.AddFace( faceVertices );
		mesh.SetFaceMaterial( face, material );
		mesh.SetFaceTextureCoords( face, new[]
		{
			new Vector2( min.x, min.y ) / textureWorldSize,
			new Vector2( max.x, min.y ) / textureWorldSize,
			new Vector2( max.x, max.y ) / textureWorldSize,
			new Vector2( min.x, max.y ) / textureWorldSize
		} );
	}

	private static void AddFloorBottomFace(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		Vector2 min,
		Vector2 max,
		float z )
	{
		var faceVertices = new[]
		{
			FloorVertex( mesh, vertices, min.x, min.y, z ),
			FloorVertex( mesh, vertices, min.x, max.y, z ),
			FloorVertex( mesh, vertices, max.x, max.y, z ),
			FloorVertex( mesh, vertices, max.x, min.y, z )
		};
		var face = mesh.AddFace( faceVertices );
		mesh.SetFaceMaterial( face, material );
		mesh.SetFaceTextureCoords( face, new[]
		{
			new Vector2( min.x, min.y ) / textureWorldSize,
			new Vector2( min.x, max.y ) / textureWorldSize,
			new Vector2( max.x, max.y ) / textureWorldSize,
			new Vector2( max.x, min.y ) / textureWorldSize
		} );
	}

	private static void AddFloorSideFaces(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		RoomLayoutRect rect,
		float bottomZ,
		float topZ,
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> allSurfaceCells )
	{
		var min = new Vector2( rect.X, rect.Y );
		var max = new Vector2( rect.X + rect.Width, rect.Y + rect.Height );

		if ( !HasFloorNeighbor( allSurfaceCells, rect, -FloorSideProbeOffset, 0.0f ) )
		{
			AddFloorWestFace( mesh, vertices, material, textureWorldSize, min, max, bottomZ, topZ );
		}

		if ( !HasFloorNeighbor( allSurfaceCells, rect, FloorSideProbeOffset, 0.0f ) )
		{
			AddFloorEastFace( mesh, vertices, material, textureWorldSize, min, max, bottomZ, topZ );
		}

		if ( !HasFloorNeighbor( allSurfaceCells, rect, 0.0f, -FloorSideProbeOffset ) )
		{
			AddFloorSouthFace( mesh, vertices, material, textureWorldSize, min, max, bottomZ, topZ );
		}

		if ( !HasFloorNeighbor( allSurfaceCells, rect, 0.0f, FloorSideProbeOffset ) )
		{
			AddFloorNorthFace( mesh, vertices, material, textureWorldSize, min, max, bottomZ, topZ );
		}
	}

	private static bool HasFloorNeighbor(
		IReadOnlyCollection<RoomLayoutFloorSurfaceCell> allSurfaceCells,
		RoomLayoutRect rect,
		float offsetX,
		float offsetY )
	{
		var probe = new Vector2( rect.Center.x + offsetX, rect.Center.y + offsetY );
		if ( offsetX < 0.0f )
		{
			probe.x = rect.X + offsetX;
		}
		else if ( offsetX > 0.0f )
		{
			probe.x = rect.X + rect.Width + offsetX;
		}

		if ( offsetY < 0.0f )
		{
			probe.y = rect.Y + offsetY;
		}
		else if ( offsetY > 0.0f )
		{
			probe.y = rect.Y + rect.Height + offsetY;
		}

		return TryGetFloorCell( allSurfaceCells, probe, out _ );
	}

	private static void AddFloorWestFace(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		Vector2 min,
		Vector2 max,
		float zMin,
		float zMax )
	{
		AddFloorVerticalFace( mesh, vertices, material, textureWorldSize, new[]
		{
			FloorVertex( mesh, vertices, min.x, min.y, zMin ),
			FloorVertex( mesh, vertices, min.x, min.y, zMax ),
			FloorVertex( mesh, vertices, min.x, max.y, zMax ),
			FloorVertex( mesh, vertices, min.x, max.y, zMin )
		}, BoxUvPlane.YZ );
	}

	private static void AddFloorEastFace(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		Vector2 min,
		Vector2 max,
		float zMin,
		float zMax )
	{
		AddFloorVerticalFace( mesh, vertices, material, textureWorldSize, new[]
		{
			FloorVertex( mesh, vertices, max.x, min.y, zMin ),
			FloorVertex( mesh, vertices, max.x, max.y, zMin ),
			FloorVertex( mesh, vertices, max.x, max.y, zMax ),
			FloorVertex( mesh, vertices, max.x, min.y, zMax )
		}, BoxUvPlane.YZ );
	}

	private static void AddFloorSouthFace(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		Vector2 min,
		Vector2 max,
		float zMin,
		float zMax )
	{
		AddFloorVerticalFace( mesh, vertices, material, textureWorldSize, new[]
		{
			FloorVertex( mesh, vertices, min.x, min.y, zMin ),
			FloorVertex( mesh, vertices, max.x, min.y, zMin ),
			FloorVertex( mesh, vertices, max.x, min.y, zMax ),
			FloorVertex( mesh, vertices, min.x, min.y, zMax )
		}, BoxUvPlane.XZ );
	}

	private static void AddFloorNorthFace(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		Vector2 min,
		Vector2 max,
		float zMin,
		float zMax )
	{
		AddFloorVerticalFace( mesh, vertices, material, textureWorldSize, new[]
		{
			FloorVertex( mesh, vertices, min.x, max.y, zMin ),
			FloorVertex( mesh, vertices, min.x, max.y, zMax ),
			FloorVertex( mesh, vertices, max.x, max.y, zMax ),
			FloorVertex( mesh, vertices, max.x, max.y, zMin )
		}, BoxUvPlane.XZ );
	}

	private static void AddFloorVerticalFace(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		Material material,
		float textureWorldSize,
		HalfEdgeMesh.VertexHandle[] faceVertices,
		BoxUvPlane uvPlane )
	{
		var face = mesh.AddFace( faceVertices );
		mesh.SetFaceMaterial( face, material );

		var uvs = new Vector2[faceVertices.Length];
		for ( var i = 0; i < faceVertices.Length; i++ )
		{
			uvs[i] = ProjectBoxUv( mesh.GetVertexPosition( faceVertices[i] ), textureWorldSize, uvPlane );
		}

		mesh.SetFaceTextureCoords( face, uvs );
	}

	private static HalfEdgeMesh.VertexHandle FloorVertex(
		PolygonMesh mesh,
		Dictionary<FloorVertexKey, HalfEdgeMesh.VertexHandle> vertices,
		float x,
		float y,
		float z )
	{
		var key = new FloorVertexKey( QuantizeCoordinate( x ), QuantizeCoordinate( y ), QuantizeCoordinate( z ) );
		if ( vertices.TryGetValue( key, out var vertex ) )
		{
			return vertex;
		}

		vertex = mesh.AddVertex( new Vector3( x, y, z ) );
		vertices[key] = vertex;
		return vertex;
	}

	private static IReadOnlyList<RoomLayoutFloorSurfaceCell> BuildFloorSurfaceCells( IReadOnlyCollection<RoomLayoutFloorSlab> floorSlabs, bool preferFirstSlab = false )
	{
		var xs = new List<float>();
		var ys = new List<float>();
		foreach ( var slab in floorSlabs )
		{
			AddUniqueCoordinate( xs, slab.Rect.X );
			AddUniqueCoordinate( xs, slab.Rect.X + slab.Rect.Width );
			AddUniqueCoordinate( ys, slab.Rect.Y );
			AddUniqueCoordinate( ys, slab.Rect.Y + slab.Rect.Height );
		}

		xs.Sort();
		ys.Sort();

		var cells = new List<RoomLayoutFloorSurfaceCell>();
		for ( var x = 0; x < xs.Count - 1; x++ )
		{
			for ( var y = 0; y < ys.Count - 1; y++ )
			{
				var min = new Vector2( xs[x], ys[y] );
				var max = new Vector2( xs[x + 1], ys[y + 1] );
				if ( !TryGetFloorCellSlab( floorSlabs, (min + max) * 0.5f, preferFirstSlab, out var slab ) )
				{
					continue;
				}

				cells.Add( new RoomLayoutFloorSurfaceCell(
					new RoomLayoutRect( min.x, min.y, max.x - min.x, max.y - min.y ),
					slab.MaterialPath ?? "",
					slab.TextureWorldSize ) );
			}
		}

		return cells;
	}

	private static bool TryGetFloorCellSlab( IReadOnlyCollection<RoomLayoutFloorSlab> floorSlabs, Vector2 point, bool preferFirstSlab, out RoomLayoutFloorSlab coveringSlab )
	{
		var found = false;
		coveringSlab = default;
		foreach ( var slab in floorSlabs )
		{
			var rect = slab.Rect;
			if ( point.x >= rect.X && point.x <= rect.X + rect.Width &&
				point.y >= rect.Y && point.y <= rect.Y + rect.Height )
			{
				coveringSlab = slab;
				found = true;
				if ( preferFirstSlab )
				{
					return true;
				}
			}
		}

		return found;
	}

	private static bool TryGetFloorCell( IReadOnlyCollection<RoomLayoutFloorSurfaceCell> surfaceCells, Vector2 point, out RoomLayoutFloorSurfaceCell coveringCell )
	{
		foreach ( var cell in surfaceCells )
		{
			var rect = cell.Rect;
			if ( point.x >= rect.X && point.x <= rect.X + rect.Width &&
				point.y >= rect.Y && point.y <= rect.Y + rect.Height )
			{
				coveringCell = cell;
				return true;
			}
		}

		coveringCell = default;
		return false;
	}

	private static void AddFloorSlab( ICollection<RoomLayoutFloorSlab> floorSlabs, string name, RoomLayoutRect rect, string materialPath, float textureWorldSize )
	{
		if ( rect.Width < 1.0f || rect.Height < 1.0f )
		{
			return;
		}

		floorSlabs.Add( new RoomLayoutFloorSlab( name, rect, materialPath ?? "", textureWorldSize ) );
	}

	private static void AddStructuralFloorSlab(
		ICollection<RoomLayoutFloorSlab> floorSlabs,
		string name,
		RoomLayoutRect rect,
		RoomLayoutSettings settings,
		string materialPath,
		float textureWorldSize )
	{
		AddFloorSlab( floorSlabs, name, StructuralFootprintRect( rect, settings ), materialPath, textureWorldSize );
	}

	private static void AddRoofSlab(
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		string name,
		RoomLayoutRect rect,
		string materialPath,
		float textureWorldSize )
	{
		AddFloorSlab( roofSlabs, name, rect, materialPath, textureWorldSize );
	}

	private static void AddStructuralRoofSlab(
		ICollection<RoomLayoutFloorSlab> roofSlabs,
		string name,
		RoomLayoutRect rect,
		RoomLayoutSettings settings,
		string materialPath,
		float textureWorldSize )
	{
		AddFloorSlab( roofSlabs, name, StructuralFootprintRect( rect, settings ), materialPath, textureWorldSize );
	}

	private static IReadOnlyList<RoomLayoutFloorSlab> ClipFloorSlabs(
		IReadOnlyCollection<RoomLayoutFloorSlab> floorSlabs,
		IReadOnlyCollection<RoomLayoutRect> cutouts )
	{
		if ( floorSlabs.Count == 0 || cutouts.Count == 0 )
		{
			return floorSlabs.ToArray();
		}

		var current = floorSlabs.ToList();
		foreach ( var cutout in cutouts.Where( cutout => cutout.Width >= 1.0f && cutout.Height >= 1.0f ) )
		{
			var next = new List<RoomLayoutFloorSlab>();
			foreach ( var slab in current )
			{
				next.AddRange( SubtractFloorCutout( slab, cutout ) );
			}

			current = next;
		}

		return current;
	}

	private static IReadOnlyList<RoomLayoutThresholdSlab> ClipThresholdSlabs(
		IReadOnlyCollection<RoomLayoutThresholdSlab> thresholdSlabs,
		IReadOnlyCollection<RoomLayoutRect> cutouts )
	{
		if ( thresholdSlabs.Count == 0 || cutouts.Count == 0 )
		{
			return thresholdSlabs.ToArray();
		}

		var current = thresholdSlabs.ToList();
		foreach ( var cutout in cutouts.Where( cutout => cutout.Width >= 1.0f && cutout.Height >= 1.0f ) )
		{
			var next = new List<RoomLayoutThresholdSlab>();
			foreach ( var slab in current )
			{
				next.AddRange( SubtractThresholdCutout( slab, cutout ) );
			}

			current = next;
		}

		return current;
	}

	private static IReadOnlyList<RoomLayoutFloorSlab> SubtractFloorCutout( RoomLayoutFloorSlab slab, RoomLayoutRect cutout )
	{
		if ( !RectsOverlap( slab.Rect, cutout ) )
		{
			return new[] { slab };
		}

		var fragments = new List<RoomLayoutFloorSlab>();
		foreach ( var fragment in SubtractRect( slab.Rect, cutout ) )
		{
			fragments.Add( new RoomLayoutFloorSlab(
				$"{slab.Name} Cut {fragments.Count:00}",
				fragment,
				slab.MaterialPath,
				slab.TextureWorldSize ) );
		}

		return fragments;
	}

	private static IReadOnlyList<RoomLayoutThresholdSlab> SubtractThresholdCutout( RoomLayoutThresholdSlab slab, RoomLayoutRect cutout )
	{
		if ( !RectsOverlap( slab.Rect, cutout ) )
		{
			return new[] { slab };
		}

		var fragments = new List<RoomLayoutThresholdSlab>();
		foreach ( var fragment in SubtractRect( slab.Rect, cutout ) )
		{
			fragments.Add( new RoomLayoutThresholdSlab(
				$"{slab.Name} Cut {fragments.Count:00}",
				fragment,
				slab.MaterialPath,
				slab.TextureWorldSize ) );
		}

		return fragments;
	}

	private static bool RectsOverlap( RoomLayoutRect a, RoomLayoutRect b )
	{
		return a.X < b.X + b.Width &&
			a.X + a.Width > b.X &&
			a.Y < b.Y + b.Height &&
			a.Y + a.Height > b.Y;
	}

	private static IReadOnlyList<RoomLayoutRect> SubtractRect( RoomLayoutRect rect, RoomLayoutRect cutout )
	{
		var rectMaxX = rect.X + rect.Width;
		var rectMaxY = rect.Y + rect.Height;
		var cutoutMaxX = cutout.X + cutout.Width;
		var cutoutMaxY = cutout.Y + cutout.Height;
		var minX = MathF.Max( rect.X, cutout.X );
		var minY = MathF.Max( rect.Y, cutout.Y );
		var maxX = MathF.Min( rectMaxX, cutoutMaxX );
		var maxY = MathF.Min( rectMaxY, cutoutMaxY );

		if ( maxX <= minX || maxY <= minY )
		{
			return new[] { rect };
		}

		var fragments = new List<RoomLayoutRect>();
		AddRectFragment( fragments, new RoomLayoutRect( rect.X, rect.Y, rect.Width, minY - rect.Y ) );
		AddRectFragment( fragments, new RoomLayoutRect( rect.X, maxY, rect.Width, rectMaxY - maxY ) );
		AddRectFragment( fragments, new RoomLayoutRect( rect.X, minY, minX - rect.X, maxY - minY ) );
		AddRectFragment( fragments, new RoomLayoutRect( maxX, minY, rectMaxX - maxX, maxY - minY ) );
		return fragments;
	}

	private static void AddRectFragment( ICollection<RoomLayoutRect> fragments, RoomLayoutRect rect )
	{
		if ( rect.Width >= 1.0f && rect.Height >= 1.0f )
		{
			fragments.Add( rect );
		}
	}

	private static IReadOnlyList<RoomLayoutFloorSlab> ExposedRoofSlabs(
		IReadOnlyCollection<RoomLayoutFloorSlab> roofSlabs,
		IReadOnlyCollection<RoomLayoutRect> coverageRects )
	{
		if ( roofSlabs.Count == 0 || coverageRects.Count == 0 )
		{
			return roofSlabs.ToArray();
		}

		var xs = new List<float>();
		var ys = new List<float>();
		foreach ( var slab in roofSlabs )
		{
			AddUniqueCoordinate( xs, slab.Rect.X );
			AddUniqueCoordinate( xs, slab.Rect.X + slab.Rect.Width );
			AddUniqueCoordinate( ys, slab.Rect.Y );
			AddUniqueCoordinate( ys, slab.Rect.Y + slab.Rect.Height );
		}

		foreach ( var rect in coverageRects )
		{
			AddUniqueCoordinate( xs, rect.X );
			AddUniqueCoordinate( xs, rect.X + rect.Width );
			AddUniqueCoordinate( ys, rect.Y );
			AddUniqueCoordinate( ys, rect.Y + rect.Height );
		}

		xs.Sort();
		ys.Sort();

		var exposed = new List<RoomLayoutFloorSlab>();
		var index = 0;
		for ( var x = 0; x < xs.Count - 1; x++ )
		{
			for ( var y = 0; y < ys.Count - 1; y++ )
			{
				var min = new Vector2( xs[x], ys[y] );
				var max = new Vector2( xs[x + 1], ys[y + 1] );
				var center = (min + max) * 0.5f;
				if ( IsCoveredByRect( coverageRects, center ) ||
					!TryGetFloorCellSlab( roofSlabs, center, preferFirstSlab: false, out var slab ) )
				{
					continue;
				}

				exposed.Add( new RoomLayoutFloorSlab(
					$"{slab.Name} {index:00}",
					new RoomLayoutRect( min.x, min.y, max.x - min.x, max.y - min.y ),
					slab.MaterialPath,
					slab.TextureWorldSize ) );
				index++;
			}
		}

		return exposed;
	}

	private static IReadOnlyCollection<RoomLayoutRect> HigherFloorCoverageRects( RoomLayoutDocument document, int floor )
	{
		var coverage = new List<RoomLayoutFloorSlab>();
		foreach ( var room in document.Rooms.Where( room => document.FloorFor( room ) > floor ) )
		{
			AddStructuralFloorSlab( coverage, "", room.Bounds, document.Settings, "", 0.0f );
		}

		foreach ( var corridor in document.Corridors.Where( corridor => document.FloorFor( corridor ) > floor && document.CorridorDoorsAreOnFloor( corridor ) ) )
		{
			AddCorridorCoverageSlabs( document, corridor, coverage );
		}

		return coverage.Select( slab => slab.Rect ).ToArray();
	}

	private static void AddCorridorCoverageSlabs(
		RoomLayoutDocument document,
		RoomLayoutCorridor corridor,
		ICollection<RoomLayoutFloorSlab> floorSlabs )
	{
		if ( !TryGetDoorPoint( document, corridor.StartDoorId, out var start ) ||
			!TryGetDoorPoint( document, corridor.EndDoorId, out var end ) )
		{
			return;
		}

		var settings = document.Settings;
		var width = CorridorClearWidth( settings, corridor, start, end );
		var roofSlabs = new List<RoomLayoutFloorSlab>();
		var thresholdSlabs = new List<RoomLayoutThresholdSlab>();
		var footprintRects = new List<RoomLayoutRect>();

		BuildDoorThreshold(
			floorSlabs,
			roofSlabs,
			thresholdSlabs,
			settings,
			corridor.Id,
			"Start",
			start,
			width,
			"",
			0.0f,
			"",
			0.0f,
			"",
			0.0f );
		BuildDoorThreshold(
			floorSlabs,
			roofSlabs,
			thresholdSlabs,
			settings,
			corridor.Id,
			"End",
			end,
			width,
			"",
			0.0f,
			"",
			0.0f,
			"",
			0.0f );

		var points = CorridorPath( settings, corridor, start, end, width );
		for ( var i = 0; i < points.Count - 1; i++ )
		{
			BuildCorridorSegmentFloor(
				floorSlabs,
				roofSlabs,
				settings,
				footprintRects,
				corridor.Id,
				i,
				points[i],
				points[i + 1],
				width,
				"",
				0.0f,
				"",
				0.0f );
		}

		BuildCorridorBendFloors( floorSlabs, roofSlabs, settings, footprintRects, corridor.Id, points, width, "", 0.0f, "", 0.0f );
		BuildCorridorOpeningFloors( floorSlabs, roofSlabs, settings, document, corridor, points, width, "", 0.0f, "", 0.0f );
	}

	private static bool IsCoveredByRect( IReadOnlyCollection<RoomLayoutRect> rects, Vector2 point )
	{
		foreach ( var rect in rects )
		{
			if ( point.x >= rect.X && point.x <= rect.X + rect.Width &&
				point.y >= rect.Y && point.y <= rect.Y + rect.Height )
			{
				return true;
			}
		}

		return false;
	}

	private static RoomLayoutRect StructuralFootprintRect( RoomLayoutRect rect, RoomLayoutSettings settings )
	{
		var overlap = MathF.Max( 0.0f, settings.WallThickness );
		return new RoomLayoutRect( rect.X - overlap, rect.Y - overlap, rect.Width + overlap * 2.0f, rect.Height + overlap * 2.0f );
	}

	private static void AddThresholdSlab( ICollection<RoomLayoutThresholdSlab> thresholdSlabs, string name, RoomLayoutRect rect, string materialPath, float textureWorldSize )
	{
		if ( rect.Width < 1.0f || rect.Height < 1.0f )
		{
			return;
		}

		thresholdSlabs.Add( new RoomLayoutThresholdSlab( name, rect, materialPath ?? "", textureWorldSize ) );
	}

	private static void CreateFloorCollider( Scene scene, GameObject parent, RoomLayoutFloorSlab slab, float floorThickness )
	{
		var colliderObject = scene.CreateObject( true );
		colliderObject.Name = $"{slab.Name} Collider";
		colliderObject.SetParent( parent, false );
		colliderObject.LocalPosition = new Vector3( slab.Rect.Center.x, slab.Rect.Center.y, -floorThickness * 0.5f );

		var collider = colliderObject.Components.Create<BoxCollider>();
		collider.Scale = new Vector3( slab.Rect.Width, slab.Rect.Height, floorThickness );
		collider.Static = true;
	}

	private static void CreateRoofCollider( Scene scene, GameObject parent, RoomLayoutFloorSlab slab, RoomLayoutSettings settings )
	{
		var roofThickness = MathF.Max( 0.5f, settings.RoofThickness );
		var bottomZ = RoofBottomZ( settings );
		var colliderObject = scene.CreateObject( true );
		colliderObject.Name = $"{slab.Name} Collider";
		colliderObject.SetParent( parent, false );
		colliderObject.LocalPosition = new Vector3( slab.Rect.Center.x, slab.Rect.Center.y, bottomZ + roofThickness * 0.5f );

		var collider = colliderObject.Components.Create<BoxCollider>();
		collider.Scale = new Vector3( slab.Rect.Width, slab.Rect.Height, roofThickness );
		collider.Static = true;
	}

	private static float RoofBottomZ( RoomLayoutSettings settings )
	{
		return MathF.Max( 0.0f, settings.WallHeight ) + MathF.Max( 0.0f, settings.WallCapHeight );
	}

	private readonly record struct RoomLayoutFloorSlab( string Name, RoomLayoutRect Rect, string MaterialPath, float TextureWorldSize );
	private readonly record struct RoomLayoutFloorSurfaceCell( RoomLayoutRect Rect, string MaterialPath, float TextureWorldSize );
	private readonly record struct RoomLayoutThresholdSlab( string Name, RoomLayoutRect Rect, string MaterialPath, float TextureWorldSize );
	private readonly record struct MaterialSurfaceKey( string MaterialPath, float TextureWorldSize );
	private readonly record struct FloorVertexKey( int X, int Y, int Z );
}