feat(app,web): every repeater's reach at once, in its own hue, as the third stop of Node positions - #593
Open
khagele wants to merge 11 commits into
Open
feat(app,web): every repeater's reach at once, in its own hue, as the third stop of Node positions#593khagele wants to merge 11 commits into
khagele wants to merge 11 commits into
Conversation
…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>
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>
Filtering to one repeater showed where it was heard and nothing about how far it reaches. The map now draws one line from the node's position to every reception attributed to it, in that reception's tier colour: strong near the node, weak at the edge, with the gaps where nobody drove. The hub is the registry's advertised position when the resolver has one and the RSSI estimate otherwise, marked so the two never look alike. On for every picked node, and toggled from a point, sighting or registry popup for any node heard, companions included. A lower bound from where hunters drove, on an unauthenticated identity; both caveats stay on the hub and the button. Decisions after design rounds R13 and R14. Closes efiten#549 Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
…e third stop of Node positions The star of one node (efiten#549) becomes the coverage overview (efiten#603): one ray per hearing attributed to a repeater, from its advertised position or its RSSI estimate, in a hue fixed per id, the signal strength in the ray's width and opacity. A two-way hearing (a Discover or trace reply) draws at full opacity, a one-way one at 40%. A tap on a repeater's marker, or a picked target, selects that star and drops the others to 25% in their own hue. In 3D the rays leave the mast 30 m up, drawn by a custom WebGL layer (raylayer.js), since a MapLibre line lies on the ground. Node positions in the filter panel is a three-stop segmented control: Off / Positions / Positions + reach. The popup Show reach buttons go. coverage.js and raylayer.js are copied whole between app and web (parity.test.js), with the twelve --ch-hue-* tokens in both sheets. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
… reach in its own hue, on the ground and from the mast in 3D The app draws the same coverage as the map (efiten#603): the FAB cycles off / positions / positions + reach with the ring showing the stop, the stars are built from the plotted window (the sender-free window while a target is picked, so the other stars stay up at a quarter), a tap on a ▲ or ● selects one, and in 3D the rays leave the mast through the same custom layer. One changelog entry for both surfaces replaces the map-only reach entry of efiten#549, and docs/2026-09-08-coverage-overview.md records the decisions. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
The pill read the checkbox that efiten#603 replaced, so the stop never counted. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
khagele
force-pushed
the
feat/549-reach
branch
from
September 8, 2026 16:39
1ff9443 to
a83d20c
Compare
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #549
Closes #603
Stacked on #592 (
feat/465-maplibre-map) and #596 (feat/595-web-3d): the rays are a MapLibre line source in 2D and ride the map's 3D view. Merge those two first; this one then rebases to its own diff.The problem
The map answers "where did I hear something" in one palette: every cell and point takes the tier colour of its best RSSI, whoever was heard there. With five repeaters along one road that is one red band, and the question at the end of a drive, which repeater covers what and where one hands over to the next, has no view. #549 asked for the reach of one node; #603 asks for all of them at once. The decisions are in the two issue threads and in
docs/2026-09-08-coverage-overview.md.My changes in this PR
nodeposmode.js); the map's filter panel has a segmented control, and?nodepos=reachtravels in the URL next to the?nodepos=1links already out there.coverage.js, copied whole between app and web): one ray per hearing attributed to a repeater (classifyReception's rule: a Repeater role, a relay hash, a Discover or trace reply), from its advertised position (▲) or its RSSI estimate (●). Signal strength runs in the ray's width and opacity over -120 to -60 dBm. The dots of the hearings take the repeater's hue while the reach is on.--ch-hue-*tokens in both sheets, the slot a hash of the id, so a repeater keeps its colour across sessions and between the app and the map. Two repeaters within 2 km never share a slot.raylayer.js, copied whole): each ray starts 30 m above the repeater and lands at ground level at the hearing, on the terrain when the mesh is on. A MapLibre line lies on the ground, so this is a custom WebGL layer of 150 lines: each ray a screen-space quad between two mercator points with altitude. No deck.gl.Verification
npm run buildcleanNew model tests, each mutation-checked:
coverage.test.js(19: attribution, two-way, the hash and its neighbour rule, the strength ramp, the origin, the grouping, the dimming),raylayer.test.js(6: the vertex layout, the premultiplied colour, the terrain lift),nodeposmode.test.js(4). Five e2e tests inreach.spec.js: the rays per repeater and their hues, the two-way opacity, the dot colours, the selection by tap and by target, the 2D/3D hand-over of the rays, the three stops in the URL.nodepos.spec.jsdrives the new control.parity.test.jspinscoverage.jsandraylayer.jsbyte for byte.In the browser: the app with 33 seeded receptions over three repeaters and a companion, in 2D, with a selection, and in 3D with the pillars; the map in 2D and 3D over the same set.
Not in this PR
🤖 Generated with Claude Code