@@ -2,7 +2,9 @@ import Point from '@mapbox/point-geometry';
22import { mat2 , mat4 , vec3 , vec4 } from 'gl-matrix' ;
33import { addDynamicAttributes , updateGlobeVertexNormal } from '../data/bucket/symbol_bucket' ;
44import { WritingMode } from '../symbol/shaping' ;
5- import { calculateGlobeLabelMatrix } from '../geo/projection/globe_util' ;
5+ import { calculateGlobeLabelMatrix , globeToMercatorTransition } from '../geo/projection/globe_util' ;
6+ import { mercatorXfromLng , mercatorYfromLat } from '../geo/mercator_coordinate' ;
7+ import EXTENT from '../style-spec/data/extent' ;
68import { degToRad } from '../util/util' ;
79import { evaluateSizeForFeature , evaluateSizeForZoom } from './symbol_size' ;
810
@@ -42,6 +44,13 @@ type ProjectionCache = {
4244 [ _ : number ] : [ number , number , number ] ;
4345} ;
4446
47+ // Pre-computed per-tile data for globe-to-mercator blending of line label placement
48+ type GlobeLineBlend = {
49+ t : number ;
50+ invMatrix : mat4 ;
51+ mercCenter : [ number , number ] ;
52+ } ;
53+
4554type PlacementStatus = {
4655 needsFlipping ?: boolean ;
4756 notEnoughRoom ?: boolean ;
@@ -267,6 +276,19 @@ function updateLineLabels(bucket: SymbolBucket,
267276 const partiallyEvaluatedSize = evaluateSizeForZoom ( sizeData , painter . transform . zoom , scaleFactor ) ;
268277 const isGlobe = tr . projection . name === 'globe' ;
269278
279+ // Pre-compute the globe-to-mercator blend args once per tile so that the per-vertex
280+ // hot path (elevatePointAndProject) never needs to call createInversionMatrix again.
281+ const globeLineBlend : GlobeLineBlend | null = ( ( ) => {
282+ if ( ! isGlobe ) return null ;
283+ const t = globeToMercatorTransition ( tr . zoom ) ;
284+ if ( t === 0 ) return null ;
285+ return {
286+ t,
287+ invMatrix : tr . projection . createInversionMatrix ( tr , tileID . canonical ) ,
288+ mercCenter : [ mercatorXfromLng ( tr . center . lng ) , mercatorYfromLat ( tr . center . lat ) ] as [ number , number ] ,
289+ } ;
290+ } ) ( ) ;
291+
270292 const clippingBuffer : [ number , number ] = [ 256 / painter . width * 2 + 1 , 256 / painter . height * 2 + 1 ] ;
271293
272294 const dynamicLayoutVertexArray = isText ?
@@ -317,6 +339,16 @@ function updateLineLabels(bucket: SymbolBucket,
317339 const renderElevatedRoads = bucket . elevationType === 'road' ;
318340 const hasElevation = ! ! tr . elevation || renderElevatedRoads ;
319341 let { x, y, z} = tr . projection . projectTilePoint ( tileAnchorPoint . x , tileAnchorPoint . y , tileID . canonical ) ;
342+
343+ // During the globe-to-mercator transition, blend the anchor position so it
344+ // tracks with the blended tile geometry (mirrors the shader's mix_globe_mercator).
345+ if ( globeLineBlend ) {
346+ const [ mx , my , mz ] = getMercatorECEF ( tileAnchorPoint , tileID . canonical , globeLineBlend ) ;
347+ x += ( mx - x ) * globeLineBlend . t ;
348+ y += ( my - y ) * globeLineBlend . t ;
349+ z += ( mz - z ) * globeLineBlend . t ;
350+ }
351+
320352 let elevationParams : ElevationParams | null = null ;
321353 if ( hasElevation ) {
322354 const elevationFeature = renderElevatedRoads && bucket . hdExt ? bucket . hdExt . getElevationFeatureForPlacedSymbol ( bucket , isText ? bucket . text : bucket . icon , s ) : null ;
@@ -360,15 +392,15 @@ function updateLineLabels(bucket: SymbolBucket,
360392
361393 const elevationParamsForPlacement = pitchWithMap ? null : elevationParams ; // When pitchWithMap, we're projecting to scaled tile coordinate space: there is no need to get elevation as it doesn't affect projection.
362394 const placeUnflipped = placeGlyphsAlongLine ( symbol , pitchScaledFontSize , false /*unflipped*/ , keepUpright , posMatrix , labelPlaneMatrix , glCoordMatrix ,
363- bucket . glyphOffsetArray , lineVertexArray , dynamicLayoutVertexArray , globeExtVertexArray , labelPlaneAnchorPoint as unknown as [ number , number , number ] , tileAnchorPoint , projectionCache , aspectRatio , elevationParamsForPlacement , tr . projection , tileID , pitchWithMap , textMaxAngleThreshold ) ;
395+ bucket . glyphOffsetArray , lineVertexArray , dynamicLayoutVertexArray , globeExtVertexArray , labelPlaneAnchorPoint as unknown as [ number , number , number ] , tileAnchorPoint , projectionCache , aspectRatio , elevationParamsForPlacement , tr . projection , tileID , pitchWithMap , textMaxAngleThreshold , globeLineBlend ) ;
364396
365397 useVertical = placeUnflipped . useVertical ;
366398
367399 if ( elevationParamsForPlacement && placeUnflipped . needsFlipping ) projectionCache = { } ; // Truncated points should be recalculated.
368400 if ( placeUnflipped . notEnoughRoom || useVertical ||
369401 ( placeUnflipped . needsFlipping &&
370402 placeGlyphsAlongLine ( symbol , pitchScaledFontSize , true /*flipped*/ , keepUpright , posMatrix , labelPlaneMatrix , glCoordMatrix ,
371- bucket . glyphOffsetArray , lineVertexArray , dynamicLayoutVertexArray , globeExtVertexArray , labelPlaneAnchorPoint as unknown as [ number , number , number ] , tileAnchorPoint , projectionCache , aspectRatio , elevationParamsForPlacement , tr . projection , tileID , pitchWithMap , textMaxAngleThreshold ) . notEnoughRoom ) ) {
403+ bucket . glyphOffsetArray , lineVertexArray , dynamicLayoutVertexArray , globeExtVertexArray , labelPlaneAnchorPoint as unknown as [ number , number , number ] , tileAnchorPoint , projectionCache , aspectRatio , elevationParamsForPlacement , tr . projection , tileID , pitchWithMap , textMaxAngleThreshold , globeLineBlend ) . notEnoughRoom ) ) {
372404 hideGlyphs ( numGlyphs , dynamicLayoutVertexArray ) ;
373405 }
374406 }
@@ -403,7 +435,8 @@ function placeFirstAndLastGlyph(
403435 projection : Projection ,
404436 tileID : OverscaledTileID ,
405437 pitchWithMap : boolean ,
406- textMaxAngleThreshold : number
438+ textMaxAngleThreshold : number ,
439+ blend ?: GlobeLineBlend | null ,
407440) : null | {
408441 first : PlacedGlyph ;
409442 last : PlacedGlyph ;
@@ -418,13 +451,13 @@ function placeFirstAndLastGlyph(
418451
419452 const firstPlacedGlyph = placeGlyphAlongLine ( fontScale * firstGlyphOffset , lineOffsetX , lineOffsetY , flip , anchorPoint , tileAnchorPoint , segment ,
420453 lineStartIndex , lineEndIndex , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , returnPathInTileCoords , true , projection , tileID , pitchWithMap ,
421- textMaxAngleThreshold ) ;
454+ textMaxAngleThreshold , blend ) ;
422455 if ( ! firstPlacedGlyph )
423456 return null ;
424457
425458 const lastPlacedGlyph = placeGlyphAlongLine ( fontScale * lastGlyphOffset , lineOffsetX , lineOffsetY , flip , anchorPoint , tileAnchorPoint , segment ,
426459 lineStartIndex , lineEndIndex , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , returnPathInTileCoords , true , projection , tileID , pitchWithMap ,
427- textMaxAngleThreshold ) ;
460+ textMaxAngleThreshold , blend ) ;
428461 if ( ! lastPlacedGlyph )
429462 return null ;
430463
@@ -480,7 +513,8 @@ function placeGlyphsAlongLine(
480513 projection : Projection ,
481514 tileID : OverscaledTileID ,
482515 pitchWithMap : boolean ,
483- textMaxAngleThreshold : number
516+ textMaxAngleThreshold : number ,
517+ blend ?: GlobeLineBlend | null ,
484518) : PlacementStatus {
485519 const fontScale = fontSize / 24 ;
486520 const lineOffsetX = symbol . lineOffsetX * fontScale ;
@@ -504,7 +538,7 @@ function placeGlyphsAlongLine(
504538 if ( numGlyphs > 1 ) {
505539 // Place the first and the last glyph in the label first, so we can figure out
506540 // the overall orientation of the label and determine whether it needs to be flipped in keepUpright mode
507- const firstAndLastGlyph = placeFirstAndLastGlyph ( fontScale , glyphOffsetArray , lineOffsetX , lineOffsetY , flip , anchorPoint , tileAnchorPoint , symbol , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , false , projection , tileID , pitchWithMap , textMaxAngleThreshold ) ;
541+ const firstAndLastGlyph = placeFirstAndLastGlyph ( fontScale , glyphOffsetArray , lineOffsetX , lineOffsetY , flip , anchorPoint , tileAnchorPoint , symbol , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , false , projection , tileID , pitchWithMap , textMaxAngleThreshold , blend ) ;
508542 if ( ! firstAndLastGlyph ) {
509543 return { notEnoughRoom : true } ;
510544 }
@@ -525,7 +559,7 @@ function placeGlyphsAlongLine(
525559 for ( let glyphIndex = glyphStartIndex + 1 ; glyphIndex < glyphStartIndex + numGlyphs - 1 ; glyphIndex ++ ) {
526560 // Since first and last glyph fit on the line, the rest of the glyphs can be placed too, but check to make sure
527561 const glyph = placeGlyphAlongLine ( fontScale * glyphOffsetArray . getoffsetX ( glyphIndex ) , lineOffsetX , lineOffsetY , flip , anchorPoint , tileAnchorPoint , segment ,
528- lineStartIndex , lineEndIndex , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , false , false , projection , tileID , pitchWithMap , textMaxAngleThreshold ) ;
562+ lineStartIndex , lineEndIndex , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , false , false , projection , tileID , pitchWithMap , textMaxAngleThreshold , blend ) ;
529563 if ( ! glyph ) {
530564 // undo previous glyphs of the symbol if it doesn't fit; it will be filled with hideGlyphs instead
531565 dynamicLayoutVertexArray . length -= 4 * ( glyphIndex - glyphStartIndex ) ;
@@ -556,7 +590,7 @@ function placeGlyphsAlongLine(
556590 }
557591 }
558592 const singleGlyph = placeGlyphAlongLine ( fontScale * glyphOffsetArray . getoffsetX ( glyphStartIndex ) , lineOffsetX , lineOffsetY , flip , anchorPoint , tileAnchorPoint , segment ,
559- lineStartIndex , lineEndIndex , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , false , false , projection , tileID , pitchWithMap , textMaxAngleThreshold ) ;
593+ lineStartIndex , lineEndIndex , lineVertexArray , labelPlaneMatrix , projectionCache , elevationParams , false , false , projection , tileID , pitchWithMap , textMaxAngleThreshold , blend ) ;
560594 if ( ! singleGlyph ) {
561595 return { notEnoughRoom : true } ;
562596 }
@@ -566,8 +600,38 @@ function placeGlyphsAlongLine(
566600 return { } ;
567601}
568602
569- function elevatePointAndProject ( p : Point , tileID : CanonicalTileID , posMatrix : mat4 , projection : Projection , elevationParams : ElevationParams | null ) {
570- const { x, y, z} = projection . projectTilePoint ( p . x , p . y , tileID ) ;
603+ // Computes the mercator-equivalent position in globe normalized ECEF space,
604+ // mirroring the vertex shader's mercator_tile_position() function.
605+ // blend.invMatrix and blend.mercCenter are pre-computed once per tile by updateLineLabels.
606+ function getMercatorECEF (
607+ p : Point ,
608+ tileID : CanonicalTileID ,
609+ blend : GlobeLineBlend ,
610+ ) : [ number , number , number ] {
611+ const tiles = 1 << tileID . z ;
612+ let mercX = ( p . x / EXTENT + tileID . x ) / tiles - blend . mercCenter [ 0 ] ;
613+ const mercY = ( p . y / EXTENT + tileID . y ) / tiles - blend . mercCenter [ 1 ] ;
614+ // Wrap to [-0.5, 0.5] — matches shader: mercator.x = wrap(mercator.x, -0.5, 0.5)
615+ mercX -= Math . round ( mercX ) ;
616+ const mercTile : [ number , number , number , number ] = [ mercX * EXTENT , mercY * EXTENT , EXTENT / ( 2 * Math . PI ) , 1.0 ] ;
617+ vec4 . transformMat4 ( mercTile , mercTile , blend . invMatrix ) ;
618+ return [ mercTile [ 0 ] , mercTile [ 1 ] , mercTile [ 2 ] ] ;
619+ }
620+
621+ function elevatePointAndProject ( p : Point , tileID : CanonicalTileID , posMatrix : mat4 , projection : Projection , elevationParams : ElevationParams | null , blend ?: GlobeLineBlend | null ) {
622+ let { x, y, z} = projection . projectTilePoint ( p . x , p . y , tileID ) ;
623+
624+ // During the globe-to-mercator transition the tile vertices are blended in the
625+ // shader via mix(globe_ecef, mercator_ecef, u_zoom_transition). Line-aligned
626+ // label vertices are projected on the CPU, so we must apply the same blend here
627+ // to keep them tracking with the underlying tile geometry.
628+ if ( blend ) {
629+ const [ mx , my , mz ] = getMercatorECEF ( p , tileID , blend ) ;
630+ x += ( mx - x ) * blend . t ;
631+ y += ( my - y ) * blend . t ;
632+ z += ( mz - z ) * blend . t ;
633+ }
634+
571635 if ( ! elevationParams ) {
572636 return project ( x , y , z , posMatrix ) ;
573637 }
@@ -584,13 +648,14 @@ function projectTruncatedLineSegment(
584648 elevationParams : ElevationParams ,
585649 projection : Projection ,
586650 tileID : CanonicalTileID ,
651+ blend ?: GlobeLineBlend | null ,
587652) : [ number , number , number ] {
588653 // We are assuming "previousTilePoint" won't project to a point within one unit of the camera plane
589654 // If it did, that would mean our label extended all the way out from within the viewport to a (very distant)
590655 // point near the plane of the camera. We wouldn't be able to render the label anyway once it crossed the
591656 // plane of the camera.
592657 const unitVertex = previousTilePoint . sub ( currentTilePoint ) . _unit ( ) . _add ( previousTilePoint ) ;
593- const projectedUnit = elevatePointAndProject ( unitVertex , tileID , projectionMatrix , projection , elevationParams ) ;
658+ const projectedUnit = elevatePointAndProject ( unitVertex , tileID , projectionMatrix , projection , elevationParams , blend ) ;
594659 vec3 . sub ( projectedUnit , previousProjectedPoint , projectedUnit ) ;
595660 vec3 . normalize ( projectedUnit , projectedUnit ) ;
596661
@@ -610,13 +675,14 @@ function placeGlyphAlongLine(
610675 lineVertexArray : SymbolLineVertexArray ,
611676 labelPlaneMatrix : mat4 ,
612677 projectionCache : ProjectionCache ,
613- elevationParams : ElevationParams | null ,
678+ elevationParams : ElevationParams | null | undefined ,
614679 returnPathInTileCoords : boolean | null | undefined ,
615680 endGlyph : boolean | null | undefined ,
616681 reprojection : Projection ,
617682 tileID : OverscaledTileID ,
618683 pitchWithMap : boolean ,
619- textMaxAngleThreshold : number
684+ textMaxAngleThreshold : number ,
685+ blend ?: GlobeLineBlend | null ,
620686) : null | PlacedGlyph {
621687
622688 const combinedOffsetX = flip ?
@@ -648,7 +714,7 @@ function placeGlyphAlongLine(
648714 let prevToCurrent = vec3 . zero ( [ ] ) ;
649715
650716 const getTruncatedLineSegment = ( ) => {
651- return projectTruncatedLineSegment ( prevVertex , currentVertex , prev , absOffsetX - distanceToPrev + 1 , labelPlaneMatrix , elevationParams , reprojection , tileID . canonical ) ;
717+ return projectTruncatedLineSegment ( prevVertex , currentVertex , prev , absOffsetX - distanceToPrev + 1 , labelPlaneMatrix , elevationParams , reprojection , tileID . canonical , blend ) ;
652718 } ;
653719
654720 while ( distanceToPrev + currentSegmentDistance <= absOffsetX ) {
@@ -667,7 +733,7 @@ function placeGlyphAlongLine(
667733 currentVertex = new Point ( lineVertexArray . getx ( currentIndex ) , lineVertexArray . gety ( currentIndex ) ) ;
668734 current = projectionCache [ currentIndex ] ;
669735 if ( ! current ) {
670- const projection = elevatePointAndProject ( currentVertex , tileID . canonical , labelPlaneMatrix , reprojection , elevationParams ) ;
736+ const projection = elevatePointAndProject ( currentVertex , tileID . canonical , labelPlaneMatrix , reprojection , elevationParams , blend ) ;
671737 if ( projection [ 3 ] > 0 ) {
672738 current = projectionCache [ currentIndex ] = projection as unknown as [ number , number , number ] ;
673739 } else {
0 commit comments