Editor/Effigy/PaintBind.cs

Utility class for determining which material slots a painted PolyMesh uses and for managing slot assignments. It computes the sorted set of slots used by faces, returns a list to bind an atlas to, checks if merging two painted meshes would cause paint loss, finds the next free slot that is not already used or named, and assigns all faces of a mesh to a given slot.

File Access
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;
	}
}