Skip to content

Commit ba5b457

Browse files
Fix hd-road-markup elevated lines rendering flat with
GitOrigin-RevId: 1f68535ce73c922e7d270404381e761a7af87b94
1 parent f1d0fdf commit ba5b457

11 files changed

Lines changed: 768 additions & 64 deletions

File tree

3d-style/source/elevation_coverage_snapshot.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import {RenderSourceType} from '../../src/source/render_source_type';
2+
import {terrainEnabled} from '../../src/style/terrain';
23

34
import type {
45
ElevationCoverageSnapshot as IElevationCoverageSnapshot,
@@ -95,9 +96,8 @@ export function buildElevationRequestParams(
9596
crossSourceElevationEnabled: boolean,
9697
): ElevationParams | null {
9798
if (tile.renderSourceType === RenderSourceType.HdRoadElevation) return null;
98-
// Under terrain: skip snapshot — HD road-markup lines drape flat.
99-
// style.terrain is synchronous; painter.terrain lags one frame.
100-
if (map.style && map.style.terrain) return null;
99+
// Active terrain: lines drape flat instead of using the snapshot.
100+
if (terrainEnabled(map.style, map.transform)) return null;
101101
if (!crossSourceElevationEnabled) return null;
102102
const snapshot = map.painter ? map.painter.elevationCoverageSnapshot : null;
103103
// Defaults false (defer) until providers settle.

3d-style/style/elevation_coverage_style.ts

Lines changed: 36 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import {ElevationCoverageManager} from '../source/elevation_coverage_manager';
22
import SourceCache from '../../src/source/source_cache';
33
import {RenderSourceType} from '../../src/source/render_source_type';
44
import {getNameFromFQID, getOuterScopeFromFQID, makeFQID} from '../../src/util/fqid';
5+
import {terrainEnabled} from '../../src/style/terrain';
56

67
import type Tile from '../../src/source/tile';
78
import type {TypedStyleLayer} from '../../src/style/style_layer/typed_style_layer';
@@ -44,7 +45,6 @@ function layerUsesHdRoadSourceLayer(layer: TypedStyleLayer): boolean {
4445
}
4546

4647
/// True when every provider-only source has finished loading.
47-
/// Dual-role sources are excluded to avoid circular deferral.
4848
function areElevationProvidersReady(style: Style, providerFQIDs: Set<string>): boolean {
4949
for (const fqid of providerFQIDs) {
5050
const sc = resolveIngestSourceCache(style, fqid) as unknown as SourceCacheTiles | undefined;
@@ -109,7 +109,7 @@ export function collectElevationProviderSourceFQIDs(style: Style): Set<string> {
109109
return providers;
110110
}
111111

112-
/// True when a consumer resolves elevation from a different source.
112+
/// True when a consumer gets elevation from a different source.
113113
export function needsCrossSourceElevation(style: Style): boolean {
114114
const consumers = collectElevationConsumerSourceFQIDs(style);
115115
if (consumers.size === 0) return false;
@@ -125,26 +125,19 @@ export function needsCrossSourceElevation(style: Style): boolean {
125125
/// Recompute the cached cross-source gate after a source change.
126126
export function updateCrossSourceElevationGate(style: Style): void {
127127
style._crossSourceElevationActive = needsCrossSourceElevation(style);
128-
if (Object.keys(style._mergedHdRoadElevationSourceCaches).length === 0) return;
128+
// HdElevationState only needed once dedicated elevation caches exist.
129+
const hdCaches = style._mergedHdRoadElevationSourceCaches;
130+
if (!hdCaches || Object.keys(hdCaches).length === 0) return;
129131
if (!style._hdElevation) style._hdElevation = new HdElevationState();
130132
style._hdElevation._needsCrossSourceElevation = style._crossSourceElevationActive;
131133
}
132134

133-
/// Reparse consumer tiles after terrain on/off (flat ↔ elevated geometry).
134-
export function handleTerrainToggle(style: Style, hadTerrain: boolean): void {
135-
if (hadTerrain === !!style.terrain) return;
136-
reparseElevationConsumerTiles(style);
137-
}
138-
139135
/// Cached cross-source gate; refreshed on source change and each frame.
140136
export function crossSourceElevationEnabledForStyle(style: Style): boolean {
141137
return style._crossSourceElevationActive === true;
142138
}
143139

144-
/// Sources whose tiles feed the elevation snapshot (providers and dual-role).
145-
/// This is exactly every source with an hd_road_* source-layer: provider-only
146-
/// and dual-role sources together make up the full set, so no consumer/provider
147-
/// split is needed.
140+
/// Sources whose tiles are scanned to build the cross-source snapshot.
148141
export function collectElevationIngestSourceFQIDs(style: Style): Set<string> {
149142
const ingest = new Set<string>();
150143
for (const layerId in style._mergedLayers) {
@@ -166,16 +159,19 @@ export function markElevationIngestSourceCachesUsed(style: Style): void {
166159
}
167160
}
168161

169-
/// Reload consumer tiles (optionally filtered). Skips non-consumers like raster-dem.
162+
/// Reload hd-road-markup consumer tiles.
170163
export function reparseElevationConsumerTiles(
171164
style: Style,
172165
shouldReload?: (tile: Tile) => boolean,
166+
includeSameSourceConsumers?: boolean,
173167
): void {
174-
// Skip dual-role ingest sources — reparsing them causes a reload loop.
175168
const consumerFQIDs = collectElevationConsumerSourceFQIDs(style);
176169
if (consumerFQIDs.size === 0) return;
177-
const ingestFQIDs = collectElevationIngestSourceFQIDs(style);
178-
for (const fqid of ingestFQIDs) consumerFQIDs.delete(fqid);
170+
if (!includeSameSourceConsumers) {
171+
// Same-source: elevation is parsed from the consumer tile itself.
172+
const ingestFQIDs = collectElevationIngestSourceFQIDs(style);
173+
for (const fqid of ingestFQIDs) consumerFQIDs.delete(fqid);
174+
}
179175
if (consumerFQIDs.size === 0) return;
180176

181177
const caches = [style._mergedOtherSourceCaches, style._mergedSymbolSourceCaches];
@@ -200,12 +196,14 @@ export class HdElevationState {
200196
manager: ElevationCoverageManager;
201197
_needsCrossSourceElevation: boolean;
202198
_ingestFQIDs: Set<string>;
199+
_terrainActiveLast: boolean | undefined;
203200

204201
constructor() {
205202
this.elevationSourceCaches = {};
206203
this.manager = new ElevationCoverageManager();
207204
this._needsCrossSourceElevation = false;
208205
this._ingestFQIDs = new Set();
206+
this._terrainActiveLast = undefined;
209207
}
210208
}
211209

@@ -237,18 +235,27 @@ function deactivateCrossSourceElevation(style: Style): void {
237235
style._hdElevation._needsCrossSourceElevation = false;
238236
style._hdElevation._ingestFQIDs.clear();
239237
style._hdElevation.manager.clear();
238+
style._hdElevation._terrainActiveLast = undefined;
240239
}
241240

242241
/// Setup provider caches, merge fragments, ingest tiles, and reparse consumers.
243242
export function setupAndUpdateElevationCoverage(style: Style): void {
244-
// Under terrain: skip. Use style.terrain, not painter.terrain (lags a frame). Keep gate for toggle.
245-
if (style.terrain) {
246-
if (style.map.painter) style.map.painter.elevationCoverageSnapshot = null;
243+
if (!hasElevationConsumers(style)) {
244+
deactivateCrossSourceElevation(style);
247245
return;
248246
}
249247

250-
if (!hasElevationConsumers(style)) {
251-
deactivateCrossSourceElevation(style);
248+
const terrainActive = terrainEnabled(style, style.map && style.map.transform);
249+
if (!style._hdElevation) style._hdElevation = new HdElevationState();
250+
const prevTerrainActive = style._hdElevation._terrainActiveLast;
251+
style._hdElevation._terrainActiveLast = terrainActive;
252+
const flipped = prevTerrainActive !== undefined && prevTerrainActive !== terrainActive;
253+
if (flipped) {
254+
reparseElevationConsumerTiles(style, undefined, true);
255+
}
256+
257+
if (terrainActive) {
258+
if (style.map.painter) style.map.painter.elevationCoverageSnapshot = null;
252259
return;
253260
}
254261

@@ -258,9 +265,12 @@ export function setupAndUpdateElevationCoverage(style: Style): void {
258265
return;
259266
}
260267

261-
if (!style._hdElevation) {
262-
style._hdElevation = new HdElevationState();
268+
// First frame with cross-source active and terrain inactive: reparse consumers so they
269+
// pick up elevation immediately rather than waiting for a snapshot change.
270+
if (prevTerrainActive === undefined) {
271+
reparseElevationConsumerTiles(style, undefined, true);
263272
}
273+
264274
const state = style._hdElevation;
265275
state._needsCrossSourceElevation = true;
266276
style._crossSourceElevationActive = true;
@@ -308,8 +318,8 @@ export function updateHdElevationSourceCache(style: Style, state: HdElevationSta
308318

309319
/// Ingest provider tiles and reparse consumers whose covering changed.
310320
export function updateElevationCoverage(style: Style, state: HdElevationState) {
311-
// Under terrain: clear snapshot (lines drape flat).
312-
if (style.terrain) {
321+
// Active terrain: markup lines drape flat, no snapshot.
322+
if (terrainEnabled(style, style.map && style.map.transform)) {
313323
state.manager.clear();
314324
if (style.map.painter) style.map.painter.elevationCoverageSnapshot = null;
315325
return;

modules/hd_main_imports.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ import {Snow} from '../src/precipitation/draw_snow';
88
import {programUniforms} from '../3d-style/render/program/program_uniforms_hd';
99
import {drawElevatedStructures, drawElevatedFillShadows, drawDepthPrepass, drawGroundShadowMask} from '../3d-style/render/draw_elevated_fill';
1010
import {HdCoverageState, updateFrcCoverage, updateHdCoverageSourceCache, updateFrcCoverageFadeRange} from '../3d-style/style/frc_coverage_style';
11-
import {HdElevationState, setupAndUpdateElevationCoverage, updateElevationCoverage, updateHdElevationSourceCache, markElevationIngestSourceCachesUsed, updateCrossSourceElevationGate, handleTerrainToggle} from '../3d-style/style/elevation_coverage_style';
11+
import {HdElevationState, setupAndUpdateElevationCoverage, updateElevationCoverage, updateHdElevationSourceCache, markElevationIngestSourceCachesUsed, updateCrossSourceElevationGate} from '../3d-style/style/elevation_coverage_style';
1212
import {buildElevationRequestParams} from '../3d-style/source/elevation_coverage_snapshot';
1313
import {FrcCoverageRenderer} from '../3d-style/render/frc_coverage_renderer';
1414
import {
@@ -57,7 +57,6 @@ export const HD = {
5757
updateHdElevationSourceCache,
5858
markElevationIngestSourceCachesUsed,
5959
updateCrossSourceElevationGate,
60-
handleTerrainToggle,
6160
buildElevationRequestParams,
6261
FrcCoverageRenderer,
6362
drawFillFrcCoverageFirstPass,

src/source/vector_tile_source.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import {makeFQID} from '../util/fqid';
1111
import {isMapboxURL} from '../util/mapbox_url';
1212
import {resolveTileProvider, processTileJSON} from './tile_provider';
1313
import {HD, prepareHD} from '../../modules/hd_main';
14+
import {terrainEnabled} from '../style/terrain';
1415
import {Standard, prepareStandard} from '../../modules/standard_main';
1516

1617
import type {ISource, SourceEvents, SourceVectorLayer} from './source';
@@ -141,6 +142,7 @@ class VectorTileSource extends Evented<SourceEvents> implements ISource<'vector'
141142
this.fire(new ErrorEvent(err));
142143
} else if (tileJSON) {
143144
this._setTileJSON(tileJSON);
145+
if (HD.updateCrossSourceElevationGate && this.map.style) HD.updateCrossSourceElevationGate(this.map.style);
144146
postTurnstileEvent(tileJSON.tiles, this.map._requestManager._customAccessToken);
145147
// `content` is included here to prevent a race condition where `Style#updateSources` is called
146148
// before the TileJSON arrives. this makes sure the tiles needed are loaded once TileJSON arrives
@@ -388,8 +390,7 @@ class VectorTileSource extends Evented<SourceEvents> implements ISource<'vector'
388390
sourceLayers: painter ? painter.frcCoverageSourceLayers : ['road', 'structure'],
389391
};
390392
})(),
391-
// style.terrain is synchronous; painter.terrain lags one frame.
392-
terrainEnabled: !!(this.map.style && this.map.style.terrain),
393+
terrainEnabled: terrainEnabled(this.map.style, this.map.transform),
393394
crossSourceElevationEnabled,
394395
elevation: HD.buildElevationRequestParams ?
395396
HD.buildElevationRequestParams(this.map, tile, crossSourceElevationActive) : null,

src/style/style.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3991,10 +3991,6 @@ class Style extends Evented<MapEvents> {
39913991
setTerrain(terrainOptions?: TerrainSpecification | TerrainSpecificationUpdate | null, drapeRenderMode: number = DrapeRenderMode.elevated) {
39923992
this._checkLoaded();
39933993

3994-
// Terrain on/off must reparse HD road-markup tiles. setTerrain(null) on a draping
3995-
// projection re-adds draping terrain, so !!terrain stays true → no reparse (correct).
3996-
const hadTerrain = !!this.terrain;
3997-
39983994
// Disabling
39993995
if (!terrainOptions) {
40003996
// This check prevents removing draping terrain not from #applyProjectionUpdate
@@ -4018,7 +4014,6 @@ class Style extends Evented<MapEvents> {
40184014

40194015
this._force3DLayerUpdate();
40204016
this._markersNeedUpdate = true;
4021-
if (HD.handleTerrainToggle) HD.handleTerrainToggle(this, hadTerrain);
40224017
return;
40234018
}
40244019

@@ -4087,7 +4082,6 @@ class Style extends Evented<MapEvents> {
40874082
this.mergeTerrain();
40884083
this.updateDrapeFirstLayers();
40894084
this._markersNeedUpdate = true;
4090-
if (HD.handleTerrainToggle) HD.handleTerrainToggle(this, hadTerrain);
40914085
}
40924086

40934087
_createFog(fogOptions: FogSpecification) {

src/style/terrain.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,3 +118,17 @@ class Terrain extends Evented {
118118
}
119119

120120
export default Terrain;
121+
122+
/// True when terrain is enabled at the current zoom.
123+
/// Zero exaggeration on zoom-dependent terrain counts as disabled.
124+
export function terrainEnabled(
125+
style: {terrain?: Terrain | null} | null | undefined,
126+
transform: {zoom: number; projection: {requiresDraping: boolean}} | null | undefined,
127+
): boolean {
128+
const terrain = style ? style.terrain : null;
129+
if (!terrain) return false;
130+
if (!transform) return true; // conservative: can't evaluate exaggeration, assume active
131+
if (transform.projection.requiresDraping) return true;
132+
if (!terrain.isZoomDependent()) return true;
133+
return terrain.getExaggeration(transform.zoom) > 0;
134+
}
62 KB
Loading

0 commit comments

Comments
 (0)