import { mat4 , vec2 , vec3 , vec4 } from 'gl-matrix' ; import { assert } from '../auxiliaries' ; import { v3 } from '../gl-matrix-extensions' ; import { FontFace } from './fontface' ; import { Glyph } from './glyph' ; import { GlyphVertices } from './glyphvertices' ; import { Label } from './label' ; type Fragment = [ number , number , Typesetter . FragmentType ] ; type Line = [ number , number , number ] ; /**
 * The typesetter is responsible for layouting text on the screen or in a virtual space. It takes a label,
 * which defines where it wants to appear (@see  Label ), and a font face that is used to display the
 * text, and computes the actual position for each glyph. Its output is a vertex array, which describes the glyphs
 * position and appearance on the screen/in the scene and which can be rendered using a LabelRenderPass.
  export class Typesetter { private static readonly DELIMITERS : string = '\x0A ,.-/()[]<>' ; param fontFace param pen param glyph param vertices param index private static writeVertex ( fontFace , pen , glyph , vertices , index ) { if ( vertices === undefined || glyph . depictable ( ) === false ) { return ; } const padding = fontFace . glyphTexturePadding ; const origin = vertices . origin ( index ) ; vec3 . set ( origin , pen [ 0 ] , pen [ 1 ] , 0.0 ) ; origin [ 0 ] += glyph . bearing [ 0 ] - padding [ 3 ] ; origin [ 1 ] += glyph . bearing [ 1 ] - glyph . extent [ 1 ] ; vec3 . set ( vertices . tangent ( index ) , glyph . extent [ 0 ] , 0.0 , 0.0 ) ; vec3 . set ( vertices . up ( index ) , 0.0 , glyph . extent [ 1 ] , 0.0 ) ; const lowerLeft = vertices . uvLowerLeft ( index ) ; vec2 . copy ( lowerLeft , glyph . subTextureOrigin ) ; const upperRight = vertices . uvUpperRight ( index ) ; vec2 . add ( upperRight , glyph . subTextureOrigin , glyph . subTextureExtent ) ; } param label private static lineAnchorOffset ( label ) { let offset = 0.0 ; const padding = label . fontFace ! . glyphTexturePadding ; const fontFace = label . fontFace ! ; switch ( label . lineAnchor ) { case Label . LineAnchor . Ascent : offset = fontFace . ascent - padding [ 0 ] ; break ; case Label . LineAnchor . Descent : offset = fontFace . descent * ( 1.0 + padding [ 0 ] / fontFace . ascent ) ; break ; case Label . LineAnchor . Center : offset = fontFace . ascent - padding [ 0 ] - 0.5 * fontFace . size ; break ; case Label . LineAnchor . Top : offset = fontFace . ascent - padding [ 0 ] + 0.5 * fontFace . lineGap ; break ; case Label . LineAnchor . Bottom : offset = fontFace . ascent - padding [ 0 ] + 0.5 * fontFace . lineGap - fontFace . lineHeight ; break ; case Label . LineAnchor . Baseline : default : offset = - padding [ 0 ] ; break ; } return offset ; } param label param text returns private static advances ( label , text ? ) { assert ( label . fontFace !== undefined , `expected a font face for label in order to resolve advances` ) ; if ( text === undefined ) { text = label . text . text ; } const advances = new Float32Array ( text . length ) ; for ( let i = 0 ; i < text . length ; ++ i ) { const charCode = text . charCodeAt ( i ) ; advances [ i ] = label . fontFace ! . glyph ( charCode ) . advance ; } return advances ; } param label param text returns private static kernings ( label , text ? ) { assert ( label . fontFace !== undefined , `expected a font face for label in order to resolve kernings` ) ; if ( text === undefined ) { const kernings = new Float32Array ( label . length ) ; for ( let i = 0 ; i < label . length ; ++ i ) { const kerning = label . kerningAfter ( i ) ; kernings [ i ] = isNaN ( kerning ) ? 0.0 : kerning ; } return kernings ; } const kerningAfter = ( index ) : number => { if ( index < 0 || index > text ! . length - 1 ) { return NaN ; } return label . fontFace ! . kerning ( text ! . charCodeAt ( index ) , text ! . charCodeAt ( index + 1 ) ) ; } ; const kernings = new Float32Array ( text . length ) ; for ( let i = 0 ; i < text . length ; ++ i ) { const kerning = kerningAfter ( i ) ; kernings [ i ] = isNaN ( kerning ) ? 0.0 : kerning ; } return kernings ; } param label returns private static fragments ( label ) { const fragments = new Array < Fragment > ( ) ; let isDelimiter ; let currentWordIndex = 0 ; for ( let i = 0 ; i < label . length ; ++ i ) { isDelimiter = Typesetter . DELIMITERS . indexOf ( label . charAt ( i ) ) > - 1 ; if ( ! isDelimiter ) { continue ; } if ( currentWordIndex < i ) { fragments . push ( [ currentWordIndex , i , Typesetter . FragmentType . Word ] ) ; } const type = label . lineFeedAt ( i ) ? Typesetter . FragmentType . LineFeed : Typesetter . FragmentType . Delimiter ; fragments . push ( [ i , i + 1 , type ] ) ; currentWordIndex = i + 1 ; } if ( ! isDelimiter ! ) { fragments . push ( [ currentWordIndex , label . length , Typesetter . FragmentType . Word ] ) ; } return fragments ; } param fragments param advances param kernings returns private static fragmentWidths ( fragments , advances , kernings ) { const widths = new Float32Array ( fragments . length ) ; for ( let i = 0 ; i < fragments . length ; ++ i ) { const fragment = fragments [ i ] ; widths [ i ] = advances . subarray ( fragment [ 0 ] , fragment [ 1 ] ) . reduce ( ( width , advance , index ) => width + advance + ( index < fragment [ 1 ] ? kernings [ index + fragment [ 0 ] ] : 0.0 ) , 0.0 ) ; } return widths ; } param label param ellipsisWidth returns private static elideThresholds ( label , ellipsisWidth ) { switch ( label . elide ) { case Label . Elide . Right : return [ label . lineWidth - ellipsisWidth , 0.0 ] ; case Label . Elide . Middle : const threshold = label . lineWidth / 2 - ellipsisWidth / 2 ; return [ threshold , threshold ] ; case Label . Elide . Left : return [ 0.0 , label . lineWidth - ellipsisWidth ] ; default : return [ 0.0 , 0.0 ] ; } } param threshold param labelFragments param labelFragmentWidths param labelAdvances param labelKernings param reverse returns private static elideFragments ( threshold , labelFragments , labelFragmentWidths , labelAdvances , labelKernings , reverse ) { const fragments = new Array < Fragment > ( ) ; const fragmentWidths = new Array < number > ( ) ; let width = 0.0 ; let lastLabelFragmentWidth = 0.0 ; for ( let i0 = reverse ? labelFragments . length - 1 : 0 ; reverse ? i0 > - 1 : i0 < labelFragments . length ; reverse ? -- i0 : ++ i0 ) { const fragment = labelFragments [ i0 ] ; if ( fragment [ 2 ] === Typesetter . FragmentType . LineFeed ) { continue ; } if ( width + labelFragmentWidths [ i0 ] < threshold ) { width += labelFragmentWidths [ i0 ] ; fragments . push ( fragment ) ; fragmentWidths . push ( labelFragmentWidths [ i0 ] ) ; continue ; } if ( fragment [ 2 ] === Typesetter . FragmentType . Delimiter || width + labelAdvances [ reverse ? fragment [ 1 ] - 1 : fragment [ 0 ] ] > threshold ) { break ; } for ( let i1 = reverse ? fragment [ 1 ] - 1 : fragment [ 0 ] ; reverse ? i1 >= fragment [ 0 ] : i1 < fragment [ 1 ] ; reverse ? -- i1 : ++ i1 ) { if ( width + lastLabelFragmentWidth + labelAdvances [ i1 ] + labelKernings [ i1 ] < threshold ) { lastLabelFragmentWidth += labelAdvances [ i1 ] + labelKernings [ i1 ] ; continue ; } fragments . push ( [ reverse ? i1 + 1 : fragment [ 0 ] , reverse ? fragment [ 1 ] : i1 , fragment [ 2 ] ] ) ; fragmentWidths . push ( lastLabelFragmentWidth ) ; break ; } break ; } return [ fragments , fragmentWidths , width + lastLabelFragmentWidth ] ; } param transform param vertices param begin param end private static transformVertices ( transform , vertices , begin , end ) { if ( vertices === undefined || mat4 . equals ( transform , mat4 . create ( ) ) ) { return ; } for ( let index = begin ; index < end ; ++ index ) { const origin = vertices . origin ( index ) ; const ll = v3 ( ) ; vec3 . transformMat4 ( ll , origin , transform ) ; const lr = v3 ( ) ; vec3 . transformMat4 ( lr , vec3 . add ( lr , origin , vertices . tangent ( index ) ) , transform ) ; const ul = v3 ( ) ; vec3 . transformMat4 ( ul , vec3 . add ( ul , origin , vertices . up ( index ) ) , transform ) ; vec3 . copy ( vertices . origin ( index ) , ll ) ; vec3 . sub ( vertices . tangent ( index ) , lr , ll ) ; vec3 . sub ( vertices . up ( index ) , ul , ll ) ; } } param currentRectangle param newRectangle private static updateRectangleMinMax ( currentRectangle , newRectangle ) { assert ( currentRectangle . length === 6 && newRectangle . length === 6 , `expected the rectangles to have 6 values!` ) ; let i = 0 ; for ( ; i < 3 ; i ++ ) { currentRectangle [ i ] = Math . min ( currentRectangle [ i ] , newRectangle [ i ] ) ; } for ( ; i < 6 ; i ++ ) { currentRectangle [ i ] = Math . max ( currentRectangle [ i ] , newRectangle [ i ] ) ; } } param currentMin param currentMax param values private static minMax ( currentMin , currentMax , values ) { const min = Math . min ( currentMin , ... values ) ; const max = Math . max ( currentMax , ... values ) ; return vec2 . fromValues ( min , max ) ; } param vertices param begin param end private static getMinMaxVertices ( vertices , begin , end ) { let minX = Number . POSITIVE_INFINITY ; let maxX = Number . NEGATIVE_INFINITY ; let minY = Number . POSITIVE_INFINITY ; let maxY = Number . NEGATIVE_INFINITY ; let minZ = Number . POSITIVE_INFINITY ; let maxZ = Number . NEGATIVE_INFINITY ; for ( let i = begin ; i < end ; ++ i ) { const x = Typesetter . minMax ( minX , maxX , [ vertices . origin ( i ) [ 0 ] , vertices . origin ( i ) [ 0 ] + vertices . up ( i ) [ 0 ] , vertices . origin ( i ) [ 0 ] + vertices . tangent ( i ) [ 0 ] ] ) ; minX = x [ 0 ] ; maxX = x [ 1 ] ; const y = Typesetter . minMax ( minY , maxY , [ vertices . origin ( i ) [ 1 ] , vertices . origin ( i ) [ 1 ] + vertices . up ( i ) [ 1 ] , vertices . origin ( i ) [ 1 ] + vertices . tangent ( i ) [ 1 ] ] ) ; minY = y [ 0 ] ; maxY = y [ 1 ] ; const z = Typesetter . minMax ( minZ , maxZ , [ vertices . origin ( i ) [ 2 ] , vertices . origin ( i ) [ 2 ] + vertices . up ( i ) [ 2 ] , vertices . origin ( i ) [ 2 ] + vertices . tangent ( i ) [ 2 ] ] ) ; minZ = z [ 0 ] ; maxZ = z [ 1 ] ; } return [ minX , minY , minZ , maxX , maxY , maxZ ] ; } param width param alignment param vertices param begin param end private static transformAlignment ( width , alignment , vertices , begin , end ) { if ( vertices === undefined || alignment === Label . Alignment . Left ) { return ; } let offset = - width ; if ( alignment === Label . Alignment . Center ) { offset *= 0.5 ; } for ( let index = begin ; index < end ; ++ index ) { vertices . origin ( index ) [ 0 ] += offset ; } } param label param vertices param lines private static transform ( label , vertices , lines ) { const boundingRectangle = [ Number . POSITIVE_INFINITY , Number . POSITIVE_INFINITY , Number . POSITIVE_INFINITY , Number . NEGATIVE_INFINITY , Number . NEGATIVE_INFINITY , Number . NEGATIVE_INFINITY ] ; for ( const line of lines ) { Typesetter . transformAlignment ( line [ 2 ] , label . alignment , vertices , line [ 0 ] , line [ 1 ] ) ; Typesetter . updateRectangleMinMax ( boundingRectangle , Typesetter . getMinMaxVertices ( vertices , line [ 0 ] , line [ 1 ] ) ) ; Typesetter . transformVertices ( label . staticTransform , vertices , line [ 0 ] , line [ 1 ] ) ; } const width = boundingRectangle [ 3 ] - boundingRectangle [ 0 ] ; const height = boundingRectangle [ 4 ] - boundingRectangle [ 1 ] ; const ll = vec4 . transformMat4 ( vec4 . create ( ) , vec4 . fromValues ( 0 , 0 , 0 , 1 ) , label . staticTransform ) ; const lr = vec4 . transformMat4 ( vec4 . create ( ) , vec4 . fromValues ( width , 0 , 0 , 1 ) , label . staticTransform ) ; const ul = vec4 . transformMat4 ( vec4 . create ( ) , vec4 . fromValues ( 0 , height , 0 , 1 ) , label . staticTransform ) ; const extent = vec2 . fromValues ( vec4 . distance ( lr , ll ) , vec4 . distance ( ul , ll ) ) ; label . extent = [ extent [ 0 ] , extent [ 1 ] ] ; } param label param vertices returns static typeset ( label , vertices ) { if ( label . length === 0 ) { return 0 ; } assert ( label . fontFace !== undefined , `expected a font face for label before typesetting` ) ; const fontFace = label . fontFace ! ; const glyphs = ( index ) : Glyph => index < label . length ? fontFace . glyph ( label . charCodeAt ( index ) ) : fontFace . glyph ( label . ellipsis . charCodeAt ( index - label . length ) ) ; const labelAdvances = Typesetter . advances ( label ) ; const labelKernings = Typesetter . kernings ( label ) ; const labelFragments = Typesetter . fragments ( label ) ; const labelFragmentWidths = Typesetter . fragmentWidths ( labelFragments , labelAdvances , labelKernings ) ; const pen = vec2 . fromValues ( 0.0 , - Typesetter . lineAnchorOffset ( label ) ) ; const lines = new Array < Line > ( ) ; let vertexIndex = 0 ; const elide = label . elide !== Label . Elide . None ; const advance = ( fragments , fragmentWidths , threshold = NaN , advances = labelAdvances , kernings = labelKernings , offset = 0 ) => { let firstIndexOfLine = vertexIndex ; for ( let i = 0 ; i < fragments . length ; ++ i ) { const fragment = fragments [ i ] ; if ( fragment [ 0 ] >= fragment [ 1 ] ) { continue ; } const lineFeed = ! elide && fragment [ 2 ] === Typesetter . FragmentType . LineFeed ; let wordWrap = false ; if ( ! elide && ! lineFeed && label . wrap ) { const depictable = fragment [ 2 ] !== Typesetter . FragmentType . Delimiter || glyphs ( fragment [ 0 ] ) . depictable ( ) ; const lookAhead = fragment [ 2 ] === Typesetter . FragmentType . Word && i < fragments . length - 1 && fragments [ i + 1 ] [ 2 ] === Typesetter . FragmentType . Delimiter ; const depictableAhead = lookAhead && glyphs ( fragments [ i + 1 ] [ 0 ] ) . depictable ( ) ; wordWrap = pen [ 0 ] + ( depictable ? fragmentWidths [ i ] : 0.0 ) + ( depictableAhead ? fragmentWidths [ i + 1 ] : 0.0 ) > label . lineWidth ; } if ( lineFeed || wordWrap ) { lines . push ( [ firstIndexOfLine , vertexIndex , pen [ 0 ] ] ) ; firstIndexOfLine = vertexIndex ; pen [ 0 ] = 0.0 ; pen [ 1 ] -= fontFace . lineHeight ; if ( lineFeed ) { continue ; } } for ( let i = fragment [ 0 ] ; i < fragment [ 1 ] ; ++ i ) { if ( glyphs ( i ) . depictable ( ) ) { Typesetter . writeVertex ( fontFace , pen , glyphs ( i ) , vertices , vertexIndex ) ; ++ vertexIndex ; } pen [ 0 ] += advances [ i - offset ] + kernings [ i - offset ] ; } } if ( firstIndexOfLine < vertexIndex ) { lines . push ( [ firstIndexOfLine , vertexIndex , pen [ 0 ] ] ) ; } } ; const fullApproximatedWidth = labelFragmentWidths . reduce ( ( accumulate , width ) => accumulate + width , 0.0 ) ; if ( elide && fullApproximatedWidth > label . lineWidth ) { const ellipsisFragments = [ [ label . length , label . length + label . ellipsis . length , Typesetter . FragmentType . Word ] ] ; const ellipsisAdvances = Typesetter . advances ( label , label . ellipsis ) ; const ellipsisKernings = Typesetter . kernings ( label , label . ellipsis ) ; const ellipsisFragmentWidths = Typesetter . fragmentWidths ( [ [ 0 , label . ellipsis . length , Typesetter . FragmentType . Word ] ] , ellipsisAdvances , ellipsisKernings ) ; const ellipsisWidth = ellipsisFragmentWidths [ 0 ] ; if ( label . lineWidth < ellipsisWidth ) { return 0 ; } const thresholds = Typesetter . elideThresholds ( label , ellipsisWidth ) ; const [ leftFragments , leftFragmentWidths , leftWidth ] = Typesetter . elideFragments ( thresholds [ 0 ] , labelFragments , labelFragmentWidths , labelAdvances , labelKernings , false ) ; if ( label . elide === Label . Elide . Middle ) { thresholds [ 1 ] += thresholds [ 0 ] - leftWidth ; } const [ rightFragments , rightFragmentWidths ] = Typesetter . elideFragments ( thresholds [ 1 ] , labelFragments , labelFragmentWidths , labelAdvances , labelKernings , true ) ; advance ( leftFragments , new Float32Array ( leftFragmentWidths ) ) ; advance ( ellipsisFragments , ellipsisFragmentWidths , NaN , ellipsisAdvances , ellipsisKernings , label . length ) ; advance ( rightFragments . reverse ( ) , new Float32Array ( rightFragmentWidths . reverse ( ) ) ) ; assert ( lines . length < 4 , `expected at most 3-line-yield from advance, given ${ lines }` ) ; lines [ 0 ] [ 1 ] = lines [ lines . length - 1 ] [ 1 ] ; lines [ 0 ] [ 2 ] = pen [ 0 ] ; lines . length = 1 ; } else { advance ( labelFragments , labelFragmentWidths , label . lineWidth ) ; } Typesetter . transform ( label , vertices , lines ) ; vertices . shrink ( vertexIndex ) ; return vertexIndex ; } } export namespace Typesetter { export enum FragmentType { Word = 0 , Delimiter = 1 , LineFeed = 2 } }