Settings, enums and geometry helpers for a procedural road generator in an editor mapping tool. RoadSettings holds type, lane, curb, sidewalk, barrier, marking and detail options; RoadTypes applies presets per road kind; RoadShape turns a PathCurve into stations and section geometry; RoadGenerator/RoadBuilder emit deck, strips, paint, barriers, lights and props as meshes.
using System.Text.Json;
namespace AdvancedMappingTools;
public enum RoadType
{
Residential,
Urban,
Country,
Highway,
Expressway,
Industrial,
Mountain,
Coastal,
Forest,
Gravel,
Dirt,
Concrete,
Asphalt,
Medieval,
Damaged,
Abandoned,
Underground,
Elevated,
Service,
[Title( "Emergency Access" )] Emergency,
[Title( "Parking Access" )] ParkingAccess,
Driveway,
Pedestrian
}
public enum CenterMarking
{
None,
Dashed,
Solid,
[Title( "Double Solid" )] DoubleSolid
}
public enum RoadDivider
{
None,
Raised,
[Title( "Jersey Barrier" )] Barrier
}
public enum ShoulderKind
{
None,
Paved,
Gravel
}
public enum RoadBarrier
{
None,
Guardrail,
[Title( "Jersey Barrier" )] Jersey,
Wall
}
public enum LightSide
{
Right,
Left,
Both,
Staggered
}
public enum RoadEnd
{
[Title( "Flat Cap" )] Cap,
[Title( "Turning Circle" )] TurningCircle
}
/// <summary>
/// One stretch of a road between two distances along its line. An end at a junction stops
/// there with paint in front of it, the junction builds on from that point.
/// </summary>
public sealed class RoadStretch
{
public float From { get; set; }
public float To { get; set; }
public bool FromJunction { get; set; }
public bool ToJunction { get; set; }
/// <summary>
/// The junction at this end is a roundabout: traffic gives way here, it does not stop.
/// </summary>
public bool FromYield { get; set; }
public bool ToYield { get; set; }
}
public sealed partial class RoadSettings : GeneratorSettings
{
RoadType _type = RoadType.Residential;
[System.Text.Json.Serialization.JsonIgnore]
public override IReadOnlySet<string> UnusedOptions
{
get
{
var h = new HashSet<string>
{
"Variation"
};
if ( !CustomDirectionWidths )
h.UnionWith( [nameof( ForwardWidth ), nameof( ReverseWidth )] );
if ( !SeparateSidewalkWidths )
h.UnionWith( [nameof( LeftWalkWidth ), nameof( RightWalkWidth )] );
return h;
}
}
/// <summary>
/// The line the road follows, set by the tool.
/// </summary>
public PathCurve Path { get; set; } = PathCurve.FromPoints( [Vector3.Zero, new Vector3( 1024.0f, 0.0f, 0.0f )] );
/// <summary>
/// Where junctions leave this road open, set when roads connect. Empty is the whole road.
/// </summary>
public List<RoadStretch> Stretches { get; set; } = [];
[Property, Group( "Road" ), Title( "Road Type" ), WideMode, Description( "Sets every option below to what this kind of road has. Change any of them afterwards." )]
public RoadType Type
{
get => _type;
set
{
if ( _type == value ) return;
_type = value;
RoadTypes.Apply( this, value );
}
}
[Property, Group( "Road" ), Range( 1, 12, slider: false ), WideMode]
public int Lanes { get; set; } = 2;
[Property, Group( "Road" ), Title( "Lane Width" ), Range( 24, 512, slider: false ), WideMode]
public float LaneWidth { get; set; } = 108.0f;
[Property, Group( "Road" ), Title( "One Way" ), WideMode, Description( "All lanes run the way the road was drawn." )]
public bool OneWay { get; set; } = false;
[Property, Group( "Road" ), WideMode]
public RoadPaving Paving { get; set; } = RoadPaving.Asphalt;
[Property, Group( "Road" ), Range( 0.5f, 128, slider: false ), WideMode]
public float Thickness { get; set; } = 8.0f;
[Property, Group( "Shape" ), Range( -32, 32, slider: false ), WideMode, Description( "How high the middle stands over the edges, so water runs off. Below zero the road falls to a channel in its middle." )]
public float Crown { get; set; } = 2.0f;
[Property, Group( "Shape" ), Range( -20, 20, slider: false ), WideMode, Description( "Percent the whole width leans. Positive lifts the left side." )]
public float Crossfall { get; set; } = 0.0f;
[Property, Group( "Shape" ), Range( -45, 45, slider: false ), WideMode, Description( "Degrees the road leans into its turns." )]
public float Banking { get; set; } = 0.0f;
[Property, Group( "Shape" ), Range( -4096, 4096, slider: false ), WideMode, Description( "Height added along the path, on top of the height of its points." )]
public float Rise { get; set; } = 0.0f;
[Property, Group( "Shape" ), Title( "Ease Ends" ), WideMode, Description( "Starts and ends level, so a climb meets the ground without a kink." )]
public bool EaseEnds { get; set; } = true;
[Property, Group( "Shape" ), Title( "End Width" ), Range( 0.25f, 4, slider: false ), WideMode, Description( "Width at the end of the road compared to its start, for a road that narrows or widens." )]
public float EndWidth { get; set; } = 1.0f;
[Property, Group( "Shape" ), Title( "Section Length" ), Range( 64, 8192, slider: false ), WideMode, Description( "The road is cut into independent sections of this length." )]
public float SectionLength { get; set; } = 512.0f;
[Property, Group( "Shape" ), Title( "Path Detail" ), Range( 4, 256, slider: false ), WideMode, Description( "Longest straight piece along the path." )]
public float PathDetail { get; set; } = 32.0f;
[Property, Group( "Shape" ), Title( "Start" ), WideMode, Description( "How the road ends here when no other road is there." )]
public RoadEnd StartEnd { get; set; } = RoadEnd.Cap;
[Property, Group( "Shape" ), Title( "End" ), WideMode]
public RoadEnd FinishEnd { get; set; } = RoadEnd.Cap;
[Property, Group( "Divider" ), Title( "Center Divider" ), WideMode]
public RoadDivider Divider { get; set; } = RoadDivider.None;
[Property, HideIf( nameof( Divider ), RoadDivider.None ), Group( "Divider" ), Title( "Divider Width" ), Range( 8, 512, slider: false ), WideMode]
public float DividerWidth { get; set; } = 48.0f;
[Property, HideIf( nameof( Divider ), RoadDivider.None ), Group( "Divider" ), Title( "Divider Height" ), Range( 1, 64, slider: false ), WideMode]
public float DividerHeight { get; set; } = 6.0f;
[Property, Group( "Shoulder" ), Title( "Shoulder" ), WideMode]
public ShoulderKind Shoulder { get; set; } = ShoulderKind.None;
[Property, HideIf( nameof( Shoulder ), ShoulderKind.None ), Group( "Shoulder" ), Title( "Shoulder Width" ), Range( 4, 512, slider: false ), WideMode]
public float ShoulderWidth { get; set; } = 48.0f;
[Property, Group( "Curbs" ), Title( "Left Curb" ), WideMode]
public bool LeftCurb { get; set; } = true;
[Property, Group( "Curbs" ), Title( "Right Curb" ), WideMode]
public bool RightCurb { get; set; } = true;
[Property, Group( "Curbs" ), Title( "Curb Shape" ), WideMode]
public CurbShape CurbShape { get; set; } = CurbShape.Chamfered;
[Property, Group( "Curbs" ), Title( "Curb Width" ), Range( 1, 64, slider: false ), WideMode]
public float CurbWidth { get; set; } = 8.0f;
[Property, Group( "Curbs" ), Title( "Curb Height" ), Range( 0.5f, 32, slider: false ), WideMode]
public float CurbHeight { get; set; } = 6.0f;
[Property, Group( "Curbs" ), Title( "Gutter Width" ), Range( 0, 64, slider: false ), WideMode, Description( "A channel along the curb. Zero leaves it out." )]
public float GutterWidth { get; set; } = 0.0f;
[Property, Group( "Sidewalks" ), Title( "Left Sidewalk" ), WideMode]
public bool LeftSidewalk { get; set; } = true;
[Property, Group( "Sidewalks" ), Title( "Right Sidewalk" ), WideMode]
public bool RightSidewalk { get; set; } = true;
[Property, Group( "Sidewalks" ), Title( "Sidewalk Width" ), Range( 8, 1024, slider: false ), WideMode]
public float SidewalkWidth { get; set; } = 72.0f;
[Property, Group( "Sidewalks" ), Title( "Sidewalk Height" ), Range( 0.5f, 32, slider: false ), WideMode]
public float SidewalkHeight { get; set; } = 5.0f;
[Property, Group( "Sidewalks" ), Title( "Verge Width" ), Range( 0, 512, slider: false ), WideMode, Description( "A grass strip between curb and sidewalk. Zero leaves it out." )]
public float VergeWidth { get; set; } = 0.0f;
[Property, Group( "Sidewalks" ), Title( "Slab Length" ), Range( 0, 512, slider: false ), WideMode, Description( "Lays the sidewalk in separate slabs with joints between them. Zero is one long piece." )]
public float SlabLength { get; set; } = 64.0f;
[Property, Group( "Barriers" ), Title( "Left Barrier" ), WideMode]
public RoadBarrier LeftBarrier { get; set; } = RoadBarrier.None;
[Property, Group( "Barriers" ), Title( "Right Barrier" ), WideMode]
public RoadBarrier RightBarrier { get; set; } = RoadBarrier.None;
[Property, Group( "Barriers" ), Title( "Barrier Height" ), Range( 8, 512, slider: false ), WideMode]
public float BarrierHeight { get; set; } = 32.0f;
[Property, Group( "Barriers" ), Title( "Post Spacing" ), Range( 16, 1024, slider: false ), WideMode]
public float PostSpacing { get; set; } = 96.0f;
[Property, Group( "Lane Markings" ), Title( "Center Line" ), WideMode]
public CenterMarking CenterLine { get; set; } = CenterMarking.Dashed;
[Property, Group( "Lane Markings" ), Title( "Yellow Center" ), WideMode]
public bool YellowCenter { get; set; } = false;
[Property, Group( "Lane Markings" ), Title( "Lane Lines" ), WideMode]
public bool LaneLines { get; set; } = true;
[Property, Group( "Lane Markings" ), Title( "Edge Lines" ), WideMode]
public bool EdgeLines { get; set; } = false;
[Property, Group( "Lane Markings" ), Title( "Line Width" ), Range( 1, 32, slider: false ), WideMode]
public float LineWidth { get; set; } = 4.0f;
[Property, Group( "Lane Markings" ), Title( "Dash Length" ), Range( 8, 1024, slider: false ), WideMode]
public float DashLength { get; set; } = 96.0f;
[Property, Group( "Lane Markings" ), Title( "Dash Gap" ), Range( 8, 1024, slider: false ), WideMode]
public float DashGap { get; set; } = 144.0f;
[Property, Group( "Lane Markings" ), Title( "Marking Wear" ), Range( 0, 1 ), WideMode, Description( "How much of the paint is gone." )]
public float MarkingWear { get; set; } = 0.0f;
[Property, Group( "Junction Markings" ), Title( "Stop Lines" ), WideMode]
public bool StopLines { get; set; } = true;
[Property, Group( "Junction Markings" ), WideMode]
public bool Crosswalks { get; set; } = true;
[Property, ShowIf( nameof( Crosswalks ), true ), Group( "Junction Markings" ), Title( "Crosswalk Width" ), Range( 24, 512, slider: false ), WideMode]
public float CrosswalkWidth { get; set; } = 96.0f;
[Property, Group( "Junction Markings" ), Title( "Arrows" ), WideMode, Description( "Painted arrows on the lanes in front of a junction." )]
public bool Arrows { get; set; } = false;
[Property, Group( "Junction Markings" ), Title( "Road Number" ), WideMode, Description( "Digits painted on the road, like a speed limit. Empty paints nothing." )]
public string RoadNumber { get; set; } = "";
[Property, Group( "Junction Markings" ), Title( "Number Spacing" ), Range( 128, 8192, slider: false ), WideMode]
public float NumberSpacing { get; set; } = 1536.0f;
[Property, Group( "Junctions" ), Title( "Connect" ), WideMode, Description( "Builds a junction where this road meets another one." )]
public bool Connect { get; set; } = true;
[Property, ShowIf( nameof( Connect ), true ), Group( "Junctions" ), Title( "Corner Radius" ), Range( 0, 1024, slider: false ), WideMode, Description( "For new junctions. A junction keeps its own value after that." )]
public float CornerRadius { get; set; } = 96.0f;
[Property, ShowIf( nameof( Connect ), true ), Group( "Junctions" ), Title( "Roundabout" ), WideMode, Description( "New junctions of this road become roundabouts." )]
public bool Roundabouts { get; set; } = false;
[Property, Group( "Junctions" ), Title( "Turning Radius" ), Range( 64, 2048, slider: false ), WideMode, Description( "Size of a turning circle at the end of the road." )]
public float TurningRadius { get; set; } = 288.0f;
[Property, Group( "Street Lights" ), Title( "Street Lights" ), WideMode]
public bool StreetLights { get; set; } = true;
[Property, ShowIf( nameof( StreetLights ), true ), Group( "Street Lights" ), Title( "Light Side" ), WideMode]
public LightSide LightSide { get; set; } = LightSide.Staggered;
[Property, ShowIf( nameof( StreetLights ), true ), Group( "Street Lights" ), Title( "Light Spacing" ), Range( 64, 8192, slider: false ), WideMode]
public float LightSpacing { get; set; } = 768.0f;
[Property, Group( "Details" ), Title( "Drains" ), WideMode, Description( "Storm drains along the curbs." )]
public bool Drains { get; set; } = true;
[Property, ShowIf( nameof( Drains ), true ), Group( "Details" ), Title( "Drain Spacing" ), Range( 64, 4096, slider: false ), WideMode]
public float DrainSpacing { get; set; } = 512.0f;
[Property, Group( "Details" ), Title( "Manholes" ), WideMode]
public bool Manholes { get; set; } = true;
[Property, ShowIf( nameof( Manholes ), true ), Group( "Details" ), Title( "Manhole Spacing" ), Range( 64, 4096, slider: false ), WideMode]
public float ManholeSpacing { get; set; } = 640.0f;
[Property, Group( "Details" ), Title( "Reflectors" ), WideMode, Description( "Small studs along the lines." )]
public bool Reflectors { get; set; } = false;
[Property, ShowIf( nameof( Reflectors ), true ), Group( "Details" ), Title( "Reflector Spacing" ), Range( 16, 2048, slider: false ), WideMode]
public float ReflectorSpacing { get; set; } = 192.0f;
[Property, Group( "Details" ), Title( "Road Signs" ), WideMode, Description( "A sign in front of every junction and along the way." )]
public bool Signs { get; set; } = true;
[Property, ShowIf( nameof( Signs ), true ), Group( "Details" ), Title( "Sign Spacing" ), Range( 128, 8192, slider: false ), WideMode]
public float SignSpacing { get; set; } = 2048.0f;
[Property, Group( "Details" ), Title( "Roadside Props" ), WideMode, Description( "Benches, bins, hydrants, mailboxes and planters along the sidewalks." )]
public bool RoadsideProps { get; set; } = false;
[Property, ShowIf( nameof( RoadsideProps ), true ), Group( "Details" ), Title( "Prop Spacing" ), Range( 32, 4096, slider: false ), WideMode]
public float PropSpacing { get; set; } = 384.0f;
[Property, Group( "Details" ), Title( "Speed Bump Spacing" ), Range( 0, 4096, slider: false ), WideMode, Description( "Zero builds none." )]
public float SpeedBumpSpacing { get; set; } = 0.0f;
[Property, Group( "Details" ), Title( "End Bollards" ), WideMode, Description( "A row of bollards across the ends of the road, to keep traffic out." )]
public bool EndBollards { get; set; } = false;
[Property, Group( "Details" ), Title( "Expansion Joints" ), Range( 0, 2048, slider: false ), WideMode, Description( "Joints across a concrete road. Zero builds none." )]
public float JointSpacing { get; set; } = 0.0f;
[Property, Group( "Damage" ), Range( 0, 1 ), WideMode, Description( "How run down the road is." )]
public float Damage { get; set; } = 0.0f;
[Property, Group( "Damage" ), WideMode]
public bool Cracks { get; set; } = true;
[Property, Group( "Damage" ), WideMode]
public bool Patches { get; set; } = true;
[Property, Group( "Damage" ), WideMode, Description( "Real holes cut into the road." )]
public bool Potholes { get; set; } = true;
[Property, Group( "Supports" ), WideMode, Description( "Columns under the road, for an elevated road." )]
public bool Supports { get; set; } = false;
[Property, ShowIf( nameof( Supports ), true ), Group( "Supports" ), Title( "Support Drop" ), Range( 16, 4096, slider: false ), WideMode, Description( "How far the columns reach below the road." )]
public float SupportDrop { get; set; } = 256.0f;
[Property, ShowIf( nameof( Supports ), true ), Group( "Supports" ), Title( "Support Spacing" ), Range( 64, 4096, slider: false ), WideMode]
public float SupportSpacing { get; set; } = 640.0f;
[Property, ShowIf( nameof( Supports ), true ), Group( "Supports" ), Title( "Support Size" ), Range( 8, 256, slider: false ), WideMode]
public float SupportSize { get; set; } = 48.0f;
[Property, Group( "Materials" ), Title( "Road" ), ResourceType( "vmat" ), WideMode, Description( "Empty paints the road in the color of its paving." )]
public string RoadMaterial { get; set; }
[Property, HideIf( nameof( Shoulder ), ShoulderKind.None ), Group( "Materials" ), Title( "Shoulder" ), ResourceType( "vmat" ), WideMode]
public string ShoulderMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Curbs" ), ResourceType( "vmat" ), WideMode]
public string CurbMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Verge" ), ResourceType( "vmat" ), WideMode]
public string VergeMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Sidewalks" ), ResourceType( "vmat" ), WideMode]
public string SidewalkMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Markings" ), ResourceType( "vmat" ), WideMode]
public string MarkingMaterial { get; set; }
[Property, Group( "Materials" ), Title( "Details" ), ResourceType( "vmat" ), WideMode, Description( "Barriers, lamps, drains and manholes." )]
public string DetailMaterial { get; set; }
/// <summary>
/// Half of the width between the curbs.
/// </summary>
public float Half => (DrivenWidth + (Divider != RoadDivider.None ? DividerWidth : 0.0f)) * 0.5f + (Shoulder == ShoulderKind.Paved ? ShoulderWidth : 0.0f);
RoadSide Side( bool curb, bool walk ) => new()
{
Shoulder = Shoulder == ShoulderKind.Gravel ? ShoulderWidth : 0.0f,
Gutter = curb ? GutterWidth : 0.0f,
CurbWidth = curb ? CurbWidth : 0.0f,
CurbHeight = curb ? CurbHeight : 0.0f,
CurbShape = CurbShape,
Verge = VergeWidth,
Sidewalk = walk ? SidewalkWidth : 0.0f,
SidewalkHeight = walk ? SidewalkHeight : 0.0f
};
public RoadSection Section() => new()
{
Half = Half,
Thickness = Thickness,
Crown = Crown,
Crossfall = Crossfall * 0.01f,
Paving = Paving,
Left = ExpandedSide( true ),
Right = ExpandedSide( false ),
RoadMaterial = RoadMaterial,
ShoulderMaterial = ShoulderMaterial,
CurbMaterial = CurbMaterial,
VergeMaterial = VergeMaterial,
SidewalkMaterial = SidewalkMaterial
};
}
/// <summary>
/// What each kind of road is made of. A type is real geometry: lanes, widths, curbs, barriers and paint,
/// not a name. Whatever a type does not mention falls back to one plain road first, so
/// switching from one type to the next leaves nothing of the last one behind.
/// </summary>
public static class RoadTypes
{
public static void Apply( RoadSettings s, RoadType type )
{
Plain( s );
switch ( type )
{
case RoadType.Residential:
s.StreetLights = true;
s.Drains = true;
s.Manholes = true;
break;
case RoadType.Urban:
s.Lanes = 4;
s.LaneWidth = 120.0f;
s.SidewalkWidth = 108.0f;
s.GutterWidth = 12.0f;
s.LightSpacing = 640.0f;
s.LightSide = LightSide.Both;
s.Drains = true;
s.Manholes = true;
s.Arrows = true;
s.RoadsideProps = true;
s.CenterLine = CenterMarking.DoubleSolid;
s.YellowCenter = true;
break;
case RoadType.Country:
Open( s, ShoulderKind.Gravel, 48.0f );
s.LaneWidth = 120.0f;
s.EdgeLines = true;
s.Reflectors = true;
s.Crown = 3.0f;
break;
case RoadType.Highway:
Open( s, ShoulderKind.Paved, 96.0f );
s.Lanes = 4;
s.LaneWidth = 132.0f;
s.Divider = RoadDivider.Barrier;
s.DividerWidth = 48.0f;
s.DividerHeight = 32.0f;
s.CenterLine = CenterMarking.None;
s.EdgeLines = true;
s.LeftBarrier = RoadBarrier.Guardrail;
s.RightBarrier = RoadBarrier.Guardrail;
s.Reflectors = true;
s.Thickness = 16.0f;
s.Crown = 0.0f;
break;
case RoadType.Expressway:
Open( s, ShoulderKind.Paved, 120.0f );
s.Lanes = 6;
s.LaneWidth = 132.0f;
s.Divider = RoadDivider.Raised;
s.DividerWidth = 192.0f;
s.DividerHeight = 6.0f;
s.CenterLine = CenterMarking.None;
s.EdgeLines = true;
s.LeftBarrier = RoadBarrier.Jersey;
s.RightBarrier = RoadBarrier.Jersey;
s.Reflectors = true;
s.StreetLights = true;
s.LightSide = LightSide.Both;
s.LightSpacing = 1024.0f;
s.Thickness = 16.0f;
s.Crown = 0.0f;
break;
case RoadType.Industrial:
s.LaneWidth = 144.0f;
s.Paving = RoadPaving.Concrete;
s.Thickness = 12.0f;
s.CurbShape = CurbShape.Mountable;
s.CurbHeight = 4.0f;
s.LeftSidewalk = false;
s.CenterLine = CenterMarking.None;
s.JointSpacing = 256.0f;
s.Drains = true;
s.Manholes = true;
s.Damage = 0.2f;
s.Potholes = false;
break;
case RoadType.Mountain:
Open( s, ShoulderKind.Gravel, 24.0f );
s.CenterLine = CenterMarking.DoubleSolid;
s.YellowCenter = true;
s.RightBarrier = RoadBarrier.Guardrail;
s.Reflectors = true;
s.Banking = 6.0f;
break;
case RoadType.Coastal:
Open( s, ShoulderKind.Paved, 36.0f );
s.LaneWidth = 120.0f;
s.LeftCurb = true;
s.LeftSidewalk = true;
s.SidewalkWidth = 120.0f;
s.RightBarrier = RoadBarrier.Wall;
s.BarrierHeight = 24.0f;
s.StreetLights = true;
s.LightSide = LightSide.Left;
s.EdgeLines = true;
break;
case RoadType.Forest:
Open( s, ShoulderKind.Gravel, 30.0f );
s.Lanes = 1;
s.LaneWidth = 144.0f;
s.Paving = RoadPaving.Dirt;
s.CenterLine = CenterMarking.None;
s.Crown = 4.0f;
break;
case RoadType.Gravel:
Open( s, ShoulderKind.None, 0.0f );
s.Lanes = 1;
s.LaneWidth = 168.0f;
s.Paving = RoadPaving.Gravel;
s.CenterLine = CenterMarking.None;
s.Crown = 3.0f;
s.Thickness = 6.0f;
break;
case RoadType.Dirt:
Open( s, ShoulderKind.None, 0.0f );
s.Lanes = 1;
s.LaneWidth = 144.0f;
s.Paving = RoadPaving.Dirt;
s.CenterLine = CenterMarking.None;
s.Crown = 5.0f;
s.Damage = 0.3f;
s.Cracks = false;
s.Patches = false;
break;
case RoadType.Concrete:
s.LaneWidth = 120.0f;
s.Paving = RoadPaving.Concrete;
s.CurbShape = CurbShape.Square;
s.JointSpacing = 192.0f;
s.Crown = 0.0f;
s.Crossfall = 1.0f;
break;
case RoadType.Asphalt:
Open( s, ShoulderKind.None, 0.0f );
s.LaneWidth = 120.0f;
break;
case RoadType.Medieval:
Open( s, ShoulderKind.None, 0.0f );
s.Lanes = 1;
s.LaneWidth = 192.0f;
s.Paving = RoadPaving.Cobbles;
s.CenterLine = CenterMarking.None;
s.Crown = -3.0f;
s.LeftCurb = true;
s.RightCurb = true;
s.CurbShape = CurbShape.Square;
s.CurbWidth = 12.0f;
s.CurbHeight = 2.0f;
break;
case RoadType.Damaged:
Apply( s, RoadType.Urban );
s.Damage = 0.6f;
s.MarkingWear = 0.4f;
s.RoadsideProps = false;
break;
case RoadType.Abandoned:
Apply( s, RoadType.Residential );
s.Damage = 0.9f;
s.MarkingWear = 0.75f;
s.StreetLights = false;
s.Signs = false;
break;
case RoadType.Underground:
Open( s, ShoulderKind.None, 0.0f );
s.LaneWidth = 120.0f;
s.LeftCurb = true;
s.LeftSidewalk = true;
s.SidewalkWidth = 36.0f;
s.SidewalkHeight = 12.0f;
s.CurbHeight = 12.0f;
s.RightBarrier = RoadBarrier.Wall;
s.LeftBarrier = RoadBarrier.Wall;
s.BarrierHeight = 128.0f;
s.CenterLine = CenterMarking.Solid;
s.Reflectors = true;
s.Crown = 0.0f;
s.GutterWidth = 8.0f;
break;
case RoadType.Elevated:
Open( s, ShoulderKind.Paved, 36.0f );
s.LaneWidth = 120.0f;
s.LeftBarrier = RoadBarrier.Jersey;
s.RightBarrier = RoadBarrier.Jersey;
s.Thickness = 24.0f;
s.Supports = true;
s.EdgeLines = true;
s.Crown = 0.0f;
break;
case RoadType.Service:
Open( s, ShoulderKind.None, 0.0f );
s.Lanes = 1;
s.LaneWidth = 144.0f;
s.Paving = RoadPaving.Concrete;
s.CenterLine = CenterMarking.None;
s.RightCurb = true;
s.CurbShape = CurbShape.Mountable;
s.Thickness = 6.0f;
break;
case RoadType.Emergency:
Open( s, ShoulderKind.Paved, 24.0f );
s.Lanes = 1;
s.LaneWidth = 168.0f;
s.CenterLine = CenterMarking.None;
s.EdgeLines = true;
s.EndBollards = true;
s.Reflectors = true;
break;
case RoadType.ParkingAccess:
s.LaneWidth = 96.0f;
s.LeftSidewalk = false;
s.CurbShape = CurbShape.Mountable;
s.CenterLine = CenterMarking.None;
s.SpeedBumpSpacing = 640.0f;
s.StopLines = true;
s.Crosswalks = false;
break;
case RoadType.Driveway:
Open( s, ShoulderKind.None, 0.0f );
s.Lanes = 1;
s.LaneWidth = 132.0f;
s.Paving = RoadPaving.Concrete;
s.CenterLine = CenterMarking.None;
s.Crown = 0.0f;
s.Thickness = 4.0f;
s.StopLines = false;
s.CornerRadius = 36.0f;
break;
case RoadType.Pedestrian:
Open( s, ShoulderKind.None, 0.0f );
s.Lanes = 1;
s.LaneWidth = 288.0f;
s.Paving = RoadPaving.Cobbles;
s.CenterLine = CenterMarking.None;
s.Crown = 0.0f;
s.EndBollards = true;
s.StreetLights = true;
s.LightSide = LightSide.Both;
s.LightSpacing = 512.0f;
s.RoadsideProps = true;
s.PropSpacing = 256.0f;
s.Crosswalks = false;
s.StopLines = false;
break;
}
}
/// <summary>
/// A road through open land: no curbs, no sidewalks, nothing that needs a town around it.
/// </summary>
static void Open( RoadSettings s, ShoulderKind shoulder, float width )
{
s.Shoulder = shoulder;
s.ShoulderWidth = MathF.Max( width, 4.0f );
s.LeftCurb = false;
s.RightCurb = false;
s.LeftSidewalk = false;
s.RightSidewalk = false;
s.Crosswalks = false;
s.StreetLights = false;
s.Drains = false;
s.Manholes = false;
}
static void Plain( RoadSettings s )
{
var plain = new RoadSettings();
foreach ( var property in EditorTypeLibrary.GetType( typeof( RoadSettings ) ).Properties.Where( x => x.HasAttribute<PropertyAttribute>() && x.CanWrite ) )
{
if ( property.Name is nameof( RoadSettings.Type ) or nameof( RoadSettings.Variation ) ) continue;
if ( property.Name.EndsWith( "Material" ) ) continue;
if ( property.Name is nameof( RoadSettings.Rise ) or nameof( RoadSettings.EaseEnds ) or nameof( RoadSettings.SectionLength ) or nameof( RoadSettings.PathDetail ) or nameof( RoadSettings.StartEnd ) or nameof( RoadSettings.FinishEnd ) ) continue;
property.SetValue( s, property.GetValue( plain ) );
}
}
}
/// <summary>
/// A road along its line: the stations to build at, including one exactly at every junction cut, and
/// what lies where. Everything of a road goes through this, so paint, curbs and
/// lamps agree on where the road runs and on the height of its top.
/// </summary>
public sealed class RoadShape
{
const float MaxTurn = 4.0f;
const float StationClearance = 2.0f;
const float SmallestBay = 0.25f;
const float FoldShare = 0.25f;
const float SqueezedStep = 0.5f;
const float EndClearance = 0.1f;
const float MaxTwist = 1.5f;
const int MaxColumns = 24;
readonly float[] _warp;
readonly List<float> _sectionEnds;
public RoadSettings Settings { get; }
public RoadSection Section { get; }
public PathTrack Track { get; }
public List<RoadStretch> Stretches { get; }
public float Length => Track.Length;
public RoadShape( RoadSettings s )
{
Settings = s;
var line = Line( s );
Section = SectionOn( s, line );
Stretches = s.Stretches.Count > 0 ? s.Stretches : [new RoadStretch { From = 0.0f, To = line.Length }];
var cuts = Stretches.SelectMany( x => new[] { x.From, x.To } ).Where( x => x > 0.0f && x < line.Length ).Distinct().OrderBy( x => x ).ToList();
var frames = new List<PathFrame>();
var next = 0;
for ( var i = 0; i < line.Count; i++ )
{
var frame = line.Frames[i];
var isEnd = i == 0 || i == line.Count - 1;
while ( next < cuts.Count && cuts[next] <= frame.Distance + StationClearance )
{
if ( !isEnd || cuts[next] < frame.Distance - 0.01f )
frames.Add( Between( line, cuts[next] ) );
next++;
}
if ( isEnd || !cuts.Any( x => MathF.Abs( x - frame.Distance ) < StationClearance ) )
frames.Add( frame );
}
Track = new PathTrack( frames, line.Closed ).Reach( Reach( 1.0f ), Reach( -1.0f ) ).Pinched();
_warp = MeasureWarp();
var used = new Dictionary<int, int>();
_sectionEnds = [.. Stretches.SelectMany( x => Bays( x, used ) ).SelectMany( x => new[] { Track.Frames[x.From].Distance, Track.Frames[x.To].Distance } ).Distinct()];
}
/// <summary>
/// On a climbing bend the pieces of a deck are warped, and a warped face is built as two flat
/// ones with a fold between them. Paint running over such a piece follows the smooth shape
/// and not the fold, so the deck would come through it. This is how far they can be apart.
/// </summary>
float[] MeasureWarp()
{
var tops = Section.TopPoints;
var warp = new float[Track.Count];
for ( var i = 0; i + 1 < Track.Count; i++ )
{
for ( var j = 0; j + 1 < tops.Length; j++ )
{
var a = Track.Place( i, tops[j] );
var b = Track.Place( i, tops[j + 1] );
var c = Track.Place( i + 1, tops[j + 1] );
var d = Track.Place( i + 1, tops[j] );
warp[i] = MathF.Max( warp[i], MathF.Abs( (c - a).Dot( Vector3.Cross( b - a, d - a ).Normal ) ) * FoldShare );
}
}
return warp;
}
/// <summary>
/// How far paint has to stand off the deck at a distance along the road.
/// </summary>
public float Warp( float distance )
{
var (index, _) = Track.Locate( distance );
return MathF.Max( _warp[index], _warp[Math.Max( index - 1, 0 )] );
}
/// <summary>
/// The cross section of a road on its line, cut across as finely as its climbing bends ask for.
/// </summary>
public static RoadSection SectionOn( RoadSettings s, PathTrack line )
{
var section = s.Section();
var worst = 0.0f;
for ( var i = 0; i + 1 < line.Count; i++ )
{
var a = line.Frames[i];
var b = line.Frames[i + 1];
var leftA = a.Position + a.Left * (section.Half * a.Width);
var rightA = a.Position - a.Left * (section.Half * a.Width);
var leftB = b.Position + b.Left * (section.Half * b.Width);
var rightB = b.Position - b.Left * (section.Half * b.Width);
worst = MathF.Max( worst, MathF.Abs( (rightB - leftA).Dot( Vector3.Cross( rightA - leftA, leftB - leftA ).Normal ) ) );
}
section.Columns = Math.Clamp( (int)MathF.Ceiling( worst / MaxTwist ), 1, MaxColumns );
return section;
}
/// <summary>
/// The stations of a road before any junction has cut it.
/// </summary>
public static PathTrack Line( RoadSettings s )
{
var path = s.Path;
if ( s.Banking != 0.0f || s.EndWidth != 1.0f )
{
path = JsonSerializer.Deserialize<PathCurve>( JsonSerializer.Serialize( path ) );
if ( s.Banking != 0.0f ) path.BankIntoTurns( s.Banking );
if ( s.EndWidth != 1.0f )
{
for ( var i = 0; i < path.Nodes.Count; i++ )
path.Nodes[i].Width = 1.0f + (s.EndWidth - 1.0f) * i / MathF.Max( path.Nodes.Count - 1, 1 );
}
}
return path.Track( s.PathDetail, FrameMode.Upright, MaxTurn, path.Closed ? 0.0f : s.Rise, s.EaseEnds ).Plumbed();
}
/// <summary>
/// A frame at any distance, kept square to the road: a junction starts from here.
/// </summary>
public static PathFrame Between( PathTrack track, float distance )
{
var (index, t) = track.Locate( distance );
var a = track.Frames[index];
var b = track.Frames[index + 1];
return new PathFrame( Vector3.Lerp( a.Position, b.Position, t ), Vector3.Lerp( a.Forward, b.Forward, t ).Normal, Vector3.Lerp( a.Left, b.Left, t ).Normal,
Vector3.Lerp( a.Up, b.Up, t ), distance, 1.0f, a.Width + (b.Width - a.Width) * t, a.Height + (b.Height - a.Height) * t );
}
float Reach( float sign )
{
var side = sign > 0.0f ? Section.Left : Section.Right;
return Section.Half + side.Width + RoadBuilder.BarrierWidth( sign > 0.0f ? Settings.LeftBarrier : Settings.RightBarrier );
}
public int Index( float distance )
{
var best = 0;
for ( var i = 1; i < Track.Count; i++ )
{
if ( MathF.Abs( Track.Frames[i].Distance - distance ) < MathF.Abs( Track.Frames[best].Distance - distance ) )
best = i;
}
return best;
}
/// <summary>
/// The sections of a stretch. They are fixed to a grid along the whole line, so a junction
/// that cuts a road somewhere leaves all sections away from it exactly as they were.
/// </summary>
public IEnumerable<(string Id, int From, int To)> Bays( RoadStretch stretch, Dictionary<int, int> used )
{
var length = MathF.Max( Settings.SectionLength, 32.0f );
var first = Index( stretch.From );
var last = Index( stretch.To );
var start = first;
int Key( int station ) => (int)MathF.Floor( Track.Frames[station].Distance / length );
for ( var i = first + 1; i <= last; i++ )
{
var isLast = i == last;
if ( !isLast && (Key( i ) == Key( start ) || Track.Frames[last].Distance - Track.Frames[i].Distance < length * SmallestBay) ) continue;
var key = Key( start );
used[key] = used.GetValueOrDefault( key ) + 1;
yield return ($"section{key:000}{(used[key] > 1 ? $"_{used[key]}" : "")}", start, i);
start = i;
}
}
/// <summary>
/// True when a stretch of a side line is squeezed into a point, on the inside of a very tight bend.
/// Nothing with a length of its own fits in there.
/// </summary>
public bool Squeezed( int from, int to, float left )
{
for ( var i = from; i < to; i++ )
{
if ( Track.Place( i, left ).Distance( Track.Place( i + 1, left ) ) < SqueezedStep ) return true;
}
return false;
}
/// <summary>
/// A point at any distance along the road, <paramref name="left"/> beside it and <paramref name="up"/> above it.
/// </summary>
public Vector3 At( float distance, float left, float up )
{
var (index, t) = Track.Locate( distance );
return Track.PlaceBetween( index, t, left, up );
}
/// <summary>
/// A point on the top of the carriageway.
/// </summary>
public Vector3 OnTop( float distance, float left, float lift = 0.0f ) => At( distance, left, Section.Surface( left ) + lift );
/// <summary>
/// Stations between two distances, with both ends.
/// </summary>
public List<float> Stops( float from, float to )
{
var stops = new List<float> { from };
foreach ( var frame in Track.Frames )
{
if ( frame.Distance > from + 0.5f && frame.Distance < to - 0.5f )
stops.Add( frame.Distance );
}
stops.Add( to );
return stops;
}
/// <summary>
/// A section run along the road between any two distances.
/// </summary>
public void Run( MeshKit kit, float from, float to, IReadOnlyList<Vector2> section )
{
var stops = Stops( from, to );
var rings = section.Select( p =>
{
var ring = new Vector3[stops.Count];
for ( var i = 0; i < stops.Count; i++ )
ring[i] = At( stops[i], p.x, p.y );
return ring;
} ).ToArray();
kit.Sweep( rings, false );
}
/// <summary>
/// A section of a road ends in a flat face. Paint reaches into the road, so it must not have an
/// end of its own in that same plane, however it happens to fall.
/// </summary>
float Beside( float distance, float inward )
{
foreach ( var end in _sectionEnds )
{
if ( MathF.Abs( end - distance ) < EndClearance ) return end + inward * EndClearance;
}
return distance;
}
/// <summary>
/// A thin body lying on the top of the carriageway, following its crown and every bend. This is
/// what paint, patches and bumps are made of. It reaches a little into the road, so
/// nothing lies flat on another surface. <paramref name = "height"/> shapes its top.
/// </summary>
public void Decal( MeshKit kit, float from, float to, float right, float left, float lift, float embed, Func<float, float, float> height = null, int rows = 0 )
{
if ( to <= from || left <= right )
return;
// Clip the body before sampling. Locate clamps out-of-path samples to the
// same endpoint, which otherwise creates zero-area faces in end bumps.
from = Math.Clamp( Beside( Math.Clamp( from, 0, Track.Length ), 1.0f ), 0, Track.Length );
to = Math.Clamp( Beside( Math.Clamp( to, 0, Track.Length ), -1.0f ), 0, Track.Length );
if ( to <= from )
return;
var along = rows > 0 ? [..Enumerable.Range( 0, rows + 1 ).Select( i => from + (to - from) * i / rows )] : Stops( from, to );
var across = new List<float>
{
right
};
across.AddRange( Section.TopPoints.Where( x => x > right + 0.01f && x < left - 0.01f ).OrderBy( x => x ) );
across.Add( left );
var upper = new Vector3[along.Count, across.Count];
var lower = new Vector3[along.Count, across.Count];
for ( var i = 0; i < along.Count; i++ )
{
for ( var j = 0; j < across.Count; j++ )
{
var warp = Warp( along[i] );
var top = lift + warp + (height?.Invoke( along[i], across[j] ) ?? 0.0f);
upper[i, j] = OnTop( along[i], across[j], top );
lower[i, j] = OnTop( along[i], across[j], -embed - warp );
}
}
kit.Shell( upper, lower );
}
/// <summary>
/// A pointed piece of paint: the tip of an arrow.
/// </summary>
public void Wedge( MeshKit kit, float wide, float tip, float middle, float halfWidth, float lift, float embed )
{
var forward = tip > wide;
var near = forward ? wide : tip;
var far = forward ? tip : wide;
var warp = MathF.Max( Warp( wide ), Warp( tip ) );
Vector3 Point( float distance, float across, float up ) => OnTop( distance, across, up + MathF.Sign( up ) * warp );
var (a, b, c, d) = forward
? (middle - halfWidth, middle + halfWidth, middle, middle)
: (middle, middle, middle - halfWidth, middle + halfWidth);
var upper = new[,] { { Point( near, a, lift ), Point( near, b, lift ) }, { Point( far, c, lift ), Point( far, d, lift ) } };
var lower = new[,] { { Point( near, a, -embed ), Point( near, b, -embed ) }, { Point( far, c, -embed ), Point( far, d, -embed ) } };
kit.Shell( upper, lower );
}
}
/// <summary>
/// Colors of parts that have no material of their own, so a road reads as a road right away.
/// </summary>
public static class RoadColors
{
public static readonly Color Asphalt = new( 0.2f, 0.2f, 0.21f );
public static readonly Color Concrete = new( 0.6f, 0.59f, 0.56f );
public static readonly Color Gravel = new( 0.45f, 0.42f, 0.37f );
public static readonly Color Dirt = new( 0.36f, 0.27f, 0.19f );
public static readonly Color Cobbles = new( 0.37f, 0.35f, 0.33f );
public static readonly Color Curb = new( 0.66f, 0.65f, 0.62f );
public static readonly Color Sidewalk = new( 0.74f, 0.73f, 0.7f );
public static readonly Color Grass = new( 0.29f, 0.42f, 0.19f );
public static readonly Color White = new( 0.93f, 0.93f, 0.9f );
public static readonly Color Yellow = new( 0.9f, 0.71f, 0.13f );
public static readonly Color Metal = new( 0.46f, 0.48f, 0.5f );
public static readonly Color Iron = new( 0.13f, 0.13f, 0.14f );
public static readonly Color Crack = new( 0.07f, 0.07f, 0.07f );
public static readonly Color Patch = new( 0.14f, 0.14f, 0.15f );
public static readonly Color Joint = new( 0.2f, 0.2f, 0.2f );
public static Color Of( RoadPaving paving ) => paving switch
{
RoadPaving.Concrete => Concrete,
RoadPaving.Gravel => Gravel,
RoadPaving.Dirt => Dirt,
RoadPaving.Cobbles => Cobbles,
_ => Asphalt
};
public static Color Of( RoadStrip strip ) => strip.Name switch
{
"shoulder" => Gravel,
"gutter" => Concrete,
"curb" => Curb,
"verge" => Grass,
_ => Sidewalk
};
public static string MaterialOf( RoadStrip strip, RoadSection section ) => strip.Name switch
{
"shoulder" => section.ShoulderMaterial,
"gutter" or "curb" => section.CurbMaterial,
"verge" => section.VergeMaterial,
_ => section.SidewalkMaterial
};
/// <summary>
/// A part in its material, or in its color while it has none.
/// </summary>
public static PartPlan Paint( this PartPlan part, string material, Color color )
{
part.Tint = string.IsNullOrEmpty( material ) ? color : Color.White;
return part;
}
}
/// <summary>
/// A road of independent parts, section by section: deck, shoulders, curbs, verges, sidewalks, paint and
/// everything standing along it. All of them are cuts through one shared cross section.
/// </summary>
public sealed class RoadGenerator : IPartGenerator
{
public const string GeneratorId = "road";
public string Id => GeneratorId;
public string Title => "Road";
public Type SettingsType => typeof( RoadSettings );
public IReadOnlyList<string> SeedNames => RoadBuilder.Categories;
public PartPlan Generate( GeneratorSettings settings, GenContext context )
{
var s = (RoadSettings)settings;
var root = context.Group( "root", $"Road: {s.Type}", "Building", RoadBuilder.RoadCategory );
if ( s.Path is null || s.Path.SegmentCount == 0 ) return root;
new RoadBuilder( s, new RoadShape( s ), context, root ).Build();
return root;
}
}
/// <summary>
/// Builds one road. The work of <see cref="RoadGenerator"/>, split off because all of it needs the same things.
/// </summary>
public sealed class RoadBuilder( RoadSettings s, RoadShape shape, GenContext context, PartPlan root )
{
public const string RoadCategory = "road";
public const string CurbCategory = "curb";
public const string SidewalkCategory = "sidewalk";
public const string MarkingCategory = "marking";
public const string DamageCategory = "damage";
public const string DetailCategory = "roaddetail";
public const string BarrierCategory = "barrier";
public const string LightCategory = "light";
public const string PropCategory = "roadprop";
public const string SupportCategory = "support";
public static readonly string[] Categories = [RoadCategory, CurbCategory, SidewalkCategory, MarkingCategory, DamageCategory, DetailCategory, BarrierCategory, LightCategory, PropCategory, SupportCategory];
const float MarkLift = 0.12f;
const float MarkEmbed = 0.5f;
const float JointLift = 0.06f;
const float JointEmbed = 1.3f;
const float CrackLift = 0.28f;
const float CrackEmbed = 0.62f;
const int MostCracks = 8;
const float PatchLift = 0.5f;
const float PatchEmbed = 0.75f;
const float CoverLift = 0.7f;
const float CoverEmbed = 0.9f;
const float StudLift = 1.0f;
const float StudEmbed = 1.1f;
const float CoverClearance = 32.0f;
const float BumpEmbed = 1.5f;
const float EndGap = 16.0f;
const float StopWidth = 12.0f;
const float CrosswalkBar = 18.0f;
const float CrosswalkSpace = 22.0f;
const float SlabJoint = 0.75f;
const float GuardrailOffset = 6.0f;
const float JerseyWidth = 24.0f;
const float WallWidth = 12.0f;
const float RailHeight = 8.0f;
const float MinRailStep = 1.0f;
const float ArrowLength = 96.0f;
const float ArrowSetback = 96.0f;
const float WearPiece = 64.0f;
const float DoubleLineGap = 8.0f;
const float SectionGap = 0.1f;
const float CrackWidth = 0.6f;
const float BumpHeight = 3.0f;
const float BumpLength = 18.0f;
const int BumpRows = 6;
const float EdgeClearance = 1.0f;
readonly Dictionary<int, int> _bays = [];
readonly RoadSection _section = shape.Section;
RoadSide Left => _section.Left;
RoadSide Right => _section.Right;
float Half => _section.Half;
/// <summary>
/// How far a barrier reaches past the edge of the road.
/// </summary>
public static float BarrierWidth( RoadBarrier barrier ) => barrier switch
{
RoadBarrier.Guardrail => GuardrailOffset + 4.0f,
RoadBarrier.Jersey => JerseyWidth,
RoadBarrier.Wall => WallWidth,
_ => 0.0f
};
PartPlan Part( string id, string name, string kind, string category, string material, Color color, Action<MeshKit> build, Func<PolygonMesh, PolygonMesh> finish = null ) =>
context.Solid( id, name, kind, category, context.Material( material ), build, finish ).Paint( material, color );
public void Build()
{
var bays = new List<(string Id, RoadStretch Stretch, int From, int To)>();
foreach ( var stretch in shape.Stretches )
foreach ( var (id, from, to) in shape.Bays( stretch, _bays ) )
bays.Add( (id, stretch, from, to) );
foreach ( var (id, stretch, from, to) in bays )
{
var name = id["section".Length..].TrimStart( '0' );
var bay = root.Add( context.Group( id, $"Section {(name.Length == 0 ? "0" : name)}", "Section", RoadCategory ) );
bay.Add( Deck( id, stretch, from, to ) );
SideStrips( bay, id, from, to, 1.0f, "left", "Left" );
SideStrips( bay, id, from, to, -1.0f, "right", "Right" );
Barriers( bay, id, stretch, from, to );
Divider( bay, id, stretch, from, to );
Markings( bay, id, stretch, from, to );
Wear( bay, id, from, to );
}
Guardrails();
JunctionPaint();
Lights();
Details();
Numbers();
Bollards();
Supports();
}
/// <summary>
/// The distances between which a stretch carries through paint, clear of what is painted at a junction.
/// </summary>
(float From, float To) PaintRange( RoadStretch stretch )
{
return (stretch.From + (stretch.FromJunction ? PaintClearance : 0.0f), stretch.To - (stretch.ToJunction ? PaintClearance : 0.0f));
}
/// <summary>
/// How much of a road in front of a junction is taken by what is painted there.
/// </summary>
float PaintClearance => (s.Crosswalks ? EndGap + s.CrosswalkWidth : 0.0f) + (s.StopLines ? EndGap + StopWidth : EndGap);
PartPlan Deck( string id, RoadStretch stretch, int from, int to )
{
var a = shape.Track.Frames[from].Distance;
var b = shape.Track.Frames[to].Distance;
return Part( $"{id}/deck", "Road Deck", "RoadDeck", RoadCategory, s.RoadMaterial, RoadColors.Of( s.Paving ),
kit => SweepEngine.Build( kit, shape.Track, from, to, _section.Deck() ),
mesh => Holes( id, mesh, a, b ) );
}
/// <summary>
/// Potholes are real: each one is cut out of the deck, so it can be stood in and it catches the light.
/// </summary>
PolygonMesh Holes( string id, PolygonMesh mesh, float from, float to )
{
if ( !s.Potholes || s.Damage <= 0.0f ) return mesh;
var stream = context.Stream( DamageCategory, $"{id}/holes" );
var count = (int)MathF.Floor( (to - from) / 256.0f * s.Damage * 2.0f + stream.Next() );
var material = context.Material( s.RoadMaterial );
for ( var i = 0; i < count; i++ )
{
var along = stream.Range( from + 24.0f, MathF.Max( to - 24.0f, from + 24.0f ) );
var across = stream.Range( -Half + 32.0f, Half - 32.0f );
var radius = stream.Range( 12.0f, 18.0f + 16.0f * s.Damage );
var depth = MathF.Min( stream.Range( 2.0f, 4.5f ), s.Thickness * 0.6f );
var center = shape.OnTop( along, across );
var kit = new MeshKit { Material = material };
const int sides = 9;
var rings = new Vector3[sides][];
for ( var k = 0; k < sides; k++ )
{
var angle = k * MathF.PI * 2.0f / sides;
var reach = radius * stream.Range( 0.7f, 1.15f );
var direction = new Vector3( MathF.Cos( angle ), MathF.Sin( angle ), 0.0f );
rings[k] =
[
center + direction * (reach * 0.55f) + Vector3.Down * depth,
center + direction * reach + Vector3.Up * 2.0f
];
}
kit.Sweep( rings, false );
var cutter = kit.Finish();
if ( cutter is null ) continue;
var cut = MeshCutter.SubtractClean( mesh, cutter, new Transform( Vector3.Zero, Rotation.Identity ), material );
if ( cut is null ) continue;
// Measured on a copy: the engine keeps the triangles it worked out once, and this mesh still gets moved.
var copy = new PolygonMesh();
copy.MergeMesh( cut, Transform.Zero, out _, out _, out _ );
var report = MeshDiagnostics.Analyze( copy );
if ( report.Errors == 0 && report.ClosedShells == report.Shells ) mesh = cut;
}
return mesh;
}
void SideStrips( PartPlan bay, string id, int from, int to, float sign, string side, string label )
{
var a = shape.Track.Frames[from].Distance;
var b = shape.Track.Frames[to].Distance;
foreach ( var strip in (sign > 0.0f ? Left : Right).Strips( s.Thickness ) )
{
if ( strip.Width <= 0.0f ) continue;
var section = _section.Place( strip, sign );
void Build( MeshKit kit )
{
if ( strip.Name != "sidewalk" || s.SlabLength <= 0.0f || shape.Squeezed( from, to, sign * (Half + (sign > 0.0f ? Left : Right).Width) ) )
{
SweepEngine.Build( kit, shape.Track, from, to, section );
return;
}
// Slabs sit on the same grid for every section, so a joint never moves with a cut.
var start = MathF.Floor( a / s.SlabLength ) * s.SlabLength;
for ( var edge = start; edge < b - 0.5f; edge += s.SlabLength )
{
var x0 = MathF.Max( edge, a );
var x1 = MathF.Min( edge + s.SlabLength, b );
if ( x1 - x0 < 1.0f ) continue;
shape.Run( kit, x0 <= a ? a : x0 + SlabJoint * 0.5f, x1 >= b ? b : x1 - SlabJoint * 0.5f, section );
}
}
bay.Add( Part( $"{id}/{strip.Name}_{side}", $"{label} {strip.Title}", strip.Kind, strip.Category, RoadColors.MaterialOf( strip, _section ), RoadColors.Of( strip ), Build ) );
}
}
void Barriers( PartPlan bay, string id, RoadStretch stretch, int from, int to )
{
void Side( RoadBarrier barrier, RoadSide side, float sign, string name, string label )
{
if ( barrier is not (RoadBarrier.Jersey or RoadBarrier.Wall) ) return;
var width = barrier == RoadBarrier.Jersey ? JerseyWidth : WallWidth;
var h = s.BarrierHeight;
var a = Half + side.Width;
var base_ = _section.Height( sign * Half ) + side.TopAt( side.Width );
var bottom = _section.Height( sign * Half ) - s.Thickness;
Vector2[] shapeOf = barrier == RoadBarrier.Jersey
? [new( a, bottom ), new( a + width, bottom ), new( a + width, base_ + h * 0.1f ), new( a + width * 0.8f, base_ + h * 0.3f ), new( a + width * 0.62f, base_ + h ), new( a + width * 0.38f, base_ + h ), new( a + width * 0.2f, base_ + h * 0.3f ), new( a, base_ + h * 0.1f )]
: [new( a, bottom ), new( a + width, bottom ), new( a + width, base_ + h ), new( a, base_ + h )];
IReadOnlyList<Vector2> section = sign > 0.0f ? shapeOf : [.. shapeOf.AsEnumerable().Reverse().Select( p => new Vector2( -p.x, p.y ) )];
bay.Add( Part( $"{id}/barrier_{name}", $"{label} Barrier", "Barrier", BarrierCategory, s.DetailMaterial, RoadColors.Concrete,
kit => SweepEngine.Build( kit, shape.Track, from, to, section ) ) );
}
Side( s.LeftBarrier, Left, 1.0f, "left", "Left" );
Side( s.RightBarrier, Right, -1.0f, "right", "Right" );
}
/// <summary>
/// Guardrails are one long rail per stretch on posts, they do not care for sections.
/// </summary>
void Guardrails()
{
void Side( RoadBarrier barrier, RoadSide side, float sign, string name, string label )
{
if ( barrier != RoadBarrier.Guardrail ) return;
var left = sign * (Half + side.Width + GuardrailOffset);
var floor = _section.Height( sign * Half ) + side.TopAt( side.Width );
var group = root.Add( context.Group( $"guardrail_{name}", $"{label} Guardrail", "Railing", BarrierCategory ) );
var rail = shape.Track.Side( left, floor + s.BarrierHeight - RailHeight * 0.5f );
for ( var n = 0; n < shape.Stretches.Count; n++ )
{
var stretch = shape.Stretches[n];
var from = shape.Index( stretch.From );
var to = Math.Min( shape.Index( stretch.To ), rail.Count - 1 );
var kept = new List<Vector3>();
for ( var i = from; i <= to; i++ )
{
var point = rail.Place( i, 0.0f );
if ( kept.Count == 0 || kept[^1].Distance( point ) >= MinRailStep || i == to ) kept.Add( point );
}
// A rail all the way around a closed road stops just short of itself.
if ( kept.Count > 2 && kept[0].Distance( kept[^1] ) < MinRailStep )
kept[^1] = Vector3.Lerp( kept[^2], kept[^1], 1.0f - MinRailStep / MathF.Max( kept[^2].Distance( kept[^1] ), MinRailStep ) );
var line = PathTrack.Along( kept, false );
group.Add( Part( $"guardrail_{name}/rail{n}", "Rail", "GuardrailBeam", BarrierCategory, s.DetailMaterial, RoadColors.Metal,
kit => SweepEngine.Build( kit, line, 0, line.Count - 1, [new( -1.0f, -RailHeight * 0.5f ), new( 1.0f, -RailHeight * 0.5f ), new( 1.0f, RailHeight * 0.5f ), new( -1.0f, RailHeight * 0.5f )] ) ) );
var count = Math.Max( (int)MathF.Round( (stretch.To - stretch.From) / MathF.Max( s.PostSpacing, 16.0f ) ), 1 );
var last = default( Vector3? );
for ( var i = 0; i <= (shape.Track.Closed && shape.Stretches.Count == 1 ? count - 1 : count); i++ )
{
var along = stretch.From + (stretch.To - stretch.From) * i / count;
var foot = shape.At( along, left, floor );
var top = s.BarrierHeight - RailHeight * 0.5f;
if ( last is Vector3 before && before.WithZ( 0.0f ).Distance( foot.WithZ( 0.0f ) ) < MathF.Max( s.PostSpacing, 16.0f ) * 0.5f ) continue;
last = foot;
group.Add( Part( $"guardrail_{name}/post{n}_{i}", "Post", "GuardrailPost", BarrierCategory, s.DetailMaterial, RoadColors.Metal,
kit => kit.Box( foot + new Vector3( -1.5f, -1.5f, -4.0f ), foot + new Vector3( 1.5f, 1.5f, top ) ) ) );
}
}
}
Side( s.LeftBarrier, Left, 1.0f, "left", "Left" );
Side( s.RightBarrier, Right, -1.0f, "right", "Right" );
}
void Divider( PartPlan bay, string id, RoadStretch stretch, int from, int to )
{
if ( s.Divider == RoadDivider.None )
return;
var range = PaintRange( stretch );
var a = MathF.Max( shape.Track.Frames[from].Distance + SectionGap, range.From );
var b = MathF.Min( shape.Track.Frames[to].Distance - SectionGap, range.To );
if ( b - a < 1.0f )
return;
var half = s.DividerWidth * 0.5f;
var low = -1.0f;
var middle = s.CustomDirectionWidths ? Lanes().Middle : 0;
if ( s.Divider == RoadDivider.Raised )
{
Vector2[] curb = [new( -half, _section.Surface( -half ) + low ), new( half, _section.Surface( half ) + low ), new( half, _section.Surface( half ) + s.DividerHeight ), new( half - s.CurbWidth, _section.Surface( half ) + s.DividerHeight ), new( -half + s.CurbWidth, _section.Surface( -half ) + s.DividerHeight ), new( -half, _section.Surface( -half ) + s.DividerHeight )];
bay.Add( Part( $"{id}/divider", "Divider", "Divider", CurbCategory, s.CurbMaterial, RoadColors.Curb, kit => shape.Run( kit, a, b, curb.Select( p => new Vector2( p.x + middle, p.y ) ).ToArray() ) ) );
if ( half > s.CurbWidth + 2.0f )
{
var inner = half - s.CurbWidth;
Vector2[] grass = [new( -inner, s.DividerHeight - 1.0f ), new( inner, s.DividerHeight - 1.0f ), new( inner, s.DividerHeight + 1.0f ), new( -inner, s.DividerHeight + 1.0f )];
bay.Add( Part( $"{id}/divider_grass", "Divider Verge", "Verge", SidewalkCategory, s.VergeMaterial, RoadColors.Grass, kit => shape.Run( kit, a + 1.0f, b - 1.0f, grass.Select( p => new Vector2( p.x + middle, p.y ) ).ToArray() ) ) );
}
return;
}
var h = s.DividerHeight;
Vector2[] jersey = [new( -half, low ), new( half, low ), new( half, h * 0.1f ), new( half * 0.6f, h * 0.3f ), new( half * 0.24f, h ), new( -half * 0.24f, h ), new( -half * 0.6f, h * 0.3f ), new( -half, h * 0.1f )];
bay.Add( Part( $"{id}/divider", "Divider", "Barrier", BarrierCategory, s.DetailMaterial, RoadColors.Concrete, kit => shape.Run( kit, a, b, jersey.Select( p => new Vector2( p.x + middle, p.y ) ).ToArray() ) ) );
}
/// <summary>
/// Where lanes lie: the right edge of the driven width, the lane width and how many lanes run each way.
/// </summary>
(float Right, float Middle, float Left, int RightLanes, int LeftLanes) Lanes()
{
var space = s.Divider != RoadDivider.None ? s.DividerWidth : 0.0f;
var all = s.DrivenWidth + space;
var right = -all * 0.5f;
if ( s.OneWay )
return (right, right + all, right + all, s.Lanes, 0);
var rightLanes = (s.Lanes + 1) / 2;
var middle = right + rightLanes * s.RightLaneWidth + space * 0.5f;
return (right, middle, -right, rightLanes, s.Lanes - rightLanes);
}
void Paint( MeshKit kit, string key, float from, float to, float right, float left, float lift = MarkLift )
{
if ( s.MarkingWear <= 0.0f )
{
shape.Decal( kit, from, to, right, left, lift, MarkEmbed );
return;
}
// Old paint comes off in pieces, always the same ones.
for ( var at = from; at < to - 0.5f; at += WearPiece )
{
var end = MathF.Min( at + WearPiece, to );
if ( context.Stream( MarkingCategory, $"{key}/{(int)at}" ).Chance( s.MarkingWear ) ) continue;
shape.Decal( kit, at, end, right, left, lift, MarkEmbed );
}
}
void Line( PartPlan bay, string id, string name, string label, float from, float to, float middle, bool dashed, bool yellow, float width = 0.0f )
{
if ( to - from < 1.0f ) return;
var half = (width > 0.0f ? width : s.LineWidth) * 0.5f;
var key = $"{id}/{name}";
bay.Add( Part( key, label, "RoadLine", MarkingCategory, s.MarkingMaterial, yellow ? RoadColors.Yellow : RoadColors.White, kit =>
{
if ( !dashed )
{
Paint( kit, key, from, to, middle - half, middle + half );
return;
}
var period = s.DashLength + s.DashGap;
for ( var n = (int)MathF.Floor( from / period ); n * period < to; n++ )
{
var a = MathF.Max( n * period, from );
var b = MathF.Min( n * period + s.DashLength, to );
if ( b - a > 1.0f ) Paint( kit, key, a, b, middle - half, middle + half );
}
} ) );
}
void Markings( PartPlan bay, string id, RoadStretch stretch, int from, int to )
{
var range = PaintRange( stretch );
var a = MathF.Max( shape.Track.Frames[from].Distance + SectionGap, range.From );
var b = MathF.Min( shape.Track.Frames[to].Distance - SectionGap, range.To );
if ( b <= a )
return;
var lanes = Lanes();
var space = s.Divider != RoadDivider.None ? s.DividerWidth * 0.5f : 0.0f;
if ( !s.OneWay && s.Divider == RoadDivider.None && lanes.LeftLanes > 0 )
{
var color = s.YellowCenter;
switch ( s.CenterLine )
{
case CenterMarking.Dashed:
Line( bay, id, "center", "Center Line", a, b, lanes.Middle, true, color );
break;
case CenterMarking.Solid:
Line( bay, id, "center", "Center Line", a, b, lanes.Middle, false, color );
break;
case CenterMarking.DoubleSolid:
Line( bay, id, "center_left", "Center Line", a, b, lanes.Middle + DoubleLineGap * 0.5f, false, color );
Line( bay, id, "center_right", "Center Line", a, b, lanes.Middle - DoubleLineGap * 0.5f, false, color );
break;
}
}
if ( s.LaneLines )
{
for ( var i = 1; i < lanes.RightLanes; i++ )
Line( bay, id, $"lane_right{i}", "Lane Line", a, b, lanes.Right + i * s.RightLaneWidth, true, false );
for ( var i = 1; i < lanes.LeftLanes; i++ )
Line( bay, id, $"lane_left{i}", "Lane Line", a, b, lanes.Left - i * s.LeftLaneWidth, true, false );
}
if ( s.EdgeLines )
{
var edge = MathF.Min( lanes.Left + (s.Shoulder == ShoulderKind.Paved ? 2.0f : -6.0f), Half - s.LineWidth );
Line( bay, id, "edge_left", "Edge Line", a, b, edge, false, false );
Line( bay, id, "edge_right", "Edge Line", a, b, -edge, false, false );
if ( s.Divider != RoadDivider.None )
{
Line( bay, id, "edge_inner_left", "Edge Line", a, b, (s.CustomDirectionWidths ? lanes.Middle : 0) + space + 4.0f, false, true );
Line( bay, id, "edge_inner_right", "Edge Line", a, b, (s.CustomDirectionWidths ? lanes.Middle : 0) - space - 4.0f, false, true );
}
}
if ( s.JointSpacing > 0.0f )
{
bay.Add( Part( $"{id}/joints", "Expansion Joints", "RoadJoint", MarkingCategory, s.MarkingMaterial, RoadColors.Joint, kit =>
{
for ( var n = (int)MathF.Ceiling( a / s.JointSpacing ); n * s.JointSpacing < b; n++ )
shape.Decal( kit, n * s.JointSpacing - 0.5f, n * s.JointSpacing + 0.5f, -Half + EdgeClearance, Half - EdgeClearance, JointLift, JointEmbed );
} ) );
}
if ( s.Reflectors )
{
bay.Add( Part( $"{id}/reflectors", "Reflectors", "Reflector", MarkingCategory, s.MarkingMaterial, RoadColors.White, kit =>
{
var middle = s.OneWay || s.Divider != RoadDivider.None ? lanes.Right + s.RightLaneWidth : lanes.Middle;
for ( var n = (int)MathF.Ceiling( a / s.ReflectorSpacing ); n * s.ReflectorSpacing < b; n++ )
shape.Decal( kit, n * s.ReflectorSpacing - 2.0f, n * s.ReflectorSpacing + 2.0f, middle + s.LineWidth, middle + s.LineWidth + 3.0f, StudLift, StudEmbed );
} ) );
}
if ( s.SpeedBumpSpacing > 0.0f )
{
for ( var n = (int)MathF.Ceiling( a / s.SpeedBumpSpacing ); n * s.SpeedBumpSpacing < b; n++ )
{
var at = n * s.SpeedBumpSpacing;
bay.Add( Part( $"{id}/bump{n}", "Speed Bump", "SpeedBump", DetailCategory, s.MarkingMaterial, RoadColors.Yellow, kit =>
{
float Hump( float along, float _ ) => BumpHeight * (1.0f - MathF.Pow( (along - at) / BumpLength, 2.0f ));
shape.Decal( kit, at - BumpLength, at + BumpLength, -Half + EdgeClearance, Half - EdgeClearance, CrackLift, BumpEmbed, Hump, BumpRows );
} ) );
}
}
}
/// <summary>
/// What is painted on a road where it reaches a junction. It stays on the road, so a junction only has to
/// build what the roads do not have: the middle and the corners.
/// </summary>
void JunctionPaint()
{
var lanes = Lanes();
for ( var n = 0; n < shape.Stretches.Count; n++ )
{
var stretch = shape.Stretches[n];
void End( bool atStart, bool yield, string name )
{
var direction = atStart ? 1.0f : -1.0f;
var cut = atStart ? stretch.From : stretch.To;
var at = cut + direction * EndGap;
var group = root.Add( context.Group( $"junction_paint{n}_{name}", "Junction Paint", "Marking", MarkingCategory ) );
if ( s.Crosswalks )
{
var a = MathF.Min( at, at + direction * s.CrosswalkWidth );
var b = MathF.Max( at, at + direction * s.CrosswalkWidth );
group.Add( Part( $"junction_paint{n}_{name}/crosswalk", "Crosswalk", "Crosswalk", MarkingCategory, s.MarkingMaterial, RoadColors.White, kit =>
{
var key = $"junction_paint{n}_{name}/crosswalk";
var count = Math.Max( (int)MathF.Floor( (Half * 2.0f - 12.0f) / (CrosswalkBar + CrosswalkSpace) ), 1 );
var start = -(count * CrosswalkBar + (count - 1) * CrosswalkSpace) * 0.5f;
for ( var i = 0; i < count; i++ )
{
var right = start + i * (CrosswalkBar + CrosswalkSpace);
Paint( kit, $"{key}{i}", a, b, right, right + CrosswalkBar );
}
} ) );
at += direction * (s.CrosswalkWidth + EndGap);
}
// Traffic drives on the right, so what comes towards this end lies on one side of the middle.
var(right, left) = s.OneWay ? (lanes.Right, lanes.Left) : atStart ? (lanes.Middle, lanes.Left) : (lanes.Right, lanes.Middle);
if ( s.CustomDirectionWidths && s.Divider != RoadDivider.None && !s.OneWay )
{
if ( atStart )
right += s.DividerWidth * .5f;
else
left -= s.DividerWidth * .5f;
}
if ( s.OneWay && atStart )
return;
right = MathF.Max( right, -Half + EdgeClearance );
left = MathF.Min( left, Half - EdgeClearance );
if ( left - right < 1.0f )
return;
if ( s.StopLines )
{
var a = MathF.Min( at, at + direction * StopWidth );
var b = MathF.Max( at, at + direction * StopWidth );
group.Add( Part( $"junction_paint{n}_{name}/stop", yield ? "Yield Line" : "Stop Line", "StopLine", MarkingCategory, s.MarkingMaterial, RoadColors.White, kit =>
{
if ( !yield )
{
Paint( kit, $"junction_paint{n}_{name}/stop", a, b, right, left );
return;
}
for ( var x = right; x < left - 8.0f; x += 40.0f )
Paint( kit, $"junction_paint{n}_{name}/stop{(int)x}", a, b, x, MathF.Min( x + 24.0f, left ) );
} ) );
}
if ( !s.Arrows )
return;
var paintLaneWidth = s.OneWay || !atStart ? s.RightLaneWidth : s.LeftLaneWidth;
var count = (int)MathF.Round( (left - right) / paintLaneWidth );
for ( var i = 0; i < count; i++ )
{
var middle = right + (i + 0.5f) * paintLaneWidth;
var tail = at + direction * (ArrowSetback + ArrowLength);
var neck = at + direction * (ArrowSetback + 32.0f);
var tip = at + direction * ArrowSetback;
group.Add( Part( $"junction_paint{n}_{name}/arrow{i}", "Arrow", "RoadArrow", MarkingCategory, s.MarkingMaterial, RoadColors.White, kit =>
{
// The shaft stops a hair short of the head, or the two would have faces in one plane.
var shaftEnd = neck + direction * SectionGap;
shape.Decal( kit, MathF.Min( tail, shaftEnd ), MathF.Max( tail, shaftEnd ), middle - 4.0f, middle + 4.0f, MarkLift, MarkEmbed );
shape.Wedge( kit, neck, tip, middle, 14.0f, MarkLift, MarkEmbed );
} ) );
}
}
if ( stretch.FromJunction )
End( true, stretch.FromYield, "start" );
if ( stretch.ToJunction )
End( false, stretch.ToYield, "end" );
}
}
void Wear( PartPlan bay, string id, int from, int to )
{
if ( s.Damage <= 0.0f ) return;
var a = shape.Track.Frames[from].Distance;
var b = shape.Track.Frames[to].Distance;
var length = b - a;
if ( s.Patches )
{
var stream = context.Stream( DamageCategory, $"{id}/patches" );
var count = (int)MathF.Floor( length / 256.0f * s.Damage * 1.5f + stream.Next() );
if ( count > 0 )
{
var spots = new List<(float X0, float X1, float Y0, float Y1)>();
for ( var i = 0; i < count; i++ )
{
var along = stream.Range( a + 8.0f, MathF.Max( b - 56.0f, a + 8.0f ) );
var across = stream.Range( -Half + 8.0f, Half - 64.0f );
var spot = (X0: along, X1: MathF.Min( along + stream.Range( 24.0f, 96.0f ), b - 1.0f ), Y0: across, Y1: MathF.Min( across + stream.Range( 24.0f, 64.0f ), Half - EdgeClearance ));
if ( !spots.Any( x => x.X0 < spot.X1 + 2.0f && spot.X0 < x.X1 + 2.0f && x.Y0 < spot.Y1 + 2.0f && spot.Y0 < x.Y1 + 2.0f ) )
spots.Add( spot );
}
bay.Add( Part( $"{id}/patches", "Patches", "RoadPatch", DamageCategory, s.RoadMaterial, RoadColors.Patch, kit =>
{
foreach ( var (x0, x1, y0, y1) in spots )
shape.Decal( kit, x0, x1, y0, y1, PatchLift, PatchEmbed );
} ) );
}
}
if ( !s.Cracks ) return;
var cracks = context.Stream( DamageCategory, $"{id}/cracks" );
var lines = Math.Min( (int)MathF.Floor( length / 128.0f * s.Damage * 2.0f + cracks.Next() ), MostCracks );
if ( lines == 0 ) return;
var paths = new List<List<(float Along, float Across)>>();
static bool Crosses( List<(float Along, float Across)> first, List<(float Along, float Across)> second ) =>
first.Zip( first.Skip( 1 ) ).Any( x => second.Zip( second.Skip( 1 ) ).Any( y => Touches( x.First, x.Second, y.First, y.Second ) ) );
for ( var n = 0; n < lines; n++ )
{
var along = cracks.Range( a + 4.0f, MathF.Max( b - 4.0f, a + 4.0f ) );
var across = cracks.Range( -Half + 8.0f, Half - 8.0f );
var heading = cracks.Range( 0.0f, MathF.PI * 2.0f );
var points = new List<(float Along, float Across)> { (along, across) };
for ( var k = cracks.Range( 3.0f, 8.0f ); k > 0.0f; k-- )
{
heading += cracks.Range( -0.9f, 0.9f );
var step = cracks.Range( 10.0f, 28.0f );
var nextAlong = along + MathF.Cos( heading ) * step;
var nextAcross = across + MathF.Sin( heading ) * step;
// A crack that reaches the edge of its section or of the road ends there, and it never crosses itself.
if ( nextAlong < a + 2.0f || nextAlong > b - 2.0f || MathF.Abs( nextAcross ) > Half - 2.0f ) break;
if ( points.SkipLast( 1 ).Zip( points.Skip( 1 ).SkipLast( 1 ) ).Any( x => Touches( x.First, x.Second, (along, across), (nextAlong, nextAcross) ) ) ) break;
along = nextAlong;
across = nextAcross;
points.Add( (along, across) );
}
if ( !paths.Any( x => Crosses( x, points ) ) ) paths.Add( points );
}
bay.Add( Part( $"{id}/cracks", "Cracks", "RoadCrack", DamageCategory, s.MarkingMaterial, RoadColors.Crack, kit =>
{
foreach ( var path in paths )
{
if ( path.Count < 2 ) continue;
var rings = new Vector3[4][];
for ( var r = 0; r < rings.Length; r++ )
rings[r] = new Vector3[path.Count];
for ( var i = 0; i < path.Count; i++ )
{
var before = path[Math.Max( i - 1, 0 )];
var after = path[Math.Min( i + 1, path.Count - 1 )];
var direction = new Vector2( after.Along - before.Along, after.Across - before.Across ).Normal;
var (along, across) = path[i];
var dx = -direction.y * CrackWidth;
var dy = direction.x * CrackWidth;
var warp = shape.Warp( along );
rings[0][i] = shape.OnTop( along - dx, across - dy, -CrackEmbed - warp );
rings[1][i] = shape.OnTop( along + dx, across + dy, -CrackEmbed - warp );
rings[2][i] = shape.OnTop( along + dx, across + dy, CrackLift + warp );
rings[3][i] = shape.OnTop( along - dx, across - dy, CrackLift + warp );
}
kit.Sweep( rings, false );
}
} ) );
}
/// <summary>
/// True when two pieces of a line cross or come too close to tell them apart.
/// </summary>
static bool Touches( (float Along, float Across) a, (float Along, float Across) b, (float Along, float Across) c, (float Along, float Across) d )
{
const float steps = 8.0f;
for ( var i = 0.0f; i <= steps; i++ )
{
for ( var k = 0.0f; k <= steps; k++ )
{
var x = a.Along + (b.Along - a.Along) * (i / steps) - c.Along - (d.Along - c.Along) * (k / steps);
var y = a.Across + (b.Across - a.Across) * (i / steps) - c.Across - (d.Across - c.Across) * (k / steps);
if ( x * x + y * y < CrackWidth * CrackWidth * 16.0f ) return true;
}
}
return false;
}
/// <summary>
/// Distances along the road that lie on a stretch and stay clear of its ends.
/// </summary>
IEnumerable<(int Number, float Along)> Spots( float spacing, float phase, float clearance )
{
var step = MathF.Max( spacing, 16.0f );
foreach ( var stretch in shape.Stretches )
{
var from = stretch.From + (stretch.FromJunction ? clearance : 0.0f);
var to = stretch.To - (stretch.ToJunction ? clearance : 0.0f);
for ( var n = (int)MathF.Ceiling( from / step - phase ); (n + phase) * step <= to; n++ )
yield return (n, (n + phase) * step);
}
}
/// <summary>
/// Where something standing beside the road goes, and how high the ground is there.
/// </summary>
(float Left, float Up) Roadside( float sign, float share )
{
var side = sign > 0.0f ? Left : Right;
var fixedPart = side.Shoulder + side.Gutter + side.CurbWidth;
var soft = side.Verge + side.Sidewalk;
var barrier = BarrierWidth( sign > 0.0f ? s.LeftBarrier : s.RightBarrier );
var outward = soft > 0.0f ? fixedPart + (side.Verge > 0.0f ? side.Verge * 0.5f : soft * share) : side.Width + barrier + 12.0f;
var top = soft > 0.0f ? side.TopAt( MathF.Min( outward, side.Width - 0.01f ) ) : side.TopAt( side.Width );
return (sign * (Half + outward), _section.Height( sign * Half ) + top);
}
PartPlan Prop( StreetProp kind, string id, string name, string category, Vector3 position, float yaw, float wear = 0.0f )
{
var settings = new StreetSettings { Kind = kind, MainMaterial = s.DetailMaterial, MetalMaterial = s.DetailMaterial, DetailMaterial = s.DetailMaterial, Wear = wear };
var prop = new StreetGenerator().Inside( settings, context, id, name, category ).At( position, yaw );
foreach ( var part in prop.All() )
part.Paint( s.DetailMaterial, RoadColors.Metal );
return prop;
}
static float YawOf( Vector3 direction ) => MathF.Atan2( direction.y, direction.x ).RadianToDegree();
void Lights()
{
if ( !s.StreetLights )
return;
var group = root.Add( context.Group( "lights", "Street Lights", "Lights", LightCategory ) );
foreach ( var(n, along)in Spots( s.LightSpacing, 0.5f, 64.0f ) )
{
// A divider wide enough to stand on takes one lamp with an arm over each side.
if ( s.Divider == RoadDivider.Raised && s.DividerWidth >= 48.0f )
{
var frame = shape.Track.FrameAt( along );
var middle = Lanes().Right + Lanes().RightLanes * s.RightLaneWidth + s.DividerWidth * 0.5f;
group.Add( Prop( StreetProp.DoubleLamp, $"lights/lamp{n}_middle", "Street Lamp", LightCategory, shape.OnTop( along, middle, s.DividerHeight ), YawOf( frame.Left ) ) );
continue;
}
var sides = s.LightSide switch
{
LightSide.Left => new[]
{
1.0f
},
LightSide.Right => new[]
{
-1.0f
},
LightSide.Both => new[]
{
1.0f,
-1.0f
},
_ => new[]
{
n % 2 == 0 ? -1.0f : 1.0f
}
};
foreach ( var sign in sides )
{
var(left, up) = Roadside( sign, 0.15f );
var position = shape.At( along, left, up );
group.Add( Prop( StreetProp.Lamp, $"lights/lamp{n}_{(sign > 0.0f ? "left" : "right")}", "Street Lamp", LightCategory, position, YawOf( shape.Track.FrameAt( along ).Left * -sign ) ) );
}
}
}
bool OnBump( float along )
{
if ( s.SpeedBumpSpacing <= 0.0f ) return false;
var nearest = MathF.Round( along / s.SpeedBumpSpacing ) * s.SpeedBumpSpacing;
return MathF.Abs( along - nearest ) < BumpLength + 16.0f;
}
void Details()
{
var lanes = Lanes();
if ( s.Manholes )
{
var group = root.Add( context.Group( "manholes", "Manholes", "Manholes", DetailCategory ) );
foreach ( var(n, along)in Spots( s.ManholeSpacing, 0.25f, PaintClearance + CoverClearance ) )
{
// In the middle of a lane, never on a line, tilted with the road.
var stream = context.Stream( DetailCategory, $"manholes/manhole{n}" );
var lane = (int)(stream.Next() * s.Lanes);
var skip = s.Divider != RoadDivider.None && lane >= lanes.RightLanes ? s.DividerWidth : 0.0f;
var across = s.LaneCenter( lane );
var center = shape.OnTop( along, across );
var frame = shape.Track.FrameAt( along );
var tilt = new Transform( center, Rotation.LookAt( frame.Forward, Vector3.Cross( frame.Forward, frame.Left ) ) );
var warp = shape.Warp( along );
group.Add( Part( $"manholes/manhole{n}", "Manhole", "Manhole", DetailCategory, s.DetailMaterial, RoadColors.Iron, kit =>
{
kit.Frame = tilt;
kit.Cylinder( Vector3.Zero, 15.0f, 15.0f, -CoverEmbed - warp, CoverLift + warp, 16 );
} ) );
}
}
if ( s.Drains )
{
var group = root.Add( context.Group( "drains", "Drains", "Drains", DetailCategory ) );
foreach ( var(n, along)in Spots( s.DrainSpacing, 0.5f, PaintClearance + CoverClearance ) )
{
if ( OnBump( along ) )
continue;
// All four bars must fit. Beyond a free end the track clamps points together.
if ( !shape.Stretches.Any( stretch => along - 14.0f >= stretch.From && along + 14.0f <= stretch.To ) )
continue;
foreach ( var(sign, name, side)in new[]
{
(1.0f, "left", Left),
(-1.0f, "right", Right)
}
)
{
if ( side.CurbWidth <= 0.0f )
continue;
var x0 = sign > 0.0f ? Half - 16.0f - EdgeClearance : -Half + EdgeClearance;
var x1 = x0 + 16.0f;
group.Add( Part( $"drains/drain{n}_{name}", "Storm Drain", "Drain", DetailCategory, s.DetailMaterial, RoadColors.Iron, kit =>
{
for ( var i = 0; i < 4; i++ )
{
var bar = along - 14.0f + i * 8.0f;
shape.Decal( kit, bar, bar + 4.0f, x0, x1, CoverLift, CoverEmbed );
}
} ) );
}
}
}
if ( s.Signs )
{
var group = root.Add( context.Group( "signs", "Road Signs", "Signs", DetailCategory ) );
foreach ( var(n, along)in Spots( s.SignSpacing, 0.25f, 128.0f ) )
{
var(left, up) = Roadside( -1.0f, 0.15f );
group.Add( Prop( StreetProp.RoadSign, $"signs/sign{n}", "Road Sign", DetailCategory, shape.At( along, left, up ), YawOf( shape.Track.FrameAt( along ).Left ) + 180.0f ) );
}
for ( var n = 0; n < shape.Stretches.Count; n++ )
{
var stretch = shape.Stretches[n];
if ( !stretch.ToJunction )
continue;
var along = stretch.To - 64.0f;
if ( along < stretch.From + 32.0f )
continue;
var(left, up) = Roadside( -1.0f, 0.15f );
group.Add( Prop( StreetProp.RoadSign, $"signs/junction{n}", "Junction Sign", DetailCategory, shape.At( along, left, up ), YawOf( shape.Track.FrameAt( along ).Left ) + 180.0f ) );
}
}
if ( s.RoadsideProps )
{
var group = root.Add( context.Group( "props", "Roadside Props", "Props", PropCategory ) );
StreetProp[] kinds = [StreetProp.Bench, StreetProp.TrashCan, StreetProp.Hydrant, StreetProp.Planter, StreetProp.Mailbox, StreetProp.Bollard, StreetProp.BikeRack, StreetProp.ParkingMeter];
foreach ( var(n, along)in Spots( s.PropSpacing, 0.33f, 96.0f ) )
{
foreach ( var(sign, name, side)in new[]
{
(1.0f, "left", Left),
(-1.0f, "right", Right)
}
)
{
if ( side.Sidewalk < 32.0f )
continue;
var stream = context.Stream( PropCategory, $"props/prop{n}_{name}" );
var kind = kinds[(int)(stream.Next() * kinds.Length)];
var(left, up) = Roadside( sign, 0.8f );
var frame = shape.Track.FrameAt( along + stream.Range( -32.0f, 32.0f ) );
group.Add( Prop( kind, $"props/prop{n}_{name}", kind.ToString(), PropCategory, shape.At( frame.Distance, left, up ), YawOf( frame.Forward ) + (sign > 0.0f ? 180.0f : 0.0f), s.Damage * 0.5f ) );
}
}
}
}
/// <summary>
/// Digits painted across the lanes, read driving forward. Each digit is made of up to seven bars.
/// </summary>
void Numbers()
{
var text = new string( (s.RoadNumber ?? "").Where( char.IsDigit ).ToArray() );
if ( text.Length == 0 ) return;
string[] segments = ["abcdef", "bc", "abdeg", "abcdg", "bcfg", "acdfg", "acdefg", "abc", "abcdefg", "abcdfg"];
const float high = 96.0f;
const float wide = 40.0f;
const float bar = 8.0f;
const float space = 16.0f;
var lanes = Lanes();
var center = s.OneWay ? 0.0f : (lanes.Right + lanes.Middle) * 0.5f;
var group = root.Add( context.Group( "numbers", "Road Numbers", "Marking", MarkingCategory ) );
foreach ( var (n, along) in Spots( s.NumberSpacing, 0.25f, 256.0f ) )
{
group.Add( Part( $"numbers/number{n}", "Road Number", "RoadNumber", MarkingCategory, s.MarkingMaterial, RoadColors.White, kit =>
{
var total = text.Length * wide + (text.Length - 1) * space;
for ( var d = 0; d < text.Length; d++ )
{
// The first digit lies on the left of the driver.
var left = center + total * 0.5f - d * (wide + space);
var right = left - wide;
foreach ( var segment in segments[text[d] - '0'] )
{
var (a, b, x0, x1) = segment switch
{
'a' => (along + high - bar, along + high, right, left),
'b' => (along + high * 0.5f, along + high, right, right + bar),
'c' => (along, along + high * 0.5f, right, right + bar),
'd' => (along, along + bar, right, left),
'e' => (along, along + high * 0.5f, left - bar, left),
'f' => (along + high * 0.5f, along + high, left - bar, left),
_ => (along + high * 0.5f - bar * 0.5f, along + high * 0.5f + bar * 0.5f, right, left)
};
shape.Decal( kit, a, b, x0, x1, MarkLift, MarkEmbed );
}
}
} ) );
}
}
void Bollards()
{
if ( !s.EndBollards ) return;
var group = root.Add( context.Group( "bollards", "End Bollards", "Bollards", DetailCategory ) );
void Row( float along, string name )
{
var count = Math.Max( (int)MathF.Round( Half * 2.0f / 64.0f ), 2 );
for ( var i = 0; i <= count; i++ )
{
var left = -Half + 16.0f + (Half * 2.0f - 32.0f) * i / count;
group.Add( Prop( StreetProp.Bollard, $"bollards/{name}{i}", "Bollard", DetailCategory, shape.OnTop( along, left ), 0.0f ) );
}
}
foreach ( var (stretch, n) in shape.Stretches.Select( ( x, i ) => (x, i) ) )
{
if ( !stretch.FromJunction && stretch.From <= 0.01f && !shape.Track.Closed ) Row( stretch.From + 24.0f, $"start{n}_" );
if ( !stretch.ToJunction && stretch.To >= shape.Length - 0.01f && !shape.Track.Closed ) Row( stretch.To - 24.0f, $"end{n}_" );
}
}
void Supports()
{
if ( !s.Supports ) return;
var group = root.Add( context.Group( "supports", "Supports", "Columns", SupportCategory ) );
var half = s.SupportSize * 0.5f;
foreach ( var (n, along) in Spots( s.SupportSpacing, 0.5f, 128.0f ) )
{
var top = shape.OnTop( along, 0.0f, -s.Thickness );
var size = Half * 0.8f;
group.Add( Part( $"supports/column{n}", "Column", "Column", SupportCategory, s.DetailMaterial, RoadColors.Concrete,
kit => kit.Box( new Vector3( top.x - half, top.y - half, top.z - s.SupportDrop ), new Vector3( top.x + half, top.y + half, top.z - 8.0f ) ) ) );
var frame = shape.Track.FrameAt( along );
group.Add( Part( $"supports/cap{n}", "Column Cap", "ColumnCap", SupportCategory, s.DetailMaterial, RoadColors.Concrete,
kit => kit.Hex(
top + frame.Left * size + frame.Forward * half + Vector3.Down * 8.0f,
top + frame.Left * size - frame.Forward * half + Vector3.Down * 8.0f,
top - frame.Left * size - frame.Forward * half + Vector3.Down * 8.0f,
top - frame.Left * size + frame.Forward * half + Vector3.Down * 8.0f,
top + frame.Left * size + frame.Forward * half,
top + frame.Left * size - frame.Forward * half,
top - frame.Left * size - frame.Forward * half,
top - frame.Left * size + frame.Forward * half ) ) );
}
}
}