Effigy/PolyMesh.cs
using System;
using System.Collections.Generic;
using System.Linq;

namespace Effigy;

/// <summary>
/// An undirected edge, identified by its two endpoints regardless of which way round they were
/// given. Two faces sharing an edge traverse it in opposite directions, so the key has to ignore
/// direction or every shared edge looks like two different edges.
/// </summary>
public readonly struct EdgeKey : IEquatable<EdgeKey>
{
	public readonly int A, B;

	public EdgeKey( int a, int b )
	{
		// Sorted, so (3,7) and (7,3) are the same key.
		A = Math.Min( a, b );
		B = Math.Max( a, b );
	}

	public bool Equals( EdgeKey o ) => A == o.A && B == o.B;
	public override bool Equals( object obj ) => obj is EdgeKey e && Equals( e );
	/// <summary>
	/// The two endpoints packed into a long and mixed by Fibonacci hashing.
	///
	/// This was HashCode.Combine, which is a general-purpose hash doing several rounds of xxHash
	/// over a randomised seed. It is a good hash and it is not cheap, and an edge key is hashed
	/// once per corner of the mesh by every routine that builds edge adjacency - about eight
	/// million times over one SurfaceIndex build on a dense import.
	///
	/// A and B are vertex indices, so they are small non-negative ints with no structure worth
	/// defending against; packing them into the two halves of a long and multiplying by the
	/// golden-ratio constant spreads them across the whole word, which is all a Dictionary bucket
	/// index needs. Taking the HIGH bits is the point of the multiply - the low bits of a product
	/// are barely mixed at all.
	/// </summary>
	public override int GetHashCode()
	{
		var packed = ((ulong)(uint)A << 32) | (uint)B;
		return (int)((packed * 11400714819323198485UL) >> 33);
	}
	public override string ToString() => $"[{A}-{B}]";
}

/// <summary>
/// One n-gon. Indices reference PolyMesh.Positions; UVs are stored PER CORNER rather than per
/// vertex.
///
/// Per-corner UVs are what make seams possible: a cube corner belongs to three faces which each
/// want a different UV for the same position. Storing UVs on the vertex would force one value and
/// smear the texture across the seam. It also makes UV subdivision purely local — see
/// CatmullClark.
/// </summary>
public sealed class Face
{
	public int[] Indices;
	public Vec2[] UVs;

	/// <summary>Material slot. Export groups faces by this, so a model can carry several materials
	/// without the kernel knowing what a material is.</summary>
	public int Material;

	public Face( int[] indices, Vec2[] uvs = null, int material = 0 )
	{
		Indices = indices;
		UVs = uvs ?? new Vec2[indices.Length];
		Material = material;

		if ( UVs.Length != Indices.Length )
			throw new ArgumentException( $"Face has {Indices.Length} corners but {UVs.Length} UVs" );
	}

	public int Count => Indices.Length;
}

/// <summary>
/// A polygon mesh of n-gons. Not a half-edge structure — adjacency is derived on demand instead.
///
/// WHY NOT HALF-EDGE: Catmull-Clark needs edge→faces and vertex→(faces, edges), and nothing else
/// in phase one needs more than that. Deriving those three maps from an indexed face list is a
/// few dozen lines and is hard to get subtly wrong; a half-edge structure is a few hundred and is
/// easy to corrupt during editing. If interactive per-element editing arrives later and profiling
/// says the rebuild hurts, that is the moment to switch, not before.
///
/// QUADS ARE THE POINT. Catmull-Clark degrades badly on triangle soup, so every primitive here is
/// built quad-dominant on purpose. See Primitives.
/// </summary>
public sealed class PolyMesh
{
	public List<Vec3> Positions = new();
	public List<Face> Faces = new();

	/// <summary>
	/// Bone influences, parallel to Positions. Null until something rigs the mesh, which is the
	/// normal state for a prop.
	///
	/// It lives on the mesh rather than beside it because everything that rebuilds a vertex list —
	/// Clone, Append, Catmull-Clark — has to rebuild the weights the same way. Kept alongside, it
	/// would be silently dropped by whichever of those somebody forgot to update, and the symptom
	/// would be a model that only loses its rig after a subdivide.
	/// </summary>
	public SkinWeights Skin;

	public bool IsRigged => Skin is not null && Skin.Count == Positions.Count;

	/// <summary>
	/// Per-vertex colour, parallel to <see cref="Positions"/>, in straight (non-premultiplied) RGBA
	/// 0..1. Null unless a caller fills it in.
	///
	/// NOTHING IN THE TOOL WRITES THIS ANY MORE, and that is deliberate rather than an oversight.
	/// This was where paint lived: per-vertex colour, composited by the material's own multiply. It
	/// was only ever as fine as the mesh — a bare box has eight vertices and all of them are corners
	/// — and the shader everything renders with turned out not to read the COLOR stream at all, so
	/// the resolution problem was academic next to the paint being invisible. Paint is a texture now;
	/// see <see cref="Paint"/>.
	///
	/// IT STAYS BECAUSE THE WRITERS TAKE IT. <c>DmxWriter</c> and <c>ObjWriter</c> both emit
	/// per-vertex colour for a mesh that carries it, <c>MeshTransform</c> preserves it across a
	/// merge, and DmxGrammarTests pins the DMX spelling — that is a capability of the mesh writers,
	/// held by a test, and it is independent of how paint happens to be stored this month. What it is
	/// NOT any more is a thing the tool produces: no feature, no brush and no rebuild fills this in,
	/// so any code branching on <see cref="HasVertexColors"/> to decide what a PART looks like is
	/// branching on something that never happens.
	/// </summary>
	public Vec4[] VertexColors;

	public bool HasVertexColors => VertexColors is not null && VertexColors.Length == Positions.Count;

	/// <summary>
	/// The paint atlas a <see cref="PaintFeature"/> replayed onto this body, or null when it
	/// carries none.
	///
	/// WHY IT RIDES THE MESH, the same reason <see cref="VertexColors"/> did before it: the exporters
	/// and the preview read the merged mesh, not the feature tree, so paint has to travel with the
	/// mesh to reach them. It is a derived artifact — the feature replays its strokes onto it every
	/// rebuild — and it is keyed to THIS body's UV layout, which is why a merged mesh can only carry
	/// one body's atlas; the common case is one painted body, and that is the case the merge preserves.
	/// </summary>
	public PaintCanvas Paint;

	public bool HasPaint => Paint is not null;

	public PolyMesh() { }

	public PolyMesh( IEnumerable<Vec3> positions, IEnumerable<Face> faces )
	{
		Positions = positions.ToList();
		Faces = faces.ToList();
	}

	public int VertexCount => Positions.Count;
	public int FaceCount => Faces.Count;

	public int AddVertex( Vec3 p )
	{
		Positions.Add( p );
		return Positions.Count - 1;
	}

	public Face AddFace( int[] indices, Vec2[] uvs = null, int material = 0 )
	{
		var f = new Face( indices, uvs, material );
		Faces.Add( f );
		return f;
	}

	/// <summary>Every distinct undirected edge, with the faces using it. A closed manifold has
	/// exactly two faces per edge; a boundary edge has one; anything else is non-manifold.</summary>
	public Dictionary<EdgeKey, List<int>> BuildEdgeFaces()
	{
		// Presized on Euler's relation: a closed quad mesh has about twice as many edges as faces,
		// a triangle mesh about one and a half times. Guessing high costs one oversized bucket
		// array; guessing low costs a rehash of every entry at each doubling, which on a dense
		// mesh is most of this function's time and all of its garbage.
		var map = new Dictionary<EdgeKey, List<int>>( Faces.Count * 2 );

		for ( var fi = 0; fi < Faces.Count; fi++ )
		{
			var indices = Faces[fi].Indices;
			var n = indices.Length;

			for ( var i = 0; i < n; i++ )
			{
				// i + 1 == n rather than a modulo: this runs once per corner of the mesh, and a
				// division is not free at that count.
				var key = new EdgeKey( indices[i], indices[i + 1 == n ? 0 : i + 1] );

				if ( !map.TryGetValue( key, out var list ) )
					// Capacity two, because a manifold edge has exactly two faces. The default
					// List starts at four on first Add, so this halves the backing arrays.
					map[key] = list = new List<int>( 2 );

				list.Add( fi );
			}
		}

		return map;
	}

	/// <summary>Faces touching each vertex, indexed by vertex.</summary>
	public List<int>[] BuildVertexFaces()
	{
		// Built through the CSR map rather than directly, so the counting happens once and every
		// list here is allocated at exactly its final size. The direct version grew each list from
		// empty - four, eight, sixteen - and threw away every intermediate array, which on a dense
		// mesh was some ninety megabytes of garbage to produce four megabytes of answer. It also
		// spent a LINQ Distinct() per face on the repeated-vertex guard; VertexFaces.Build does
		// that with a scan of the three or four corners instead.
		var csr = VertexFaces.Build( this );
		var map = new List<int>[Positions.Count];

		for ( var v = 0; v < map.Length; v++ )
		{
			var start = csr.Offsets[v];
			var count = csr.Offsets[v + 1] - start;
			var list = new List<int>( count );

			for ( var i = 0; i < count; i++ )
				list.Add( csr.Items[start + i] );

			map[v] = list;
		}

		return map;
	}

	/// <summary>Edges touching each vertex, indexed by vertex.</summary>
	public List<EdgeKey>[] BuildVertexEdges()
	{
		// DEDUPED PER VERTEX, NOT GLOBALLY.
		//
		// This used to hold one HashSet<(int, EdgeKey)> across the whole mesh and hash a 12-byte
		// tuple twice per corner - some two million hashes on a dense body, into a set that grew
		// to two million entries. The duplicate an edge arrives as is always the SECOND face
		// sharing it, so the only list it can already be in is the one being appended to, and that
		// list is the vertex's valence long: four entries, scanned linearly, no hashing anywhere.
		//
		// Quadratic in valence, which is the right trade - valence is four on a quad mesh and a
		// pole with fifty edges is still fifty times fifty against a hash of every corner.
		var map = new List<EdgeKey>[Positions.Count];

		for ( var i = 0; i < map.Length; i++ )
			map[i] = new List<EdgeKey>( 4 );

		foreach ( var f in Faces )
		{
			var indices = f.Indices;
			var n = indices.Length;

			for ( var i = 0; i < n; i++ )
			{
				var a = indices[i];
				var b = indices[i + 1 == n ? 0 : i + 1];
				var key = new EdgeKey( a, b );

				AddOnce( map[a], key );
				AddOnce( map[b], key );
			}
		}

		return map;

		static void AddOnce( List<EdgeKey> list, EdgeKey key )
		{
			for ( var i = 0; i < list.Count; i++ )
			{
				if ( list[i].Equals( key ) )
					return;
			}

			list.Add( key );
		}
	}

	/// <summary>Centroid of a face's corners.</summary>
	public Vec3 FaceCentroid( Face f )
	{
		var sum = Vec3.Zero;

		foreach ( var i in f.Indices )
			sum += Positions[i];

		return sum / f.Count;
	}

	/// <summary>Newell's method, which is correct for non-planar n-gons where a simple
	/// cross-product of the first three corners is not.</summary>
	public Vec3 FaceNormal( Face f )
	{
		var n = Vec3.Zero;

		for ( var i = 0; i < f.Count; i++ )
		{
			var a = Positions[f.Indices[i]];
			var b = Positions[f.Indices[(i + 1) % f.Count]];

			n += new Vec3(
				(a.y - b.y) * (a.z + b.z),
				(a.z - b.z) * (a.x + b.x),
				(a.x - b.x) * (a.y + b.y) );
		}

		return n.Normal;
	}

	/// <summary>Area-weighted vertex normals. Area weighting rather than a plain average, so a
	/// vertex surrounded by one big face and several slivers leans toward the big one.</summary>
	public Vec3[] ComputeVertexNormals()
	{
		var normals = new Vec3[Positions.Count];

		foreach ( var f in Faces )
		{
			var n = FaceNormal( f );
			var weight = FaceArea( f );

			foreach ( var i in f.Indices )
				normals[i] += n * weight;
		}

		for ( var i = 0; i < normals.Length; i++ )
			normals[i] = normals[i].Normal;

		return normals;
	}

	/// <summary>
	/// Fan triangulation about the centroid, PROJECTED ONTO THE FACE'S OWN NORMAL so the fan's
	/// backward triangles subtract instead of adding.
	///
	/// It used to sum |cross| and so measured a convex face exactly and a CONCAVE one too big -
	/// every fan triangle that wound backwards was counted as material rather than as the notch it
	/// stands for. That was true of nothing this kernel made until a cap with a hole in it started
	/// coming back as two n-gons, each of which wraps around the hole and is concave by
	/// construction. A washer then measured a volume larger than its own solid, which is the sort
	/// of thing that reads as "the cap is broken" when the cap is fine and the ruler is not.
	///
	/// Exact for any planar polygon, concave or not, and identical to the old sum for a convex one:
	/// every fan triangle winds the same way there. For a mildly non-planar face it is the area
	/// projected onto the Newell normal, which is the honest generalisation and is what the
	/// area-weighted vertex normals wanted from it anyway.
	/// </summary>
	public float FaceArea( Face f ) => FaceArea( f, FaceNormal( f ) );

	/// <summary>
	/// <see cref="FaceArea(Face)"/> for a caller that already has the face's Newell normal.
	///
	/// Every caller that wants a face's area wants its normal too - the area is only meaningful as
	/// the area projected onto that normal - so the one-argument form recomputed a normal the
	/// caller was holding. Area-weighted normals over a dense mesh did that once per face and it
	/// was the single biggest cost in the viewport's rebuild.
	/// </summary>
	public float FaceArea( Face f, Vec3 normal )
	{
		var c = FaceCentroid( f );
		var area = 0f;

		for ( var i = 0; i < f.Count; i++ )
		{
			var a = Positions[f.Indices[i]] - c;
			var b = Positions[f.Indices[(i + 1) % f.Count]] - c;
			area += Vec3.Dot( Vec3.Cross( a, b ), normal ) * 0.5f;
		}

		return MathF.Abs( area );
	}

	/// <summary>
	/// Enclosed volume with sign. Positive when face windings put normals outward, negative when
	/// the solid is inside-out.
	///
	/// Divergence theorem: sum over faces of (centroid · normal) * area equals three times the
	/// enclosed volume. A mesh can be closed, manifold, Euler-correct and still inverted — this
	/// is the quantity that sees it. Used as a refusal, so it lives on the mesh rather than as a
	/// private copy in every test file.
	/// </summary>
	public float SignedVolume()
	{
		var acc = 0f;

		foreach ( var f in Faces )
			acc += Vec3.Dot( FaceCentroid( f ), FaceNormal( f ) ) * FaceArea( f );

		return acc / 3f;
	}

	/// <summary>
	/// Diagonal of the axis-aligned bounds - this tool's one answer to "how big is this model".
	///
	/// Effigy's units are dimensionless: a default primitive is one unit across and a room is
	/// hundreds, so anything with a distance in it needs a default that SCALES rather than a
	/// constant. The bake's search range, reprojection's, and the sculpt brush's starting radius are
	/// all a fraction of this, and they were three copies of the same loop until they were not.
	/// </summary>
	public float BoundsDiagonal
	{
		get
		{
			if ( Positions.Count == 0 )
				return 0f;

			var min = Positions[0];
			var max = Positions[0];

			foreach ( var p in Positions )
			{
				min = new Vec3( MathF.Min( min.x, p.x ), MathF.Min( min.y, p.y ), MathF.Min( min.z, p.z ) );
				max = new Vec3( MathF.Max( max.x, p.x ), MathF.Max( max.y, p.y ), MathF.Max( max.z, p.z ) );
			}

			return (max - min).Length;
		}
	}

	public PolyMesh Clone()
	{
		var m = new PolyMesh { Positions = new List<Vec3>( Positions ), Skin = Skin?.Clone() };

		if ( VertexColors is not null )
			m.VertexColors = (Vec4[])VertexColors.Clone();

		if ( Paint is not null )
			m.Paint = Paint.Clone();

		foreach ( var f in Faces )
			m.Faces.Add( new Face( (int[])f.Indices.Clone(), (Vec2[])f.UVs.Clone(), f.Material ) );

		return m;
	}
}

/// <summary>What Validate found. Kept as data rather than thrown, because a mesh mid-edit is
/// allowed to be briefly invalid and the caller decides whether it matters.</summary>
public sealed class MeshValidation
{
	public List<string> Errors = new();
	public int BoundaryEdges;
	public int NonManifoldEdges;
	public bool IsClosed => BoundaryEdges == 0 && NonManifoldEdges == 0;
	public bool IsValid => Errors.Count == 0;

	public override string ToString() =>
		IsValid
			? $"valid, {(IsClosed ? "closed" : $"{BoundaryEdges} boundary edges")}"
			: string.Join( "; ", Errors );
}

public static class MeshValidator
{
	public static MeshValidation Validate( PolyMesh mesh )
	{
		var r = new MeshValidation();

		for ( var fi = 0; fi < mesh.Faces.Count; fi++ )
		{
			var f = mesh.Faces[fi];

			if ( f.Count < 3 )
				r.Errors.Add( $"face {fi} has {f.Count} corners" );

			if ( f.UVs.Length != f.Count )
				r.Errors.Add( $"face {fi} has {f.Count} corners but {f.UVs.Length} UVs" );

			foreach ( var i in f.Indices )
			{
				if ( i < 0 || i >= mesh.Positions.Count )
					r.Errors.Add( $"face {fi} references vertex {i}, out of range" );
			}

			// A scan rather than Distinct().Count(): this runs over every face of the mesh, and
			// the LINQ form allocates an enumerator and a set per face to compare three or four
			// integers.
			if ( RepeatsAVertex( f.Indices ) )
				r.Errors.Add( $"face {fi} repeats a vertex" );
		}

		foreach ( var (key, faces) in mesh.BuildEdgeFaces() )
		{
			if ( faces.Count == 1 ) r.BoundaryEdges++;
			else if ( faces.Count > 2 )
			{
				r.NonManifoldEdges++;
				r.Errors.Add( $"edge {key} is shared by {faces.Count} faces" );
			}
		}

		return r;
	}

	static bool RepeatsAVertex( int[] indices )
	{
		for ( var i = 1; i < indices.Length; i++ )
		{
			for ( var j = 0; j < i; j++ )
			{
				if ( indices[i] == indices[j] )
					return true;
			}
		}

		return false;
	}

	/// <summary>V - E + F. Equals 2 for a closed genus-0 surface, and is the cheapest single check
	/// that a subdivision pass did not quietly corrupt the topology.</summary>
	public static int EulerCharacteristic( PolyMesh mesh ) =>
		mesh.VertexCount - mesh.BuildEdgeFaces().Count + mesh.FaceCount;
}