Data-driven `line-dasharray` of `[]` leaves lines undrawn in some tiles (LineAtlas key changes after `addDash` mutates the array)
Assessment
- Difficulty
- 3/5
- Estimated time
- Half a day
- Newbie friendliness
- 62/100
- Issue type
- Bug
- Clarity
- Clearly specified
- Activity status
- Active
- Tech stack
- typescript
- Domain
- computer-graphics
Research direction
Start in src/render/line_atlas.ts at LineAtlas#addDash and getKey, where the empty dasharray is mutated in place, then in src/data/bucket/line_bucket.ts at addFeatureDashes, where the key is recomputed from that mutated array. Run the repro page with showTileBoundaries on and check that every tile's lineAtlas.positions holds one key for [] and no "1round" key. Done means the [] case gets a single consistent key, the caller's array is left unmodified, and all ten lines draw in every tile.
Written by the indexing model from the issue text.
Description
mapbox-gl-js version
3.32.0
Browser and version
Chrome (macOS)
Expected behavior
- Add a GeoJSON source with line features whose
dashproperty is an empty array[](a solid line). - Add a line layer with
'line-cap': 'round'and a data-driven'line-dasharray': ['get', 'dash']. - View the lines at zoom 14+ with
showTileBoundarieson.
Expected: Every line is drawn as a solid line across all tiles. LineAtlas#addDash says an empty dasharray "draws a full line", and the style spec doesn't forbid an empty array.
Actual behavior
Some lines are missing within individual tiles: the stroke stops and starts exactly at tile boundaries. Which lines and tiles are affected changes after zooming or calling setData. No warning or error is logged. Using [1, 0] (or leaving line-dasharray out) draws every line fully.
The repro page with tile boundaries shown: the bottom line is drawn up to the edge of tile 14/2650/5412, missing across that whole tile, and drawn again after it. The other lines cross the same tile normally.
Cause
LineAtlas#addDash builds the cache key from the array, then mutates the caller's array in place when it's empty (line_atlas.ts#L163-L180):
const key = this.getKey(dasharray, lineCap); // [] -> "round"
if (this.positions[key]) return this.positions[key];
...
if (dasharray.length === 0) {
dasharray.push(1); // caller's array is now [1]
}
...
this.positions[key] = pos; // stored under "round"
LineBucket#addFeatureDashes then computes the key again from the same, now mutated, array (line_bucket.ts#L508-L511):
lineAtlas.addDash(dashArray, cap);
feature.patterns[layer.id] = [lineAtlas.getKey(dashArray, cap)]; // "1round", not in positions
So in each tile, the first feature with an unmutated [] is stored under "round" but looked up under "1round". That lookup finds nothing, so the feature gets no dash position in that tile and isn't drawn there. Later features in the same tile hit the cached "round" entry before the mutation, so they're fine.
The mutated array is the feature's own property value, shared across that worker's tiles. So the next tile sees [1], and the failure moves around depending on tile processing order. That's why the affected lines vary and change on zoom.
The new assert on main (line_bucket.ts#L718, "Dash key ... has no LineAtlas position") should fire for this case in dev builds.
Checking the loaded tiles confirms it: with [], the tiles' lineAtlas.positions hold "round" and, in some tiles, "1round". A "1round" key can only exist because the worker mutated a feature's array. With [1, 0], every tile holds only "1,0round".
Possible fix
Normalize without mutating the input, and use one key for both storing and looking up. For example, in addDash:
if (dasharray.length === 0) dasharray = [1];
const key = this.getKey(dasharray, lineCap);
and have addFeatureDashes use a key returned by addDash, or normalize the same way before calling getKey. It would also help to warnOnce if an empty dasharray is ever treated differently from a solid line.
Workaround
Use [1, 0] instead of [] for solid lines, or don't set line-dasharray on features that should be solid.
Link to the demonstration
No response
Steps to trigger the unexpected behavior
<script src="https://api.mapbox.com/mapbox-gl-js/v3.32.0/mapbox-gl.js"></script> <style> body { margin: 0 } #map {
width: 100vw;
height: 100vh
}
</style>
<script>
// No Mapbox-hosted data is used, so the page renders without a valid token
mapboxgl.accessToken = '';
// Ten straight lines, each with its own empty `dash` array (solid line).
// Change `[]` to `[1, 0]` and every line renders fully.
const center = [-121.758798, 51.994982];
const features = [];
for (let i = 0; i < 10; i++) {
const lat = center[1] - 0.005 + i * 0.001;
features.push({
type: 'Feature',
properties: { dash: [] },
geometry: { type: 'LineString', coordinates: [[center[0] - 0.05, lat], [center[0] + 0.05, lat + 0.001]] },
});
}
const map = new mapboxgl.Map({
container: 'map',
style: { version: 8, sources: {}, layers: [{ id: 'bg', type: 'background', paint: { 'background-color': '#ffffff' } }] },
center,
zoom: 14.5,
});
map.showTileBoundaries = true;
map.on('load', () => {
map.addSource('lines', { type: 'geojson', data: { type: 'FeatureCollection', features } });
map.addLayer({
id: 'lines',
type: 'line',
source: 'lines',
layout: { 'line-cap': 'round', 'line-join': 'round' },
paint: { 'line-color': '#ff0000', 'line-width': 4, 'line-dasharray': ['get', 'dash'] },
});
});
</script>
Relevant log output
- Dominant language
- TypeScript
- Stars
- 12.5k
- Forks
- 2.4k
- PR merge metrics
- No merged PRs in 30d
Getting set up
- No Dockerfile or Docker Compose file
- Has a pull request template
- Read the contributing guide
First steps
- Read the whole issue, then the project's contributing guide.
- Comment on the issue to say you are picking it up — it saves two people doing the same work.
- Fork the repository and make your change on a branch.
- Open a pull request that references the issue number.
More from mapbox/mapbox-gl-js
-
auto-triaged bug :lady_beetle:
Difficulty 1/5 Under an hour Newbie friendliness 84/100
mapbox/mapbox-gl-js#13746 ·
-
TypeError: Cannot read properties of undefined (reading 'get') when a map is removed while a color-theme LUT is decodingPossibly taken @Piyush495 claimed this 22 days ago. Openauto-triaged bug :lady_beetle:
Difficulty 2/5 1-3 hours Newbie friendliness 82/100
mapbox/mapbox-gl-js#13737 · 1 comment ·
-
auto-triaged bug :lady_beetle:
Difficulty 2/5 1-3 hours Newbie friendliness 84/100
mapbox/mapbox-gl-js#13723 · 2 comments ·
-
auto-triaged bug :lady_beetle:
Difficulty 1/5 Under an hour Newbie friendliness 86/100
mapbox/mapbox-gl-js#13717 ·
-
>= v3.21 Symbol layer without feature-state expression crash the mapPossibly taken @liueric-dev claimed this 63 days ago. Openauto-triaged bug :lady_beetle:
Difficulty 2/5 1-3 hours Newbie friendliness 74/100
mapbox/mapbox-gl-js#13714 · 1 reaction ·
All issues in mapbox/mapbox-gl-js
Similar issues
-
e2e-failure ready-to-code
Difficulty 1/5 Under an hour Newbie friendliness 82/100
redhat-developer/rhdh-plugin-export-overlays#4616 · 1 comment ·
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 78/100
Maintainers usually reply within 1 day
-
Difficulty 2/5 1-3 hours Newbie friendliness 66/100
Maintainers usually reply within 1 day
-
ai-handle
Difficulty 2/5 1-3 hours Newbie friendliness 72/100
nextlevelbuilder/dewee-web-v2#73 ·
Maintainers usually reply within 1 day
-
backend bug good first issue
Difficulty 2/5 1-3 hours Newbie friendliness 70/100
PoCInnovation/Distribox#255 ·
Maintainers usually reply within 1 day