Skip to content

feat(web): bring the app's 3D to the map: view button, bars, pillars, buildings, terrain and rotation - #596

Open
khagele wants to merge 7 commits into
efiten:masterfrom
khagele:feat/595-web-3d
Open

feat(web): bring the app's 3D to the map: view button, bars, pillars, buildings, terrain and rotation#596
khagele wants to merge 7 commits into
efiten:masterfrom
khagele:feat/595-web-3d

Conversation

@khagele

@khagele khagele commented Sep 6, 2026

Copy link
Copy Markdown
Contributor

Closes #595

Stacked on #592 (the map on MapLibre). It also carries the app-side commits it builds on, cherry-picked: #586 (terrain, app/src/terrain.js, three commits) and #588 (a bar's tint, pillarTint). Those show in this diff until they merge; the web half is what this PR adds.

The problem

Since #465 the map runs on MapLibre and could pitch, but nothing on the site reached it: rotation and tilt were off, there was no hex-3d, points-3d or buildings-3d layer, no terrain and no control. A visitor who knows the app's 3D view could not get the same read of a cell's signal on the site, and the site is meant to be the superset of the app.

My changes in this PR

The app's 3D, whole (decision log docs/2026-09-06-map-in-3d.md):

  • A view button in the map's control corner under the zoom and the compass (mapcore.js addButton, a MapLibre control group of one). Pressed, the camera eases to the app's pitch of 60; the hex cells stand as bars in the tint of their own cell (app: 3D bars don't render the tier colour their own flat cell does #412), the receptions as octagon pillars collapsed where they coincide (app: coincident 3D pillars z-fight against each other when receptions share a position #402), the buildings rise from the hosted style's own vector source. The layer mode stays the segmented control in the filter panel; which layers a view shows is maplayers.js, so Both in 3D draws pillars over flat cells, as the app does.
  • 3D carries the terrain. Kasper's call (2026-09-06): the 3D view is the terrain switch. Hillshade and the relief mesh come with it at the exaggeration from Settings (steps 1, 2, 4, 7, 10, default 7, the app's control) and go with it. The app follows the same rule since this morning (feat(app): ship terrain on the AWS DEM, with a terrain button and an exaggeration setting #586, its terrain button went), so terrainPlan is read the same way on both surfaces.
  • Rotation and tilt on: right- or ctrl-drag, two-finger turn and tilt. MapLibre's compass draws the bearing and the pitch and puts north back; it is drawn as a mask over currentColor, since the library's needle is a dark image on our dark surface.
  • The view travels in the URL: ?view=3d is the layer state and on its own implies the fixed pitch; ?pitch= and ?bearing= are the camera and win when present. A flat north-up view keeps them out. ?exag= travels when off the default and persists like the theme.
  • Shared modules whole: signal.js, maplayers.js, pointmarker.js, terrain.js are the app's files in web/, pinned byte for byte in parity.test.js. The web copy of signal.js used to be a hand-kept half.
  • --ch-building added to both themes with the app's values, pinned per theme: undefined it was '', which MapLibre rejects and then skips the layer without a word.
  • Changelog entry in both copies.

Three e2e tests fixed on the way, all pre-existing and all only visible under a full parallel run. Two were races in the tests: popover.spec.js read a panel's box once right after a resize (now polled, every edge still has to end up inside), and onboarding.spec.js:233 counted the callouts and then waited on the inline panel from a later placement pass (now one atomic snapshot). The third was a bug in the map, in its own commit: a guest deep-linking ?nodepos=1 got the "needs a verified member account" key from the role gate, which also unchecks the box, and the refresh the gate then asks for redrew from the box and took the key back 250 ms later. The key now answers the ask, held past the uncheck until the role can see the layer; the test arms a MutationObserver and forces a draw, so it fails every time without the fix (3 of 3, checked) instead of 2 in 6.

Verification

Suite Result
web vitest 599 passed (35 files), eslint clean
web e2e --fail-on-flaky-tests 206 passed, five consecutive full runs exit 0 after the three fixes; the base (#592) 3 of 3
new view3d.spec.js 6 tests: the button both ways, the shared link, the fixed tilt, keyboard rotate and the compass, pillars and Both in 3D, the terrain following the view and the exaggeration

Browser, Playwright Chromium at 1100x700 against real OpenFreeMap tiles and the real DEM (Liège, relief and a city): 2D Both draws 40 points and 31 cells; the button eases to pitch 60, the mesh arrives once the DEM tiles are in (ready: true, mesh: true), 40 pillars over flat cells, buildings up, hillshade on; Hex draws 31 bars; three Shift+Arrow turns give bearing=-45 in the URL with the compass turned; Settings shows the exaggeration row with the five steps.

Decisions worth a look

  • Pillars are rebuilt on every move, since their footprint floor is a pixel size; the app does it at 1 Hz.
  • Age fade is not on the pillars: the site's points carry no time window the way the app's records do.

🤖 Generated with Claude Code

khagele and others added 4 commits September 6, 2026 08:59
…D parity

The website drew its map with Leaflet on Carto raster tiles while the app
had moved to MapLibre on OpenFreeMap's vector styles (efiten#147): two engines,
two basemaps, two treatments of the same colour tokens, and no road to the
3D views the app has. map.js now builds its map from mapcore.js, a port of
the app's huntmap.js shape (the same styles, the bare fallback, the sky,
overlays re-added on every style load), with the collections the layers
read built in mapmodel.js, which the unit suite covers.

Points, hex cells, the CoreScope layers, Locate's cloud and dots, the node
positions and the ticker's ring are GeoJSON sources swapped whole; markers
and popups are MapLibre's. Zoom keeps Leaflet's numbers in shared links and
in the server's hex binning, converted at the edge, so no link and no cell
changes. The e2e fixtures answer the hosted style inline so load fires
offline, and the specs read sources through page hooks where they counted
Leaflet's SVG paths.

Closes efiten#465

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…exaggeration setting

Terrain was pulled in efiten#247 because the DEM source kept the map loading and
froze weak GPUs. efiten#394 decided it comes back on the AWS terrarium tiles,
capped at z10, exaggeration 7 by default because the Low Countries show no
relief otherwise. This adds the source and a hillshade layer under the
signal overlays, a sixth FAB that turns terrain on and off, and the
exaggeration steps in Settings with the slope caveat on the control.

The mesh waits: hillshade follows the button, setTerrain needs the button,
DEM tiles for the view and a 3D view, so a slow host leaves the map flat
rather than stalled. Leaving 3D drops the mesh before the tilt back, and
entering 3D sets it once the tilt has settled, since easeTo({pitch}) is a
no-op under a mesh.

Closes efiten#396

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
Kasper (2026-09-06): the button is the opt-in, not the opt-out. The
loader answers on only for a stored '1'; the hint, the decision log and
the release note say so.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e light

The flat cell is the tier colour at the tier's opacity over the basemap, a
19% tint for faint; the bar drew the token opaque at 0.85 for every tier,
so a faint bar stood as solid purple on a pale patch, worse the weaker the
signal. On top, MapLibre's default light darkened every face by 7 to 17%.

The bar now takes the cell's tint pre-mixed over the theme background,
opaque, so shared walls keep depth-testing, and the style light sits at
0.15 on every style load. Measured pixel for pixel in both themes: the bar
is within a few levels of its cell at hot and at faint. The theme joins the
hex cache key so a switch rebuilds the colours.

Closes efiten#412

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
khagele and others added 2 commits September 6, 2026 11:15
Kasper (2026-09-06): 3D takes the terrain and the exaggeration along at
once, the same rule the map follows in efiten#595. The sixth FAB goes, with
its loader, its CSS and its place in the onboarding ring; huntmap's plan
reads mode3D as the switch and the map only takes the exaggeration.
Settings keeps the exaggeration. Decision log and release note follow.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… buildings, terrain and rotation

Closes efiten#595. The map could pitch since efiten#465 and nothing reached it.

A view button under the zoom and the compass eases to the app's pitch;
hex cells stand as bars in their own tint (efiten#412), receptions as octagon
pillars collapsed where they coincide (efiten#402), buildings rise from the
hosted style's source. 3D carries the terrain on the site (Kasper,
2026-09-06): hillshade and the mesh come with the view at the Settings
exaggeration. Rotation and tilt on; MapLibre's compass in the house
colours. ?view=3d, ?pitch=, ?bearing= and ?exag= travel in the URL.

signal.js, maplayers.js, pointmarker.js and terrain.js are the app's
files whole in web/, pinned byte for byte. --ch-building added to both
themes: undefined it was '', which MapLibre rejects and then skips the
layer silently.

Two e2e races fixed: popover's on-screen read polls, onboarding:233
reads one snapshot.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
A guest deep-linking ?nodepos=1 got the "needs a verified member
account" key from the role gate, which also unchecks the box; the
refresh the gate then asks for redrew from the box and took the key
back 250 ms later. The ask is now held below member until the role can
see the layer. The efiten#376 test arms an observer and forces a draw, so a
flash no longer passes it.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
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.

web: 3D on the MapLibre map, parity with the app (view, extrusions, terrain, rotation)

1 participant