diff --git a/meshterm/ui/map_render.py b/meshterm/ui/map_render.py index 175008c..ad6f3bd 100644 --- a/meshterm/ui/map_render.py +++ b/meshterm/ui/map_render.py @@ -376,9 +376,10 @@ def render_ground( boundaries and the through-roads (:data:`_COARSE_ROAD_PRIORITY`) — no buildings, no back streets, no text at all. It is the same picture at a lower resolution of detail, and it is a quarter of the work: on the PicoCalc, 249 ms against 962 at zoom 13, 120 ms - against 529 at zoom 16. The map draws one of these before the full frame so a view - that has run past its last ground has *something* true on it within a blink, and so - that a pan being held has a unit of work small enough to abandon (see + against 529 at zoom 16. Drawn before the full frame it gives a view that has run past + its last ground *something* true within a blink, and gives a pan being held a unit of + work small enough to abandon — which is what the map asks for when + :data:`~meshterm.ui.map_screen._COARSE_PREVIEW` is on (see :meth:`meshterm.ui.map_screen.MapScreen._draw_ground`). Args: diff --git a/meshterm/ui/map_screen.py b/meshterm/ui/map_screen.py index 0c28dca..5635cf0 100644 --- a/meshterm/ui/map_screen.py +++ b/meshterm/ui/map_screen.py @@ -88,6 +88,19 @@ _TILE_CACHE_SCREENS = 2 #: history for a pan away and back to be instant. _MIN_TILE_CACHE = 8 +#: Whether a moved view is answered with the **rough first pass** before its finished +#: frame — the ground fills, the watercourses and the through-roads, in a quarter of the +#: time (see :func:`~meshterm.ui.map_render.render_ground`). Currently **off**: every view +#: waits for the whole picture, and what it stands on until then is the last frame's +#: ground reprojected (:class:`~meshterm.ui.map_render.Ghost`), or nothing where the view +#: has run past that too. +#: +#: The switch is here rather than in the renderer because it is a decision about *when to +#: ask*, not about what can be drawn: both passes still work, both are still tested, and +#: the abandonment point between them (:meth:`MapScreen._draw_ground`) is still where a +#: held pan key gets off. Turning it back on is this line. +_COARSE_PREVIEW = False + #: How many pans in one direction it takes to read as a heading rather than a nudge. _PREFETCH_MOMENTUM = 2 @@ -375,11 +388,12 @@ class MapScreen(Screen): edge you are panning onto — instead of the map blanking to black between every keypress and flashing back when the frame lands (JP, 2026-08-09). - The last of those is where the map used to go black *and stay black*: four coarse - pan steps are 120% of the screen, so nothing drawn is under the view any more and - there is no ground to reproject. What fixes that is not this method but the one it - schedules — see :meth:`_draw_ground`, which now lands a rough picture in a quarter - of the time rather than nothing at all for a whole raster. + The last of those is where the map goes black *and stays black*: four coarse pan + steps are 120% of the screen, so nothing drawn is under the view any more and + there is no ground to reproject. The answer to that is not this method but the one + it schedules — :meth:`_draw_ground` can land a rough picture in a quarter of the + time rather than nothing at all for a whole raster, which is + :data:`_COARSE_PREVIEW`, currently off. """ key = self._ground_key(vp) if self._frame_key == key and self._frame is not None: @@ -468,7 +482,7 @@ class MapScreen(Screen): self._frame_key, self._frame_coarse = key, False return self._drawing = key - preview = not self._ground_drawn(key) + preview = _COARSE_PREVIEW and not self._ground_drawn(key) asyncio.ensure_future(self._draw_ground(key, vp, tiles, markers, find, preview)) async def _draw_ground( @@ -488,7 +502,8 @@ class MapScreen(Screen): frame (measured worst-case delay ~50 ms, against the ~1 s of a blocking draw). **Two passes, because a whole frame is too big a thing to wait for or to throw - away.** A coarse pass (ground, water, through-roads; see + away** — where ``preview`` asks for both, which :data:`_COARSE_PREVIEW` currently + does not. A coarse pass (ground, water, through-roads; see :func:`~meshterm.ui.map_render.render_ground`) is a quarter of the work — 249 ms against 962 at zoom 13 on the PicoCalc — and it lands first, so a view that has run past every scrap of drawn ground shows something true within a blink instead @@ -514,9 +529,10 @@ class MapScreen(Screen): tiles: The decoded tiles it draws from. markers: The nodes to overlay, snapshotted at request time. find: The live find filter, likewise. - preview: Whether to draw the coarse pass first. Skipped where this view's - ground is already drawn in full (see :meth:`_ground_drawn`): there a - coarse picture would *remove* detail the reader can already see. + preview: Whether to draw the coarse pass first. Off wholesale while + :data:`_COARSE_PREVIEW` is; and skipped anyway where this view's ground is + already drawn in full (see :meth:`_ground_drawn`), since there a coarse + picture would *remove* detail the reader can already see. """ if preview and await self._pass(key, vp, tiles, markers, find, coarse=True): if self._wanted is not None: diff --git a/tests/test_map.py b/tests/test_map.py index 18e1068..f6ae7c5 100644 --- a/tests/test_map.py +++ b/tests/test_map.py @@ -2083,17 +2083,21 @@ def test_map_abandons_the_finished_pass_for_a_view_that_moved(monkeypatch) -> No monkeypatch.setattr(ms, "render_ground", render) - async def drive() -> None: + async def drive() -> tuple: screen.render_body(80) while screen._drawing is not None: await asyncio.sleep(0) passes.clear() screen._drawing = ("k",) await screen._draw_ground(("k",), screen._viewport, {}, [], "", True) + # Read here, not after the loop closes: the newer view's own draw is a task of its + # own by now, and what it goes on to ask for is that view's business, not this + # one's. The question is only what the *abandoned* key got. + return list(passes), screen._frame_coarse - asyncio.run(drive()) - assert False not in passes, "a finished pass was drawn for a view already left behind" - assert screen._frame_coarse is True + stale, rough_on_screen = asyncio.run(drive()) + assert stale == [True], "a finished pass was drawn for a view already left behind" + assert rough_on_screen is True, "the abandoned view left no rough picture on screen" def test_map_skips_the_coarse_pass_over_a_picture_already_drawn(monkeypatch) -> None: # noqa: ANN001 @@ -2123,6 +2127,41 @@ def test_map_skips_the_coarse_pass_over_a_picture_already_drawn(monkeypatch) -> assert coarse_asked == [False], "a drawn frame was replaced by a coarser one" +def test_the_rough_pass_is_a_switch_the_map_reads(monkeypatch) -> None: # noqa: ANN001 + """Both passes work; whether a moved view is offered the first one is a knob. + + Off, every view waits for the whole picture and stands on the last frame's ground + until it lands. The machinery is :meth:`MapScreen._draw_ground`'s either way — what + the switch decides is the *asking*, which is why it is read where the raster is + requested and not inside the renderer. + """ + from meshterm.ui import map_screen as ms + + def first_draw() -> list: + """The passes a view with nothing drawn under it asks for.""" + asked: list = [] + monkeypatch.setattr( + ms, "render_ground", + lambda vp, tiles, m, **k: (asked.append(k.get("coarse", False)) + or (["x"], None)), + ) + screen = _map_over(_StubSource()) + + async def drive() -> None: + screen.render_body(80) + while screen._drawing is not None: + await asyncio.sleep(0) + + asyncio.run(drive()) + return asked + + monkeypatch.setattr(ms, "_COARSE_PREVIEW", True) + assert first_draw() == [True, False], "the rough pass did not come first when asked for" + + monkeypatch.setattr(ms, "_COARSE_PREVIEW", False) + assert first_draw() == [False], "a rough pass was drawn with the switch off" + + def test_map_keeps_its_detail_while_a_find_is_typed(monkeypatch) -> None: # noqa: ANN001 """Typing a node name moves the view nowhere, so the streets must not flatten.