Fixed tiling bug and multi map bug
This commit is contained in:
@@ -4,13 +4,17 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from dataclasses import dataclass
|
from dataclasses import dataclass
|
||||||
from math import isfinite
|
from math import isfinite
|
||||||
|
from threading import RLock
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .commands import CommandKind, MapCommand
|
from .commands import CommandKind, MapCommand
|
||||||
from .projection import latlon_to_world, screen_to_world, world_to_latlon
|
from .projection import latlon_to_world, screen_to_world, world_to_latlon
|
||||||
from .sizing import effective_draw_size
|
from .sizing import effective_draw_size
|
||||||
from .state import DirtyFlags, MapState, mark_dirty
|
from .state import DirtyFlags, MapState, mark_dirty
|
||||||
from .types import LatLon, Point
|
from .types import LatLon, Point, Tag
|
||||||
|
|
||||||
|
_drag_owner_lock = RLock()
|
||||||
|
_drag_owner_map: Tag | None = None
|
||||||
|
|
||||||
|
|
||||||
@dataclass(frozen=True, slots=True)
|
@dataclass(frozen=True, slots=True)
|
||||||
@@ -152,6 +156,12 @@ def handle_mouse_down(state: MapState, mouse_pos: tuple[float, float], hit_rect:
|
|||||||
state.interaction.active_drag = False
|
state.interaction.active_drag = False
|
||||||
state.interaction.last_mouse_position = None
|
state.interaction.last_mouse_position = None
|
||||||
return
|
return
|
||||||
|
with _drag_owner_lock:
|
||||||
|
global _drag_owner_map
|
||||||
|
if _drag_owner_map is not None and _drag_owner_map != state.tag:
|
||||||
|
return
|
||||||
|
_drag_owner_map = state.tag
|
||||||
|
with state.lock:
|
||||||
state.interaction.active_drag = True
|
state.interaction.active_drag = True
|
||||||
state.interaction.last_mouse_position = mouse_pos
|
state.interaction.last_mouse_position = mouse_pos
|
||||||
|
|
||||||
@@ -159,6 +169,9 @@ def handle_mouse_down(state: MapState, mouse_pos: tuple[float, float], hit_rect:
|
|||||||
def handle_mouse_drag(state: MapState, mouse_pos: tuple[float, float]) -> None:
|
def handle_mouse_drag(state: MapState, mouse_pos: tuple[float, float]) -> None:
|
||||||
"""Update center from a mouse drag event."""
|
"""Update center from a mouse drag event."""
|
||||||
|
|
||||||
|
with _drag_owner_lock:
|
||||||
|
if _drag_owner_map != state.tag:
|
||||||
|
return
|
||||||
with state.lock:
|
with state.lock:
|
||||||
if not state.interaction.active_drag:
|
if not state.interaction.active_drag:
|
||||||
return
|
return
|
||||||
@@ -172,6 +185,10 @@ def handle_mouse_drag(state: MapState, mouse_pos: tuple[float, float]) -> None:
|
|||||||
def handle_mouse_release(state: MapState) -> None:
|
def handle_mouse_release(state: MapState) -> None:
|
||||||
"""End any active drag."""
|
"""End any active drag."""
|
||||||
|
|
||||||
|
with _drag_owner_lock:
|
||||||
|
global _drag_owner_map
|
||||||
|
if _drag_owner_map == state.tag:
|
||||||
|
_drag_owner_map = None
|
||||||
with state.lock:
|
with state.lock:
|
||||||
state.interaction.active_drag = False
|
state.interaction.active_drag = False
|
||||||
state.interaction.last_mouse_position = None
|
state.interaction.last_mouse_position = None
|
||||||
@@ -186,6 +203,9 @@ def handle_mouse_wheel(
|
|||||||
) -> None:
|
) -> None:
|
||||||
"""Apply wheel zoom when the cursor is over the concrete map rectangle."""
|
"""Apply wheel zoom when the cursor is over the concrete map rectangle."""
|
||||||
|
|
||||||
|
with state.lock:
|
||||||
|
if not state.is_visible:
|
||||||
|
return
|
||||||
if not hit_rect.contains(mouse_pos[0], mouse_pos[1]):
|
if not hit_rect.contains(mouse_pos[0], mouse_pos[1]):
|
||||||
return
|
return
|
||||||
zoom_state_at_screen_point(
|
zoom_state_at_screen_point(
|
||||||
@@ -202,6 +222,7 @@ def update_drag_from_button_state(
|
|||||||
mouse_pos: tuple[float, float],
|
mouse_pos: tuple[float, float],
|
||||||
hit_rect: HitRect,
|
hit_rect: HitRect,
|
||||||
is_down: bool,
|
is_down: bool,
|
||||||
|
can_start: bool = True,
|
||||||
) -> None:
|
) -> None:
|
||||||
"""Poll left-button state and keep drag interaction moving."""
|
"""Poll left-button state and keep drag interaction moving."""
|
||||||
|
|
||||||
@@ -217,6 +238,9 @@ def update_drag_from_button_state(
|
|||||||
handle_mouse_drag(state, mouse_pos)
|
handle_mouse_drag(state, mouse_pos)
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if not can_start:
|
||||||
|
return
|
||||||
|
|
||||||
if hit_rect.contains(mouse_pos[0], mouse_pos[1]):
|
if hit_rect.contains(mouse_pos[0], mouse_pos[1]):
|
||||||
handle_mouse_down(state, mouse_pos, hit_rect)
|
handle_mouse_down(state, mouse_pos, hit_rect)
|
||||||
|
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ from __future__ import annotations
|
|||||||
|
|
||||||
from collections.abc import Callable
|
from collections.abc import Callable
|
||||||
from dataclasses import replace
|
from dataclasses import replace
|
||||||
|
from threading import RLock
|
||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
from .commands import CommandKind, MapCommand
|
from .commands import CommandKind, MapCommand
|
||||||
@@ -14,7 +15,12 @@ from .projection import latlon_to_world
|
|||||||
from .sizing import SizeMeasurement, apply_size_measurement
|
from .sizing import SizeMeasurement, apply_size_measurement
|
||||||
from .state import DirtyFlags, MapState
|
from .state import DirtyFlags, MapState
|
||||||
from .tiles import Tile, VisibleTile
|
from .tiles import Tile, VisibleTile
|
||||||
from .types import Color, LatLon
|
from .types import Color, LatLon, Tag
|
||||||
|
|
||||||
|
_scheduler_lock = RLock()
|
||||||
|
_scheduled_renderers: dict[Tag, MapRenderer] = {}
|
||||||
|
_scheduler_dpg: Any | None = None
|
||||||
|
_scheduler_frame_scheduled = False
|
||||||
|
|
||||||
|
|
||||||
class MapRenderer:
|
class MapRenderer:
|
||||||
@@ -29,23 +35,15 @@ class MapRenderer:
|
|||||||
self.last_overlay_count: int = 0
|
self.last_overlay_count: int = 0
|
||||||
|
|
||||||
def schedule_next_frame(self) -> None:
|
def schedule_next_frame(self) -> None:
|
||||||
"""Schedule this renderer to run on the next Dear PyGui frame."""
|
"""Register this renderer with the shared Dear PyGui frame pump."""
|
||||||
|
|
||||||
with self.state.lock:
|
_schedule_renderer(self)
|
||||||
if self.state.frame_scheduled:
|
|
||||||
return
|
|
||||||
self.state.frame_scheduled = True
|
|
||||||
frame = self._dpg.get_frame_count() + 1
|
|
||||||
self._dpg.set_frame_callback(frame, self._frame_callback)
|
|
||||||
|
|
||||||
def _frame_callback(self, sender: Any | None = None, app_data: Any | None = None) -> None:
|
def _frame_callback(self, sender: Any | None = None, app_data: Any | None = None) -> None:
|
||||||
_ = (sender, app_data)
|
_ = (sender, app_data)
|
||||||
with self.state.lock:
|
|
||||||
self.state.frame_scheduled = False
|
|
||||||
if not self._dpg.does_item_exist(self.state.drawlist_tag):
|
if not self._dpg.does_item_exist(self.state.drawlist_tag):
|
||||||
return
|
return
|
||||||
self.render_frame()
|
self.render_frame()
|
||||||
self.schedule_next_frame()
|
|
||||||
|
|
||||||
def render_frame(self) -> None:
|
def render_frame(self) -> None:
|
||||||
"""Drain pending commands, refresh size, process tiles, and redraw."""
|
"""Drain pending commands, refresh size, process tiles, and redraw."""
|
||||||
@@ -146,6 +144,7 @@ class MapRenderer:
|
|||||||
mouse_pos=(float(mouse_pos[0]), float(mouse_pos[1])),
|
mouse_pos=(float(mouse_pos[0]), float(mouse_pos[1])),
|
||||||
hit_rect=self.last_hit_rect,
|
hit_rect=self.last_hit_rect,
|
||||||
is_down=is_down,
|
is_down=is_down,
|
||||||
|
can_start=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
def _measure_child_content(self) -> tuple[int, int]:
|
def _measure_child_content(self) -> tuple[int, int]:
|
||||||
@@ -181,8 +180,8 @@ class MapRenderer:
|
|||||||
tile = self.state.tile_manager.get_ready_tile(visible_tile.tile_id)
|
tile = self.state.tile_manager.get_ready_tile(visible_tile.tile_id)
|
||||||
if tile is None or tile.texture_tag is None:
|
if tile is None or tile.texture_tag is None:
|
||||||
continue
|
continue
|
||||||
screen_x = visible_tile.screen_x
|
screen_x = _snap_tile_position(visible_tile.screen_x)
|
||||||
screen_y = visible_tile.screen_y
|
screen_y = _snap_tile_position(visible_tile.screen_y)
|
||||||
self._dpg.draw_image(
|
self._dpg.draw_image(
|
||||||
tile.texture_tag,
|
tile.texture_tag,
|
||||||
(screen_x, screen_y),
|
(screen_x, screen_y),
|
||||||
@@ -402,6 +401,54 @@ class MapRenderer:
|
|||||||
return get_config().prefetch_margin_tiles
|
return get_config().prefetch_margin_tiles
|
||||||
|
|
||||||
|
|
||||||
|
def _schedule_renderer(renderer: MapRenderer) -> None:
|
||||||
|
"""Schedule the shared frame pump for all registered map renderers."""
|
||||||
|
|
||||||
|
global _scheduler_dpg, _scheduler_frame_scheduled
|
||||||
|
|
||||||
|
with _scheduler_lock:
|
||||||
|
_scheduled_renderers[renderer.state.tag] = renderer
|
||||||
|
_scheduler_dpg = renderer._dpg
|
||||||
|
if _scheduler_frame_scheduled:
|
||||||
|
return
|
||||||
|
_scheduler_frame_scheduled = True
|
||||||
|
dpg = renderer._dpg
|
||||||
|
|
||||||
|
frame = dpg.get_frame_count() + 1
|
||||||
|
dpg.set_frame_callback(frame, _shared_frame_callback)
|
||||||
|
|
||||||
|
|
||||||
|
def _shared_frame_callback(sender: Any | None = None, app_data: Any | None = None) -> None:
|
||||||
|
"""Render every live map from one Dear PyGui frame callback."""
|
||||||
|
|
||||||
|
_ = (sender, app_data)
|
||||||
|
global _scheduler_frame_scheduled
|
||||||
|
|
||||||
|
with _scheduler_lock:
|
||||||
|
renderers = tuple(_scheduled_renderers.values())
|
||||||
|
_scheduler_frame_scheduled = False
|
||||||
|
|
||||||
|
for renderer in renderers:
|
||||||
|
renderer._frame_callback()
|
||||||
|
|
||||||
|
with _scheduler_lock:
|
||||||
|
active_renderers = [
|
||||||
|
renderer
|
||||||
|
for renderer in _scheduled_renderers.values()
|
||||||
|
if renderer._dpg.does_item_exist(renderer.state.drawlist_tag)
|
||||||
|
]
|
||||||
|
_scheduled_renderers.clear()
|
||||||
|
_scheduled_renderers.update((renderer.state.tag, renderer) for renderer in active_renderers)
|
||||||
|
if not active_renderers or _scheduler_frame_scheduled:
|
||||||
|
return
|
||||||
|
_scheduler_frame_scheduled = True
|
||||||
|
dpg = _scheduler_dpg
|
||||||
|
|
||||||
|
if dpg is not None:
|
||||||
|
frame = dpg.get_frame_count() + 1
|
||||||
|
dpg.set_frame_callback(frame, _shared_frame_callback)
|
||||||
|
|
||||||
|
|
||||||
def drain_renderer_commands(state: MapState) -> list[MapCommand]:
|
def drain_renderer_commands(state: MapState) -> list[MapCommand]:
|
||||||
"""Drain and apply GUI-thread command side effects."""
|
"""Drain and apply GUI-thread command side effects."""
|
||||||
|
|
||||||
@@ -456,6 +503,10 @@ def _rgba(color: Color) -> tuple[int, int, int, int]:
|
|||||||
return (int(color[0]), int(color[1]), int(color[2]), int(color[3]))
|
return (int(color[0]), int(color[1]), int(color[2]), int(color[3]))
|
||||||
|
|
||||||
|
|
||||||
|
def _snap_tile_position(value: float) -> int:
|
||||||
|
return int(round(value))
|
||||||
|
|
||||||
|
|
||||||
def _latlon_to_screen(
|
def _latlon_to_screen(
|
||||||
lat: float,
|
lat: float,
|
||||||
lon: float,
|
lon: float,
|
||||||
|
|||||||
@@ -81,8 +81,16 @@ def map_widget(
|
|||||||
draw_pos = tuple(float(value) for value in dpg.get_item_rect_min(state.drawlist_tag))
|
draw_pos = tuple(float(value) for value in dpg.get_item_rect_min(state.drawlist_tag))
|
||||||
return calculate_hit_rect(state, (draw_pos[0], draw_pos[1]))
|
return calculate_hit_rect(state, (draw_pos[0], draw_pos[1]))
|
||||||
|
|
||||||
|
def _is_drawlist_hovered() -> bool:
|
||||||
|
try:
|
||||||
|
return bool(dpg.is_item_hovered(state.drawlist_tag))
|
||||||
|
except Exception:
|
||||||
|
return False
|
||||||
|
|
||||||
def _on_mouse_down(sender: Any, app_data: Any, user_data: Any) -> None:
|
def _on_mouse_down(sender: Any, app_data: Any, user_data: Any) -> None:
|
||||||
_ = (sender, app_data, user_data)
|
_ = (sender, app_data, user_data)
|
||||||
|
if not _is_drawlist_hovered():
|
||||||
|
return
|
||||||
handle_mouse_down(state, _mouse_pos(), _hit_rect())
|
handle_mouse_down(state, _mouse_pos(), _hit_rect())
|
||||||
|
|
||||||
def _on_mouse_drag(sender: Any, app_data: Any, user_data: Any) -> None:
|
def _on_mouse_drag(sender: Any, app_data: Any, user_data: Any) -> None:
|
||||||
@@ -95,6 +103,8 @@ def map_widget(
|
|||||||
|
|
||||||
def _on_mouse_wheel(sender: Any, app_data: Any, user_data: Any) -> None:
|
def _on_mouse_wheel(sender: Any, app_data: Any, user_data: Any) -> None:
|
||||||
_ = (sender, user_data)
|
_ = (sender, user_data)
|
||||||
|
if not _is_drawlist_hovered():
|
||||||
|
return
|
||||||
handle_mouse_wheel(
|
handle_mouse_wheel(
|
||||||
state,
|
state,
|
||||||
mouse_pos=_mouse_pos(),
|
mouse_pos=_mouse_pos(),
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ from __future__ import annotations
|
|||||||
import pytest
|
import pytest
|
||||||
|
|
||||||
import dpg_map as dpgm
|
import dpg_map as dpgm
|
||||||
|
import dpg_map.interaction as interaction_module
|
||||||
from dpg_map.commands import CommandKind
|
from dpg_map.commands import CommandKind
|
||||||
from dpg_map.interaction import (
|
from dpg_map.interaction import (
|
||||||
calculate_hit_rect,
|
calculate_hit_rect,
|
||||||
@@ -74,6 +75,43 @@ def test_polled_drag_starts_and_moves_while_button_is_down() -> None:
|
|||||||
assert state.center[1] < 0.0
|
assert state.center[1] < 0.0
|
||||||
|
|
||||||
|
|
||||||
|
def test_renderer_poll_does_not_start_drag_for_inactive_map() -> None:
|
||||||
|
state = create_map_state(tag="poll-no-start", center=(0.0, 0.0), zoom=3)
|
||||||
|
apply_size_measurement(state, SizeMeasurement(width=400, height=300, visible=True))
|
||||||
|
rect = calculate_hit_rect(state, (10.0, 20.0))
|
||||||
|
|
||||||
|
update_drag_from_button_state(
|
||||||
|
state,
|
||||||
|
mouse_pos=(20.0, 30.0),
|
||||||
|
hit_rect=rect,
|
||||||
|
is_down=True,
|
||||||
|
can_start=False,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert state.interaction.active_drag is False
|
||||||
|
assert state.center == (0.0, 0.0)
|
||||||
|
|
||||||
|
|
||||||
|
def test_drag_owner_prevents_second_map_from_tracking_first_map() -> None:
|
||||||
|
with interaction_module._drag_owner_lock:
|
||||||
|
interaction_module._drag_owner_map = None
|
||||||
|
state_a = create_map_state(tag="drag-owner-a", center=(0.0, 0.0), zoom=3)
|
||||||
|
state_b = create_map_state(tag="drag-owner-b", center=(0.0, 0.0), zoom=3)
|
||||||
|
apply_size_measurement(state_a, SizeMeasurement(width=400, height=300, visible=True))
|
||||||
|
apply_size_measurement(state_b, SizeMeasurement(width=400, height=300, visible=True))
|
||||||
|
rect_a = calculate_hit_rect(state_a, (10.0, 20.0))
|
||||||
|
rect_b = calculate_hit_rect(state_b, (10.0, 20.0))
|
||||||
|
|
||||||
|
handle_mouse_down(state_a, (20.0, 30.0), rect_a)
|
||||||
|
handle_mouse_down(state_b, (20.0, 30.0), rect_b)
|
||||||
|
handle_mouse_drag(state_a, (45.0, 30.0))
|
||||||
|
handle_mouse_drag(state_b, (45.0, 30.0))
|
||||||
|
|
||||||
|
assert state_a.center[1] < 0.0
|
||||||
|
assert state_b.center == (0.0, 0.0)
|
||||||
|
handle_mouse_release(state_a)
|
||||||
|
|
||||||
|
|
||||||
def test_wheel_zoom_keeps_cursor_latlon_stable() -> None:
|
def test_wheel_zoom_keeps_cursor_latlon_stable() -> None:
|
||||||
state = create_map_state(tag="wheel", center=(47.9029, 1.9093), zoom=8)
|
state = create_map_state(tag="wheel", center=(47.9029, 1.9093), zoom=8)
|
||||||
apply_size_measurement(state, SizeMeasurement(width=800, height=600, visible=True))
|
apply_size_measurement(state, SizeMeasurement(width=800, height=600, visible=True))
|
||||||
|
|||||||
@@ -3,11 +3,12 @@ from __future__ import annotations
|
|||||||
from typing import Any
|
from typing import Any
|
||||||
|
|
||||||
import dpg_map as dpgm
|
import dpg_map as dpgm
|
||||||
|
import dpg_map.renderer as renderer_module
|
||||||
from dpg_map.commands import CommandKind, MapCommand
|
from dpg_map.commands import CommandKind, MapCommand
|
||||||
from dpg_map.providers import TileProvider
|
from dpg_map.providers import TileProvider
|
||||||
from dpg_map.renderer import MapRenderer, drain_renderer_commands
|
from dpg_map.renderer import MapRenderer, drain_renderer_commands
|
||||||
from dpg_map.state import DirtyFlags, create_map_state
|
from dpg_map.state import DirtyFlags, create_map_state
|
||||||
from dpg_map.tiles import TileID, TileResult, TileStatus
|
from dpg_map.tiles import TileID, TileResult, TileStatus, VisibleTile
|
||||||
|
|
||||||
|
|
||||||
class FakeDpg:
|
class FakeDpg:
|
||||||
@@ -15,6 +16,7 @@ class FakeDpg:
|
|||||||
self.items: set[str | int] = set()
|
self.items: set[str | int] = set()
|
||||||
self.deleted: list[tuple[str | int, bool]] = []
|
self.deleted: list[tuple[str | int, bool]] = []
|
||||||
self.drawn: list[tuple[str, str | int]] = []
|
self.drawn: list[tuple[str, str | int]] = []
|
||||||
|
self.images: list[tuple[Any, Any, str | int]] = []
|
||||||
|
|
||||||
def does_item_exist(self, tag: str | int) -> bool:
|
def does_item_exist(self, tag: str | int) -> bool:
|
||||||
return tag in self.items
|
return tag in self.items
|
||||||
@@ -33,6 +35,7 @@ class FakeDpg:
|
|||||||
def draw_image(self, *args: Any, parent: str | int, **kwargs: Any) -> None:
|
def draw_image(self, *args: Any, parent: str | int, **kwargs: Any) -> None:
|
||||||
_ = (args, kwargs)
|
_ = (args, kwargs)
|
||||||
self.drawn.append(("image", parent))
|
self.drawn.append(("image", parent))
|
||||||
|
self.images.append((args[1], args[2], parent))
|
||||||
|
|
||||||
def draw_text(self, *args: Any, parent: str | int, **kwargs: Any) -> None:
|
def draw_text(self, *args: Any, parent: str | int, **kwargs: Any) -> None:
|
||||||
_ = (args, kwargs)
|
_ = (args, kwargs)
|
||||||
@@ -47,6 +50,51 @@ class FakeDpg:
|
|||||||
self.drawn.append(("polyline", parent))
|
self.drawn.append(("polyline", parent))
|
||||||
|
|
||||||
|
|
||||||
|
class FrameCallbackDpg(FakeDpg):
|
||||||
|
def __init__(self) -> None:
|
||||||
|
super().__init__()
|
||||||
|
self.frame_count = 0
|
||||||
|
self.callbacks: dict[int, Any] = {}
|
||||||
|
|
||||||
|
def get_frame_count(self) -> int:
|
||||||
|
return self.frame_count
|
||||||
|
|
||||||
|
def set_frame_callback(self, frame: int, callback: Any) -> None:
|
||||||
|
self.callbacks[frame] = callback
|
||||||
|
|
||||||
|
|
||||||
|
def test_shared_frame_pump_renders_multiple_maps_from_one_frame_callback() -> None:
|
||||||
|
with renderer_module._scheduler_lock:
|
||||||
|
renderer_module._scheduled_renderers.clear()
|
||||||
|
renderer_module._scheduler_frame_scheduled = False
|
||||||
|
renderer_module._scheduler_dpg = None
|
||||||
|
|
||||||
|
state_a = create_map_state(tag="frame-pump-a")
|
||||||
|
state_b = create_map_state(tag="frame-pump-b")
|
||||||
|
fake = FrameCallbackDpg()
|
||||||
|
fake.items.update({state_a.drawlist_tag, state_b.drawlist_tag})
|
||||||
|
renderer_a = MapRenderer(state_a, fake)
|
||||||
|
renderer_b = MapRenderer(state_b, fake)
|
||||||
|
rendered: list[str] = []
|
||||||
|
renderer_a.render_frame = lambda: rendered.append("a") # type: ignore[method-assign]
|
||||||
|
renderer_b.render_frame = lambda: rendered.append("b") # type: ignore[method-assign]
|
||||||
|
|
||||||
|
renderer_a.schedule_next_frame()
|
||||||
|
renderer_b.schedule_next_frame()
|
||||||
|
assert list(fake.callbacks) == [1]
|
||||||
|
|
||||||
|
fake.frame_count = 1
|
||||||
|
fake.callbacks[1]()
|
||||||
|
|
||||||
|
assert rendered == ["a", "b"]
|
||||||
|
assert sorted(renderer_module._scheduled_renderers) == ["frame-pump-a", "frame-pump-b"]
|
||||||
|
|
||||||
|
with renderer_module._scheduler_lock:
|
||||||
|
renderer_module._scheduled_renderers.clear()
|
||||||
|
renderer_module._scheduler_frame_scheduled = False
|
||||||
|
renderer_module._scheduler_dpg = None
|
||||||
|
|
||||||
|
|
||||||
def test_renderer_command_drain_preserves_structural_order_and_coalesces() -> None:
|
def test_renderer_command_drain_preserves_structural_order_and_coalesces() -> None:
|
||||||
state = create_map_state(tag="renderer-drain")
|
state = create_map_state(tag="renderer-drain")
|
||||||
state.dirty = DirtyFlags.NONE
|
state.dirty = DirtyFlags.NONE
|
||||||
@@ -109,6 +157,37 @@ def test_overlay_draw_clears_only_overlay_layer() -> None:
|
|||||||
assert ("text", "overlay-draw##layer-overlays") in fake.drawn
|
assert ("text", "overlay-draw##layer-overlays") in fake.drawn
|
||||||
|
|
||||||
|
|
||||||
|
def test_tile_draw_snaps_fractional_positions_to_integer_pixels() -> None:
|
||||||
|
state = create_map_state(tag="tile-snap", center=(47.0, 2.0), zoom=8)
|
||||||
|
tile_id = TileID("osm", 8, 129, 89)
|
||||||
|
state.tile_manager._result_queue.put(
|
||||||
|
TileResult(
|
||||||
|
tile_id,
|
||||||
|
generation=state.generation,
|
||||||
|
status=TileStatus.READY,
|
||||||
|
width=1,
|
||||||
|
height=1,
|
||||||
|
pixels=(1.0, 1.0, 1.0, 1.0),
|
||||||
|
source="disk",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
state.tile_manager.drain_results(generation=state.generation, provider_name="osm")
|
||||||
|
state.tile_manager.set_texture_tag(tile_id, "tile-texture")
|
||||||
|
fake = FakeDpg()
|
||||||
|
fake.items.add(state.drawlist_tag)
|
||||||
|
renderer = MapRenderer(state, fake)
|
||||||
|
|
||||||
|
renderer._draw_tile_layer(
|
||||||
|
visible_tiles=[VisibleTile(tile_id, 10.4, 20.6)],
|
||||||
|
width=400,
|
||||||
|
height=300,
|
||||||
|
attribution="Tiles",
|
||||||
|
tile_size=256,
|
||||||
|
)
|
||||||
|
|
||||||
|
assert fake.images == [((10, 21), (266, 277), "tile-snap##layer-tiles")]
|
||||||
|
|
||||||
|
|
||||||
def test_overlay_update_drain_sets_only_overlay_dirty() -> None:
|
def test_overlay_update_drain_sets_only_overlay_dirty() -> None:
|
||||||
state = create_map_state(tag="overlay-dirty")
|
state = create_map_state(tag="overlay-dirty")
|
||||||
state.dirty = DirtyFlags.NONE
|
state.dirty = DirtyFlags.NONE
|
||||||
|
|||||||
Reference in New Issue
Block a user