namespace XFEEDToolBox.Core.Galaxy;
public readonly record struct StarColour(byte R,byte G,byte B);
public enum StellarMorphology { Star, BrownDwarf, WhiteDwarf, NeutronStar, BlackHole }
public readonly record struct StellarSprite(float Radius,float CoreRadius,float Opacity,StellarMorphology Morphology,float Corona);
public static class StellarAppearance
{
/// <summary>
/// Original Galaxy52B instance size is converted to IEEE half before the
/// VS multiplies a unit camera-facing corner. Project without a pixel floor.
/// </summary>
public static float OriginalProjectedHalfExtent(float styleSize,double pixelsPerLightYear)
{
if(!float.IsFinite(styleSize)||styleSize<=0||!double.IsFinite(pixelsPerLightYear)||pixelsPerLightYear<=0)return 0;
var projected=(double)(float)(Half)styleSize*pixelsPerLightYear;
return double.IsFinite(projected)&&projected<=float.MaxValue?(float)projected:0;
}
/// <summary>
/// Original physical BackgroundStars layer selection(3B32EE0), from the
/// frozen shared-appearance maps50/60. Unmapped classes return layer13.
/// Types41/42 also require the original separate black-hole renderer.
/// </summary>
public static int OriginalSprite(ushort classification)
{
var type=classification&63;
if(type is 41 or 42)return 8;
var luminosity=(classification>>10)&31;
if(luminosity<=13)return 5;
if(luminosity==24)return 6;
return type switch
{
0 or 1=>0,2 or 3 or 4 or 5 or 6=>1,7 or 8 or 11=>2,9 or 10=>3,
12 or 13 or 14 or 15 or 16=>4,17 or 18 or 19 or 20 or 21 or 22 or 23 or 24=>5,
40=>7,41 or 42=>8,43=>9,46=>10,_=>13
};
}
public static readonly IReadOnlyList<StarColour> Palette=[new(160,188,255),new(181,203,255),new(222,233,255),new(249,247,235),new(255,239,177),new(255,187,107),new(255,115,70),new(215,77,58),new(170,83,118),new(129,94,133),new(208,233,255),new(164,224,255),new(166,161,222),new(226,230,240)];
public static int ColourIndex(string value)
{
var s=value.Trim().ToUpperInvariant();
if(s.Length==0)return 13;
if(s=="H"||s.Contains("BLACK HOLE",StringComparison.Ordinal)||s.Contains("BLACKHOLE",StringComparison.Ordinal))return 12;
if(s=="N"||s.Contains("NEUTRON",StringComparison.Ordinal))return 11;
if(s.StartsWith('D')||s.Contains("WHITE DWARF",StringComparison.Ordinal))return 10;
if(s.StartsWith('W')||s.StartsWith("AEBE",StringComparison.Ordinal)||s.Contains("HERBIG",StringComparison.Ordinal))return 0;
if(s is "TTS"||s.Contains("T TAURI",StringComparison.Ordinal))return 5;
if(s[0]=='C'||s is "S" or "MS"||s.StartsWith("S ",StringComparison.Ordinal)||s.StartsWith("S-TYPE",StringComparison.Ordinal)||s.StartsWith("MS-TYPE",StringComparison.Ordinal))return 6;
const string classes="OBAFGKMLTY";
var index=classes.IndexOf(s[0]);
return index>=0&&(s.Length==1||s[1] is ' ' or '_'||char.IsDigit(s[1]))?index:13;
}
public static float PhysicalRadius(string value)
{
var type=value.Replace("_"," ",StringComparison.Ordinal);
if(type.Contains("Supergiant",StringComparison.OrdinalIgnoreCase)||type.Contains("Super Giant",StringComparison.OrdinalIgnoreCase))return 2.4f;
if(type.Contains("Giant",StringComparison.OrdinalIgnoreCase))return 1.8f;
return 1;
}
/// <summary>Original territory point-size mode 2; geometry mode 1 does not use it.</summary>
public static float PowerTerritoryDistanceScale(double eyeDistance)=>
double.IsFinite(eyeDistance)?(float)Math.Max(1,Math.Min(Math.Max(0,eyeDistance),50)/25):1;
/// <summary>
/// Screen-space stellar light. A star's spectral class controls its photosphere;
/// route and selection importance controls only the marker allocation around it.
/// Distance here is measured from the camera focus, not from Sol or the player.
/// </summary>
public static StellarSprite Sprite(int spectral,double distance,double pixelsPerLightYear,int importance=0,bool realistic=true,float originalSize=0)
{
spectral=spectral>=0&&spectral<Palette.Count?spectral:13;
distance=double.IsFinite(distance)?Math.Max(0,distance):0;
pixelsPerLightYear=double.IsFinite(pixelsPerLightYear)?Math.Max(.00001,pixelsPerLightYear):4;
var morphology=spectral switch{7 or 8 or 9=>StellarMorphology.BrownDwarf,10=>StellarMorphology.WhiteDwarf,11=>StellarMorphology.NeutronStar,12=>StellarMorphology.BlackHole,_=>StellarMorphology.Star};
var hasOriginalSize=realistic&&float.IsFinite(originalSize)&&originalSize>0&&originalSize<=8;
var luminosity=hasOriginalSize?1:spectral switch{0=>1.28,1=>1.17,2=>1.1,3=>1.04,4=>1,5=>.9,6=>.78,7=>.64,8=>.56,9=>.48,10=>.74,11=>.69,12=>.85,_=>.9};
// Keep distant stars sub-pixel instead of giving every catalogue entry the
// same large opaque disc. Zooming nearby reveals a small limb-darkened core.
var zoom=Math.Clamp(Math.Pow(pixelsPerLightYear/4,.24),.48,3.3);
var attenuation=1/Math.Sqrt(1+distance*pixelsPerLightYear/420);
var core=(float)Math.Clamp(1.38*luminosity*zoom*(.48+.52*attenuation),.42,6);
// The original compact's logarithmic size replaces the estimated
// spectral size. Keep its ratio to original uniform size .7, including
// the original small-size range, instead of applying a pixel minimum.
if(hasOriginalSize)core*=originalSize/GalaxyOriginalAppearance.DefaultSize;
var opacity=(float)Math.Clamp((.3+.7*attenuation)*(spectral is 7 or 8 or 9?.68:1),.12,1);
if(!realistic){core=Math.Clamp(core*1.4f,2.6f,4.8f);opacity=Math.Max(.78f,opacity);}
var corona=spectral switch{7 or 8 or 9=>.065f,10=>.16f,11=>.18f,12=>0,_=>.2f};
var radius=realistic?Math.Max(4.5f,core*6):Math.Max(3,core*1.85f);
if(morphology==StellarMorphology.NeutronStar&&realistic)radius=Math.Max(radius,core*8);
if(importance>=2)radius=Math.Max(radius,26);
return new(radius,core,opacity,morphology,corona);
}
}
public readonly record struct MapSegment(double X1,double Y1,double X2,double Y2);
public static class InfiniteGalaxyGrid
{
public const int MaximumLines=GalaxyOriginalGrid.MaximumLines;
// Invert the actual camera projection at the viewport corners. Only the
// visible portion of the unbounded XZ plane is emitted, with bounded density.
public static int Write(Span<MapSegment> output,double width,double height,double yaw,double pitch,double scale,double panX,double panY,out double step)
{
step=0;
if(output.Length<8||!double.IsFinite(width+height+yaw+pitch+scale+panX+panY)||width<=0||height<=0||scale<=0||Math.Abs(Math.Sin(pitch))<.015)return 0;
var cy=Math.Cos(yaw);var sy=Math.Sin(yaw);var sp=Math.Sin(pitch);
var minX=double.PositiveInfinity;var maxX=double.NegativeInfinity;var minZ=minX;var maxZ=maxX;
for(var i=0;i<4;i++)
{
var x=((i%2)*width-width/2-panX)/scale;var depth=((i/2)*height-height/2-panY)/(scale*sp);
var wx=x*cy-depth*sy;var wz=x*sy+depth*cy;
minX=Math.Min(minX,wx);maxX=Math.Max(maxX,wx);minZ=Math.Min(minZ,wz);maxZ=Math.Max(maxZ,wz);
}
var budget=Math.Min(MaximumLines,output.Length);var wanted=Math.Max(Math.Max(maxX-minX,maxZ-minZ)/(budget/2-4),24/(scale*Math.Abs(sp)));
var decade=Math.Pow(10,Math.Floor(Math.Log10(wanted)));var fraction=wanted/decade;
step=decade*(fraction<=1?1:fraction<=2?2:fraction<=5?5:10);
if(!double.IsFinite(step)||step<=0)return 0;
var count=0;
for(var axis=0;axis<2;axis++)
{
var first=Math.Ceiling((axis==0?minX:minZ)/step);var n=(int)Math.Min(budget,Math.Floor((axis==0?maxX:maxZ)/step)-first+1);
for(var i=0;i<n&&count<budget;i++)
{
var at=(first+i)*step;
var x1=axis==0?at:minX;var x2=axis==0?at:maxX;var z1=axis==0?minZ:at;var z2=axis==0?maxZ:at;
var ax=width/2+panX+(x1*cy+z1*sy)*scale;var ay=height/2+panY+(-x1*sy+z1*cy)*sp*scale;
var bx=width/2+panX+(x2*cy+z2*sy)*scale;var by=height/2+panY+(-x2*sy+z2*cy)*sp*scale;
if(Clip(ax,ay,bx,by,width,height,out var line))output[count++]=line;
}
}
return count;
}
private static bool Clip(double x1,double y1,double x2,double y2,double width,double height,out MapSegment line)
{
line=default;var dx=x2-x1;var dy=y2-y1;var low=0d;var high=1d;
if(!Bound(-dx,x1,ref low,ref high)||!Bound(dx,width-x1,ref low,ref high)||!Bound(-dy,y1,ref low,ref high)||!Bound(dy,height-y1,ref low,ref high))return false;
line=new(x1+dx*low,y1+dy*low,x1+dx*high,y1+dy*high);return true;
}
private static bool Bound(double p,double q,ref double low,ref double high)
{
if(Math.Abs(p)<1e-12)return q>=-1e-7;
var t=q/p;if(p<0){if(t>high)return false;low=Math.Max(low,t);}else{if(t<low)return false;high=Math.Min(high,t);}return true;
}
}
namespace XFEEDToolBox.Core.Galaxy;
public readonly record struct StarColour(byte R,byte G,byte B);
public enum StellarMorphology { Star, BrownDwarf, WhiteDwarf, NeutronStar, BlackHole }
public readonly record struct StellarSprite(float Radius,float CoreRadius,float Opacity,StellarMorphology Morphology,float Corona);
public static class StellarAppearance
{
/// <summary>
/// Original Galaxy52B instance size is converted to IEEE half before the
/// VS multiplies a unit camera-facing corner. Project without a pixel floor.
/// </summary>
public static float OriginalProjectedHalfExtent(float styleSize,double pixelsPerLightYear)
{
if(!float.IsFinite(styleSize)||styleSize<=0||!double.IsFinite(pixelsPerLightYear)||pixelsPerLightYear<=0)return 0;
var projected=(double)(float)(Half)styleSize*pixelsPerLightYear;
return double.IsFinite(projected)&&projected<=float.MaxValue?(float)projected:0;
}
/// <summary>
/// Original physical BackgroundStars layer selection(3B32EE0), from the
/// frozen shared-appearance maps50/60. Unmapped classes return layer13.
/// Types41/42 also require the original separate black-hole renderer.
/// </summary>
public static int OriginalSprite(ushort classification)
{
var type=classification&63;
if(type is 41 or 42)return 8;
var luminosity=(classification>>10)&31;
if(luminosity<=13)return 5;
if(luminosity==24)return 6;
return type switch
{
0 or 1=>0,2 or 3 or 4 or 5 or 6=>1,7 or 8 or 11=>2,9 or 10=>3,
12 or 13 or 14 or 15 or 16=>4,17 or 18 or 19 or 20 or 21 or 22 or 23 or 24=>5,
40=>7,41 or 42=>8,43=>9,46=>10,_=>13
};
}
public static readonly IReadOnlyList<StarColour> Palette=[new(160,188,255),new(181,203,255),new(222,233,255),new(249,247,235),new(255,239,177),new(255,187,107),new(255,115,70),new(215,77,58),new(170,83,118),new(129,94,133),new(208,233,255),new(164,224,255),new(166,161,222),new(226,230,240)];
public static int ColourIndex(string value)
{
var s=value.Trim().ToUpperInvariant();
if(s.Length==0)return 13;
if(s=="H"||s.Contains("BLACK HOLE",StringComparison.Ordinal)||s.Contains("BLACKHOLE",StringComparison.Ordinal))return 12;
if(s=="N"||s.Contains("NEUTRON",StringComparison.Ordinal))return 11;
if(s.StartsWith('D')||s.Contains("WHITE DWARF",StringComparison.Ordinal))return 10;
if(s.StartsWith('W')||s.StartsWith("AEBE",StringComparison.Ordinal)||s.Contains("HERBIG",StringComparison.Ordinal))return 0;
if(s is "TTS"||s.Contains("T TAURI",StringComparison.Ordinal))return 5;
if(s[0]=='C'||s is "S" or "MS"||s.StartsWith("S ",StringComparison.Ordinal)||s.StartsWith("S-TYPE",StringComparison.Ordinal)||s.StartsWith("MS-TYPE",StringComparison.Ordinal))return 6;
const string classes="OBAFGKMLTY";
var index=classes.IndexOf(s[0]);
return index>=0&&(s.Length==1||s[1] is ' ' or '_'||char.IsDigit(s[1]))?index:13;
}
public static float PhysicalRadius(string value)
{
var type=value.Replace("_"," ",StringComparison.Ordinal);
if(type.Contains("Supergiant",StringComparison.OrdinalIgnoreCase)||type.Contains("Super Giant",StringComparison.OrdinalIgnoreCase))return 2.4f;
if(type.Contains("Giant",StringComparison.OrdinalIgnoreCase))return 1.8f;
return 1;
}
/// <summary>Original territory point-size mode 2; geometry mode 1 does not use it.</summary>
public static float PowerTerritoryDistanceScale(double eyeDistance)=>
double.IsFinite(eyeDistance)?(float)Math.Max(1,Math.Min(Math.Max(0,eyeDistance),50)/25):1;
/// <summary>
/// Screen-space stellar light. A star's spectral class controls its photosphere;
/// route and selection importance controls only the marker allocation around it.
/// Distance here is measured from the camera focus, not from Sol or the player.
/// </summary>
public static StellarSprite Sprite(int spectral,double distance,double pixelsPerLightYear,int importance=0,bool realistic=true,float originalSize=0)
{
spectral=spectral>=0&&spectral<Palette.Count?spectral:13;
distance=double.IsFinite(distance)?Math.Max(0,distance):0;
pixelsPerLightYear=double.IsFinite(pixelsPerLightYear)?Math.Max(.00001,pixelsPerLightYear):4;
var morphology=spectral switch{7 or 8 or 9=>StellarMorphology.BrownDwarf,10=>StellarMorphology.WhiteDwarf,11=>StellarMorphology.NeutronStar,12=>StellarMorphology.BlackHole,_=>StellarMorphology.Star};
var hasOriginalSize=realistic&&float.IsFinite(originalSize)&&originalSize>0&&originalSize<=8;
var luminosity=hasOriginalSize?1:spectral switch{0=>1.28,1=>1.17,2=>1.1,3=>1.04,4=>1,5=>.9,6=>.78,7=>.64,8=>.56,9=>.48,10=>.74,11=>.69,12=>.85,_=>.9};
// Keep distant stars sub-pixel instead of giving every catalogue entry the
// same large opaque disc. Zooming nearby reveals a small limb-darkened core.
var zoom=Math.Clamp(Math.Pow(pixelsPerLightYear/4,.24),.48,3.3);
var attenuation=1/Math.Sqrt(1+distance*pixelsPerLightYear/420);
var core=(float)Math.Clamp(1.38*luminosity*zoom*(.48+.52*attenuation),.42,6);
// The original compact's logarithmic size replaces the estimated
// spectral size. Keep its ratio to original uniform size .7, including
// the original small-size range, instead of applying a pixel minimum.
if(hasOriginalSize)core*=originalSize/GalaxyOriginalAppearance.DefaultSize;
var opacity=(float)Math.Clamp((.3+.7*attenuation)*(spectral is 7 or 8 or 9?.68:1),.12,1);
if(!realistic){core=Math.Clamp(core*1.4f,2.6f,4.8f);opacity=Math.Max(.78f,opacity);}
var corona=spectral switch{7 or 8 or 9=>.065f,10=>.16f,11=>.18f,12=>0,_=>.2f};
var radius=realistic?Math.Max(4.5f,core*6):Math.Max(3,core*1.85f);
if(morphology==StellarMorphology.NeutronStar&&realistic)radius=Math.Max(radius,core*8);
if(importance>=2)radius=Math.Max(radius,26);
return new(radius,core,opacity,morphology,corona);
}
}
public readonly record struct MapSegment(double X1,double Y1,double X2,double Y2);
public static class InfiniteGalaxyGrid
{
public const int MaximumLines=GalaxyOriginalGrid.MaximumLines;
// Invert the actual camera projection at the viewport corners. Only the
// visible portion of the unbounded XZ plane is emitted, with bounded density.
public static int Write(Span<MapSegment> output,double width,double height,double yaw,double pitch,double scale,double panX,double panY,out double step)
{
step=0;
if(output.Length<8||!double.IsFinite(width+height+yaw+pitch+scale+panX+panY)||width<=0||height<=0||scale<=0||Math.Abs(Math.Sin(pitch))<.015)return 0;
var cy=Math.Cos(yaw);var sy=Math.Sin(yaw);var sp=Math.Sin(pitch);
var minX=double.PositiveInfinity;var maxX=double.NegativeInfinity;var minZ=minX;var maxZ=maxX;
for(var i=0;i<4;i++)
{
var x=((i%2)*width-width/2-panX)/scale;var depth=((i/2)*height-height/2-panY)/(scale*sp);
var wx=x*cy-depth*sy;var wz=x*sy+depth*cy;
minX=Math.Min(minX,wx);maxX=Math.Max(maxX,wx);minZ=Math.Min(minZ,wz);maxZ=Math.Max(maxZ,wz);
}
var budget=Math.Min(MaximumLines,output.Length);var wanted=Math.Max(Math.Max(maxX-minX,maxZ-minZ)/(budget/2-4),24/(scale*Math.Abs(sp)));
var decade=Math.Pow(10,Math.Floor(Math.Log10(wanted)));var fraction=wanted/decade;
step=decade*(fraction<=1?1:fraction<=2?2:fraction<=5?5:10);
if(!double.IsFinite(step)||step<=0)return 0;
var count=0;
for(var axis=0;axis<2;axis++)
{
var first=Math.Ceiling((axis==0?minX:minZ)/step);var n=(int)Math.Min(budget,Math.Floor((axis==0?maxX:maxZ)/step)-first+1);
for(var i=0;i<n&&count<budget;i++)
{
var at=(first+i)*step;
var x1=axis==0?at:minX;var x2=axis==0?at:maxX;var z1=axis==0?minZ:at;var z2=axis==0?maxZ:at;
var ax=width/2+panX+(x1*cy+z1*sy)*scale;var ay=height/2+panY+(-x1*sy+z1*cy)*sp*scale;
var bx=width/2+panX+(x2*cy+z2*sy)*scale;var by=height/2+panY+(-x2*sy+z2*cy)*sp*scale;
if(Clip(ax,ay,bx,by,width,height,out var line))output[count++]=line;
}
}
return count;
}
private static bool Clip(double x1,double y1,double x2,double y2,double width,double height,out MapSegment line)
{
line=default;var dx=x2-x1;var dy=y2-y1;var low=0d;var high=1d;
if(!Bound(-dx,x1,ref low,ref high)||!Bound(dx,width-x1,ref low,ref high)||!Bound(-dy,y1,ref low,ref high)||!Bound(dy,height-y1,ref low,ref high))return false;
line=new(x1+dx*low,y1+dy*low,x1+dx*high,y1+dy*high);return true;
}
private static bool Bound(double p,double q,ref double low,ref double high)
{
if(Math.Abs(p)<1e-12)return q>=-1e-7;
var t=q/p;if(p<0){if(t>high)return false;low=Math.Max(low,t);}else{if(t<low)return false;high=Math.Min(high,t);}return true;
}
}