Editor/Stair/ArchStairGen.cs

Editor code that generates stair geometry for an architectural system. It chooses brush skins from style/palette, computes stair shape and voiding, and builds treads, landings, stringers, enclosures and balustrade pieces by emitting meshes (prisms and polygons) into an ArchMesh canvas.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace Sunless.Architecture;

public readonly struct ArchStairSkin
{
	public ArchBrush Tread { get; init; }
	public ArchBrush Riser { get; init; }
	public ArchBrush Stringer { get; init; }
	public ArchBrush Rail { get; init; }
	public ArchBrush Wall { get; init; }
	public ArchBrush Leaf { get; init; }
	public ArchBrush Ceiling { get; init; }
	public ArchBrush Trim { get; init; }
}

public static partial class ArchStairGen
{
	public static void Build(
		ArchMesh canvas,
		ArchStairPart stair,
		ArchRoom room,
		ArchBuilding building,
		ArchKit kit,
		ArchStyle style,
		List<ArchDoorRequest> doors,
		ArchPlan plan = null )
	{
		var chain = new[] { stair.Palette, room.Palette, building.Palette };

		var skin = new ArchStairSkin
		{
			Tread = style.Brush( ArchSurface.StairTread, chain ),
			Riser = style.Brush( ArchSurface.StairRiser, chain ),
			Stringer = style.Brush( ArchSurface.StairStringer, chain ),
			Rail = style.Brush( ArchSurface.Railing, chain ),
			Wall = style.Brush( ArchSurface.WallInterior, chain ),
			Leaf = style.Brush( ArchSurface.DoorLeaf, chain ),
			Ceiling = style.Brush( ArchSurface.Ceiling, chain ),
			Trim = style.Brush( ArchSurface.Trim, chain )
		};

		var shape = ArchStairShape.Resolve( stair, room, kit, building?.Rooms, building );
		var voiding = ArchStairVoid.Open( plan, kit, stair, shape, room?.Floor ?? 0 );

		// The flight itself is the part - treads, pads and the carriage under them stay on its body, and only
		// what is hung on it afterwards is filed as a piece of its own.
		foreach ( var run in shape.Runs )
		{
			Treads( canvas, stair, shape, run, kit, skin, voiding );
		}

		foreach ( var pad in shape.Pads )
		{
			Landing( canvas, stair, pad, kit, skin, voiding );
		}

		using ( canvas.Part( ArchPieces.Stringers ) )
		{
			Carriage( canvas, stair, shape, kit, skin );
		}

		using ( canvas.Part( ArchPieces.Enclosure ) )
		{
			Wrap( canvas, stair, shape, skin );
			Enclosure( canvas, stair, shape, kit, skin, doors );
		}

		// The newels are filed apart from the infill they carry: a post is what a balustrade is hung ON, and one
		// row for every post on the stair is the row somebody actually wants to hide, collide or re-skin.
		var posts = new List<ArchStairGuardNode>();

		using ( canvas.Part( ArchPieces.Balustrade ) )
		{
			Balustrade( canvas, stair, shape, kit, skin, voiding, posts, plan, building );
		}

		using ( canvas.Part( ArchPieces.Posts ) )
		{
			Newels( canvas, kit, skin, posts );
		}
	}

	// A tread oversails its riser by the nosing and sits BETWEEN the strings. A boolean over the flight
	// takes its bite out of every step it reaches, so a tread comes out as whatever the cut left of its
	// width - none at all where the shaft takes the lot.
	static void Treads( ArchMesh canvas, ArchStairPart stair, ArchStairShape shape, ArchStairRun run, ArchKit kit, ArchStairSkin skin, ArchStairVoiding voiding )
	{
		var thickness = MathF.Max( 1f, stair.TreadThickness );
		var nosing = MathF.Max( 0f, stair.TreadNosing );
		var inset = Inset( kit );

		var from = inset;
		var to = run.Width - inset;

		for ( var step = 0; step < run.Steps; step++ )
		{
			var top = run.StepTop( step );
			var nose = step * run.Going;

			foreach ( var span in ArchStairVoid.Spans( voiding, run.Axes, nose + run.Going * 0.5f, from, to, top - thickness, top ) )
			{
				Block( canvas, run.Axes, nose - nosing, nose + run.Going, span.From, span.To, top - thickness, top, skin.Tread );

				if ( !stair.Risers )
				{
					continue;
				}

				// The riser's head is bitten into the tread it carries — flush faces z-fight the whole flight.
				Block( canvas, run.Axes, nose, nose + kit.StringerThickness, span.From, span.To, top - run.Rise,
					ArchContact.Bury( kit, top - thickness, -1f ), skin.Riser );
			}
		}
	}

	// The pad reaches back under the last tread; its underside is a CEILING to the storey below. A landing is a
	// plate rather than a strip, so a boolean over it is clipped both ways and it comes out as the cells it left.
	static void Landing( ArchMesh canvas, ArchStairPart stair, ArchStairPad pad, ArchKit kit, ArchStairSkin skin, ArchStairVoiding voiding )
	{
		var thickness = MathF.Max( kit.FloorThickness * 0.5f, stair.TreadThickness );
		var back = ArchContact.Bury( kit, pad.AlongFrom, 1f );

		foreach ( var cell in ArchStairVoid.Cells( voiding, pad.Axes, back, pad.AlongTo, pad.AcrossFrom, pad.AcrossTo,
			pad.Height - thickness, pad.Height ) )
		{
			Slab( canvas, pad.Axes, cell.AlongFrom, cell.AlongTo, cell.AcrossFrom, cell.AcrossTo,
				pad.Height - thickness, pad.Height, skin.Tread, skin.Ceiling );
		}
	}

	static void Slab(
		ArchMesh canvas,
		ArchStairAxes axes,
		float alongFrom,
		float alongTo,
		float acrossFrom,
		float acrossTo,
		float bottom,
		float top,
		ArchBrush brush,
		ArchBrush soffit )
	{
		var lo = MathF.Min( alongFrom, alongTo );
		var hi = MathF.Max( alongFrom, alongTo );

		if ( hi - lo < 0.05f || acrossTo - acrossFrom < 0.05f || top - bottom < 0.05f )
		{
			return;
		}

		var lower = new List<Vector3>
		{
			axes.Point( lo, acrossFrom, bottom ),
			axes.Point( hi, acrossFrom, bottom ),
			axes.Point( hi, acrossTo, bottom ),
			axes.Point( lo, acrossTo, bottom )
		};

		var upper = new List<Vector3>();

		foreach ( var point in lower )
		{
			upper.Add( point.WithZ( top ) );
		}

		canvas.Prism( lower, upper, brush, cap: false );
		canvas.Polygon( lower, soffit, ArchMesh.Newell( lower ).z > 0f );
		canvas.Polygon( upper, brush, ArchMesh.Newell( upper ).z < 0f );
	}

	static void Block(
		ArchMesh canvas,
		ArchStairAxes axes,
		float alongFrom,
		float alongTo,
		float acrossFrom,
		float acrossTo,
		float bottom,
		float top,
		ArchBrush brush )
	{
		var lo = MathF.Min( alongFrom, alongTo );
		var hi = MathF.Max( alongFrom, alongTo );

		if ( hi - lo < 0.05f || acrossTo - acrossFrom < 0.05f || top - bottom < 0.05f )
		{
			return;
		}

		var lower = new List<Vector3>
		{
			axes.Point( lo, acrossFrom, bottom ),
			axes.Point( hi, acrossFrom, bottom ),
			axes.Point( hi, acrossTo, bottom ),
			axes.Point( lo, acrossTo, bottom )
		};

		var upper = new List<Vector3>();

		foreach ( var point in lower )
		{
			upper.Add( point.WithZ( top ) );
		}

		canvas.Prism( lower, upper, brush );
	}
}