Editor/Porch/ArchOverhangGen.cs
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Nothing under it carries it, so it needs a boxed underside of its own.
public static class ArchOverhangGen {
const float Walkable = 24f;
public static void Build( ArchMesh canvas, ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, ArchGround ground ) {
new ArchOverhangGenerationService( room, building, plan, kit, style, ground ).Create( canvas );
}
// How deep the boxed underside hangs below the slab it belongs to.
public static float Depth( ArchKit kit ) => MathF.Max( 3f, kit.FasciaHeight );
// THE FACE ANYTHING UNDER AN OVERHANG MEETS FIRST, and it is not the slab's own underside - the box hangs a
// fascia depth lower than that. A column that measured the slab stops inside the very thing it is propping,
// and a cursor that measured it lands eight inches above the surface being pointed at.
public static float Soffit( ArchRoom room, ArchKit kit ) => room.BaseHeight - kit.FloorThickness - Depth( kit );
// The post a storey props itself on, which is a PILLAR - the same section, footing and dressing the Pillars
// tool authors, so a prop under an overhang and a column in a colonnade are one thing dressed twice.
public static ArchPillarPart Section( ArchRoom room ) {
var section = (room?.OverhangPost ?? new ArchPillarPart()).Section();
section.Placement = PillarPlacement.Freestanding;
section.Span = PillarSpan.None;
return section;
}
// Walkable depth needs carrying: posts land on open-air edges, corners first, then evenly. Set IN by their own
// seat, and the shell they lean on grown by the same, or a post on the corner of the loop stands three
// quarters of itself out in the air and the run along the building grows a row it never needed.
internal static void Posts( ArchMesh canvas, List<List<Vector2>> region, ArchOverhangSupportService support, ArchRoom room, ArchKit kit, float top, ArchBrush shaft, ArchBrush cap ) {
var section = Section( room );
var seat = MathF.Max( 1f, section.Seat );
if ( ArchFootprint.Shrink( region, MathF.Max( Walkable, seat * 2f ) * 0.5f ).Count == 0 ) {
return;
}
var stood = ArchFootprint.Shrink( region, seat );
var leaning = ArchFootprint.Grow( support.Carried, seat );
var spacing = MathF.Max( 32f, MathF.Max( section.Width, section.Depth ) * 8f );
foreach ( var loop in ArchFootprint.Outer( stood ) ) {
var open = ArchRegion.Open( loop, leaning ).Select( run => ArchRunPath.Of( run, top ) );
foreach ( var at in ArchBays.Posts( ArchBays.Over( open, spacing, MathF.Max( 6f, seat * 2f ) ) ) ) {
Stand( canvas, section, at.Point, support.GradeAt( at.Point ), top, shaft, cap );
}
}
}
static void Stand( ArchMesh canvas, ArchPillarPart section, Vector2 at, float grade, float top, ArchBrush shaft, ArchBrush cap ) {
var post = section.Section();
post.Placement = PillarPlacement.Freestanding;
post.Origin = at;
post.BaseHeight = grade;
post.Height = MathF.Max( 8f, top - grade );
ArchPillarGen.Stand( canvas, post, shaft, cap );
}
// The line below grows by half a wall first, or the footprint reads unsupported.
public static List<List<Vector2>> Unsupported( ArchRoom room, ArchBuilding building, ArchKit kit ) {
return new ArchOverhangSupportService( room, building, null, kit, default ).Resolve();
}
}
sealed class ArchOverhangGenerationService {
readonly ArchRoom room;
readonly ArchBuilding building;
readonly ArchPlan plan;
readonly ArchKit kit;
readonly ArchStyle style;
readonly ArchGround ground;
public ArchOverhangGenerationService( ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchStyle style, ArchGround ground ) {
this.room = room;
this.building = building;
this.plan = plan;
this.kit = kit;
this.style = style;
this.ground = ground;
}
public void Create( ArchMesh canvas ) {
var support = new ArchOverhangSupportService( room, building, plan, kit, ground );
var region = support.Resolve();
if ( region.Count == 0 ) {
return;
}
var chain = new[] { room.Palette, building.Palette };
var soffit = style.Brush( ArchSurface.Soffit, chain );
var depth = ArchOverhangGen.Depth( kit );
var top = room.BaseHeight - kit.FloorThickness;
foreach ( var loop in ArchFootprint.Outer( region ) ) {
ArchFloorGen.Slab( canvas, loop, null, top - depth, top, soffit );
}
// The slab overhead is poured to the wall CENTRELINE, so half a thickness of it is never there. Over a
// storey below, that storey's own wall fills it; over open air nothing does, and the slot runs right
// round the overhang with the wall's bare foot showing through it.
foreach ( var loop in ArchFootprint.Outer( support.Lip( region ) ) ) {
ArchFloorGen.Slab( canvas, loop, null, top, room.BaseHeight, soffit );
}
if ( !room.OverhangPosts ) {
return;
}
var posts = new[] { room.OverhangPost?.Palette ?? new ArchPalette(), room.Palette, building.Palette };
using ( canvas.Part( ArchPieces.Posts ) ) {
ArchOverhangGen.Posts( canvas, region, support, room, kit, top - depth,
style.Brush( ArchSurface.Pillar, posts ), style.Brush( ArchSurface.PillarCap, posts ) );
}
}
}
sealed class ArchOverhangSupportService {
readonly ArchRoom room;
readonly ArchBuilding building;
readonly ArchPlan plan;
readonly ArchKit kit;
readonly ArchGround ground;
readonly List<List<Vector2>> carried;
readonly List<List<Vector2>> standing;
public ArchOverhangSupportService( ArchRoom room, ArchBuilding building, ArchPlan plan, ArchKit kit, ArchGround ground ) {
this.room = room;
this.building = building;
this.plan = plan;
this.kit = kit;
this.ground = ground;
carried = room is null || building is null || room.Floor <= 0
? new List<List<Vector2>>()
: ArchRegion.Shell( ArchRegion.Storey( building, room.Floor - 1 ), kit.WallThickness );
standing = Standing();
}
// Out to the OUTER wall face, not the centreline: the underside has to finish where the elevation does.
public List<List<Vector2>> Resolve() {
return standing.Count == 0
? new List<List<Vector2>>()
: ArchFootprint.Subtract( ArchRegion.Shell( standing, kit.WallThickness ), carried );
}
// The half thickness the slab above never reaches, wherever nothing below stands in for it.
public List<List<Vector2>> Lip( IEnumerable<IReadOnlyList<Vector2>> resolved ) {
return standing.Count == 0 ? new List<List<Vector2>>() : ArchFootprint.Subtract( resolved, standing );
}
List<List<Vector2>> Standing() {
var none = new List<List<Vector2>>();
if ( room is null || building is null || room.Floor <= 0 || room.Spans || carried.Count == 0 ) {
return none;
}
var footprint = ArchFloorGen.Footprint( room );
if ( footprint.Count < 3 || !ArchFootprint.Bearing( footprint, out _ ) ) {
return none;
}
return new List<List<Vector2>> { footprint };
}
// The same question a hand-placed column asks, so it goes to the same place: the storey below carries the post
// where that storey is really under it, and over open air it seeks the ground.
public float GradeAt( Vector2 point ) {
var datum = building.Rooms
.Where( other => other.Floor == room.Floor - 1 )
.Select( other => other.BaseHeight )
.DefaultIfEmpty( kit.GroundClearance )
.Min();
var covered = ArchPillarSoffit.Covering( plan, kit, point, datum );
return ArchPillarSeat.Under( plan, building, kit, ground, point, datum,
covered < float.MaxValue ? MathF.Max( datum + ArchPillarSeat.Clear, covered ) : datum + ArchPillarSeat.Clear );
}
public IReadOnlyList<List<Vector2>> Carried => carried;
}