Editor/Floor/ArchFloorGen.Foundation.cs

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.

File AccessNetworking
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 );
		}
	}
}