Code/Network/RoadJunctionBuilder.cs

Static class that generates 3D meshes for road junctions. It calculates the geometry and applies textures for the road surface, managing road arms and skirts for continuous transitions.

using System;
using System.Collections.Generic;
using Sandbox;

namespace RedSnail.RoadTool2;

/// <summary>
/// Generates the surface of a junction: the layout's polygon fanned out from the node. Each triangle of the fan is
/// planar, so none can be dropped by the mesh builder whatever the arms' heights do.
/// </summary>
internal static class RoadJunctionBuilder
{
	/// <summary>
	/// How far below its foot a skirt's closed-off end reaches: well under any ground dipping beside it.
	/// </summary>
	private const float SkirtCloseDepth = 20.0f;

	/// <summary>
	/// Each arm's texture runs on into the junction over the arm's own stretch of the outline (see
	/// <see cref="RoadJunctionLayout.PolygonArms"/>) and meets the next arm's on a line from the node to the middle
	/// of the corner between them, where the borders meet too (or, for a corner on one of the rows, to where the outline
	/// comes closest to the node). A road texture with something across its width (lanes,
	/// worn wheel tracks, dark edges) turns the corner with the road instead of being cut by a projection laid over
	/// it, and nothing is stretched; a road that curves on its way in has its texture bend with it (see
	/// <see cref="RoadFanBuilder"/>). An arm with a different road surface from the junction's still has the
	/// junction's surface turn into it the same way (carried on across it instead, a texture fitted to the main road's
	/// width would run off its edges and start again); a road that blends the junction's surface into its own (see
	/// <see cref="RoadProfile.RoadBlendMaterial"/>) starts doing so across its mouth (see <see cref="RoadMouthBlend"/>),
	/// where its share of the corners starts, and carries the blend on from its row. An arm whose road has banks has its
	/// road's surface carried on down under the ground past its half of each corner, as its road's own is past its edges.
	/// <paramref name="_ProfileOf"/> gives each arm's road type, and <paramref name="_BlendOf"/> its road's blend.
	/// </summary>
	public static PolygonMesh Build(RoadJunctionLayout _Layout, RoadJunctionLayout.Arm _Main, Func<RoadJunctionLayout.Arm, bool> _SameSurface, float _TextureRepeat, bool _FitWidth, Material _Material,
		Func<RoadJunctionLayout.Arm, RoadProfile> _ProfileOf = null, Func<RoadJunctionLayout.Arm, RoadSurfaceBlend> _BlendOf = null)
	{
		var mesh = new PolygonMesh();
		var polygon = _Layout.Polygon;
		var owners = _Layout.PolygonArms;

		if (polygon.Count < 3)
			return mesh;

		int mainIndex = _Main is null ? -1 : _Layout.Arms.IndexOf(_Main);

		if (owners is null || owners.Count != polygon.Count || mainIndex < 0)
			return Build(_Layout, _TextureRepeat, _Material);

		var armMappings = new RoadArmMapping[_Layout.Arms.Count];
		var mappings = new IFanMapping[_Layout.Arms.Count];
		var own = new bool[_Layout.Arms.Count];
		bool allSame = true;

		for (int i = 0; i < mappings.Length; i++)
		{
			var arm = _Layout.Arms[i];

			armMappings[i] = Mapping(arm, _TextureRepeat, _FitWidth);
			mappings[i] = armMappings[i];
			own[i] = true;
			allSame &= i == mainIndex || _SameSurface(arm);
		}

		// A road splitting in two round a nose far out from the node (a highway's exit or entry, a fork) is laid out
		// as roads of its own instead (see RoadSplitBuilder), when its texture's edges are what has to wrap the nose.
		if (_FitWidth && allSame && RoadSplitBuilder.TryBuild(_Layout, armMappings, _Material, out var split))
		{
			AddSkirts(split, _Layout, mappings, _ProfileOf, _Material);

			return split;
		}

		var points = new List<Vector3>();
		var arms = new List<int>();
		var blends = new IFanBlend[_Layout.Arms.Count];

		Outline(_Layout, mainIndex, points, arms);

		if (_ProfileOf is not null && _BlendOf is not null)
		{
			for (int i = 0; i < blends.Length; i++)
				blends[i] = i == mainIndex ? null : MouthBlend(_Layout, points, arms, i, _ProfileOf, _BlendOf);
		}

		RoadFanBuilder.Build(mesh, _Material, _Layout.Centre, points, arms, mappings, own, null, true, blends);
		AddSkirts(mesh, _Layout, mappings, _ProfileOf, _Material);

		return mesh;
	}



	/// <summary>
	/// The outline the surface is fanned out to, with an edge cut straight across from one arm's row to the next arm's
	/// halved, and the arm each edge belongs to: each takes the half on its side.
	/// </summary>
	private static void Outline(RoadJunctionLayout _Layout, int _MainIndex, List<Vector3> _Points, List<int> _Arms)
	{
		var polygon = _Layout.Polygon;
		var owners = _Layout.PolygonArms;

		for (int i = 0; i < polygon.Count; i++)
		{
			int next = (i + 1) % polygon.Count;

			_Points.Add(polygon[i]);

			if (owners[i] >= 0)
			{
				_Arms.Add(owners[i]);
				continue;
			}

			int before = OwnerNear(owners, i, -1);
			int after = OwnerNear(owners, next, 1);

			_Arms.Add(before >= 0 ? before : _MainIndex);

			if (before >= 0 && after >= 0)
			{
				_Points.Add((polygon[i] + polygon[next]) * 0.5f);
				_Arms.Add(after);
			}
		}
	}



	/// <summary>
	/// Where a road blending the junction's surface into its own starts its blend across its mouth (see
	/// <see cref="RoadMouthBlend"/>): the line from the middle of the corner on one side of the arm to the middle of the
	/// corner on the other, where the arm's share of the outline starts and ends, and its skirt and banks with it. In the
	/// arm's own terms: how far from the node the line crosses the road's centreline (never past the row), and how much
	/// further from the node it lies for each unit out to the road's right. False where the arm has no share of the
	/// outline, or the whole of it.
	/// </summary>
	internal static bool Mouth(RoadJunctionLayout _Layout, RoadJunctionLayout.Arm _Arm, RoadJunctionLayout.Arm _Main, out float _Centre, out float _Slope)
	{
		_Centre = 0.0f;
		_Slope = 0.0f;

		int mainIndex = _Main is null ? -1 : _Layout.Arms.IndexOf(_Main);

		if (_Layout.Polygon.Count < 3 || _Layout.PolygonArms is null || _Layout.PolygonArms.Count != _Layout.Polygon.Count || mainIndex < 0)
			return false;

		var points = new List<Vector3>();
		var arms = new List<int>();

		Outline(_Layout, mainIndex, points, arms);

		return Mouth(_Layout, points, arms, _Layout.Arms.IndexOf(_Arm), out _Centre, out _Slope);
	}



	private static bool Mouth(RoadJunctionLayout _Layout, List<Vector3> _Points, List<int> _Arms, int _Index, out float _Centre, out float _Slope)
	{
		_Centre = 0.0f;
		_Slope = 0.0f;

		int count = _Points.Count;
		int first = -1;

		if (_Index < 0)
			return false;

		// The arm's stretch, from the edge it takes over at to the last before it hands over.
		for (int k = 0; k < count; k++)
		{
			if (_Arms[k] == _Index && _Arms[(k + count - 1) % count] != _Index)
			{
				first = k;
				break;
			}
		}

		if (first < 0)
			return false;

		int last = first;

		while (_Arms[(last + 1) % count] == _Index)
			last = (last + 1) % count;

		var arm = _Layout.Arms[_Index];
		var mapping = Mapping(arm, 1.0f, false);
		Vector2 from = mapping.Measure(_Points[first]);
		Vector2 to = mapping.Measure(_Points[(last + 1) % count]);
		float fromNode = arm.AtStart ? from.y : arm.Length - from.y;
		float toNode = arm.AtStart ? to.y : arm.Length - to.y;
		float across = to.x - from.x;

		_Slope = MathF.Abs(across) > 1.0f ? (toNode - fromNode) / across : 0.0f;
		_Centre = Math.Clamp((fromNode + toNode) * 0.5f - _Slope * (from.x + to.x) * 0.5f, 0.0f, arm.Trim);

		return true;
	}



	/// <summary>
	/// The blend across an arm's mouth, or null where its road does not blend the junction's surface into its own there.
	/// It starts where the road's own blend says, which is where the mouth's line crosses the road's centreline.
	/// </summary>
	private static IFanBlend MouthBlend(RoadJunctionLayout _Layout, List<Vector3> _Points, List<int> _Arms, int _Index,
		Func<RoadJunctionLayout.Arm, RoadProfile> _ProfileOf, Func<RoadJunctionLayout.Arm, RoadSurfaceBlend> _BlendOf)
	{
		var arm = _Layout.Arms[_Index];
		var profile = _ProfileOf(arm);

		if (profile?.RoadBlendMaterial is null || arm.Segment.Bridge is not null)
			return null;

		var blend = _BlendOf(arm);

		if (blend.IsNone || !(arm.AtStart ? blend.AtStart : blend.AtEnd) || !Mouth(_Layout, _Points, _Arms, _Index, out _, out float slope))
			return null;

		float centre = arm.AtStart ? blend.StartAt : arm.Length - blend.EndAt;

		return new RoadMouthBlend(arm, profile, blend, arm.AtStart ? blend.StartSource : blend.EndSource, centre, slope);
	}



	/// <summary>
	/// Under banks (see <see cref="RoadProfile.SkirtWidth"/>), each stretch of the outline an arm with a skirt owns (its
	/// half of the corner either side of it) gets a skirt carried on out past it and down, so the ground coming over the
	/// kerb never shows a gap beneath it: the arm's road's own skirt carried on round the corner, in that road's surface
	/// and mapping, so whatever of it the ground leaves showing matches the road's. Its end on the arm's row heads out
	/// along the row, where the road's skirt carries on from it; where it meets the next arm's half of the corner
	/// instead it is closed off, down past its foot, so nothing can be seen under it from that side. A strip of its
	/// own, so the mesh never has to join it to the fan's rim.
	/// </summary>
	private static void AddSkirts(PolygonMesh _Mesh, RoadJunctionLayout _Layout, IReadOnlyList<IFanMapping> _Mappings, Func<RoadJunctionLayout.Arm, RoadProfile> _ProfileOf, Material _Material)
	{
		if (_ProfileOf is null)
			return;

		var polygon = _Layout.Polygon;
		var owners = _Layout.PolygonArms;
		int count = polygon.Count;
		var rows = new HashSet<int>(_Layout.RowStarts);
		var profiles = new RoadProfile[_Layout.Arms.Count];

		// A bridge's deck has no skirt, so nor does its share of the corners.
		for (int i = 0; i < profiles.Length; i++)
			profiles[i] = _Layout.Arms[i].Segment.Bridge is null ? _ProfileOf(_Layout.Arms[i]) : null;

		for (int first = 0; first < count; first++)
		{
			int owner = owners[first];

			// A stretch starts where an arm's half of a corner does.
			if (!Skirted(first) || (Skirted(Previous(first)) && owners[Previous(first)] == owner))
				continue;

			var profile = profiles[owner];
			var arm = _Layout.Arms[owner];
			var mapping = new RoadArmMapping(arm.Table, arm.AtStart, arm.Trim, arm.Widths, profile.RoadTextureRepeat, profile.RoadTextureFitsWidth, true);
			var material = profile.ResolvedRoadMaterial;
			bool fit = profile.RoadTextureFitsWidth;
			var vertices = new List<Vector3> { polygon[first] };
			int last = first;

			while (Next(last) != first && Skirted(Next(last)) && owners[Next(last)] == owner)
				last = Next(last);

			for (int e = first; ; e = Next(e))
			{
				vertices.Add(polygon[Next(e)]);

				if (e == last)
					break;
			}

			// Out from each point square to the outline there; along the row at a row's end, as the road's skirt goes.
			var outwards = new Vector3[vertices.Count];

			for (int j = 0; j < vertices.Count; j++)
			{
				int before = j == 0 ? Previous(first) : -1;
				int after = j == vertices.Count - 1 ? Next(last) : -1;

				if (before >= 0 && rows.Contains(before))
					outwards[j] = Flat(polygon[first] - polygon[before]).Normal;
				else if (after >= 0 && rows.Contains(after))
					outwards[j] = Flat(polygon[after] - polygon[Next(after)]).Normal;
				else
					outwards[j] = (EdgeOutward(j == 0 ? polygon[before] : vertices[j - 1], vertices[j]) + EdgeOutward(vertices[j], j == vertices.Count - 1 ? polygon[Next(after)] : vertices[j + 1])).Normal;
			}

			var top = new HalfEdgeMesh.VertexHandle[vertices.Count];
			var bottom = new HalfEdgeMesh.VertexHandle[vertices.Count];
			var bottoms = new Vector3[vertices.Count];

			for (int j = 0; j < vertices.Count; j++)
			{
				bottoms[j] = vertices[j] + outwards[j] * profile.SkirtWidth - Vector3.Up * profile.SkirtDepth;
				top[j] = _Mesh.AddVertices(vertices[j])[0];
				bottom[j] = _Mesh.AddVertices(bottoms[j])[0];
			}

			// The outline runs counter-clockwise seen from above, the junction on its left: wound so the strip faces up.
			for (int j = 0; j < vertices.Count - 1; j++)
			{
				Vector2 topA = SkirtUv(mapping, vertices[j], fit);
				Vector2 topB = SkirtUv(mapping, vertices[j + 1], fit);
				Vector2 bottomA = SkirtUv(mapping, bottoms[j], fit);
				Vector2 bottomB = SkirtUv(mapping, bottoms[j + 1], fit);

				MeshUtility.AddTexturedTriangle(_Mesh, material, top[j], bottom[j], bottom[j + 1], topA, bottomA, bottomB);
				MeshUtility.AddTexturedTriangle(_Mesh, material, top[j], bottom[j + 1], top[j + 1], topA, bottomB, topB);
			}

			// Closed off where it stops beside another arm's half of the corner.
			if (!rows.Contains(Previous(first)))
				CloseSkirt(_Mesh, material, vertices[0], bottoms[0], vertices[1] - vertices[0], profile, mapping, fit);

			if (!rows.Contains(Next(last)))
				CloseSkirt(_Mesh, material, vertices[^1], bottoms[^1], vertices[^2] - vertices[^1], profile, mapping, fit);
		}

		bool Skirted(int _Edge) => owners[_Edge] >= 0 && !rows.Contains(_Edge) && profiles[owners[_Edge]] is { HasSkirt: true };
		int Previous(int _Edge) => (_Edge + count - 1) % count;
		int Next(int _Edge) => (_Edge + 1) % count;
	}



	/// <summary>
	/// A point of a skirt in its road's own mapping: carried on out past the edge at the texture's own size, a texture
	/// fitted to the width turned back on itself there (see <see cref="RoadSurfaceBuilder.SkirtShare"/>).
	/// </summary>
	private static Vector2 SkirtUv(RoadArmMapping _Mapping, Vector3 _Point, bool _FitWidth)
	{
		Vector2 uv = _Mapping.Uv(_Point);

		return _FitWidth ? new Vector2(RoadSurfaceBuilder.SkirtShare(uv.x), uv.y) : uv;
	}



	/// <summary>
	/// Closes off a skirt where it stops beside another arm's half of a corner: a face straight down from its end (its
	/// top on the kerb, its bottom out at its foot) to well under its foot, facing away from it (<paramref name="_Into"/>
	/// runs back along it), so the ground dipping beside it never shows a way in under it.
	/// </summary>
	private static void CloseSkirt(PolygonMesh _Mesh, Material _Material, Vector3 _Top, Vector3 _Bottom, Vector3 _Into, RoadProfile _Profile, RoadArmMapping _Mapping, bool _FitWidth)
	{
		float repeat = MathF.Max(1.0f, _Profile.RoadTextureRepeat);
		float topDrop = _Profile.SkirtDepth + SkirtCloseDepth;
		Vector3 topFoot = _Top - Vector3.Up * topDrop;
		Vector3 bottomFoot = _Bottom - Vector3.Up * SkirtCloseDepth;
		Vector2 topUv = SkirtUv(_Mapping, _Top, _FitWidth);
		Vector2 bottomUv = SkirtUv(_Mapping, _Bottom, _FitWidth);
		Vector2 topFootUv = topUv + new Vector2(0.0f, topDrop / repeat);
		Vector2 bottomFootUv = bottomUv + new Vector2(0.0f, SkirtCloseDepth / repeat);

		var top = _Mesh.AddVertices(_Top)[0];
		var bottom = _Mesh.AddVertices(_Bottom)[0];
		var below = _Mesh.AddVertices(bottomFoot)[0];
		var under = _Mesh.AddVertices(topFoot)[0];

		// Wound to face away from the skirt it closes.
		if (Vector3.Dot(Vector3.Cross(_Bottom - _Top, bottomFoot - _Top), _Into) > 0.0f)
		{
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, top, below, bottom, topUv, bottomFootUv, bottomUv);
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, top, under, below, topUv, topFootUv, bottomFootUv);
		}
		else
		{
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, top, bottom, below, topUv, bottomUv, bottomFootUv);
			MeshUtility.AddTexturedTriangle(_Mesh, _Material, top, below, under, topUv, bottomFootUv, topFootUv);
		}
	}



	/// <summary>
	/// Out of the junction, flat, from the edge of its outline between two points (which runs counter-clockwise).
	/// </summary>
	private static Vector3 EdgeOutward(Vector3 _From, Vector3 _To)
	{
		Vector3 along = Flat(_To - _From).Normal;

		return new Vector3(along.y, -along.x, 0.0f);
	}



	private static Vector3 Flat(Vector3 _Vector)
	{
		return _Vector.WithZ(0.0f);
	}



	/// <summary>
	/// The plain top-down projection, for an outline whose arms were never noted (built before they were).
	/// </summary>
	public static PolygonMesh Build(RoadJunctionLayout _Layout, float _TextureRepeat, Material _Material)
	{
		var mesh = new PolygonMesh();
		var polygon = _Layout.Polygon;

		if (polygon.Count < 3)
			return mesh;

		var centre = mesh.AddVertices(_Layout.Centre)[0];
		var ring = mesh.AddVertices(polygon.ToArray());
		Vector2 centreUv = PlanarUv(_Layout.Centre, _TextureRepeat);

		for (int i = 0; i < polygon.Count; i++)
		{
			int next = (i + 1) % polygon.Count;

			MeshUtility.AddTexturedTriangle(mesh, _Material, centre, ring[i], ring[next],
				centreUv, PlanarUv(polygon[i], _TextureRepeat), PlanarUv(polygon[next], _TextureRepeat));
		}

		return mesh;
	}



	/// <summary>
	/// An arm's mapping, with the node held inside the arm's texture: a lane that sits off to one side of the road's
	/// centreline (a ramp starting from the side of a highway) can end beside the node rather than across it, and its
	/// texture's edges would otherwise be drawn out across the junction from the node.
	/// </summary>
	private static RoadArmMapping Mapping(RoadJunctionLayout.Arm _Arm, float _TextureRepeat, bool _FitWidth)
	{
		return new RoadArmMapping(_Arm.Table, _Arm.AtStart, _Arm.Trim, _Arm.Widths, _TextureRepeat, _FitWidth, true);
	}



	/// <summary>
	/// The arm owning the nearest edge that has one, walking round the outline one way from an edge.
	/// </summary>
	private static int OwnerNear(IReadOnlyList<int> _Owners, int _From, int _Step)
	{
		for (int k = 0; k < _Owners.Count; k++)
		{
			int owner = _Owners[((_From + _Step * k) % _Owners.Count + _Owners.Count) % _Owners.Count];

			if (owner >= 0)
				return owner;
		}

		return -1;
	}



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