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 );
}
}