Editor/UI/BoneOverlay.cs
// SPDX-FileCopyrightText: 2023 Blender Authors
// SPDX-License-Identifier: GPL-2.0-or-later
// Octahedral shape data adapted from Blender overlay_shape.cc.
// See docs/third-party/blender.md for the pinned source and license.
using Sandbox;
using Vec=System.Numerics.Vector3;
using Quat=System.Numerics.Quaternion;
namespace HumanoidRigger.Editor;

sealed class BoneOverlay
{
    readonly SceneWorld world;
    readonly List<SceneModel> models=[];
    readonly Dictionary<int,Mesh> meshes=[];
    readonly Dictionary<string,(Mesh Mesh,Vertex[] Vertices)> buffers=[];
    internal sealed record PickShape(string Role,Vec Start,Vec End,Vec[] Vertices);
    readonly Dictionary<string,PickShape> pickShapes=[];
    internal IEnumerable<PickShape> PickShapes=>pickShapes.Values;
    string selected;
    GeneratedRig displayedRig;
    Material material;
    Material BoneMaterial
    {
        get
        {
            if(material is not null)return material;
            material=Material.Load("materials/gizmo/solid.vmat");
            return material;
        }
    }
    public int BoneCount=>models.Count;
    internal SceneModel[] SceneObjects=>models.ToArray();
    public BoneOverlay(SceneWorld world){this.world=world;}
    public void Clear(){foreach(var model in models)model.Delete();models.Clear();meshes.Clear();buffers.Clear();pickShapes.Clear();displayedRig=null;}
    public void Select(string role)
    {
        selected=role;
        foreach(var (bone,buffer) in buffers)
        {
            for(int i=0;i<buffer.Vertices.Length;i++)buffer.Vertices[i].Color=VertexColor(bone,buffer.Vertices[i].Normal);
            buffer.Mesh.SetVertexBufferData<Vertex>(buffer.Vertices.AsSpan());
        }
    }
    Color VertexColor(string role,Vector3 normal)
        =>((role==selected?Color.White:BoneColor(role))*(.65f+.35f*Math.Abs(normal.z))).WithAlpha(1);

    // Blender's unit bone points along +Y. Its widest section is at 10% of its length.
    static readonly Vec[] shape=[new(0,0,0),new(.1f,.1f,.1f),new(.1f,.1f,-.1f),new(-.1f,.1f,-.1f),new(-.1f,.1f,.1f),new(0,1,0)];
    static readonly int[] triangles=[2,1,0,3,2,0,4,3,0,1,4,0,5,1,2,5,2,3,5,3,4,5,4,1];

    public void Draw(GeneratedRig rig,Anatomy anatomy,Vec[] positions,Quat[] rotations)
    {
        if(displayedRig!=rig||material is null&&models.Count>0){Clear();displayedRig=rig;}
        if(positions.Length!=rig.Bones.Length)return;
        var ends=RigGeometry.SegmentEnds(rig);
        for(int i=0;i<rig.Bones.Length;i++)
        {
            var bone=rig.Bones[i];if(!bone.Deform)continue;
            var end=ends[i];
            if(Vec.DistanceSquared(end,bone.Position)<1e-8f)
            {
                string tipRole=bone.Role.Length>3?bone.Role[..^3]+"Tip"+bone.Role[^2..]:"";
                if(anatomy.Points.TryGetValue(tipRole,out var tip))end=tip.Position;
                else if(bone.Role=="Head")end=bone.Position+Vec.UnitY*anatomy.Height*.05f;
                else if(bone.Parent>=0)end=bone.Position+(bone.Position-rig.Bones[bone.Parent].Position)*.4f;
            }
            var direction=end-bone.Position;float length=direction.Length();if(length<1e-5f)continue;
            var axis=direction/length;
            var normal=Vec.Transform(Vec.UnitZ,bone.Rotation);
            if(Math.Abs(Vec.Dot(normal,axis))>.95f)normal=Math.Abs(axis.Z)<.9f?Vec.UnitZ:Vec.UnitY;
            var right=Vec.Normalize(Vec.Cross(axis,normal));normal=Vec.Cross(right,axis);
            var delta=rotations is null?Quat.Identity:rotations[i];
            var canonical=shape.Select(p=>positions[i]+Vec.Transform((right*p.X+axis*p.Y+normal*p.Z)*length,delta)).ToArray();
            pickShapes[bone.Role]=new(bone.Role,canonical[0],canonical[5],canonical);
            var points=canonical.Select(RiggerViewport.Engine).ToArray();
            var vertices=new List<Vertex>();
            for(int t=0;t<triangles.Length;t+=3)
            {
                // Canonical-to-engine conversion reverses handedness.
                int a=triangles[t],b=triangles[t+2],c=triangles[t+1];
                var n=Vector3.Cross(points[b]-points[a],points[c]-points[a]).Normal;
                foreach(int index in new[]{a,b,c})vertices.Add(new Vertex(points[index],n,new Vector4(1,0,0,1),Vector2.Zero){Color=VertexColor(bone.Role,n)});
            }
            bool create=!meshes.TryGetValue(i,out var mesh);
            if(create)
            {
                mesh=new Mesh(BoneMaterial);
                mesh.CreateVertexBuffer(vertices.Count,vertices);mesh.CreateIndexBuffer(vertices.Count,Enumerable.Range(0,vertices.Count).ToArray());meshes.Add(i,mesh);
            }
            else mesh.SetVertexBufferData(vertices);
            buffers[bone.Role]=(mesh,vertices.ToArray());
            mesh.Bounds=new BBox(points.Aggregate((a,b)=>Vector3.Min(a,b)),points.Aggregate((a,b)=>Vector3.Max(a,b)));
            if(create)
            {
                var model=new SceneModel(world,Model.Builder.AddMesh(mesh).Create(),Transform.Zero){RenderLayer=SceneRenderLayer.OverlayWithoutDepth};
                models.Add(model);
            }
        }
    }
    static Color BoneColor(string role)=>role.EndsWith(".L")?new(86/255f,180/255f,233/255f):role.EndsWith(".R")?new(230/255f,159/255f,0):Color.White;
}