Editor/Effigy/MeshSplit.cs
using System;
using System.Collections.Generic;

namespace Effigy;

/// <summary>
/// Take a mesh apart into the separate solids it actually contains.
///
/// WHY THIS EXISTS. A cut is allowed to sever a part. Drill a slot all the way across a bar and
/// what comes back from the boolean is one PolyMesh holding two blocks that touch nowhere — and
/// every consumer of that mesh treats it as one body, because nothing ever asked. The symptoms are
/// all downstream and none of them look like the cut: the Parts list shows one part where the
/// screen shows two, hiding one hides both, a per-body material paints both, the collision builder
/// wraps one convex hull around the pair and fills in the gap the cut just made, and a mass or
/// volume readout is the sum of two things the user thinks of as separate.
///
/// It is not the boolean's fault and it is not fixable there: "one mesh" is a perfectly good answer
/// to "subtract this", and the question of how many SOLIDS that is belongs to whoever holds the
/// part list. So this is the answer to that question, asked after every cut.
///
/// CONNECTED MEANS SHARING A VERTEX, not sharing an edge, and that is the conservative choice on
/// purpose. Two blocks joined at a single corner are one solid by the shared-vertex rule and two by
/// the shared-edge rule, and the shared-vertex rule is the one that can only ever split things that
/// are genuinely apart. Splitting a part someone thinks of as one part is the failure that matters
/// here: it renames bodies, and every sketch and picked face hanging off the second one moves to an
/// id that did not exist before. Failing to split, by contrast, is exactly the behaviour that
/// shipped before this file existed.
///
/// THE ORDER IS A PROMISE. Pieces come back largest-volume first, ties broken on the minimum corner
/// in x, then y, then z. Every rebuild runs the same features over the same numbers, so the pieces
/// have to come out in the same order or the ids assigned to them shuffle between rebuilds — which
/// is the same silent reattachment FeatureContext.NewBodyId was written to prevent, arriving from
/// the mesh side. Volume alone is not enough: a symmetric part cut down the middle gives two pieces
/// of identical volume, and float addition over two different face orders does not reliably give
/// identical floats, so the corner is what settles it.
/// </summary>
public static class MeshSplit
{
	/// <summary>
	/// The mesh's connected pieces, in the order described above. A mesh that is already one solid
	/// comes back as a single-element list holding a CLONE — never the input — so a caller can
	/// treat one piece and several the same way without wondering who owns what.
	/// </summary>
	public static List<PolyMesh> ConnectedPieces( PolyMesh mesh )
	{
		var pieces = new List<PolyMesh>();

		if ( mesh is null || mesh.FaceCount == 0 )
			return pieces;

		var groups = FaceGroups( mesh );

		foreach ( var group in groups )
			pieces.Add( Extract( mesh, group ) );

		pieces.Sort( Compare );

		return pieces;
	}

	/// <summary>
	/// Whether this mesh is more than one solid, without paying for the extraction.
	///
	/// The common case by a wide margin is one piece, and a caller that only wants to know whether
	/// anything changed should not have to rebuild the mesh to find out.
	/// </summary>
	public static int PieceCount( PolyMesh mesh ) =>
		mesh is null || mesh.FaceCount == 0 ? 0 : FaceGroups( mesh ).Count;

	/// <summary>
	/// Union-find over faces, joined through the vertices they share.
	///
	/// Vertices rather than edges — see the class comment. Positions are compared by INDEX and not
	/// by value: two coincident vertices that no face lists in common are two vertices, and welding
	/// them here would be a topological edit this was not asked to make.
	/// </summary>
	static List<List<int>> FaceGroups( PolyMesh mesh )
	{
		var parent = new int[mesh.FaceCount];

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

		var firstFaceAt = new Dictionary<int, int>();

		for ( var fi = 0; fi < mesh.FaceCount; fi++ )
		{
			foreach ( var vi in mesh.Faces[fi].Indices )
			{
				if ( firstFaceAt.TryGetValue( vi, out var other ) )
					Union( other, fi );
				else
					firstFaceAt[vi] = fi;
			}
		}

		// Keyed by root and built in face order, so the groups themselves are in a stable order
		// before the sort below re-orders them by shape. Two runs over the same mesh produce the
		// same lists in the same order, which is what makes the ids stable.
		var groups = new Dictionary<int, List<int>>();
		var order = new List<int>();

		for ( var fi = 0; fi < mesh.FaceCount; fi++ )
		{
			var root = Find( fi );

			if ( !groups.TryGetValue( root, out var list ) )
			{
				groups[root] = list = new List<int>();
				order.Add( root );
			}

			list.Add( fi );
		}

		var result = new List<List<int>>( order.Count );

		foreach ( var root in order )
			result.Add( groups[root] );

		return result;

		int Find( int i )
		{
			while ( parent[i] != i )
			{
				parent[i] = parent[parent[i]];
				i = parent[i];
			}

			return i;
		}

		void Union( int a, int b )
		{
			var ra = Find( a );
			var rb = Find( b );

			if ( ra == rb )
				return;

			// Smaller root wins, so the representative of a group does not depend on which face
			// happened to be visited first.
			if ( ra < rb )
				parent[rb] = ra;
			else
				parent[ra] = rb;
		}
	}

	/// <summary>
	/// One group of faces as a mesh of its own, with its vertices renumbered to just the ones it
	/// uses.
	///
	/// Per-corner UVs, the material slot and the skin weights all come across. Skin especially:
	/// dropping it would mean a rigged part silently loses its binding the first time a cut severs
	/// it, which is precisely the failure mode PolyMesh.Skin's own comment describes.
	/// </summary>
	static PolyMesh Extract( PolyMesh mesh, List<int> faces )
	{
		var piece = new PolyMesh();
		var remap = new Dictionary<int, int>();

		var rigged = mesh.IsRigged;

		if ( rigged )
			piece.Skin = new SkinWeights();

		foreach ( var fi in faces )
		{
			var face = mesh.Faces[fi];
			var indices = new int[face.Count];

			for ( var i = 0; i < face.Count; i++ )
			{
				var vi = face.Indices[i];

				if ( !remap.TryGetValue( vi, out var mapped ) )
				{
					mapped = piece.AddVertex( mesh.Positions[vi] );
					remap[vi] = mapped;

					if ( rigged )
						piece.Skin.Vertices.Add( mesh.Skin[vi] );
				}

				indices[i] = mapped;
			}

			piece.AddFace( indices, (Vec2[])face.UVs.Clone(), face.Material );
		}

		return piece;
	}

	/// <summary>Largest first, ties broken on the minimum corner. See the class comment for why the
	/// tiebreak is not optional.</summary>
	static int Compare( PolyMesh a, PolyMesh b )
	{
		var volumeA = MathF.Abs( a.SignedVolume() );
		var volumeB = MathF.Abs( b.SignedVolume() );

		// A relative tolerance, because two halves of a symmetric part differ in the last bits of a
		// number whose size depends on the part. Comparing them exactly would let float noise decide
		// the order, which is the whole thing this is here to prevent.
		var scale = MathF.Max( MathF.Max( volumeA, volumeB ), 1e-6f );

		if ( MathF.Abs( volumeA - volumeB ) > scale * 1e-5f )
			return volumeB.CompareTo( volumeA );

		MinCorner( a, out var cornerA );
		MinCorner( b, out var cornerB );

		if ( cornerA.x != cornerB.x ) return cornerA.x.CompareTo( cornerB.x );
		if ( cornerA.y != cornerB.y ) return cornerA.y.CompareTo( cornerB.y );

		return cornerA.z.CompareTo( cornerB.z );
	}

	static void MinCorner( PolyMesh mesh, out Vec3 min )
	{
		min = new Vec3( float.MaxValue, float.MaxValue, float.MaxValue );

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