drawFill reads layer.layout unguarded: layers added from a styledata listener are painted before recalculate
- Dominant language
- TypeScript
- Stars
- 12.4k
- Forks
- 2.4k
- PR merge metrics
- No merged PRs in 30d
Description
## Summary
`Style.update()` fires `styledata` synchronously at its own tail, from inside `Map._render()`, after the per-layer `recalculate()` loop and before `painter.render()`. A layer added by a `styledata` listener therefore enters the render order for the current frame with `layout === undefined`, and is painted before it is ever evaluated.
`drawFill` reads `layer.layout` unguarded, so it throws. Two sibling functions in the same file already guard the identical access.
## Version
Reproduced on **v3.20.0**. The unguarded read is still present on **v3.28.1**.
## Error
```
Uncaught TypeError: Cannot read properties of undefined (reading 'get')
at drawFill
at Painter.renderLayer
at Painter.render
at Map._render
at
```
## Mechanism
Within a single frame:
1. `Map._render()` → `this.style.update(parameters)` — `src/ui/map.ts:4537`
2. `Style.update()` recalculates layers — `src/style/style.ts:1797`
```ts
if (layer.visibility !== 'none' || layer.hasTransition()) layer.recalculate(parameters, this._availableImages);
```
3. `Style.update()` then fires `styledata` at its tail — `src/style/style.ts:1870`
```ts
if (changed) {
this.fire(new Event('data', {dataType: 'style'}));
}
```
A listener calling `map.addLayer()` here inserts a layer **after** step 2 has run.
4. `Map._render()` → `this.painter.render(this.style, …)` — `src/ui/map.ts:4570`
5. `Painter.renderLayer` guards only on `isHidden()` and `coords.length` — `src/render/painter.ts:1497-1499`. It never checks `layout`.
6. `drawFill` — `src/render/draw_fill.ts:80`
```ts
if (layer.layout.get('fill-elevation-reference') !== 'none') { // throws
```
`layout` is assigned only in `StyleLayer.recalculate()` (`src/style/style_layer.ts:315-322`), so the freshly added layer has none.
`isHidden()` does not save it either: when the `addLayer` spec has no `layout` block, `visibility` stays `undefined`, and `isHidden()` only returns `true` for the literal `'none'` (`src/style/style_layer.ts:301-305`).
## Minimal reproduction
```js
map.on('load', () => {
map.addSource('demo', {type: 'geojson', data: polygonsInViewport});
});
// Re-create the layer on each styledata. Note: no `layout` block in the spec.
map.on('styledata', () => {
if (!map.isStyleLoaded()) return;
if (map.getLayer('demo-fill')) map.removeLayer('demo-fill');
map.addLayer({
id: 'demo-fill',
type: 'fill',
source: 'demo',
paint: {'fill-color': '#ff0000'}
});
});
```
Then trigger any `styledata` while the polygons are inside the viewport.
The source must have renderable tiles on screen — otherwise `renderLayer` returns early on `coords.length === 0` and never reaches `drawFill`. That is what makes this look intermittent.
## Suggested fix
`drawFill` is the only unguarded consumer on this path. The same guard already exists twice in that file (v3.20.0 lines 151 and 235):
```ts
if (layer.layout && layer.layout.get('fill-elevation-reference') !== 'none') {
elevationType = 'road';
}
```
Alternatively, skip layers with no `layout` in `Painter.renderLayer`, which would cover any other consumer reachable the same way.
Contributor guide
Research direction
Start at src/render/draw_fill.ts:80 and compare the layer.layout access with the two guarded accesses in the same file. Use the styledata reproduction to exercise src/ui/map.ts and src/render/painter.ts with a newly added fill layer; done means the frame renders without the undefined-layout exception.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- typescript
- Domain
- computer-graphics, frontend
- Issue type
- Bug
- Difficulty
- 1/5
- Estimated time
- Under an hour
- Activity status
- Active
- Clarity
- Clearly specified
- Newbie friendliness
- 86/100