Editor tool that builds a suspension bridge mesh along a drawn path. Generates the deck sweep, two towers at each end, sagging main cables, hangers and railings.
namespace AdvancedMappingTools;
/// <summary>
/// A suspension bridge along a path: deck, two towers, main cables with sag and hangers.
/// The path runs from one end of the bridge to the other.
/// </summary>
[ToolCategory( "Tunnels & Bridges" )]
[Title( "Suspension Bridge" ), Icon( "cable" )]
public sealed class SuspensionBridgeEditor( PrimitiveTool tool ) : PathEditor( tool )
{
protected override bool SnapsToLines => true;
protected override int MinPoints => 2;
[Property, Group( "Bridge" ), Range( 16, 1024, slider: false ), WideMode]
public float Width { get; set; } = 96.0f;
[Property, Group( "Bridge" ), Range( 1, 64, slider: false ), WideMode]
public float Thickness { get; set; } = 8.0f;
[Property, Group( "Bridge" ), Title( "Tower Height" ), Range( 16, 1024, slider: false ), WideMode]
public float TowerHeight { get; set; } = 192.0f;
[Property, Group( "Bridge" ), Range( 0, 512, slider: false ), WideMode, Description( "How far the cable sags in the middle." )]
public float Sag { get; set; } = 64.0f;
[Property, Group( "Bridge" ), Title( "Hanger Spacing" ), Range( 8, 256, slider: false ), WideMode]
public float HangerSpacing { get; set; } = 32.0f;
[Property, Group( "Bridge" ), Title( "Railing Height" ), Range( 0, 128, slider: false ), WideMode]
public float RailingHeight { get; set; } = 36.0f;
static float Arch( float t )
{
var s = 2.0f * t - 1.0f;
return 1.0f - s * s;
}
protected override void BuildMesh( MeshKit kit )
{
var path = SamplePath();
if ( path.Count < 2 ) return;
var width = MathF.Max( Width, 16.0f );
var half = width * 0.5f;
var t = Math.Clamp( Thickness, 1.0f, 64.0f );
var tower = MathF.Max( TowerHeight, 16.0f );
var sag = MathF.Max( Sag, 0.0f );
kit.Material = SurfaceMaterial;
SweepPath( kit, path, Closed, [new( -half, 0.0f ), new( half, 0.0f ), new( half, t ), new( -half, t )] );
var start = path[0];
var end = path[^1];
var direction = (end - start).WithZ( 0.0f ).Normal;
var normal = new Vector3( -direction.y, direction.x, 0.0f );
kit.Material = TrimMaterial;
foreach ( var at in new[] { start, end } )
{
foreach ( var side in new[] { -1.0f, 1.0f } )
{
var basePoint = at + normal * (half + 3.0f) * side;
kit.Box( basePoint + new Vector3( -6, -6, 0 ), basePoint + new Vector3( 6, 6, tower ) );
kit.Box( basePoint + new Vector3( -10, -10, tower - 6 ), basePoint + new Vector3( 10, 10, tower ) );
}
}
var cableTop = tower - 8.0f;
var steps = Math.Clamp( (int)((end - start).Length / 32.0f), 4, 64 );
foreach ( var side in new[] { -1.0f, 1.0f } )
{
var previous = start + normal * (half + 3.0f) * side + Vector3.Up * cableTop;
for ( var i = 1; i <= steps; i++ )
{
var f = i / (float)steps;
var point = start + (end - start) * f;
var z = cableTop - sag * Arch( f );
var next = point + normal * (half + 3.0f) * side + Vector3.Up * z;
kit.Beam( previous, next, 3.0f, 3.0f );
previous = next;
}
}
if ( HangerSpacing <= 0.0f ) return;
foreach ( var (position, _) in WalkPath( path, Closed, MathF.Max( HangerSpacing, 8.0f ) ) )
{
var length = Math.Clamp( (position - start).Length / MathF.Max( (end - start).Length, 1.0f ), 0.0f, 1.0f );
var z = cableTop - sag * Arch( length );
if ( z <= t + 1.0f ) continue;
foreach ( var side in new[] { -1.0f, 1.0f } )
{
var cable = position + normal * (half + 3.0f) * side;
kit.Box( cable + new Vector3( -2, -2, t ), cable + new Vector3( 2, 2, z ) );
}
}
if ( RailingHeight <= 0.0f ) return;
foreach ( var side in new[] { -1.0f, 1.0f } )
{
var line = path.Select( p => p + normal * half * side + Vector3.Up * (t + RailingHeight) ).ToList();
for ( var i = 0; i + 1 < line.Count; i++ )
kit.Beam( line[i], line[i + 1], 3.0f, 3.0f );
}
}
}