From 91c9b6fa5486b4a2bfd5dd404350b78388a26af0 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 04:13:18 +0100 Subject: [PATCH 01/17] Split texture and image data into separate modules --- arcade/texture/image_data.py | 82 ++++++++++++++++++++++++++++++++++++ arcade/texture/texture.py | 81 +---------------------------------- 2 files changed, 83 insertions(+), 80 deletions(-) create mode 100644 arcade/texture/image_data.py diff --git a/arcade/texture/image_data.py b/arcade/texture/image_data.py new file mode 100644 index 000000000..e26f5f90e --- /dev/null +++ b/arcade/texture/image_data.py @@ -0,0 +1,82 @@ +import hashlib +from typing import Optional, Tuple +import PIL.Image + + +class ImageData: + """ + A class holding the image for a texture with other metadata such as the hash. + This information is used internally by the texture atlas to identify unique textures. + + If a hash is not provided, it will be calculated. + It's important that all hashes are of the same type. + By default, the hash is calculated using the sha256 algorithm. + + The ability to provide a hash directly is mainly there + for ensuring we can load and save texture atlases to disk. + + :param PIL.Image.Image image: The image for this texture + :param str hash: The hash of the image + """ + __slots__ = ("image", "hash", "__weakref__") + hash_func = "sha256" + + def __init__(self, image: PIL.Image.Image, hash: Optional[str] = None): + self.image = image + self.hash = hash or self.calculate_hash(image) + + @classmethod + def calculate_hash(cls, image: PIL.Image.Image) -> str: + """ + Calculates the hash of an image. + + The algorithm used is defined by the ``hash_func`` class variable. + """ + hash = hashlib.new(cls.hash_func) + hash.update(image.tobytes()) + return hash.hexdigest() + + @property + def width(self) -> int: + """ + The width of the image + """ + return self.image.width + + @property + def height(self) -> int: + """ + The height of the image + """ + return self.image.height + + @property + def size(self) -> Tuple[int, int]: + """ + The size of the image + """ + return self.image.size + + # ImageData uniqueness is based on the hash + # ----------------------------------------- + def __hash__(self) -> int: + return hash(self.hash) + + def __eq__(self, other) -> bool: + if other is None: + return False + if not isinstance(other, self.__class__): + return False + return self.hash == other.hash + + def __ne__(self, other) -> bool: + if other is None: + return True + if not isinstance(other, self.__class__): + return True + return self.hash != other.hash + + # ----------------------------------------- + + def __repr__(self): + return f"" diff --git a/arcade/texture/texture.py b/arcade/texture/texture.py index 418a52a71..59b3d0407 100644 --- a/arcade/texture/texture.py +++ b/arcade/texture/texture.py @@ -1,5 +1,4 @@ import logging -import hashlib from typing import Any, Dict, Optional, Tuple, Type, Union, TYPE_CHECKING from pathlib import Path from weakref import WeakSet @@ -25,6 +24,7 @@ from arcade.hitbox import HitBoxAlgorithm from arcade import cache as _cache from arcade import hitbox +from .image_data import ImageData if TYPE_CHECKING: from arcade.sprite_list import SpriteList @@ -33,85 +33,6 @@ LOG = logging.getLogger(__name__) -class ImageData: - """ - A class holding the image for a texture with other metadata such as the hash. - This information is used internally by the texture atlas to identify unique textures. - - If a hash is not provided, it will be calculated. - It's important that all hashes are of the same type. - By default, the hash is calculated using the sha256 algorithm. - - The ability to provide a hash directly is mainly there - for ensuring we can load and save texture atlases to disk. - - :param PIL.Image.Image image: The image for this texture - :param str hash: The hash of the image - """ - __slots__ = ("image", "hash", "__weakref__") - hash_func = "sha256" - - def __init__(self, image: PIL.Image.Image, hash: Optional[str] = None): - self.image = image - self.hash = hash or self.calculate_hash(image) - - @classmethod - def calculate_hash(cls, image: PIL.Image.Image) -> str: - """ - Calculates the hash of an image. - - The algorithm used is defined by the ``hash_func`` class variable. - """ - hash = hashlib.new(cls.hash_func) - hash.update(image.tobytes()) - return hash.hexdigest() - - @property - def width(self) -> int: - """ - The width of the image - """ - return self.image.width - - @property - def height(self) -> int: - """ - The height of the image - """ - return self.image.height - - @property - def size(self) -> Tuple[int, int]: - """ - The size of the image - """ - return self.image.size - - # ImageData uniqueness is based on the hash - # ----------------------------------------- - def __hash__(self) -> int: - return hash(self.hash) - - def __eq__(self, other) -> bool: - if other is None: - return False - if not isinstance(other, self.__class__): - return False - return self.hash == other.hash - - def __ne__(self, other) -> bool: - if other is None: - return True - if not isinstance(other, self.__class__): - return True - return self.hash != other.hash - - # ----------------------------------------- - - def __repr__(self): - return f"" - - class Texture: """ An arcade.Texture is simply a wrapper for image data as a Pillow image From 4acb24ca1b4d1e586d982b2373e1915ec82eefe1 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 04:13:49 +0100 Subject: [PATCH 02/17] Update __init__ --- arcade/texture/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/arcade/texture/__init__.py b/arcade/texture/__init__.py index 8e5b40e4d..01cfe590a 100644 --- a/arcade/texture/__init__.py +++ b/arcade/texture/__init__.py @@ -1,4 +1,5 @@ -from .texture import Texture, ImageData +from .image_data import ImageData +from .texture import Texture from .loading import ( load_texture, load_textures, @@ -15,6 +16,8 @@ get_default_texture, get_default_image, ) +from .manager import TextureManager +from .spritesheet import SpriteSheet __all__ = [ "Texture", @@ -29,4 +32,6 @@ "cleanup_texture_cache", "get_default_texture", "get_default_image", - ] + "TextureManager", + "SpriteSheet", +] From d9af48c26b0365994ba7d57a52fe3f5fd22226e7 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 04:14:18 +0100 Subject: [PATCH 03/17] Initial spritesheet module --- arcade/texture/spritesheet.py | 70 +++++++++++++++++++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 arcade/texture/spritesheet.py diff --git a/arcade/texture/spritesheet.py b/arcade/texture/spritesheet.py new file mode 100644 index 000000000..bc1d81274 --- /dev/null +++ b/arcade/texture/spritesheet.py @@ -0,0 +1,70 @@ +from PIL import Image +from pathlib import Path +from typing import Union, Tuple + +import arcade +from arcade.types import Rect + + +class SpriteSheet: + """ + Class to hold a sprite sheet. A sprite sheet is a single image that contains + multiple textures. Textures can be created from the sprite sheet by cropping + out sections of the image. + + This is only a utility class. It does not have any special functionality + + :param path: Path to the file to load. + """ + def __init__(self, path: Union[str, Path]): + self._path = path + path = arcade.resources.resolve_resource_path(path) + self._image = Image.open(path).convert("RGBA") + self._flip_flags = (False, False) + + @property + def flip_flags(self) -> Tuple[bool, bool]: + """ + Query the orientation of the sprite sheet. + This can be used to determine if the sprite sheet needs to be flipped. + + Default values are ``(False, False)``. Will be modified when + :py:meth:`flip_left_right` or :py:meth:`flip_top_bottom` is called. + + :return: Tuple of booleans ``(flip_left_right, flip_top_bottom)``. + """ + return self._flip_flags + + def flip_left_right(self) -> None: + """ + Flip the sprite sheet left/right. + """ + self._image = self._image.transpose(Image.FLIP_LEFT_RIGHT) + self._flip_flags = (not self._flip_flags[0], self._flip_flags[1]) + + def flip_top_bottom(self): + """ + Flip the sprite sheet up/down. + """ + self._image = self._image.transpose(Image.FLIP_TOP_BOTTOM) + self._flip_flags = (self._flip_flags[0], not self._flip_flags[1]) + + def crop(self, area: Rect): + """ + Crop a texture from the sprite sheet. + + :param area: Area to crop ``(x, y, width, height)`` + """ + pass + + def crop_grid(self, width: int, height: int, count: int, column_count: int, spacing: int = 0): + """ + Crop a grid of textures from the sprite sheet. + + :param width: Width of the crop. + :param height: Height of the crop. + :param count: Number of textures to crop. + :param column_count: Number of columns in the grid. + :param spacing: Spacing between the textures. + """ + pass From 870d6817d6885c04ca80e4e336d0721c5ab65f95 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 04:14:41 +0100 Subject: [PATCH 04/17] pep8 --- arcade/texture/loading.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/texture/loading.py b/arcade/texture/loading.py index 6326d3d27..fbc3cac9f 100644 --- a/arcade/texture/loading.py +++ b/arcade/texture/loading.py @@ -133,7 +133,7 @@ def _load_or_get_image( ) -> Tuple[ImageData, bool]: """ Load an image, or return a cached version - + :param str file_path: Path to image :param str hit_box_algorithm: The hit box algorithm :param hash: Hash of the image From 16f327e242dc4ec8feec3b785e99afe30043effa Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 04:15:05 +0100 Subject: [PATCH 05/17] Initial texture manager --- arcade/texture/manager.py | 71 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 arcade/texture/manager.py diff --git a/arcade/texture/manager.py b/arcade/texture/manager.py new file mode 100644 index 000000000..d8901e52a --- /dev/null +++ b/arcade/texture/manager.py @@ -0,0 +1,71 @@ +from pathlib import Path +from typing import Union, Optional + +import arcade +from arcade.hitbox import HitBoxAlgorithm +from arcade.texture import Texture +from arcade.cache import ( + TextureCache, + ImageDataCache, + HitBoxCache, +) +from . import SpriteSheet + + +class TextureManager: + """ + This class is used to manage textures. It is used to keep track of + textures that have been loaded, and to make sure we don't load the + same texture twice. + + Textures loaded through this manager is cached internally. + """ + def __init__(self): + self._sprite_sheets = {} + self._textures = {} + self._hit_box_cache = HitBoxCache() + self._image_data_cache = ImageDataCache() + self._texture_cache = TextureCache() + + def texture( + self, + path: Union[str, Path], + hit_box_algorithm: Optional[HitBoxAlgorithm] = None, + cache: bool = True, + ) -> Texture: + """ + Loads a texture or returns a cached version. + + :param path: Path to the file to load. + :param hit_box_algorithm: Algorithm to use to create a hit box for the texture. + """ + # TODO: DON'T CALL arcade.load_texture? + texture = arcade.load_texture(path, hit_box_algorithm=hit_box_algorithm) + # Do caching here + return texture + + def spritesheet(self, path: Union[str, Path], cache: bool = True) -> SpriteSheet: + """ + Loads a spritesheet or returns a cached version. + + :param path: Path to the file to load. + :param cache: If ``True``, the spritesheet will be cached. If ``False``, the + spite sheet will not be cached or returned from the cache. + """ + path = arcade.resources.resolve_resource_path(path) + if path in self._sprite_sheets and cache: + return self._sprite_sheets[path] + + sprite_sheet = SpriteSheet(path) + if cache: + self._sprite_sheets[path] = sprite_sheet + return sprite_sheet + + def flush(self, textures: bool = True, sprite_sheets: bool = True): + """ + Remove contents from the texture manager. + """ + if textures: + self._textures.clear() + if sprite_sheets: + self._sprite_sheets.clear() From b2472177972bb375fde19b448d3401dd33f3c15f Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 18:35:27 +0100 Subject: [PATCH 06/17] SpriteSheet tests --- tests/unit/texture/test_sprite_sheet.py | 58 +++++++++++++++++++++++++ 1 file changed, 58 insertions(+) create mode 100644 tests/unit/texture/test_sprite_sheet.py diff --git a/tests/unit/texture/test_sprite_sheet.py b/tests/unit/texture/test_sprite_sheet.py new file mode 100644 index 000000000..b4ab4abb9 --- /dev/null +++ b/tests/unit/texture/test_sprite_sheet.py @@ -0,0 +1,58 @@ +from pathlib import Path +import pytest +from PIL import Image +import arcade + +SPRITE_SHEET_RESOURCE = ":resources:images/spritesheets/codepage_437.png" +SPRITE_SHEET_PATH = arcade.resources.resolve_resource_path(SPRITE_SHEET_RESOURCE) + + +@pytest.fixture +def image(): + return Image.new("RGBA", (10, 10)) + + +def test_create_from_path(): + ss = arcade.SpriteSheet(SPRITE_SHEET_RESOURCE) + assert ss.flip_flags == (False, False) + assert ss.image is not None + assert ss.path == SPRITE_SHEET_PATH + + +def test_create_from_image(image): + ss = arcade.SpriteSheet(image=image) + assert ss.flip_flags == (False, False) + assert ss.image == image + assert ss.path is None + + +def test_flip(): + ss = arcade.SpriteSheet(SPRITE_SHEET_RESOURCE) + im = Image.open(SPRITE_SHEET_PATH).convert("RGBA") + assert ss.image == im + assert ss.flip_flags == (False, False) + + ss.flip_left_right() + assert ss.flip_flags == (True, False) + assert ss.image != im + im = im.transpose(Image.FLIP_LEFT_RIGHT) + assert ss.image == im + + ss.flip_top_bottom() + assert ss.flip_flags == (True, True) + assert ss.image != im + im = im.transpose(Image.FLIP_TOP_BOTTOM) + assert ss.image == im + + # Flip back to original + ss.flip_left_right() + ss.flip_top_bottom() + assert ss.flip_flags == (False, False) + + +def test_crop(): + pass + + +def test_crop_grid(): + pass From dd7439906ee9f74ecd72240a5cdd0b6677f9562f Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 18:35:51 +0100 Subject: [PATCH 07/17] SpriteSheet: Allow creating from image + props --- arcade/texture/spritesheet.py | 42 ++++++++++++++++++++++++++++++----- 1 file changed, 37 insertions(+), 5 deletions(-) diff --git a/arcade/texture/spritesheet.py b/arcade/texture/spritesheet.py index bc1d81274..aa57bdfb2 100644 --- a/arcade/texture/spritesheet.py +++ b/arcade/texture/spritesheet.py @@ -1,6 +1,6 @@ from PIL import Image from pathlib import Path -from typing import Union, Tuple +from typing import Union, Tuple, Optional import arcade from arcade.types import Rect @@ -16,12 +16,44 @@ class SpriteSheet: :param path: Path to the file to load. """ - def __init__(self, path: Union[str, Path]): - self._path = path - path = arcade.resources.resolve_resource_path(path) - self._image = Image.open(path).convert("RGBA") + def __init__( + self, + path: Optional[Union[str, Path]] = None, + image: Optional[Image.Image] = None, + ): + self._path = None + if path: + self._path = arcade.resources.resolve_resource_path(path) + self._image = Image.open(self._path).convert("RGBA") + elif image: + self._image = image + else: + raise ValueError("Must provide either path or image") + self._flip_flags = (False, False) + @classmethod + def from_image(cls, image: Image.Image): + return cls(image=image) + + @property + def image(self) -> Image.Image: + """ + The image of the sprite sheet. + + :return: The image. + """ + return self._image + + @property + def path(self) -> Optional[Path]: + """ + The path to the sprite sheet. + + :return: The path. + """ + return self._path + @property def flip_flags(self) -> Tuple[bool, bool]: """ From 921d59e855c216386760fbb347745ca3bacef754 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 18:36:16 +0100 Subject: [PATCH 08/17] Include SpriteSheet class in arcade module --- arcade/__init__.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/arcade/__init__.py b/arcade/__init__.py index 3b657f675..56113a01f 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -105,6 +105,7 @@ def configure_logging(level: Optional[int] = None): from .application import open_window from .texture import Texture +from .texture import SpriteSheet from .texture import load_spritesheet from .texture import load_texture from .texture import load_texture_pair @@ -278,6 +279,7 @@ def configure_logging(level: Optional[int] = None): 'SpriteSolidColor', 'Text', 'Texture', + 'SpriteSheet', 'TextureAtlas', 'load_atlas', 'save_atlas', From 8ea1415fabc817aafdc3b0b04753fc6bd9a24ecc Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 18:54:13 +0100 Subject: [PATCH 09/17] Fix circular import --- arcade/texture/spritesheet.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/arcade/texture/spritesheet.py b/arcade/texture/spritesheet.py index aa57bdfb2..1df6ec93c 100644 --- a/arcade/texture/spritesheet.py +++ b/arcade/texture/spritesheet.py @@ -2,7 +2,6 @@ from pathlib import Path from typing import Union, Tuple, Optional -import arcade from arcade.types import Rect @@ -21,9 +20,10 @@ def __init__( path: Optional[Union[str, Path]] = None, image: Optional[Image.Image] = None, ): + from arcade.resources import resolve_resource_path self._path = None if path: - self._path = arcade.resources.resolve_resource_path(path) + self._path = resolve_resource_path(path) self._image = Image.open(self._path).convert("RGBA") elif image: self._image = image From 07560cd9882f594884303d48de8c5b2a14fedb09 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 19:14:27 +0100 Subject: [PATCH 10/17] resources: Only stat the resource once. --- arcade/resources/__init__.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/arcade/resources/__init__.py b/arcade/resources/__init__.py index 11fcd49b6..b90d94bb3 100644 --- a/arcade/resources/__init__.py +++ b/arcade/resources/__init__.py @@ -35,12 +35,14 @@ def resolve_resource_path(path: Union[str, Path]) -> Path: path = Path(path) # Check for the existence of the file and provide useful feedback to - # avoid deep stack trace into pathlib - if not path.exists(): + # avoid deep stack trace into pathlib. + try: + path.resolve(strict=True) + except FileNotFoundError: raise FileNotFoundError(f"Cannot locate resource : {path}") # Always return absolute paths - return path.resolve() + return path def add_resource_handle(handle: str, path: Union[str, Path]) -> None: From a0caf10f493cc489907ec268ea8654f6383f08fe Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 19:15:08 +0100 Subject: [PATCH 11/17] Add TextureManager to arcade --- arcade/__init__.py | 2 ++ arcade/texture/__init__.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/arcade/__init__.py b/arcade/__init__.py index 56113a01f..8538623b9 100644 --- a/arcade/__init__.py +++ b/arcade/__init__.py @@ -105,6 +105,7 @@ def configure_logging(level: Optional[int] = None): from .application import open_window from .texture import Texture +from .texture import TextureManager from .texture import SpriteSheet from .texture import load_spritesheet from .texture import load_texture @@ -279,6 +280,7 @@ def configure_logging(level: Optional[int] = None): 'SpriteSolidColor', 'Text', 'Texture', + 'TextureManager', 'SpriteSheet', 'TextureAtlas', 'load_atlas', diff --git a/arcade/texture/__init__.py b/arcade/texture/__init__.py index 01cfe590a..2068abea8 100644 --- a/arcade/texture/__init__.py +++ b/arcade/texture/__init__.py @@ -1,4 +1,6 @@ from .image_data import ImageData +from .manager import TextureManager +from .spritesheet import SpriteSheet from .texture import Texture from .loading import ( load_texture, @@ -16,8 +18,6 @@ get_default_texture, get_default_image, ) -from .manager import TextureManager -from .spritesheet import SpriteSheet __all__ = [ "Texture", From 8f57fb60317dcce81149dc35ecae5e2b5480fe18 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 19:31:01 +0100 Subject: [PATCH 12/17] missing docstring --- arcade/texture/spritesheet.py | 1 + 1 file changed, 1 insertion(+) diff --git a/arcade/texture/spritesheet.py b/arcade/texture/spritesheet.py index 1df6ec93c..054a43d52 100644 --- a/arcade/texture/spritesheet.py +++ b/arcade/texture/spritesheet.py @@ -14,6 +14,7 @@ class SpriteSheet: This is only a utility class. It does not have any special functionality :param path: Path to the file to load. + :param image: PIL image to use. """ def __init__( self, From 2af2b5d006e3f3b7784f29d75939ad6e6adca76f Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sat, 18 Mar 2023 16:40:40 +0100 Subject: [PATCH 13/17] Placeholder unit test for camera --- tests/unit/camera/test_camera.py | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 tests/unit/camera/test_camera.py diff --git a/tests/unit/camera/test_camera.py b/tests/unit/camera/test_camera.py new file mode 100644 index 000000000..795c2ecde --- /dev/null +++ b/tests/unit/camera/test_camera.py @@ -0,0 +1,11 @@ +import arcade + + +def test_camera(window): + c1 = arcade.SimpleCamera() + assert c1.viewport == (0, 0, *window.size) + assert c1.projection == (0, window.width, 0, window.height) + + c2 = arcade.Camera() + assert c2.viewport == (0, 0, *window.size) + assert c2.projection == (0, window.width, 0, window.height) From 561a9dcc8496f5b487c271ef749b1a8ed12f6ec9 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 2 Apr 2023 16:58:15 +0200 Subject: [PATCH 14/17] push partial work --- arcade/texture/manager.py | 25 ++++++-- arcade/texture/spritesheet.py | 79 +++++++++++++++++++------ tests/unit/texture/test_sprite_sheet.py | 12 +++- 3 files changed, 90 insertions(+), 26 deletions(-) diff --git a/arcade/texture/manager.py b/arcade/texture/manager.py index d8901e52a..615f6bc0d 100644 --- a/arcade/texture/manager.py +++ b/arcade/texture/manager.py @@ -3,7 +3,7 @@ import arcade from arcade.hitbox import HitBoxAlgorithm -from arcade.texture import Texture +from .texture import Texture from arcade.cache import ( TextureCache, ImageDataCache, @@ -11,7 +11,6 @@ ) from . import SpriteSheet - class TextureManager: """ This class is used to manage textures. It is used to keep track of @@ -22,7 +21,6 @@ class TextureManager: """ def __init__(self): self._sprite_sheets = {} - self._textures = {} self._hit_box_cache = HitBoxCache() self._image_data_cache = ImageDataCache() self._texture_cache = TextureCache() @@ -61,11 +59,26 @@ def spritesheet(self, path: Union[str, Path], cache: bool = True) -> SpriteSheet self._sprite_sheets[path] = sprite_sheet return sprite_sheet - def flush(self, textures: bool = True, sprite_sheets: bool = True): + def flush( + self, + sprite_sheets: bool = True, + textures: bool = True, + image_data: bool = True, + hit_boxes: bool = False, + ): """ Remove contents from the texture manager. + + :param sprite_sheets: If ``True``, sprite sheets will be flushed. + :param textures: If ``True``, textures will be flushed. + :param image_data: If ``True``, image data will be flushed. + :param hit_boxes: If ``True``, hit boxes will be flushed. """ - if textures: - self._textures.clear() if sprite_sheets: self._sprite_sheets.clear() + if textures: + self._texture_cache.flush() + if image_data: + self._image_data_cache.flush() + if hit_boxes: + self._hit_box_cache.flush() diff --git a/arcade/texture/spritesheet.py b/arcade/texture/spritesheet.py index 054a43d52..f334f85e8 100644 --- a/arcade/texture/spritesheet.py +++ b/arcade/texture/spritesheet.py @@ -1,8 +1,13 @@ from PIL import Image from pathlib import Path -from typing import Union, Tuple, Optional +from typing import Union, Tuple, Optional, List, TYPE_CHECKING +# from arcade import Texture from arcade.types import Rect +from .texture import Texture + +if TYPE_CHECKING: + from arcade.hitbox import HitBoxAlgorithm class SpriteSheet: @@ -17,14 +22,14 @@ class SpriteSheet: :param image: PIL image to use. """ def __init__( - self, - path: Optional[Union[str, Path]] = None, - image: Optional[Image.Image] = None, - ): - from arcade.resources import resolve_resource_path + self, + path: Optional[Union[str, Path]] = None, + image: Optional[Image.Image] = None, + ): + from arcade.resources import resolve self._path = None if path: - self._path = resolve_resource_path(path) + self._path = resolve(path) self._image = Image.open(self._path).convert("RGBA") elif image: self._image = image @@ -35,17 +40,24 @@ def __init__( @classmethod def from_image(cls, image: Image.Image): + """ + Create a sprite sheet from a PIL image. + + :param image: PIL image to use. + """ return cls(image=image) @property def image(self) -> Image.Image: """ - The image of the sprite sheet. - - :return: The image. + Get or set the PIL image for this sprite sheet. """ return self._image + @image.setter + def image(self, image: Image.Image): + self._image = image + @property def path(self) -> Optional[Path]: """ @@ -90,14 +102,47 @@ def crop(self, area: Rect): """ pass - def crop_grid(self, width: int, height: int, count: int, column_count: int, spacing: int = 0): + def crop_sections(self, sections: List[Rect]): """ - Crop a grid of textures from the sprite sheet. + Crop multiple textures from the sprite sheet by specifying a list of + areas to crop. - :param width: Width of the crop. - :param height: Height of the crop. - :param count: Number of textures to crop. - :param column_count: Number of columns in the grid. - :param spacing: Spacing between the textures. + :param sections: List of areas to crop ``[(x, y, width, height), ...]`` """ pass + + def crop_grid( + self, + size: Tuple[int, int], + columns: int, + count: int, + margin: Rect = (0, 0, 0, 0), + hit_box_algorithm: Optional["HitBoxAlgorithm"] = None, + ) -> List[Texture]: + """ + Crop a grid of textures from the sprite sheet. + + :param size: Size of each texture ``(width, height)`` + :param columns: Number of columns in the grid + :param count: Number of textures to crop + :param margin: The margin around each texture ``(left, right, bottom, top)`` + :param hit_box_algorithm: Hit box algorithm to use for the textures. + """ + textures = [] + width, height = size + left, right, bottom, top = margin + + for sprite_no in range(count): + row = sprite_no // columns + column = sprite_no % columns + + x = (width + left + right) * column + y = (height + top + bottom) * row + im = self.image.crop((x, y, x + width, y + height)) + + texture = Texture(im, hit_box_algorithm=hit_box_algorithm) + texture.file_path = self._path + texture.crop_values = x, y, width, height + textures.append(texture) + + return textures diff --git a/tests/unit/texture/test_sprite_sheet.py b/tests/unit/texture/test_sprite_sheet.py index b4ab4abb9..e83e21a1e 100644 --- a/tests/unit/texture/test_sprite_sheet.py +++ b/tests/unit/texture/test_sprite_sheet.py @@ -1,10 +1,9 @@ -from pathlib import Path import pytest from PIL import Image import arcade SPRITE_SHEET_RESOURCE = ":resources:images/spritesheets/codepage_437.png" -SPRITE_SHEET_PATH = arcade.resources.resolve_resource_path(SPRITE_SHEET_RESOURCE) +SPRITE_SHEET_PATH = arcade.resources.resolve(SPRITE_SHEET_RESOURCE) @pytest.fixture @@ -55,4 +54,11 @@ def test_crop(): def test_crop_grid(): - pass + ss = arcade.SpriteSheet(SPRITE_SHEET_RESOURCE) + textures = ss.crop_grid( + size=(8, 16), + margins=(0, 1, 0, 0), + columns=32, + count=255, + ) + assert len(textures) == 256 From 524094e44088a15e2efad2d1e241a66558bf5778 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Mon, 10 Apr 2023 00:04:38 +0200 Subject: [PATCH 15/17] Move TextureManager import --- arcade/texture/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/arcade/texture/__init__.py b/arcade/texture/__init__.py index 2068abea8..20fd4efdb 100644 --- a/arcade/texture/__init__.py +++ b/arcade/texture/__init__.py @@ -1,5 +1,4 @@ from .image_data import ImageData -from .manager import TextureManager from .spritesheet import SpriteSheet from .texture import Texture from .loading import ( @@ -18,6 +17,7 @@ get_default_texture, get_default_image, ) +from .manager import TextureManager __all__ = [ "Texture", From fbb2c2bdbc6dcc7174caa84d1bc3caf3bde5ba31 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 31 Dec 2023 10:45:10 +0100 Subject: [PATCH 16/17] Fix count in sprite sheet crop test --- tests/unit/texture/test_sprite_sheet.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/texture/test_sprite_sheet.py b/tests/unit/texture/test_sprite_sheet.py index 964864fa3..d75c0bed9 100644 --- a/tests/unit/texture/test_sprite_sheet.py +++ b/tests/unit/texture/test_sprite_sheet.py @@ -61,4 +61,4 @@ def test_crop_grid(): columns=32, count=255, ) - assert len(textures) == 256 + assert len(textures) == 255 From 520aa2f55ab9e07054b036dec3445f16683213d1 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Tue, 2 Jan 2024 15:42:52 +0100 Subject: [PATCH 17/17] Adapt to the new texture cache --- arcade/texture/manager.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/arcade/texture/manager.py b/arcade/texture/manager.py index 615f6bc0d..f7e4f78b2 100644 --- a/arcade/texture/manager.py +++ b/arcade/texture/manager.py @@ -77,8 +77,8 @@ def flush( if sprite_sheets: self._sprite_sheets.clear() if textures: - self._texture_cache.flush() + self._texture_cache.clear() if image_data: - self._image_data_cache.flush() + self._image_data_cache.clear() if hit_boxes: - self._hit_box_cache.flush() + self._hit_box_cache.clear()