editor/AdvancedMappingTools/Geometry/SuspensionBridgeEditor.cs

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