Editor/Road/ArchRoadTerrainContribution.cs

Editor component that modifies terrain for roads and tunnels. It computes samples near planned roads, finds closest road frames, and carves or cuts the terrain heightmap in request.Map to match road and tunnel geometry.

File Access
namespace Sunless.Architecture;

public sealed class ArchRoadTerrainContribution : IArchTerrainContribution
{
	public string Ident => "sunless.arch_roads";

	public int Carve( ArchTerrainRequest request )
	{
		var padding = MathF.Max( 8f, request.Kit.RoadPadding );
		var inset = MathF.Max( 0f, request.Kit.RoadInset );
		var ground = new ArchGround( request.Terrain.Scene );
		var stamped = 0;
		var shallow = 0;

		foreach ( var road in request.Plan.Roads().Where( entry => entry.Stamp ) )
		{
			var curve = road.Curve();

			if ( !curve.IsUsable )
			{
				continue;
			}

			var frames = ArchRoadGen.Frames( road, curve );

			if ( frames.Count < 2 )
			{
				continue;
			}

			var bores = ArchTunnel.Shapes( road, curve, frames, request.Kit, ground, request.Plan )
				.Where( shape => shape.Part.Bored )
				.ToList();

			var verge = road.Reach();
			var reach = verge + padding;

			var min = new Vector2( frames.Min( frame => frame.Position.x ), frames.Min( frame => frame.Position.y ) );
			var max = new Vector2( frames.Max( frame => frame.Position.x ), frames.Max( frame => frame.Position.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 );

			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 );

					if ( !Closest( frames, world, out var surface, out var gap, out var right, out var along ) || gap > reach )
					{
						continue;
					}

					if ( ArchBridge.Carried( road, along ) )
					{
						continue;
					}

					if ( bores.Any( bore => bore.Carries( along ) ) )
					{
						continue;
					}

					var index = y * request.Resolution + x;
					var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
					var shoulder = road.Reach( right );
					var target = Shoulder( road, request.Kit, surface, gap, shoulder, inset );

					if ( gap > shoulder )
					{
						target = MathX.Lerp( target, current, Ease( (gap - shoulder) / padding ) );
					}

					// A road is cut into the ground, never stood on a bank raised to meet it: filling here is what
					// walled a street in on both sides and left the kerb reading as a plinth.
					if ( target >= current )
					{
						shallow++;
						continue;
					}

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

		// Silence here reads as a broken stamp; it is nearly always a road laid over the ground rather than into it.
		if ( stamped == 0 && shallow > 0 )
		{
			Log.Info( $"Architecture: every one of {shallow} samples under the roads already sits below them - nothing to cut. Lower the run onto the ground, or give the ground headroom first." );
		}

		return stamped;
	}

	public int Cut( ArchTerrainRequest request )
	{
		var ground = new ArchGround( request.Terrain.Scene );
		var cut = 0;

		foreach ( var road in request.Plan.Roads() )
		{
			var curve = road.Curve();

			if ( !curve.IsUsable )
			{
				continue;
			}

			var frames = ArchRoadGen.Frames( road, curve );

			foreach ( var tunnel in ArchTunnel.Shapes( road, curve, frames, request.Kit, ground, request.Plan ) )
			{
				foreach ( var mouth in new[] { tunnel.Near, tunnel.Far } )
				{
					cut += Cutting( tunnel, mouth, request );
				}
			}
		}

		return cut;
	}

	static int Cutting( ArchTunnelShape shape, ArchTunnelMouth mouth, ArchTerrainRequest request )
	{
		var part = shape.Part;
		var frame = mouth.Frame;
		var reach = ArchTunnel.Reach( part, shape.Section );
		var padding = MathF.Max( 8f, request.Kit.RoadPadding );
		var inset = MathF.Max( 0f, request.Kit.RoadInset );
		var run = MathF.Max( MathF.Max( 96f, part.WingWall ), MathF.Max( 12f, part.PortalDepth ) * 2f );
		var splay = MathF.Tan( Math.Clamp( part.WingSplay, 0f, 75f ).DegreeToRadian() );

		var outward = mouth.Near ? -frame.Along : frame.Along;
		var ahead = new Vector2( outward.x, outward.y ).Normal;
		var sideways = new Vector2( -ahead.y, ahead.x );
		var floor = frame.Position.z + shape.Section.Floor - inset;
		var widest = reach + run * splay + padding;

		var lo = Cell( frame.Flat - new Vector2( widest + run, widest + run ), request.Origin, request.Step, request.Resolution );
		var hi = Cell( frame.Flat + new Vector2( widest + run, widest + run ), request.Origin, request.Step, request.Resolution );
		var cut = 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 offset = world - frame.Flat;
				var along = Vector2.Dot( offset, ahead );

				if ( along < 0f || along > run + padding )
				{
					continue;
				}

				var across = MathF.Abs( Vector2.Dot( offset, sideways ) );
				var half = reach + along * splay;
				var outside = MathF.Max( across - half, along - run );

				if ( outside >= padding )
				{
					continue;
				}

				var index = y * request.Resolution + x;
				var current = request.Origin.z + request.Map[index] / (float)ushort.MaxValue * request.MaxHeight;
				var target = outside > 0f ? MathX.Lerp( current, floor, Ease( 1f - outside / padding ) ) : floor;

				if ( target >= current )
				{
					continue;
				}

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

		return cut;
	}

	// The ground meets the section it runs beside, not the crown. Carve the whole verge to the carriageway and the
	// pavement is left standing on a plinth its own kerb height - which is what the drive's landing already knew.
	public static float Shoulder( ArchRoadPart road, ArchKit kit, float surface, float gap, float shoulder, float inset )
	{
		var carriage = road.HalfWidth;
		var grade = surface - inset;

		if ( gap <= carriage || shoulder <= carriage + 0.01f )
		{
			return grade;
		}

		return MathX.Lerp( grade, surface + ArchCrossover.Back( road, kit ),
			Math.Clamp( (gap - carriage) / (shoulder - carriage), 0f, 1f ) );
	}

	static bool Closest( IReadOnlyList<ArchFrame> frames, Vector2 point, out float height, out float gap, out bool right, out float station )
	{
		height = 0f;
		gap = float.MaxValue;
		right = false;
		station = 0f;

		for ( var index = 0; index < frames.Count - 1; index++ )
		{
			var from = frames[index];
			var to = frames[index + 1];
			var span = to.Flat - from.Flat;
			var length = span.Length;

			if ( length < 0.01f )
			{
				continue;
			}

			var direction = span / length;
			var along = Math.Clamp( Vector2.Dot( point - from.Flat, direction ), 0f, length );
			var offset = point - (from.Flat + direction * along);
			var reach = offset.Length;

			if ( reach >= gap )
			{
				continue;
			}

			gap = reach;
			// Which pavement this sample stands beside: only that side's verge decides how high the ground comes up.
			right = Vector2.Dot( offset, new Vector2( direction.y, -direction.x ) ) > 0f;
			height = MathX.Lerp( from.Position.z, to.Position.z, along / length );
			station = MathX.Lerp( from.Distance, to.Distance, along / length );
		}

		return gap < float.MaxValue;
	}

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