Network/RoadBorderBuilder.cs

Mesh builder that sweeps a road-border profile (sidewalk or countryside verge) along a path or road segment and emits polygon mesh geometry. It evaluates cross-sections, handles corner arcs and mitres, tapering/blending between segments, UVs, vertex blends for gravel spills, and produces caps and corner patch geometry.

Native Interop
using System;
using System.Collections.Generic;
using System.Text;
using Sandbox;
using Sandbox.Utility;

namespace RedSnail.RoadTool2;

/// <summary>
/// Which ends of a road stretch's borders are open (nothing continues them) and need closing off.
/// </summary>
[Flags]
internal enum BorderCaps
{
	None = 0,
	StartLeft = 1,
	StartRight = 2,
	EndLeft = 4,
	EndRight = 8
}

/// <summary>
/// Generates road borders (sidewalks, country side verges) by sweeping a profile's cross-section along a path of
/// road-edge points: the sides of a road chunk, or the outline of a junction between two arms.
/// </summary>
internal static class RoadBorderBuilder
{
	/// <summary>
	/// A point of a border run: the road edge, the direction away from the road, and the along-road coordinate
	/// the textures run on.
	/// </summary>
	public readonly struct PathPoint
	{
		public readonly Vector3 Position;
		public readonly Vector3 Outward;
		public readonly float Along;

		/// <summary>
		/// How far the outer part of the profile is lifted above the road edge here (the kerb stays on the road,
		/// the outer edge takes all of it). Along a road it runs linearly from one end's corner height difference
		/// to the other's; on a corner's arc (<see cref="IsArc"/>) it is whatever brings the row to the corner's
		/// one height, and nodes further out than the fillet radius collapse onto the centre.
		/// </summary>
		public readonly float Lift;
		public readonly RoadJunctionLayout.CornerFold Fold;
		public readonly bool IsArc;

		public PathPoint(Vector3 _Position, Vector3 _Outward, float _Along, float _Lift = 0.0f, RoadJunctionLayout.CornerFold _Fold = null, bool _IsArc = false)
		{
			Position = _Position;
			Outward = _Outward;
			Along = _Along;
			Lift = _Lift;
			Fold = _Fold;
			IsArc = _IsArc;
		}

		/// <summary>
		/// True when a node at this offset sits on the corner's centre rather than at its own offset.
		/// </summary>
		public bool IsFolded(float _Offset)
		{
			return IsArc && Fold is not null && _Offset >= Fold.Radius;
		}
	}

	/// <summary>
	/// The height the outer edge of one side's border is lifted along a segment: the corner height difference at
	/// each end (0 where the end has no corner on that side), interpolated linearly between the two tangent
	/// points. Arm chunks and the junction runs evaluate this same line, so the seam between them is exact and
	/// nothing depends on how long the road is.
	/// </summary>
	public readonly record struct SideBlend(bool Active, float StartDistance, float StartDelta, float EndDistance, float EndDelta)
	{
		public float DeltaAt(float _Distance)
		{
			if (!Active)
				return 0.0f;

			float span = EndDistance - StartDistance;

			if (span <= 0.001f)
				return StartDelta;

			return MathX.Lerp(StartDelta, EndDelta, Math.Clamp((_Distance - StartDistance) / span, 0.0f, 1.0f));
		}

		public void AddToHash(ref StableHash _Hash)
		{
			_Hash.Add(Active ? 1 : 0);

			if (!Active)
				return;

			_Hash.Add(StartDistance);
			_Hash.Add(StartDelta);
			_Hash.Add(EndDistance);
			_Hash.Add(EndDelta);
		}
	}

	/// <summary>
	/// The border lifts of a segment's two sides.
	/// </summary>
	public readonly record struct SegmentBorderBlends(SideBlend Left, SideBlend Right)
	{
		public void AddToHash(ref StableHash _Hash)
		{
			Left.AddToHash(ref _Hash);
			Right.AddToHash(ref _Hash);
		}
	}

	/// <summary>
	/// One surface of the profile, between two of its nodes. U runs across the profile: fixed per node, or
	/// accumulated along the cross-section from <see cref="AccumulateFrom"/> when the shape varies (verges, and a
	/// pavement whose texture keeps its size whatever its width), at <see cref="AcrossRepeat"/> units a repeat when
	/// that is set and at <see cref="Repeat"/> otherwise. V runs along the road at <see cref="Repeat"/>. Planar faces
	/// take a top-down projection instead, like the road they extend. A blendable face is the one a neighbouring
	/// country road's gravel spills onto (the pavement), painted with blend weights where it does.
	/// </summary>
	private readonly struct Face
	{
		public readonly int NodeA;
		public readonly int NodeB;
		public readonly Material Material;
		public readonly float Repeat;
		public readonly bool Planar;
		public readonly int AccumulateFrom;
		public readonly float U0;
		public readonly float U1;
		public readonly bool Blendable;
		public readonly float AcrossRepeat;

		public Face(int _NodeA, int _NodeB, Material _Material, float _Repeat, bool _Planar = false, int _AccumulateFrom = -1, float _U0 = 0.0f, float _U1 = 1.0f, bool _Blendable = false, float _AcrossRepeat = 0.0f)
		{
			NodeA = _NodeA;
			NodeB = _NodeB;
			Material = _Material;
			Repeat = _Repeat;
			Planar = _Planar;
			AccumulateFrom = _AccumulateFrom;
			U0 = _U0;
			U1 = _U1;
			Blendable = _Blendable;
			AcrossRepeat = _AcrossRepeat;
		}
	}

	// Separate regions of the noise field for each thing that wanders, so they don't move in step.
	private const float BandShoulder = 0.0f;
	private const float BandOuter = 1000.0f;
	private const float BandRoll = 2000.0f;
	private const float BandSpill = 3000.0f;

	/// <summary>
	/// Two corners of a face closer than this are one point: the face is a triangle.
	/// </summary>
	private const float JoinDistance = 1.0f;

	/// <summary>
	/// How far the gravel's front wanders back and forth along the road, as a fraction of the spill length, and
	/// the size of the noise it wanders by (about a wavelength per hundred units).
	/// </summary>
	private const float SpillWobble = 0.35f;
	private const float SpillNoiseScale = 0.01f;



	/// <summary>
	/// Both borders of a stretch of road, rows at the same distances as the road surface.
	/// </summary>
	public static PolygonMesh BuildSegment(RoadCurveTable _Table, float _StartDistance, float _EndDistance, in RoadWidth _Width, RoadProfile _Profile, float _Spacing, float _Lift, BorderCaps _Caps, in SegmentBorderBlends _Blends, in SegmentBorderTapers _Tapers, BorderDebug _Debug = null)
	{
		var mesh = new PolygonMesh();

		// The same rows the surface is built on, so the border's inner edge sits exactly on the road's.
		var rows = RoadRows.Build(_Table, _StartDistance, _EndDistance, _Spacing, _Width, _Profile.Simplification);

		var left = new List<PathPoint>(rows.Count);
		var right = new List<PathPoint>(rows.Count);

		_Debug?.Note($"SEGMENT start={_StartDistance:0.###} end={_EndDistance:0.###} length={_Table.Length:0.###} width={_Width.Base} extras={_Width.LeftStart:0.#}/{_Width.LeftEnd:0.#}/{_Width.RightStart:0.#}/{_Width.RightEnd:0.#} lift={_Lift} spacing={_Spacing} caps={_Caps} profile={_Profile.ResourceName} border={_Profile.Border} borderWidth={_Profile.BorderWidth} curb={_Profile.CurbHeight} bevel={_Profile.CurbBevel}");
		_Debug?.Note($"BLEND left {Describe(_Blends.Left)}");
		_Debug?.Note($"BLEND right {Describe(_Blends.Right)}");
		_Debug?.Note($"TAPER left start={_Tapers.Left.StartLength:0.###}/{_Tapers.Left.StartWidth:0.###} end={_Tapers.Left.EndLength:0.###}/{_Tapers.Left.EndWidth:0.###} right start={_Tapers.Right.StartLength:0.###}/{_Tapers.Right.StartWidth:0.###} end={_Tapers.Right.EndLength:0.###}/{_Tapers.Right.EndWidth:0.###}");

		foreach (float distance in rows)
		{
			Vector3 rightDirection = RoadCurve.RightOf(_Table.TangentAt(distance));

			RoadSurfaceBuilder.CrossSection(_Table, distance, _Width, _Lift, out Vector3 leftPos, out Vector3 rightPos);

			left.Add(new PathPoint(leftPos, -rightDirection, distance, _Blends.Left.DeltaAt(distance)));
			right.Add(new PathPoint(rightPos, rightDirection, distance, _Blends.Right.DeltaAt(distance)));
		}

		var leftShape = new ProfileShape(_Profile, _Tapers.Left, _Table.Length);
		var rightShape = new ProfileShape(_Profile, _Tapers.Right, _Table.Length);

		Sweep(mesh, leftShape, left, _Caps.HasFlag(BorderCaps.StartLeft), _Caps.HasFlag(BorderCaps.EndLeft), _Debug, BorderDebug.ArmColor, "arm L");
		Sweep(mesh, rightShape, right, _Caps.HasFlag(BorderCaps.StartRight), _Caps.HasFlag(BorderCaps.EndRight), _Debug, BorderDebug.ArmColor, "arm R");

		return mesh;
	}



	/// <summary>
	/// A border along a path that is not a road's edge: the rim of a parking lot, where the points and the way out
	/// of the surface come from the caller. No taper, no corner lifts; just the profile's cross-section swept along.
	/// </summary>
	public static void BuildPath(PolygonMesh _Mesh, RoadProfile _Profile, IReadOnlyList<PathPoint> _Path, bool _CapStart, bool _CapEnd, BorderDebug _Debug = null)
	{
		if (!_Profile.HasBorder || _Path.Count < 2)
			return;

		Sweep(_Mesh, new ProfileShape(_Profile, default, 1.0f), _Path, _CapStart, _CapEnd, _Debug, BorderDebug.ArmColor, "lot");
	}



	/// <summary>
	/// A profile as swept along one segment: the profile itself, plus the taper at the segment's ends and the
	/// segment's length the taper is measured against.
	/// </summary>
	public readonly struct ProfileShape
	{
		public readonly RoadProfile Profile;
		public readonly RoadBorderTaper Taper;
		public readonly float Length;

		public ProfileShape(RoadProfile _Profile, in RoadBorderTaper _Taper, float _Length)
		{
			Profile = _Profile;
			Taper = _Taper;
			Length = _Length;
		}
	}



	/// <summary>
	/// The borders round a junction: each corner path is swept in two halves, one in each neighbouring arm's
	/// profile, so a road's border carries on round the corner in its own style. Where two road types meet round a
	/// corner, each arm's border has already tapered, by its trim, into the cross-section the two share, and its
	/// taper holds that shape on past the trim, so both halves are built in it and meet flush. Where they have not
	/// (borders of different kinds, or a road type with no transition) the halves are closed off against each other.
	/// The arms' segment blends and tapers are what their chunks were built with, so each half continues its arm's
	/// border exactly.
	/// </summary>
	public static PolygonMesh BuildJunction(RoadJunctionLayout _Layout, Func<RoadSegment, RoadProfile> _ProfileOf, Func<RoadSegment, SegmentBorderBlends> _BlendsOf, Func<RoadSegment, SegmentBorderTapers> _TaperOf, BorderDebug _Debug = null)
	{
		var mesh = new PolygonMesh();

		if (_Debug is not null)
		{
			_Debug.Note($"JUNCTION node={_Layout.Node.Id} position={BorderDebug.Format(_Layout.Node.Position)} radius={_Layout.Radius} centre={BorderDebug.Format(_Layout.Centre)}");
			_Layout.Describe(_Debug);

			foreach (var arm in _Layout.Arms)
				_Debug.Note($"BLEND arm={arm.Segment.Id} left {Describe(_BlendsOf(arm.Segment).Left)} right {Describe(_BlendsOf(arm.Segment).Right)}");
		}

		foreach (var path in _Layout.CornerPaths)
		{
			var profileA = _ProfileOf(path.ArmA.Segment);
			var profileB = _ProfileOf(path.ArmB.Segment);
			bool differ = !SameProfile(profileA, profileB);

			// The first half runs on arm A's left looking out of the node, the second on arm B's right; which side of
			// the segment that is depends on which way the segment runs.
			var taperA = _TaperOf(path.ArmA.Segment).Side(path.ArmA.AtStart);
			var taperB = _TaperOf(path.ArmB.Segment).Side(!path.ArmB.AtStart);
			var shapeA = new ProfileShape(profileA, taperA, path.ArmA.Length);
			var shapeB = new ProfileShape(profileB, taperB, path.ArmB.Length);

			// Two borders that have both tapered into the shape they share meet flush; anything else is two
			// different borders, closed off against each other.
			bool meet = taperA.TapersAt(path.ArmA.AtStart) && taperB.TapersAt(path.ArmB.AtStart);
			bool closeBetween = differ && !meet;

			if (profileA.HasBorder)
				Sweep(mesh, shapeA, HalfPath(path, true, _BlendsOf(path.ArmA.Segment)), false, closeBetween || !profileB.HasBorder, _Debug, BorderDebug.FirstHalfColor, $"J{_Layout.Node.Id} A{path.ArmA.Segment.Id}");

			if (profileB.HasBorder)
				Sweep(mesh, shapeB, HalfPath(path, false, _BlendsOf(path.ArmB.Segment)), closeBetween || !profileA.HasBorder, false, _Debug, BorderDebug.SecondHalfColor, $"J{_Layout.Node.Id} B{path.ArmB.Segment.Id}");
		}

		return mesh;
	}



	private static string Describe(in SideBlend _Blend)
	{
		return _Blend.Active
			? $"active from={_Blend.StartDistance:0.###} delta={_Blend.StartDelta:0.###} to={_Blend.EndDistance:0.###} delta={_Blend.EndDelta:0.###}"
			: "inactive";
	}



	/// <summary>
	/// The same road type, whether or not the two references are the same loaded instance of the asset.
	/// </summary>
	public static bool SameProfile(RoadProfile _A, RoadProfile _B)
	{
		if (ReferenceEquals(_A, _B))
			return true;

		if (_A is null || _B is null)
			return false;

		return !string.IsNullOrEmpty(_A.ResourcePath) && _A.ResourcePath == _B.ResourcePath;
	}



	/// <summary>
	/// One half of a corner path as a border run. Away-from-road directions come with the path points (the frame
	/// of the arm or the arc they are on), so the arm chunk's border meets this one exactly at the row. The along
	/// coordinate carries the arm's curve distance on round the corner.
	/// </summary>
	private static List<PathPoint> HalfPath(RoadJunctionLayout.CornerPath _Path, bool _FirstHalf, in SegmentBorderBlends _Blends)
	{
		var points = _Path.Points;
		int first = _FirstHalf ? 0 : _Path.MidIndex;
		int last = _FirstHalf ? _Path.MidIndex : points.Count - 1;
		var arm = _FirstHalf ? _Path.ArmA : _Path.ArmB;
		int count = last - first + 1;
		var result = new List<PathPoint>(count);

		if (count < 2)
			return result;

		// The first half runs on its arm's left (looking out of the node), the second on its arm's right; which
		// side of the segment that is depends on which way the segment runs.
		bool segmentLeft = _FirstHalf == arm.AtStart;
		var side = segmentLeft ? _Blends.Left : _Blends.Right;

		// Walking towards the node runs against an arm that starts there, and with one that ends there.
		float sign = _FirstHalf ? (arm.AtStart ? -1.0f : 1.0f) : (arm.AtStart ? 1.0f : -1.0f);
		var along = new float[count];

		if (_FirstHalf)
		{
			along[0] = arm.CurveDistance(arm.Trim);

			for (int k = 1; k < count; k++)
				along[k] = along[k - 1] + sign * Vector3.DistanceBetween(points[first + k - 1], points[first + k]);
		}
		else
		{
			along[count - 1] = arm.CurveDistance(arm.Trim);

			for (int k = count - 1; k > 0; k--)
				along[k - 1] = along[k] - sign * Vector3.DistanceBetween(points[first + k - 1], points[first + k]);
		}

		for (int k = 0; k < count; k++)
		{
			int index = first + k;
			Vector3 outward = _Path.Outwards[index];

			// A sharp corner point has no frame of its own: take the right of the way through it.
			if (outward.IsNearZeroLength && index > 0 && index < points.Count - 1)
			{
				Vector3 direction = points[index + 1] - points[index - 1];
				outward = new Vector3(direction.y, -direction.x, 0.0f).Normal;
			}

			var fold = _Path.Fold;
			bool onArc = fold is not null && index >= _Path.ArcFirst && index <= _Path.ArcLast;

			// Straight stretches take the segment's lift line, exactly like the arm chunk's rows do.
			result.Add(new PathPoint(points[index], outward, along[k], side.DeltaAt(along[k]), fold, onArc));
		}

		return result;
	}



	/// <summary>
	/// Sweeps the profile's cross-section along the path, one row of vertices per point, and closes the open ends.
	/// Every face is emitted as two triangles: a verge that rolls with noise is nowhere near planar.
	/// </summary>
	public static void Sweep(PolygonMesh _Mesh, in ProfileShape _Shape, IReadOnlyList<PathPoint> _Path, bool _CapStart, bool _CapEnd, BorderDebug _Debug = null, Color _DebugColor = default, string _DebugName = null)
	{
		var profile = _Shape.Profile;

		if (!profile.HasBorder)
			return;

		var path = Compact(_Path);

		if (path.Count < 2)
			return;

		var faces = BuildFaces(profile);
		int nodeCount = NodeCount(profile);

		var shapes = new Vector2[path.Count][];
		var positions = new Vector3[path.Count][];
		var across = new float[path.Count][];
		var handles = new HalfEdgeMesh.VertexHandle[path.Count][];
		var reliefs = new float[path.Count];
		var spills = new float[path.Count];

		// Gravel spilling over from a country road on the other side of a node is painted onto the pavement as
		// vertex blend weights, which need a material that mixes gravel in by them.
		var blendMaterial = profile.Border == RoadBorderStyle.Sidewalk ? profile.BlendMaterial : null;

		// One winding for the whole run, from which side of its path the border lies on, summed over every step of
		// the path: the first and last points alone say nothing on a run that comes back round on itself (the rim
		// of a parking lot). Deciding it per quad from the quad's own normal goes wrong on thin or folded quads,
		// and a quad wound against its neighbours makes the mesh builder drop whichever face comes second: a hole.
		float winding = 0.0f;

		for (int i = 0; i < path.Count - 1; i++)
			winding += Vector3.Dot(Vector3.Cross(path[i + 1].Position - path[i].Position, path[i].Outward), Vector3.Up);

		bool flip = winding > 0.0f;

		_Debug?.Note($"RUN name=\"{_DebugName}\" rows={path.Count} (given {_Path.Count}) capStart={(_CapStart ? 1 : 0)} capEnd={(_CapEnd ? 1 : 0)} flip={(flip ? 1 : 0)}");

		for (int i = 0; i < path.Count; i++)
		{
			var shape = new Vector2[nodeCount];
			var rows = new Vector3[nodeCount];

			reliefs[i] = EvaluateProfile(_Shape, path[i], shape, out spills[i]);

			// A pavement takes its lift across the width it has at this row, which a taper may have changed: two
			// halves of a corner meeting at a shared width then lift their outer edges alike.
			float liftWidth = profile.Border == RoadBorderStyle.Sidewalk ? ShapeWidth(shape) : profile.BorderWidth;

			for (int k = 0; k < nodeCount; k++)
				rows[k] = NodePosition(path[i], shape[k], liftWidth);

			shapes[i] = shape;
			positions[i] = rows;
		}

		// A border tapered to nothing at its end, in height or in width, has no face to close.
		_CapStart &= reliefs[0] > 0.01f && ShapeWidth(shapes[0]) > 0.5f;
		_CapEnd &= reliefs[^1] > 0.01f && ShapeWidth(shapes[^1]) > 0.5f;

		var stalls = RemoveFolds(path, positions, nodeCount);

		for (int i = 0; i < path.Count; i++)
		{
			var rows = positions[i];
			var distances = new float[nodeCount];

			for (int k = 1; k < nodeCount; k++)
				distances[k] = distances[k - 1] + Vector3.DistanceBetween(rows[k - 1], rows[k]);

			across[i] = distances;
			handles[i] = new HalfEdgeMesh.VertexHandle[nodeCount];

			for (int k = 0; k < nodeCount; k++)
			{
				if (stalls[i][k] >= 0)
					continue;

				// Two nodes of a row standing in the same place are one vertex. It happens wherever a profile's
				// relief has run out — a kerb flattened into a country road's shoulder has its kerb face and its
				// outer face squeezed to nothing, and a border grown from nothing at a gore has the whole section
				// on one point. Two vertices there would leave the faces between them as zero-area triangles,
				// which is degenerate geometry; sharing the vertex makes the face builder drop them instead, and
				// the quad on the other side of the seam comes out as the triangle it should be.
				if (k > 0 && stalls[i][k - 1] < 0 && Vector3.DistanceBetween(rows[k - 1], rows[k]) < JoinDistance)
				{
					handles[i][k] = handles[i][k - 1];
					continue;
				}

				handles[i][k] = _Mesh.AddVertices(rows[k])[0];
			}
		}

		// A stalled node shares the vertex it stalled on, so the faces round it come out as triangles instead of
		// zero-area quads. Backward stalls point at later rows, hence the second pass.
		for (int i = 0; i < path.Count; i++)
		{
			for (int k = 0; k < nodeCount; k++)
			{
				int source = i;

				while (stalls[source][k] >= 0)
					source = stalls[source][k];

				handles[i][k] = handles[source][k];
			}
		}

		if (_Debug is not null)
		{
			for (int i = 0; i < path.Count; i++)
			{
				var point = path[i];
				var rows = positions[i];

				_Debug.Line(rows[0], rows[nodeCount - 1], _DebugColor);

				if (i == 0 || i == path.Count - 1)
					_Debug.Marker(rows[nodeCount - 1], _DebugColor);

				var nodes = new StringBuilder();

				for (int k = 0; k < nodeCount; k++)
					nodes.Append(' ').Append(BorderDebug.Format(rows[k])).Append(point.IsFolded(shapes[i][k].x) ? "F" : "").Append(stalls[i][k] >= 0 ? $"S{stalls[i][k]}" : "");

				_Debug.Note($"R {i} pos={BorderDebug.Format(point.Position)} out={BorderDebug.Format(point.Outward)} along={point.Along:0.###} lift={point.Lift:0.###} arc={(point.IsArc ? 1 : 0)} fold={(point.Fold is not null ? 1 : 0)} relief={reliefs[i]:0.###} spill={spills[i]:0.###} |{nodes}");
			}
		}

		if (_Debug is not null && _CapStart)
			_Debug.Line(positions[0][0], positions[0][nodeCount - 1], BorderDebug.CapColor);

		if (_Debug is not null && _CapEnd)
			_Debug.Line(positions[^1][0], positions[^1][nodeCount - 1], BorderDebug.CapColor);

		for (int i = 0; i < path.Count - 1; i++)
		{
			foreach (var face in faces)
			{
				int a = face.NodeA;
				int b = face.NodeB;

				// A face whose nodes have both collapsed onto the corner centre has nothing left between them;
				// its stretch across the corner is the block corner patch instead.
				if (IsFoldedFace(path[i], shapes[i], a, b) || IsFoldedFace(path[i + 1], shapes[i + 1], a, b))
					continue;

				// Corners of the two rows that sit on one point (the row before a corner's arc reaches the centre
				// the arc rows fold onto; every arc row's outer node is the centre) make the face a triangle. Cut
				// as a quad it would also give a needle-thin sliver with a random normal, which shows through from
				// below and z-fights from above.
				bool outerJoined = positions[i][b].Distance(positions[i + 1][b]) < JoinDistance;
				bool innerJoined = positions[i][a].Distance(positions[i + 1][a]) < JoinDistance;

				if (outerJoined && innerJoined)
					continue;

				Vector2 uv0 = FaceUv(face, path[i], positions[i][a], across[i], a);
				Vector2 uv1 = FaceUv(face, path[i], positions[i][b], across[i], b);
				Vector2 uv2 = FaceUv(face, path[i + 1], positions[i + 1][b], across[i + 1], b);
				Vector2 uv3 = FaceUv(face, path[i + 1], positions[i + 1][a], across[i + 1], a);

				var material = face.Material;
				Color32[] blends = null;

				// Only the faces the gravel reaches take the blend material; the rest of the pavement stays on the
				// plain one, and a weight of nothing on their shared row makes the change invisible.
				if (face.Blendable && blendMaterial is not null && (spills[i] > 0.0f || spills[i + 1] > 0.0f))
				{
					material = blendMaterial;

					// One weight per row, the same right across the pavement, so a face's four corners hold a
					// gradient that runs one way only. Weighting the kerb side differently from the outer one
					// leaves the four corners non-planar, and the two triangles the face is split into then read
					// that gradient differently: the split shows up as a kink in the gravel's front, which is a
					// far louder thing than the slant it was there to give.
					var near = SpillBlend(spills[i]);
					var far = SpillBlend(spills[i + 1]);

					blends = new[] { near, near, far, far };
				}

				if (outerJoined)
				{
					// The folded row's vertex is the exact centre; keep that one.
					bool keepNext = path[i + 1].IsFolded(shapes[i + 1][b].x) || !path[i].IsFolded(shapes[i][b].x);
					var outer = keepNext ? handles[i + 1][b] : handles[i][b];
					Vector2 uvOuter = keepNext ? uv2 : uv1;

					AddTriangle(_Mesh, material, flip, handles[i][a], outer, handles[i + 1][a], uv0, uvOuter, uv3,
						blends is null ? null : new[] { blends[0], keepNext ? blends[2] : blends[1], blends[3] });
				}
				else if (innerJoined)
				{
					AddTriangle(_Mesh, material, flip, handles[i][a], handles[i][b], handles[i + 1][b], uv0, uv1, uv2,
						blends is null ? null : new[] { blends[0], blends[1], blends[2] });
				}
				else
				{
					AddQuad(_Mesh, material, flip,
						handles[i][a], handles[i][b], handles[i + 1][b], handles[i + 1][a],
						uv0, uv1, uv2, uv3, blends);
				}
			}
		}

		AddCornerPatches(_Mesh, faces, path, shapes, positions, across, _Debug);

		var capMaterial = profile.Border == RoadBorderStyle.CountrySide ? profile.ResolvedRoadMaterial : profile.ResolvedBorderMaterial;
		float capRepeat = profile.Border == RoadBorderStyle.CountrySide ? profile.RoadTextureRepeat : profile.BorderTextureRepeat;

		if (_CapStart)
			AddCap(_Mesh, capMaterial, capRepeat, handles[0], positions[0], shapes[0], path[0].Position - path[1].Position);

		if (_CapEnd)
			AddCap(_Mesh, capMaterial, capRepeat, handles[^1], positions[^1], shapes[^1], path[^1].Position - path[^2].Position);
	}



	/// <summary>
	/// How far out from the road edge a cross-section reaches.
	/// </summary>
	private static float ShapeWidth(Vector2[] _Shape)
	{
		float width = 0.0f;

		foreach (var node in _Shape)
			width = MathF.Max(width, node.x);

		return width;
	}



	private static List<PathPoint> Compact(IReadOnlyList<PathPoint> _Path)
	{
		var result = new List<PathPoint>(_Path.Count);

		foreach (var point in _Path)
		{
			if (result.Count > 0 && Vector3.DistanceBetween(result[^1].Position, point.Position) < 0.01f)
				continue;

			result.Add(point);
		}

		return result;
	}



	private static int NodeCount(RoadProfile _Profile)
	{
		return _Profile.Border switch
		{
			RoadBorderStyle.Sidewalk => 4,
			RoadBorderStyle.CountrySide => DropFoot(_Profile) + 1 + _Profile.SlopeSegments,
			_ => 0
		};
	}



	/// <summary>
	/// Whether a country side border has a shoulder of road material before it drops away. A road type that asks
	/// for no width there gets none: the verge starts at the road's edge, rather than a hair out from it behind a
	/// strip too thin to see and too thin to be worth a face.
	/// </summary>
	private static bool HasShoulder(RoadProfile _Profile)
	{
		return _Profile.BorderWidth > 0.0f;
	}



	/// <summary>
	/// The node at the foot of the drop, which the verge's slope runs from: past the road's edge, and past the
	/// shoulder when there is one.
	/// </summary>
	private static int DropFoot(RoadProfile _Profile)
	{
		return HasShoulder(_Profile) ? 2 : 1;
	}



	private static Face[] BuildFaces(RoadProfile _Profile)
	{
		switch (_Profile.Border)
		{
			case RoadBorderStyle.Sidewalk:
				{
					var material = _Profile.ResolvedBorderMaterial;
					float repeat = _Profile.BorderTextureRepeat;

					// Kerb face, pavement, outer face. The pavement's texture keeps its own size across, measured out
					// from the kerb: a narrower pavement shows less of it and a wider one runs on into the next repeat,
					// instead of the one texture being squeezed or pulled to fit. The kerb texture follows the slope of a
					// bevelled kerb instead of stretching.
					float bevelU = new Vector2(_Profile.CurbBevel, _Profile.CurbHeight).Length / repeat;
					float heightU = _Profile.CurbHeight / repeat;

					return new[]
					{
					new Face(0, 1, material, repeat, _U0: bevelU, _U1: 0.0f),
					new Face(1, 2, material, repeat, _AccumulateFrom: 1, _Blendable: true, _AcrossRepeat: _Profile.BorderTextureWidth),
					new Face(2, 3, material, repeat, _U0: 1.0f - heightU, _U1: 1.0f)
				};
				}

			case RoadBorderStyle.CountrySide:
				{
					var roadMaterial = _Profile.ResolvedRoadMaterial;
					var vergeMaterial = _Profile.ResolvedBorderMaterial;
					float roadRepeat = _Profile.RoadTextureRepeat;
					float vergeRepeat = _Profile.BorderTextureRepeat;

					// Shoulder (an extension of the road, so it tiles like one) where the road type has one, the drop
					// (the road's exposed thickness), then the slope down to the terrain.
					int foot = DropFoot(_Profile);
					var faces = new List<Face>();

					if (HasShoulder(_Profile))
						faces.Add(new Face(0, 1, roadMaterial, roadRepeat, _Planar: true));

					faces.Add(new Face(foot - 1, foot, roadMaterial, roadRepeat, _U0: 0.0f, _U1: _Profile.Drop / roadRepeat));

					for (int k = 0; k < _Profile.SlopeSegments; k++)
						faces.Add(new Face(foot + k, foot + 1 + k, vergeMaterial, vergeRepeat, _AccumulateFrom: foot));

					return faces.ToArray();
				}

			default:
				return Array.Empty<Face>();
		}
	}



	/// <summary>
	/// The cross-section at a point as (distance from the road edge, height above the road surface) per node.
	/// Country side borders read noise at the point's position, so the wander is continuous across chunk and
	/// junction seams and differs between the two sides of the road. Near a tapered end the relief (kerb height,
	/// drop, verge depth, roll) is scaled down and the width run towards the neighbour's, so the border comes
	/// down to a flat strip of the neighbour's width at the node. Returns the relief in force at the point, and
	/// how much of a neighbouring country road's gravel has spilled this far (1 at the node, 0 beyond the taper).
	/// </summary>
	private static float EvaluateProfile(in ProfileShape _Shape, in PathPoint _Point, Vector2[] _Nodes, out float _Spill)
	{
		var profile = _Shape.Profile;
		float relief = _Shape.Taper.ReliefAt(_Point.Along, _Shape.Length, out float towards, out float targetWidth, out bool spill);
		bool tapering = towards > 0.0f;

		_Spill = spill ? SpillAt(towards, _Point.Position) : 0.0f;

		switch (profile.Border)
		{
			case RoadBorderStyle.Sidewalk:
				{
					float width = tapering ? MathX.Lerp(profile.BorderWidth, targetWidth, towards) : profile.BorderWidth;
					float height = profile.CurbHeight * relief;

					_Nodes[0] = new Vector2(0.0f, 0.0f);
					_Nodes[1] = new Vector2(MathF.Min(profile.CurbBevel * relief, width), height);
					_Nodes[2] = new Vector2(width, height);
					_Nodes[3] = new Vector2(width, 0.0f);
					break;
				}

			case RoadBorderStyle.CountrySide:
				{
					float scale = profile.ChaosScale;
					// A relief above 1 raises a verge to meet a deeper one; it does not make it any wilder.
					float amplitude = profile.Chaos * MathF.Min(relief, 1.0f);
					float x = _Point.Position.x * scale;
					float y = _Point.Position.y * scale;

					// Noise is 0..1; centre it to roughly plus or minus the chaos amount.
					float meanderShoulder = (Noise.Perlin(x + BandShoulder, y) - 0.5f) * 2.0f * amplitude;
					float meanderOuter = (Noise.Perlin(x + BandOuter, y) - 0.5f) * 2.0f * amplitude;

					// A shoulder that is meant to be there is floored, so a strong inward wander can never fold it back
					// onto the road; a road type that asks for none has none at all, and its verge drops away at the
					// road's own edge.
					bool hasShoulder = HasShoulder(profile);
					float shoulder = hasShoulder ? MathF.Max(1.0f, profile.BorderWidth + meanderShoulder) : 0.0f;
					float span = MathF.Max(1.0f, profile.VergeWidth + meanderOuter);

					// Towards a different road type the whole strip runs to that type's width, keeping its proportions.
					if (tapering)
					{
						float total = shoulder + span;
						float ratio = total > 0.001f ? MathX.Lerp(total, targetWidth, towards) / total : 1.0f;

						shoulder = hasShoulder ? MathF.Max(0.5f, shoulder * ratio) : 0.0f;
						span = MathF.Max(0.5f, span * ratio);
					}

					int foot = DropFoot(profile);

					_Nodes[0] = new Vector2(0.0f, 0.0f);

					if (hasShoulder)
						_Nodes[1] = new Vector2(shoulder, 0.0f);

					_Nodes[foot] = new Vector2(shoulder, -profile.Drop * relief);

					for (int k = 1; k <= profile.SlopeSegments; k++)
					{
						float fraction = (float)k / profile.SlopeSegments;

						// The roll varies along the road and across the slope, so the verge undulates in 3D.
						float roll = (Noise.Fbm(3, x, y, BandRoll + fraction * profile.VergeWidth * scale) - 0.5f) * 2.0f * amplitude;

						_Nodes[foot + k] = new Vector2(shoulder + fraction * span, (-profile.Drop - fraction * profile.VergeDepth) * relief + roll);
					}

					break;
				}
		}

		return relief;
	}



	/// <summary>
	/// How much gravel has spilled to a point a fraction of the way into a taper (1 at the node): the fraction
	/// itself, with its front pushed back and forth by noise read at the point, most in the middle of the run so
	/// the node stays fully covered and the far end clean.
	/// </summary>
	private static float SpillAt(float _Towards, Vector3 _Position)
	{
		float wobble = (Noise.Perlin(_Position.x * SpillNoiseScale + BandSpill, _Position.y * SpillNoiseScale) - 0.5f) * 2.0f * SpillWobble;

		return Math.Clamp(_Towards + wobble * 4.0f * _Towards * (1.0f - _Towards), 0.0f, 1.0f);
	}



	/// <summary>
	/// A spill amount as the vertex blend a blendable material reads: its red channel is how much of the second
	/// layer (the gravel) shows.
	/// </summary>
	private static Color32 SpillBlend(float _Spill)
	{
		byte weight = (byte)Math.Clamp(MathF.Round(_Spill * 255.0f), 0.0f, 255.0f);

		return new Color32(weight, 0, 0, 255);
	}



	private static Vector2 FaceUv(in Face _Face, in PathPoint _Point, Vector3 _Position, float[] _Across, int _Node)
	{
		if (_Face.Planar)
			return new Vector2(_Position.x, _Position.y) / _Face.Repeat;

		float across = _Face.AcrossRepeat > 0.0f ? _Face.AcrossRepeat : _Face.Repeat;
		float u = _Face.AccumulateFrom >= 0
			? (_Across[_Node] - _Across[_Face.AccumulateFrom]) / across
			: (_Node == _Face.NodeA ? _Face.U0 : _Face.U1);

		return new Vector2(u, _Point.Along / _Face.Repeat);
	}



	/// <summary>
	/// On the inside of a bend tighter than the road is wide plus the border, the border's outer edge runs
	/// backwards: a row's outer node lands behind the previous row's and the face between them folds over itself.
	/// The offset curve has a loop there, and the fix is the one for offset curves: cut the loop out. A node whose
	/// face would fold instead stays on the previous row's node (it shares that vertex), so the outer edge runs
	/// straight from the last good node to the next one. The first and last rows are never moved: they are the
	/// rows a run shares with its neighbouring piece, and the seam stays exact; a fold against the last row is
	/// taken out by stalling the rows before it onto it instead. Arc rows are left to the corner fold. Returns,
	/// per row and node, the row whose node it stalled onto (-1 when it kept its own).
	/// </summary>
	internal static int[][] RemoveFolds(List<PathPoint> _Path, Vector3[][] _Positions, int _NodeCount)
	{
		int count = _Path.Count;
		var stalls = new int[count][];

		for (int i = 0; i < count; i++)
		{
			stalls[i] = new int[_NodeCount];
			Array.Fill(stalls[i], -1);
		}

		if (count < 3)
			return stalls;

		for (int k = 1; k < _NodeCount; k++)
		{
			// Forward: each row against the last row that kept its own node.
			for (int i = 1; i < count - 1; i++)
			{
				if (_Path[i].IsArc || _Path[i - 1].IsArc)
					continue;

				if (FaceIsSimple(_Positions[i - 1][0], _Positions[i][0], _Positions[i][k], _Positions[i - 1][k]))
					continue;

				_Positions[i][k] = _Positions[i - 1][k];
				stalls[i][k] = i - 1;
			}

			// Backward: the last row cannot move, so rows folding against it move onto it.
			for (int i = count - 2; i >= 1; i--)
			{
				if (_Path[i].IsArc || _Path[i + 1].IsArc)
					continue;

				if (FaceIsSimple(_Positions[i][0], _Positions[i + 1][0], _Positions[i + 1][k], _Positions[i][k]))
					break;

				_Positions[i][k] = _Positions[i + 1][k];
				stalls[i][k] = i + 1;
			}
		}

		return stalls;
	}



	/// <summary>
	/// True when the face with these corners (kerb, next kerb, next node, node) does not cross itself in plan.
	/// </summary>
	private static bool FaceIsSimple(Vector3 _A, Vector3 _B, Vector3 _C, Vector3 _D)
	{
		return !SegmentsCross(Flat(_A), Flat(_B), Flat(_C), Flat(_D)) && !SegmentsCross(Flat(_B), Flat(_C), Flat(_D), Flat(_A));
	}



	/// <summary>
	/// Proper crossing of the segments p-q and r-s; touching at an end does not count.
	/// </summary>
	private static bool SegmentsCross(Vector2 _P, Vector2 _Q, Vector2 _R, Vector2 _S)
	{
		Vector2 pq = _Q - _P;
		Vector2 rs = _S - _R;
		float denominator = pq.x * rs.y - pq.y * rs.x;

		if (MathF.Abs(denominator) < 0.000001f)
			return false;

		Vector2 pr = _R - _P;
		float t = (pr.x * rs.y - pr.y * rs.x) / denominator;
		float u = (pr.x * pq.y - pr.y * pq.x) / denominator;

		return t > 0.001f && t < 0.999f && u > 0.001f && u < 0.999f;
	}



	/// <summary>
	/// Where a profile node lands for a path point. The lift is taken in full by the outer edge of the border and
	/// not at all by the kerb, with the nodes in between sharing it by their offset, so a lifted row tilts across
	/// its width. On a corner's arc the lift is whatever brings the row to the corner's one height, and a node
	/// further out than the fillet radius would pass through the centre and come back folded, so it stops at the
	/// centre instead: the rows of the arc make a pie, and the block corner beyond the centre is patched in
	/// separately.
	/// </summary>
	private static Vector3 NodePosition(in PathPoint _Point, Vector2 _Node, float _BorderWidth)
	{
		if (_Point.IsFolded(_Node.x))
			return new Vector3(_Point.Fold.Centre.x, _Point.Fold.Centre.y, _Point.Fold.ApexHeight + _Node.y);

		float weight = MathF.Min(_Node.x / MathF.Max(_BorderWidth, 1.0f), 1.0f);
		float lift = _Point.IsArc && _Point.Fold is { } fold ? fold.ApexHeight - _Point.Position.z : _Point.Lift;
		Vector3 flat = _Point.Position + _Point.Outward * _Node.x;

		return new Vector3(flat.x, flat.y, _Point.Position.z + weight * lift + _Node.y);
	}



	private static bool IsFoldedFace(in PathPoint _Point, Vector2[] _Shape, int _NodeA, int _NodeB)
	{
		return _Point.IsFolded(_Shape[_NodeA].x) && _Point.IsFolded(_Shape[_NodeB].x);
	}



	/// <summary>
	/// The block corner: what lies beyond the corner centre, out to where the two straight outer edges of the
	/// border meet. For a face whose outer node collapsed onto the centre it is the triangle from the centre to
	/// that node on the last full row and to the mitre point; for a face whose nodes both collapsed it is the
	/// strip between their two mitre points. Both halves of a corner contribute their triangle, together making
	/// the square. Fresh vertices: these faces share no edges with the sweep.
	/// </summary>
	private static void AddCornerPatches(PolygonMesh _Mesh, Face[] _Faces, List<PathPoint> _Path, Vector2[][] _Shapes, Vector3[][] _Positions, float[][] _Across, BorderDebug _Debug)
	{
		int arcStart = -1;
		int arcEnd = -1;

		for (int i = 0; i < _Path.Count; i++)
		{
			if (!_Path[i].IsArc)
				continue;

			if (arcStart < 0)
				arcStart = i;

			arcEnd = i;
		}

		if (arcStart < 0 || _Path[arcStart].Fold is not { } fold)
			return;

		// The last full row before the arc (first half of a corner) or the first after it (second half).
		int t = arcStart > 0 ? arcStart - 1 : (arcEnd < _Path.Count - 1 ? arcEnd + 1 : -1);

		if (t < 0)
			return;

		var row = _Path[t];
		var shape = _Shapes[t];
		var bisector = new Vector3(fold.Bisector.x, fold.Bisector.y, 0.0f);

		// Which way the along-the-road coordinate runs towards the corner, for the faces that keep it.
		float towardsCorner = MathF.Sign(_Path[arcStart].Along - row.Along);

		_Debug?.Note($"PATCHES arcStart={arcStart} arcEnd={arcEnd} row={t} apex={BorderDebug.Format(fold.Apex)} centre={BorderDebug.Format(fold.Centre)} bisector={BorderDebug.Format(fold.Bisector)} halfSin={fold.HalfAngleSin:0.####} radius={fold.Radius:0.###} apexHeight={fold.ApexHeight:0.###}");

		foreach (var face in _Faces)
		{
			int a = face.NodeA;
			int b = face.NodeB;

			// Nothing collapsed: the sweep covered this face all the way round.
			if (shape[b].x < fold.Radius)
				continue;

			_Debug?.Note($"PATCH face={a}-{b} offsetA={shape[a].x:0.###} offsetB={shape[b].x:0.###} mitreA={BorderDebug.Format(fold.MitrePoint(shape[a].x))} mitreB={BorderDebug.Format(fold.MitrePoint(shape[b].x))} rowA={BorderDebug.Format(_Positions[t][a])} rowB={BorderDebug.Format(_Positions[t][b])}");

			Vector2 edge = shape[b] - shape[a];
			Vector3 expected = bisector * -edge.y + Vector3.Up * edge.x;

			// The profile's texture runs across the border and along the road; a block corner keeps the row's
			// across coordinate and carries the along coordinate on to the mitre.
			Vector2 mitreB = fold.MitrePoint(shape[b].x);
			var pointB = _Positions[t][b];
			var cornerB = new Vector3(mitreB.x, mitreB.y, fold.ApexHeight + shape[b].y);
			Vector2 uvB = FaceUv(face, row, pointB, _Across[t], b);
			Vector2 uvCornerB = face.Planar ? PlanarUv(cornerB, face.Repeat) : uvB + new Vector2(0.0f, towardsCorner * Flat(pointB).Distance(Flat(cornerB)) / face.Repeat);

			if (shape[a].x < fold.Radius)
			{
				var centre = new Vector3(fold.Centre.x, fold.Centre.y, fold.ApexHeight + shape[b].y);

				// A border exactly as wide as the fillet: the block corner is a point.
				if (Flat(pointB).Distance(Flat(centre)) < 0.5f || Flat(cornerB).Distance(Flat(centre)) < 0.5f)
					continue;

				Vector2 uvCentre = face.Planar ? PlanarUv(centre, face.Repeat) : uvB;

				AddPatchTriangle(_Mesh, face.Material, expected, centre, pointB, cornerB, uvCentre, uvB, uvCornerB);

				if (_Debug is not null)
				{
					_Debug.Line(centre, pointB, BorderDebug.PatchColor);
					_Debug.Line(pointB, cornerB, BorderDebug.PatchColor);
					_Debug.Line(cornerB, centre, BorderDebug.PatchColor);
				}

				continue;
			}

			Vector2 mitreA = fold.MitrePoint(shape[a].x);
			var pointA = _Positions[t][a];
			var cornerA = new Vector3(mitreA.x, mitreA.y, fold.ApexHeight + shape[a].y);

			if (Flat(pointA).Distance(Flat(cornerA)) < 0.5f && Flat(pointB).Distance(Flat(cornerB)) < 0.5f)
				continue;

			Vector2 uvA = FaceUv(face, row, pointA, _Across[t], a);
			Vector2 uvCornerA = face.Planar ? PlanarUv(cornerA, face.Repeat) : uvA + new Vector2(0.0f, towardsCorner * Flat(pointA).Distance(Flat(cornerA)) / face.Repeat);

			AddPatchQuad(_Mesh, face.Material, expected, pointA, cornerA, cornerB, pointB, uvA, uvCornerA, uvCornerB, uvB);

			if (_Debug is not null)
			{
				_Debug.Line(pointA, cornerA, BorderDebug.PatchColor);
				_Debug.Line(cornerA, cornerB, BorderDebug.PatchColor);
				_Debug.Line(cornerB, pointB, BorderDebug.PatchColor);
			}
		}
	}



	private static void AddPatchTriangle(PolygonMesh _Mesh, Material _Material, Vector3 _Expected, Vector3 _A, Vector3 _B, Vector3 _C, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC)
	{
		var handles = _Mesh.AddVertices(_A, _B, _C);

		if (Vector3.Dot(Vector3.Cross(_B - _A, _C - _A), _Expected) >= 0.0f)
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[1], handles[2], _UvA, _UvB, _UvC);
		else
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[2], handles[1], _UvA, _UvC, _UvB);
	}



	private static void AddPatchQuad(PolygonMesh _Mesh, Material _Material, Vector3 _Expected, Vector3 _A, Vector3 _B, Vector3 _C, Vector3 _D, Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD)
	{
		var handles = _Mesh.AddVertices(_A, _B, _C, _D);
		Vector3 normal = Vector3.Cross(_B - _A, _C - _A);

		if (normal.IsNearZeroLength)
			normal = Vector3.Cross(_C - _A, _D - _A);

		// One decision for both triangles: they share an edge and must agree.
		if (Vector3.Dot(normal, _Expected) >= 0.0f)
		{
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[1], handles[2], _UvA, _UvB, _UvC);
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[2], handles[3], _UvA, _UvC, _UvD);
		}
		else
		{
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[2], handles[1], _UvA, _UvC, _UvB);
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, handles[0], handles[3], handles[2], _UvA, _UvD, _UvC);
		}
	}



	private static Vector2 PlanarUv(Vector3 _Position, float _Repeat)
	{
		return new Vector2(_Position.x, _Position.y) / _Repeat;
	}



	private static Vector2 Flat(Vector3 _Vector)
	{
		return new Vector2(_Vector.x, _Vector.y);
	}



	/// <summary>
	/// One triangle in the run's winding: the corners in the same order a quad's would be.
	/// </summary>
	/// <summary>
	/// A triangle in the run's winding, with blend weights per corner (in the given, unflipped order) when the
	/// face is painted.
	/// </summary>
	private static void AddTriangle(PolygonMesh _Mesh, Material _Material, bool _Flip,
		HalfEdgeMesh.VertexHandle _HA, HalfEdgeMesh.VertexHandle _HB, HalfEdgeMesh.VertexHandle _HC,
		Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Color32[] _Blends = null)
	{
		HalfEdgeMesh.FaceHandle face;

		if (!_Flip)
		{
			face = MeshUtility.AddTexturedTriangle(_Mesh, _Material, _HA, _HB, _HC, _UvA, _UvB, _UvC);

			if (_Blends is not null)
				MeshUtility.SetFaceBlends(_Mesh, face, _Blends);
		}
		else
		{
			face = MeshUtility.AddTexturedTriangle(_Mesh, _Material, _HA, _HC, _HB, _UvA, _UvC, _UvB);

			if (_Blends is not null)
				MeshUtility.SetFaceBlends(_Mesh, face, new[] { _Blends[0], _Blends[2], _Blends[1] });
		}
	}



	/// <summary>
	/// A quad as two triangles, in the run's winding.
	/// </summary>
	private static void AddQuad(PolygonMesh _Mesh, Material _Material, bool _Flip,
		HalfEdgeMesh.VertexHandle _HA, HalfEdgeMesh.VertexHandle _HB, HalfEdgeMesh.VertexHandle _HC, HalfEdgeMesh.VertexHandle _HD,
		Vector2 _UvA, Vector2 _UvB, Vector2 _UvC, Vector2 _UvD, Color32[] _Blends = null)
	{
		AddTriangle(_Mesh, _Material, _Flip, _HA, _HB, _HC, _UvA, _UvB, _UvC, _Blends is null ? null : new[] { _Blends[0], _Blends[1], _Blends[2] });
		AddTriangle(_Mesh, _Material, _Flip, _HA, _HC, _HD, _UvA, _UvC, _UvD, _Blends is null ? null : new[] { _Blends[0], _Blends[2], _Blends[3] });
	}



	/// <summary>
	/// Closes a run's end with the cross-section itself, fanned from the road edge and facing along the road.
	/// </summary>
	private static void AddCap(PolygonMesh _Mesh, Material _Material, float _Repeat, HalfEdgeMesh.VertexHandle[] _Handles, Vector3[] _Positions, Vector2[] _Shape, Vector3 _Facing)
	{
		for (int k = 1; k < _Positions.Length - 1; k++)
		{
			Vector3 normal = Vector3.Cross(_Positions[k] - _Positions[0], _Positions[k + 1] - _Positions[0]);
			Vector2 uv0 = _Shape[0] / _Repeat;
			Vector2 uv1 = _Shape[k] / _Repeat;
			Vector2 uv2 = _Shape[k + 1] / _Repeat;

			if (Vector3.Dot(normal, _Facing) >= 0.0f)
				MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Handles[0], _Handles[k], _Handles[k + 1], uv0, uv1, uv2);
			else
				MeshUtility.AddTexturedTriangle(_Mesh, _Material, _Handles[0], _Handles[k + 1], _Handles[k], uv0, uv2, uv1);
		}
	}
}