From 91c9b6fa5486b4a2bfd5dd404350b78388a26af0 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 12 Mar 2023 04:13:18 +0100 Subject: [PATCH 01/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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/21] 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() From 7682158b24b3c9109a60dec0fab7747326c1fa7d Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 14 Jan 2024 07:39:12 +0100 Subject: [PATCH 18/21] Initial work --- arcade/context.py | 2 +- arcade/texture_atlas/base.py | 110 ++++++++++++++++++++++++++--------- 2 files changed, 84 insertions(+), 28 deletions(-) diff --git a/arcade/context.py b/arcade/context.py index 060f756d8..5d3d39e6a 100644 --- a/arcade/context.py +++ b/arcade/context.py @@ -43,7 +43,7 @@ class ArcadeContext(Context): it's not clear what thread will gc the object. """ - atlas_size = 512, 512 + atlas_size: Tuple[int, int] = 512, 512 def __init__(self, window: pyglet.window.Window, gc_mode: str = "context_gc", gl_api: str = "gl"): diff --git a/arcade/texture_atlas/base.py b/arcade/texture_atlas/base.py index f4b808cc7..86c2f96c3 100644 --- a/arcade/texture_atlas/base.py +++ b/arcade/texture_atlas/base.py @@ -10,6 +10,17 @@ Pyglet atlases are located here: https://github.com/einarf/pyglet/blob/master/pyglet/image/atlas.py +Allocation: +Pyglet's allocator is a simple row based allocator only keeping +track of horizontal strips and how far in the x direction the +each strip is filled. We can't really "deallocate" unless it's +a region at the end of a strip and even doing that is awkward. + +When an image is removed from the atlas we simply just lose that +region until we rebuild the atlas. It can be a good idea to count +the number of lost regions to use as an indicator later. When an +atlas is full we can first rebuild it if there are lost regions +instead of increasing the size. """ from __future__ import annotations @@ -161,31 +172,67 @@ class ImageDataRefCounter: """ Helper class to keep track of how many times an image is used by a texture in the atlas to determine when it's safe to remove it. + + Multiple Texture instances can and will use the same ImageData + instance. """ def __init__(self) -> None: self._data: Dict[str, int] = {} + self._num_decref = 0 def inc_ref(self, image_data: "ImageData") -> None: + """Increment the reference count for an image.""" self._data[image_data.hash] = self._data.get(image_data.hash, 0) + 1 - - def dec_ref(self, image_data: "ImageData") -> None: - # TODO: Should we raise an error if the ref count is 0? + + def dec_ref(self, image_data: "ImageData") -> int: + """ + Decrement the reference count for an image returning the new value. + """ if image_data.hash not in self._data: - return - self._data[image_data.hash] -= 1 - if self._data[image_data.hash] == 0: + raise RuntimeError(f"Image {image_data.hash} not in ref counter") + + val = self._data[image_data.hash] - 1 + self._data[image_data.hash] = val + + if val < 0: + raise RuntimeError(f"Image {image_data.hash} ref count went below zero") + if val == 0: del self._data[image_data.hash] - def get_refs(self, image_data: "ImageData") -> int: + self._num_decref += 1 + + return val + + def get_ref_count(self, image_data: "ImageData") -> int: + """ + Get the reference count for an image. + + Args: + image_data (ImageData): The image to get the reference count for + """ return self._data.get(image_data.hash, 0) - def count_refs(self) -> int: + def count_all_refs(self) -> int: """Helper function to count the total number of references.""" return sum(self._data.values()) + def get_total_decref(self, reset=True) -> int: + """ + Get the total number of decrefs. + + Args: + reset (bool): Reset the counter after getting the value + """ + num_decref = self._num_decref + if reset: + self._num_decref = 0 + return num_decref + def __len__(self) -> int: return len(self._data) + def __repr__(self) -> str: + return f"" class TextureAtlas: """ @@ -270,8 +317,10 @@ def __init__( # when to remove an image from the atlas. self._image_ref_count = ImageDataRefCounter() - # A list of all the images this atlas contains + # A list of all the images this atlas contains. + # Unique by: Internal hash property self._images: WeakSet[ImageData] = WeakSet() + # Unique by: # A set of textures this atlas contains for fast lookups + set operations self._textures: WeakSet["Texture"] = WeakSet() @@ -432,6 +481,7 @@ def add(self, texture: "Texture") -> Tuple[int, AtlasRegion]: :param texture: The texture to add :return: texture_id, AtlasRegion tuple + :raises AllocatorException: If there are no room for the texture """ # If the texture is already in the atlas we also have the image # and can return early with the texture id and region @@ -442,23 +492,30 @@ def add(self, texture: "Texture") -> Tuple[int, AtlasRegion]: LOG.info("Attempting to add texture: %s", texture.atlas_name) - # Add the image if we don't already have it. - # If the atlas is full we will try to resize it. + # Add the *image* to the atlas if it's not already there if not self.has_image(texture.image_data): try: x, y, slot, region = self.allocate(texture.image_data) except AllocatorException: LOG.info("[%s] No room for %s size %s", id(self), texture.atlas_name, texture.image.size) - if self._auto_resize: - width = min(self.width * 2, self.max_width) - height = min(self.height * 2, self.max_height) - if self._size == (width, height): - raise - self.resize((width, height)) + if not self._auto_resize: + raise + + # If we have lost regions we can try to rebuild the atlas + removed_image_count = self._image_ref_count.get_total_decref() + if removed_image_count > 0: + LOG.info("[%s] Rebuilding atlas due to %s lost images", id(self), removed_image_count) + self.rebuild() return self.add(texture) - else: + + width = min(self.width * 2, self.max_width) + height = min(self.height * 2, self.max_height) + if self._size == (width, height): raise + self.resize((width, height)) + return self.add(texture) + # Write the pixel data to the atlas texture self.write_image(texture.image_data.image, x, y) self._image_ref_count.inc_ref(texture.image_data) @@ -480,14 +537,12 @@ def _allocate_texture(self, texture: "Texture") -> Tuple[int, AtlasRegion]: f"Max number of slots: {self._num_texture_slots}" )) - # Add the texture to the atlas existing_slot = self._texture_uv_slots.get(texture.atlas_name) slot = existing_slot if existing_slot is not None else self._texture_uv_slots_free.popleft() self._texture_uv_slots[texture.atlas_name] = slot + image_region = self.get_image_region_info(texture.image_data.hash) - texture_region = copy.deepcopy(image_region) - # Since we copy the original image region we can always apply the - # transform without worrying about multiple transforms. + texture_region = copy.deepcopy(image_region) texture_region.texture_coordinates = Transform.transform_texture_coordinates_order( texture_region.texture_coordinates, texture._vertex_order ) @@ -629,7 +684,8 @@ def remove(self, texture: "Texture") -> None: Remove a texture from the atlas. This doesn't erase the pixel data from the atlas texture - itself, but leaves the area unclaimed. + itself, but leaves the area unclaimed. The area will be + reclaimed when the atlas is rebuilt. :param texture: The texture to remove """ @@ -641,17 +697,16 @@ def remove(self, texture: "Texture") -> None: self._texture_uv_slots_free.appendleft(slot) # Decrement the reference count for the image - self._image_ref_count.dec_ref(texture.image_data) # print("Dec ref", texture.image_data.hash, self._image_ref_count.get_refs(texture.image_data)) # Reclaim the image in the atlas if it's not used by any other texture - if self._image_ref_count.get_refs(texture.image_data) == 0: + if self._image_ref_count.dec_ref(texture.image_data) == 0: self._images.remove(texture.image_data) del self._image_regions[texture.image_data.hash] slot = self._image_uv_slots[texture.image_data.hash] del self._image_uv_slots[texture.image_data.hash] self._image_uv_slots_free.appendleft(slot) - # print("Reclaimed image", texture.image_data.hash) + print("Reclaimed image", texture.image_data.hash) def update_texture_image(self, texture: "Texture"): """ @@ -732,6 +787,7 @@ def resize(self, size: Tuple[int, int]) -> None: :param size: The new size """ + print("RESIZE", self.size) LOG.info("[%s] Resizing atlas from %s to %s", id(self), self._size, size) # Only resize if the size actually changed @@ -835,11 +891,11 @@ def clear( self._fbo.clear() self._textures = WeakSet() self._images = WeakSet() - self._image_ref_count = ImageDataRefCounter() self._image_regions = dict() self._texture_regions = dict() self._allocator = Allocator(*self._size) if clear_image_ids: + self._image_ref_count = ImageDataRefCounter() self._image_uv_slots_free = deque(i for i in range(self._num_image_slots)) self._image_uv_slots = dict() if clear_texture_ids: From 54a1f3a3c2b5a793bdcbb059cd36cf56b963b331 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 14 Jan 2024 11:51:00 +0100 Subject: [PATCH 19/21] More fixes --- arcade/experimental/atlas_load_save.py | 6 ++- arcade/texture_atlas/base.py | 67 ++++++++++++++++++-------- arcade/texture_atlas/helpers.py | 9 ++-- 3 files changed, 56 insertions(+), 26 deletions(-) diff --git a/arcade/experimental/atlas_load_save.py b/arcade/experimental/atlas_load_save.py index 27fc17bcd..d3c76b948 100644 --- a/arcade/experimental/atlas_load_save.py +++ b/arcade/experimental/atlas_load_save.py @@ -1,5 +1,7 @@ """ Quick and dirty atlas load/save testing. +Loading and saving atlases are not officially supported. +This is simply an experiment. Dump atlas: python arcade/experimental/atlas_load_save.py save @@ -19,7 +21,7 @@ import arcade from arcade.texture_atlas.helpers import save_atlas, load_atlas -MODE = 'load' +MODE = 'save' RESOURCE_ROOT = arcade.resources.ASSET_PATH DESTINATION = Path.cwd() @@ -78,7 +80,7 @@ def __init__(self): # Make a sprite for each texture self.sp = arcade.SpriteList(atlas=self.atlas) - for i, texture in enumerate(self.atlas._textures): + for i, texture in enumerate(self.atlas.textures): pos = i * 64 sprite = arcade.Sprite( texture, diff --git a/arcade/texture_atlas/base.py b/arcade/texture_atlas/base.py index 86c2f96c3..4b81723a2 100644 --- a/arcade/texture_atlas/base.py +++ b/arcade/texture_atlas/base.py @@ -24,6 +24,7 @@ """ from __future__ import annotations +import abc import copy import math import time @@ -41,7 +42,7 @@ from array import array from collections import deque from contextlib import contextmanager -from weakref import WeakSet +from weakref import WeakSet, WeakValueDictionary import PIL import PIL.Image @@ -232,9 +233,31 @@ def __len__(self) -> int: return len(self._data) def __repr__(self) -> str: - return f"" + return f"" -class TextureAtlas: + +class BaseTextureAtlas(abc.ABC): + + def __init__(self, ctx: Optional["ArcadeContext"]): + self._ctx = ctx or arcade.get_window().ctx + + @property + def ctx(self) -> "ArcadeContext": + return self._ctx + + @abc.abstractmethod + def add(self, texture: "Texture") -> Tuple[int, AtlasRegion]: + """Add a texture to the atlas.""" + raise NotImplementedError + + @abc.abstractmethod + def remove(self, texture: "Texture") -> None: + """Remove a texture from the atlas.""" + raise NotImplementedError + + + +class TextureAtlas(BaseTextureAtlas): """ A texture atlas with a size in a context. @@ -277,7 +300,7 @@ def __init__( ctx: Optional["ArcadeContext"] = None, capacity: int = 2, ): - self._ctx = ctx or arcade.get_window().ctx + super().__init__(ctx) self._max_size = self._ctx.info.MAX_VIEWPORT_DIMS self._size: Tuple[int, int] = size self._allocator = Allocator(*self._size) @@ -322,7 +345,10 @@ def __init__( self._images: WeakSet[ImageData] = WeakSet() # Unique by: # A set of textures this atlas contains for fast lookups + set operations - self._textures: WeakSet["Texture"] = WeakSet() + # self._textures: WeakSet["Texture"] = WeakSet() + + # atlas_name: Texture + self._textures: WeakValueDictionary[str, "Texture"] = WeakValueDictionary() # Texture containing texture coordinates for images and textures # The 4096 width is a safe constant for all GL implementations @@ -463,7 +489,7 @@ def textures(self) -> List["Texture"]: A new list is constructed from the internal weak set of textures. """ - return list(self._textures) + return list(self._textures.values()) @property def images(self) -> List["ImageData"]: @@ -518,9 +544,10 @@ def add(self, texture: "Texture") -> Tuple[int, AtlasRegion]: # Write the pixel data to the atlas texture self.write_image(texture.image_data.image, x, y) + info = self._allocate_texture(texture) self._image_ref_count.inc_ref(texture.image_data) texture.add_atlas_ref(self) - return self._allocate_texture(texture) + return info def _allocate_texture(self, texture: "Texture") -> Tuple[int, AtlasRegion]: """ @@ -537,6 +564,8 @@ def _allocate_texture(self, texture: "Texture") -> Tuple[int, AtlasRegion]: f"Max number of slots: {self._num_texture_slots}" )) + # NOTE: This is also called when re-building the atlas meaning we + # need to support updating the texture coordinates for existing textures existing_slot = self._texture_uv_slots.get(texture.atlas_name) slot = existing_slot if existing_slot is not None else self._texture_uv_slots_free.popleft() self._texture_uv_slots[texture.atlas_name] = slot @@ -554,7 +583,7 @@ def _allocate_texture(self, texture: "Texture") -> Tuple[int, AtlasRegion]: self._texture_uv_data[offset + i] = texture_region.texture_coordinates[i] self._texture_uv_data_changed = True - self._textures.add(texture) + self._textures[texture.atlas_name] = texture return slot, texture_region @@ -689,16 +718,13 @@ def remove(self, texture: "Texture") -> None: :param texture: The texture to remove """ - self._textures.remove(texture) + del self._textures[texture.atlas_name] # Reclaim the texture uv slot del self._texture_regions[texture.atlas_name] slot = self._texture_uv_slots[texture.atlas_name] del self._texture_uv_slots[texture.atlas_name] self._texture_uv_slots_free.appendleft(slot) - # Decrement the reference count for the image - # print("Dec ref", texture.image_data.hash, self._image_ref_count.get_refs(texture.image_data)) - # Reclaim the image in the atlas if it's not used by any other texture if self._image_ref_count.dec_ref(texture.image_data) == 0: self._images.remove(texture.image_data) @@ -706,7 +732,6 @@ def remove(self, texture: "Texture") -> None: slot = self._image_uv_slots[texture.image_data.hash] del self._image_uv_slots[texture.image_data.hash] self._image_uv_slots_free.appendleft(slot) - print("Reclaimed image", texture.image_data.hash) def update_texture_image(self, texture: "Texture"): """ @@ -769,7 +794,7 @@ def get_image_id(self, hash: str) -> int: def has_texture(self, texture: "Texture") -> bool: """Check if a texture is already in the atlas""" - return texture in self._textures + return texture.atlas_name in self._textures def has_image(self, image_data: "ImageData") -> bool: """Check if a image is already in the atlas""" @@ -787,7 +812,6 @@ def resize(self, size: Tuple[int, int]) -> None: :param size: The new size """ - print("RESIZE", self.size) LOG.info("[%s] Resizing atlas from %s to %s", id(self), self._size, size) # Only resize if the size actually changed @@ -816,7 +840,7 @@ def resize(self, size: Tuple[int, int]) -> None: # Store old images and textures before clearing the atlas images = list(self._images) - textures = list(self._textures) + textures = self.textures # Clear the atlas without wiping the image and texture ids self.clear(clear_texture_ids=False, clear_image_ids=False, texture=False) for image in sorted(images, key=lambda x: x.height): @@ -855,16 +879,17 @@ def resize(self, size: Tuple[int, int]) -> None: duration = time.perf_counter() - resize_start LOG.info("[%s] Atlas resize took %s seconds", id(self), duration) - # print(duration) def rebuild(self) -> None: - """Rebuild the underlying atlas texture. + """ + Rebuild the underlying atlas texture. This method also tries to organize the textures more efficiently ordering them by size. The texture ids will persist so the sprite list don't need to be rebuilt. """ # Hold a reference to the old textures - textures = list(self._textures) + textures = self.textures + self._image_ref_count = ImageDataRefCounter() # Clear the atlas but keep the uv slot mapping self.clear(clear_image_ids=False, clear_texture_ids=False) # Add textures back sorted by height to potentially make more room @@ -889,7 +914,7 @@ def clear( """ if texture: self._fbo.clear() - self._textures = WeakSet() + self._textures = WeakValueDictionary() self._images = WeakSet() self._image_regions = dict() self._texture_regions = dict() @@ -1160,7 +1185,7 @@ def _check_size(self, size: Tuple[int, int]) -> None: def print_contents(self): """Debug method to print the contents of the atlas""" print("Textures:") - for texture in self._textures: + for texture in self.textures: print("->", texture) print("Images:") for image in self._images: diff --git a/arcade/texture_atlas/helpers.py b/arcade/texture_atlas/helpers.py index ecc51324f..7f1be2cad 100644 --- a/arcade/texture_atlas/helpers.py +++ b/arcade/texture_atlas/helpers.py @@ -1,3 +1,6 @@ +""" +THIS IS AN EXPERIMENTAL MODULE WITH NO GUARANTEES OF STABILITY OR SUPPORT. +""" from __future__ import annotations import json @@ -67,8 +70,8 @@ def save_atlas(atlas: TextureAtlas, directory: Path, name: str, resource_root: P # Textures textures = [] - for texture in atlas._textures: - if texture.file_path is None: + for texture in atlas.textures: + if texture.file_path is None: raise ValueError("Can't save a texture not loaded from a file") textures.append({ @@ -154,7 +157,7 @@ def load_atlas( ) texture._vertex_order = tuple(tex['vertex_order']) # type: ignore texture._update_cache_names() - atlas._textures.add(texture) + atlas._textures[texture.atlas_name] = texture # Cache the texture strongly so it doesn't get garbage collected cache.texture_cache.put(texture, file_path=resource_root / tex['hash']) texture.file_path = resource_root / tex['path'] From 53921af9a3d64f9cdb9265e32068660382175aa2 Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 14 Jan 2024 12:18:32 +0100 Subject: [PATCH 20/21] Tests: count_refs -> count_all_refs --- tests/unit/atlas/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/atlas/conftest.py b/tests/unit/atlas/conftest.py index f24cac250..0d3cb11d7 100644 --- a/tests/unit/atlas/conftest.py +++ b/tests/unit/atlas/conftest.py @@ -26,7 +26,7 @@ def check_internals(atlas: arcade.TextureAtlas, *, num_textures = 0, num_images # Misc assert len(atlas._image_ref_count) == num_images # the number of image refs should be the same as the number of textures - assert atlas._image_ref_count.count_refs() == num_textures + assert atlas._image_ref_count.count_all_refs() == num_textures # TODO: Check the size of these when when texture row allocation is fixed # atlas._image_uv_data # atlas._texture_uv_data From 76958593a6c2318a93324818e322a74debb4449f Mon Sep 17 00:00:00 2001 From: Einar Forselv Date: Sun, 14 Jan 2024 12:27:00 +0100 Subject: [PATCH 21/21] pep8 --- arcade/texture_atlas/base.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/arcade/texture_atlas/base.py b/arcade/texture_atlas/base.py index 4b81723a2..631090fbd 100644 --- a/arcade/texture_atlas/base.py +++ b/arcade/texture_atlas/base.py @@ -184,7 +184,7 @@ def __init__(self) -> None: def inc_ref(self, image_data: "ImageData") -> None: """Increment the reference count for an image.""" self._data[image_data.hash] = self._data.get(image_data.hash, 0) + 1 - + def dec_ref(self, image_data: "ImageData") -> int: """ Decrement the reference count for an image returning the new value. @@ -254,7 +254,6 @@ def add(self, texture: "Texture") -> Tuple[int, AtlasRegion]: def remove(self, texture: "Texture") -> None: """Remove a texture from the atlas.""" raise NotImplementedError - class TextureAtlas(BaseTextureAtlas): @@ -343,10 +342,6 @@ def __init__( # A list of all the images this atlas contains. # Unique by: Internal hash property self._images: WeakSet[ImageData] = WeakSet() - # Unique by: - # A set of textures this atlas contains for fast lookups + set operations - # self._textures: WeakSet["Texture"] = WeakSet() - # atlas_name: Texture self._textures: WeakValueDictionary[str, "Texture"] = WeakValueDictionary() @@ -571,7 +566,7 @@ def _allocate_texture(self, texture: "Texture") -> Tuple[int, AtlasRegion]: self._texture_uv_slots[texture.atlas_name] = slot image_region = self.get_image_region_info(texture.image_data.hash) - texture_region = copy.deepcopy(image_region) + texture_region = copy.deepcopy(image_region) texture_region.texture_coordinates = Transform.transform_texture_coordinates_order( texture_region.texture_coordinates, texture._vertex_order )