@@ -1,8 +1,13 @@
/**
* StructureLevelPass — MSDF-rendered level numbers above structures.
* StructureLevelPass — level numbers above structures.
*
* Renders level digits for structures with level > 1 using the same MSDF
* atlas and glyph infrastructure as NamePass. One instanced draw call per frame.
* Renders level digits for structures with level > 1 in one of two fonts,
* switchable at runtime via settings.structureLevel.classicFont:
* - classic: the round_6x6_modified bitmap font (white digits with a
* baked-in dark outline), matching the v31 StructureLayer look.
* - new: the overpass-bold MSDF font shared with NamePass.
* Both fonts are loaded up front; draw() binds the active one. One instanced
* draw call per frame.
*
* Only visible when zoom > dotsThreshold (matching structure icon visibility).
*/
@@ -24,13 +29,15 @@ import type { GlyphTables } from "./name-pass/AtlasData";
import { buildGlyphTables , parseAtlasData } from "./name-pass/AtlasData" ;
import { buildGlyphMetricsTex } from "./name-pass/DataTextures" ;
import { layoutString } from "./name-pass/TextLayout" ;
import type { BMChar , ParsedAtlas } from "./name-pass/Types" ;
import { CHAR_RANGE , MAX_CHARS } from "./name-pass/Types" ;
import { assetUrl } from "src/core/AssetUrls" ;
import fragSrc from "../shaders/structure-level/structure-level.frag.glsl?raw" ;
import vertSrc from "../shaders/structure-level/structure-level.vert.glsl?raw" ;
const atlasUrl = assetUrl ( "atlases/msdf-atlas .png" ) ;
const classicAtlasUrl = assetUrl ( "fonts/round_6x6_modified .png" ) ;
const msdfAtlasUrl = assetUrl ( "atlases/msdf-atlas.png" ) ;
// ---------------------------------------------------------------------------
// Constants
@@ -51,6 +58,51 @@ const MAX_LEVEL_CHARS = 4;
const FLOATS_PER_INSTANCE = 5 ; // worldX, worldY, cursorX, charCode, atlasIdx
const BYTES_PER_INSTANCE = FLOATS_PER_INSTANCE * 4 ;
// ---------------------------------------------------------------------------
// round_6x6_modified bitmap font (digits only)
// ---------------------------------------------------------------------------
// Atlas-level metrics, taken from resources/fonts/round_6x6_modified.xml.
const FONT_SIZE = 16 ;
const FONT_BASE = 16 ;
const FONT_SCALE_W = 208 ;
const FONT_SCALE_H = 114 ;
/**
* Digit glyph metrics for round_6x6_modified. Level labels only ever contain
* digits, which sit in a uniform 16× 16 grid at y=64 (x = digit·16) and share
* xadvance=14, xoffset=0, yoffset=0. See resources/fonts/round_6x6_modified.xml.
*/
function buildDigitChars ( ) : BMChar [ ] {
const chars : BMChar [ ] = [ ] ;
for ( let d = 0 ; d <= 9 ; d ++ ) {
chars . push ( {
id : 48 + d ,
char : String ( d ) ,
width : 16 ,
height : 16 ,
xoffset : 0 ,
yoffset : 0 ,
xadvance : 14 ,
x : d * 16 ,
y : 64 ,
page : 0 ,
} ) ;
}
return chars ;
}
/** Per-font GPU + CPU resources. */
interface FontBundle {
glyph : GlyphTables ;
metricsTex : WebGLTexture ;
fontSize : number ;
base : number ;
atlasScaleH : number ;
distanceRange : number ;
atlasTex : WebGLTexture | null ;
atlasReady : boolean ;
}
// ---------------------------------------------------------------------------
// StructureLevelPass
// ---------------------------------------------------------------------------
@@ -66,40 +118,41 @@ export class StructureLevelPass {
private uDotsThreshold : WebGLUniformLocation ;
private uScaleFactor : WebGLUniformLocation ;
private uIconGrowZoom : WebGLUniformLocation ;
private uFontSize : WebGLUniformLocation ;
private uAtlasScaleH : WebGLUniformLocation ;
private uBase : WebGLUniformLocation ;
private uDistRange : WebGLUniformLocation ;
private uOutlineWidth : WebGLUniformLocation ;
private uLevelScale : WebGLUniformLocation ;
private uLevelOffsetY : WebGLUniformLocation ;
private uHighlightMask : WebGLUniformLocation ;
private uHighlightDimAlpha : WebGLUniformLocation ;
private uClassic : WebGLUniformLocation ;
private vao : WebGLVertexArrayObject ;
private instanceBuf : DynamicInstanceBuffer ;
private instanceCount = 0 ;
private glyphMetricsTex : WebGLTexture ;
private atlasTex : WebGLTexture | null = null ;
private atlasReady = false ;
// CPU-side glyph tables for layoutString
private glyph : GlyphTables ;
private kernTable : Int8Array ;
// Both fonts are loaded; draw() selects per settings.structureLevel.classicFont.
private classic : FontBundle ;
private msdf : FontBundle ;
private kernTable : Int8Array ; // shared zero table — digits don't kern
private mapW : number ;
// Reusable buffers for layoutString
private charCodes = new Uint8Array ( MAX_CHARS ) ;
private cursors = new Float32Array ( MAX_CHARS ) ;
private distanceRange : number ;
private fontSize : number ;
private atlasScaleH : number ;
private base : number ;
/** unitType string → atlas column index (0– 5). */
private typeToAtlasCol = new Map < string , number > ( ) ;
/** Build-button hover highlight bitmask (0 = off). */
private highlightMask = 0 ;
// Last units uploaded + which font that layout used, so draw() can re-layout
// when the font toggles (digit advances differ between fonts).
private lastUnits : Map < number , UnitState > | null = null ;
private layoutClassic : boolean | null = null ;
constructor (
gl : WebGL2RenderingContext ,
header : RendererConfig ,
@@ -117,14 +170,42 @@ export class StructureLevelPass {
if ( col >= 0 ) this . typeToAtlasCol . set ( header . unitTypes [ i ] , col ) ;
}
// Parse atlas data (same source as NamePass)
const atlas = parseAtlasData ( ) ;
this . glyph = buildGlyphTables ( atlas . chars ) ;
this . kernTable = new Int8Array ( CHAR_RANGE * CHAR_RANGE ) ; // digits don't kern
this . distanceRange = atlas . distanceRange ;
this . fontSize = atlas . fontSize ;
this . atlasScaleH = atlas . scaleH ;
this . base = atlas . base ;
// Classic bitmap font (round_6x6_modified) — digits only.
const classicChars = buildDigitChars ( ) ;
const classicAtlas : ParsedAtlas = {
fontSize : FONT_SIZE ,
base : FONT_BASE ,
scaleW : FONT_SCALE_W ,
scaleH : FONT_SCALE_H ,
distanceRange : 0 ,
chars : classicChars ,
kernings : [ ] ,
} ;
this . classic = {
glyph : buildGlyphTables ( classicChars ) ,
metricsTex : buildGlyphMetricsTex ( gl , classicAtlas ) ,
fontSize : classicAtlas.fontSize ,
base : classicAtlas.base ,
atlasScaleH : classicAtlas.scaleH ,
distanceRange : classicAtlas.distanceRange ,
atlasTex : null ,
atlasReady : false ,
} ;
// New MSDF font (overpass-bold) — same atlas/data as NamePass.
const msdfAtlas = parseAtlasData ( ) ;
this . msdf = {
glyph : buildGlyphTables ( msdfAtlas . chars ) ,
metricsTex : buildGlyphMetricsTex ( gl , msdfAtlas ) ,
fontSize : msdfAtlas.fontSize ,
base : msdfAtlas.base ,
atlasScaleH : msdfAtlas.scaleH ,
distanceRange : msdfAtlas.distanceRange ,
atlasTex : null ,
atlasReady : false ,
} ;
// Compile shaders
this . program = createProgram ( gl , vertSrc , fragSrc ) ;
@@ -134,18 +215,7 @@ export class StructureLevelPass {
gl . uniform1i ( gl . getUniformLocation ( this . program , "uAtlas" ) , 0 ) ;
gl . uniform1i ( gl . getUniformLocation ( this . program , "uGlyphMetrics" ) , 1 ) ;
// Static uniform s
gl . uniform1f (
gl . getUniformLocation ( this . program , "uFontSize" ) ! ,
this . fontSize ,
) ;
gl . uniform1f (
gl . getUniformLocation ( this . program , "uAtlasScaleH" ) ! ,
this . atlasScaleH ,
) ;
gl . uniform1f ( gl . getUniformLocation ( this . program , "uBase" ) ! , this . base ) ;
// Dynamic uniform locations
// Uniform location s
this . uCamera = gl . getUniformLocation ( this . program , "uCamera" ) ! ;
this . uZoom = gl . getUniformLocation ( this . program , "uZoom" ) ! ;
this . uIconSize = gl . getUniformLocation ( this . program , "uIconSize" ) ! ;
@@ -155,6 +225,9 @@ export class StructureLevelPass {
) ! ;
this . uScaleFactor = gl . getUniformLocation ( this . program , "uScaleFactor" ) ! ;
this . uIconGrowZoom = gl . getUniformLocation ( this . program , "uIconGrowZoom" ) ! ;
this . uFontSize = gl . getUniformLocation ( this . program , "uFontSize" ) ! ;
this . uAtlasScaleH = gl . getUniformLocation ( this . program , "uAtlasScaleH" ) ! ;
this . uBase = gl . getUniformLocation ( this . program , "uBase" ) ! ;
this . uDistRange = gl . getUniformLocation ( this . program , "uDistRange" ) ! ;
this . uOutlineWidth = gl . getUniformLocation ( this . program , "uOutlineWidth" ) ! ;
this . uLevelScale = gl . getUniformLocation ( this . program , "uLevelScale" ) ! ;
@@ -167,12 +240,11 @@ export class StructureLevelPass {
this . program ,
"uHighlightDimAlpha" ,
) ! ;
this . uClassic = gl . getUniformLocation ( this . program , "uClassic" ) ! ;
// Glyph metrics data texture
this . glyphMetricsTex = buildGlyphMetricsTex ( gl , atlas ) ;
// Start async MSDF atlas load
this . loadAtlas ( ) ;
// Start async atlas loads (both fonts)
this . loadAtlas ( classicAtlasUrl , this . classic ) ;
this . loadAtlas ( msdfAtlasUrl , this . msdf ) ;
// Instance buffer
const glBuf = gl . createBuffer ( ) ! ;
@@ -212,7 +284,7 @@ export class StructureLevelPass {
gl . bindVertexArray ( null ) ;
}
private loadAtlas ( ) : void {
private loadAtlas ( url : string , bundle : FontBundle ) : void {
const img = new Image ( ) ;
img . crossOrigin = "anonymous" ;
img . onload = ( ) = > {
@@ -224,15 +296,19 @@ export class StructureLevelPass {
gl . texParameteri ( gl . TEXTURE_2D , gl . TEXTURE_WRAP_S , gl . CLAMP_TO_EDGE ) ;
gl . texParameteri ( gl . TEXTURE_2D , gl . TEXTURE_WRAP_T , gl . CLAMP_TO_EDGE ) ;
gl . texImage2D ( gl . TEXTURE_2D , 0 , gl . RGBA , gl . RGBA , gl . UNSIGNED_BYTE , img ) ;
this . atlasTex = tex ;
this . atlasReady = true ;
bundle . atlasTex = tex ;
bundle . atlasReady = true ;
} ;
img . src = atlasUrl ;
img . src = url ;
}
updateStructures ( units : Map < number , UnitState > ) : void {
let count = 0 ;
this . lastUnits = units ;
const classic = this . settings . structureLevel . classicFont ;
this . layoutClassic = classic ;
const glyph = classic ? this . classic.glyph : this.msdf.glyph ;
let count = 0 ;
for ( const unit of units . values ( ) ) {
if ( ! unit . isActive ) continue ;
if ( ! STRUCTURE_TYPES . has ( unit . unitType ) ) continue ;
@@ -241,7 +317,7 @@ export class StructureLevelPass {
const levelStr = unit . level . toString ( ) ;
layoutString (
levelStr ,
this . glyph ,
glyph ,
this . kernTable ,
this . charCodes ,
this . cursors ,
@@ -282,7 +358,15 @@ export class StructureLevelPass {
}
draw ( cameraMatrix : Float32Array , zoom : number ) : void {
if ( ! this . atlasReady || this . instanceCount === 0 ) return ;
const classic = this . settings . structureLevel . classicFont ;
// Re-layout if the font toggled since the buffer was built — digit advances
// (and so cursor positions) differ between the two fonts.
if ( this . lastUnits !== null && this . layoutClassic !== classic ) {
this . updateStructures ( this . lastUnits ) ;
}
const font = classic ? this . classic : this.msdf ;
if ( ! font . atlasReady || this . instanceCount === 0 ) return ;
const gl = this . gl ;
const ss = this . settings . structure ;
@@ -295,17 +379,21 @@ export class StructureLevelPass {
gl . uniform1f ( this . uDotsThreshold , ss . dotsZoomThreshold ) ;
gl . uniform1f ( this . uScaleFactor , ss . iconScaleFactorZoomedOut ) ;
gl . uniform1f ( this . uIconGrowZoom , ss . iconGrowZoom ) ;
gl . uniform1f ( this . uDistRange , this . distanceRange ) ;
gl . uniform1f ( this . uFontSize , font . fontSize ) ;
gl . uniform1f ( this . uAtlasScaleH , font . atlasScaleH ) ;
gl . uniform1f ( this . uBase , font . base ) ;
gl . uniform1f ( this . uDistRange , font . distanceRange ) ;
gl . uniform1f ( this . uOutlineWidth , sl . outlineWidth ) ;
gl . uniform1f ( this . uLevelScale , sl . scale ) ;
gl . uniform1f ( this . uLevelOffsetY , sl . offsetY ) ;
gl . uniform1i ( this . uHighlightMask , this . highlightMask ) ;
gl . uniform1f ( this . uHighlightDimAlpha , ss . highlightDimAlpha ) ;
gl . uniform1i ( this . uClassic , classic ? 1 : 0 ) ;
gl . activeTexture ( gl . TEXTURE0 ) ;
gl . bindTexture ( gl . TEXTURE_2D , this . atlasTex ! ) ;
gl . bindTexture ( gl . TEXTURE_2D , font . atlasTex ! ) ;
gl . activeTexture ( gl . TEXTURE1 ) ;
gl . bindTexture ( gl . TEXTURE_2D , this . glyphMetricsTex ) ;
gl . bindTexture ( gl . TEXTURE_2D , font . metricsTex ) ;
gl . bindVertexArray ( this . vao ) ;
gl . drawArraysInstanced ( gl . TRIANGLES , 0 , 6 , this . instanceCount ) ;
@@ -328,7 +416,9 @@ export class StructureLevelPass {
gl . deleteProgram ( this . program ) ;
this . instanceBuf . dispose ( ) ;
gl . deleteVertexArray ( this . vao ) ;
gl . deleteTexture ( this . glyphMetricsTex ) ;
if ( this . atlasTex ) gl . deleteTexture ( this . atlasTex ) ;
gl . deleteTexture ( this . classic . metricsTex ) ;
gl . deleteTexture ( this . msdf . metricsTex ) ;
if ( this . classic . atlasTex ) gl . deleteTexture ( this . classic . atlasTex ) ;
if ( this . msdf . atlasTex ) gl . deleteTexture ( this . msdf . atlasTex ) ;
}
}