step 6: add stable overlays and live update stress tests
This commit is contained in:
@@ -1 +1,46 @@
|
||||
"""Draw layer bookkeeping helpers."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from typing import Any
|
||||
|
||||
from .types import Tag
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class DrawLayerTags:
|
||||
"""Internal Dear PyGui draw layer tags for one map."""
|
||||
|
||||
background: str
|
||||
tiles: str
|
||||
overlays: str
|
||||
attribution: str
|
||||
|
||||
|
||||
def draw_layer_tags(map_tag: Tag) -> DrawLayerTags:
|
||||
"""Return stable internal draw layer tags for a map."""
|
||||
|
||||
return DrawLayerTags(
|
||||
background=f"{map_tag}##layer-background",
|
||||
tiles=f"{map_tag}##layer-tiles",
|
||||
overlays=f"{map_tag}##layer-overlays",
|
||||
attribution=f"{map_tag}##layer-attribution",
|
||||
)
|
||||
|
||||
|
||||
def ensure_draw_layers(dpg: Any, *, drawlist_tag: Tag, map_tag: Tag) -> DrawLayerTags:
|
||||
"""Create draw layers if needed and return their tags."""
|
||||
|
||||
tags = draw_layer_tags(map_tag)
|
||||
for layer_tag in (tags.background, tags.tiles, tags.overlays, tags.attribution):
|
||||
if not dpg.does_item_exist(layer_tag):
|
||||
dpg.add_draw_layer(parent=drawlist_tag, tag=layer_tag)
|
||||
return tags
|
||||
|
||||
|
||||
def clear_draw_layer(dpg: Any, layer_tag: Tag) -> None:
|
||||
"""Clear one draw layer without touching sibling layers."""
|
||||
|
||||
if dpg.does_item_exist(layer_tag):
|
||||
dpg.delete_item(layer_tag, children_only=True)
|
||||
|
||||
@@ -3,13 +3,18 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from dataclasses import replace
|
||||
from typing import Any
|
||||
|
||||
from .commands import CommandKind, MapCommand
|
||||
from .draw_layers import DrawLayerTags, clear_draw_layer, ensure_draw_layers
|
||||
from .interaction import HitRect, calculate_hit_rect
|
||||
from .overlays import MarkerOverlay, Overlay, PolylineOverlay, TrajectoryOverlay
|
||||
from .projection import latlon_to_world
|
||||
from .sizing import SizeMeasurement, apply_size_measurement
|
||||
from .state import DirtyFlags, MapState
|
||||
from .tiles import Tile, VisibleTile
|
||||
from .types import Color, LatLon
|
||||
|
||||
|
||||
class MapRenderer:
|
||||
@@ -18,10 +23,10 @@ class MapRenderer:
|
||||
def __init__(self, state: MapState, dpg: Any) -> None:
|
||||
self.state = state
|
||||
self._dpg = dpg
|
||||
self._background_tag = f"{state.tag}##background"
|
||||
self._attribution_tag = f"{state.tag}##attribution"
|
||||
self._layers: DrawLayerTags | None = None
|
||||
self.last_drained_commands: tuple[MapCommand, ...] = ()
|
||||
self.last_hit_rect: HitRect | None = None
|
||||
self.last_overlay_count: int = 0
|
||||
|
||||
def schedule_next_frame(self) -> None:
|
||||
"""Schedule this renderer to run on the next Dear PyGui frame."""
|
||||
@@ -51,7 +56,8 @@ class MapRenderer:
|
||||
|
||||
with self.state.lock:
|
||||
dirty = self.state.dirty
|
||||
should_draw = bool(dirty & (DirtyFlags.FULL | DirtyFlags.SIZE | DirtyFlags.TILES))
|
||||
draw_tiles = bool(dirty & (DirtyFlags.SIZE | DirtyFlags.TILES | DirtyFlags.PROVIDER))
|
||||
draw_overlays = bool(dirty & (DirtyFlags.SIZE | DirtyFlags.OVERLAYS))
|
||||
visible = self.state.is_visible
|
||||
width = self.state.measured_width or self.state.last_nonzero_width
|
||||
height = self.state.measured_height or self.state.last_nonzero_height
|
||||
@@ -61,6 +67,12 @@ class MapRenderer:
|
||||
zoom = self.state.zoom
|
||||
generation = self.state.generation
|
||||
cache_dir = self.state.cache_dir
|
||||
overlays = tuple(
|
||||
_copy_overlay_for_render(overlay) for overlay in self.state.overlays.values()
|
||||
)
|
||||
layers = {
|
||||
name: (layer.show, layer.z_index) for name, layer in self.state.layers.items()
|
||||
}
|
||||
self.state.dirty = DirtyFlags.NONE
|
||||
|
||||
accepted_tiles = self.state.tile_manager.drain_results(
|
||||
@@ -84,7 +96,7 @@ class MapRenderer:
|
||||
margin=self._prefetch_margin(),
|
||||
)
|
||||
|
||||
if visible and (should_draw or accepted_tiles):
|
||||
if visible and (draw_tiles or accepted_tiles):
|
||||
self._draw_tile_layer(
|
||||
visible_tiles=visible_tiles,
|
||||
width=width,
|
||||
@@ -92,6 +104,16 @@ class MapRenderer:
|
||||
attribution=provider_attribution,
|
||||
tile_size=provider.tile_size,
|
||||
)
|
||||
if visible and draw_overlays:
|
||||
self._draw_overlay_layer(
|
||||
overlays=overlays,
|
||||
layers=layers,
|
||||
center=center,
|
||||
zoom=zoom,
|
||||
width=width,
|
||||
height=height,
|
||||
tile_size=provider.tile_size,
|
||||
)
|
||||
|
||||
def _update_size_from_dpg(self) -> None:
|
||||
width, height = self._measure_child_content()
|
||||
@@ -128,12 +150,14 @@ class MapRenderer:
|
||||
) -> None:
|
||||
width = max(1, int(width))
|
||||
height = max(1, int(height))
|
||||
self._dpg.delete_item(self.state.drawlist_tag, children_only=True)
|
||||
layers = self._ensure_draw_layers()
|
||||
clear_draw_layer(self._dpg, layers.background)
|
||||
clear_draw_layer(self._dpg, layers.tiles)
|
||||
clear_draw_layer(self._dpg, layers.attribution)
|
||||
self._dpg.draw_rectangle(
|
||||
(0, 0),
|
||||
(width, height),
|
||||
parent=self.state.drawlist_tag,
|
||||
tag=self._background_tag,
|
||||
parent=layers.background,
|
||||
color=(54, 68, 78, 255),
|
||||
fill=(29, 38, 45, 255),
|
||||
)
|
||||
@@ -147,19 +171,193 @@ class MapRenderer:
|
||||
tile.texture_tag,
|
||||
(screen_x, screen_y),
|
||||
(screen_x + tile_size, screen_y + tile_size),
|
||||
parent=self.state.drawlist_tag,
|
||||
parent=layers.tiles,
|
||||
)
|
||||
label = attribution or "Map tiles"
|
||||
text_y = max(28, height - 24)
|
||||
self._dpg.draw_text(
|
||||
(12, text_y),
|
||||
label,
|
||||
parent=self.state.drawlist_tag,
|
||||
tag=self._attribution_tag,
|
||||
parent=layers.attribution,
|
||||
color=(172, 184, 192, 255),
|
||||
size=12,
|
||||
)
|
||||
|
||||
def _draw_overlay_layer(
|
||||
self,
|
||||
*,
|
||||
overlays: tuple[Overlay, ...],
|
||||
layers: dict[str, tuple[bool, int]],
|
||||
center: LatLon,
|
||||
zoom: int,
|
||||
width: int,
|
||||
height: int,
|
||||
tile_size: int,
|
||||
) -> None:
|
||||
draw_layers = self._ensure_draw_layers()
|
||||
clear_draw_layer(self._dpg, draw_layers.overlays)
|
||||
if width <= 0 or height <= 0:
|
||||
self.last_overlay_count = 0
|
||||
return
|
||||
|
||||
center_x, center_y = latlon_to_world(center[0], center[1], zoom, tile_size)
|
||||
visible_overlays = [
|
||||
overlay
|
||||
for overlay in overlays
|
||||
if overlay.show and layers.get(overlay.layer, (True, 0))[0]
|
||||
]
|
||||
visible_overlays.sort(key=lambda overlay: layers.get(overlay.layer, (True, 0))[1])
|
||||
drawn = 0
|
||||
for overlay in visible_overlays:
|
||||
if isinstance(overlay, MarkerOverlay):
|
||||
self._draw_marker_overlay(
|
||||
overlay, center_x, center_y, zoom, width, height, tile_size
|
||||
)
|
||||
drawn += 1
|
||||
elif isinstance(overlay, PolylineOverlay):
|
||||
self._draw_polyline_overlay(
|
||||
overlay,
|
||||
center_x,
|
||||
center_y,
|
||||
zoom,
|
||||
width,
|
||||
height,
|
||||
tile_size,
|
||||
draw_layers.overlays,
|
||||
)
|
||||
drawn += 1
|
||||
elif isinstance(overlay, TrajectoryOverlay):
|
||||
self._draw_trajectory_overlay(
|
||||
overlay,
|
||||
center_x,
|
||||
center_y,
|
||||
zoom,
|
||||
width,
|
||||
height,
|
||||
tile_size,
|
||||
draw_layers.overlays,
|
||||
)
|
||||
drawn += 1
|
||||
self.last_overlay_count = drawn
|
||||
|
||||
def _draw_marker_overlay(
|
||||
self,
|
||||
overlay: MarkerOverlay,
|
||||
center_x: float,
|
||||
center_y: float,
|
||||
zoom: int,
|
||||
width: int,
|
||||
height: int,
|
||||
tile_size: int,
|
||||
) -> None:
|
||||
layers = self._ensure_draw_layers()
|
||||
x, y = _latlon_to_screen(
|
||||
overlay.lat,
|
||||
overlay.lon,
|
||||
center_x,
|
||||
center_y,
|
||||
zoom,
|
||||
width,
|
||||
height,
|
||||
tile_size,
|
||||
)
|
||||
radius = max(1.0, float(overlay.radius))
|
||||
self._dpg.draw_circle(
|
||||
(x, y),
|
||||
radius,
|
||||
parent=layers.overlays,
|
||||
color=(255, 255, 255, 230),
|
||||
fill=_rgba(overlay.color),
|
||||
thickness=1.5,
|
||||
segments=20,
|
||||
)
|
||||
if overlay.show_label and overlay.label:
|
||||
self._dpg.draw_text(
|
||||
(x + radius + 4.0, y - 7.0),
|
||||
overlay.label,
|
||||
parent=layers.overlays,
|
||||
color=(245, 248, 250, 255),
|
||||
size=12,
|
||||
)
|
||||
|
||||
def _draw_polyline_overlay(
|
||||
self,
|
||||
overlay: PolylineOverlay,
|
||||
center_x: float,
|
||||
center_y: float,
|
||||
zoom: int,
|
||||
width: int,
|
||||
height: int,
|
||||
tile_size: int,
|
||||
parent: str,
|
||||
) -> None:
|
||||
points = _screen_points(
|
||||
overlay.points,
|
||||
center_x=center_x,
|
||||
center_y=center_y,
|
||||
zoom=zoom,
|
||||
width=width,
|
||||
height=height,
|
||||
tile_size=tile_size,
|
||||
)
|
||||
if len(points) < 2:
|
||||
return
|
||||
self._dpg.draw_polyline(
|
||||
points,
|
||||
parent=parent,
|
||||
closed=overlay.closed,
|
||||
color=_rgba(overlay.color),
|
||||
thickness=max(1.0, float(overlay.thickness)),
|
||||
)
|
||||
|
||||
def _draw_trajectory_overlay(
|
||||
self,
|
||||
overlay: TrajectoryOverlay,
|
||||
center_x: float,
|
||||
center_y: float,
|
||||
zoom: int,
|
||||
width: int,
|
||||
height: int,
|
||||
tile_size: int,
|
||||
parent: str,
|
||||
) -> None:
|
||||
points = _screen_points(
|
||||
overlay.points,
|
||||
center_x=center_x,
|
||||
center_y=center_y,
|
||||
zoom=zoom,
|
||||
width=width,
|
||||
height=height,
|
||||
tile_size=tile_size,
|
||||
)
|
||||
if len(points) >= 2:
|
||||
self._dpg.draw_polyline(
|
||||
points,
|
||||
parent=parent,
|
||||
color=_rgba(overlay.color),
|
||||
thickness=max(1.0, float(overlay.thickness)),
|
||||
)
|
||||
if overlay.show_points and points:
|
||||
stride = max(1, int(overlay.point_stride))
|
||||
for point in points[::stride]:
|
||||
self._dpg.draw_circle(
|
||||
point,
|
||||
2.5,
|
||||
parent=parent,
|
||||
color=_rgba(overlay.color),
|
||||
fill=_rgba(overlay.color),
|
||||
segments=8,
|
||||
)
|
||||
|
||||
def _ensure_draw_layers(self) -> DrawLayerTags:
|
||||
if self._layers is None or not self._dpg.does_item_exist(self._layers.overlays):
|
||||
self._layers = ensure_draw_layers(
|
||||
self._dpg,
|
||||
drawlist_tag=self.state.drawlist_tag,
|
||||
map_tag=self.state.tag,
|
||||
)
|
||||
return self._layers
|
||||
|
||||
def _ensure_texture(self, tile: Tile) -> None:
|
||||
if tile.texture_tag is not None:
|
||||
return
|
||||
@@ -231,3 +429,43 @@ def make_frame_pump(state: MapState, dpg: Any) -> Callable[[], None]:
|
||||
state.renderer = renderer
|
||||
renderer.schedule_next_frame()
|
||||
return renderer.render_frame
|
||||
|
||||
|
||||
def _rgba(color: Color) -> tuple[int, int, int, int]:
|
||||
if len(color) == 3:
|
||||
return (int(color[0]), int(color[1]), int(color[2]), 255)
|
||||
return (int(color[0]), int(color[1]), int(color[2]), int(color[3]))
|
||||
|
||||
|
||||
def _latlon_to_screen(
|
||||
lat: float,
|
||||
lon: float,
|
||||
center_x: float,
|
||||
center_y: float,
|
||||
zoom: int,
|
||||
width: int,
|
||||
height: int,
|
||||
tile_size: int,
|
||||
) -> tuple[float, float]:
|
||||
world_x, world_y = latlon_to_world(lat, lon, zoom, tile_size)
|
||||
return (world_x - center_x + width / 2.0, world_y - center_y + height / 2.0)
|
||||
|
||||
|
||||
def _screen_points(
|
||||
points: tuple[LatLon, ...],
|
||||
*,
|
||||
center_x: float,
|
||||
center_y: float,
|
||||
zoom: int,
|
||||
width: int,
|
||||
height: int,
|
||||
tile_size: int,
|
||||
) -> list[tuple[float, float]]:
|
||||
return [
|
||||
_latlon_to_screen(lat, lon, center_x, center_y, zoom, width, height, tile_size)
|
||||
for lat, lon in points
|
||||
]
|
||||
|
||||
|
||||
def _copy_overlay_for_render(overlay: Overlay) -> Overlay:
|
||||
return replace(overlay)
|
||||
|
||||
Reference in New Issue
Block a user