Editor-side foundation generation for architectural floors. Provides functions to find the pad a building stands on, compute foundation lift, and generate foundation geometry (plinths/solids) into an ArchMesh based on room groups, kit and style.
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 IArchCarveHost StandsOn( ArchPlan plan, ArchBuilding building )
{
if ( plan is null || building is null )
{
return null;
}
// Shell, not centroid: a wing off the pad's edge can't drag the building off it.
var shell = building.Rooms.FirstOrDefault( room => room.Floor == 0 && room.HasFootprint );
if ( shell is null )
{
return null;
}
var centre = Vector2.Zero;
foreach ( var point in shell.Footprint )
{
centre += point;
}
return ArchCarveHosts.Under( plan, 0, centre / shell.Footprint.Count );
}
// Derived from rooms, never stored: a saved lift goes stale the moment the foundation switches off. Held for
// the build, because every part of a building rides its lift and finding the pad walks the plan.
public static float GradeLift( ArchPlan plan, ArchBuilding building, ArchKit kit )
{
return ArchBuildMemo.Held( memo => memo.Lifts, building?.Id ?? 0, () => Graded( plan, building, kit ) );
}
static float Graded( ArchPlan plan, ArchBuilding building, ArchKit kit )
{
if ( building is null )
{
return 0f;
}
if ( StandsOn( plan, building ) is { } pad )
{
return MathF.Max( 0f, pad.TopHeight );
}
var raised = building.Rooms.Any( room => room.Floor == 0 && room.HasFloor && room.RaisedFoundation );
return raised ? MathF.Max( 0f, kit.FoundationRise ) : 0f;
}
public static void Foundation( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchKit kit, ArchStyle style, ArchPlan plan = null )
{
var group = Group( new List<ArchRoom> { room }, FloorKey( room, building, style ), kit );
if ( group is not null )
{
Foundation( canvas, group, building, kit, style, plan );
}
}
// ONE solid: a masonry ring decomposes to coplanar face pairs at every corner.
public static void Foundation( ArchMesh canvas, ArchFloorGroup group, ArchBuilding building, ArchKit kit, ArchStyle style, ArchPlan plan = null )
{
var lead = group.Lead;
var depth = MathF.Max( 0f, kit.FoundationDepth );
// A pad is already the plinth: a second footing shows where the coping is shallow.
if ( group.Level != 0 || !lead.HasFloor || depth < 1f || StandsOn( plan, building ) is not null )
{
return;
}
var brush = style.Brush( ArchSurface.Foundation, new[] { lead.Palette, building.Palette } );
var bite = ArchContact.Bite( kit );
var soffit = lead.BaseHeight - kit.FloorThickness;
var bottom = soffit + bite - depth;
var footprint = group.Region.Where( ArchFootprint.IsRectilinear ).ToList();
// A hole through the slab has to go through what the slab STANDS ON, or the plinth fills it from
// underneath and the floor is only open when you look at it from above. It takes the slab's own
// holes rather than its own band's: a hole that stops on the footing is a pit, not a hole.
var wells = Wells( plan, group, building, kit, soffit, lead.BaseHeight );
if ( footprint.Count != group.Region.Count )
{
Solid( canvas, group.Region, wells, bottom, soffit + bite, brush );
return;
}
// Raised foundation: the plinth runs past the slab's edge to just under the finished floor.
var top = lead.RaisedFoundation ? lead.BaseHeight - bite * 2f : soffit + bite;
// Oversize clears half a wall first, or the plinth never projects past it.
var oversize = MathF.Max( 0f, kit.WallThickness ) * 0.5f + MathF.Max( 0f, kit.FoundationOversize );
// The plinth breaks over each mouth: the approach IS the surface across that threshold.
var kinds = ArchKinds.Load();
var foundationMouths = ArchFoundationMouths.Load();
var mouths = group.Rooms
.SelectMany( room => ArchPlanStore.FiledOn( ArchKind.Approach, room, kinds ) )
.Select( approach => foundationMouths.Open( ArchKind.Approach,
new ArchFoundationMouthRequest( approach, kit, oversize + bite ) ) )
.Where( mouth => mouth.Count >= 3 )
.ToList();
var open = mouths.Concat( wells.Select( well => (IReadOnlyList<Vector2>)well.Outline() ) ).ToList();
Plinth( canvas, ArchFootprint.Subtract( ArchFootprint.Grow( footprint, oversize ), ArchFootprint.Union( open ) ),
bottom, top, brush );
}
// Every hole in the slab this footing carries: the stairwells stored on the building and the shafts a
// cut derives, both asked with the SLAB's band so the footing loses exactly what the floor over it did.
public static List<ArchFloorCutout> Wells( ArchPlan plan, ArchFloorGroup group, ArchBuilding building, ArchKit kit, float soffit, float top )
{
var wells = ArchCut.Stored( building, group.Level )
.Concat( ArchCut.Holes( plan, group.Level, kit, soffit, top, building.Id, ArchCutAffects.Foundations ) )
.ToList();
return wells;
}
// Lofted, not boxed: boxes over an L share a face; its caps are never seen.
static void Plinth( ArchMesh canvas, List<List<Vector2>> region, float bottom, float top, ArchBrush brush )
{
var solid = ArchFootprint.Outer( region ).ToList();
if ( solid.Count != region.Count )
{
Solid( canvas, region, null, bottom, top, brush );
return;
}
foreach ( var loop in solid )
{
ArchSlab.Face( canvas, loop.Select( point => new Vector3( point.x, point.y, bottom ) ).ToList(),
Vector3.Up * (top - bottom), brush );
}
}
}