Editor-side stair carriage generator. It builds stringers, caps and planks for non-solid stair runs by computing dimensions from kit and run data and adding prisms to an ArchMesh using provided brushes.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
public static partial class ArchStairGen
{
// Treads sit BETWEEN the strings — flush ones z-fight; the under-stair fill sits between the same planks.
static float Inset( ArchKit kit ) => MathF.Max( 0.5f, kit.StringerThickness );
static void Carriage( ArchMesh canvas, ArchStairPart stair, ArchStairShape shape, ArchKit kit, ArchStairSkin skin )
{
// A solid core carries itself - its mass IS the structure, and a plank standing on the fill's side face
// is two surfaces on one plane. That is also why concrete steps wear no timber capping down their edge.
if ( Solid( stair ) )
{
return;
}
foreach ( var run in shape.Runs )
{
Strings( canvas, stair, run, kit, skin );
}
}
// Lofted to stay in the rake's plane — a world-square box pokes through the treads.
static void Strings( ArchMesh canvas, ArchStairPart stair, ArchStairRun run, ArchKit kit, ArchStairSkin skin )
{
var thickness = MathF.Max( 0.5f, kit.StringerThickness );
var depth = MathF.Max( 4f, kit.StringerDepth );
// Flush against a wall the string's face z-fights the wall — bite it in instead.
var bite = ArchContact.Bite( kit );
var right = run.WalledRight ? bite : 0f;
var left = run.WalledLeft ? bite : 0f;
Plank( canvas, run, 0f, run.Length, -right, thickness + right, depth, run.BaseHeight, run.TopHeight, skin.Stringer );
Plank( canvas, run, 0f, run.Length, run.Width - thickness, thickness + left, depth, run.BaseHeight, run.TopHeight, skin.Stringer );
// This takes over the floor's cut-edge trim — open sides only, a wall has no cut edge.
if ( !run.WalledRight )
{
Capping( canvas, run, -right, thickness + right, kit, skin );
}
if ( !run.WalledLeft )
{
Capping( canvas, run, run.Width - thickness, thickness + left, kit, skin );
}
}
// Proud as trim and bitten in; stops at the floor plane or the last inch pokes through the landing.
static void Capping( ArchMesh canvas, ArchStairRun run, float lane, float width, ArchKit kit, ArchStairSkin skin )
{
var proud = ArchContact.Proud( kit );
var over = MathF.Max( 0.2f, proud * 0.5f );
var to = run.Under( proud );
if ( to < 1f )
{
return;
}
Plank( canvas, run, 0f, to, lane - over, width + over * 2f, proud + ArchContact.Bite( kit ),
run.BaseHeight + proud, run.Rake( to ) + proud, skin.Trim );
}
static void Plank(
ArchMesh canvas,
ArchStairRun run,
float from,
float to,
float lane,
float thickness,
float depth,
float low,
float high,
ArchBrush brush )
{
if ( to - from < 0.5f )
{
return;
}
var lower = new List<Vector3>
{
run.Axes.Point( from, lane, low - depth ),
run.Axes.Point( to, lane, high - depth ),
run.Axes.Point( to, lane + thickness, high - depth ),
run.Axes.Point( from, lane + thickness, low - depth )
};
var upper = new List<Vector3>
{
run.Axes.Point( from, lane, low ),
run.Axes.Point( to, lane, high ),
run.Axes.Point( to, lane + thickness, high ),
run.Axes.Point( from, lane + thickness, low )
};
canvas.Prism( lower, upper, brush );
}
}