Editor code that generates the geometry for a road junction apron and kerbs. It builds an apron mesh by creating concentric rings toward a crowned center, draws bands and wedges between rings, and generates kerb/verge geometry for junction corners using road kerb helpers. Also provides ArchRoadSheet to map world points to road sheet UV/station coordinates.
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;
namespace Sunless.Architecture;
// Corner reuses the road's own kerb section, so its heights cannot disagree with the legs.
public static class ArchJunctionGen
{
const float Step = 36f;
const int Rings = 3;
public static void Build( ArchOutputService output, string path, ArchJunctionNode node, ArchKit kit, ArchStyle style )
{
if ( node.Legs.Count < 3 )
{
return;
}
output.Add( $"{path}/Apron", Transform.Zero, canvas => Apron( canvas, node, kit, style ) );
for ( var index = 0; index < node.Corners.Count; index++ )
{
var corner = node.Corners[index];
output.Add( $"{path}/Kerb_{index + 1}", Transform.Zero, canvas => Verge( canvas, corner, kit, style ) );
}
}
// Pavement on the right: a corner runs one leg's left edge to the next's right.
static void Verge( ArchMesh canvas, ArchJunctionCorner corner, ArchKit kit, ArchStyle style )
{
var road = corner.From.Road;
if ( road.Pavements == RoadSide.None || road.PavementWidth < 6f )
{
return;
}
// Corner turns in a few hundred inches, so its walk is far finer than a road's.
var frames = corner.Path.Walk( Math.Clamp( corner.Path.Length / 12f, 8f, Step ) );
if ( frames.Count >= 2 )
{
ArchRoadKerb.Corner( canvas, road, frames, true, kit, style );
}
}
// Rings closing on the crown, not one flat plate that cannot carry a tiled surface.
static void Apron( ArchMesh canvas, ArchJunctionNode node, ArchKit kit, ArchStyle style )
{
var loop = node.Boundary( Step );
if ( loop.Count < 3 )
{
return;
}
var lead = node.Legs[0].Road;
var brush = style.Brush( ArchSurface.Road, lead.Palette );
var crown = Crown( loop, lead );
var sheet = new ArchRoadSheet( node.Legs[0] );
var rings = new List<List<Vector3>> { loop };
for ( var ring = 1; ring < Rings; ring++ )
{
var reach = ring / (float)Rings;
rings.Add( loop.Select( point => Vector3.Lerp( point, crown, reach ) ).ToList() );
}
for ( var ring = 0; ring < rings.Count - 1; ring++ )
{
Band( canvas, rings[ring], rings[ring + 1], sheet, brush );
}
Fan( canvas, rings[^1], crown, sheet, brush );
// A slab, not a skin: beside solid legs a thin surface reads as a hole.
var floor = loop.Min( point => point.z ) - MathF.Max( 1f, lead.Thickness );
var under = loop.Select( point => point.WithZ( floor ) ).ToList();
canvas.Prism( under, loop, brush, false );
canvas.Polygon( under, brush, true );
}
static void Band( ArchMesh canvas, IReadOnlyList<Vector3> outer, IReadOnlyList<Vector3> inner, ArchRoadSheet sheet, ArchBrush brush )
{
for ( var index = 0; index < outer.Count; index++ )
{
var next = (index + 1) % outer.Count;
canvas.Ribbon( outer[index], outer[next], inner[next], inner[index], sheet.Along, brush, new ArchWeave
{
A = sheet.At( outer[index] ),
B = sheet.At( outer[next] ),
C = sheet.At( inner[next] ),
D = sheet.At( inner[index] )
} );
}
}
static void Fan( ArchMesh canvas, IReadOnlyList<Vector3> ring, Vector3 crown, ArchRoadSheet sheet, ArchBrush brush )
{
for ( var index = 0; index < ring.Count; index++ )
{
var next = (index + 1) % ring.Count;
canvas.Wedge( ring[index], ring[next], crown, sheet.Along, brush, new ArchWeave
{
A = sheet.At( ring[index] ),
B = sheet.At( ring[next] ),
C = sheet.At( crown ),
D = sheet.At( crown )
} );
}
}
// Crowned like the roads, so water runs off and a leg's crown meets no dish.
static Vector3 Crown( IReadOnlyList<Vector3> loop, ArchRoadPart lead )
{
var centre = Vector3.Zero;
foreach ( var point in loop )
{
centre += point;
}
return centre / loop.Count + Vector3.Up * MathF.Max( 0f, lead.Camber );
}
}
// One leg's developed surface, so the apron shares the road's grid, not a world projection.
public sealed class ArchRoadSheet
{
readonly Vector3 origin;
readonly Vector3 along;
readonly Vector3 across;
readonly float station;
readonly float offset;
public ArchRoadSheet( ArchJunctionLeg leg )
{
var frame = leg.Frame();
origin = frame.Position;
along = frame.Along;
across = frame.Across;
station = leg.Cut;
// The road unrolls from its LEFT edge, so the sheet must start there too.
offset = leg.HalfWidth * MathF.Max( 0.01f, frame.WidthScale );
}
public Vector3 Along => along;
public Vector2 At( Vector3 point )
{
var reach = point - origin;
return new Vector2( station + Vector3.Dot( reach, along ), offset + Vector3.Dot( reach, across ) );
}
}