Editor/Road/ArchRoadLines.cs
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 = ArchLap.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 );
}
}