| Pick pass (clickable entities, drag handles) |
ClickMap, MapClick extends Clickable, Clicklist, ClickLocation, Gob.GobClick |
| ⚠️ A pick off the view answers twice |
Clicklist.get(out, c, cb) with c outside the view, and fuzzyget when the fuzz box misses the view entirely, defer cb.accept(null) and then go on to issue the pixel read anyway — there is no return after the defer. get’s read answers cb a second time; fuzzyget’s indexes a null area inside its read callback. A caller whose cb is not idempotent, or who picks at a point that can be off the view, guards it themself |
| What a ground click resolves to |
The pick pass (clmaptree) holds ground only: each raster adds MapClick(cut).apply(cut.flat) — the cut’s flat mesh, whose per-vertex ClickLocation.vertex is the 0..1 place over the cut — and checkmapclick reads back which MapClick and where, then (ul + pos × sz) × tilesz. Fork: MapClick carries ul, sz and map rather than only the cut, so ground that has no MapMesh is clickable too (MapView.farclick(ul, sz, mesh): any model carrying ClickLocation.vertex, in session tile coords, map null so no member offset applies). The view distance’s recorded cuts (MapView.RecallClick, exactly the cuts the recorded raster has in the scene) and its far cells are in this pass, so a click on them is an ordinary wdgmsg("click", …) to that place. Where the pick finds no ground at all, Hittest calls nohit, which upstream leaves empty and which drops whatever the object pick found under the same pixel; fork: Hittest hands that on through nohit(pc, inf) (which falls back to nohit(pc) for every subclass that does not override it). Click.nohit sends an object found there as a click on that object at its own rc, the pair MiniMap.mvclick sends, and with no object sends the ground click anyway at MapView.groundless(pc): the pointer’s ray (the inverse of prj · cam · loc, NDC z −1 → 1, scene y negated back to map y) meeting the plane at the character’s height, nothing past the far plane or above the horizon, and nothing from a view that is no number. It is taken when the Click is built, on the UI thread with the camera the pick is drawn with, because the pick answers frames later on the callback thread |
| Click dispatch ← intercept point |
MapView.Hittest resolves pick → Click.hit → clickhit ends in wdgmsg("click", …); every fork tap on a map click hangs there (a ghost click, the voice move intent for a ground click, the click token). ⚠️ The pick is ASYNCHRONOUS, on a thread of its own: env.submit queues the readback, and the completion is run from GLEnvironment.callbacks by GLEnvironment.cbthread — the "Render-query callback thread", started on demand and exiting after 5 idle seconds, which is neither the UI thread nor the render thread. So the pick cannot serve anything that must answer inside the event (a press that takes a grab, for one), and — the half that is easy to miss — whatever the callback body touches, it touches off the UI thread, while the frame mutates it underneath: checkmapclick resolves its cut and its coordinate there, so any state it reaches has to be published for that thread rather than merely reachable from it. Fork: four // addon: hooks at the top of mousedown/mouseup/mousemove/mousewheel (+) run a synchronous test first and fall through when it misses |