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

namespace Effigy;

/// <summary>
/// Which material slots a painted body's atlas should bind to.
///
/// THE BUG THIS EXISTS FOR. Compile used to write the authored .vmat onto
/// <c>MaterialNames[0]</c> unconditionally. Slot 0 is where faces start, so that is usually right,
/// and that is why it shipped. It is wrong when every face has been dropped onto another slot
/// (the atlas binds to a slot nothing samples) and wrong when slot 0 already carries a material
/// the user chose (that name is overwritten). Bind the slots the painted faces actually wear.
/// </summary>
public static class PaintBind
{
	/// <summary>Every material slot a face of this mesh is on, sorted.</summary>
	public static SortedSet<int> SlotsOn( PolyMesh mesh )
	{
		var slots = new SortedSet<int>();

		if ( mesh?.Faces is null )
			return slots;

		foreach ( var face in mesh.Faces )
			slots.Add( face.Material );

		return slots;
	}

	/// <summary>
	/// The slots this painted body should bind its atlas to: the slots its faces wear.
	///
	/// Does not invent a slot. Does not write slot 0 unless a face is actually on it. A second
	/// painted body that shares a slot with this one is the caller's problem — isolate faces onto
	/// a fresh slot before asking, or accept that two atlases cannot share a name.
	/// </summary>
	public static List<int> SlotsToBind( PolyMesh mesh )
	{
		var slots = new List<int>();

		foreach ( var slot in SlotsOn( mesh ) )
			slots.Add( slot );

		return slots;
	}

	/// <summary>
	/// Whether merging <paramref name="source"/> into <paramref name="target"/> would drop an
	/// atlas. Paint is one canvas per body; a merge can show one, so the target keeps its own and
	/// only adopts the source's when it has none. Two painted bodies is the case this names.
	/// </summary>
	public static bool MergeDropsPaint( PolyMesh target, PolyMesh source ) =>
		target?.Paint is not null && source?.Paint is not null;

	/// <summary>A free slot no face on <paramref name="used"/> wears and no name occupies.</summary>
	public static int NextFreeSlot( IReadOnlyDictionary<int, string> names, IEnumerable<int> used )
	{
		var taken = new HashSet<int>();

		if ( used is not null )
		{
			foreach ( var slot in used )
				taken.Add( slot );
		}

		if ( names is not null )
		{
			foreach ( var slot in names.Keys )
				taken.Add( slot );
		}

		// Slot 0 is the default every face starts on; MaterialDrop never allocates it either.
		for ( var slot = 1; slot < 64; slot++ )
		{
			if ( !taken.Contains( slot ) )
				return slot;
		}

		return -1;
	}

	/// <summary>Move every face of <paramref name="mesh"/> onto <paramref name="slot"/>.</summary>
	public static void AssignAllFaces( PolyMesh mesh, int slot )
	{
		if ( mesh?.Faces is null )
			return;

		foreach ( var face in mesh.Faces )
			face.Material = slot;
	}
}