Procedural part generators for district bridges and canals. The bridge generator builds a deck, road surface, walkways, girders, railings, optional truss or arch sides, abutments and piers along a PathCurve or a straight track, skipping pier positions that fall inside keepout circles or a no-pier span. The canal generator clips an outline polygon against a rotated strip to produce bed, bank and water prisms.
namespace AdvancedMappingTools;
public sealed class BridgeKeepout
{
public Vector2 A { get; set; }
public Vector2 B { get; set; }
public float Radius { get; set; }
public bool Contains( Vector2 p )
{
var d = B - A;
var t = Math.Clamp( ((p.x - A.x) * d.x + (p.y - A.y) * d.y) / MathF.Max( d.LengthSquared, .001f ), 0, 1 );
return (p - (A + d * t)).Length < Radius;
}
}
public sealed class DistrictBridgeSettings : GeneratorSettings
{
public override IReadOnlySet<string> UnusedOptions => Path is null ? new HashSet<string>
{
nameof( RouteLength )
}
: new HashSet<string>
{
nameof( Length )
};
public PathCurve Path { get; set; }
public List<BridgeKeepout> Keepouts { get; set; } = [];
[Property, Group( "Bridge" ), Range( 16, 65536, slider: false ), WideMode]
public float Length { get => Path is null ? _length : Track().Length; set => _length = value; }
float _length = 1024;
[System.Text.Json.Serialization.JsonIgnore, Property, ReadOnly, Group( "Bridge" ), Title( "Route Length" ), WideMode]
public float RouteLength => Length;
[Property, Group( "Bridge" ), Range( 32, 2048, slider: false ), WideMode]
public float Width { get; set; } = 256;
[Property, Group( "Bridge" ), WideMode]
public WorldBridgeStyle Style { get; set; }
[Property, Group( "Bridge" ), Range( 0, 2048, slider: false ), WideMode]
public float Rise { get; set; } = 8;
[Property, Group( "Bridge" ), Range( 0, 8192, slider: false ), WideMode]
public float RampLength { get; set; }
[Property, Group( "Bridge" ), Range( 8, 128, slider: false ), WideMode]
public float Thickness { get; set; } = 24;
[Property, Group( "Walkways" ), Range( 0, 128, slider: false ), WideMode]
public float WalkwayWidth { get; set; } = 32;
[Property, Group( "Walkways" ), Range( 0, 128, slider: false ), WideMode]
public float RailingHeight { get; set; } = 42;
[Property, Group( "Walkways" ), Range( 48, 256, slider: false ), WideMode]
public float PostSpacing { get; set; } = 96;
[Property, Group( "Supports" ), WideMode]
public bool Supports { get; set; }
[Property, Group( "Supports" ), Range( 128, 4096, slider: false ), WideMode]
public float SupportSpacing { get; set; } = 768;
[Property, Group( "Supports" ), Range( 32, 512, slider: false ), WideMode]
public float BankDepth { get; set; } = 96;
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string Material { get; set; }
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string SurfaceMaterial { get; set; }
public float NoPierFrom { get; set; } = -1;
public float NoPierTo { get; set; } = -1;
public float HeightAt( float x, float? span = null )
{
var length = span ?? Length;
var t = Math.Clamp( x / MathF.Max( length, 1 ), 0, 1 );
if ( RampLength <= 0 )
return Rise * MathF.Sin( t * MathF.PI );
var ramp = Math.Clamp( RampLength, 1, length * .45f );
return Rise * Math.Clamp( MathF.Min( x, length - x ) / ramp, 0, 1 );
}
public PathTrack Track() => (Path ?? PathCurve.FromPoints( [Vector3.Zero, new Vector3( MathF.Max( Length, 1 ), 0, 0 )] )).Track( 64, FrameMode.Upright, 4 ).Plumbed();
}
public sealed class DistrictBridgeGenerator : IPartGenerator
{
public string Id => "district_bridge";
public string Title => "World Builder Bridge";
public Type SettingsType => typeof(DistrictBridgeSettings);
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (DistrictBridgeSettings)settings;
var root = context.Group( "root", Title, "Bridge", "bridge" );
var track = s.Track();
if ( track.Count < 2 )
return root;
var length = track.Length;
var half = MathF.Max( s.Width, 32 ) * .5f;
var thick = MathF.Max( s.Thickness, 8 );
var walk = Math.Clamp( s.WalkwayWidth, 0, s.Width * .22f );
var roadHalf = half - walk;
var steel = context.Material( s.Material );
float H( float x ) => s.HeightAt( x, length );
Vector3 At( float x, float y, float lift = 0 ) => track.PlaceAt( Math.Clamp( x, 0, length ), y, H( x ) + lift );
var steps = Math.Clamp( (int)MathF.Ceiling( length / 64 ), 8, 256 );
var stations = Enumerable.Range( 0, steps + 1 ).Select( i => length * i / steps ).ToList();
if ( s.RampLength > 0 )
{
stations.Add( MathF.Min( s.RampLength, length * .45f ) );
stations.Add( length - MathF.Min( s.RampLength, length * .45f ) );
}
stations = stations.Distinct().OrderBy( x => x ).ToList();
void Surface( string id, string kind, float right, float left, float bottom, float top, Material material, Color? tint = null )
{
var part = context.Solid( id, kind, kind, "bridge", material, kit =>
{
var upper = new Vector3[stations.Count, 2];
var lower = new Vector3[stations.Count, 2];
for ( var i = 0; i < stations.Count; i++ )
{
upper[i, 0] = At( stations[i], right, top );
upper[i, 1] = At( stations[i], left, top );
lower[i, 0] = At( stations[i], right, bottom );
lower[i, 1] = At( stations[i], left, bottom );
}
kit.Shell( upper, lower );
} );
part.Tint = tint;
root.Add( part );
}
Surface( "deck", "BridgeDeck", -half, half, -thick, -2, steel, new Color( .57f, .59f, .61f ) );
Surface( "surface", "BridgeRoadSurface", -roadHalf, roadHalf, -2, 0, context.Material( s.SurfaceMaterial ), new Color( .15f, .16f, .17f ) );
foreach ( var sign in new[]
{
-1,
1
}
)
{
var side = sign < 0 ? "right" : "left";
var y = sign * (half - 6);
var lo = sign < 0 ? -half : roadHalf;
var hi = sign < 0 ? -roadHalf : half;
if ( walk > 0 )
Surface( "walk/" + side, "Footway", lo, hi, -2, 6, steel, new Color( .68f, .69f, .68f ) );
root.Add( context.Solid( "girder/" + side, "Longitudinal Girder", "Girder", "bridge", steel, kit =>
{
for ( var i = 0; i + 1 < stations.Count; i++ )
kit.Beam( At( stations[i], sign * half * .55f, -thick - 8 ), At( stations[i + 1], sign * half * .55f, -thick - 8 ), 8, 16 );
} ) );
if ( s.RailingHeight > 0 )
root.Add( context.Solid( "rail/" + side, "Posts, base plates and double handrail", "Railing", "bridge", steel, kit =>
{
for ( var i = 0; i + 1 < stations.Count; i++ )
foreach ( var h in new[]
{
s.RailingHeight * .5f,
s.RailingHeight
}
)
kit.Beam( At( stations[i], y, 6 + h ), At( stations[i + 1], y, 6 + h ), 3, 3 );
var count = Math.Max( 1, (int)MathF.Ceiling( length / MathF.Max( s.PostSpacing, 48 ) ) );
for ( var i = 0; i <= count; i++ )
{
var x = length * i / count;
var a = At( x, y, 6 );
kit.Beam( a, At( x, y, 6 + s.RailingHeight ), 4, 4 );
var frame = track.FrameAt( x );
kit.Frame = new Transform( a, Rotation.LookAt( frame.Forward, Vector3.Up ) );
kit.Box( new Vector3( -5, -5, 0 ), new Vector3( 5, 5, 2 ) );
kit.Frame = Transform.Zero;
}
} ) );
if ( s.Style == WorldBridgeStyle.Truss )
root.Add( context.Solid( "truss/" + side, "Framed side truss", "Truss", "bridge", steel, kit =>
{
var count = Math.Clamp( (int)MathF.Ceiling( length / 128 ), 2, 64 );
var height = Math.Clamp( s.Width * .4f, 64, 144 );
for ( var i = 0; i < count; i++ )
{
var a = length * i / count;
var b = length * (i + 1) / count;
kit.Beam( At( a, y, 8 ), At( b, y, 8 ), 6, 6 );
kit.Beam( At( a, y, height ), At( b, y, height ), 6, 6 );
kit.Beam( At( a, y, 8 ), At( a, y, height ), 6, 6 );
kit.Beam( At( a, y, i % 2 == 0 ? 8 : height ), At( b, y, i % 2 == 0 ? height : 8 ), 5, 5 );
}
kit.Beam( At( length, y, 8 ), At( length, y, height ), 6, 6 );
} ) );
if ( s.Style == WorldBridgeStyle.Arch )
root.Add( context.Solid( "arch/" + side, "Arch rib and vertical hangers", "Arch", "bridge", steel, kit =>
{
var count = Math.Clamp( (int)MathF.Ceiling( length / 64 ), 8, 64 );
float H( float x ) => 16 + MathF.Min( length * .16f, 160 ) * MathF.Sin( MathF.PI * x / length );
for ( var i = 0; i < count; i++ )
{
var a = length * i / count;
var b = length * (i + 1) / count;
kit.Beam( At( a, y, H( a ) ), At( b, y, H( b ) ), 8, 8 );
if ( i > 0 )
kit.Beam( At( a, y, 6 ), At( a, y, H( a ) ), 3, 3 );
}
} ) );
}
root.Add( context.Solid( "markings", "Broken centre line", "RoadMarking", "bridge", context.Material( null ), kit =>
{
for ( var x = 12f; x < length - 12; x += 80 )
{
var end = MathF.Min( x + 40, length - 12 );
kit.Beam( At( x, 0, .2f ), At( end, 0, .2f ), 3, .2f );
}
} ) );
root.Add( context.Solid( "crossbeams", "Deck crossbeams", "Crossbeam", "bridge", steel, kit =>
{
for ( var x = 96f; x < length; x += 192 )
kit.Beam( At( x, -half * .65f, -thick - 8 ), At( x, half * .65f, -thick - 8 ), 12, 16 );
} ) );
foreach ( var end in new[]
{
0f,
length
}
)
root.Add( context.Solid( end == 0 ? "abutment/start" : "abutment/end", "Bank abutment and bearing shelf", "Abutment", "bridge", steel, kit =>
{
var frame = track.FrameAt( end );
var p = At( end, 0, -thick - 20 );
kit.Frame = new Transform( p, Rotation.LookAt( frame.Forward, Vector3.Up ) );
var inward = end == 0 ? -1 : 1;
kit.Box( new Vector3( MathF.Min( 0, inward * 32 ), -half, -MathF.Max( s.BankDepth - thick - 20, 8 ) ), new Vector3( MathF.Max( 0, inward * 32 ), half, 0 ) );
kit.Box( new Vector3( -12, -half, -4 ), new Vector3( 12, half, 0 ) );
foreach ( var sign in new[]
{
-1,
1
}
)
kit.Box( new Vector3( -10, sign * half * .55f - 10, 0 ), new Vector3( 10, sign * half * .55f + 10, 6 ) );
} ) );
if ( s.Supports )
{
var span = MathF.Max( s.SupportSpacing, 128 );
var count = Math.Min( 128, (int)(length / span) );
for ( var i = 1; i <= count; i++ )
{
float? chosen = null;
foreach ( var delta in new[]
{
0f,
-96,
96,
-192,
192
}
)
{
var x = length * i / (count + 1) + delta;
if ( x < 64 || x > length - 64 || H( x ) < thick + 64 )
continue;
var pos = At( x, 0 );
if ( s.Keepouts.Any( k => k.Contains( new Vector2( pos.x, pos.y ) ) ) || (x >= s.NoPierFrom && x <= s.NoPierTo) )
continue;
chosen = x;
break;
}
if ( chosen is not float station )
continue;
root.Add( context.Solid( $"support/{i}", "Pier, footing, crosshead and bearings", "Pier", "bridge", steel, kit =>
{
var top = H( station ) - thick - 20;
var frame = track.FrameAt( station );
var origin = frame.Position;
kit.Frame = new Transform( origin, Rotation.LookAt( frame.Forward, Vector3.Up ) );
kit.Box( new Vector3( -24, -half * .5f, -12 ), new Vector3( 24, half * .5f, 0 ) );
kit.Box( new Vector3( -12, -half * .3f, 0 ), new Vector3( 12, half * .3f, top - 12 ) );
kit.Box( new Vector3( -20, -half * .7f, top - 12 ), new Vector3( 20, half * .7f, top ) );
foreach ( var sign in new[]
{
-1,
1
}
)
kit.Box( new Vector3( -10, sign * half * .55f - 10, top ), new Vector3( 10, sign * half * .55f + 10, top + 6 ) );
} ) );
}
}
return root;
}
}
public sealed class DistrictCanalSettings : GeneratorSettings
{
public List<Vector2> Outline { get; set; } = [];
public Vector2 Center { get; set; }
[Property, Group( "Canal" ), Range( 16, 2048, slider: false ), WideMode]
public float Width { get; set; } = 320;
[Property, Group( "Canal" ), Range( 16, 512, slider: false ), WideMode]
public float Depth { get; set; } = 96;
[Property, Group( "Canal" ), WideMode]
public float Angle { get; set; } = 90;
public float BaseHeight { get; set; }
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string WaterMaterial { get; set; }
[Property, Group( "Materials" ), ResourceType( "vmat" ), WideMode]
public string BankMaterial { get; set; }
}
public sealed class DistrictCanalGenerator : IPartGenerator
{
public string Id => "district_canal";
public string Title => "World Builder Canal";
public Type SettingsType => typeof(DistrictCanalSettings);
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (DistrictCanalSettings)settings;
var root = context.Group( "root", Title, "Canal", "water" );
if ( s.Outline.Count < 3 )
return root;
var a = s.Angle.DegreeToRadian();
var u = new Vector2( MathF.Cos( a ), MathF.Sin( a ) );
var v = new Vector2( -u.y, u.x );
var half = MathF.Max( s.Width, 16 ) * .5f;
var reach = s.Outline.Max( p => (p - s.Center).Length ) + s.Width + 64;
void Piece( string id, float lo, float hi, float bottom, float top, string material, Color? tint = null )
{
var ring = DistrictLayout.Clip( s.Outline, DistrictContent.Strip( s.Center, u, v, lo, hi, reach ) );
if ( ring.Count < 3 || DistrictLayout.Area( ring ) < 1 )
return;
var node = context.Solid( id, id, id == "water" ? "Water" : "CanalBank", "water", context.Material( material ), kit => kit.Prism( ring.Select( p => new Vector3( p.x, p.y, 0 ) ).ToList(), bottom, top ) );
node.Tint = tint;
root.Add( node );
}
Piece( "bed", -half, half, s.BaseHeight - s.Depth - 8, s.BaseHeight - s.Depth, s.BankMaterial );
Piece( "bank/right", -half - 8, -half, s.BaseHeight - s.Depth, s.BaseHeight, s.BankMaterial );
Piece( "bank/left", half, half + 8, s.BaseHeight - s.Depth, s.BaseHeight, s.BankMaterial );
var level = s.BaseHeight - MathF.Min( s.Depth * .25f, 24 );
Piece( "water", -half, half, level - .25f, level, s.WaterMaterial, new Color( .12f, .36f, .47f ) );
return root;
}
}