import * as THREE from "three" ; import { FontCatalog } from "./rendering/FontCatalog" ; import { GlyphData } from "./rendering/GlyphData" ; import { TextBufferObject } from "./rendering/TextBufferObject" ; import { QUAD_VERTEX_MEMORY_FOOTPRINT , TextGeometry } from "./rendering/TextGeometry" ; import { SdfTextMaterial } from "./rendering/TextMaterials" ; import { FontVariant , TextLayoutStyle , TextRenderStyle } from "./rendering/TextStyle" ; import { LineTypesetter } from "./typesetting/LineTypesetter" ; import { PathTypesetter , PathTypesettingParameters } from "./typesetting/PathTypesetter" ; import { TypesettingParameters } from "./typesetting/Typesetter" ; import { createSdfTextMaterial } from "./utils/MaterialUtils" ; const tempTextPosition = new THREE . Vector3 ( ) ; const tempTextBounds = { array : [ new THREE . Box2 ( ) ] , offset : 0 } ; let tempVertexBuffer = new Float32Array ( ) ; interface TextPlacementParameters { input : string | GlyphData [ ] ; layer : TextCanvasLayer ; textPath ? : THREE . Path | THREE . CurvePath < THREE . Vector2 > ; textPathOverflow ? : boolean ; bounds ? : THREE . Box2 ; individualBounds ? : THREE . Box2 [ ] ; computeTextBuffer ? : boolean ; letterCaseArray ? : boolean [ ] ; } /**
 * Optional parameters passed on [[TextCanvas]].`measureText` function call.
 */ export interface MeasurementParameters { /**
     * Path where text should be placed on. Overrides the original position parameter.
     */ path ? : THREE . Path | THREE . CurvePath < THREE . Vector2 > ; /**
     * If `true`, text on a path will be placed even when its size its bigger than the path's size.
     */ pathOverflow ? : boolean ; /**
     * Output per-character bounds.
     */ outputCharacterBounds ? : THREE . Box2 [ ] ; /**
     * Array containing info on whether the glyphs are upper or lower case. Needed to support
     * `SmallCaps`.
     */ letterCaseArray ? : boolean [ ] ; } /**
 * Optional parameters passed on [[TextCanvas]].`addText` function call.
 */ export interface AdditionParameters { /**
     * Path where text should be placed on. Overrides the original position parameter.
     */ path ? : THREE . Path | THREE . CurvePath < THREE . Vector2 > ; /**
     * If `true`, text on a path will be placed even when its size its bigger than the path's size.
     */ pathOverflow ? : boolean ; /**
     * Layer where text will be added.
     */ layer ? : number ; /**
     * If `true`, the input position parameter will be updated to contain the position of the last
     * glyph added.
     */ updatePosition ? : boolean ; /**
     * Object containing additional data intended to be retrieved during picking.
     */ pickingData ? : any ; /**
     * Array containing info on whether the glyphs are upper or lower case. Needed to support
     * `SmallCaps`.
     */ letterCaseArray ? : boolean [ ] ; } /**
 * Optional parameters passed on [[TextCanvas]].`createTextBufferObject` function call.
 */ export interface TextBufferCreationParameters { /**
     * Path where text should be placed on. Overrides the original position parameter.
     */ path ? : THREE . Path | THREE . CurvePath < THREE . Vector2 > ; /**
     * If `true`, text on a path will be placed even when its size its bigger than the path's size.
     */ pathOverflow ? : boolean ; /**
     * Output text bounding-box.
     */ outputBounds ? : boolean ; /**
     * Output per-character bounds.
     */ outputCharacterBounds ? : boolean ; /**
     * Array containing info on whether the glyphs are upper or lower case. Needed to support
     * `SmallCaps`.
     */ letterCaseArray ? : boolean [ ] ; /**
     * If `true`, both the [[TextRenderStyle]] and [[TextLayoutStyle]] used to generate the
     * [[TextBufferObject]] will be stored in it.
     */ storeStyles ? : boolean ; } /**
 * Optional parameters passed on [[TextCanvas]].`addTextBufferObject` function call.
 */ export interface TextBufferAdditionParameters { layer ? : number ; position ? : THREE . Vector3 ; scale ? : number ; rotation ? : number ; color ? : THREE . Color ; opacity ? : number ; backgroundColor ? : THREE . Color ; backgroundOpacity ? : number ; pickingData ? : any ; } /**
 * Default's [[TextCanvas]] layer identifier.
 */ export const DEFAULT_TEXT_CANVAS_LAYER = 0 ; /**
 * [[TextCanvas]] rendering layer.
 */ export interface TextCanvasLayer { id : number ; storage : TextGeometry ; } /**
 * [[TextCanvas]] construction parameters.
 */ export interface TextCanvasParameters { /**
     * WebGLRenderer internally used by this `TextCanvas`.
     */ renderer : THREE . WebGLRenderer ; /**
     * Initial [[FontCatalog]].
     */ fontCatalog : FontCatalog ; /**
     * Minimum amount of glyphs each [[TextCanvas]] layer can store.
     */ minGlyphCount : number ; /**
     * Maximum amount of glyphs each [[TextCanvas]] layer can store.
     */ maxGlyphCount : number ; /**
     * Material used to render text.
     */ material ? : THREE . Material ; /**
     * Material used to render text background.
     */ backgroundMaterial ? : THREE . Material ; /**
     * Optional Canvas Name
     */ name ? : string ; } /**
 * Describes estimated usage of memory on heap and GPU.
 */ export interface MemoryUsage { heapSize : number ; gpuSize : number ; } /**
 * three.js text rendering engine which can manage and render high-quality, transformable, stylable
 * and properly layout SDF and MSDF text.
 */ export class TextCanvas { private static readonly defaultTextRenderStyle : TextRenderStyle = new TextRenderStyle ( ) ; private static readonly defaultTextLayoutStyle : TextLayoutStyle = new TextLayoutStyle ( ) ; /**
     * Minimum amount of glyphs each [[TextCanvas]] layer can store.
     */ readonly minGlyphCount : number ; /**
     * Maximum amount of glyphs each [[TextCanvas]] layer can store.
     */ readonly maxGlyphCount : number ; readonly name ? : string ; private readonly m_renderer : THREE . WebGLRenderer ; private m_fontCatalog : FontCatalog ; private readonly m_currentTextRenderStyle : TextRenderStyle ; private readonly m_currentTextLayoutStyle : TextLayoutStyle ; private m_material : SdfTextMaterial | THREE . Material ; private m_bgMaterial : SdfTextMaterial | THREE . Material ; private m_ownsMaterial : boolean ; private m_ownsBgMaterial : boolean ; private readonly m_defaultLayer : TextCanvasLayer ; private readonly m_layers : TextCanvasLayer [ ] ; private readonly m_lineTypesetter : LineTypesetter ; private readonly m_pathTypesetter : PathTypesetter ; param params returns constructor ( params ) { this . m_renderer = params . renderer ; this . m_fontCatalog = params . fontCatalog ; this . minGlyphCount = params . minGlyphCount ; this . maxGlyphCount = params . maxGlyphCount ; this . name = params . name ; if ( params . material === undefined ) { this . m_ownsMaterial = true ; this . m_material = createSdfTextMaterial ( { fontCatalog : params . fontCatalog , rendererCapabilities : this . m_renderer . capabilities } ) ; } else { this . m_ownsMaterial = false ; this . m_material = params . material ; } if ( params . backgroundMaterial === undefined ) { this . m_ownsBgMaterial = true ; this . m_bgMaterial = createSdfTextMaterial ( { fontCatalog : params . fontCatalog , isBackground : true , rendererCapabilities : this . m_renderer . capabilities } ) ; } else { this . m_ownsBgMaterial = false ; this . m_bgMaterial = params . backgroundMaterial ; } this . m_defaultLayer = { id : DEFAULT_TEXT_CANVAS_LAYER , storage : new TextGeometry ( new THREE . Scene ( ) , this . m_material , this . m_bgMaterial , this . minGlyphCount , this . maxGlyphCount ) } ; this . m_layers = [ this . m_defaultLayer ] ; this . m_currentTextRenderStyle = new TextRenderStyle ( ) . copy ( TextCanvas . defaultTextRenderStyle ) ; this . m_currentTextLayoutStyle = new TextLayoutStyle ( ) . copy ( TextCanvas . defaultTextLayoutStyle ) ; this . m_lineTypesetter = new LineTypesetter ( ) ; this . m_pathTypesetter = new PathTypesetter ( ) ; } /**
     * Currently active [[FontCatalog]].
     */ get fontCatalog ( ) : FontCatalog { return this . m_fontCatalog ; } set fontCatalog ( value ) { this . m_fontCatalog = value ; const material = this . m_material as THREE . RawShaderMaterial ; material . uniforms . sdfTexture . value = this . m_fontCatalog . texture ; material . uniforms . sdfParams . value = new THREE . Vector4 ( this . m_fontCatalog . textureSize . x , this . m_fontCatalog . textureSize . y , this . m_fontCatalog . size , this . m_fontCatalog . distanceRange ) ; material . defines . MSDF = this . m_fontCatalog . type === "msdf" ? 1.0 : 0.0 ; const bgMaterial = this . m_bgMaterial as THREE . RawShaderMaterial ; bgMaterial . uniforms . sdfTexture . value = this . m_fontCatalog . texture ; bgMaterial . uniforms . sdfParams . value = new THREE . Vector4 ( this . m_fontCatalog . textureSize . x , this . m_fontCatalog . textureSize . y , this . m_fontCatalog . size , this . m_fontCatalog . distanceRange ) ; bgMaterial . defines . MSDF = this . m_fontCatalog . type === "msdf" ? 1.0 : 0.0 ; } /**
     * Currently active text rendering material.
     */ get material ( ) : THREE . Material { return this . m_material ; } set material ( value ) { if ( this . m_ownsMaterial ) { this . m_material . dispose ( ) ; this . m_ownsMaterial = false ; } this . m_material = value ; for ( const layer of this . m_layers ) { layer . storage . mesh . material = this . m_material ; } } /**
     * Currently active text background rendering material.
     */ get backgroundMaterial ( ) : THREE . Material { return this . m_bgMaterial ; } set backgroundMaterial ( value ) { if ( this . m_ownsBgMaterial ) { this . m_bgMaterial . dispose ( ) ; this . m_ownsBgMaterial = false ; } this . m_bgMaterial = value ; for ( const layer of this . m_layers ) { layer . storage . backgroundMesh . material = this . m_bgMaterial ; } } /**
     * Currently active text rendering style.
     */ get textRenderStyle ( ) : TextRenderStyle { return this . m_currentTextRenderStyle ; } set textRenderStyle ( style ) { this . m_currentTextRenderStyle . copy ( style ) ; } /**
     * Currently active text layout style.
     */ get textLayoutStyle ( ) : TextLayoutStyle { return this . m_currentTextLayoutStyle ; } set textLayoutStyle ( style ) { this . m_currentTextLayoutStyle . copy ( style ) ; } /**
     * Clears all the placed glyphs in this `TextCanvas` (as well as resetting the current style).
     */ clear ( ) { for ( const layer of this . m_layers ) { layer . storage . clear ( ) ; } this . m_currentTextRenderStyle . copy ( TextCanvas . defaultTextRenderStyle ) ; this . m_currentTextLayoutStyle . copy ( TextCanvas . defaultTextLayoutStyle ) ; } param camera param lowerLayerId param higherLayerId param target param clear render ( camera , lowerLayerId ? , higherLayerId ? , target ? , clear ? ) { this . m_fontCatalog . update ( this . m_renderer ) ; let oldTarget = null ; if ( target !== undefined ) { oldTarget = this . m_renderer . getRenderTarget ( ) ; this . m_renderer . setRenderTarget ( target ) ; } if ( clear === true ) { this . m_renderer . clear ( true ) ; } for ( let i = 0 ; i < this . m_layers . length ; i ++ ) { const layer = this . m_layers [ i ] ; if ( layer . id >= ( lowerLayerId ?? 0 ) ) { if ( higherLayerId === undefined || layer . id < higherLayerId ) { layer . storage . update ( ) ; this . m_renderer . render ( layer . storage . scene , camera ) ; } else { break ; } } } if ( target !== undefined ) { this . m_renderer . setRenderTarget ( oldTarget ) ; } } param layerId returns addLayer ( layerId ) { let result = this . getLayer ( layerId ) ; if ( result === undefined ) { result = { id : layerId , storage : new TextGeometry ( new THREE . Scene ( ) , this . m_material , this . m_bgMaterial , this . minGlyphCount , this . maxGlyphCount ) } ; this . m_layers . push ( result ) ; this . m_layers . sort ( ( a , b ) => { return a . id - b . id ; } ) ; } return result ; } param layerId returns getLayer ( layerId ) { return this . m_layers . find ( layer => layer . id === layerId ) ; } returns getAllLayers ( ) { return this . m_layers ; } param text param outputBounds param params returns measureText ( text , outputBounds , params ? ) { tempTextPosition . set ( 0 , 0 , 0 ) ; let path ; let pathOverflow ; let upperCaseArray ; let outputCharacterBounds ; if ( params !== undefined ) { path = params . path ; pathOverflow = params . pathOverflow ; outputCharacterBounds = params . outputCharacterBounds ; if ( params . path !== undefined ) { const pathOrigin = params . path . getPoint ( 0 ) ; if ( pathOrigin === null ) { return false ; } tempTextPosition . set ( pathOrigin . x , pathOrigin . y , 0.0 ) ; } if ( params . letterCaseArray ) { upperCaseArray = params . letterCaseArray ; } } return this . placeText ( { input : text , layer : this . m_defaultLayer , textPath : path , textPathOverflow : pathOverflow , bounds : outputBounds , individualBounds : outputCharacterBounds , letterCaseArray : upperCaseArray } ) ; } param text param position param params returns addText ( text , position , params ? ) { tempTextPosition . copy ( position ) ; let path ; let pathOverflow ; let upperCaseArray ; let targetLayer = this . m_defaultLayer ; if ( params !== undefined ) { path = params . path ; pathOverflow = params . pathOverflow ; if ( params . layer !== undefined ) { let tempLayer = this . getLayer ( params . layer ) ; if ( tempLayer === undefined ) { tempLayer = this . addLayer ( params . layer ) ; } targetLayer = tempLayer ; } if ( params . path !== undefined ) { tempTextPosition . set ( 0 , 0 , tempTextPosition . z ) ; } if ( params . letterCaseArray ) { upperCaseArray = params . letterCaseArray ; } } const prevDrawCount = targetLayer . storage . drawCount ; const result = this . placeText ( { input : text , textPath : path , textPathOverflow : pathOverflow , layer : targetLayer , letterCaseArray : upperCaseArray } ) ; if ( result && params !== undefined ) { if ( params . updatePosition === true ) { position . copy ( tempTextPosition ) ; } if ( params . pickingData !== undefined ) { targetLayer . storage . addPickingData ( prevDrawCount , targetLayer . storage . drawCount , params . pickingData ) ; } } else if ( ! result ) { ( targetLayer . storage as any ) . m_drawCount = prevDrawCount ; } return result ; } param text param params returns createTextBufferObject ( text , params ? ) { tempTextPosition . set ( 0 , 0 , 0 ) ; let glyphArray ; let upperCaseArray ; const smallCapsEnabled = this . m_currentTextRenderStyle . fontVariant === FontVariant . SmallCaps ; if ( typeof text !== "string" ) { glyphArray = text ; if ( params !== undefined && params . letterCaseArray ) { upperCaseArray = params . letterCaseArray ; } } else { upperCaseArray = [ ] ; glyphArray = this . m_fontCatalog . getGlyphs ( text , this . m_currentTextRenderStyle , smallCapsEnabled ? upperCaseArray : undefined ) ; if ( glyphArray === undefined ) { return undefined ; } } let path ; let pathOverflow ; let textBounds ; let characterBounds ; let renderStyle ; let layoutStyle ; if ( params !== undefined ) { path = params . path ; pathOverflow = params . pathOverflow ; if ( params . outputBounds === true ) { textBounds = new THREE . Box2 ( ) ; } if ( params . outputCharacterBounds === true ) { characterBounds = [ ] ; } if ( params . storeStyles === true ) { renderStyle = this . m_currentTextRenderStyle . clone ( ) ; layoutStyle = this . m_currentTextLayoutStyle . clone ( ) ; } } this . placeText ( { input : text , layer : this . m_defaultLayer , computeTextBuffer : true , textPath : path , textPathOverflow : pathOverflow , bounds : textBounds , individualBounds : characterBounds , letterCaseArray : upperCaseArray } ) ; return new TextBufferObject ( glyphArray , new Float32Array ( tempVertexBuffer ) , textBounds , characterBounds , renderStyle , layoutStyle ) ; } param textBufferObject param params returns addTextBufferObject ( textBufferObject , params ? ) { let targetLayer = this . m_defaultLayer ; let position ; let scale ; let rotation ; let color ; let opacity ; let bgColor ; let bgOpacity ; if ( params !== undefined ) { if ( params . layer !== undefined ) { let tempLayer = this . getLayer ( params . layer ) ; if ( tempLayer === undefined ) { tempLayer = this . addLayer ( params . layer ) ; } targetLayer = tempLayer ; } position = params . position ?. clone ( ) ; scale = params . scale ; rotation = params . rotation ; color = params . color ; opacity = params . opacity ; bgColor = params . backgroundColor ; bgOpacity = params . backgroundOpacity ; } const prevDrawCount = targetLayer . storage . drawCount ; const result = targetLayer . storage . addTextBufferObject ( textBufferObject , position , scale , rotation , color , opacity , bgColor , bgOpacity ) ; if ( result && params !== undefined ) { if ( params . pickingData !== undefined ) { targetLayer . storage . addPickingData ( prevDrawCount , targetLayer . storage . drawCount , params . pickingData ) ; } } else if ( ! result ) { ( targetLayer . storage as any ) . m_drawCount = prevDrawCount ; } return result ; } param screenPosition param pickCallback pickText ( position , callback ) { for ( const layer of this . m_layers ) { layer . storage . pick ( position , callback ) ; } } param info getMemoryUsage ( info ) { this . m_fontCatalog . updateMemoryUsage ( info ) ; for ( const layer of this . m_layers ) { layer . storage . updateMemoryUsage ( info ) ; } } private placeText ( params ) { if ( params . input . length === 0 || this . m_currentTextLayoutStyle . maxLines ! === 0 ) { if ( params . bounds !== undefined ) { params . bounds . min . set ( 0 , 0 ) ; params . bounds . max . set ( 0 , 0 ) ; } if ( params . individualBounds !== undefined ) { params . individualBounds . length = 0 ; } return true ; } let glyphArray ; let smallCapsTransformations ; const smallCapsEnabled = this . m_currentTextRenderStyle . fontVariant === FontVariant . SmallCaps ; if ( typeof params . input !== "string" ) { glyphArray = params . input ; if ( params . letterCaseArray ) { smallCapsTransformations = params . letterCaseArray ; } } else { smallCapsTransformations = [ ] ; glyphArray = this . m_fontCatalog . getGlyphs ( params . input , this . m_currentTextRenderStyle , smallCapsEnabled ? smallCapsTransformations : undefined ) ; if ( glyphArray === undefined ) { return false ; } } let glyphBounds ; if ( params . individualBounds !== undefined ) { tempTextBounds . array = params . individualBounds ; tempTextBounds . offset = 0 ; glyphBounds = tempTextBounds ; } if ( params . bounds !== undefined ) { params . bounds . min . set ( Infinity , Infinity ) ; params . bounds . max . set ( - Infinity , - Infinity ) ; } if ( params . computeTextBuffer === true ) { tempVertexBuffer = new Float32Array ( glyphArray . length * QUAD_VERTEX_MEMORY_FOOTPRINT ) ; } const isPath = params . textPath !== undefined ; const typesettingParams = { glyphs : glyphArray , fontCatalog : this . m_fontCatalog , textRenderStyle : this . m_currentTextRenderStyle , textLayoutStyle : this . m_currentTextLayoutStyle , position : tempTextPosition , geometry : params . layer . storage , smallCapsArray : smallCapsEnabled ? smallCapsTransformations : undefined , globalBounds : params . bounds , individualBounds : glyphBounds , vertexBuffer : params . computeTextBuffer === true ? tempVertexBuffer : undefined } ; let result = true ; if ( isPath ) { Object . assign ( typesettingParams as PathTypesettingParameters , { path : params . textPath , pathOverflow : params . textPathOverflow === true } ) ; result = this . m_pathTypesetter . arrangeGlyphs ( typesettingParams as PathTypesettingParameters ) ; } else { result = this . m_lineTypesetter . arrangeGlyphs ( typesettingParams ) ; } if ( glyphBounds !== undefined ) { glyphBounds . array . length = glyphBounds . offset ; } return result ; } }