Code/Building/JunctionLayout.cs

Layout and geometry for river junctions. Computes arms for rivers meeting at a node, samples centre/left frames, finds where left bank of one arm meets right bank of the next, optionally fits fillet arcs, computes trims and builds a counterclockwise outline polygon for the confluence.

File Access
using System;
using System.Collections.Generic;
using System.Linq;
using Sandbox;

namespace RedSnail.WaterTool2;

/// <summary>
/// Where rivers meet at a node (a confluence, a fork, or a bend sharp enough to need one): how far back from the node
/// each river's own mesh has to stop, and the outline of the water that fills the space between them, with rounded
/// banks. All of it is worked out on the rivers' real curved banks, so the outline lands exactly on the rows the
/// rivers stop at. Worked out from the graph on every reconcile, never stored.
/// </summary>
internal sealed class JunctionLayout
{
	public sealed class Arm
	{
		public WaterRiver River;
		public WaterCurveTable Table;

		/// <summary>
		/// The river starts at this node, so its water leaves the confluence (otherwise it ends here, and its water
		/// arrives).
		/// </summary>
		public bool AtStart;

		/// <summary>
		/// Horizontal unit direction leaving the node.
		/// </summary>
		public Vector2 Direction;

		public RiverWidth Width;

		/// <summary>
		/// The trim the corners ask for; <see cref="Trim"/> is this unless a short river had to give some back.
		/// <see cref="SharpTrim"/> is what the corners would ask for with no fillet at all.
		/// </summary>
		public float RawTrim;
		public float SharpTrim;

		/// <summary>
		/// How far from the node, along the curve, the river's own mesh starts.
		/// </summary>
		public float Trim;

		/// <summary>
		/// Samples of the river near the node, from a straight extension behind the node (negative distances, where
		/// the outer corner of a sharp bend lives) out along the curve. Banks are offsets of these.
		/// </summary>
		public float[] Distances;
		public Vector3[] Centres;
		public Vector3[] Lefts;
		public float[] Halves;

		public int NodeIndex;

		public float Length => Table.Length;

		public int SampleCount => Distances.Length;



		/// <summary>
		/// The curve distance of a point measured from this node along the arm.
		/// </summary>
		public float CurveDistance(float _FromNode)
		{
			return AtStart ? _FromNode : Table.Length - _FromNode;
		}



		/// <summary>
		/// How far each bank sits from the centre at a distance from the node; the node's own value behind it.
		/// </summary>
		public float Half(float _FromNode)
		{
			return Width.HalfAt(CurveDistance(Math.Clamp(_FromNode, 0.0f, Length)));
		}



		public float OffsetAt(in EdgeRef _Edge, float _FromNode)
		{
			return _Edge.Side * (Half(_FromNode) + _Edge.Extra);
		}



		public float OffsetAt(in EdgeRef _Edge, int _Index)
		{
			return _Edge.Side * (Halves[_Index] + _Edge.Extra);
		}



		public Vector3 EdgePoint(int _Index, float _Offset)
		{
			return Centres[_Index] + Lefts[_Index] * _Offset;
		}



		public Vector3[] OffsetPolyline(in EdgeRef _Edge)
		{
			var points = new Vector3[SampleCount];

			for (int i = 0; i < SampleCount; i++)
				points[i] = EdgePoint(i, OffsetAt(_Edge, i));

			return points;
		}
	}

	/// <summary>
	/// A line along one of an arm's banks (+1 its left, -1 its right, looking out of the node), pushed outward from
	/// that bank by <see cref="Extra"/> (inward when negative).
	/// </summary>
	public readonly record struct EdgeRef(float Side, float Extra)
	{
		public static readonly EdgeRef Left = new(1.0f, 0.0f);
		public static readonly EdgeRef Right = new(-1.0f, 0.0f);
	}

	/// <summary>
	/// Where arm A's left bank meets the right bank of arm B, the next arm counter-clockwise.
	/// </summary>
	private sealed class Corner
	{
		public int ArmA;
		public int ArmB;
		public Vector3 Point;
		public float ParamA;
		public float ParamB;
		public bool HasFillet;
		public Vector2 Centre;
		public float Radius;
		public Vector3 TangentA;
		public Vector3 TangentB;
		public float TangentParamA;
		public float TangentParamB;
		public Vector2 DirectionA;
		public Vector2 DirectionB;
		public float Angle;

		/// <summary>
		/// Which way each bank is pushed sideways to move into the corner's angle.
		/// </summary>
		public float SignA = 1.0f;
		public float SignB = 1.0f;

		public float EndA => HasFillet ? TangentParamA : ParamA;

		public float EndB => HasFillet ? TangentParamB : ParamB;
	}

	private readonly struct Hit
	{
		public readonly Vector3 Point;
		public readonly float DistanceA;
		public readonly float DistanceB;
		public readonly Vector2 DirectionA;
		public readonly Vector2 DirectionB;

		public Hit(Vector3 _Point, float _DistanceA, float _DistanceB, Vector2 _DirectionA, Vector2 _DirectionB)
		{
			Point = _Point;
			DistanceA = _DistanceA;
			DistanceB = _DistanceB;
			DirectionA = _DirectionA;
			DirectionB = _DirectionB;
		}
	}

	/// <summary>
	/// How far out along each arm (in river widths) corners are looked for.
	/// </summary>
	private const float ForwardReach = 4.0f;

	/// <summary>
	/// How far behind the node (in river widths) the arms are extended straight, to catch the outside corner of a
	/// sharp bend.
	/// </summary>
	private const float BackwardReach = 3.0f;

	private const float SampleStep = 30.0f;
	private const float ArcStepDegrees = 15.0f;

	/// <summary>
	/// How far past a sharp corner's point the arm rows sit.
	/// </summary>
	private const float SharpMargin = 3.0f;

	public WaterNode Node { get; private set; }

	/// <summary>
	/// The fillet radius the corners were built with: the node's, unless a short river forced it smaller.
	/// </summary>
	public float Radius { get; }

	public List<Arm> Arms { get; } = new();

	private readonly List<Corner> m_Corners = new();



	private JunctionLayout(WaterNode _Node, float _Radius)
	{
		Node = _Node;
		Radius = _Radius;
	}



	/// <summary>
	/// The layout of the node's confluence, or null when there is nothing to fill: fewer than two usable arms, or arms
	/// running straight through each other (a smooth bend).
	/// </summary>
	public static JunctionLayout Create(WaterNetwork _Network, WaterNode _Node, float _Radius, IReadOnlyDictionary<int, RiverWidth> _Widths)
	{
		var layout = new JunctionLayout(_Node, MathF.Max(0.0f, _Radius));

		foreach (var river in _Network.GetRiversAt(_Node.Id))
		{
			var table = _Network.GetCurveTable(river);

			if (table is null || !_Widths.TryGetValue(river.Id, out var width))
				continue;

			bool atStart = river.StartNodeId == _Node.Id;
			Vector3 tangent = atStart ? table.Curve.Tangent(0.0f) : -table.Curve.Tangent(1.0f);
			var direction = new Vector2(tangent.x, tangent.y);

			// A vertical arm (a waterfall) has no bank to meet anything on.
			if (direction.Length < 0.001f)
				continue;

			layout.Arms.Add(new Arm
			{
				River = river,
				Table = table,
				AtStart = atStart,
				Direction = direction.Normal,
				Width = width
			});
		}

		if (layout.Arms.Count < 2)
			return null;

		layout.Arms.Sort((a, b) => MathF.Atan2(a.Direction.y, a.Direction.x).CompareTo(MathF.Atan2(b.Direction.y, b.Direction.x)));

		float maxWidth = layout.Arms.Max(arm => arm.Width.MaxHalf * 2.0f);

		foreach (var arm in layout.Arms)
			layout.BuildSamples(arm, maxWidth);

		layout.FindCorners();

		// Nothing was trimmed: the arms run straight into each other and the plain strips already join up.
		if (layout.Arms.All(arm => arm.RawTrim <= 0.01f))
			return null;

		layout.ResetTrims();

		return layout;
	}



	public Arm FindArm(int _RiverId)
	{
		foreach (var arm in Arms)
		{
			if (arm.River.Id == _RiverId)
				return arm;
		}

		return null;
	}



	/// <summary>
	/// Puts the trims back to what the corners asked for, before the network gives short rivers some back.
	/// </summary>
	public void ResetTrims()
	{
		foreach (var arm in Arms)
			arm.Trim = arm.RawTrim;
	}



	private void BuildSamples(Arm _Arm, float _MaxWidth)
	{
		float forward = MathF.Min(_Arm.Length, ForwardReach * _MaxWidth);
		float backward = BackwardReach * _MaxWidth;
		int forwardCount = Math.Max(1, (int)MathF.Ceiling(forward / SampleStep));
		int backwardCount = Math.Max(1, (int)MathF.Ceiling(backward / SampleStep));
		int total = backwardCount + forwardCount + 1;

		_Arm.Distances = new float[total];
		_Arm.Centres = new Vector3[total];
		_Arm.Lefts = new Vector3[total];
		_Arm.Halves = new float[total];
		_Arm.NodeIndex = backwardCount;

		for (int k = 0; k <= forwardCount; k++)
		{
			float distance = k == forwardCount ? forward : forward * k / forwardCount;
			int index = backwardCount + k;

			ArmFrame(_Arm, distance, out _Arm.Centres[index], out _Arm.Lefts[index]);

			_Arm.Distances[index] = distance;
			_Arm.Halves[index] = _Arm.Half(distance);
		}

		// Behind the node the river does not exist; carry its frame at the node straight backwards.
		var direction = new Vector3(_Arm.Direction.x, _Arm.Direction.y, 0.0f);

		for (int k = 1; k <= backwardCount; k++)
		{
			float distance = -backward * k / backwardCount;
			int index = backwardCount - k;

			_Arm.Distances[index] = distance;
			_Arm.Centres[index] = _Arm.Centres[backwardCount] + direction * distance;
			_Arm.Lefts[index] = _Arm.Lefts[backwardCount];
			_Arm.Halves[index] = _Arm.Halves[backwardCount];
		}
	}



	/// <summary>
	/// Meets each arm's left bank with the right bank of the next arm round, rounds the corner off and pushes the arm
	/// trims out past it.
	/// </summary>
	private void FindCorners()
	{
		int count = Arms.Count;

		for (int i = 0; i < count; i++)
		{
			int j = (i + 1) % count;
			var a = Arms[i];
			var b = Arms[j];

			if (!Intersect(a, EdgeRef.Left, b, EdgeRef.Right, out Hit hit))
				continue;

			RefineCrossing(a, EdgeRef.Left, b, EdgeRef.Right, ref hit);

			float angle = MathF.Acos(Math.Clamp(Vector2.Dot(hit.DirectionA, hit.DirectionB), -1.0f, 1.0f)).RadianToDegree();

			var bisector = (hit.DirectionA + hit.DirectionB).Normal;
			var leftNormalA = new Vector2(-hit.DirectionA.y, hit.DirectionA.x);
			var rightNormalB = new Vector2(hit.DirectionB.y, -hit.DirectionB.x);

			var corner = new Corner
			{
				ArmA = i,
				ArmB = j,
				Point = hit.Point,
				ParamA = hit.DistanceA,
				ParamB = hit.DistanceB,
				DirectionA = hit.DirectionA,
				DirectionB = hit.DirectionB,
				Angle = angle,
				SignA = Vector2.Dot(bisector, leftNormalA) >= 0.0f ? 1.0f : -1.0f,
				SignB = Vector2.Dot(bisector, rightNormalB) >= 0.0f ? 1.0f : -1.0f
			};

			TryFillet(corner, a, b);
			m_Corners.Add(corner);

			// A sharp corner keeps the rows a hair past its point, so the stretch from row to corner exists.
			float margin = !corner.HasFillet && angle >= 5.0f && angle <= 175.0f ? SharpMargin : 0.0f;

			a.RawTrim = MathF.Max(a.RawTrim, corner.EndA + margin);
			b.RawTrim = MathF.Max(b.RawTrim, corner.EndB + margin);
			a.SharpTrim = MathF.Max(a.SharpTrim, corner.ParamA);
			b.SharpTrim = MathF.Max(b.SharpTrim, corner.ParamB);
		}
	}



	/// <summary>
	/// A circle of the node's radius tangent to both banks: its centre is where the banks, each pushed sideways by the
	/// radius towards the angle between them, cross; the tangent points are the closest points of the banks to it.
	/// </summary>
	private void TryFillet(Corner _Corner, Arm _A, Arm _B)
	{
		float radius = Radius;

		if (radius <= 0.0f)
			return;

		// Nearly straight through, or folded back on itself: nothing sensible to round.
		if (_Corner.Angle < 5.0f || _Corner.Angle > 175.0f)
			return;

		var pushedA = new EdgeRef(1.0f, _Corner.SignA * radius);
		var pushedB = new EdgeRef(-1.0f, _Corner.SignB * radius);

		if (!Intersect(_A, pushedA, _B, pushedB, out Hit centre))
			return;

		var centre2 = new Vector2(centre.Point.x, centre.Point.y);
		Vector3 tangentA;
		Vector3 tangentB;
		float paramA;
		float paramB;

		if (RefineCrossing(_A, pushedA, _B, pushedB, ref centre))
		{
			centre2 = new Vector2(centre.Point.x, centre.Point.y);
			paramA = centre.DistanceA;
			paramB = centre.DistanceB;

			ArmFrame(_A, paramA, out Vector3 centreA, out Vector3 leftA);
			ArmFrame(_B, paramB, out Vector3 centreB, out Vector3 leftB);

			tangentA = centreA + leftA * _A.OffsetAt(EdgeRef.Left, paramA);
			tangentB = centreB + leftB * _B.OffsetAt(EdgeRef.Right, paramB);
		}
		else
		{
			ClosestPoint(_A, EdgeRef.Left, centre2, out tangentA, out paramA);
			ClosestPoint(_B, EdgeRef.Right, centre2, out tangentB, out paramB);
		}

		_Corner.HasFillet = true;
		_Corner.Centre = centre2;
		_Corner.Radius = radius;
		_Corner.TangentA = tangentA;
		_Corner.TangentB = tangentB;
		_Corner.TangentParamA = paramA;
		_Corner.TangentParamB = paramB;
	}



	/// <summary>
	/// The centreline point and left direction of an arm at a distance from the node along it, straight on behind the
	/// node and past the arm's end, computed exactly as the river's rows are.
	/// </summary>
	private static void ArmFrame(Arm _Arm, float _FromNode, out Vector3 _Centre, out Vector3 _Left)
	{
		if (_FromNode < 0.0f)
		{
			_Centre = _Arm.Centres[_Arm.NodeIndex] + new Vector3(_Arm.Direction.x, _Arm.Direction.y, 0.0f) * _FromNode;
			_Left = _Arm.Lefts[_Arm.NodeIndex];
			return;
		}

		float onCurve = MathF.Min(_FromNode, _Arm.Length);
		float curveDistance = _Arm.CurveDistance(onCurve);

		_Arm.Table.Frame(curveDistance, out Vector3 position, out Vector3 tangent);

		Vector3 right = WaterCurve.RightOf(tangent);

		// Looking out of the node, an arm that ends here runs the other way, so its sides swap.
		_Centre = position;
		_Left = _Arm.AtStart ? -right : right;

		if (_FromNode > _Arm.Length)
			_Centre += Vector3.Cross(_Left, Vector3.Up) * (_FromNode - _Arm.Length);
	}



	private static Vector2 OffsetPoint(Arm _Arm, float _FromNode, in EdgeRef _Edge)
	{
		ArmFrame(_Arm, _FromNode, out Vector3 centre, out Vector3 left);

		return Flat(centre + left * _Arm.OffsetAt(_Edge, _FromNode));
	}



	/// <summary>
	/// Moves a crossing found on the sampled polylines onto the curves themselves (Newton on the two distances along
	/// the arms), so corner points and fillet centres are exact rather than a sample step off. False, with the hit
	/// untouched, when it does not converge.
	/// </summary>
	private static bool RefineCrossing(Arm _A, in EdgeRef _OffsetA, Arm _B, in EdgeRef _OffsetB, ref Hit _Hit)
	{
		const float step = 0.5f;
		const float maxStep = 200.0f;

		float distanceA = _Hit.DistanceA;
		float distanceB = _Hit.DistanceB;

		for (int iteration = 0; iteration < 10; iteration++)
		{
			Vector2 error = OffsetPoint(_A, distanceA, _OffsetA) - OffsetPoint(_B, distanceB, _OffsetB);

			if (error.Length < 0.002f)
				break;

			Vector2 slopeA = (OffsetPoint(_A, distanceA + step, _OffsetA) - OffsetPoint(_A, distanceA - step, _OffsetA)) / (2.0f * step);
			Vector2 slopeB = (OffsetPoint(_B, distanceB + step, _OffsetB) - OffsetPoint(_B, distanceB - step, _OffsetB)) / (2.0f * step);

			// Solve slopeA * deltaA - slopeB * deltaB = -error.
			float determinant = slopeB.x * slopeA.y - slopeA.x * slopeB.y;

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

			float deltaA = (error.x * slopeB.y - slopeB.x * error.y) / determinant;
			float deltaB = (error.x * slopeA.y - slopeA.x * error.y) / determinant;

			if (MathF.Abs(deltaA) > maxStep || MathF.Abs(deltaB) > maxStep)
				return false;

			distanceA = Math.Clamp(distanceA + deltaA, _A.Distances[0], _A.Distances[^1]);
			distanceB = Math.Clamp(distanceB + deltaB, _B.Distances[0], _B.Distances[^1]);
		}

		if ((OffsetPoint(_A, distanceA, _OffsetA) - OffsetPoint(_B, distanceB, _OffsetB)).Length > 0.05f)
			return false;

		ArmFrame(_A, distanceA, out Vector3 centreA, out Vector3 leftA);
		ArmFrame(_B, distanceB, out Vector3 centreB, out Vector3 leftB);

		Vector3 point = (centreA + leftA * _A.OffsetAt(_OffsetA, distanceA) + centreB + leftB * _B.OffsetAt(_OffsetB, distanceB)) * 0.5f;
		Vector2 directionA = Flat(Vector3.Cross(leftA, Vector3.Up)).Normal;
		Vector2 directionB = Flat(Vector3.Cross(leftB, Vector3.Up)).Normal;

		_Hit = new Hit(point, distanceA, distanceB, directionA, directionB);

		return true;
	}



	/// <summary>
	/// First crossing of two arms' offset polylines, scanning outwards from behind the node so the crossing nearest the
	/// node wins. Two banks that start on the same point are a river running straight through: they meet at the node
	/// and nothing needs rounding.
	/// </summary>
	private static bool Intersect(Arm _A, in EdgeRef _OffsetA, Arm _B, in EdgeRef _OffsetB, out Hit _Hit)
	{
		var polyA = _A.OffsetPolyline(_OffsetA);
		var polyB = _B.OffsetPolyline(_OffsetB);

		Vector3 startA = polyA[_A.NodeIndex];
		Vector3 startB = polyB[_B.NodeIndex];

		if (Flat(startA).Distance(Flat(startB)) < 1.0f)
		{
			_Hit = new Hit((startA + startB) * 0.5f, 0.0f, 0.0f, SegmentDirection(polyA, _A.NodeIndex), SegmentDirection(polyB, _B.NodeIndex));
			return true;
		}

		for (int i = 0; i < polyA.Length - 1; i++)
		{
			Vector2 p = Flat(polyA[i]);
			Vector2 r = Flat(polyA[i + 1]) - p;

			for (int j = 0; j < polyB.Length - 1; j++)
			{
				Vector2 q = Flat(polyB[j]);
				Vector2 s = Flat(polyB[j + 1]) - q;
				float denominator = Cross(r, s);

				if (MathF.Abs(denominator) < 0.000001f)
					continue;

				Vector2 offset = q - p;
				float t = Cross(offset, s) / denominator;
				float u = Cross(offset, r) / denominator;

				if (t < -0.0001f || t > 1.0001f || u < -0.0001f || u > 1.0001f)
					continue;

				Vector3 pointA = Vector3.Lerp(polyA[i], polyA[i + 1], t);
				Vector3 pointB = Vector3.Lerp(polyB[j], polyB[j + 1], u);

				_Hit = new Hit((pointA + pointB) * 0.5f,
					MathX.Lerp(_A.Distances[i], _A.Distances[i + 1], t),
					MathX.Lerp(_B.Distances[j], _B.Distances[j + 1], u),
					r.Normal, s.Normal);

				return true;
			}
		}

		_Hit = default;
		return false;
	}



	/// <summary>
	/// The point of an arm's offset polyline nearest to a point, and its distance from the node.
	/// </summary>
	private static void ClosestPoint(Arm _Arm, in EdgeRef _Offset, Vector2 _Target, out Vector3 _Point, out float _Distance)
	{
		var poly = _Arm.OffsetPolyline(_Offset);
		float best = float.MaxValue;

		_Point = poly[_Arm.NodeIndex];
		_Distance = 0.0f;

		for (int i = 0; i < poly.Length - 1; i++)
		{
			Vector2 p = Flat(poly[i]);
			Vector2 segment = Flat(poly[i + 1]) - p;
			float lengthSquared = segment.LengthSquared;

			if (lengthSquared <= 0.0f)
				continue;

			float t = Math.Clamp(Vector2.Dot(_Target - p, segment) / lengthSquared, 0.0f, 1.0f);
			float distance = (p + segment * t).Distance(_Target);

			if (distance >= best)
				continue;

			best = distance;
			_Point = Vector3.Lerp(poly[i], poly[i + 1], t);
			_Distance = MathX.Lerp(_Arm.Distances[i], _Arm.Distances[i + 1], t);
		}
	}



	private static Vector2 SegmentDirection(Vector3[] _Poly, int _Index)
	{
		int from = Math.Min(_Index, _Poly.Length - 2);

		return (Flat(_Poly[from + 1]) - Flat(_Poly[from])).Normal;
	}



	/// <summary>
	/// Builds the outline of the confluence once the arm trims are final, counter-clockwise round the node: each arm
	/// contributes the whole row its river stops at (every point of it, exactly as the river has it), then its left
	/// bank back to the corner it shares with the next arm, the corner (or its fillet arc), and the next arm's right
	/// bank out to that arm's row. <paramref name="_RowOf"/> hands over each arm's row, left bank first looking
	/// downstream; each row is given its own id, counting on from <paramref name="_FirstRowId"/>.
	/// </summary>
	public List<OutlineVertex> BuildOutline(Func<Arm, RiverRow> _RowOf, int _FirstRowId)
	{
		var outline = new List<OutlineVertex>();

		for (int i = 0; i < Arms.Count; i++)
		{
			var arm = Arms[i];
			var row = _RowOf(arm);

			if (row is null || row.Count < 2)
				return null;

			// Looking out of the node the polygon crosses each row from its right bank to its left. For a river leaving
			// the node that is its own right to left; for one arriving, its own left to right.
			int rowId = _FirstRowId + i;

			for (int k = 0; k < row.Count; k++)
				AddVertex(outline, OutlineVertex.OnRow(row, arm.AtStart ? row.Count - 1 - k : k, rowId));

			var corner = m_Corners.Find(candidate => candidate.ArmA == i);

			// No corner, or a corner the arms had to be trimmed short of: nothing joins this side to the next arm, the
			// outline cuts straight across to its row.
			if (corner is null || corner.EndA > arm.Trim + 0.01f || corner.EndB > Arms[corner.ArmB].Trim + 0.01f)
				continue;

			var next = Arms[corner.ArmB];

			// Down A's left bank from its row to the corner...
			for (int k = arm.SampleCount - 1; k >= 0; k--)
			{
				if (arm.Distances[k] < arm.Trim && arm.Distances[k] > corner.EndA)
					AddVertex(outline, OutlineVertex.Free(arm.EdgePoint(k, arm.Halves[k])));
			}

			AddCorner(outline, corner);

			// ...and out along B's right bank to its row, which the next arm adds itself.
			for (int k = 0; k < next.SampleCount; k++)
			{
				if (next.Distances[k] > corner.EndB && next.Distances[k] < next.Trim)
					AddVertex(outline, OutlineVertex.Free(next.EdgePoint(k, -next.Halves[k])));
			}
		}

		return SurfaceMesher.RemoveDuplicates(outline);
	}



	/// <summary>
	/// The corner itself, or its fillet arc, its height eased from one bank to the other.
	/// </summary>
	private static void AddCorner(List<OutlineVertex> _Outline, Corner _Corner)
	{
		if (!_Corner.HasFillet)
		{
			AddVertex(_Outline, OutlineVertex.Free(_Corner.Point));
			return;
		}

		AddVertex(_Outline, OutlineVertex.Free(_Corner.TangentA));

		float angle0 = MathF.Atan2(_Corner.TangentA.y - _Corner.Centre.y, _Corner.TangentA.x - _Corner.Centre.x);
		float angle1 = MathF.Atan2(_Corner.TangentB.y - _Corner.Centre.y, _Corner.TangentB.x - _Corner.Centre.x);
		float delta = angle1 - angle0;

		// Shortest way round: a fillet arc is always less than a half turn.
		if (delta > MathF.PI)
			delta -= MathF.PI * 2.0f;
		else if (delta < -MathF.PI)
			delta += MathF.PI * 2.0f;

		int steps = Math.Max(2, (int)MathF.Ceiling(MathF.Abs(delta) / ArcStepDegrees.DegreeToRadian()));

		for (int k = 1; k < steps; k++)
		{
			float fraction = (float)k / steps;
			float angle = angle0 + delta * fraction;
			var point = _Corner.Centre + new Vector2(MathF.Cos(angle), MathF.Sin(angle)) * _Corner.Radius;

			AddVertex(_Outline, OutlineVertex.Free(new Vector3(point.x, point.y, MathX.Lerp(_Corner.TangentA.z, _Corner.TangentB.z, fraction))));
		}

		AddVertex(_Outline, OutlineVertex.Free(_Corner.TangentB));
	}



	/// <summary>
	/// Appends a point unless it sits on the one before. A row's point always wins over a free point there: the
	/// row is shared with the river and has to be there exactly as it is.
	/// </summary>
	private static void AddVertex(List<OutlineVertex> _Outline, OutlineVertex _Vertex)
	{
		if (_Outline.Count > 0 && Flat(_Outline[^1].Position).Distance(Flat(_Vertex.Position)) < 0.5f)
		{
			if (_Outline[^1].IsFixed || !_Vertex.IsFixed)
				return;

			_Outline[^1] = _Vertex;
			return;
		}

		_Outline.Add(_Vertex);
	}



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