namespace XFEEDToolBox.Core.Galaxy;
/// <summary>Fills the render budget across occupied screen cells, keeping sparse edges and mandatory markers.</summary>
public sealed class GalaxyMapLod
{
/// <summary>Priority retains an eligible published point, never overrides a query or visibility rejection.</summary>
public static bool IsPriority(GalacticSystem system,GalaxyCatalogueSelection? selection,bool eligible,bool retainedMarker=false)
=>eligible&&(retainedMarker||selection?.IsForced(system)==true);
public const int Columns=32,Rows=18,Cells=Columns*Rows;
private readonly int[] counts=new int[Cells],quotas=new int[Cells],seen=new int[Cells];
private double width=1,height=1;
private int capacity,priorityCount,priorityAccepted,priorityQuota;
public int Candidates {get;private set;}
public int Budget {get;private set;}
public int OccupiedCells {get;private set;}
public bool Active=>Candidates>capacity;
public void Begin(double viewportWidth,double viewportHeight,int maximum)
{
width=Math.Max(1,viewportWidth);height=Math.Max(1,viewportHeight);capacity=Math.Max(0,maximum);
Candidates=Budget=OccupiedCells=priorityCount=priorityAccepted=priorityQuota=0;
Array.Clear(counts);Array.Clear(quotas);Array.Clear(seen);
}
private int Cell(double x,double y)=>Math.Clamp((int)(x/width*Columns),0,Columns-1)+Math.Clamp((int)(y/height*Rows),0,Rows-1)*Columns;
public void Count(double x,double y,bool priority=false)
{
Candidates++;if(priority){priorityCount++;return;}counts[Cell(x,y)]++;
}
public void Seal()
{
priorityQuota=Math.Min(capacity,priorityCount);Budget=Math.Min(capacity,Candidates);
var available=Budget-priorityQuota;var largest=0;
foreach(var count in counts){if(count>0)OccupiedCells++;largest=Math.Max(largest,count);}
var low=0;var high=largest;
while(low<high)
{
var level=low+(high-low+1)/2;long needed=0;
foreach(var count in counts)needed+=Math.Min(count,level);
if(needed<=available)low=level;else high=level-1;
}
var used=0;for(var i=0;i<Cells;i++){quotas[i]=Math.Min(counts[i],low);used+=quotas[i];}
for(var i=0;i<Cells&&used<available;i++)if(quotas[i]<counts[i]){quotas[i]++;used++;}
}
public bool Accept(double x,double y,bool priority=false)
{
if(priority)return priorityAccepted++<priorityQuota;
var cell=Cell(x,y);var count=counts[cell];if(count==0)return false;
var at=seen[cell]++;var quota=quotas[cell];
// Sample the entire source order within each overflowing tile; taking its
// first N entries would still favour the nearest central depth slice.
return (long)(at+1)*quota/count>(long)at*quota/count;
}
}
namespace XFEEDToolBox.Core.Galaxy;
/// <summary>Fills the render budget across occupied screen cells, keeping sparse edges and mandatory markers.</summary>
public sealed class GalaxyMapLod
{
/// <summary>Priority retains an eligible published point, never overrides a query or visibility rejection.</summary>
public static bool IsPriority(GalacticSystem system,GalaxyCatalogueSelection? selection,bool eligible,bool retainedMarker=false)
=>eligible&&(retainedMarker||selection?.IsForced(system)==true);
public const int Columns=32,Rows=18,Cells=Columns*Rows;
private readonly int[] counts=new int[Cells],quotas=new int[Cells],seen=new int[Cells];
private double width=1,height=1;
private int capacity,priorityCount,priorityAccepted,priorityQuota;
public int Candidates {get;private set;}
public int Budget {get;private set;}
public int OccupiedCells {get;private set;}
public bool Active=>Candidates>capacity;
public void Begin(double viewportWidth,double viewportHeight,int maximum)
{
width=Math.Max(1,viewportWidth);height=Math.Max(1,viewportHeight);capacity=Math.Max(0,maximum);
Candidates=Budget=OccupiedCells=priorityCount=priorityAccepted=priorityQuota=0;
Array.Clear(counts);Array.Clear(quotas);Array.Clear(seen);
}
private int Cell(double x,double y)=>Math.Clamp((int)(x/width*Columns),0,Columns-1)+Math.Clamp((int)(y/height*Rows),0,Rows-1)*Columns;
public void Count(double x,double y,bool priority=false)
{
Candidates++;if(priority){priorityCount++;return;}counts[Cell(x,y)]++;
}
public void Seal()
{
priorityQuota=Math.Min(capacity,priorityCount);Budget=Math.Min(capacity,Candidates);
var available=Budget-priorityQuota;var largest=0;
foreach(var count in counts){if(count>0)OccupiedCells++;largest=Math.Max(largest,count);}
var low=0;var high=largest;
while(low<high)
{
var level=low+(high-low+1)/2;long needed=0;
foreach(var count in counts)needed+=Math.Min(count,level);
if(needed<=available)low=level;else high=level-1;
}
var used=0;for(var i=0;i<Cells;i++){quotas[i]=Math.Min(counts[i],low);used+=quotas[i];}
for(var i=0;i<Cells&&used<available;i++)if(quotas[i]<counts[i]){quotas[i]++;used++;}
}
public bool Accept(double x,double y,bool priority=false)
{
if(priority)return priorityAccepted++<priorityQuota;
var cell=Cell(x,y);var count=counts[cell];if(count==0)return false;
var at=seen[cell]++;var quota=quotas[cell];
// Sample the entire source order within each overflowing tile; taking its
// first N entries would still favour the nearest central depth slice.
return (long)(at+1)*quota/count>(long)at*quota/count;
}
}