Editor/WeaponImporter/Tool/Engine/PreviewModels.cs
using Sandbox;
using WeaponImporter.Core.Analysis;
using WeaponImporter.Core.Geometry;
using N = System.Numerics;

namespace WeaponImporter.Tool;

/// <summary>
/// Render models built in memory, so the weapon appears in the viewport the moment it is
/// analyzed (before anything is compiled): the file's own UVs and normals, one mesh per material,
/// textured when <see cref="PreviewMaterials"/> are given.
/// </summary>
public static class PreviewModels
{
    public const string SurfaceMaterial = "materials/default.vmat";

    /// <summary>The canonical weapon (weapon triangles only) as a static render model.</summary>
    public static Model BuildWeaponModel( WeaponAnalysis analysis, IReadOnlyList<Material> materials = null )
        => Build( analysis.Asset.Mesh, analysis.WeaponTriangles, materials );

    /// <summary>
    /// A render model of some triangles of a mesh. <paramref name="materials"/> follow the mesh's
    /// materials (null or missing entries use the plain surface material).
    /// </summary>
    public static Model Build( TriMesh mesh, IReadOnlyList<int> triangles, IReadOnlyList<Material> materials = null )
    {
        var normals = mesh.EnsureNormals();
        var uvs = mesh.CornerUVs;
        var fallback = Material.Load( SurfaceMaterial );
        var builder = Model.Builder;
        foreach ( var group in triangles.GroupBy( t => mesh.TriangleMaterial.Length > t ? mesh.TriangleMaterial[t] : 0 ) )
        {
            var vertices = new List<Vertex>( group.Count() * 3 );
            var indices = new List<int>( group.Count() * 3 );
            var bounds = Bounds.Empty;
            foreach ( var t in group )
            {
                var (a, b, c) = mesh.Triangle( t );
                var tangent = N.Vector3.Normalize( b - a );
                var corners = new[] { a, b, c };
                for ( var k = 0; k < 3; k++ )
                {
                    var corner = t * 3 + k;
                    var p = corners[k];
                    var n = normals[corner];
                    // No UVs in the file: a planar projection keeps the surface readable.
                    var uv = uvs is not null ? uvs[corner] : new N.Vector2( p.X * 0.1f, p.Y * 0.1f );
                    indices.Add( vertices.Count );
                    vertices.Add( new Vertex( p.ToEngine(), n.ToEngine(), new Vector4( tangent.X, tangent.Y, tangent.Z, 1 ), new Vector4( uv.X, uv.Y, 0, 0 ) ) );
                    bounds = bounds.Encapsulate( p );
                }
            }
            var material = materials is not null && group.Key >= 0 && group.Key < materials.Count ? materials[group.Key] ?? fallback : fallback;
            var m = new Mesh( material );
            m.CreateVertexBuffer( vertices.Count, Vertex.Layout, vertices );
            m.CreateIndexBuffer( indices.Count, indices );
            m.Bounds = new BBox( bounds.Min.ToEngine(), bounds.Max.ToEngine() );
            builder = builder.AddMesh( m );
        }
        return builder.Create();
    }
}