Code/PolygonMeshBuilder.Bevel.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Sandbox.Polygons;

partial class PolygonMeshBuilder
{
	private HashSet<(int A, int B)> PossibleCuts { get; } = new();

	[ThreadStatic] private static List<(int A, int B)> Bevel_PossibleCutList;

	[ThreadStatic] private static List<int> Bevel_ActiveEdgeList;


	/// <summary>
	/// Add faces starting at each active edge, traveling inwards and upwards to produce a bevel.
	/// If the bevel distance is large enough the mesh will become closed. Otherwise, you can use
	/// <see cref="Close"/> to add a flat face after the bevel.
	/// </summary>
	/// <param name="width">Total distance inwards.</param>
	/// <param name="height">Total distance upwards, away from the plane of the polygon.</param>
	public PolygonMeshBuilder Bevel( float width, float height )
	{
		var angle = MathF.Atan2( width, height );

		return Bevel( width, height, angle, angle );
	}

	/// <summary>
	/// Add faces starting at each active edge, traveling inwards and upwards to produce a bevel.
	/// Use <paramref name="prevAngle"/> and <paramref name="nextAngle"/> to control the normal directions
	/// at the start and end of the bevel faces. Angles are in radians, with 0 pointing outwards along
	/// the plane of the polygon, and PI/2 pointing upwards away from the plane.
	/// If the bevel distance is large enough the mesh will become closed. Otherwise, you can use
	/// <see cref="Close"/> to add a flat face after the bevel.
	/// </summary>
	/// <param name="width">Total distance inwards.</param>
	/// <param name="height">Total distance upwards, away from the plane of the polygon.</param>
	/// <param name="prevAngle">Angle, in radians, to use for normals at the outside of the bevel.</param>
	/// <param name="nextAngle"></param>
	public PolygonMeshBuilder Bevel( float width, float height, float prevAngle, float nextAngle )
	{
		if ( width < 0f )
		{
			throw new ArgumentOutOfRangeException( nameof( width ) );
		}

		Validate();
		Bevel_UpdateExistingVertices( width, height, prevAngle, nextAngle );

		var cutList = Bevel_PossibleCutList ??= new List<(int A, int B)>();
		var edgeList = Bevel_ActiveEdgeList ??= new List<int>();

		var finished = false;
		var endDist = _nextDistance;

		if ( MathF.Abs( _nextDistance ) > 0.001f )
		{
			var maxIterations = _activeEdges.Count * _activeEdges.Count;

			int iterations;
			for ( iterations = 0; iterations < maxIterations && _activeEdges.Count > 0; ++iterations )
			{
				int? closedEdge = null;
				int? splitEdge = null;
				int? splittingEdge = null;

				Vector2 bestPos = default;

				var bestDist = _nextDistance;
				var bestMerge = false;

				foreach ( var index in _activeEdges )
				{
					ref var edge = ref _allEdges[index];

					if ( edge.MaxDistance >= bestDist ) continue;

					var next = _allEdges[edge.NextEdge];

					bestDist = edge.MaxDistance;
					closedEdge = edge.Index;
					bestPos = (edge.Project( edge.MaxDistance ) + next.Project( edge.MaxDistance )) * 0.5f;
				}

				cutList.Clear();
				cutList.AddRange( PossibleCuts );

				foreach ( var (index, otherIndex) in cutList )
				{
					if ( !_activeEdges.Contains( index ) || !_activeEdges.Contains( otherIndex ) )
					{
						PossibleCuts.Remove( (index, otherIndex) );
						continue;
					}

					var edge = _allEdges[index];
					var other = _allEdges[otherIndex];

					var splitDist = CalculateSplitDistance( edge, other, _allEdges[other.NextEdge],
						out var splitPos, out var merge );

					if ( splitDist - _nextDistance > 0.001f )
					{
						PossibleCuts.Remove( (index, otherIndex) );
						continue;
					}

					if ( splitDist >= bestDist ) continue;

					bestDist = splitDist;
					bestPos = splitPos;
					bestMerge = merge;

					closedEdge = null;
					splitEdge = other.Index;
					splittingEdge = edge.Index;
				}

				if ( splittingEdge != null && bestMerge )
				{
					Bevel_Merge( splittingEdge.Value, splitEdge.Value, bestPos, bestDist );
					continue;
				}

				if ( splittingEdge != null )
				{
					Bevel_Split( splittingEdge.Value, splitEdge.Value, bestPos, bestDist );
					continue;
				}

				if ( closedEdge != null )
				{
					Bevel_Close( closedEdge.Value, bestPos, bestDist );
					continue;
				}

				finished = true;
				break;
			}

			if ( _activeEdges.Count > 0 && iterations == maxIterations )
			{
				throw new Exception( $"Exploded after {iterations} with {_activeEdges.Count} active edges!" );
			}
		}
		else
		{
			finished = true;
		}

		if ( !finished && _activeEdges.Count > 0 )
		{
			endDist = _activeEdges.Max( i => _allEdges[i].Distance );
		}

		EnsureCapacity( _activeEdges.Count );

		edgeList.Clear();
		edgeList.AddRange( _activeEdges );

		_activeEdges.Clear();

		foreach ( var index in edgeList )
		{
			ref var b = ref _allEdges[index];
			ref var a = ref _allEdges[b.PrevEdge];
			ref var c = ref _allEdges[b.NextEdge];
			ref var d = ref _allEdges[AddEdge( b.Project( endDist ), b.Tangent, endDist )];

			var ai = AddVertices( ref a );
			var bi = AddVertices( ref b );
			var ci = AddVertices( ref c );

			ConnectEdges( ref a, ref d );
			ConnectEdges( ref d, ref c );

			var di = AddVertices( ref d, true );

			AddTriangle( ai.Next, di.Prev, bi.Prev );
			AddTriangle( bi.Next, di.Next, ci.Prev );

			_activeEdges.Add( d.Index );
		}

		PostBevel();

		return this;
	}

	private void Bevel_UpdateExistingVertices( float width, float height, float prevAngle, float nextAngle )
	{
		_nextDistance = _prevDistance + width;
		_nextHeight = _prevHeight + height;
		_nextAngle = nextAngle;
		_minSmoothNormalDot = MathF.Cos( Math.Clamp( MaxSmoothAngle, 0f, MathF.PI * (511f / 512f) ) );

		_invDistance = width <= 0.0001f ? 0f : 1f / (_nextDistance - _prevDistance);

		if ( !SkipNormals && Math.Abs( _prevAngle - prevAngle ) >= 0.001f )
		{
			foreach ( var index in _activeEdges )
			{
				ref var edge = ref _allEdges[index];
				edge.Vertices = (-1, -1);
			}
		}

		_prevAngle = prevAngle;

		PossibleCuts.Clear();

		foreach ( var index in _activeEdges )
		{
			ref var edge = ref _allEdges[index];
			UpdateMaxDistance( ref edge, _allEdges[edge.NextEdge] );

			foreach ( var otherIndex in _activeEdges )
			{
				if ( otherIndex != index )
				{
					PossibleCuts.Add( (index, otherIndex) );
				}
			}
		}
	}

	private void Bevel_Merge( int edgeA, int edgeB, Vector2 mergePos, float bestDist )
	{
		EnsureCapacity( 2 );

		ref var a = ref _allEdges[edgeA];
		ref var b = ref _allEdges[edgeB];

		_activeEdges.Remove( a.Index );
		_activeEdges.Remove( b.Index );

		if ( a.NextEdge == b.Index && b.NextEdge == a.Index )
		{
			return;
		}

		ref var aPrev = ref _allEdges[a.PrevEdge];
		ref var bPrev = ref _allEdges[b.PrevEdge];

		ref var aNext = ref _allEdges[a.NextEdge];
		ref var bNext = ref _allEdges[b.NextEdge];

		ref var aNew = ref _allEdges[AddEdge( mergePos, a.Tangent, bestDist, 1 )];
		ref var bNew = ref _allEdges[AddEdge( mergePos, b.Tangent, bestDist, -1 )];

		var aPrevi = AddVertices( ref aPrev ).Next;
		var ai = AddVertices( ref a );
		var aNexti = AddVertices( ref aNext ).Prev;
		var bPrevi = AddVertices( ref bPrev ).Next;
		var bi = AddVertices( ref b );
		var bNexti = AddVertices( ref bNext ).Prev;

		_activeEdges.Add( aNew.Index );
		_activeEdges.Add( bNew.Index );

		ConnectEdges( ref bPrev, ref aNew );
		ConnectEdges( ref aNew, ref aNext );

		ConnectEdges( ref aPrev, ref bNew );
		ConnectEdges( ref bNew, ref bNext );

		UpdateMaxDistance( ref bPrev, aNew );
		UpdateMaxDistance( ref aNew, aNext );
		UpdateMaxDistance( ref aNext, _allEdges[aNext.NextEdge] );

		UpdateMaxDistance( ref aPrev, bNew );
		UpdateMaxDistance( ref bNew, bNext );
		UpdateMaxDistance( ref bNext, _allEdges[bNext.NextEdge] );

		var aNewi = AddVertices( ref aNew );
		var bNewi = AddVertices( ref bNew );

		AddTriangle( aPrevi, bNewi.Prev, ai.Prev );
		AddTriangle( ai.Next, aNewi.Next, aNexti );
		AddTriangle( bPrevi, aNewi.Prev, bi.Prev );
		AddTriangle( bi.Next, bNewi.Next, bNexti );

		AddAllPossibleCuts( aNew.Index );
		AddAllPossibleCuts( aNext.Index );
		AddAllPossibleCuts( bNew.Index );
		AddAllPossibleCuts( bNext.Index );
	}

	private void Bevel_Split( int splittingEdge, int splitEdge, Vector2 splitPos, float bestDist )
	{
		EnsureCapacity( 2 );

		ref var a = ref _allEdges[splitEdge];
		ref var d = ref _allEdges[splittingEdge];
		ref var b = ref _allEdges[AddEdge( splitPos, a.Tangent, bestDist, 1 )];
		ref var c = ref _allEdges[d.PrevEdge];
		ref var e = ref _allEdges[AddEdge( splitPos, d.Tangent, bestDist, -1 )];
		ref var aNext = ref _allEdges[a.NextEdge];
		ref var dNext = ref _allEdges[d.NextEdge];

		var ai = AddVertices( ref a ).Next;
		var fi = AddVertices( ref aNext ).Prev;
		var ci = AddVertices( ref c ).Next;
		var di = AddVertices( ref d );
		var gi = AddVertices( ref dNext ).Prev;

		_activeEdges.Remove( d.Index );
		_activeEdges.Add( b.Index );
		_activeEdges.Add( e.Index );

		ConnectEdges( ref a, ref e );
		ConnectEdges( ref e, ref dNext );

		ConnectEdges( ref c, ref b );
		ConnectEdges( ref b, ref aNext );

		UpdateMaxDistance( ref a, e );
		UpdateMaxDistance( ref e, dNext );
		UpdateMaxDistance( ref dNext, _allEdges[dNext.NextEdge] );

		UpdateMaxDistance( ref c, b );
		UpdateMaxDistance( ref b, aNext );
		UpdateMaxDistance( ref aNext, _allEdges[aNext.NextEdge] );

		var bi = AddVertices( ref b );
		var ei = AddVertices( ref e );

		AddTriangle( ai, bi.Next, fi );
		AddTriangle( ci, bi.Prev, di.Prev );
		AddTriangle( di.Next, ei.Next, gi );

		AddAllPossibleCuts( b.Index );
		AddAllPossibleCuts( dNext.Index );
		AddAllPossibleCuts( e.Index );
		AddAllPossibleCuts( aNext.Index );
	}

	private void Bevel_Close( int closedEdge, Vector2 closePos, float bestDist )
	{
		EnsureCapacity( 1 );

		ref var b = ref _allEdges[closedEdge];
		ref var a = ref _allEdges[b.PrevEdge];
		ref var c = ref _allEdges[b.NextEdge];
		ref var cNext = ref _allEdges[c.NextEdge];
		ref var d = ref _allEdges[AddEdge( closePos, c.Tangent, bestDist )];

		_activeEdges.Remove( b.Index );
		_activeEdges.Remove( c.Index );

		if ( b.PrevEdge == b.NextEdge )
		{
			return;
		}

		_activeEdges.Add( d.Index );

		ConnectEdges( ref a, ref d );
		ConnectEdges( ref d, ref cNext );

		UpdateMaxDistance( ref a, d );
		UpdateMaxDistance( ref d, cNext );
		UpdateMaxDistance( ref cNext, _allEdges[cNext.NextEdge] );

		var ai = AddVertices( ref a );
		var bi = AddVertices( ref b );
		var ci = AddVertices( ref c );
		var ei = AddVertices( ref cNext );
		var di = AddVertices( ref d );

		var fi = _vertices.Count;

		_vertices.Add( new(
			_vertices[di.Prev].Position,
			_vertices[bi.Next].Normal,
			_vertices[bi.Next].Tangent ) );

		AddTriangle( ai.Next, di.Prev, bi.Prev );
		AddTriangle( bi.Next, fi, ci.Prev );
		AddTriangle( ci.Next, di.Next, ei.Prev );

		AddAllPossibleCuts( d.Index );
		AddAllPossibleCuts( cNext.Index );
	}

	private void PostBevel()
	{
		_prevDistance = _nextDistance;
		_prevHeight = _nextHeight;
		_prevAngle = _nextAngle;
	}

	private void AddAllPossibleCuts( int index )
	{
		foreach ( var otherIndex in _activeEdges )
		{
			if ( otherIndex != index )
			{
				PossibleCuts.Add( (index, otherIndex) );
				PossibleCuts.Add( (otherIndex, index) );
			}
		}
	}

	private static Vector3 RotateNormal( Vector3 oldNormal, float sin, float cos )
	{
		var normal2d = new Vector2( oldNormal.x, oldNormal.y );

		if ( normal2d.LengthSquared <= 0.000001f )
		{
			return oldNormal;
		}

		normal2d = normal2d.Normal;

		return new Vector3( normal2d.x * cos, normal2d.y * cos, sin ).Normal;
	}

	private static float GetEpsilon( Vector2 vec, float frac = 0.0001f )
	{
		return Math.Max( Math.Abs( vec.x ), Math.Abs( vec.y ) ) * frac;
	}

	private static float GetEpsilon( Vector2 a, Vector2 b, float frac = 0.0001f )
	{
		return Math.Max( GetEpsilon( a ), GetEpsilon( b ) );
	}

	private static void UpdateMaxDistance( ref Edge edge, in Edge nextEdge )
	{
		if ( edge.NextEdge == edge.PrevEdge )
		{
			edge.MaxDistance = edge.Distance;
			return;
		}

		var baseDistance = Math.Max( edge.Distance, nextEdge.Distance );
		var thisOrigin = edge.Project( baseDistance );
		var nextOrigin = nextEdge.Project( baseDistance );

		var posDist = Vector2.Dot( nextOrigin - thisOrigin, edge.Tangent );

		var dPrev = Vector2.Dot( edge.Velocity, edge.Tangent );
		var dNext = Vector2.Dot( nextEdge.Velocity, edge.Tangent );

		if ( dPrev - dNext <= 0.001f )
		{
			var epsilon = GetEpsilon( thisOrigin, nextOrigin, 0.001f );
			edge.MaxDistance = posDist <= epsilon ? baseDistance : float.PositiveInfinity;
		}
		else
		{
			edge.MaxDistance = baseDistance + MathF.Max( 0f, posDist / (dPrev - dNext) );
		}
	}

	private static void SimpleConnectEdges( ref Edge prev, ref Edge next )
	{
		prev.NextEdge = next.Index;
		next.PrevEdge = prev.Index;
	}

	private static void ConnectEdges( ref Edge prev, ref Edge next )
	{
		SimpleConnectEdges( ref prev, ref next );

		var sum = prev.Normal + next.Normal;
		var sqrMag = sum.LengthSquared;

		if ( sqrMag < 0.001f )
		{
			next.Velocity = Vector2.Zero;
		}
		else
		{
			next.Velocity = 2f * sum / sum.LengthSquared;
		}
	}

	private static float CalculateSplitDistance( in Edge edge, in Edge other, in Edge otherNext,
		out Vector2 splitPos, out bool merge )
	{
		splitPos = default;
		merge = false;

		if ( other.Index == edge.Index || edge.Twin == other.Index || edge.Velocity.LengthSquared <= 0f )
		{
			return float.PositiveInfinity;
		}

		var dv = Vector2.Dot( other.Velocity - edge.Velocity, other.Normal );

		if ( dv <= GetEpsilon( edge.Velocity, other.Velocity ) )
		{
			return float.PositiveInfinity;
		}

		var baseDistance = Math.Max( edge.Distance, Math.Max( other.Distance, otherNext.Distance ) );
		var thisOrigin = edge.Project( baseDistance );
		var edgeOrigin = other.Project( baseDistance );

		var dx = Vector2.Dot( thisOrigin - edgeOrigin, other.Normal );

		if ( dx <= -GetEpsilon( thisOrigin, edgeOrigin ) )
		{
			return float.PositiveInfinity;
		}

		var t = dx / dv;

		if ( t <= -0.0001f )
		{
			return float.PositiveInfinity;
		}

		if ( baseDistance + t >= edge.MaxDistance || baseDistance + t >= other.MaxDistance )
		{
			return float.PositiveInfinity;
		}

		splitPos = thisOrigin + edge.Velocity * t;

		var prevPos = edgeOrigin + other.Velocity * t;
		var nextPos = otherNext.Project( baseDistance + t );

		var dPrev = Vector2.Dot( splitPos - prevPos, other.Tangent );
		var dNext = Vector2.Dot( splitPos - nextPos, other.Tangent );

		var epsilon = GetEpsilon( prevPos, nextPos );

		if ( dPrev <= -epsilon || dNext >= -epsilon )
		{
			return float.PositiveInfinity;
		}

		if ( dPrev <= epsilon )
		{
			if ( edge.NextEdge == other.Index || edge.PrevEdge == other.Index )
			{
				return float.PositiveInfinity;
			}

			merge = true;
		}

		return baseDistance + Math.Max( 0f, t );
	}
}