3d-tiles-renderer
Version:
https://github.com/AnalyticalGraphicsInc/3d-tiles/tree/master/specification
181 lines (124 loc) • 4.32 kB
JavaScript
import { MVTGlyphs } from './MVTGlyphs.js';
// Scratch set for storing which characters are needed by the current glyphs
const _needed = new Set();
/**
* Renders text labels one glyph per character, laid out along each annotation's path. Characters are
* rasterized into the atlas on demand, so a label's text may change at any time.
* @extends MVTGlyphs
*/
export class MVTLabelGlyphs extends MVTGlyphs {
/**
* @param {Object} [options]
* @param {number} [options.size=16] - Glyph size in pixels.
* @param {number} [options.glyphSize] - Atlas slot size in pixels (defaults to `16 * devicePixelRatio`).
* @param {number} [options.slotCount=64] - Initial atlas slot capacity ( grows as needed ).
* @param {string|null} [options.font=null] - Explicit CSS font string; overrides `fontFamily`.
* @param {string} [options.fontFamily='sans-serif'] - Font family used to build the CSS font when
* `font` isn't given.
* @param {string} [options.strokeStyle='black'] - Outline color drawn under each glyph.
* @param {number} [options.strokeWidth=0] - Outline width in atlas pixels ( 0 disables the outline ).
*/
constructor( options = {} ) {
const {
size = 16,
glyphSize = 16 * window.devicePixelRatio,
slotCount = 64,
font = null,
fontFamily = 'sans-serif',
strokeStyle = 'black',
strokeWidth = 0,
} = options;
super();
// CSS font used to rasterize glyphs, sized to fit the atlas slot
const fontSize = Math.round( glyphSize * 0.7 );
this._font = font ?? `400 ${ fontSize }px ${ fontFamily }`;
// advance-width cache, keyed per character
this._advanceCache = new Map();
// styling
this._strokeStyle = strokeStyle;
this._strokeWidth = strokeWidth;
this.glyphAtlas.resize( slotCount, glyphSize );
this.size = size;
}
/**
* Resets the cached glyphs content. Used when changing fonts or styles.
*/
reset() {
this._advanceCache.clear();
this.glyphAtlas.clear();
}
/**
* Advance width of `char` in the label's size units, cached per character.
* @param {string} char - The character to measure.
* @returns {number} The advance width.
*/
measureChar( char ) {
const { _advanceCache, glyphAtlas, _font } = this;
if ( ! _advanceCache.has( char ) ) {
const multiplier = this.size / glyphAtlas.slotSize;
const info = glyphAtlas.measureChar( char, _font );
const advance = info.width + 2;
_advanceCache.set( char, advance * multiplier );
}
return _advanceCache.get( char );
}
// rasterize a character into the atlas, evicting a glyph that isn't needed this frame ( or
// growing the atlas if every rasterized glyph is still in use )
_drawChar( char, needed ) {
const { glyphAtlas } = this;
if ( glyphAtlas.capacity === glyphAtlas.count ) {
let toRemove = null;
for ( const c of glyphAtlas.keys() ) {
if ( ! needed.has( c ) ) {
toRemove = c;
break;
}
}
if ( toRemove !== null ) {
glyphAtlas.release( toRemove );
} else {
glyphAtlas.resize( glyphAtlas.capacity * 2 );
}
}
glyphAtlas.drawChar( char, char, {
font: this._font,
color: 'white',
strokeStyle: this._strokeStyle,
strokeWidth: this._strokeWidth,
} );
}
_updateGeometry() {
const { _orderedEntries, glyphAtlas } = this;
// collect the characters needed this frame from each items text, and
// the total glyph count
_needed.clear();
let count = 0;
for ( const entry of _orderedEntries ) {
const { text, characterPositions } = entry.item;
count += characterPositions.length;
for ( let c = 0, l = text.length; c < l; c ++ ) {
_needed.add( text[ c ] );
}
}
// make sure every needed character is rasterized before we look up its uv
for ( const char of _needed ) {
if ( ! glyphAtlas.has( char ) ) {
this._drawChar( char, _needed );
}
}
this._resizeGeometry( count );
let i = 0;
for ( const entry of _orderedEntries ) {
const anchor = entry.item;
const { fade } = entry;
const positions = anchor.characterPositions;
const angles = anchor.characterAngles;
const text = anchor.text;
for ( let c = 0, l = positions.length; c < l; c ++ ) {
this._writeGlyph( i ++, positions[ c ], text[ c ], fade, angles[ c ] );
}
}
this._markNeedsUpdate();
_needed.clear();
}
}