Static builder that makes the mesh for one kind of road junction: a three-arm split where one road forks in two around a nose. It fans the trunk's share from the node, lays each branch as rows running from its outer kerb to a spine through the apex and down the gore to the tip, checks the triangles cover the outline within a small tolerance, and returns false so the caller falls back to a plain fan when anything does not lay out cleanly.
using System;
using System.Collections.Generic;
using Sandbox;
namespace RedSnail.RoadTool2;
/// <summary>
/// The surface of a junction where one road splits in two around a nose (a highway exit or entry, a fork), for a road
/// texture fitted to the road's width. Fanned out from the node like any junction, such a junction reaches its nose
/// thousands of units out in slivers, and no mapping laid over those can wrap the texture's edge round the nose: it
/// smears, and the branches' lane edges show as lines across the gore. Here only the trunk's share (the road the
/// split comes from) is fanned. Each branch is laid like a road of its own, in rows across it from the node out to
/// its row, between its outer kerb and a spine from the node through the apex (where the branches' inner edges
/// cross) to the nose's tip; its texture is fitted across that, edge on the outer kerb and round the nose, and held
/// just short of its edge along the spine, which is no kerb.
/// </summary>
internal static class RoadSplitBuilder
{
/// <summary>
/// How far short of its edge (in U) a branch's texture is held along the spine.
/// </summary>
private const float SpineMargin = 0.05f;
/// <summary>
/// A nose sharper than this (the sine of its corner's half angle) is what makes a split.
/// </summary>
private const float MaxHalfAngleSin = 0.5f;
/// <summary>
/// A nose nearer the node than this many times the wider branch's width is fanned like any other corner.
/// </summary>
private const float MinReach = 2.0f;
/// <summary>
/// The spine's points from the node to the apex are this share of the wider branch's width apart; from the apex
/// to the tip, and along each row, this share of the branch's own. Close enough that the surface strays no more
/// than a few units from the plain fan the ground under a junction is shaped to, even round a steep nose.
/// </summary>
private const float RowStep = 0.25f;
private const float PointStep = 0.0625f;
/// <summary>
/// How far (as a share of the junction's area) the triangles may cover more or less than its outline before
/// something is taken to have gone wrong and the junction is fanned instead.
/// </summary>
private const double AreaTolerance = 0.001;
/// <summary>
/// A point of a branch's strip and its texture coordinates.
/// </summary>
private readonly record struct Mark(Vector3 Position, Vector2 Uv);
/// <summary>
/// Builds the junction's surface as a split, when it is one: three arms, two of which (the branches) meet at a
/// sharp, filleted nose well out from the node, beyond the point their inner edges cross. Anything else, or
/// anything the rows would not lay out cleanly, is left to the fan. <paramref name="_Mappings"/> are the arms' own
/// mappings, in the layout's order.
/// </summary>
public static bool TryBuild(RoadJunctionLayout _Layout, IReadOnlyList<RoadArmMapping> _Mappings, Material _Material, out PolygonMesh _Mesh)
{
_Mesh = null;
var polygon = _Layout.Polygon;
var rows = _Layout.RowStarts;
int count = polygon.Count;
if (_Layout.Arms.Count != 3 || rows is null || rows.Count != 3 || _Mappings.Count != 3 || count < 3)
return false;
foreach (int row in rows)
{
if (row < 0 || row >= count)
return false;
}
RoadJunctionLayout.CornerPath nose = null;
foreach (var path in _Layout.CornerPaths)
{
if (path.Fold is null || path.Fold.HalfAngleSin >= MaxHalfAngleSin)
continue;
if (nose is not null)
return false;
nose = path;
}
if (nose is null)
return false;
// Counter-clockwise: the trunk, then the branch whose left edge runs to the nose, then the other.
int first = _Layout.Arms.IndexOf(nose.ArmA);
int second = _Layout.Arms.IndexOf(nose.ArmB);
int trunk = 3 - first - second;
if (first < 0 || second != (first + 1) % 3 || trunk != (second + 1) % 3)
return false;
// The trunk comes in from the far side: a branch at a right angle to it is a corner, not a split.
var trunkDirection = _Layout.Arms[trunk].Direction;
if (Vector2.Dot(trunkDirection, _Layout.Arms[first].Direction) > -0.5f || Vector2.Dot(trunkDirection, _Layout.Arms[second].Direction) > -0.5f)
return false;
// The nose's points are the polygon's from the first branch's row on, its middle the tip.
int noseStart = (rows[first] + 1) % count;
if (Wrap(rows[second] - noseStart, count) != nose.Points.Count - 1 || nose.MidIndex <= 0 || nose.MidIndex >= nose.Points.Count - 1)
return false;
int tip = (noseStart + nose.MidIndex) % count;
var node = _Layout.Centre;
Vector3 tipPoint = polygon[tip];
var apex = OnCone(_Layout, new Vector3(nose.Fold.Apex.x, nose.Fold.Apex.y, node.z));
float reach = Flat(tipPoint - node).Length;
float width = MathF.Max(RowWidth(_Layout.Arms[first]), RowWidth(_Layout.Arms[second]));
// The apex has to lie between the node and the nose, the nose well out.
if (reach < MinReach * width || Vector2.Dot(Flat(apex - node), Flat(tipPoint - node)) <= 0.0f || Flat(apex - node).Length >= reach)
return false;
// Where the trunk's share ends on its two kerbs: their points nearest the node.
int outerFirst = Nearest(polygon, (rows[trunk] + 1) % count, rows[first], node);
int outerSecond = Nearest(polygon, (rows[second] + 1) % count, rows[trunk], node);
if (outerFirst < 0 || outerSecond < 0)
return false;
// The spine, shared by both branches: from the node to the apex, and on down the gore to the tip.
var spine = new List<Vector3> { node };
AddSpan(spine, node, apex, width * RowStep, _Layout);
AddSpan(spine, apex, tipPoint, width * PointStep, _Layout);
spine.RemoveAt(spine.Count - 1);
// Each branch's two sides, from the node's end out to its row: its outer kerb, and the spine and its half of
// the nose.
var firstOuter = Walk(polygon, outerFirst, rows[first]);
var firstInner = Walk(polygon, noseStart, tip);
var secondOuter = Walk(polygon, (rows[second] + 1) % count, outerSecond);
var secondInner = Walk(polygon, tip, rows[second]);
firstInner.Reverse();
secondOuter.Reverse();
firstInner.InsertRange(0, spine);
secondInner.InsertRange(0, spine);
// Rows across a branch need both its sides to run steadily away from the node; anything else is fanned.
if (!Outward(firstOuter, _Layout.Arms[first], _Mappings[first]) || !Outward(firstInner, _Layout.Arms[first], _Mappings[first])
|| !Outward(secondOuter, _Layout.Arms[second], _Mappings[second]) || !Outward(secondInner, _Layout.Arms[second], _Mappings[second]))
return false;
var firstTriangles = BuildBranch(_Layout, _Layout.Arms[first], _Mappings[first], firstOuter, firstInner, spine.Count);
var secondTriangles = BuildBranch(_Layout, _Layout.Arms[second], _Mappings[second], secondOuter, secondInner, spine.Count);
if (firstTriangles is null || secondTriangles is null)
return false;
// The trunk's share fanned from the node, and both branches, have to cover the outline exactly: no more (two
// triangles over one spot) and no less (a hole).
var share = Walk(polygon, outerSecond, outerFirst);
double outline = 0.0;
double covered = 0.0;
for (int i = 0; i < count; i++)
outline += Cross(polygon[i], polygon[(i + 1) % count], node);
for (int i = 0; i < share.Count - 1; i++)
covered += Cross(share[i], share[i + 1], node);
foreach (var triangle in firstTriangles)
covered += Cross(triangle[1].Position, triangle[2].Position, triangle[0].Position);
foreach (var triangle in secondTriangles)
covered += Cross(triangle[1].Position, triangle[2].Position, triangle[0].Position);
if (outline <= 0.0 || Math.Abs(covered - outline) > outline * AreaTolerance)
return false;
var mesh = new PolygonMesh();
var vertices = new Dictionary<Vector2, HalfEdgeMesh.VertexHandle>();
HalfEdgeMesh.VertexHandle Vertex(Vector3 _Position)
{
var key = Flat(_Position);
if (!vertices.TryGetValue(key, out var handle))
{
handle = mesh.AddVertices(_Position)[0];
vertices[key] = handle;
}
return handle;
}
// The trunk first, then each branch from the node out, so every face joins on to those before it.
RoadFanBuilder.Build(mesh, _Material, node, share, new int[share.Count], new IFanMapping[] { _Mappings[trunk] }, new[] { true }, Vertex, false);
foreach (var triangle in firstTriangles)
MeshUtility.AddTexturedTriangle(mesh, _Material, Vertex(triangle[0].Position), Vertex(triangle[1].Position), Vertex(triangle[2].Position), triangle[0].Uv, triangle[1].Uv, triangle[2].Uv);
foreach (var triangle in secondTriangles)
MeshUtility.AddTexturedTriangle(mesh, _Material, Vertex(triangle[0].Position), Vertex(triangle[1].Position), Vertex(triangle[2].Position), triangle[0].Uv, triangle[1].Uv, triangle[2].Uv);
_Mesh = mesh;
return true;
}
/// <summary>
/// One branch's triangles, facing up, in the order they join on to one another. <paramref name="_Outer"/> runs
/// along its outer kerb from the trunk's share to its row; <paramref name="_Inner"/> along the spine and then, from
/// <paramref name="_KerbFrom"/> on, round the nose's half from its tip to the row. A row crosses the branch from each
/// point of the inner side to the point of the outer kerb as far along it (as a share of the way, so the first row is
/// the trunk's fan's end spoke and the last the branch's own row); the band between two rows is zipped from the kerb
/// in, with whatever of the kerb's own points lie between them fanned from the first row's first point in. Null when
/// the rows would fold over one another anywhere.
/// </summary>
private static List<Mark[]> BuildBranch(RoadJunctionLayout _Layout, RoadJunctionLayout.Arm _Arm, RoadArmMapping _Mapping, List<Vector3> _Outer, List<Vector3> _Inner,
int _KerbFrom)
{
if (_Outer.Count < 2 || _Inner.Count - _KerbFrom < 2)
return null;
// The branch's texture runs from its edge on the outer kerb to its other edge round the nose.
float outerU = _Mapping.KerbUv(_Outer[^1]).x;
float innerEdgeU = _Mapping.KerbUv(_Inner[^1]).x;
bool innerHigh = innerEdgeU > outerU;
float InnerU(int _Index)
{
if (_Index >= _KerbFrom)
return innerEdgeU;
float u = _Mapping.Uv(_Inner[_Index]).x;
return innerHigh ? MathF.Min(u, innerEdgeU - SpineMargin) : MathF.Max(u, innerEdgeU + SpineMargin);
}
Mark MarkAt(Vector3 _Point, float _U)
{
return new Mark(_Point, new Vector2(_U, _Mapping.Uv(_Point).y));
}
float[] outerAt = Levels(_Outer, _Arm, _Mapping);
float[] innerAt = Levels(_Inner, _Arm, _Mapping);
float outerSpan = outerAt[^1] - outerAt[0];
float innerSpan = innerAt[^1] - innerAt[0];
if (outerSpan < 1.0f || innerSpan < 1.0f)
return null;
float step = MathF.Max(1.0f, RowWidth(_Arm) * PointStep);
var rows = new List<(Mark[] Points, int Next)>(_Inner.Count);
for (int k = 0; k < _Inner.Count; k++)
{
bool last = k == _Inner.Count - 1;
Vector3 outerPoint;
int next;
if (k == 0)
{
outerPoint = _Outer[0];
next = 1;
}
else if (last)
{
outerPoint = _Outer[^1];
next = _Outer.Count;
}
else
{
outerPoint = PointAt(_Outer, outerAt, outerAt[0] + (innerAt[k] - innerAt[0]) / innerSpan * outerSpan, out next);
}
Vector3 innerPoint = _Inner[k];
float innerU = InnerU(k);
// The first row is the trunk's fan's end spoke, whole.
int pieces = k == 0 ? 1 : Math.Max(1, (int)MathF.Ceiling(Flat(innerPoint - outerPoint).Length / step));
var row = new Mark[pieces + 1];
for (int j = 0; j <= pieces; j++)
{
float f = (float)j / pieces;
Vector3 point = j == 0 ? outerPoint : j == pieces ? innerPoint : Vector3.Lerp(outerPoint, innerPoint, f);
// Inside, on the surface the plain fan would have; the branch's own row straight, as its road's is.
if (j > 0 && j < pieces && !last)
point = OnCone(_Layout, point);
row[j] = MarkAt(point, MathX.Lerp(outerU, innerU, f));
}
rows.Add((row, next));
}
var triangles = new List<Mark[]>();
for (int k = 0; k < rows.Count - 1; k++)
{
var (a, aNext) = rows[k];
var (b, bNext) = rows[k + 1];
// The kerb's own points between the two rows, fanned from the first row's first point in.
var kerb = new List<Mark> { a[0] };
for (int i = aNext; i < bNext && i < _Outer.Count; i++)
{
if (Flat(_Outer[i] - a[0].Position).Length > 0.01f && Flat(_Outer[i] - b[0].Position).Length > 0.01f)
kerb.Add(MarkAt(_Outer[i], outerU));
}
if (kerb.Count > 1)
{
kerb.Add(b[0]);
for (int j = 0; j < kerb.Count - 1; j++)
Add(triangles, kerb[j + 1], kerb[j], a[1]);
Zip(triangles, a, 1, b);
}
else
{
Zip(triangles, a, 0, b);
}
}
// One winding for the whole branch, facing up; a triangle against it means the rows fold over somewhere.
double area = 0.0;
foreach (var triangle in triangles)
area += Cross(triangle[1].Position, triangle[2].Position, triangle[0].Position);
if (area < 0.0)
{
foreach (var triangle in triangles)
(triangle[1], triangle[2]) = (triangle[2], triangle[1]);
}
foreach (var triangle in triangles)
{
if (Cross(triangle[1].Position, triangle[2].Position, triangle[0].Position) <= 0.0)
return null;
}
return triangles;
}
/// <summary>
/// Triangles between two rows of a band (<paramref name="_A"/> from <paramref name="_From"/> on, and
/// <paramref name="_B"/>), both running from the kerb in, in the order their points come along them.
/// </summary>
private static void Zip(List<Mark[]> _Triangles, Mark[] _A, int _From, Mark[] _B)
{
float[] atA = Fractions(_A, _From);
float[] atB = Fractions(_B, 0);
int a = _From;
int b = 0;
while (a < _A.Length - 1 || b < _B.Length - 1)
{
if (b == _B.Length - 1 || (a < _A.Length - 1 && atA[a + 1] <= atB[b + 1]))
{
Add(_Triangles, _A[a], _A[a + 1], _B[b]);
a++;
}
else
{
Add(_Triangles, _A[a], _B[b + 1], _B[b]);
b++;
}
}
}
/// <summary>
/// Adds a triangle unless its points are in a line (rows that share a point on the kerb, points along a straight
/// kerb), which would make no face.
/// </summary>
private static void Add(List<Mark[]> _Triangles, Mark _A, Mark _B, Mark _C)
{
if (Math.Abs(Cross(_B.Position, _C.Position, _A.Position)) < 0.002)
return;
_Triangles.Add(new[] { _A, _B, _C });
}
/// <summary>
/// How far along a row each of its points is from <paramref name="_From"/>, as a share of the rest of its length.
/// </summary>
private static float[] Fractions(Mark[] _Row, int _From)
{
var at = new float[_Row.Length];
for (int i = _From + 1; i < _Row.Length; i++)
at[i] = at[i - 1] + Flat(_Row[i].Position - _Row[i - 1].Position).Length;
float length = at[^1];
for (int i = _From; i < at.Length; i++)
at[i] = length > 0.0f ? at[i] / length : 0.0f;
return at;
}
/// <summary>
/// Points from <paramref name="_From"/> to <paramref name="_To"/> along a straight line, the first left out (it is
/// already there) and the rest on the junction's surface.
/// </summary>
private static void AddSpan(List<Vector3> _Points, Vector3 _From, Vector3 _To, float _Step, RoadJunctionLayout _Layout)
{
int pieces = Math.Max(1, (int)MathF.Ceiling(Flat(_To - _From).Length / MathF.Max(1.0f, _Step)));
for (int j = 1; j <= pieces; j++)
_Points.Add(j == pieces ? _To : OnCone(_Layout, Vector3.Lerp(_From, _To, (float)j / pieces)));
}
/// <summary>
/// Whether a line runs steadily out from the node along an arm: no point of it more than a unit back from the
/// furthest out before it.
/// </summary>
private static bool Outward(List<Vector3> _Points, RoadJunctionLayout.Arm _Arm, RoadArmMapping _Mapping)
{
float furthest = float.MinValue;
foreach (var point in _Points)
{
float fromNode = FromNode(point, _Arm, _Mapping);
if (fromNode < furthest - 1.0f)
return false;
furthest = MathF.Max(furthest, fromNode);
}
return true;
}
/// <summary>
/// How far out from the node along the arm each point of a line is, never less than the point before's.
/// </summary>
private static float[] Levels(List<Vector3> _Points, RoadJunctionLayout.Arm _Arm, RoadArmMapping _Mapping)
{
var at = new float[_Points.Count];
for (int i = 0; i < _Points.Count; i++)
{
float fromNode = FromNode(_Points[i], _Arm, _Mapping);
at[i] = i == 0 ? fromNode : MathF.Max(fromNode, at[i - 1]);
}
return at;
}
private static float FromNode(Vector3 _Point, RoadJunctionLayout.Arm _Arm, RoadArmMapping _Mapping)
{
float distance = _Mapping.Measure(_Point).y;
return _Arm.AtStart ? distance : _Arm.Length - distance;
}
/// <summary>
/// The point of a line a distance out from the node, and the index of the line's next point after it.
/// </summary>
private static Vector3 PointAt(List<Vector3> _Points, float[] _At, float _Distance, out int _Next)
{
for (int i = 1; i < _Points.Count; i++)
{
if (_At[i] < _Distance)
continue;
float span = _At[i] - _At[i - 1];
float t = span > 0.0001f ? (_Distance - _At[i - 1]) / span : 1.0f;
_Next = i;
return Vector3.Lerp(_Points[i - 1], _Points[i], Math.Clamp(t, 0.0f, 1.0f));
}
_Next = _Points.Count;
return _Points[^1];
}
/// <summary>
/// A point brought to the height the junction's plain fan has there: on the line from the node out through it to
/// the outline, the node's height and the outline's in proportion.
/// </summary>
private static Vector3 OnCone(RoadJunctionLayout _Layout, Vector3 _Point)
{
var polygon = _Layout.Polygon;
var node = _Layout.Centre;
Vector2 ray = Flat(_Point - node);
float nearest = float.MaxValue;
float height = node.z;
for (int i = 0; i < polygon.Count; i++)
{
Vector3 a = polygon[i];
Vector3 b = polygon[(i + 1) % polygon.Count];
Vector2 edge = Flat(b - a);
Vector2 toA = Flat(a - node);
float denominator = Cross(ray, edge);
if (MathF.Abs(denominator) < 0.0001f)
continue;
float along = Cross(toA, edge) / denominator;
float on = Cross(toA, ray) / denominator;
if (along > 0.0001f && on >= -0.0001f && on <= 1.0001f && along < nearest)
{
nearest = along;
height = MathX.Lerp(a.z, b.z, Math.Clamp(on, 0.0f, 1.0f));
}
}
if (nearest == float.MaxValue)
return _Point;
return new Vector3(_Point.x, _Point.y, node.z + (height - node.z) / nearest);
}
/// <summary>
/// The outline's points from one index round to another, both included.
/// </summary>
private static List<Vector3> Walk(IReadOnlyList<Vector3> _Polygon, int _From, int _To)
{
var points = new List<Vector3>();
int count = _Polygon.Count;
for (int i = _From; ; i = (i + 1) % count)
{
points.Add(_Polygon[i]);
if (i == _To || points.Count > count)
break;
}
return points;
}
/// <summary>
/// The index of the outline's point nearest a position, strictly between two indices (going round); -1 when there
/// is none between them.
/// </summary>
private static int Nearest(IReadOnlyList<Vector3> _Polygon, int _After, int _Before, Vector3 _Position)
{
int count = _Polygon.Count;
int best = -1;
float nearest = float.MaxValue;
for (int i = (_After + 1) % count; i != _Before; i = (i + 1) % count)
{
float distance = Flat(_Polygon[i] - _Position).Length;
if (distance < nearest)
{
nearest = distance;
best = i;
}
}
return best;
}
private static float RowWidth(RoadJunctionLayout.Arm _Arm)
{
float distance = _Arm.CurveDistance(_Arm.Trim);
return _Arm.Widths.WidthAt(distance);
}
private static int Wrap(int _Index, int _Count)
{
return ((_Index % _Count) + _Count) % _Count;
}
private static Vector2 Flat(Vector3 _Vector)
{
return new Vector2(_Vector.x, _Vector.y);
}
private static float Cross(Vector2 _A, Vector2 _B)
{
return _A.x * _B.y - _A.y * _B.x;
}
/// <summary>
/// Twice the area (in plan, counter-clockwise positive) of the triangle from an origin to two points, worked out
/// from the origin so far-out coordinates lose nothing.
/// </summary>
private static double Cross(Vector3 _A, Vector3 _B, Vector3 _Origin)
{
double ax = (double)_A.x - _Origin.x;
double ay = (double)_A.y - _Origin.y;
double bx = (double)_B.x - _Origin.x;
double by = (double)_B.y - _Origin.y;
return ax * by - ay * bx;
}
}