Editor/Stair/ArchStairCarriage.cs

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.

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