Editor utility that builds road lane markings and zebra crossings into an ArchMesh. It samples an ArchCurve, computes offsets for centre, lane and edge lines, supports dashed runs and zebra bars, and sweeps strips into the mesh with optional carve volumes.
using System;
using System.Collections.Generic;
using Sandbox;
namespace Sunless.Architecture;
public static class ArchRoadLines
{
public static void Build(
ArchMesh canvas,
ArchRoadPart road,
ArchCurve curve,
IReadOnlyList<ArchRoadCrossing> crossings,
ArchKit kit,
ArchStyle style,
ArchRoadSpan span,
IReadOnlyList<ArchCarveVolume> carves = null )
{
var brush = style.Brush( ArchSurface.RoadLine, road.Palette );
// Proud, not a bite: a bite shares a plane with the camber crease.
var lift = ArchContact.Proud( kit );
Paint( canvas, road, curve, kit, kit.FindRoadLine( road.CentreLine ), 0f, lift, brush, span, carves );
foreach ( var offset in LaneOffsets( road ) )
{
Paint( canvas, road, curve, kit, kit.FindRoadLine( road.LaneLine ), offset, lift, brush, span, carves );
}
foreach ( var side in new[] { -1f, 1f } )
{
Paint( canvas, road, curve, kit, kit.FindRoadLine( road.EdgeLine ), side * (road.HalfWidth - kit.EdgeLineInset), lift, brush, span, carves );
}
foreach ( var crossing in crossings )
{
if ( crossing.Kind == CrossingKind.Zebra && crossing.Distance >= span.From && crossing.Distance <= span.To )
{
Zebra( canvas, road, curve, crossing, kit, lift, brush, carves );
}
}
}
// Lane boundaries only - the centre is drawn by its own line, which is usually a different one.
static IEnumerable<float> LaneOffsets( ArchRoadPart road )
{
var lanes = Math.Clamp( road.Lanes, 1, 12 );
if ( lanes < 3 )
{
yield break;
}
var step = road.Width / lanes;
for ( var index = 1; index < lanes; index++ )
{
var offset = -road.HalfWidth + step * index;
if ( MathF.Abs( offset ) > 0.5f )
{
yield return offset;
}
}
}
static void Paint( ArchMesh canvas, ArchRoadPart road, ArchCurve curve, ArchKit kit, ArchRoadLine line, float offset, float lift, ArchBrush brush, ArchRoadSpan span, IReadOnlyList<ArchCarveVolume> carves )
{
if ( line is null || line.Width < 0.2f )
{
return;
}
var strands = Math.Clamp( line.Count, 1, 4 );
var spread = (strands - 1) * (line.Width + line.Separation) * 0.5f;
for ( var strand = 0; strand < strands; strand++ )
{
var centre = offset - spread + strand * (line.Width + line.Separation);
foreach ( var run in Runs( span, line ) )
{
Strip( canvas, road, curve, kit, centre, line.Width, lift, run.From, run.To, brush, carves );
}
}
}
// Dash phase from the road's own start, not the span's: marks do not shift where a junction interrupts them.
static IEnumerable<(float From, float To)> Runs( ArchRoadSpan span, ArchRoadLine line )
{
if ( line.Dash < 1f || line.Gap < 0.5f )
{
yield return (span.From, span.To);
yield break;
}
var pitch = line.Dash + line.Gap;
var at = line.Gap * 0.5f;
while ( at + line.Dash <= span.To )
{
if ( at >= span.From )
{
yield return (at, at + line.Dash);
}
at += pitch;
}
}
static void Strip( ArchMesh canvas, ArchRoadPart road, ArchCurve curve, ArchKit kit, float centre, float width, float lift, float from, float to, ArchBrush brush, IReadOnlyList<ArchCarveVolume> carves )
{
var rows = new List<Vector3[]>();
var frames = new List<ArchFrame>();
var half = width * 0.5f;
foreach ( var distance in ArchDivide.AtMost( to - from, road.Precision ).Nodes )
{
if ( !curve.Sample( from + distance, out var frame ) )
{
continue;
}
rows.Add( new[]
{
frame.Side( centre - half, Deck( road, centre - half ) + lift ),
frame.Side( centre + half, Deck( road, centre + half ) + lift )
} );
frames.Add( frame );
}
ArchMeshSweep.Skin( canvas, rows, frames, brush, false, carves );
}
static void Zebra( ArchMesh canvas, ArchRoadPart road, ArchCurve curve, ArchRoadCrossing crossing, ArchKit kit, float lift, ArchBrush brush, IReadOnlyList<ArchCarveVolume> carves )
{
var half = MathF.Max( 24f, crossing.Width ) * 0.5f;
var bar = MathF.Max( 4f, kit.ZebraBar );
var reach = road.HalfWidth - kit.EdgeLineInset;
foreach ( var centre in ArchDivide.AtMost( half * 2f, bar + MathF.Max( 2f, kit.ZebraGap ) ).Centres )
{
var rows = new List<Vector3[]>();
var frames = new List<ArchFrame>();
foreach ( var edge in new[] { -bar * 0.5f, bar * 0.5f } )
{
if ( !curve.Sample( crossing.Distance - half + centre + edge, out var frame ) )
{
continue;
}
rows.Add( new[]
{
frame.Side( -reach, Deck( road, -reach ) + lift ),
frame.Side( reach, Deck( road, reach ) + lift )
} );
frames.Add( frame );
}
ArchMeshSweep.Skin( canvas, rows, frames, brush, false, carves );
}
}
// A marking lies on the camber, not on the crown, or the outer half of every line hangs in the air.
static float Deck( ArchRoadPart road, float across )
{
return -MathF.Max( 0f, road.Camber ) * Math.Clamp( MathF.Abs( across ) / road.HalfWidth, 0f, 1f );
}
}