Editor/Stair/ArchApproachTerrainContribution.cs

Editor component that contributes stair/approach terrain to an architectural terrain map. It iterates plan buildings, rooms and approaches, computes ramp or spline runs, and stamps height values into a heightmap (request.Map) to shape roads/ramps with padding, inset and easing.

File Access
namespace Sunless.Architecture;

public sealed class ArchApproachTerrainContribution : IArchTerrainContribution
{
	public string Ident => "sunless.arch.stair";

	public int Raise( ArchTerrainRequest request )
	{
		var stamped = 0;

		foreach ( var building in request.Plan.Buildings )
		{
			var lift = ArchAsks.Lift( request.Plan, building, request.Kit );

			foreach ( var room in building.Rooms )
			{
				foreach ( var approach in room.Approaches )
				{
					if ( approach.IsSpline )
					{
						foreach ( var run in ArchApproachGen.SplineRuns( approach, room, building, request.Kit, request.Plan ) )
						{
							stamped += Along( run, lift, request );
						}

						continue;
					}

					if ( ArchApproachGen.Resolved( approach, room, building, request.Kit, request.Plan, out var ramp ) )
					{
						stamped += Along( ramp, lift, request );
					}
				}
			}
		}

		return stamped;
	}

	public int Carve( ArchTerrainRequest request ) => 0;

	public int Cut( ArchTerrainRequest request ) => 0;

	static int Along( ArchApproachGen.ArchRampRun ramp, float lift, ArchTerrainRequest request )
	{
		var span = ramp.Foot - ramp.Wall;
		var length = span.Length;

		if ( length < 1f )
		{
			return 0;
		}

		var padding = MathF.Max( 8f, request.Kit.RoadPadding );
		var inset = MathF.Max( 0f, request.Kit.RoadInset );
		var direction = span / length;
		var sideways = new Vector2( -direction.y, direction.x );
		var reach = ramp.HalfWidth + padding;

		var min = new Vector2( MathF.Min( ramp.Wall.x, ramp.Foot.x ), MathF.Min( ramp.Wall.y, ramp.Foot.y ) );
		var max = new Vector2( MathF.Max( ramp.Wall.x, ramp.Foot.x ), MathF.Max( ramp.Wall.y, ramp.Foot.y ) );

		var lo = Cell( min - new Vector2( reach, reach ), request.Origin, request.Step, request.Resolution );
		var hi = Cell( max + new Vector2( reach, reach ), request.Origin, request.Step, request.Resolution );
		var stamped = 0;

		for ( var y = lo.y; y <= hi.y; y++ )
		{
			for ( var x = lo.x; x <= hi.x; x++ )
			{
				var world = new Vector2( request.Origin.x + x * request.Step, request.Origin.y + y * request.Step );
				var along = Vector2.Dot( world - ramp.Wall, direction );
				var gap = MathF.Abs( Vector2.Dot( world - ramp.Wall, sideways ) );
				var outside = MathF.Max( gap - ramp.HalfWidth, MathF.Max( -along, along - length ) );

				if ( outside >= padding )
				{
					continue;
				}

				var index = y * request.Resolution + x;
				var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
				var deck = MathX.Lerp( ramp.WallTop, ramp.FootTop, Math.Clamp( along / length, 0f, 1f ) ) + lift;
				var target = deck - inset;

				if ( outside > 0f )
				{
					target = MathX.Lerp( current, target, Ease( 1f - outside / padding ) );
				}

				request.Map[index] = (ushort)Math.Clamp( (target - request.Origin.z) / request.MaxHeight * ushort.MaxValue, 0f, ushort.MaxValue );
				stamped++;
			}
		}

		return stamped;
	}

	static (int x, int y) Cell( Vector2 world, Vector3 origin, float step, int resolution )
	{
		var x = (int)MathF.Round( (world.x - origin.x) / step );
		var y = (int)MathF.Round( (world.y - origin.y) / step );

		return (Math.Clamp( x, 0, resolution - 1 ), Math.Clamp( y, 0, resolution - 1 ));
	}

	static float Ease( float t )
	{
		var clamped = Math.Clamp( t, 0f, 1f );

		return clamped * clamped * (3f - 2f * clamped);
	}
}