Editor/Core/Analysis/FrontSilhouette.cs
#nullable enable annotations
namespace HumanoidRigger;
using Vector3=System.Numerics.Vector3;
using Vector2=System.Numerics.Vector2;

/// <summary>Frontal occupancy of a window of the character. Open space between
/// a limb and the body shows where that limb leaves it, whatever the mesh's
/// connectivity, vertex density or winding.</summary>
internal sealed class FrontSilhouette
{
    readonly bool[] filled;readonly float[] near,far;
    internal readonly int Columns,Rows;internal readonly float Left,Bottom,Cell;
    internal FrontSilhouette(IEnumerable<MeshPart> meshes,float left,float bottom,float width,float tall,float cell)
    {
        Left=left;Bottom=bottom;Cell=cell;
        Columns=(int)MathF.Ceiling(width/cell)+2;Rows=(int)MathF.Ceiling(tall/cell)+2;
        filled=new bool[Columns*Rows];near=new float[filled.Length];far=new float[filled.Length];
        Array.Fill(near,float.PositiveInfinity);Array.Fill(far,float.NegativeInfinity);
        foreach(var mesh in meshes)for(int t=0;t<mesh.Triangles.Length;t+=3)
        {
            var a=mesh.Vertices[mesh.Triangles[t]];var b=mesh.Vertices[mesh.Triangles[t+1]];var c=mesh.Vertices[mesh.Triangles[t+2]];
            var low=Vector3.Min(a,Vector3.Min(b,c));var high=Vector3.Max(a,Vector3.Max(b,c));
            if(high.X<left||low.X>left+Columns*cell||high.Y<bottom||low.Y>bottom+Rows*cell)continue;
            // Sample densely enough that thin or edge-on faces still cover
            // their cells; a missed cell would read as open space.
            int divisions=Math.Clamp((int)MathF.Ceiling(Math.Max(high.X-low.X,high.Y-low.Y)/(cell*.5f)),1,256);
            for(int u=0;u<=divisions;u++)for(int v=0;v<=divisions-u;v++)
            {
                var p=a+(b-a)*(u/(float)divisions)+(c-a)*(v/(float)divisions);
                int x=Column(p.X),y=Row(p.Y);if(x<0||y<0||x>=Columns||y>=Rows)continue;
                int i=x+Columns*y;filled[i]=true;near[i]=Math.Min(near[i],p.Z);far[i]=Math.Max(far[i],p.Z);
            }
        }
    }
    internal int Column(float x)=>(int)MathF.Floor((x-Left)/Cell);
    internal int Row(float y)=>(int)MathF.Floor((y-Bottom)/Cell);
    internal bool Filled(int x,int y)=>x>=0&&y>=0&&x<Columns&&y<Rows&&filled[x+Columns*y];
    internal bool Inside(Vector2 point)=>Filled(Column(point.X),Row(point.Y));
    /// <summary>Middle of the surface's depth range, where the window covers it.</summary>
    internal float? Depth(Vector2 point)
    {
        int x=Column(point.X),y=Row(point.Y);
        return Filled(x,y)?(near[x+Columns*y]+far[x+Columns*y])*.5f:null;
    }
    /// <summary>Length of a straight path that crosses open space inside the window.</summary>
    internal float OpenLength(Vector2 from,Vector2 to)
    {
        float length=Vector2.Distance(from,to);int samples=(int)MathF.Ceiling(length/(Cell*.5f));
        if(samples<2)return 0;
        int open=0;
        for(int i=1;i<samples;i++)
        {
            var p=Vector2.Lerp(from,to,i/(float)samples);int x=Column(p.X),y=Row(p.Y);
            if(x>=0&&y>=0&&x<Columns&&y<Rows&&!filled[x+Columns*y])open++;
        }
        return length*open/samples;
    }
    /// <summary>Connected sets of cells satisfying a test, in scan order.</summary>
    internal List<List<int>> Regions(Func<int,int,bool> member)
    {
        var cells=new bool[Columns*Rows];
        for(int y=0;y<Rows;y++)for(int x=0;x<Columns;x++)cells[x+Columns*y]=member(x,y);
        var seen=new bool[cells.Length];var regions=new List<List<int>>();
        for(int start=0;start<cells.Length;start++)
        {
            if(!cells[start]||seen[start])continue;
            var component=new List<int>();var queue=new Queue<int>();queue.Enqueue(start);seen[start]=true;
            while(queue.TryDequeue(out int i))
            {
                component.Add(i);int x=i%Columns,y=i/Columns;
                foreach(var (dx,dy) in new[]{(1,0),(-1,0),(0,1),(0,-1)})
                {
                    int px=x+dx,py=y+dy;if(px<0||py<0||px>=Columns||py>=Rows)continue;
                    int n=px+Columns*py;if(!cells[n]||seen[n])continue;seen[n]=true;queue.Enqueue(n);
                }
            }
            regions.Add(component);
        }
        return regions;
    }
}