editor/AdvancedMappingTools/Procedural/District/DistrictOverpass.cs

Editor-side planner for procedural district overpass bridges. It finds street chains parallel to the grid, routes a bridge over the closest chain, trims the covered street segments and builds a DistrictBridgeRecipe with supports, keepouts and materials.

namespace AdvancedMappingTools;
public sealed record DistrictOverpassPlan( DistrictBridgeRecipe Bridge, Dictionary<string, (float From, float To)> Removed );
public static class DistrictOverpass
{
	static float Dot( Vector2 a, Vector2 b ) => a.x * b.x + a.y * b.y;
	static string Node( Vector2 p ) => FormattableString.Invariant( $"{p.x:0.00}/{p.y:0.00}" );
	public static string Key( DistrictStreet s ) => Node( s.A ) + "-" + Node( s.B );
	public static DistrictOverpassPlan Plan( DistrictEditor e, DistrictPlan layout, IReadOnlyList<Vector2> outline, float z )
	{
		if ( !e.AddOverpass )
			return null;
		var radians = e.GridAngle.DegreeToRadian();
		var u = new Vector2( MathF.Cos( radians ), MathF.Sin( radians ) );
		var v = new Vector2( -u.y, u.x );
		var center = outline.Aggregate( Vector2.Zero, ( s, p ) => s + p ) / outline.Count + v * e.OverpassOffset;
		var edges = layout.Streets.Where( s => MathF.Abs( Dot( (s.B - s.A).Normal, u ) ) > .8f ).ToList();
		var adjacency = new Dictionary<string, List<DistrictStreet>>();
		foreach ( var edge in edges )
			foreach ( var p in new[]
			{
				edge.A,
				edge.B
			}

 )
			{
				var key = Node( p );
				if ( !adjacency.ContainsKey( key ) )
					adjacency[key] = [];
				adjacency[key].Add( edge );
			}

		var visited = new HashSet<string>();
		var routes = new List<(List<Vector2> Points, List<DistrictStreet> Edges, float Distance, float Length)>();
		foreach ( var seed in edges )
		{
			if ( visited.Contains( Key( seed ) ) )
				continue;
			var component = new List<DistrictStreet>();
			var queue = new Queue<DistrictStreet>();
			queue.Enqueue( seed );
			while ( queue.Count > 0 )
			{
				var edge = queue.Dequeue();
				if ( !visited.Add( Key( edge ) ) )
					continue;
				component.Add( edge );
				foreach ( var p in new[]
				{
					edge.A,
					edge.B
				}

 )
					foreach ( var next in adjacency[Node( p )] )
						if ( !visited.Contains( Key( next ) ) )
							queue.Enqueue( next );
			}

			var ids = component.Select( Key ).ToHashSet();
			var ends = component.SelectMany( s => new[] { s.A, s.B } ).Distinct().Where( p => adjacency[Node( p )].Count( s => ids.Contains( Key( s ) ) ) == 1 ).OrderBy( p => Dot( p, u ) ).ToList();
			if ( ends.Count < 2 )
				continue;
			var points = new List<Vector2>
			{
				ends[0]
			};
			var ordered = new List<DistrictStreet>();
			var used = new HashSet<string>();
			while ( true )
			{
				var next = adjacency[Node( points[^1] )].FirstOrDefault( s => ids.Contains( Key( s ) ) && !used.Contains( Key( s ) ) );
				if ( next == null )
					break;
				used.Add( Key( next ) );
				ordered.Add( next );
				points.Add( Node( next.A ) == Node( points[^1] ) ? next.B : next.A );
			}

			var length = Enumerable.Range( 0, points.Count - 1 ).Sum( i => (points[i + 1] - points[i]).Length );
			if ( length < MathF.Max( e.StreetWidth * 3, 512 ) )
				continue;
			var distance = MathF.Abs( points.Average( p => Dot( p - center, v ) ) );
			routes.Add( (points, ordered, distance, length) );
		}

		if ( routes.Count == 0 )
			return null;
		var route = routes.OrderBy( r => r.Distance ).ThenByDescending( r => r.Length ).First();
		var margin = MathF.Max( e.StreetWidth * .8f, 96 );
		var from = margin;
		var to = route.Length - margin;
		if ( to - from < 256 )
			return null;
		var removed = new Dictionary<string, (float From, float To)>();
		var trimmed = new List<Vector2>();
		var distanceAlong = 0f;
		for ( var i = 0; i + 1 < route.Points.Count; i++ )
		{
			var a = route.Points[i];
			var b = route.Points[i + 1];
			var len = (b - a).Length;
			var lo = Math.Clamp( (from - distanceAlong) / len, 0, 1 );
			var hi = Math.Clamp( (to - distanceAlong) / len, 0, 1 );
			if ( hi > lo )
			{
				var p = a + (b - a) * lo;
				var q = a + (b - a) * hi;
				if ( trimmed.Count == 0 || trimmed[^1].Distance( p ) > .01f )
					trimmed.Add( p );
				trimmed.Add( q );
				var edge = route.Edges[i];
				removed[Key( edge )] = Node( edge.A ) == Node( a ) ? (lo, hi) : (1 - hi, 1 - lo);
			}

			distanceAlong += len;
		}

		if ( trimmed.Count < 2 )
			return null;
		var world = new Transform( new Vector3( trimmed[0].x, trimmed[0].y, z ), Rotation.FromYaw( e.GridAngle ) );
		var path = PathCurve.FromPoints( trimmed.Select( p => world.PointToLocal( new Vector3( p.x, p.y, z ) ) ), PathInterpolation.CatmullRom );
		var track = path.Track( 64, FrameMode.Upright, 4 ).Plumbed();
		var settings = new DistrictBridgeSettings
		{
			Path = path,
			Length = track.Length,
			Width = MathF.Max( e.StreetWidth * .7f, 64 ),
			Rise = MathF.Max( e.OverpassHeight, 0 ),
			RampLength = MathF.Max( e.OverpassRamp, 1 ),
			Style = e.BridgeStyle,
			RailingHeight = e.BridgeRailingHeight,
			WalkwayWidth = e.BridgeWalkwayWidth,
			PostSpacing = e.BridgePostSpacing,
			SupportSpacing = e.BridgeSupportSpacing,
			Thickness = e.BridgeThickness,
			Supports = true,
			Material = e.BridgeMaterial?.ResourcePath ?? e.Trim?.ResourcePath,
			SurfaceMaterial = e.BridgeRoadMaterial?.ResourcePath
		};
		return new( new( "overpass", settings, world ), removed );
	}

	public static void ProtectSupports( DistrictBridgeRecipe bridge, IEnumerable<RoadSettings> roads, DistrictCanalSettings canal )
	{
		var settings = bridge.Settings;
		settings.Keepouts = [];
		foreach ( var road in roads )
		{
			var points = road.Path.Sample( 96, 180 ).Select( p => bridge.World.PointToLocal( p.Position ) ).ToArray();
			for ( var i = 0; i + 1 < points.Length; i++ )
				settings.Keepouts.Add( new() { A = new( points[i].x, points[i].y ), B = new( points[i + 1].x, points[i + 1].y ), Radius = DistrictEditor.FullWidth( road ) * .5f + settings.Width * .3f + 32 } );
		}

		if ( canal != null )
		{
			var angle = canal.Angle.DegreeToRadian();
			var axis = new Vector2( MathF.Cos( angle ), MathF.Sin( angle ) );
			var reach = canal.Outline.Max( p => (p - canal.Center).Length ) + canal.Width;
			var a = bridge.World.PointToLocal( new Vector3( canal.Center.x - axis.x * reach, canal.Center.y - axis.y * reach, canal.BaseHeight ) );
			var b = bridge.World.PointToLocal( new Vector3( canal.Center.x + axis.x * reach, canal.Center.y + axis.y * reach, canal.BaseHeight ) );
			settings.Keepouts.Add( new() { A = new( a.x, a.y ), B = new( b.x, b.y ), Radius = canal.Width * .5f + 32 } );
		}
	}

	public static bool NearBuilding( DistrictBridgeRecipe bridge, Vector2 center, float radius )
	{
		var point = bridge.World.PointToLocal( new Vector3( center.x, center.y, bridge.World.Position.z ) );
		var path = bridge.Settings.Path ?? PathCurve.FromPoints( [Vector3.Zero, new( bridge.Settings.Length, 0, 0 )] );
		var points = path.Sample( 96, 180 ).Select( p => new Vector2( p.Position.x, p.Position.y ) ).ToArray();
		for ( var i = 0; i + 1 < points.Length; i++ )
			if ( new BridgeKeepout
			{
				A = points[i],
				B = points[i + 1],
				Radius = bridge.Settings.Width * .5f + radius + 48
			}.Contains( new Vector2( point.x, point.y ) ) )
				return true;
		return false;
	}
}