Editor/Floor/ArchFloorGen.Foundation.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 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;
}
// HOW FAR A PLINTH STANDS PROUD of the footprint it carries: half a wall to clear the elevation, then the kit's
// own oversail. ONE answer, because the generator pours the plinth on it and the snap offers its edge - worked
// out twice, the corner an author can plainly see standing on the plinth is not a corner the cursor can reach,
// and a column aimed at it comes to rest on the wall face a whole oversail inside it.
public static float FoundationReach( ArchKit kit ) {
return MathF.Max( 0f, kit?.WallThickness ?? 0f ) * 0.5f + MathF.Max( 0f, kit?.FoundationOversize ?? 0f );
}
public static int FoundationLevel( ArchBuilding building ) {
return building?.Rooms
.Where( room => room.HasFloor && ArchLayerGate.On( room ) )
.Select( room => room.Floor )
.DefaultIfEmpty( 0 )
.Min() ?? 0;
}
// Whether this storey stands a plinth at all - the same gate the generator opens on, asked without a group.
public static bool Founded( ArchPlan plan, ArchBuilding building, ArchKit kit, int level ) {
return level == FoundationLevel( building )
&& MathF.Max( 0f, kit?.FoundationDepth ?? 0f ) >= 1f
&& building?.Rooms.Any( room => room.Floor == level && room.HasFloor && ArchLayerGate.On( room ) ) == true
&& (level < 0 || StandsOn( plan, building ) is null);
}
// The plinth's OUTER RING. The generator breaks it at every approach mouth and every well after this; the ring
// and the reach are what anything asking where its edge runs needs, and both come from here.
public static List<List<Vector2>> FoundationOutline( ArchPlan plan, ArchBuilding building, ArchKit kit, int level ) {
if ( !Founded( plan, building, kit, level ) ) {
return new List<List<Vector2>>();
}
var storey = ArchRegion.Storey( building, level );
return storey.Count == 0 ? new List<List<Vector2>>() : ArchFootprint.Grow( storey, FoundationReach( kit ) );
}
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 != FoundationLevel( building ) || !lead.HasFloor || depth < 1f
|| (group.Level >= 0 && StandsOn( plan, building ) is not null) ) {
return;
}
var brush = style.Brush( ArchSurface.Foundation, new[] { lead.Palette, building.Palette } );
var bite = ArchLap.Bite( kit );
var soffit = lead.BaseHeight - kit.FloorThickness;
var bottom = soffit + bite - depth;
var footprint = group.Region;
// 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 );
// Raised foundation: the plinth runs past the slab's edge to just under the finished floor.
var top = lead.RaisedFoundation ? lead.BaseHeight + bite : soffit + bite;
var oversize = FoundationReach( kit );
// 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 );
var foundationOwner = building.Rooms
.Where( room => room.Floor == FoundationLevel( building ) && room.HasFloor && ArchLayerGate.On( room ) )
.OrderBy( room => room.Id )
.FirstOrDefault();
if ( ReferenceEquals( lead, foundationOwner ) ) {
foreach ( var room in building.Rooms.Where( room => room.Floor < 0 ) ) {
foreach ( var wall in room.Walls ) {
foreach ( var opening in wall.Openings ) {
ArchBasementWell.Build( canvas, ArchBasementWell.Resolve( room, wall, opening, building, kit ), 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 );
}
}
}