Code/PolygonMeshBuilder.cs
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.CompilerServices;
using System.Runtime.InteropServices;

namespace Sandbox.Polygons;

/// <summary>
/// Helper class for building 3D meshes based on a 2D polygon. Supports
/// concave polygons with holes, although edges must not intersect.
/// </summary>
public partial class PolygonMeshBuilder : Pooled<PolygonMeshBuilder>
{
	public record struct Vertex( Vector3 Position, Vector3 Normal, Vector4 Tangent )
	{
		public static VertexAttribute[] Layout { get; } = new[]
		{
			new VertexAttribute( VertexAttributeType.Position, VertexAttributeFormat.Float32 ),
			new VertexAttribute( VertexAttributeType.Normal, VertexAttributeFormat.Float32 ),
			new VertexAttribute( VertexAttributeType.Tangent, VertexAttributeFormat.Float32, 4 )
		};
	}

	private int _nextEdgeIndex;
	private Edge[] _allEdges = new Edge[64];
	private readonly HashSet<int> _activeEdges = new ();

	private readonly List<Vertex> _vertices = new ();
	private readonly List<int> _indices = new ();

	private float _prevDistance;
	private float _nextDistance;

	private float _invDistance;

	private float _prevHeight;
	private float _nextHeight;

	private float _prevAngle;
	private float _nextAngle;

	private float _minSmoothNormalDot;

	private bool _validated;

	/// <summary>
	/// Number of edges that will be affected by calls to methods like <see cref="Bevel"/>, <see cref="Round"/>, and <see cref="Close"/>.
	/// </summary>
	public int ActiveEdgeCount => _activeEdges.Count;

	/// <summary>
	/// If true, no active edges remain because the mesh is fully closed.
	/// </summary>
	public bool IsClosed => _activeEdges.Count == 0;

	/// <summary>
	/// Corners of the original polygon with an interior or exterior
	/// angle less than this (in radians) will have smooth normals.
	/// </summary>
	public float MaxSmoothAngle { get; set; } = 0f;

	/// <summary>
	/// If true, don't bother generating normals / tangents.
	/// </summary>
	public bool SkipNormals { get; set; }

	/// <summary>
	/// Positions of each vertex in the generated mesh.
	/// </summary>
	public IEnumerable<Vector3> Positions => _vertices.Select( x => x.Position );

	/// <summary>
	/// Normals of each vertex in the generated mesh.
	/// </summary>
	public IEnumerable<Vector3> Normals => _vertices.Select( x => x.Normal );

	/// <summary>
	/// U-tangents, and the signs of the V-tangents, of each vertex in the generated mesh.
	/// </summary>
	public IEnumerable<Vector4> Tangents => _vertices.Select( x => x.Tangent );

	/// <summary>
	/// Positions, normals, and tangents of each vertex.
	/// </summary>
	public List<Vertex> Vertices => _vertices;

	/// <summary>
	/// Indices of vertices describing the triangulation of the generated mesh.
	/// </summary>
	public List<int> Indices => _indices;

	/// <summary>
	/// Clear all geometry from this builder.
	/// </summary>
	public PolygonMeshBuilder Clear()
	{
		_nextEdgeIndex = 0;
		_activeEdges.Clear();

		_vertices.Clear();
		_indices.Clear();

		_prevDistance = 0f;
		_nextDistance = 0f;

		_invDistance = 0f;

		_prevHeight = 0f;
		_nextHeight = 0f;

		_prevAngle = 0f;
		_nextAngle = 0f;

		_minSmoothNormalDot = 0f;

		_validated = true;

		return this;
	}

	/// <summary>
	/// Reset this builder to be like a new instance.
	/// </summary>
	public override void Reset()
	{
		Clear();

		MaxSmoothAngle = 0f;
		SkipNormals = false;
	}

	private static int NextPowerOfTwo( int value )
	{
		var po2 = 1;
		while ( po2 < value )
		{
			po2 <<= 1;
		}

		return po2;
	}

	private void EnsureCapacity( int toAdd )
	{
		if ( _nextEdgeIndex + toAdd > _allEdges.Length )
		{
			Array.Resize( ref _allEdges, NextPowerOfTwo( _nextEdgeIndex + toAdd ) );
		}
	}

	private int AddEdge( Vector2 origin, Vector2 tangent, float distance, int? twinOffset = null )
	{
		var edge = new Edge( _nextEdgeIndex, origin, tangent, distance, twinOffset != null ? _nextEdgeIndex + twinOffset.Value : -1 );
		_allEdges[edge.Index] = edge;
		++_nextEdgeIndex;
		return edge.Index;
	}

	private void Invalidate()
	{
		_validated = false;
	}

	/// <summary>
	/// Add a set of active edges forming a loop. Clockwise loops will be a solid polygon, and count-clockwise
	/// will form a hole. Holes must be inside of solid polygons, otherwise the mesh can't be closed correctly.
	/// </summary>
	/// <param name="vertices">List of vertices to read a range from.</param>
	/// <param name="offset">Index of the first vertex in the loop.</param>
	/// <param name="count">Number of vertices in the loop.</param>
	/// <param name="reverse">If true, reverse the order of the vertices in the loop.</param>
	public PolygonMeshBuilder AddEdgeLoop( IReadOnlyList<Vector2> vertices, int offset, int count, bool reverse = false )
	{
		return AddEdgeLoop( vertices, offset, count, Vector2.Zero, Vector2.One, reverse );
	}

	public PolygonMeshBuilder AddEdgeLoop( IReadOnlyList<Vector2> vertices, int offset, int count, Vector2 position, Vector2 scale, bool reverse = false )
	{
		var firstIndex = _nextEdgeIndex;

		EnsureCapacity( count );
		Invalidate();

        var prevVertex = position + vertices[offset + count - 1] * scale;
		for ( var i = 0; i < count; ++i )
		{
			var nextVertex = position + vertices[offset + i] * scale;

			_activeEdges.Add( AddEdge( prevVertex, Helpers.NormalizeSafe( nextVertex - prevVertex ), _prevDistance ) );

			prevVertex = nextVertex;
		}

		var prevIndex = count - 1;
		for ( var i = 0; i < count; ++i )
		{
			ref var prevEdge = ref _allEdges[firstIndex + prevIndex];
			ref var nextEdge = ref _allEdges[firstIndex + i];

			if ( reverse )
			{
				ConnectEdges( ref nextEdge, ref prevEdge );
			}
			else
			{
				ConnectEdges( ref prevEdge, ref nextEdge );
			}

			prevIndex = i;
		}

		return this;
	}

	[ThreadStatic]
	private static Dictionary<int, int> AddEdges_VertexMap;

	/// <summary>
	/// Add a raw set of edges. Be careful to ensure that each loop of edges is fully closed.
	/// </summary>
	/// <param name="vertices">Positions of vertices to connect with edges.</param>
	/// <param name="edges">Indices of the start and end vertices of each edge.</param>
	public void AddEdges( IReadOnlyList<Vector2> vertices, IReadOnlyList<(int Prev, int Next)> edges )
	{
		AddEdges_VertexMap ??= new Dictionary<int, int>();
		AddEdges_VertexMap.Clear();

		EnsureCapacity( edges.Count );
		Invalidate();

        foreach ( var (i, j) in edges )
		{
			var prev = vertices[i];
			var next = vertices[j];

			var index = AddEdge( prev, Helpers.NormalizeSafe( next - prev ), _prevDistance );

			_activeEdges.Add( index );
			AddEdges_VertexMap.Add( i, index );
		}

		for ( var i = 0; i < edges.Count; ++i )
		{
			var edge = edges[i];

			ref var prev = ref _allEdges[AddEdges_VertexMap[edge.Prev]];
			ref var next = ref _allEdges[AddEdges_VertexMap[edge.Next]];

			ConnectEdges( ref prev, ref next );
		}
	}

	private static float LerpRadians( float a, float b, float t )
	{
		var delta = b - a;
		delta -= MathF.Floor( delta * (0.5f / MathF.PI) ) * MathF.PI * 2f;

		if ( delta > MathF.PI )
		{
			delta -= MathF.PI * 2f;
		}

		return a + delta * Math.Clamp( t, 0f, 1f );
	}

	private Vector4 GetTangent( Vector3 normal )
	{
		var tangent = Vector3.Cross( normal, new Vector3( 0f, 0f, 1f ) ).Normal;

		return new Vector4( tangent, 1f );
	}

	private (int Prev, int Next) AddVertices( ref Edge edge, bool forceMaxDistance = false )
	{
		if ( edge.Vertices.Prev > -1 )
		{
			return edge.Vertices;
		}

		var prevEdge = _allEdges[edge.PrevEdge];

		var index = _vertices.Count;
		var prevNormal = -prevEdge.Normal;
		var nextNormal = -edge.Normal;

		var t = forceMaxDistance ? 1f : (edge.Distance - _prevDistance) * _invDistance;
		var height = _prevHeight + t * (_nextHeight - _prevHeight);

		var pos = new Vector3( edge.Origin.x, edge.Origin.y, height );

		if ( SkipNormals || MathF.Abs( _nextHeight - _prevHeight ) <= 0.001f )
		{
			_vertices.Add( new(
				pos,
				new Vector3( 0f, 0f, 1f ),
				new Vector4( 1f, 0f, 0f, 1f ) ) );

			edge.Vertices = (index, index);
		}
		else
		{
			var angle = LerpRadians( _prevAngle, _nextAngle, t );
			var cos = MathF.Cos( angle );
			var sin = MathF.Sin( angle );

			if ( Vector2.Dot( prevNormal, nextNormal ) >= _minSmoothNormalDot )
			{
				var normal = new Vector3( (prevNormal.x + nextNormal.x) * cos, (prevNormal.y + nextNormal.y) * cos, sin * 2f ).Normal;

				_vertices.Add( new( pos, normal, GetTangent( normal ) ) );

				edge.Vertices = (index, index);
			}
			else
			{
				var normal0 = new Vector3( prevNormal.x * cos, prevNormal.y * cos, sin ).Normal;
				var normal1 = new Vector3( nextNormal.x * cos, nextNormal.y * cos, sin ).Normal;

				_vertices.Add( new( pos, normal0, GetTangent( normal0 ) ) );
				_vertices.Add( new( pos, normal1, GetTangent( normal1 ) ) );

				edge.Vertices = (index, index + 1);
			}
		}

		return edge.Vertices;
	}

	private void AddTriangle( int a, int b, int c )
	{
		_indices.Add( a );
		_indices.Add( b );
		_indices.Add( c );
	}

	/// <summary>
	/// Add faces on each active edge extending upwards by the given height.
	/// </summary>
	/// <param name="height">Total distance upwards, away from the plane of the polygon.</param>
	public PolygonMeshBuilder Extrude( float height )
	{
		return Bevel( 0f, height );
	}

	/// <summary>
	/// Add faces on each active edge extending inwards by the given width. This will close the mesh if <paramref name="width"/> is large enough.
	/// </summary>
	/// <param name="width">Total distance inwards.</param>
	public PolygonMeshBuilder Inset( float width )
	{
		return Bevel( width, 0f );
	}

	[ThreadStatic]
	private static Dictionary<int, int> Mirror_IndexMap;

	/// <summary>
	/// Mirrors all previously created faces. The mirror plane is normal to the Z axis, with a given distance from the origin.
	/// </summary>
	/// <param name="z">Distance of the mirror plane from the origin.</param>
	public PolygonMeshBuilder Mirror( float z = 0f )
	{
		Mirror_IndexMap ??= new Dictionary<int, int>();
		Mirror_IndexMap.Clear();

		_vertices.EnsureCapacity( _vertices.Count * 2 );
		_indices.EnsureCapacity( _indices.Count * 2 );

		var indexCount = _indices.Count;
		var vertexCount = _vertices.Count;

		for ( var i = 0; i < vertexCount; i++ )
		{
			var vertex = _vertices[i];
			var position = vertex.Position;
			var normal = vertex.Normal;
			var tangent = vertex.Tangent;

			if ( Math.Abs( position.z - z ) <= 0.001f && (SkipNormals || Math.Abs( normal.z ) <= 0.0001f && Math.Abs( tangent.z ) <= 0.0001f) )
			{
				Mirror_IndexMap.Add( i, i );
			}
			else
			{
				Mirror_IndexMap.Add( i, _vertices.Count );

				_vertices.Add( new(
					new Vector3( position.x, position.y, z * 2f - position.z ),
					new Vector3( normal.x, normal.y, -normal.z ),
					new Vector4( tangent.x, tangent.y, -tangent.z, tangent.w ) ) );
			}
		}

		for ( var i = 0; i < indexCount; i += 3 )
		{
			var a = Mirror_IndexMap[_indices[i + 0]];
			var b = Mirror_IndexMap[_indices[i + 1]];
			var c = Mirror_IndexMap[_indices[i + 2]];

			_indices.Add( a );
			_indices.Add( c );
			_indices.Add( b );
		}

		return this;
	}

	/// <summary>
	/// Perform successive <see cref="Bevel"/>s so that the edge of the polygon curves inwards in a quarter circle arc.
	/// </summary>
	/// <param name="radius">Radius of the arc.</param>
	/// <param name="faces">How many bevels to split the rounded edge into.</param>
	/// <param name="smooth">If true, use smooth normals rather than flat shading.</param>
	/// <param name="convex">If true, the faces will be pointing outwards from the center of the arc.</param>
	public PolygonMeshBuilder Arc( float radius, int faces, bool smooth = true, bool convex = true )
	{
		return Arc( radius, radius, faces, smooth, convex );
	}

	/// <summary>
	/// Perform successive <see cref="Bevel"/>s so that the edge of the polygon curves inwards in a quarter circle arc.
	/// </summary>
	/// <param name="width">Total distance inwards.</param>
	/// <param name="height">Total distance upwards, away from the plane of the polygon.</param>
	/// <param name="faces">How many bevels to split the rounded edge into.</param>
	/// <param name="smooth">If true, use smooth normals rather than flat shading.</param>
	/// <param name="convex">If true, the faces will be pointing outwards from the center of the arc.</param>
	public PolygonMeshBuilder Arc( float width, float height, int faces, bool smooth = true, bool convex = true )
	{
		var prevWidth = 0f;
		var prevHeight = 0f;
		var prevTheta = 0f;

		static float MapAngle( float theta, bool convex, bool positive )
		{
			var min = positive ? 0f : MathF.PI * 0.5f;
			return convex ? min + theta : min + MathF.PI * 0.5f - theta;
		}

		for ( var i = 0; i < faces; ++i )
		{
			var theta = MathF.PI * 0.5f * (i + 1f) / faces;

			var cos = MathF.Cos( theta );
			var sin = MathF.Sin( theta );

			var nextWidth = 1f - cos;
			var nextHeight = sin;

			if ( smooth )
			{
				if ( height >= 0f == convex )
				{
					Bevel( (nextWidth - prevWidth) * width,
						(nextHeight - prevHeight) * height,
						MapAngle( prevTheta, convex, height >= 0f ),
						MapAngle( theta, convex, height >= 0f ) );
				}
				else
				{
					Bevel( (nextHeight - prevHeight) * width,
						(nextWidth - prevWidth) * height,
						MapAngle( prevTheta, convex, height >= 0f ),
						MapAngle( theta, convex, height >= 0f ) );
				}
			}
			else
			{
				if ( height >= 0f == convex )
				{
					Bevel( (nextWidth - prevWidth) * width,
						(nextHeight - prevHeight) * height );
				}
				else
				{
					Bevel( (nextHeight - prevHeight) * width,
						(nextWidth - prevWidth) * height );
				}
			}

			prevWidth = nextWidth;
			prevHeight = nextHeight;
			prevTheta = theta;
		}

		return this;
	}
}