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