Editor-side static generator for road geometry. It computes frames along a road curve, resolves bridges, tunnels and junction spans, and emits mesh-building calls for carriageways, pavements, markings, barriers, bridge and tunnel parts into an ArchOutputService.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchRoadGen
{
public static void Build( ArchOutputService output, string path, ArchRoadPart road, ArchPlan plan, ArchKit kit, ArchStyle style, IReadOnlyList<ArchJunctionNode> junctions, ArchGround ground )
{
var curve = road.Curve();
if ( !curve.IsUsable )
{
return;
}
var frames = Frames( road, curve );
if ( frames.Count < 2 )
{
return;
}
var crossings = ArchCrossover.Crossings( road, plan, curve );
// Resolved first: the resolved span, not the drag's numbers, is where the structure actually is.
var bridges = ArchBridge.Shapes( road, plan, curve, Frames( road, curve, crossings, kit ), kit, ground );
var tunnels = ArchTunnel.Shapes( road, curve, Frames( road, curve, crossings, kit ), kit, ground, plan );
// A road stops at every junction it reaches; the junction fills what lies between the spans.
var spans = ArchJunction.Spans( road, junctions, curve.Length );
var carves = Carves( plan, road, kit );
for ( var index = 0; index < spans.Count; index++ )
{
var span = spans[index];
var suffix = spans.Count > 1 ? $"_{index + 1}" : "";
var swept = ArchJunction.Clipped( curve, frames, span );
if ( swept.Count < 2 )
{
continue;
}
// A junction-cut span has two ends, so only an untouched loop still closes.
var loop = road.Closed && span.Length >= curve.Length - 1f;
output.Add( $"{path}/{ArchPieces.Carriageway}{suffix}", Transform.Zero, canvas => Carriageway( canvas, road, bridges, swept, style, loop, carves ) );
foreach ( var right in Sides( road.Pavements ) )
{
output.Add( $"{path}/{ArchPieces.Pavement}/{ArchPieces.Side( right )}{suffix}", Transform.Zero,
canvas => ArchRoadKerb.Build( canvas, road, bridges, curve, crossings, right, kit, style, span, loop, carves ) );
}
if ( road.Markings )
{
output.Add( $"{path}/{ArchPieces.Markings}{suffix}", Transform.Zero,
canvas => ArchRoadLines.Build( canvas, road, curve, crossings, kit, style, span, carves ) );
}
}
foreach ( var right in Sides( road.Barriers ) )
{
output.Add( $"{path}/{ArchPieces.Barrier}/{ArchPieces.Side( right )}", Transform.Zero, canvas => Barrier( canvas, road, bridges, tunnels, curve, right, kit, style ) );
}
// The carriageway already swept the deck; here is only what hangs below it and stands on it.
foreach ( var bridge in bridges )
{
var name = ArchNames.Bridge( bridge.Part );
var shape = bridge;
output.Add( $"{path}/{name}/{ArchPieces.Girder}", Transform.Zero, canvas => ArchBridgeGen.Girder( canvas, shape, kit, style ) );
output.Add( $"{path}/{name}/{ArchPieces.Bents}", Transform.Zero, canvas => ArchBridgeGen.Bents( canvas, shape, kit, style ) );
if ( shape.Part.Abutments )
{
output.Add( $"{path}/{name}/{ArchPieces.Abutments}", Transform.Zero, canvas => ArchBridgeGen.Abutments( canvas, shape, kit, style ) );
}
foreach ( var right in new[] { false, true } )
{
var side = right;
output.Add( $"{path}/{name}/{ArchPieces.Upstand}/{ArchPieces.Side( side )}", Transform.Zero,
canvas => ArchBridgeGen.Upstand( canvas, shape, side, kit, style ) );
if ( !shape.Part.Parapet )
{
continue;
}
output.Add( $"{path}/{name}/{ArchPieces.Parapet}/{ArchPieces.Side( side )}", Transform.Zero,
canvas => ArchBridgeGen.Parapet( canvas, shape, side, curve, kit, style ) );
}
}
// The road runs through the bore on the ground; here is only the bore around it and the portals.
foreach ( var tunnel in tunnels )
{
var name = ArchNames.Tunnel( tunnel.Part );
var shape = tunnel;
output.Add( $"{path}/{name}/{ArchPieces.Lining}", Transform.Zero, canvas => ArchTunnelGen.Lining( canvas, shape, style ) );
output.Add( $"{path}/{name}/{ArchPieces.Invert}", Transform.Zero, canvas => ArchTunnelGen.Invert( canvas, shape, kit, style ) );
foreach ( var right in new[] { false, true } )
{
var side = right;
if ( !ArchTunnelGen.Walked( road, shape.Part, side ) )
{
continue;
}
output.Add( $"{path}/{name}/{ArchPieces.Walkway}/{ArchPieces.Side( side )}", Transform.Zero,
canvas => ArchTunnelGen.Walkway( canvas, shape, side, kit, style ) );
}
if ( !shape.Part.Portals )
{
continue;
}
output.Add( $"{path}/{name}/{ArchPieces.Portal}/{ArchPieces.Near}", Transform.Zero, canvas => ArchTunnelPortal.Build( canvas, shape, shape.Near, kit, style ) );
output.Add( $"{path}/{name}/{ArchPieces.Portal}/{ArchPieces.Far}", Transform.Zero, canvas => ArchTunnelPortal.Build( canvas, shape, shape.Far, kit, style ) );
}
}
// The cuts standing over this road, resolved ONCE for every strip it lays: a hole in the street is world-space
// like every other, so the carriageway and the pavement ask ArchCut what covers them exactly as a slab does.
// Scoped to the road as its own host, and ordered against it, so a cut made before the road was laid leaves it
// alone. A road stands on no storey, which is why the level asked for is the ground's.
static List<ArchCarveVolume> Carves( ArchPlan plan, ArchRoadPart road, ArchKit kit )
{
if ( plan is null )
{
return new List<ArchCarveVolume>();
}
return ArchCut.Volumes( plan, 0, kit, float.MinValue * 0.5f, float.MaxValue * 0.5f,
road.Id, ArchCutAffects.Roadway, road.Id ).ToList();
}
public static List<ArchFrame> Frames( ArchRoadPart road, ArchCurve curve )
{
return curve.Walk( road.Precision, road.Simplify, road.StraightThreshold, road.MinimumStraightRun,
ArchBridge.Stations( road ).Concat( ArchTunnel.Stations( road, curve ) ) );
}
// Pins crossover edges and bridge ends: simplification collapses exactly the frames those features are described at.
public static List<ArchFrame> Frames( ArchRoadPart road, ArchCurve curve, IReadOnlyList<ArchRoadCrossing> crossings, ArchKit kit )
{
return curve.Walk( road.Precision, road.Simplify, road.StraightThreshold, road.MinimumStraightRun,
ArchCrossover.Stations( road, crossings, kit )
.Concat( ArchBridge.Stations( road ) )
.Concat( ArchTunnel.Stations( road, curve ) ) );
}
public static IEnumerable<bool> Sides( RoadSide setting )
{
if ( setting is RoadSide.Left or RoadSide.Both )
{
yield return false;
}
if ( setting is RoadSide.Right or RoadSide.Both )
{
yield return true;
}
}
// Left edge, crown, right edge: the camber crease is a real edge loop, not a shading trick.
static void Carriageway(
ArchMesh canvas,
ArchRoadPart road,
IReadOnlyList<ArchBridgeShape> bridges,
IReadOnlyList<ArchFrame> frames,
ArchStyle style,
bool closed,
IReadOnlyList<ArchCarveVolume> carves )
{
var brush = style.Brush( ArchSurface.Road, road.Palette );
var camber = MathF.Max( 0f, road.Camber );
var rows = new List<Vector3[]>();
foreach ( var frame in frames )
{
var half = road.HalfWidth * frame.WidthScale;
rows.Add( new[]
{
frame.Side( -half, -camber ),
frame.Position,
frame.Side( half, -camber )
} );
}
ArchMeshSweep.Solid( canvas, rows, frames, road.Thickness, new[] { brush }, brush, closed,
soffit: row => ArchBridge.Soffit( bridges, road, frames[row] ), carves: carves );
}
// Gives up the stretch a bridge parapet takes over, and any stretch inside a bore.
static void Barrier(
ArchMesh canvas,
ArchRoadPart road,
IReadOnlyList<ArchBridgeShape> bridges,
IReadOnlyList<ArchTunnelShape> tunnels,
ArchCurve curve,
bool right,
ArchKit kit,
ArchStyle style )
{
var stand = Verge( road, right );
var lift = road.Carries( road.Pavements, right ) ? road.KerbHeight : 0f;
var shape = ArchBarrierShape.Resolve( curve, road.Barrier, right ? stand : -stand, lift, kit );
ArchBarrierGen.Build( canvas, shape, road.Barrier, kit, ArchBarrierBrushes.Of( style, road.Palette ),
distance => Parapeted( bridges, distance ) || Lined( tunnels, distance ) );
}
// On the RESOLVED span: the two railings meet at the abutment, neither stretch left bare.
static bool Parapeted( IReadOnlyList<ArchBridgeShape> bridges, float distance )
{
foreach ( var bridge in bridges )
{
if ( bridge.Part.Parapet && bridge.Carries( distance ) )
{
return true;
}
}
return false;
}
static bool Lined( IReadOnlyList<ArchTunnelShape> tunnels, float distance )
{
foreach ( var tunnel in tunnels )
{
if ( tunnel.Carries( distance ) )
{
return true;
}
}
return false;
}
// Where a barrier stands: the outer edge of whatever carries it, pulled in by the inset.
public static float Verge( ArchRoadPart road, bool right )
{
return MathF.Max( 4f, road.Reach( right ) - MathF.Max( 0f, road.BarrierInset ) );
}
}