Skip to content

Commit 6baa1bc

Browse files
underootgithub-actions[bot]
authored andcommitted
[GLJS-1527] Simplify update placement method (internal-8745)
GitOrigin-RevId: 3b22b3f77cc0952f674d9c07f2865c52dda42494
1 parent 9d41404 commit 6baa1bc

File tree

6 files changed

+133
-56
lines changed

6 files changed

+133
-56
lines changed

src/style/pauseable_placement.ts

Lines changed: 25 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -72,21 +72,34 @@ class PauseablePlacement {
7272
_forceFullPlacement: boolean;
7373
_showCollisionBoxes: boolean;
7474
_inProgressLayer: LayerPlacement | null | undefined;
75+
_fadeDuration: number;
7576

76-
constructor(transform: Transform, order: Array<string>,
77-
forceFullPlacement: boolean,
78-
showCollisionBoxes: boolean,
79-
fadeDuration: number,
80-
crossSourceCollisions: boolean,
81-
prevPlacement?: Placement,
82-
fogState?: FogState | null,
83-
buildingIndex?: BuildingIndex | null
84-
) {
77+
startNewPlacement(
78+
transform: Transform,
79+
order: Array<string>,
80+
showCollisionBoxes: boolean,
81+
fadeDuration: number,
82+
crossSourceCollisions: boolean,
83+
prevPlacement?: Placement,
84+
fogState?: FogState | null,
85+
buildingIndex?: BuildingIndex | null
86+
): PauseablePlacement {
8587
this.placement = new Placement(transform, fadeDuration, crossSourceCollisions, prevPlacement, fogState, buildingIndex);
8688
this._currentPlacementIndex = order.length - 1;
87-
this._forceFullPlacement = forceFullPlacement;
89+
this._forceFullPlacement = false;
8890
this._showCollisionBoxes = showCollisionBoxes;
91+
this._fadeDuration = fadeDuration;
8992
this._done = false;
93+
this._inProgressLayer = null;
94+
return this;
95+
}
96+
97+
requestFullPlacement(): void {
98+
this._forceFullPlacement = true;
99+
}
100+
101+
isFullPlacementRequested(): boolean {
102+
return this._forceFullPlacement;
90103
}
91104

92105
isDone(): boolean {
@@ -98,7 +111,7 @@ class PauseablePlacement {
98111

99112
const shouldPausePlacement = () => {
100113
const elapsedTime = browser.now() - startTime;
101-
return this._forceFullPlacement ? false : elapsedTime > 2;
114+
return this.isFullPlacementRequested() || this._fadeDuration === 0 ? false : elapsedTime > 2;
102115
};
103116

104117
while (this._currentPlacementIndex >= 0) {
@@ -143,6 +156,7 @@ class PauseablePlacement {
143156
this._currentPlacementIndex--;
144157
}
145158
PerformanceUtils.recordPlacementTime(browser.now() - startTime);
159+
this._forceFullPlacement = false;
146160
this._done = true;
147161
}
148162

src/style/style.ts

Lines changed: 35 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,6 @@ import type {PropertyValidatorOptions} from '../style-spec/validate/validate_pro
7373
import type Tile from '../source/tile';
7474
import type GeoJSONSource from '../source/geojson_source';
7575
import type {ReplacementSource} from "../../3d-style/source/replacement_source";
76-
import type Painter from '../render/painter';
7776
import type SymbolStyleLayer from '../style/style_layer/symbol_style_layer';
7877
import type {
7978
ColorThemeSpecification,
@@ -341,7 +340,6 @@ class Style extends Evented<MapEvents> {
341340
_rtlTextPluginCallback: (state: {pluginStatus: string; pluginURL: string | null | undefined}) => void;
342341
_changes: StyleChanges;
343342
_optionsChanged: boolean;
344-
_layerOrderChanged: boolean;
345343
_availableImages: ImageId[];
346344
_availableModels: StyleModelMap;
347345
_markersNeedUpdate: boolean;
@@ -2717,7 +2715,7 @@ class Style extends Evented<MapEvents> {
27172715
}
27182716

27192717
this._order.splice(index, 0, id);
2720-
this._layerOrderChanged = true;
2718+
this._handleLayerOrderChange();
27212719
this._layers[id] = layer;
27222720

27232721
const sourceCache = this.getOwnLayerSourceCache(layer);
@@ -2794,7 +2792,7 @@ class Style extends Evented<MapEvents> {
27942792
this._order.splice(newIndex, 0, id);
27952793

27962794
this._changes.setDirty();
2797-
this._layerOrderChanged = true;
2795+
this._handleLayerOrderChange();
27982796

27992797
this.mergeLayers();
28002798
}
@@ -2821,7 +2819,7 @@ class Style extends Evented<MapEvents> {
28212819
delete this._layers[id];
28222820

28232821
this._changes.setDirty();
2824-
this._layerOrderChanged = true;
2822+
this._handleLayerOrderChange();
28252823

28262824
this._configDependentLayers.delete(layer.fqid);
28272825
this._indoorDependentLayers.delete(layer.fqid);
@@ -4117,17 +4115,40 @@ class Style extends Evented<MapEvents> {
41174115
}
41184116
}
41194117

4118+
_handleLayerOrderChange() {
4119+
this._requestFullLabelPlacement();
4120+
this.fire(new Event('neworder'));
4121+
}
4122+
4123+
_requestFullLabelPlacement() {
4124+
if (!this.pauseablePlacement) {
4125+
this.pauseablePlacement = new PauseablePlacement();
4126+
}
4127+
4128+
// Anything that changes our "in progress" layer and tile indices requires us
4129+
// to start over. When we start over, we do a full placement instead of incremental
4130+
// to prevent starvation.
4131+
// We need to restart placement to keep layer indices in sync.
4132+
this.pauseablePlacement.requestFullPlacement();
4133+
}
4134+
4135+
_setLabelPlacementStale() {
4136+
if (this.placement) {
4137+
this.placement.setStale();
4138+
}
4139+
}
4140+
41204141
_updatePlacement(
4121-
painter: Painter,
41224142
transform: Transform,
41234143
showCollisionBoxes: boolean,
41244144
fadeDuration: number,
41254145
crossSourceCollisions: boolean,
41264146
replacementSource: ReplacementSource,
4127-
forceFullPlacement: boolean = false,
4128-
): {
4129-
needsRerender: boolean;
4130-
} {
4147+
): boolean {
4148+
if (!this.pauseablePlacement) {
4149+
this.pauseablePlacement = new PauseablePlacement();
4150+
}
4151+
41314152
let symbolBucketsChanged = false;
41324153
let placementCommitted = false;
41334154

@@ -4156,16 +4177,6 @@ class Style extends Evented<MapEvents> {
41564177
}
41574178
this.crossTileSymbolIndex.pruneUnusedLayers(this._mergedOrder);
41584179

4159-
// Anything that changes our "in progress" layer and tile indices requires us
4160-
// to start over. When we start over, we do a full placement instead of incremental
4161-
// to prevent starvation.
4162-
// We need to restart placement to keep layer indices in sync.
4163-
forceFullPlacement = forceFullPlacement || this._layerOrderChanged;
4164-
4165-
if (this._layerOrderChanged) {
4166-
this.fire(new Event('neworder'));
4167-
}
4168-
41694180
const transformChanged = Boolean(this.placement && !transform.equals(this.placement.transform));
41704181
const replacementSourceChanged = Boolean(this.placement && ((this.placement.lastReplacementSourceUpdateTime !== 0 && !replacementSource) || this.placement.lastReplacementSourceUpdateTime !== replacementSource.updateTime));
41714182

@@ -4175,19 +4186,12 @@ class Style extends Evented<MapEvents> {
41754186
// we will do expensive full placements on every frame.
41764187
const fullFrameUpdateRequired = (transformChanged || replacementSourceChanged || symbolBucketsChanged || (this.placement && this.placement.isStale())) && fadeDuration === 0;
41774188

4178-
if (forceFullPlacement || !this.pauseablePlacement || fullFrameUpdateRequired || (fadeDuration !== 0 && this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) {
4189+
if (this.pauseablePlacement.isFullPlacementRequested() || !this.pauseablePlacement.placement || fullFrameUpdateRequired || (fadeDuration !== 0 && this.pauseablePlacement.isDone() && !this.placement.stillRecent(browser.now(), transform.zoom))) {
41794190
const fogState = this.fog && transform.projection.supportsFog ? this.fog.state : null;
4180-
this.pauseablePlacement = new PauseablePlacement(transform, this._mergedOrder, forceFullPlacement || fadeDuration === 0, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement, fogState, this._buildingIndex);
4181-
this._layerOrderChanged = false;
4191+
this.pauseablePlacement = this.pauseablePlacement.startNewPlacement(transform, this._mergedOrder, showCollisionBoxes, fadeDuration, crossSourceCollisions, this.placement, fogState, this._buildingIndex);
41824192
}
41834193

4184-
if (this.pauseablePlacement.isDone()) {
4185-
// the last placement finished running, but the next one hasn’t
4186-
// started yet because of the `stillRecent` check immediately
4187-
// above, so mark it stale to ensure that we request another
4188-
// render frame
4189-
this.placement.setStale();
4190-
} else {
4194+
if (!this.pauseablePlacement.isDone()) {
41914195
this.pauseablePlacement.continuePlacement(this._mergedOrder, this._mergedLayers, layerTiles, layerTilesInYOrder, this.map.painter.scaleFactor);
41924196

41934197
if (this.pauseablePlacement.isDone()) {
@@ -4216,8 +4220,7 @@ class Style extends Evented<MapEvents> {
42164220
}
42174221

42184222
// needsRender is false when we have just finished a placement that didn't change the visibility of any symbols
4219-
const needsRerender = !this.pauseablePlacement.isDone() || this.placement.hasTransitions(browser.now());
4220-
return {needsRerender};
4223+
return !this.pauseablePlacement.isDone() || this.placement.isStale() || this.placement.hasTransitions(browser.now());
42214224
}
42224225

42234226
_releaseSymbolFadeTiles() {

src/symbol/placement.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1393,8 +1393,7 @@ export class Placement {
13931393
}
13941394

13951395
hasTransitions(now: number): boolean {
1396-
return this.stale ||
1397-
now - this.lastPlacementChangeTime < this.fadeDuration;
1396+
return now - this.lastPlacementChangeTime < this.fadeDuration;
13981397
}
13991398

14001399
stillRecent(now: number, zoom: number): boolean {

src/ui/map.ts

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -466,7 +466,6 @@ export class Map extends Camera {
466466
_styleDirty?: boolean;
467467
_sourcesDirty?: boolean;
468468
_placementDirty?: boolean;
469-
_scaleFactorChanged?: boolean;
470469
_loaded: boolean;
471470
_fullyLoaded: boolean; // accounts for placement finishing as well
472471
_trackResize: boolean;
@@ -638,7 +637,6 @@ export class Map extends Camera {
638637
this._containerHeight = 0;
639638
this._showParseStatus = true;
640639
this._precompilePrograms = options.precompilePrograms;
641-
this._scaleFactorChanged = false;
642640

643641
this._averageElevationLastSampledAt = -Infinity;
644642
this._averageElevationExaggeration = 0;
@@ -1239,7 +1237,9 @@ export class Map extends Camera {
12391237
this.painter.scaleFactor = scaleFactor;
12401238
DevTools.refresh();
12411239

1242-
this._scaleFactorChanged = true;
1240+
if (this.style) {
1241+
this.style._setLabelPlacementStale();
1242+
}
12431243

12441244
this.style._updateFilteredLayers((layer) => layer.type === 'symbol');
12451245
this._update(true);
@@ -4257,6 +4257,12 @@ export class Map extends Camera {
42574257
this.painter = new Painter(gl, this._contextCreateOptions, this.transform, this._scaleFactor, this._worldview);
42584258
this.on('data', (event) => {
42594259
if (event.dataType === 'source') {
4260+
const elevationSource = this.transform.elevation ? this.transform.elevation._source() : null;
4261+
// Force a new label placement when a DEM source was updated,
4262+
// to ensure that labels are placed according to the updated terrain.
4263+
if (elevationSource && event.sourceCacheId === elevationSource.id && this.style) {
4264+
this.style._setLabelPlacementStale();
4265+
}
42604266
this.painter.setTileLoadedFlag(true);
42614267
}
42624268
});
@@ -4488,12 +4494,8 @@ export class Map extends Camera {
44884494
averageElevationChanged = this._updateAverageElevation(frameStartTime);
44894495
}
44904496

4491-
const updatePlacementResult = this.style && this.style._updatePlacement(this.painter, this.painter.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions, this.painter.replacementSource, this._scaleFactorChanged);
4492-
if (this._scaleFactorChanged) {
4493-
this._scaleFactorChanged = false;
4494-
}
4495-
if (updatePlacementResult) {
4496-
this._placementDirty = updatePlacementResult.needsRerender;
4497+
if (this.style) {
4498+
this._placementDirty = this.style._updatePlacement(this.painter.transform, this.showCollisionBoxes, fadeDuration, this._crossSourceCollisions, this.painter.replacementSource);
44974499
}
44984500

44994501
// Actually draw

test/unit/style/style.test.ts

Lines changed: 61 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2004,7 +2004,7 @@ test('Style defers expensive methods', async () => {
20042004
style.update({});
20052005

20062006
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
2007-
expect(style.fire.mock.calls[0][0].type).toEqual('data');
2007+
expect(style.fire.mock.calls[0][0].type).toEqual('neworder');
20082008

20092009
// called per source
20102010
expect(style.reloadSource).toHaveBeenCalledTimes(2);
@@ -2797,3 +2797,63 @@ test('Occlusion ordering & draped layers', async () => {
27972797
makeFQID('symbol-occlusion-3'),
27982798
]);
27992799
});
2800+
2801+
describe('Style#_updatePlacement', () => {
2802+
test('does not re-trigger placement on repaint with fadeDuration: 0 when nothing changed', async () => {
2803+
const map = new StubMap();
2804+
// Mock painter with scaleFactor (required by _updatePlacement)
2805+
// @ts-expect-error - painter is not part of StubMap but required for _updatePlacement
2806+
map.painter = {scaleFactor: 1};
2807+
2808+
// Mock replacement source with updateTime (required by _updatePlacement)
2809+
const replacementSource = {updateTime: 0};
2810+
2811+
const style = new Style(map);
2812+
style.loadJSON({
2813+
"version": 8,
2814+
"sources": {
2815+
"geojson": {
2816+
"type": "geojson",
2817+
"data": {"type": "FeatureCollection", "features": []}
2818+
}
2819+
},
2820+
"layers": [{
2821+
"id": "symbol",
2822+
"type": "symbol",
2823+
"source": "geojson"
2824+
}]
2825+
});
2826+
2827+
await waitFor(style, 'style.load');
2828+
2829+
const tr = map.transform;
2830+
tr.resize(512, 512);
2831+
2832+
// Compile the style layers before calling _updatePlacement
2833+
style.update({zoom: tr.zoom, fadeDuration: 0});
2834+
2835+
// First call to _updatePlacement - initializes placement
2836+
style._updatePlacement(tr, false, 0, false, replacementSource);
2837+
2838+
// Placement should be done after first call with fadeDuration: 0
2839+
expect(style.pauseablePlacement.isDone()).toBeTruthy();
2840+
2841+
// Spy on placement methods AFTER initial placement
2842+
const startNewPlacementSpy = vi.spyOn(style.pauseablePlacement, 'startNewPlacement');
2843+
const continuePlacementSpy = vi.spyOn(style.pauseablePlacement, 'continuePlacement');
2844+
2845+
// Second call to _updatePlacement - should NOT trigger new placement
2846+
style._updatePlacement(tr, false, 0, false, replacementSource);
2847+
2848+
// Assert placement methods were NOT called
2849+
expect(startNewPlacementSpy).not.toHaveBeenCalled();
2850+
expect(continuePlacementSpy).not.toHaveBeenCalled();
2851+
2852+
// Third call to _updatePlacement - verify consistent behavior
2853+
style._updatePlacement(tr, false, 0, false, replacementSource);
2854+
2855+
// Assert placement methods STILL were not called (verifies it's not a one-time skip)
2856+
expect(startNewPlacementSpy).not.toHaveBeenCalled();
2857+
expect(continuePlacementSpy).not.toHaveBeenCalled();
2858+
});
2859+
});

test/unit/terrain/terrain.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -290,7 +290,6 @@ describe('Elevation', () => {
290290
test('disable terrain', async () => {
291291
// eslint-disable-next-line @typescript-eslint/no-unsafe-member-access
292292
expect(map.painter.terrain).toBeTruthy();
293-
await waitFor(map, "idle");
294293
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-unsafe-member-access
295294
map.setTerrain(null);
296295
await waitFor(map, "render");

0 commit comments

Comments
 (0)