Editor utility for generating floor geometry. It computes polygon footprints from room walls or explicit footprints, builds slab shapes including cutouts and recesses, tessellates footprint into merged rectangular cells, and emits mesh polygons to an ArchMesh.
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;
}
}