import * as THREE from 'three/webgpu';
import SpriteTextModule from 'three-spritetext';
import { requestRenderAll } from './renderRequests';
const SpriteText = SpriteTextModule.default || SpriteTextModule;
/*
* Text sprites (labels, marker numbers) are drawn on a canvas by
* three-spritetext. To keep the text sharp the canvas resolution is chosen
* from the size of the text on screen instead of a fixed font size, and it
* is redrawn once web fonts have finished loading.
*/
//Canvas pixels per screen pixel, a little oversampling smooths the edges
const OVERSAMPLE = 1.5;
const MIN_FONT_SIZE = 16;
const MAX_FONT_SIZE = 256;
//Only redraw when the screen scale has changed by more than this ratio
const RESCALE_THRESHOLD = 0.1;
/*
* Screen pixels per unit of sprite height. Text sprites do not use size
* attenuation, their height is in view units at a distance of one, so this
* is viewportHeight * pixelRatio / (2 * tan(fov / 2)). Default to a 1000
* pixels viewport with the 40 degrees field of view used by the scenes.
*/
let pixelsPerUnit = 1000 / (2 * Math.tan((20 * Math.PI) / 180));
//Weak references to every live text sprite, so they can be redrawn
const sprites = new Set();
let fontListenerAdded = false;
const fontSizeForHeight = (textHeight) => {
const size = Math.round(textHeight * pixelsPerUnit * OVERSAMPLE);
return Math.min(Math.max(size, MIN_FONT_SIZE), MAX_FONT_SIZE);
};
/*
* The canvas matches the on screen size so mipmaps are not required,
* they blur the text when it is shrunk.
*/
const applyTextureSettings = (sprite) => {
const texture = sprite.material?.map;
if (texture) {
texture.generateMipmaps = false;
texture.minFilter = THREE.LinearFilter;
texture.magFilter = THREE.LinearFilter;
texture.anisotropy = 1;
}
};
/*
* SpriteText redraws its canvas and creates a new texture in every setter,
* set all font properties before a single redraw instead.
*/
const setFont = (sprite, fontFace, fontSize, fontWeight) => {
if ('_fontFace' in sprite && typeof sprite._genCanvas === 'function') {
sprite._fontFace = fontFace;
sprite._fontSize = fontSize;
sprite._fontWeight = fontWeight;
sprite._genCanvas();
} else {
sprite.fontFace = fontFace;
sprite.fontSize = fontSize;
sprite.fontWeight = fontWeight;
}
};
/*
* Redraw the sprite at the resolution required for its current height.
*/
const refreshTextSprite = (sprite, force = false) => {
const fontSize = fontSizeForHeight(sprite.textHeight);
if (force || fontSize !== sprite.fontSize) {
setFont(sprite, sprite.fontFace, fontSize, sprite.fontWeight);
applyTextureSettings(sprite);
}
};
const forEachSprite = (callback) => {
sprites.forEach((ref) => {
const sprite = ref.deref();
if (sprite) {
callback(sprite);
} else {
sprites.delete(ref);
}
});
};
const redrawAll = () => {
forEachSprite((sprite) => refreshTextSprite(sprite, true));
requestRenderAll();
};
/*
* The canvas is drawn with a fallback font if a web font has not been
* loaded yet, redraw once it is available.
*/
const waitForFont = (sprite) => {
if (typeof document === 'undefined' || !document.fonts) {
return;
}
if (!fontListenerAdded && document.fonts.addEventListener) {
//Catch fonts from stylesheets added after the sprites are created
document.fonts.addEventListener('loadingdone', redrawAll);
fontListenerAdded = true;
}
const font = `${sprite.fontWeight} ${sprite.fontSize}px ${sprite.fontFace}`;
try {
if (!document.fonts.check(font)) {
document.fonts
.load(font)
.then(() => {
refreshTextSprite(sprite, true);
requestRenderAll();
})
.catch(() => {});
}
} catch {
//Invalid font string, keep the fallback font
}
};
const registerTextSprite = (sprite) => {
sprites.add(new WeakRef(sprite));
};
/**
* Stop tracking a text sprite, call this when it is disposed.
*/
const releaseTextSprite = (sprite) => {
sprites.forEach((ref) => {
const trackedSprite = ref.deref();
if (!trackedSprite || trackedSprite === sprite) {
sprites.delete(ref);
}
});
};
/**
* Create a text sprite rendered with a node material, the canvas resolution
* is chosen from the size of the text on screen.
*
* @param {String} text - Text to display.
* @param {Number} textHeight - Height of a line of text in view units.
* @param {String} colour - CSS colour of the text.
* @param {String} fontFace - Font family.
* @param {Number|String} fontWeight - Font weight.
* @return {SpriteText}
*/
const createTextSprite = (text, textHeight, colour, fontFace, fontWeight) => {
const sprite = colour
? new SpriteText(text, textHeight, colour)
: new SpriteText(text, textHeight);
setFont(sprite, fontFace, fontSizeForHeight(textHeight), fontWeight);
//Replace the SpriteMaterial created by SpriteText with a node material
//using the same canvas texture.
const originalMaterial = sprite.material;
sprite.material = new THREE.SpriteNodeMaterial({
map: originalMaterial.map,
});
originalMaterial.map = null;
originalMaterial.dispose();
sprite.material.sizeAttenuation = false;
applyTextureSettings(sprite);
registerTextSprite(sprite);
waitForFont(sprite);
return sprite;
};
/**
* Set the height of a text sprite and redraw it once at the matching
* resolution.
*/
const setTextSpriteHeight = (sprite, textHeight) => {
if ('_textHeight' in sprite) {
sprite._textHeight = textHeight;
refreshTextSprite(sprite, true);
} else {
sprite.textHeight = textHeight;
refreshTextSprite(sprite);
}
};
/**
* Update the number of screen pixels per unit of text height, e.g. when the
* viewport is resized. Text sprites are redrawn if it has changed
* significantly.
*
* @param {Number} value - viewportHeight * pixelRatio / (2 * tan(fov / 2))
*/
const setTextPixelsPerUnit = (value) => {
if (!(value > 0) || !isFinite(value)) {
return;
}
if (Math.abs(value / pixelsPerUnit - 1) > RESCALE_THRESHOLD) {
pixelsPerUnit = value;
forEachSprite((sprite) => refreshTextSprite(sprite));
requestRenderAll();
}
};
const getTextPixelsPerUnit = () => pixelsPerUnit;
export {
applyTextureSettings as applyTextSpriteTextureSettings,
createTextSprite,
fontSizeForHeight,
getTextPixelsPerUnit,
refreshTextSprite,
registerTextSprite,
releaseTextSprite,
setTextPixelsPerUnit,
setTextSpriteHeight,
};