diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml deleted file mode 100644 index 6048d2b..0000000 --- a/.github/workflows/test.yml +++ /dev/null @@ -1,31 +0,0 @@ -name: test - -on: - push: - branches: - - master - pull_request: - branches: - - master - -jobs: - test: - runs-on: [ubuntu-latest] - strategy: - matrix: - python-version: ['3.9', '3.10', '3.11', '3.12'] - dependencies: - - pygame pyglet - - pygame - - pyglet - - "null" - steps: - - uses: actions/checkout@v4 - - uses: actions/setup-python@v5 - with: - python-version: ${{ matrix.python-version }} - - name: Install Requirements - if: ${{ matrix.dependencies != 'null' }} - run: pip install ${{ matrix.dependencies }} - - name: Run Tests - run: python -m unittest tests/pytmx/test_pytmx.py diff --git a/.gitignore b/.gitignore index 5688433..aa82534 100644 --- a/.gitignore +++ b/.gitignore @@ -7,3 +7,4 @@ thumbs.db build dist venv +.venv/ \ No newline at end of file diff --git a/pytmx/__init__.py b/pytmx/__init__.py index 056a051..63f596b 100644 --- a/pytmx/__init__.py +++ b/pytmx/__init__.py @@ -16,6 +16,7 @@ You should have received a copy of the GNU Lesser General Public License along with pytmx. If not, see . """ + import logging from .pytmx import * diff --git a/pytmx/pytmx.py b/pytmx/pytmx.py index 7d40621..12fa923 100644 --- a/pytmx/pytmx.py +++ b/pytmx/pytmx.py @@ -17,6 +17,7 @@ License along with pytmx. If not, see . """ + from __future__ import annotations import gzip @@ -46,6 +47,7 @@ "TiledElement", "TiledImageLayer", "TiledMap", + "TiledGroupLayer", "TiledObject", "TiledObjectGroup", "TiledTileLayer", @@ -621,6 +623,10 @@ def parse_xml(self, node: ElementTree.Element) -> None: # iterate through tile objects and handle the image for o in [o for o in self.objects if o.gid]: + # Decode rotation and flipping flags from the GID + gid, flags = decode_gid(o.gid) + rotation = self.get_rotation_from_flags(flags) # Get rotation based on flags + # gids might also have properties assigned to them # in that case, assign the gid properties to the object as well p = self.get_tile_properties_by_gid(o.gid) @@ -628,12 +634,32 @@ def parse_xml(self, node: ElementTree.Element) -> None: for key in p: o.properties.setdefault(key, p[key]) + # Adjust based on rotation + if rotation == 90: + o.x, o.y = o.x + o.height, o.y + elif rotation == 180: + o.x, o.y = o.x + o.width, o.y + o.height + elif rotation == 270: + o.x, o.y = o.x, o.y + o.width + + # Adjust Y-coordinate if invert_y is enabled if self.invert_y: o.y -= o.height self.reload_images() return self - + + def get_rotation_from_flags(self, flags: TileFlags) -> int: + """Determine the rotation angle from TileFlags.""" + if flags.flipped_diagonally: + if flags.flipped_horizontally and not flags.flipped_vertically: + return 90 + elif flags.flipped_horizontally and flags.flipped_vertically: + return 180 + elif not flags.flipped_horizontally and flags.flipped_vertically: + return 270 + return 0 + def reload_images(self) -> None: """Load or reload the map images from disk. @@ -1022,6 +1048,13 @@ def get_tile_colliders(self) -> Iterable[tuple[int, list[dict]]]: if colliders: yield gid, colliders + def get_tile_flags_by_gid(self, gid: int) -> TileFlags: + real_gid = self.tiledgidmap[gid] + flags_list = self.gidmap[real_gid] + for tile_gid, flags in flags_list: + if gid == tile_gid: + return flags + def pixels_to_tile_pos(self, position: tuple[int, int]) -> tuple[int, int]: return int(position[0] / self.tilewidth), int(position[1] / self.tileheight) @@ -1112,7 +1145,7 @@ def register_gid( self.gidmap[tiled_gid].append((gid, flags)) self.tiledgidmap[gid] = tiled_gid return gid - + else: return 0 @@ -1498,6 +1531,7 @@ def __init__(self, parent, node, custom_types) -> None: self.id = 0 self.name = None self.type = None + self.object_type = "rectangle" self.x = 0 self.y = 0 self.width = 0 @@ -1545,33 +1579,64 @@ def read_points(text): # correctly handle "tile objects" (object with gid set) if self.gid: + self.object_type = "tile" # set the object type to tile self.gid = self.parent.register_gid_check_flags(self.gid) points = None polygon = node.find("polygon") if polygon is not None: + self.object_type = "polygon" points = read_points(polygon.get("points")) self.closed = True polyline = node.find("polyline") if polyline is not None: + self.object_type = "polyline" points = read_points(polyline.get("points")) self.closed = False + ellipse = node.find("ellipse") + if ellipse is not None: + self.object_type = "ellipse" + + point = node.find("point") + if point is not None: + self.object_type = "point" + + text = node.find("text") + if text is not None: + self.object_type = "text" + # NOTE: The defaults have been taken from the tiled editor version 1.11.0 + self.text = text.text + self.font_family = text.get("fontfamily", "Sans Serif") + # Not sure if this is really font size or not, but it's called + # pixel size in the .tmx file. + self.pixel_size = int(text.get("pixelsize", 16)) + self.wrap = bool(text.get("wrap", False)) + self.bold = bool(text.get("bold", False)) + self.italic = bool(text.get("italic", False)) + self.underline = bool(text.get("underline", False)) + self.strike_out = bool(text.get("strikeout", False)) + self.kerning = bool(text.get("kerning", True)) + self.h_align = text.get("halign", "left") + self.v_align = text.get("valign", "top") + self.color = text.get("color", "#000000FF") + if points: - x1 = x2 = y1 = y2 = 0 - for x, y in points: - if x < x1: - x1 = x - if x > x2: - x2 = x - if y < y1: - y1 = y - if y > y2: - y2 = y - self.width = abs(x1) + abs(x2) - self.height = abs(y1) + abs(y2) + xs, ys = zip(*points) + self.width = max(xs) - min(xs) + self.height = max(ys) - min(ys) self.points = tuple([Point(i[0] + self.x, i[1] + self.y) for i in points]) + # Set the points for a rectangle + elif self.object_type == "rectangle": + self.points = tuple( + [ + Point(self.x, self.y), + Point(self.x + self.width, self.y), + Point(self.x + self.width, self.y + self.height), + Point(self.x, self.y + self.height), + ] + ) return self