editor/AdvancedMappingTools/Procedural/RoadGenerator.cs

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 ) ) );
		}
	}
}