Code/AutoRig/Mesh/RigMesh.cs

Data classes for an imported triangle mesh and its materials. RigMesh stores positions, normals, UVs, triangle indices, per-triangle tags and material indices, computes bounds and surface area, and validates structural invariants. RigMaterial holds material name, optional embedded image bytes or source path and a tint.

using System.Numerics;

namespace AutoRig.Mesh;

// s&box compat: the engine defines Vector2/Vector3 in the GLOBAL namespace, which
// shadows using-directive imports - alias explicitly to System.Numerics.
using Vector2 = System.Numerics.Vector2;
using Vector3 = System.Numerics.Vector3;


/// <summary>
/// A triangle mesh as loaded from a source file: indexed positions with optional
/// normals/UVs, and a per-triangle tag pointing at the source node/group/material
/// name it came from (used later for part naming and classification hints).
/// </summary>
/// <summary>A source material: name plus its base-color image (png/jpg bytes as
/// stored in the file) and/or a flat tint. Carried through to export.</summary>
public sealed class RigMaterial
{
    public string Name = "";

    /// <summary>Encoded base-color image bytes (png/jpg) or null when untextured.</summary>
    public byte[]? BaseColorImage;

    /// <summary>Image path as referenced by the source file (relative), for textures
    /// stored BESIDE the model instead of embedded — the editor layer resolves it
    /// (Code/ does no file IO).</summary>
    public string? SourceImagePath;

    /// <summary>Flat base color factor (multiplies the texture; white = none).</summary>
    public Vector3 Tint = new( 1f, 1f, 1f );
}

public sealed class RigMesh
{
    public string SourceName = "";
    public Vector3[] Positions = [];
    public Vector3[] Normals = [];
    public Vector2[] Uvs = [];
    public int[] Triangles = [];
    public int[] TriangleTags = [];
    public string[] Tags = [];

    /// <summary>Source materials (empty when the file had none / importer skips them).</summary>
    public RigMaterial[] Materials = [];

    /// <summary>Per-triangle material index into <see cref="Materials"/>, -1 = none.
    /// Empty array = legacy/no material data.</summary>
    public int[] TriangleMaterials = [];

    public int TriangleCount => Triangles.Length / 3;

    public Aabb3 ComputeBounds() => Aabb3.FromPoints( Positions );

    public float SurfaceArea()
    {
        double area = 0;
        for ( var t = 0; t < Triangles.Length; t += 3 )
        {
            var a = Positions[Triangles[t]];
            var b = Positions[Triangles[t + 1]];
            var c = Positions[Triangles[t + 2]];
            area += Vector3.Cross( b - a, c - a ).Length() * 0.5f;
        }
        return (float)area;
    }

    /// <summary>Checks structural invariants.</summary>
    /// <exception cref="FormatException">On any malformed field.</exception>
    public void Validate()
    {
        if ( Triangles.Length % 3 != 0 )
            throw new FormatException( $"Triangle index count {Triangles.Length} is not a multiple of 3." );
        if ( Normals.Length != 0 && Normals.Length != Positions.Length )
            throw new FormatException( $"Normal count {Normals.Length} does not match position count {Positions.Length}." );
        if ( Uvs.Length != 0 && Uvs.Length != Positions.Length )
            throw new FormatException( $"UV count {Uvs.Length} does not match position count {Positions.Length}." );
        if ( TriangleTags.Length != TriangleCount )
            throw new FormatException( $"Triangle tag count {TriangleTags.Length} does not match triangle count {TriangleCount}." );
        if ( TriangleMaterials.Length != 0 && TriangleMaterials.Length != TriangleCount )
            throw new FormatException(
                $"Triangle material count {TriangleMaterials.Length} does not match triangle count {TriangleCount}." );
        foreach ( var m in TriangleMaterials )
            if ( m < -1 || m >= Materials.Length )
                throw new FormatException( $"Triangle material index {m} out of range (have {Materials.Length})." );
        foreach ( var i in Triangles )
            if ( i < 0 || i >= Positions.Length )
                throw new FormatException( $"Triangle index {i} out of range (0..{Positions.Length - 1})." );
        foreach ( var tag in TriangleTags )
            if ( tag < 0 || tag >= Tags.Length )
                throw new FormatException( $"Triangle tag {tag} out of range (0..{Tags.Length - 1})." );
        foreach ( var p in Positions )
            if ( !float.IsFinite( p.X ) || !float.IsFinite( p.Y ) || !float.IsFinite( p.Z ) )
                throw new FormatException( "Mesh contains a non-finite vertex position." );
    }
}