Editor-side static helper that generates the kerb (road verge) mesh for roads. It computes frame rows, paving cells, brushes and vertex rows then calls mesh-sweep routines to build kerb geometry for straight and corner sections.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
// Verge section, road edge outward; the pavement field is loop-cut into cells so it lays as paving stones.
public static class ArchRoadKerb
{
// Ribs 0-4 are the kerb stone; the 4-5 gap IS the paving, one edge loop, collapsed where no crossover reaches.
const int Field = 4;
public static void Build(
ArchMesh canvas,
ArchRoadPart road,
IReadOnlyList<ArchBridgeShape> bridges,
ArchCurve curve,
IReadOnlyList<ArchRoadCrossing> crossings,
bool right,
ArchKit kit,
ArchStyle style,
ArchRoadSpan span,
bool closed,
IReadOnlyList<ArchCarveVolume> carves = null )
{
if ( road.PavementWidth < 6f )
{
return;
}
var frames = ArchJunction.Clipped( curve, Frames( road, curve, crossings, kit ), span );
Sweep( canvas, road, bridges, frames, crossings, right, kit, style, closed, true, carves );
}
// The corner path IS the kerb line, so the section starts on the frame with no centreline offset.
// Frames cannot say zero: ArchCurve reads a zero width scale as 1.
public static void Corner( ArchMesh canvas, ArchRoadPart road, IReadOnlyList<ArchFrame> frames, bool right, ArchKit kit, ArchStyle style )
{
if ( road.PavementWidth < 6f )
{
return;
}
Sweep( canvas, road, Array.Empty<ArchBridgeShape>(), frames, Array.Empty<ArchRoadCrossing>(), right, kit, style, false, false );
}
static void Sweep(
ArchMesh canvas,
ArchRoadPart road,
IReadOnlyList<ArchBridgeShape> bridges,
IReadOnlyList<ArchFrame> frames,
IReadOnlyList<ArchRoadCrossing> crossings,
bool right,
ArchKit kit,
ArchStyle style,
bool closed,
bool centreline,
IReadOnlyList<ArchCarveVolume> carves = null )
{
if ( frames.Count < 2 )
{
return;
}
var paved = ArchCrossover.Paved( road, crossings, right );
var cells = Cells( road );
var rows = new List<Vector3[]>();
foreach ( var frame in frames )
{
var edge = centreline ? road.HalfWidth * frame.WidthScale : 0f;
rows.Add( Section( road, frame, crossings, paved, right, cells, kit, edge ) );
}
// Only centreline runs take the deck's soffit: a corner's frames misread as road distances.
var soffit = centreline ? ( Func<int, float?> )(row => ArchBridge.Soffit( bridges, road, frames[row] )) : null;
ArchMeshSweep.Solid( canvas, rows, frames, road.Thickness + road.KerbHeight,
Brushes( road, cells, right, style ), style.Brush( ArchSurface.Kerb, road.Palette ), closed,
Paving( road, paved, cells, right, style ), soffit, carves );
}
// Paving walks its own frames at the paver pitch, never simplified; off, the whole verge follows the road's frames.
static List<ArchFrame> Frames( ArchRoadPart road, ArchCurve curve, IReadOnlyList<ArchRoadCrossing> crossings, ArchKit kit )
{
if ( road.PaverSize < 2f )
{
return ArchRoadGen.Frames( road, curve, crossings, kit );
}
return curve.Walk( MathF.Min( road.Precision, road.PaverSize ), false, 1f, 3,
ArchCrossover.Stations( road, crossings, kit ) );
}
// The crossover is one column between stone and splay, no per-face test; null when nothing is paved.
static Func<int, int, ArchBrush?> Paving( ArchRoadPart road, IReadOnlyList<ArchRoadCrossing> paved, int cells, bool right, ArchStyle style )
{
if ( paved.Count == 0 )
{
return null;
}
var brush = style.Brush( ArchSurface.Deck, road.Palette );
// Left section reads outward in: the paving column lands the same count in from the far end.
var column = right ? Field : cells;
return ( _, gap ) => gap == column ? brush : null;
}
public static int Cells( ArchRoadPart road )
{
if ( road.PaverSize >= 2f )
{
return ArchDivide.AtMost( FieldWidth( road ), road.PaverSize ).Count;
}
return Math.Max( 1, road.PavementCourses );
}
public static float FieldWidth( ArchRoadPart road )
{
return MathF.Max( 2f, Width( road ) - Stone( road ) );
}
static float Stone( ArchRoadPart road ) => MathF.Max( 0f, road.KerbWidth );
static List<ArchBrush> Brushes( ArchRoadPart road, int cells, bool right, ArchStyle style )
{
var kerb = style.Brush( ArchSurface.Kerb, road.Palette );
var pavement = style.Brush( ArchSurface.Pavement, road.Palette );
var gaps = new List<ArchBrush>();
// One gap more than cells: the crossover's column takes pavement material where no drive paves it.
for ( var gap = 0; gap <= Field + cells; gap++ )
{
gaps.Add( gap < Field ? kerb : pavement );
}
// The left section is read outward in, so its brushes are too.
if ( !right )
{
gaps.Reverse();
}
return gaps;
}
static Vector3[] Section(
ArchRoadPart road,
ArchFrame frame,
IReadOnlyList<ArchRoadCrossing> crossings,
IReadOnlyList<ArchRoadCrossing> paved,
bool right,
int cells,
ArchKit kit,
float edge )
{
var camber = -MathF.Max( 0f, road.Camber );
var top = camber + ArchCrossover.KerbTop( road, crossings, right, frame.Distance, kit );
var back = ArchCrossover.Back( road, kit );
var bevel = Bevel( road, kit );
var rise = top - camber;
var stone = Stone( road );
// The foot starts a bite inside the carriageway, or the kerb's back and the road's flank sit back to back.
var foot = -ArchContact.Bite( kit );
var ribs = new List<Vector2>
{
new( foot, camber ),
road.Kerb == KerbStyle.Rolled ? new Vector2( bevel * 0.15f, camber + rise * 0.5f ) : new Vector2( foot, camber ),
road.Kerb == KerbStyle.Rolled ? new Vector2( bevel * 0.55f, camber + rise * 0.86f ) : new Vector2( foot, camber ),
new( bevel, top ),
new( bevel + stone, top )
};
var field = FieldWidth( road );
// Measured from the back of the STONE: the first field rib lands on it and the splay can collapse; else sliver quads.
float Raked( float across ) => MathX.Lerp( top, back, Math.Clamp( (across - bevel) / MathF.Max( 1f, field - bevel ), 0f, 1f ) );
// The splay is one rib; courses held off it collapse at the mouth's deepest and reappear as it tapers.
var splay = MathF.Max( bevel, ArchCrossover.Depth( paved, frame.Distance, field ) );
ribs.Add( new Vector2( stone + splay, Raked( splay ) ) );
foreach ( var (_, across) in ArchDivide.AtMost( field, field / MathF.Max( 1, cells ) ).Bays )
{
var held = MathF.Max( across, splay );
ribs.Add( new Vector2( stone + held, Raked( held ) ) );
}
return Row( frame, edge, ribs, right );
}
static Vector3[] Row( ArchFrame frame, float edge, IReadOnlyList<Vector2> ribs, bool right )
{
var row = new Vector3[ribs.Count];
for ( var index = 0; index < ribs.Count; index++ )
{
var across = edge + ribs[index].x;
row[index] = frame.Side( right ? across : -across, ribs[index].y );
}
// Columns always run left to right across the run, so a left-hand section is read outward in.
if ( !right )
{
Array.Reverse( row );
}
return row;
}
static float Width( ArchRoadPart road ) => MathF.Max( 6f, road.PavementWidth );
static float Bevel( ArchRoadPart road, ArchKit kit )
{
return road.Kerb switch
{
KerbStyle.Square => 0f,
KerbStyle.Rolled => MathF.Max( road.KerbBevel, road.KerbHeight * 1.2f ),
_ => MathF.Max( 0f, road.KerbBevel )
};
}
}