Editor-side static class for stair enclosure generation. It computes whether stair runs are solid or filled, creates wall wraps, fills stair runs and pads, slices geometry for door openings, and produces door requests for cupboard under-stair openings.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Only floor-standing runs get closed; an upper flight is over the one below, floating over nothing.
public static partial class ArchStairGen
{
// A SOLID core is masonry, so every flight in it stands on its own mass down to the shaft floor - which is
// the concrete stair, and what carving a platform used to be needed for. Two lanes never overlap in plan, so
// filling under both leaves the well between them open.
static bool Solid( ArchStairPart stair ) => stair.Core?.Fill == StairFill.Solid;
static bool Filled( ArchStairPart stair, ArchStairShape shape, ArchStairRun run )
{
return Solid( stair ) || (stair.Under != StairUnder.Open && run.BaseHeight <= shape.BaseHeight + 0.5f);
}
// The walls a flight carries up its own well: one ring round the whole footprint, from the bottom
// step to the head, so a stair that leaves the room it started in arrives inside its own core.
// Rings from a grown outline rather than boxes butted, so the corners mitre at any yaw.
static void Wrap( ArchMesh canvas, ArchStairPart stair, ArchStairShape shape, ArchStairSkin skin )
{
if ( stair.Wrap != StairWrap.Walls )
{
return;
}
var thickness = MathF.Max( 1f, stair.WrapThickness );
foreach ( var loop in ArchFootprint.Union( shape.Footprint() ) )
{
ArchCarveRing.Walls( canvas, loop, thickness, shape.BaseHeight, shape.TopHeight, skin.Wall );
}
}
static void Enclosure(
ArchMesh canvas,
ArchStairPart stair,
ArchStairShape shape,
ArchKit kit,
ArchStairSkin skin,
List<ArchDoorRequest> doors )
{
if ( stair.Under == StairUnder.Open && !Solid( stair ) )
{
return;
}
foreach ( var run in shape.Runs )
{
if ( Filled( stair, shape, run ) )
{
Fill( canvas, stair, run, kit, skin, doors );
}
}
// The enclosure covers the WHOLE footprint, landings included, or the arrival stands on open air.
foreach ( var pad in shape.Pads )
{
var drop = MathF.Max( kit.FloorThickness * 0.5f, stair.TreadThickness );
var inset = MathF.Max( 0.5f, kit.StringerThickness );
Block( canvas, pad.Axes, pad.AlongFrom, pad.AlongTo, pad.AcrossFrom + inset, pad.AcrossTo - inset,
shape.BaseHeight, pad.Height - drop, skin.Wall );
}
}
// Three capped slices form the doorway; their end caps are the jambs.
static void Fill(
ArchMesh canvas,
ArchStairPart stair,
ArchStairRun run,
ArchKit kit,
ArchStairSkin skin,
List<ArchDoorRequest> doors )
{
var drop = MathF.Max( 1f, stair.TreadThickness );
var floor = run.BaseHeight;
var low = run.BaseHeight + run.Rise - drop;
var high = run.TopHeight - drop;
// The soffit runs BETWEEN the strings — flush puts two surfaces on one plane.
var inset = MathF.Max( 0.5f, kit.StringerThickness );
float Soffit( float along ) => MathX.Lerp( low, high, MathX.Clamp( along / MathF.Max( 1f, run.Length ), 0f, 1f ) );
if ( stair.Under != StairUnder.Cupboard || !Doorway( stair, run, kit, Soffit, out var from, out var to, out var height ) )
{
Slice( canvas, run, inset, 0f, run.Length, floor, Soffit, skin.Wall );
return;
}
var head = floor + height;
Slice( canvas, run, inset, 0f, from, floor, Soffit, skin.Wall );
Slice( canvas, run, inset, from, to, head, Soffit, skin.Wall );
Slice( canvas, run, inset, to, run.Length, floor, Soffit, skin.Wall );
doors.Add( Leaf( stair, run, kit, from, to, floor, height, inset, skin ) );
}
// Lofting the two side polygons onto each other is what makes Prism close the ends.
static void Slice(
ArchMesh canvas,
ArchStairRun run,
float inset,
float from,
float to,
float bottom,
Func<float, float> soffit,
ArchBrush brush )
{
if ( to - from < 0.5f )
{
return;
}
var near = MathF.Max( bottom + 0.5f, soffit( from ) );
var far = MathF.Max( bottom + 0.5f, soffit( to ) );
if ( near - bottom < 0.5f && far - bottom < 0.5f )
{
return;
}
var section = new List<Vector3>();
var opposite = new List<Vector3>();
foreach ( var corner in new[] { (from, bottom), (to, bottom), (to, far), (from, near) } )
{
section.Add( run.Axes.Point( corner.Item1, inset, corner.Item2 ) );
opposite.Add( run.Axes.Point( corner.Item1, run.Width - inset, corner.Item2 ) );
}
canvas.Prism( section, opposite, brush );
}
// Back from the top for a jamb pier; the head clamps to the soffit, so a shallow flight gets a short door.
static bool Doorway( ArchStairPart stair, ArchStairRun run, ArchKit kit, Func<float, float> soffit, out float from, out float to, out float height )
{
var preset = kit.FindOpening( stair.CupboardOpening );
from = 0f;
to = 0f;
height = 0f;
if ( preset is null )
{
return false;
}
to = MathF.Max( 0f, run.Length - run.Going );
from = MathF.Max( 0f, to - preset.Width );
if ( to - from < 24f )
{
return false;
}
height = MathF.Min( preset.Height, soffit( from ) - run.BaseHeight - kit.CasingWidth );
return height >= 48f;
}
static ArchDoorRequest Leaf(
ArchStairPart stair,
ArchStairRun run,
ArchKit kit,
float from,
float to,
float floor,
float height,
float inset,
ArchStairSkin skin )
{
var lane = run.Width * 0.5f;
var mirrored = run.WalledLeft;
var hinge = run.Axes.Point( mirrored ? to : from, lane, floor );
return new ArchDoorRequest
{
Opening = new ArchOpening { Id = stair.Id, Preset = stair.CupboardOpening, Kind = OpeningKind.Door, Width = to - from, Height = height },
Rotation = Rotation.FromYaw( run.Axes.Yaw ),
HingeLocal = hinge,
LeafWidth = to - from,
LeafHeight = height,
LeafThickness = kit.DoorLeafThickness,
Mirrored = mirrored,
Brush = skin.Leaf
};
}
}