Skip to content

Basemaps: stack web basemaps and blend tile layers - #427

Merged
giswqs merged 3 commits into
mainfrom
fix/issue-329-basemap-blend
Oct 18, 2026
Merged

giswqs merged 3 commits into
mainfrom
fix/issue-329-basemap-blend

Conversation

@giswqs

@giswqs giswqs commented Oct 17, 2026 •

Copy link
Copy Markdown
Member

Web tile layers could only be drawn plainly over each other, and the Basemap gallery's Web Basemaps always replaced the current basemap (replace: true), so a satellite image could not be draped over a hillshade.

Changes

  • Model: TileLayer gains blend (the same Blend modes layers already have: Normal, Multiply, Screen, Overlay, Darken, Lighten, Difference). It is saved with the project, and only when it isn't Normal, so existing projects are unchanged.
  • Commands: map.addTiles and map.setTiles take "blend", and map.tiles lists it. An unknown mode is an error. Changes can be undone.
  • Renderer: a blended tile layer is drawn on a layer of its own and composited with its tiny-skia blend mode over the tile layers and basemap beneath. This works on the flat Web Mercator map and in the mosaic that is warped onto the globe and other projections. In the mosaic a layer blends with the tile layers beneath it, and the mosaic then goes over the built-in basemap.
  • UI: the Layers pane's right-click menu for a tile layer has a Blend submenu next to Opacity. The Basemap gallery has a Stack on the current basemaps checkbox: when it's ticked, a Web Basemap is added over the others instead of replacing them.
  • Docs: docs/user-guide/maps.md and the map.addTiles row in docs/plugins.md are updated.

The external Basemaps plugin (opengeos/rustgis-plugins) can now stack by sending replace: false and set a blend mode with blend. That change belongs in that repo.

Tests

  • render::tiles::tests::blends_tile_layers_with_those_beneath: a style tile layer set to Multiply over red raster tiles draws red × blue instead of blue, Screen draws lighter than either layer, and Multiply also holds on the globe.
  • engine::cmd::tiles::tests::stacks_basemaps_and_blends_them: stacks two basemaps without replace, sets the blend through map.addTiles and map.setTiles (and undoes it), rejects unknown modes and round-trips the project.

Verification

  • cargo xtask ci and cargo build --target wasm32-unknown-unknown -p rustgis-web pass locally.
  • Headless ui_shot with real tiles over the Grand Canyon: Esri World Hillshade under USGS Imagery. With the imagery set to Multiply, the hillshade's shadows and ridges show through the orthophoto. Switching it back to Normal shows the plain imagery. The Layers pane menu shows Blend: Multiply and the mode list in both the light and dark themes. The Basemap gallery shows the new Stack on the current basemaps checkbox above the Web Basemaps.

Fixes #329

Tile layers take a blend mode (Normal, Multiply, Screen, Overlay,
Darken, Lighten, Difference), set with map.addTiles/map.setTiles and
saved with the project. The renderer draws a blended tile layer on a
layer of its own and composites it with that mode, on the flat map and
in the mosaic warped onto the globe and other projections. The Layers
pane's tile layer menu picks the mode, and the Basemap gallery's Web
Basemaps can stack on the current basemaps instead of replacing them.

Fixes #329
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 9 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 4 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
  • Configuration used: Organization UI
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 411a645a-cc15-4b49-b816-71a6f1eb9dfc
📥 Commits

Reviewing files that changed from the base of the PR and between c60bbfe and 289d79d.

📒 Files selected for processing: 8
  • crates/engine/src/cmd/tiles.rs
  • crates/map/src/tiles.rs
  • crates/render/src/lib.rs
  • crates/render/src/tiles.rs
  • crates/ui-egui/src/panes.rs
  • crates/ui-egui/src/ribbon.rs
  • docs/plugins.md
  • docs/user-guide/maps.md
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

Comment thread crates/render/src/tiles.rs Outdated
Comment thread crates/ui-egui/src/ribbon.rs Outdated
@github-actions

Copy link
Copy Markdown

Code review

The change is small and consistent: a per-tile-layer blend mode that is saved with the project, exposed through the commands, rendered, and added to the UI. I found no crashes or security problems. I read the diff and the surrounding code but did not build or run the tests.

Bugs

  • Re-clicking a stacked basemap resets its settings (medium confidence). ribbon.rs:616. With "Stack on the current basemaps" ticked, clicking an already-shown basemap calls map.addTiles. That removes the same-url layer and re-inserts it on top as a fresh Normal layer. The blend mode, opacity and position the user set are lost. Make a click on a selected button a no-op, or remove that layer.

Performance

  • Full-size pixmap per blended layer, per redraw (medium confidence). crates/render/src/tiles.rs:167. draw_layer allocates a zeroed pixmap the size of the canvas for every non-Normal layer, on every frame and export. A scratch pixmap reused across the layer loop would avoid this. If Pixmap::new fails, it falls back to Normal blending without any log.

Quality

  • Doc comment out of date (low confidence). The doc comment on tile_row in panes.rs lists opacity, move and remove but not the new Blend menu.
  • Blend on the lowest layer (low confidence). A non-Normal blend on the bottom tile layer blends against the map background or graticule, not a basemap. This is probably acceptable, but the docs could say so.
  • Partial command validation (informational). blend is validated before any mutation in set, so errors leave the map untouched. In add, an invalid blend is rejected before the map is touched. The test covers both.

Security

  • Nothing found. blend is parsed through the Blend::from_code whitelist, with no panics or indexing.

AGENTS.md

  • No problems found. There is no unwrap in non-test code, and the new UI strings use tl! and i18n::fmt. The change touches no layering, wasm or asset rules. The new render::blend_mode is pub(crate).

Change size

Compared: the whole PR against main (merge base c60bbfe to PR head fcd1d69).

Metric Value
Files changed 8 (added 0, modified 8, removed 0, renamed 0)
Size before 517.4 KB
Size after 523.5 KB
Total size change +6.1 KB
File Before After Change
crates/engine/src/cmd/tiles.rs 9.5 KB 11.9 KB +2.4 KB
crates/render/src/tiles.rs 10.6 KB 12.4 KB +1.8 KB
crates/ui-egui/src/ribbon.rs 165.9 KB 166.5 KB +687 B
crates/ui-egui/src/panes.rs 177.7 KB 178.2 KB +532 B
crates/map/src/tiles.rs 12.2 KB 12.5 KB +352 B
docs/user-guide/maps.md 14.6 KB 15.0 KB +325 B
crates/render/src/lib.rs 105.3 KB 105.3 KB +11 B
docs/plugins.md 21.7 KB 21.7 KB +8 B

@github-actions

github-actions Bot commented Oct 17, 2026 •

Copy link
Copy Markdown

📖 Docs preview

Item Value
Preview https://preview.rustgis.app/pr-427/
Web app https://preview.rustgis.app/pr-427/app/
Commit 289d79d

GitHub Pages takes a minute or two to publish a new commit.

- render: blended tile layers share one scratch pixmap per draw instead of
  allocating a zeroed full-size pixmap for each, and a failed allocation
  (falling back to Normal blending) is logged.
- ribbon: when stacking web basemaps, clicking a basemap already shown
  removes it instead of re-adding it, which reset its blend mode, opacity
  and position.
@github-actions

Copy link
Copy Markdown

Code review

This PR adds a per-layer blend mode to web tile layers. The command params, TileLayer serde, renderer, Layers pane menu, a "Stack on the current basemaps" option and docs are all covered. I read the full diff and the surrounding add and set command code. I did not build or run anything, so the tests are unexecuted. I found no blocking issues and posted no inline comments.

Bugs

  • None found. Confidence is moderate to high. Checked:
    • blend(p)? is parsed before any mutation in both add and set, so an invalid mode leaves the map untouched. The test asserts tiles.len() == 2 after the failed add.
    • draw_layer falls back to an unblended draw if the scratch pixmap can't be allocated, so it won't panic. There is no unwrap or expect.
    • Blended layers in the warped-mosaic path blend inside the mosaic, so the result is consistent across projections.
  • Low-confidence behavior note: add replaces an existing layer with the same URL and kind (tiles.retain), which also resets its blend. The ribbon avoids this in stack mode by removing the layer instead. Other callers, such as a plugin re-adding a layer, will silently lose the blend. This is arguably intended, but docs/user-guide/maps.md doesn't mention it.

Performance

  • Each blended tile layer is drawn onto a full-size scratch pixmap, cleared, then composited. The scratch pixmap is reused across layers within a draw, but it is a new allocation per draw call. The extra cost is about one full-canvas fill and composite per blended layer per frame. That is fine for typical use, but it shows up when panning with several blended layers on a large or high-DPI canvas. Keeping the scratch pixmap across frames would remove it. Confidence is low, and I'd treat this as optional.

Quality

  • draw_layer passes labels through for blended layers. Labels are collected rather than drawn into the pixmap, so they should be unaffected, but I didn't read rustgis_tiles::draw to confirm.
  • ribbon.rs: the "stack" checkbox state is persisted in egui memory under web-basemaps-stack. The Basemaps plugin tab and the "Add Tile Layer…" dialog don't use it, so stacking is only available for the six built-in web basemaps. Confidence is high, and it is probably fine for a first step.
  • ribbon.rs: ui.checkbox(...), then resp.on_hover_text(...).changed() works, but the resp binding followed by an immediate move is a little awkward. This is a nit.
  • Minor: blend_mode is now pub(crate) in render/src/lib.rs, which is fine.

AGENTS.md

  • Wrapping in tl! and using i18n::fmt follow the guidelines. No unwrap, expect or panic in non-test code. I found nothing that breaks wasm.
  • map.addTiles and map.setTiles params docs are updated. The blend field is skipped on serialization when it is Normal, so existing projects are unchanged.
  • Test coverage is good: engine round-trip, undo and invalid input, plus render tests for multiply, screen and the globe mosaic. hostile_params_never_panic should cover the new param through the command registry, but I did not run it.

Change size

Compared: the whole PR against main (merge base c60bbfe to PR head a036add).

Metric Value
Files changed 8 (added 0, modified 8, removed 0, renamed 0)
Size before 517.4 KB
Size after 524.4 KB
Total size change +7.0 KB
File Before After Change
crates/render/src/tiles.rs 10.6 KB 13.0 KB +2.4 KB
crates/engine/src/cmd/tiles.rs 9.5 KB 11.9 KB +2.4 KB
crates/ui-egui/src/ribbon.rs 165.9 KB 166.9 KB +1015 B
crates/ui-egui/src/panes.rs 177.7 KB 178.2 KB +532 B
crates/map/src/tiles.rs 12.2 KB 12.5 KB +352 B
docs/user-guide/maps.md 14.6 KB 15.0 KB +325 B
crates/render/src/lib.rs 105.3 KB 105.3 KB +11 B
docs/plugins.md 21.7 KB 21.7 KB +8 B

The Basemap gallery's Stack tooltip and the user guide now say that
clicking a web basemap that already shows removes it when stacking.
@github-actions

Copy link
Copy Markdown

I'll fold that into the summary instead.

Code review

Blending tile layers is small and well-contained. I found no bugs. I read the diff and the surrounding Blend and addTiles code, but did not build or run anything.

Bugs

  • None found. blend is parsed (and rejected if unknown) before any state changes in both add and set. The test confirms a bad mode leaves the layer count unchanged. Blend::from_code is case-insensitive and trims, which the test relies on. skip_serializing_if keeps old projects unchanged on save, and #[serde(default)] loads them.
  • The draw path checks the scratch size, clears it between layers, and falls back to an unblended draw if allocation fails. This is panic-free, as AGENTS.md requires.

Quality

  • Ribbon stacking ignores errors (low confidence, crates/ui-egui/src/ribbon.rs around line 315). The result of app.run(...) is discarded. With replace: false, map.addTiles can now fail on the MAX_TILE_LAYERS limit, so a click can do nothing with no feedback. A hidden layer with the same id isn't sel, so stacking it again adds a second copy. Neither case is serious.
  • Scratch pixmap cost (low confidence). Each blended tile layer composites a full-canvas pixmap. The scratch is reused between layers in one draw, so this is acceptable. It is allocated again on every frame, which is only worth revisiting if profiling shows a cost.
  • Cross-crate visibility. blend_mode in crates/render/src/lib.rs is now pub(crate), which is fine.

Security

  • No concerns. blend is a closed enum parsed from a string, and unknown values return an error.

Performance

  • Nothing beyond the scratch-pixmap note above. Layers with Blend::Normal take the original direct-draw path, so default behaviour costs nothing extra.

AGENTS.md

  • User-visible strings use tl! and i18n::fmt. There are no unwrap/expect calls in non-test code (the new ones are in tests). The ui-egui code stays in L6 and the render code stays in L3.
  • The command schema strings, docs/plugins.md and docs/user-guide/maps.md are updated.
  • There is no new hostile_params_never_panic case for blend. That test already runs junk params through every command, and the new code returns an error for them, so this is optional.

Change size

Compared: the whole PR against main (merge base c60bbfe to PR head 289d79d).

Metric Value
Files changed 8 (added 0, modified 8, removed 0, renamed 0)
Size before 517.4 KB
Size after 524.6 KB
Total size change +7.1 KB
File Before After Change
crates/render/src/tiles.rs 10.6 KB 13.0 KB +2.4 KB
crates/engine/src/cmd/tiles.rs 9.5 KB 11.9 KB +2.4 KB
crates/ui-egui/src/ribbon.rs 165.9 KB 166.9 KB +1.1 KB
crates/ui-egui/src/panes.rs 177.7 KB 178.2 KB +532 B
docs/user-guide/maps.md 14.6 KB 15.0 KB +377 B
crates/map/src/tiles.rs 12.2 KB 12.5 KB +352 B
crates/render/src/lib.rs 105.3 KB 105.3 KB +11 B
docs/plugins.md 21.7 KB 21.7 KB +8 B

@giswqs
giswqs merged commit 3ed7f1c into main Oct 18, 2026
17 checks passed
@giswqs
giswqs deleted the fix/issue-329-basemap-blend branch October 18, 2026 00:05
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[Feature]: Support concurrent basemaps with blend mode controls (Multiply, Overlay) in the Layers pane

1 participant