Editor/Output/ArchFaceMapper.cs
using HalfEdgeMesh;

namespace Sunless.Architecture;

// HOW A FACE'S TEXTURE AXES ARE WRITTEN - the fourth host hook, and the only one that exists for cost alone.
//
// A stock PolygonMesh computes a face's coordinates as it takes its axes, and doing that per face rebuilds the
// mesh's whole texture-size table through native interop: Material.FirstTexture is an interop call and a Texture
// allocation, with two attribute lookups behind it. A canvas finishes by computing every coordinate in one pass,
// so all of it is thrown away again - measured at 2369ms of a 2548ms build on a plan of climber cards.
//
// A game whose engine can write the axes ALONE answers here. Nothing answering is the tool standing on its own, so
// the stock call is the stock answer. This may only ever change what a build COSTS: the axes a mapper writes have
// to be the axes it was handed, or every face in the plan is mapped to something the tool did not ask for.
public interface IArchFaceMapper {
	void Map( PolygonMesh mesh, FaceHandle face, Vector4 axisU, Vector4 axisV, Vector2 scale );
}

// HELD, and released by the hotload, for the reason ArchAddons is: this is asked once per emitted face, and a
// discovery scan there would cost more than the call it exists to make cheap. Forget is what keeps a game's
// pre-edit types collectable.
public static class ArchFaceMappers {
	static volatile IArchFaceMapper held;

	[EditorEvent.Hotload]
	static void Forget() {
		held = null;
	}

	// Read through a local, the way ArchAddons is: a canvas can be built off the main thread while a hotload on it
	// clears the field.
	public static IArchFaceMapper Standing() {
		return held ?? (held = Discovered());
	}

	// Ordered by name and first answer taken, so two games open in one editor cannot make the result depend on which
	// assembly enrolled first - and the stock answer is excluded from the scan that would otherwise find it, because
	// it declares the contract too and "ArchStockFaceMapper" sorts ahead of anything a game is likely to call its own.
	static IArchFaceMapper Discovered() {
		return ArchDiscovery.EnrolledByName<IArchFaceMapper>().FirstOrDefault( mapper => mapper is not ArchStockFaceMapper )
			?? new ArchStockFaceMapper();
	}
}

// What a stock engine offers: the axes, and the coordinates it insists on computing from them. Correct everywhere
// and slow in a generator, which is the whole reason the hook above exists.
sealed class ArchStockFaceMapper : IArchFaceMapper {
	public void Map( PolygonMesh mesh, FaceHandle face, Vector4 axisU, Vector4 axisV, Vector2 scale ) {
		mesh.SetFaceTextureParameters( face, axisU, axisV, scale );
	}
}