diff --git a/apps/pyglet_demo.py b/apps/pyglet_demo.py index d47123f..1f85e96 100644 --- a/apps/pyglet_demo.py +++ b/apps/pyglet_demo.py @@ -9,6 +9,7 @@ Notice: slow! no transparency! """ + import logging logger = logging.getLogger(__name__) @@ -17,11 +18,15 @@ logger.addHandler(ch) logger.setLevel(logging.INFO) -import pyglet +from collections.abc import Iterator +from pathlib import Path +from typing import Optional +import pyglet from pyglet.sprite import Sprite from pytmx import * +from pytmx.pytmx import ColorLike, PointLike from pytmx.util_pyglet import load_pyglet @@ -32,7 +37,7 @@ class TiledRenderer: no shape drawing yet """ - def __init__(self, filename) -> None: + def __init__(self, filename: str) -> None: tm = load_pyglet(filename) self.size = tm.width * tm.tilewidth, tm.height * tm.tileheight self.tmx_data = tm @@ -40,13 +45,60 @@ def __init__(self, filename) -> None: self.sprites = [] # container for tiles self.generate_sprites() - def draw_rect(self, color, rect, width) -> None: - # TODO: use pyglet.shapes - pass - - def draw_lines(self, color, closed, points, width) -> None: - # TODO: use pyglet.shapes - pass + def draw_rect( + self, color: ColorLike, rect: tuple[int, int, int, int], width: int + ) -> None: + x, y, w, h = rect + y = self.size[1] - y - h # Adjust Y if needed for pixel origin + rect_shape = pyglet.shapes.Rectangle(x, y, w, h, color=color, batch=self.batch) + rect_shape.opacity = 128 # optional: make it semi-transparent + if width > 0: + border_color = ( + max(0, color[0] - 50), + max(0, color[1] - 50), + max(0, color[2] - 50), + ) + pyglet.shapes.Line( + x, y, x + w, y, thickness=width, color=border_color, batch=self.batch + ) # bottom + pyglet.shapes.Line( + x, + y + h, + x + w, + y + h, + thickness=width, + color=border_color, + batch=self.batch, + ) # top + pyglet.shapes.Line( + x, y, x, y + h, thickness=width, color=border_color, batch=self.batch + ) # left + pyglet.shapes.Line( + x + w, + y, + x + w, + y + h, + thickness=width, + color=border_color, + batch=self.batch, + ) # right + + def draw_lines( + self, color: ColorLike, closed: bool, points: list[PointLike], width: int + ) -> None: + # Flip Y-axis if necessary + flipped_points = [(x, self.size[1] - y) for x, y in points] + + if closed: + polygon = pyglet.shapes.Polygon( + *flipped_points, color=color, batch=self.batch + ) + polygon.opacity = 128 + else: + for (x1, y1), (x2, y2) in zip(flipped_points, flipped_points[1:]): + line = pyglet.shapes.Line( + x1, y1, x2, y2, thickness=width, color=color, batch=self.batch + ) def generate_sprites(self) -> None: tw = self.tmx_data.tilewidth @@ -100,19 +152,19 @@ def generate_sprites(self) -> None: sprite = Sprite(layer.image, x, y, batch=self.batch) self.sprites.append(sprite) - def draw(self): + def draw(self) -> None: self.batch.draw() class SimpleTest: - def __init__(self, filename) -> None: - self.renderer = None - self.running = False - self.dirty = False - self.exit_status = 0 + def __init__(self, filename: str) -> None: + self.renderer: TiledRenderer + self.running: bool = False + self.dirty: bool = False + self.exit_status: int = 0 self.load_map(filename) - def load_map(self, filename) -> None: + def load_map(self, filename: str) -> None: self.renderer = TiledRenderer(filename) logger.info("Objects in map:") @@ -129,37 +181,37 @@ def draw(self) -> None: self.renderer.draw() -def all_filenames(): - import glob - import os.path - - _list = glob.glob(os.path.join("data", "*.tmx")) - try: - while _list: - yield _list.pop(0) - except IndexError: - pyglet.app.exit() +def all_filenames() -> Iterator[str]: + data_dir = Path("apps/data") + for path in sorted(data_dir.glob("*.tmx")): + yield str(path) + pyglet.app.exit() class TestWindow(pyglet.window.Window): - def __init__(self, width, height, vsync): + def __init__(self, width: int, height: int, vsync: bool) -> None: super().__init__(width=width, height=height, vsync=vsync) - self.fps_display = pyglet.window.FPSDisplay(self, color=(50, 255, 50, 255)) - self.filenames = all_filenames() + self.fps_display: pyglet.window.FPSDisplay = pyglet.window.FPSDisplay( + self, color=(50, 255, 50, 255) + ) + self.filenames: Iterator[str] = all_filenames() + self.contents: Optional[SimpleTest] = None self.next_map() def on_draw(self) -> None: self.clear() - self.contents.draw() + if self.contents: + self.contents.draw() self.fps_display.draw() def next_map(self) -> None: try: - self.contents = SimpleTest(next(self.filenames)) + filename = next(self.filenames) + self.contents = SimpleTest(filename) except StopIteration: pyglet.app.exit() - def on_key_press(self, symbol, mod): + def on_key_press(self, symbol: int, mod: int) -> None: if symbol == pyglet.window.key.ESCAPE: pyglet.app.exit() else: @@ -168,5 +220,5 @@ def on_key_press(self, symbol, mod): if __name__ == "__main__": window = TestWindow(600, 600, vsync=False) - pyglet.clock.schedule_interval(window.draw, 1/120) + pyglet.clock.schedule_interval(window.draw, 1 / 120) pyglet.app.run(None) diff --git a/pytmx/util_pyglet.py b/pytmx/util_pyglet.py index 6e2334a..1a64416 100644 --- a/pytmx/util_pyglet.py +++ b/pytmx/util_pyglet.py @@ -20,6 +20,8 @@ import logging logger = logging.getLogger(__name__) +from pathlib import Path +from typing import Any, Optional try: import pyglet @@ -28,9 +30,12 @@ raise import pytmx +from pytmx.pytmx import ColorLike, TileFlags -def pyglet_image_loader(filename, colorkey, **kwargs): +def pyglet_image_loader( + filename: str, colorkey: Optional[ColorLike] = None, **kwargs: Any +): """basic image loading with pyglet returns pyglet Images, not textures @@ -46,29 +51,60 @@ def pyglet_image_loader(filename, colorkey, **kwargs): if colorkey: logger.debug("colorkey not implemented") - image = pyglet.resource.image(filename) + image_path = Path(filename) + pyglet.resource.path = [str(image_path.parent.resolve())] + pyglet.resource.reindex() - def load_image(rect=None, flags=None): - if rect: - try: + image = pyglet.resource.image(image_path.name) + + def load_image( + rect: Optional[tuple[int, int, int, int]] = None, + flags: Optional[TileFlags] = None, + ): + try: + if rect: x, y, w, h = rect y = image.height - y - h tile = image.get_region(x, y, w, h) - except: - logger.error("cannot get region %s of image", rect) - raise - else: - tile = image + else: + tile = image + + angle, flip_x, flip_y = handle_flags(flags) + tile = tile.get_transform(flip_x=flip_x, flip_y=flip_y, rotate=int(angle)) - if flags: - logger.error("tile flags are not implemented") + return tile - return tile + except Exception as e: + logger.error( + "Error extracting or transforming tile %s: %s", rect, e, exc_info=True + ) + raise return load_image -def load_pyglet(filename, *args, **kwargs): +def handle_flags(flags: Optional[TileFlags]) -> tuple[float, bool, bool]: + """ + Convert Tiled tile flip flags into SDL2 rendering parameters. + """ + if not flags: + return 0.0, False, False + + flipped_h = flags.flipped_horizontally + flipped_v = flags.flipped_vertically + flipped_d = flags.flipped_diagonally + + if flipped_d: + # Diagonal flip overrides horizontal/vertical flips for rotation + if flipped_v: + return 270.0, False, False + else: + return 90.0, False, False + + return 0.0, flipped_h, flipped_v + + +def load_pyglet(filename: str, *args: Any, **kwargs: Any) -> pytmx.TiledMap: kwargs["image_loader"] = pyglet_image_loader kwargs["invert_y"] = True return pytmx.TiledMap(filename, *args, **kwargs)