Editor/Core/Character.cs
namespace HumanoidRigger;
using Vector3 = System.Numerics.Vector3;

public enum CharacterPose { Auto, TPose, APose1, APose2, Relaxed, Unknown }
public enum MeshKind { Body, Clothing, Hair, Shoe, Accessory }
public sealed record MeshPart(string Name, Vector3[] Vertices, int[] Triangles, MeshKind Kind)
{
    // Triangle-corner channels preserve seams and hard edges without changing the
    // control points or connectivity used by the anatomical and skinning solvers.
    public Vector3[] CornerNormals {get;init;}=[];
    // Canonical texture coordinates use a lower-left origin. Native rendering/DMX invert V.
    public System.Numerics.Vector2[] CornerTexCoords {get;init;}=[];
    System.Numerics.Vector4[] cornerColors=[];
    // Existing editor objects can survive hotload before this optional channel existed.
    public System.Numerics.Vector4[] CornerColors {get=>cornerColors??[];init=>cornerColors=value;}
    public int[] TriangleMaterials {get;init;}=[];
}
public sealed record SourceBone(string Name, int Parent, Vector3 Position);
public sealed record Landmark(string Role, Vector3 Position, float Confidence, bool Corrected = false);

/// <summary>Canonical space is X left, Y up, Z forward, in centimeters.</summary>
public sealed class ImportedCharacter
{
    public string Name { get; init; } = "character";
    public MeshPart[] Meshes { get; init; } = [];
    public SourceBone[] ExistingBones { get; init; } = [];
    public bool HasExistingSkin { get; init; }
    public string SourcePath {get;init;}="";
    public string[] ImportWarnings {get;init;}=[];
    public SourceMaterial[] Materials {get;init;}=[];
    public Dictionary<string,byte[]> EmbeddedTextures {get;init;}=new(StringComparer.OrdinalIgnoreCase);
    public int SourceUpAxis { get; init; } = 1;
    public float SourceUnitCm { get; init; } = 1;
    public Vector3 Minimum => Meshes.SelectMany(m => m.Vertices).Aggregate(Vector3.Min);
    public Vector3 Maximum => Meshes.SelectMany(m => m.Vertices).Aggregate(Vector3.Max);
    public float Height => Maximum.Y - Minimum.Y;
    /// <summary>Scale for anatomical constraints; scene bounds may include long props.</summary>
    public float AnatomicalHeight
    {
        get
        {
            float minimum=float.PositiveInfinity,maximum=float.NegativeInfinity;
            foreach(var mesh in Meshes.Where(m=>m.Kind==MeshKind.Body))foreach(var p in mesh.Vertices)
            {minimum=Math.Min(minimum,p.Y);maximum=Math.Max(maximum,p.Y);}
            return minimum<=maximum?maximum-minimum:Height;
        }
    }
    public void Validate()
    {
        if (Meshes.Length == 0 || Meshes.All(m => m.Vertices.Length == 0)) throw new FormatException("The model contains no mesh.");
        foreach (var m in Meshes)
        {
            if (m.Vertices.Any(v => !Geometry.Finite(v))) throw new FormatException("The mesh contains non-finite coordinates.");
            if (m.Triangles.Length % 3 != 0 || m.Triangles.Any(i => i < 0 || i >= m.Vertices.Length)) throw new FormatException("Invalid mesh topology.");
            if(m.CornerNormals.Length!=0&&(m.CornerNormals.Length!=m.Triangles.Length||m.CornerNormals.Any(n=>!Geometry.Finite(n))))throw new FormatException("Invalid mesh normals.");
            if(m.CornerTexCoords.Length!=0&&(m.CornerTexCoords.Length!=m.Triangles.Length||m.CornerTexCoords.Any(uv=>!float.IsFinite(uv.X)||!float.IsFinite(uv.Y))))throw new FormatException("Invalid mesh texture coordinates.");
            if(m.CornerColors.Length!=0&&(m.CornerColors.Length!=m.Triangles.Length||m.CornerColors.Any(c=>!float.IsFinite(c.X)||!float.IsFinite(c.Y)||!float.IsFinite(c.Z)||!float.IsFinite(c.W))))throw new FormatException("Invalid mesh vertex colors.");
            if(m.TriangleMaterials.Length!=0&&(m.TriangleMaterials.Length!=m.Triangles.Length/3||m.TriangleMaterials.Any(i=>i< -1||i>=Materials.Length)))throw new FormatException("Invalid mesh material assignment.");
        }
        if (Height < 0.001f) throw new FormatException("The model has no usable height.");
    }
}

public static class Geometry
{
    public static bool Finite(Vector3 v) => float.IsFinite(v.X) && float.IsFinite(v.Y) && float.IsFinite(v.Z);
    public static Vector3 Mean(IEnumerable<Vector3> points)
    {
        var sum = Vector3.Zero; var n = 0;
        foreach (var p in points) { sum += p; n++; }
        return n == 0 ? throw new InvalidOperationException("Empty geometry region.") : sum / n;
    }
    public static Vector3 ClosestOnSegment(Vector3 p, Vector3 a, Vector3 b)
    {
        var d = b - a;
        return a + d * Math.Clamp(Vector3.Dot(p-a,d) / Math.Max(d.LengthSquared(),1e-12f),0,1);
    }
    public static MeshPart Merge(IEnumerable<MeshPart> source)
    {
        var parts=source.ToArray();var triangles=new List<int>();int offset=0;
        foreach(var part in parts){triangles.AddRange(part.Triangles.Select(i=>i+offset));offset+=part.Vertices.Length;}
        return new("Combined surface",parts.SelectMany(p=>p.Vertices).ToArray(),triangles.ToArray(),MeshKind.Body);
    }
    public static List<int>[] Neighbors(MeshPart m)=>Neighbors(m,0);
    public static List<int>[] Neighbors(MeshPart m,float weldTolerance)
    {
        var edges = Enumerable.Range(0,m.Vertices.Length).Select(_ => new HashSet<int>()).ToArray();
        for (int i=0;i<m.Triangles.Length;i+=3)
            for (int j=0;j<3;j++) { var a=m.Triangles[i+j]; var b=m.Triangles[i+(j+1)%3]; edges[a].Add(b); edges[b].Add(a); }
        if(weldTolerance>0)
        {
            var groups=new Dictionary<(long,long,long),List<int>>();
            for(int i=0;i<m.Vertices.Length;i++)
            {
                var p=m.Vertices[i];var key=((long)MathF.Floor(p.X/weldTolerance),(long)MathF.Floor(p.Y/weldTolerance),(long)MathF.Floor(p.Z/weldTolerance));
                for(int x=-1;x<=1;x++)for(int y=-1;y<=1;y++)for(int z=-1;z<=1;z++)
                    if(groups.TryGetValue((key.Item1+x,key.Item2+y,key.Item3+z),out var nearby))
                        foreach(int other in nearby)if(Vector3.DistanceSquared(p,m.Vertices[other])<=weldTolerance*weldTolerance)
                        {edges[i].Add(other);edges[other].Add(i);}
                if(!groups.TryGetValue(key,out var group))groups[key]=group=[];
                group.Add(i);
            }
        }
        return edges.Select(x=>x.ToList()).ToArray();
    }
    public static int[] Components(MeshPart m)=>Components(Neighbors(m));
    public static int[] Components(List<int>[] adjacency)
    {
        var labels=Enumerable.Repeat(-1,adjacency.Length).ToArray(); var id=0;
        for(int i=0;i<labels.Length;i++)
        {
            if(labels[i]>=0) continue;
            var queue=new Queue<int>(); queue.Enqueue(i); labels[i]=id;
            while(queue.TryDequeue(out int v)) foreach(var next in adjacency[v]) if(labels[next]<0) { labels[next]=id; queue.Enqueue(next); }
            id++;
        }
        return labels;
    }
}