florian.berthold 48c401909e fix: pin click should not bubble to canvas (was placing waypoints under notes)
Clicking a Note (or any pin) in Route mode used to:
  1. fire the canvas click handler → drop a waypoint at the note's spot
  2. only THEN run the pin's own click/dblclick handlers

So a single click on a note added a stray waypoint, and a double-click added two and may not have reliably reached the dblclick handler. Edit-via-dblclick therefore felt broken.

Fix: every interactive pin (note, text label, route waypoint, pull marker) now stops 'click' propagation explicitly. Click and dblclick on a pin no longer affect the active tool. Drag detection bumped to a 3px threshold and now only persists history when the pin actually moved.
2026-04-25 23:22:04 +02:00

mplus-routes

Mythic+ route planner for Ascension classic-vanilla dungeons. Static web app targeted at mplus.exil.es. Uses Keystone.guru map tiles + enemy data, courtesy of RaiderIO (verbal permission, see attribution below).

Stack

  • Maps: keystone.guru z=4 tile pyramid stitched to 6144×4096 WebP per dungeon-floor (~150600 KB each).
  • Data: keystone.guru mapcontext/data/<slug>/<mapping_id>/split_floors.js — enemies (with positions, classification, NPC linkage), enemy packs (polygon outlines), enemy patrols (polylines), map icons (skulls, doors, comments, gateways), NPC names from companion en_US.js.
  • Frontend: vanilla HTML/CSS/JS, single page, no build step. Pan/zoom via CSS transform on the canvas stage; SVG overlay in image-pixel space.
  • Hosting: static. ~470 MB of WebPs total. Drop behind nginx; no backend.

Layout

mplus-routes/
├── data/
│   ├── kg_dungeons.json      registry: tile_key, data_slug, mapping_id per dungeon
│   └── kg/                   raw kg tiles + data (gitignored)
│       ├── _summary.json
│       └── <tile_key>/
│           ├── split_floors.js
│           ├── lang.js
│           └── floor<n>/z4/<x>_<y>.png
├── tools/
│   ├── kg_fetch.py           fetch tiles + data files for every dungeon in the registry
│   ├── kg_resync_summary.py  rebuild data/kg/_summary.json from disk (after partial fetches)
│   ├── kg_stitch.py          assemble 16×16 tile grids → web/assets/maps/<key>[_floor<n>].webp
│   ├── kg_build_data.py      combine into web/assets/dungeons.json
│   └── check_kg_alignment.py server-side alignment sanity check (PIL renders into /tmp/)
└── web/
    ├── index.html
    ├── style.css
    ├── app.js
    └── assets/
        ├── dungeons.json
        └── maps/             per-dungeon WebPs (gitignored — too big without LFS)

Build

python3 -m venv .venv
.venv/bin/pip install Pillow

.venv/bin/python tools/kg_fetch.py --workers 32 --zoom 4   # fetch tiles + data (~1.3 GB raw)
.venv/bin/python tools/kg_stitch.py --workers 4            # → web/assets/maps/*.webp (~470 MB)
.venv/bin/python tools/kg_build_data.py                    # → web/assets/dungeons.json

Run locally

cd web && python3 -m http.server 8765   # → http://localhost:8765/

Deployment

Ansible role + playbook for mplus.exil.es live in /home/sub/repos/sub-net/ansible/roles/mplus_routes and playbooks/setup_mplus_routes.yml. Targets exiles01:/srv/www/mplus.exil.es/ behind the public HAProxy SNI router. The wildcard *.exil.es cert covers TLS automatically.

First-time bring-up:

  1. Add mplus.exil.es A/AAAA records in NetBox (→ public HAProxy VIPs)
  2. Push the Ansible commit, trigger Ansible_DeployPublicHaproxy
  3. Materialize web/assets/maps/ on the deploy host (run the build pipeline above)
  4. Run playbooks/setup_mplus_routes.yml

Coverage

29 classic dungeons in the picker:

Dungeon Enemies Packs Map icons
Ragefire Chasm 139 9 1
The Deadmines 218 26 13
Wailing Caverns 261 18 8
Shadowfang Keep 140 6 5
The Stockade 94 21 5
Blackfathom Deeps 240 20 21
Gnomeregan 282 44 25
Razorfen Kraul 199 39 12
Razorfen Downs 219 53 12
SM (4 wings) 69+96+87+79 22+25+20+25 7+2+4+4
Uldaman 261 33 12
Zul'Farrak 302 40 6
Maraudon 548 107 4
Blackrock Depths 819 169 33
Lower Blackrock Spire 380 98 17
Upper Blackrock Spire 207 56 20
Stratholme 377 77 19
Scholomance 344 64 14
Dire Maul (3 wings) 229+300+241 58+44+48 7+7+10
Naxxramas (40) 574 92 2
Molten Core 241 39 1
Blackwing Lair 79 15 1
Zul'Gurub 0 0 0
Sunken Temple 0 0 0

ZG and Sunken Temple have map tiles but no enemy data — kg's mapping_id search didn't find populated data for them at time of build. Picker still lets you draw routes on the map manually.

Attribution

Map tiles and enemy data derived from Keystone.guru (RaiderIO, Inc.). Used with permission from the maintainers for the Ascension community deployment at mplus.exil.es. World of Warcraft trademarks belong to Blizzard Entertainment; this site is not affiliated with Blizzard.

Refresh data sources

Both kg's tiles and data update over time. To refresh:

  • Re-run the wide mapping-id probe (tools/ doesn't ship one — easiest is to fetch the kg homepage and find current ids, or re-run a probe like the one in data/kg/_summary.json headers note).
  • Update data/kg_dungeons.json with new mapping_ids.
  • Re-run kg_fetch.pykg_stitch.pykg_build_data.py.

Known gaps

  • 2/29 dungeons (Zul'Gurub, Sunken Temple) are present without enemy data.
  • Multi-floor dungeons (LBRS 7, BWL 4, Naxx 6, etc.) have all floors mapped individually; the floor tabs in the UI let you switch between them.
  • No trash-pull authoring beyond kg's data — packs are inherited as-is. For custom-routing ("skip this pack, take this one"), use the route+pull tools in the toolbar.
S
Description
Mythic+ route planner for Ascension classic dungeons — mplus.exil.es
Readme 124 MiB
Languages
Python 46.6%
JavaScript 40.9%
CSS 9.4%
HTML 3.1%