Editor-side utility for bridge/arch placement in road editor. Computes bridge spans, soffit/girder/kerb dimensions, pier/bent placement, wing walls, sections and related geometry by sampling road curve, frames, ground and kit parameters.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Where a road is carried on a structure - answered here and nowhere else, or four consumers drift.
public static class ArchBridge
{
// Pier height with no terrain - skipped piers read as a deck floating on air.
const float NominalRise = 168f;
public const float ShortestSpan = 96f;
// Below this a pier reads as a fin in the median - better moved than squeezed in.
const float Narrowest = 18f;
// A bent only decides which median it lands in - that is measured in feet.
const float HuntStep = 12f;
const float ProbeStep = 24f;
// A clamp, not a search: past this the drag was describing something else.
const float Trimmed = 0.4f;
public static ArchBridgePart Carrying( ArchRoadPart road, float distance )
{
return new ArchRoadAttachmentService( road ).BridgeAt( distance );
}
public static bool Carried( ArchRoadPart road, float distance ) => Carrying( road, distance ) is not null;
// The ONE soffit answer the carriageway and both verges take - a verge hangs a kerb's height lower.
public static float? Soffit( ArchRoadPart road, ArchFrame frame )
{
var bridge = Carrying( road, frame.Distance );
return bridge is null ? null : Soffit( road, bridge, frame );
}
// Read off the RESOLVED spans - their ends moved to the lip of what they cross.
public static float? Soffit( IReadOnlyList<ArchBridgeShape> shapes, ArchRoadPart road, ArchFrame frame )
{
foreach ( var shape in shapes )
{
if ( shape.Carries( frame.Distance ) )
{
return Soffit( road, shape.Part, frame );
}
}
return null;
}
public static float Soffit( ArchRoadPart road, ArchBridgePart part, ArchFrame frame )
{
return frame.Position.z - MathF.Max( 0f, road.Camber ) - Slab( part );
}
public static float Slab( ArchBridgePart part ) => MathF.Max( 4f, part.Slab );
public static float Girder( ArchBridgePart part ) => MathF.Max( 4f, part.Girder );
public static float Upstand( ArchBridgePart part ) => MathF.Max( 0f, part.Upstand );
public static float Kerb( ArchBridgePart part ) => Math.Clamp( part.UpstandWidth, 4f, 48f );
// Both ends of every span: the deck steps into its depth over one cell; a straight bridge survives.
public static IEnumerable<float> Stations( ArchRoadPart road )
{
return new ArchRoadAttachmentService( road ).BridgeStations();
}
// Half a post in so the post's face lines up with the fascia - not BarrierInset: a parapet has nowhere to sit back to.
public static float Stand( ArchRoadPart road, ArchBridgePart part, bool right )
{
return MathF.Max( 4f, road.Reach( right ) - MathF.Max( 2f, part.Barrier.PostSize ) * 0.5f );
}
// Over a bridge the deck IS the road.
public static float Edge( ArchRoadPart road, ArchFrame frame, bool right ) => road.Reach( right, frame.WidthScale );
// Set back from the deck edge, never so far the two sides meet.
public static float Beam( ArchRoadPart road, ArchBridgePart part, ArchFrame frame, bool right )
{
var edge = Edge( road, frame, right );
return MathF.Max( edge * 0.25f, edge - MathF.Max( 0f, part.EdgeBeam ) );
}
// Resolved on the road's OWN frames; the plan comes in so piers clear the roads they fly over.
public static List<ArchBridgeShape> Shapes(
ArchRoadPart road,
ArchPlan plan,
ArchCurve curve,
IReadOnlyList<ArchFrame> frames,
ArchKit kit,
ArchGround ground )
{
var shapes = new List<ArchBridgeShape>();
foreach ( var part in ArchLayerGate.Enabled( road.Bridges ) )
{
var shape = Resolve( road, part, plan, curve, frames, kit, ground );
if ( shape.IsUsable )
{
shapes.Add( shape );
}
}
return shapes;
}
public static ArchBridgeShape Resolve(
ArchRoadPart road,
ArchBridgePart part,
ArchPlan plan,
ArchCurve curve,
IReadOnlyList<ArchFrame> frames,
ArchKit kit,
ArchGround ground )
{
return new ArchBridgeShapeBuilder()
.On( road, part )
.WithPlan( plan )
.Along( curve )
.WithFrames( frames )
.WithKit( kit )
.Over( ground )
.Create();
}
internal static ArchBridgeShape ResolveCore(
ArchRoadPart road,
ArchBridgePart part,
ArchPlan plan,
ArchCurve curve,
IReadOnlyList<ArchFrame> frames,
ArchKit kit,
ArchGround ground )
{
var span = Clamped( part, curve.Length );
var empty = new ArchBridgeShape { Part = part, Road = road, Span = span };
if ( span.Length < ShortestSpan || frames.Count < 2 )
{
return empty;
}
span = Lipped( road, part, curve, span, ground, kit );
var deck = ArchJunction.Clipped( curve, frames, span );
if ( deck.Count < 2 )
{
return empty;
}
var grade = Grade( road, frames, kit );
var under = ArchBridgeGap.Roads( plan );
var division = Divided( part, span.Length );
var bents = new List<ArchBridgeBent>();
foreach ( var offset in division.Inner )
{
if ( Stood( road, part, curve, span, offset, division.Step, under, kit, ground, grade, bents ) is { } bent )
{
bents.Add( bent );
}
}
var near = Landing( road, part, deck[0], true, ground, grade );
var far = Landing( road, part, deck[^1], false, ground, grade );
var wings = new List<ArchWingWall>();
wings.AddRange( Wings( road, part, near, true, ground ) );
wings.AddRange( Wings( road, part, far, false, ground ) );
return new ArchBridgeShape
{
Part = part,
Road = road,
Span = span,
Frames = deck,
Bents = bents,
Wings = wings,
Near = near,
Far = far
};
}
// A span starts at the LIP of the crossing - an abutment inside the bank is buried; a clamp, not a search.
static ArchRoadSpan Lipped( ArchRoadPart road, ArchBridgePart part, ArchCurve curve, ArchRoadSpan span, ArchGround ground, ArchKit kit )
{
var reach = span.Length * Trimmed;
var lipped = new ArchRoadSpan
{
From = span.From + Lip( road, part, curve, span.From, reach, 1f, ground, kit ),
To = span.To - Lip( road, part, curve, span.To, reach, -1f, ground, kit )
};
return lipped.Length < ShortestSpan ? span : lipped;
}
// The first clear station is the crest, so the ground an abutment-depth further in must clear too.
static float Lip( ArchRoadPart road, ArchBridgePart part, ArchCurve curve, float station, float reach, float inward, ArchGround ground, ArchKit kit )
{
var clear = MathF.Max( 0f, kit.GroundClearance );
var depth = MathF.Max( 12f, part.AbutmentDepth );
for ( var walked = 0f; walked <= reach; walked += ProbeStep )
{
if ( Clear( road, part, curve, station + walked * inward, ground, clear )
&& Clear( road, part, curve, station + (walked + depth) * inward, ground, clear ) )
{
return walked;
}
}
return 0f;
}
static bool Clear( ArchRoadPart road, ArchBridgePart part, ArchCurve curve, float station, ArchGround ground, float clear )
{
if ( !curve.Sample( station, out var frame ) )
{
return false;
}
var bearing = Soffit( road, part, frame ) - Girder( part ) - clear;
return ground.Under( frame.Flat, bearing - 1f ) < bearing;
}
// Piers stand between the abutments: two bents cut the span into three bays; the ends never get a pier.
static ArchDivision Divided( ArchBridgePart part, float length )
{
return part.Bents > 0
? ArchDivide.Into( length, part.Bents + 1 )
: ArchDivide.AtMost( length, MathF.Max( ShortestSpan, part.BentSpacing ) );
}
// Hunts outward from the wanted station - the median is the first gap that fits; never stands on a road.
static ArchBridgeBent? Stood(
ArchRoadPart road,
ArchBridgePart part,
ArchCurve curve,
ArchRoadSpan span,
float offset,
float bay,
IReadOnlyList<ArchRoadWalk> under,
ArchKit kit,
ArchGround ground,
float grade,
IReadOnlyList<ArchBridgeBent> placed )
{
var depth = MathF.Max( 8f, part.PierDepth );
foreach ( var station in Hunted( offset, bay * 0.5f ) )
{
if ( station < depth * 0.5f || station > span.Length - depth * 0.5f )
{
continue;
}
if ( !curve.Sample( span.From + station, out var frame ) || Crowded( placed, frame, depth, kit ) )
{
continue;
}
var bent = Bent( road, part, frame, ground, grade );
var stands = Stands( road, part, frame, under, kit, ground, bent.Ground );
if ( stands.Count == 0 )
{
continue;
}
return bent with { Stands = stands };
}
return null;
}
static IEnumerable<float> Hunted( float offset, float reach )
{
yield return offset;
for ( var hunted = HuntStep; hunted <= MathF.Max( 0f, reach ); hunted += HuntStep )
{
yield return offset - hunted;
yield return offset + hunted;
}
}
static bool Crowded( IReadOnlyList<ArchBridgeBent> placed, ArchFrame frame, float depth, ArchKit kit )
{
foreach ( var bent in placed )
{
if ( MathF.Abs( bent.Frame.Distance - frame.Distance ) < depth + MathF.Max( 0f, kit.PierClearance ) )
{
return true;
}
}
return false;
}
// Where a bent's shafts stand, resolved HERE - the geometry, the ghost and arch_measure cannot disagree.
static List<ArchBridgeStand> Stands(
ArchRoadPart road,
ArchBridgePart part,
ArchFrame frame,
IReadOnlyList<ArchRoadWalk> under,
ArchKit kit,
ArchGround ground,
float datum )
{
var edges = Edges( road, part, frame );
var domain = Standing( edges, part );
var gaps = under.Count == 0
? new List<ArchGap> { domain }
: ArchBridgeGap.Across( under, frame, Soffit( road, part, frame ), MathF.Max( 8f, part.PierDepth ), domain, kit );
var stands = new List<ArchBridgeStand>();
foreach ( var gap in gaps )
{
if ( Stand( part, gap, domain, edges ) is { } stand )
{
stands.Add( stand with { Ground = Footed( frame, stand, ground, datum ) } );
}
}
return stands;
}
static float Footed( ArchFrame frame, ArchBridgeStand stand, ArchGround ground, float datum )
{
var lowest = ground.Under( Flat( frame, stand.From ), datum );
lowest = MathF.Min( lowest, ground.Under( Flat( frame, stand.To ), datum ) );
foreach ( var across in stand.Shafts )
{
lowest = MathF.Min( lowest, ground.Under( Flat( frame, across ), datum ) );
}
return lowest;
}
// A blade IS the gap; columns spread across it, held off the edges by half their own width.
static ArchBridgeStand? Stand( ArchBridgePart part, ArchGap gap, ArchGap domain, ArchGap edges )
{
if ( !gap.Holds( Narrowest ) )
{
return null;
}
var blade = part.Pier == PierStyle.Blade;
var width = blade ? gap.Width : Math.Clamp( part.PierWidth, 6f, gap.Width );
// The headstock cantilevers to the deck edge where clear; stops where a gap blocks it.
return new ArchBridgeStand
{
From = gap.From <= domain.From + 1f ? edges.From : gap.From,
To = gap.To >= domain.To - 1f ? edges.To : gap.To,
Width = width,
Shafts = blade ? new List<float> { gap.Middle } : Columns( part, gap, width )
};
}
static List<float> Columns( ArchBridgePart part, ArchGap gap, float width )
{
var run = MathF.Max( 0f, gap.Width - width );
var wanted = Math.Clamp( part.Columns, 1, 12 );
var columns = Math.Clamp( (int)MathF.Floor( run / MathF.Max( 1f, width * 2f ) ) + 1, 1, wanted );
if ( columns == 1 )
{
return new List<float> { gap.Middle };
}
var shafts = new List<float>();
foreach ( var along in ArchDivide.Into( run, columns - 1 ).Nodes )
{
shafts.Add( gap.From + width * 0.5f + along );
}
return shafts;
}
// Squared to the road's frame so a pier faces the traffic; read by the generator AND the ghost.
public static List<Vector3> Section( ArchFrame frame, float across, float width, float depth, float height )
{
var half = width * 0.5f;
var reach = frame.Along * (depth * 0.5f);
var near = frame.Side( across - half ).WithZ( height ) - reach;
var far = frame.Side( across + half ).WithZ( height ) - reach;
return new List<Vector3> { near, far, far + reach * 2f, near + reach * 2f };
}
public static ArchGap Edges( ArchRoadPart road, ArchBridgePart part, ArchFrame frame )
{
return new ArchGap { From = -Beam( road, part, frame, false ), To = Beam( road, part, frame, true ) };
}
// Stepped in by the same setback the fascia steps in by; never so far it closes.
public static ArchGap Standing( ArchGap edges, ArchBridgePart part )
{
var pinch = MathF.Min( MathF.Max( 0f, part.EdgeBeam ), MathF.Max( 0f, (edges.Width - 12f) * 0.5f ) );
return new ArchGap { From = edges.From + pinch, To = edges.To - pinch };
}
static ArchBridgeBent Bent( ArchRoadPart road, ArchBridgePart part, ArchFrame frame, ArchGround ground, float grade )
{
var bearing = Soffit( road, part, frame ) - Girder( part );
return new ArchBridgeBent
{
Frame = frame,
Bearing = bearing,
Ground = ground.Under( frame.Flat, MathF.Min( grade, bearing - NominalRise ) ),
Stands = new List<ArchBridgeStand>()
};
}
// An abutment reaches the ground fallen away in FRONT of it - probed over its own footprint, so self-limiting.
static ArchBridgeBent Landing( ArchRoadPart road, ArchBridgePart part, ArchFrame frame, bool near, ArchGround ground, float grade )
{
var bearing = Soffit( road, part, frame ) - Girder( part );
var datum = MathF.Min( grade, bearing - NominalRise );
var into = near ? frame.Along : -frame.Along;
var ahead = new Vector2( into.x, into.y );
var lowest = ground.Under( frame.Flat, datum );
foreach ( var step in ArchDivide.AtMost( MathF.Max( 12f, part.AbutmentDepth ), ProbeStep ).Nodes )
{
lowest = MathF.Min( lowest, ground.Under( frame.Flat + ahead * step, datum ) );
}
return new ArchBridgeBent
{
Frame = frame,
Bearing = bearing,
Ground = lowest,
Stands = new List<ArchBridgeStand>()
};
}
// No terrain: the road's own approaches, as the LOWER of that and a nominal pier's worth.
static float Grade( ArchRoadPart road, IReadOnlyList<ArchFrame> frames, ArchKit kit )
{
var lowest = float.MaxValue;
foreach ( var frame in frames )
{
lowest = MathF.Min( lowest, frame.Position.z );
}
return lowest - MathF.Max( 0f, kit.GroundClearance );
}
// Coped to the deck's EDGE, not the soffit - and nothing built where there is no fill to retain.
static IEnumerable<ArchWingWall> Wings( ArchRoadPart road, ArchBridgePart part, ArchBridgeBent bent, bool near, ArchGround ground )
{
if ( !part.Abutments || bent.Rise < Girder( part ) )
{
return Array.Empty<ArchWingWall>();
}
var frame = bent.Frame;
return ArchWing.Splayed( frame, near ? -frame.Along : frame.Along,
Edge( road, frame, false ), Edge( road, frame, true ),
MathF.Max( 0f, part.WingWall ), part.WingSplay,
Soffit( road, part, frame ) + Slab( part ), bent.Ground, ground );
}
static Vector2 Flat( ArchFrame frame, float across )
{
var point = frame.Side( across );
return new Vector2( point.x, point.y );
}
// Clamped and ordered, so a span dragged backwards or off the road's end still describes a stretch.
public static ArchRoadSpan Clamped( ArchBridgePart part, float length )
{
var from = Math.Clamp( MathF.Min( part.From, part.To ), 0f, length );
var to = Math.Clamp( MathF.Max( part.From, part.To ), 0f, length );
return new ArchRoadSpan { From = from, To = to };
}
}