Unit tests for paint material and binding logic. Verifies canvas baking to opaque RGBA, that paint slots are detected for binding, merge behavior naming, and that generated .vmat text references the correct image and structure.
using System;
using Effigy;
using static Effigy.Tests.Report;
namespace Effigy.Tests;
/// <summary>
/// The authoring half of the paint path: what the painted canvas bakes to, and the .vmat text that
/// compiles it. A headless suite cannot compile an asset, but it can say whether the file the editor
/// will write is structurally sound and names the right image — the same bargain VmdlMaterialsTests
/// makes for the remap node.
/// </summary>
public static class PaintMaterialTests
{
public static void Run()
{
Section( "paint material: the canvas bakes opaque over the base colour" );
TestOpaqueBake();
Section( "paint bind: the atlas goes on the slots the faces actually wear" );
TestBindUsesThePaintedSlots();
TestMergeDropIsNamed();
Section( "paint material: the vmat names its image" );
TestVmatNamesTheImage();
}
static void TestOpaqueBake()
{
var canvas = new PaintCanvas( 2, 1 );
// One texel painted red, one left transparent.
canvas.Blend( 0, 0, 255, 0, 0, 1f );
var opaque = PaintMaterial.OpaqueRgba( canvas );
Check( "the bake is the right size", opaque.Length == 2 * 1 * 4, $"{opaque.Length}" );
Check( "a painted texel stays its colour and goes opaque",
opaque[0] == 255 && opaque[1] == 0 && opaque[2] == 0 && opaque[3] == 255,
$"({opaque[0]},{opaque[1]},{opaque[2]},{opaque[3]})" );
Check( "an unpainted texel falls back to the default base, not white",
opaque[4] == PaintMaterial.DefaultBaseR
&& opaque[5] == PaintMaterial.DefaultBaseG
&& opaque[6] == PaintMaterial.DefaultBaseB
&& opaque[7] == 255,
$"({opaque[4]},{opaque[5]},{opaque[6]},{opaque[7]})" );
var overRed = PaintMaterial.OpaqueRgba( canvas, 200, 10, 10 );
Check( "a caller can pass the bound material's colour as the base",
overRed[4] == 200 && overRed[5] == 10 && overRed[6] == 10 && overRed[7] == 255 );
}
static void TestBindUsesThePaintedSlots()
{
var mesh = Primitives.Box( 2f, 2f, 2f );
foreach ( var face in mesh.Faces )
face.Material = 2;
var slots = PaintBind.SlotsToBind( mesh );
Check( "faces on slot 2 bind slot 2, not slot 0",
slots.Count == 1 && slots[0] == 2,
string.Join( ",", slots ) );
Check( "slot 0 is not invented", !slots.Contains( 0 ) );
}
static void TestMergeDropIsNamed()
{
var a = Primitives.Box( 2f, 2f, 2f );
var b = Primitives.Box( 2f, 2f, 2f );
a.Paint = new PaintCanvas( 8, 8 );
b.Paint = new PaintCanvas( 8, 8 );
Check( "two painted bodies merging is a drop, said rather than silent",
PaintBind.MergeDropsPaint( a, b ) );
var plain = Primitives.Box( 1f, 1f, 1f );
Check( "a painted body into an unpainted rest is not a drop",
!PaintBind.MergeDropsPaint( plain, a ) );
}
static void TestVmatNamesTheImage()
{
var vmat = PaintMaterial.VmatSource( "models/effigy/box_paint.png" );
Check( "the vmat binds the PNG as its colour",
vmat.Contains( "TextureColor \"models/effigy/box_paint.png\"" ) );
Check( "braces balance", CountOf( vmat, "{" ) == CountOf( vmat, "}" ),
$"{CountOf( vmat, "{" )} open, {CountOf( vmat, "}" )} close" );
Check( "it is the Layer0 form the engine's own templates ship",
vmat.Contains( "Layer0" ) && vmat.Contains( "shader \"complex.vfx\"" ) );
// A .vtex is not a valid texture input — the engine's compiler says so in its own words.
// The material must name the image, not a texture wrapper.
Check( "and it never names a .vtex",
!vmat.Contains( ".vtex" ) );
}
static int CountOf( string text, string needle )
{
var count = 0;
var at = 0;
while ( (at = text.IndexOf( needle, at, StringComparison.Ordinal )) >= 0 )
{
count++;
at += needle.Length;
}
return count;
}
}