Skip to content

Commit f66997a

Browse files
dcojgithub-actions[bot]
authored andcommitted
[GLJS-1853] fix for line-aligned label placement during globe-mercator transition (internal-15603)
GitOrigin-RevId: ee928c6a36644ce3eac9a41d8888c18dec404143
1 parent f607fdc commit f66997a

2 files changed

Lines changed: 83 additions & 17 deletions

File tree

src/symbol/projection.ts

Lines changed: 83 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@ import Point from '@mapbox/point-geometry';
22
import {mat2, mat4, vec3, vec4} from 'gl-matrix';
33
import {addDynamicAttributes, updateGlobeVertexNormal} from '../data/bucket/symbol_bucket';
44
import {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';
68
import {degToRad} from '../util/util';
79
import {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+
4554
type 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 {
268 Bytes
Loading

0 commit comments

Comments
 (0)