Effigy/AtlasId.cs
using System;

namespace Effigy;

/// <summary>
/// A stable id for a mesh's atlas: every corner UV, in face order then corner order.
///
/// LIVE NOW, and the whole reason it exists is the move back to a texture atlas. A replayed paint
/// canvas is keyed to the UV layout, because a re-unwrap keeps the topology and moves every island —
/// a canvas cached on topology alone would be handed back against a rearranged atlas, paint scattered
/// onto unrelated faces, silently.
///
/// <see cref="MultiresSculpt.TopologyId"/> deliberately ignores UVs, because positions are what a
/// parametric edit is expected to change. That is exactly what makes it the wrong key for a cached
/// paint canvas: a re-unwrap keeps the topology and moves every island, so it produces the same
/// topology id, and a canvas keyed on that alone would be handed back against a rearranged atlas —
/// paint scattered onto unrelated faces, silently. This id sits beside TopologyId in the cache key,
/// so "same mesh, re-unwrapped" and "same mesh, same atlas" stop being the same thing.
///
/// Floats are hashed by their exact bits rather than rounded, because an island nudged a hair is a
/// different atlas and the id has to say so.
/// </summary>
public static class AtlasId
{
	public static long Of( PolyMesh mesh )
	{
		if ( mesh is null )
			throw new ArgumentNullException( nameof( mesh ) );

		// FNV-1a, written out rather than leaning on GetHashCode: this value goes in a file and has
		// to mean the same thing in the next process and on the next runtime.
		const long prime = 0x100000001b3;
		var hash = unchecked((long)0xcbf29ce484222325);

		void Mix( float value )
		{
			var bits = BitConverter.SingleToInt32Bits( value );

			for ( var b = 0; b < 4; b++ )
			{
				hash ^= (bits >> (b * 8)) & 0xff;
				hash = unchecked(hash * prime);
			}
		}

		foreach ( var face in mesh.Faces )
		{
			// The corner count delimits the faces, the same way TopologyId mixes face.Count: without
			// it, a quad's UVs and two triangles' UVs are the same flat sequence and the id cannot
			// tell them apart.
			Mix( face.UVs.Length );

			foreach ( var uv in face.UVs )
			{
				Mix( uv.x );
				Mix( uv.y );
			}
		}

		return hash;
	}
}