diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 387b2a1..4f27f06 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -6,106 +6,147 @@ permissions: contents: read jobs: - linux: - runs-on: ubuntu-latest - strategy: - matrix: - target: [x86_64, x86, aarch64, armv7, s390x, ppc64le] - if: "startsWith(github.ref, 'refs/tags/')" - steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 - with: - python-version: '3.10' - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - manylinux: auto - - name: Upload wheels - uses: actions/upload-artifact@v3 - with: - name: wheels - path: dist - windows: - runs-on: windows-latest - strategy: - matrix: - target: [x64, x86] - if: "startsWith(github.ref, 'refs/tags/')" + build-sdist: + name: build sdist + runs-on: ubuntu-latest steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + - uses: PyO3/maturin-action@v1 with: - python-version: '3.10' - architecture: ${{ matrix.target }} - - name: Build wheels - uses: PyO3/maturin-action@v1 - with: - target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 + command: sdist + args: --out dist + rust-toolchain: stable + - uses: actions/upload-artifact@v4 with: - name: wheels + name: pypi_files_sdist path: dist - macos: - runs-on: macos-latest + build: + name: build on ${{ matrix.os }} (${{ matrix.target }} - ${{ matrix.interpreter || 'all' }}${{ matrix.os == 'linux' && format(' - {0}', matrix.manylinux == 'auto' && 'manylinux' || matrix.manylinux) || '' }}) strategy: + fail-fast: false matrix: + os: [linux, macos, windows] target: [x86_64, aarch64] + manylinux: [auto] + include: + - os: linux + manylinux: auto + target: i686 + - os: linux + manylinux: auto + target: aarch64 + - os: linux + manylinux: auto + target: armv7 + - os: linux + manylinux: auto + target: ppc64le + - os: linux + manylinux: auto + target: s390x + - os: windows + target: i686 + python-architecture: x86 + exclude: + - os: windows + target: aarch64 + runs-on: ${{ (matrix.os == 'linux' && 'ubuntu') || matrix.os }}-latest if: "startsWith(github.ref, 'refs/tags/')" steps: - - uses: actions/checkout@v3 - - uses: actions/setup-python@v4 + - uses: actions/checkout@v4 + + - name: set up python + uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.11' + architecture: ${{ matrix.python-architecture || 'x64' }} + + - run: pip install -U twine + - name: Build wheels uses: PyO3/maturin-action@v1 with: target: ${{ matrix.target }} - args: --release --out dist --find-interpreter - sccache: 'true' - - name: Upload wheels - uses: actions/upload-artifact@v3 + manylinux: ${{ matrix.manylinux }} + args: --release --out dist --interpreter ${{ matrix.interpreter || '3.8 3.9 3.10 3.11 3.12' }} + rust-toolchain: stable + docker-options: -e CI + + - run: ${{ (matrix.os == 'windows' && 'dir') || 'ls -lh' }} dist/ + + - run: twine check --strict dist/* + + - uses: actions/upload-artifact@v4 with: - name: wheels + name: pypi_files_${{ matrix.os }}_${{ matrix.target }}_${{ matrix.interpreter || 'all' }}_${{ matrix.manylinux }} path: dist - sdist: + inspect-pypi-assets: + needs: [build, build-sdist] runs-on: ubuntu-latest - if: "startsWith(github.ref, 'refs/tags/')" + steps: - - uses: actions/checkout@v3 - - name: Build sdist - uses: PyO3/maturin-action@v1 - with: - command: sdist - args: --out dist - - name: Upload sdist - uses: actions/upload-artifact@v3 + - uses: actions/checkout@v4 + + - name: get dist artifacts + uses: actions/download-artifact@v4 with: - name: wheels + pattern: pypi_files_* + merge-multiple: true path: dist + - name: list dist files + run: | + ls -lh dist/ + ls -l dist/ + echo "`ls dist | wc -l` files" + + - name: extract and list sdist file + run: | + mkdir sdist-files + tar -xvf dist/*.tar.gz -C sdist-files + tree -a sdist-files + + - name: extract and list wheel file + run: | + ls dist/*cp310-manylinux*x86_64.whl | head -n 1 + python -m zipfile --list `ls dist/*cp310-manylinux*x86_64.whl | head -n 1` + release: - name: Release + needs: [build, build-sdist] + if: success() && startsWith(github.ref, 'refs/tags/') runs-on: ubuntu-latest - if: "startsWith(github.ref, 'refs/tags/')" - needs: [linux, windows, macos, sdist] + steps: - - uses: actions/download-artifact@v3 + - uses: actions/checkout@v4 + + - name: set up python + uses: actions/setup-python@v5 with: - name: wheels - - name: Publish to PyPI - uses: PyO3/maturin-action@v1 + python-version: '3.10' + + - run: pip install -U twine + + - name: get dist artifacts + uses: actions/download-artifact@v4 + with: + pattern: pypi_files_* + merge-multiple: true + path: dist + + - run: twine check --strict dist/* + + - name: upload to pypi + run: twine upload dist/* env: - MATURIN_PYPI_TOKEN: ${{ secrets.PYPI_API_TOKEN }} + TWINE_USERNAME: __token__ + TWINE_PASSWORD: ${{ secrets.PYPI_API_TOKEN }} + + - name: upload to github release + uses: softprops/action-gh-release@v1 with: - command: upload - args: --skip-existing * \ No newline at end of file + files: | + wasm/*.whl + prerelease: ${{ contains(github.ref, 'alpha') || contains(github.ref, 'beta') }} \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index a693f8e..252c875 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4,7 +4,7 @@ version = 3 [[package]] name = "arcade-accelerate" -version = "0.1.0" +version = "1.0.2" dependencies = [ "float_eq", "pyo3", @@ -46,11 +46,17 @@ dependencies = [ "wasi", ] +[[package]] +name = "heck" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "95505c38b4572b2d910cecb0281560f54b440a19336cbbcb27bf6ce6adc6f5a8" + [[package]] name = "indoc" -version = "1.0.9" +version = "2.0.4" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bfa799dd5ed20a7e349f3b4639aa80d74549c81716d9ec4f994c9b5815598306" +checksum = "1e186cfbae8084e513daff4240b4797e342f988cecda4fb6c939150f96315fd8" [[package]] name = "libc" @@ -70,9 +76,9 @@ dependencies = [ [[package]] name = "memoffset" -version = "0.8.0" +version = "0.9.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d61c719bcfbcf5d62b3a09efa6088de8c54bc0bfcd3ea7ae39fcc186108b8de1" +checksum = "5a634b1c61a95585bd15607c6ab0c4e5b226e695ff2800ba0cdccddf208c406c" dependencies = [ "autocfg", ] @@ -106,6 +112,12 @@ dependencies = [ "windows-sys", ] +[[package]] +name = "portable-atomic" +version = "1.6.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7170ef9988bc169ba16dd36a7fa041e5c4cbeb6a35b76d4c03daded371eae7c0" + [[package]] name = "ppv-lite86" version = "0.2.17" @@ -123,15 +135,16 @@ dependencies = [ [[package]] name = "pyo3" -version = "0.18.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "06a3d8e8a46ab2738109347433cb7b96dffda2e4a218b03ef27090238886b147" +checksum = "53bdbb96d49157e65d45cc287af5f32ffadd5f4761438b527b055fb0d4bb8233" dependencies = [ "cfg-if", "indoc", "libc", "memoffset", "parking_lot", + "portable-atomic", "pyo3-build-config", "pyo3-ffi", "pyo3-macros", @@ -140,9 +153,9 @@ dependencies = [ [[package]] name = "pyo3-build-config" -version = "0.18.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "75439f995d07ddfad42b192dfcf3bc66a7ecfd8b4a1f5f6f046aa5c2c5d7677d" +checksum = "deaa5745de3f5231ce10517a1f5dd97d53e5a2fd77aa6b5842292085831d48d7" dependencies = [ "once_cell", "target-lexicon", @@ -150,9 +163,9 @@ dependencies = [ [[package]] name = "pyo3-ffi" -version = "0.18.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "839526a5c07a17ff44823679b68add4a58004de00512a95b6c1c98a6dcac0ee5" +checksum = "62b42531d03e08d4ef1f6e85a2ed422eb678b8cd62b762e53891c05faf0d4afa" dependencies = [ "libc", "pyo3-build-config", @@ -160,9 +173,9 @@ dependencies = [ [[package]] name = "pyo3-macros" -version = "0.18.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "bd44cf207476c6a9760c4653559be4f206efafb924d3e4cbf2721475fc0d6cc5" +checksum = "7305c720fa01b8055ec95e484a6eca7a83c841267f0dd5280f0c8b8551d2c158" dependencies = [ "proc-macro2", "pyo3-macros-backend", @@ -172,11 +185,13 @@ dependencies = [ [[package]] name = "pyo3-macros-backend" -version = "0.18.1" +version = "0.20.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dc1f43d8e30460f36350d18631ccf85ded64c059829208fe680904c65bcd0a4c" +checksum = "7c7e9b68bb9c3149c5b0cade5d07f953d6d125eb4337723c4ccdb665f1f96185" dependencies = [ + "heck", "proc-macro2", + "pyo3-build-config", "quote", "syn", ] @@ -243,9 +258,9 @@ checksum = "a507befe795404456341dfab10cef66ead4c041f62b8b11bbb92bffe5d0953e0" [[package]] name = "syn" -version = "1.0.109" +version = "2.0.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "72b64191b275b66ffe2469e8af2c1cfe3bafa67b529ead792a6d0160888b4237" +checksum = "79d9531f94112cfc3e4c8f5f02cb2b58f72c97b7efd85f70203cc6d8efda5927" dependencies = [ "proc-macro2", "quote", @@ -266,9 +281,9 @@ checksum = "e5464a87b239f13a63a501f2701565754bae92d243d4bb7eb12f6d57d2269bf4" [[package]] name = "unindent" -version = "0.1.11" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e1766d682d402817b5ac4490b3c3002d91dfa0d22812f341609f97b08757359c" +checksum = "c7de7d73e1754487cb58364ee906a499937a0dfabd86bcb980fa99ec8c8fa2ce" [[package]] name = "wasi" diff --git a/Cargo.toml b/Cargo.toml index e2ba62f..e176b88 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "arcade-accelerate" -version = "0.1.0" +version = "1.0.2" edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -10,5 +10,5 @@ crate-type = ["cdylib"] [dependencies] float_eq = "1" -pyo3 = "0.18.1" +pyo3 = "0.20.3" rand = "0.8.5" diff --git a/benchmark/__main__.py b/benchmark/__main__.py index 09e8450..d37662f 100644 --- a/benchmark/__main__.py +++ b/benchmark/__main__.py @@ -1,3 +1,4 @@ +import multiprocessing import argparse import sys from datetime import datetime @@ -39,4 +40,5 @@ def parse_args(args): if __name__ == "__main__": + multiprocessing.set_start_method("spawn") main() diff --git a/benchmark/manager.py b/benchmark/manager.py index a29ca6e..018762c 100644 --- a/benchmark/manager.py +++ b/benchmark/manager.py @@ -90,9 +90,9 @@ def create_test_instances(self): # If a test have multiple instances, create one instance for each if cls.instances: for params, _ in cls.instances: - self.add_test_instance(cls(**params)) + self.add_test_instance(cls(self.session_dir, **params)) else: - self.add_test_instance(cls()) + self.add_test_instance(cls(self.session_dir)) if self.debug: num_instances = len(self.test_instances) @@ -120,7 +120,8 @@ def get_test_instance(self, name: str) -> Optional[PerfTest]: def run(self): """Run all tests""" for instance in self.test_instances: - instance.run(self.session_dir) + instance.start() + instance.join() def create_graph( self, diff --git a/benchmark/tests/arcade/collision.py b/benchmark/tests/arcade/collision.py index 31cccec..b8c64bf 100644 --- a/benchmark/tests/arcade/collision.py +++ b/benchmark/tests/arcade/collision.py @@ -1,4 +1,5 @@ import random +from pathlib import Path import arcade @@ -20,8 +21,9 @@ class Test(ArcadePerfTest): name = "collision" instances = (({"method": 3}, "Simple"),) - def __init__(self, method: int = DEFAULT_METHOD): + def __init__(self, session_dir: Path, method: int = DEFAULT_METHOD): super().__init__( + session_dir, size=(SCREEN_WIDTH, SCREEN_HEIGHT), title=SCREEN_TITLE, start_count=0, diff --git a/benchmark/tests/arcade_accelerate/collision.py b/benchmark/tests/arcade_accelerate/collision.py index 062d61d..aa65b7e 100644 --- a/benchmark/tests/arcade_accelerate/collision.py +++ b/benchmark/tests/arcade_accelerate/collision.py @@ -1,4 +1,8 @@ import random +from pathlib import Path + +import arcade_accelerate +arcade_accelerate.bootstrap() import arcade @@ -20,8 +24,9 @@ class Test(AcceleratedPerfTest): name = "collision" instances = (({"method": 3}, "Simple"),) - def __init__(self, method: int = DEFAULT_METHOD): + def __init__(self, session_dir: Path, method: int = DEFAULT_METHOD): super().__init__( + session_dir, size=(SCREEN_WIDTH, SCREEN_HEIGHT), title=SCREEN_TITLE, start_count=0, diff --git a/benchmark/tests/base.py b/benchmark/tests/base.py index fbaa27b..1888b79 100644 --- a/benchmark/tests/base.py +++ b/benchmark/tests/base.py @@ -1,13 +1,11 @@ -import sys +import multiprocessing from pathlib import Path from typing import Tuple -import arcade - from benchmark.timing import PerformanceTiming -class PerfTest: +class PerfTest(multiprocessing.Process): name = "default" type = "default" series_name = "default" @@ -15,6 +13,7 @@ class PerfTest: def __init__( self, + session_dir: Path, size: Tuple[int, int], title: str = "Perf Test", start_count: int = 0, @@ -22,6 +21,8 @@ def __init__( duration: float = 60.0, **kwargs, ): + super().__init__() + self.session_dir = session_dir self.size = size self.title = title self.start_count = start_count @@ -43,9 +44,9 @@ def on_update(self, delta_time: float): def update_state(self): pass - def run(self, session_dir: Path): + def run(self): self.frame = 0 - out_path = session_dir / "data" + out_path = self.session_dir / "data" out_path.mkdir(parents=True, exist_ok=True) self.timing = PerformanceTiming( @@ -61,6 +62,7 @@ class ArcadePerfTest(PerfTest): def __init__( self, + session_dir: Path, size: Tuple[int, int], title: str = "Perf Test", start_count: int = 0, @@ -69,6 +71,7 @@ def __init__( **kwargs, ): super().__init__( + session_dir, size=size, title=title, start_count=start_count, @@ -99,9 +102,9 @@ def run_test(self): self.update_state() self.window.flip() - def run(self, session_dir: Path): + def run(self): """Run the test collecting data.""" - super().run(session_dir) + super().run() self.create_window() self.setup() @@ -129,6 +132,7 @@ def run(self, session_dir: Path): self.timing.write() def create_window(self): + import arcade try: self.window = arcade.get_window() self.window.set_size(*self.size) @@ -142,22 +146,4 @@ def create_window(self): class AcceleratedPerfTest(ArcadePerfTest): - type = "arcade-accelerate" - - def run(self, session_dir: Path): - # This is necessary to unload arcade and ensure that we have the arcade-accelerate bootstrap applied - # The test module itself is responsbile for applying the bootstrap, but arcade needs to be fully unloaded before then - to_uncache = [] - for mod in sys.modules: - if mod.startswith("arcade."): - to_uncache.append(mod) - - for mod in to_uncache: - del sys.modules[mod] - - import arcade_accelerate - - arcade_accelerate.bootstrap() - import arcade - - super().run(session_dir) + type = "accelerate" \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 8b6a363..96e5cdf 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["maturin>=0.14,<0.15"] +requires = ["maturin>=1.4.0,<1.5.0"] build-backend = "maturin" [project] name = "arcade-accelerate" -version = "0.1.0" +version = "1.0.2" description = "A companion library for Arcade providing accelerated Rust functions" readme = "README.md" authors = [ diff --git a/python/arcade_accelerate/__init__.py b/python/arcade_accelerate/__init__.py index 313071d..0fca670 100644 --- a/python/arcade_accelerate/__init__.py +++ b/python/arcade_accelerate/__init__.py @@ -1,73 +1,73 @@ import sys -import arcade -from arcade_accelerate import arcade_accelerate # type: ignore +from arcade_accelerate import arcade_accelerate +from arcade_accelerate.module_patcher import ( + AutoPopulatingDictionary, + PatchingMetaPathFinder, +) def bootstrap(): - """Replace arcade math functions with rust accelerated versions.""" - patch_math() - patch_geometry() - patch_hitboxes() - patch_spritelist_collision() - patch_sprite() - - exclude = [ - "arcade.hitbox.base", - "arcade.math", - "arcade.geometry", - "arcade.sprite_list.collision", - "arcade.sprite.base", - ] - - pkgs = [] - for mod in exclude: - pkg = mod.split(".", 1)[0] - pkgs.append(pkg) - - to_uncache = [] - for mod in sys.modules: - if mod in exclude: - continue + patches = AutoPopulatingDictionary() + sys.meta_path.insert(0, PatchingMetaPathFinder(patches)) - if mod in pkgs: - to_uncache.append(mod) - continue - - for pkg in pkgs: - if mod.startswith(pkg + "."): - to_uncache.append(mod) - break - - for mod in to_uncache: - del sys.modules[mod] + """Replace arcade math functions with rust accelerated versions.""" + patch_math(patches) + patch_geometry(patches) + patch_hitboxes(patches) + patch_spritelist_collision(patches) -def patch_hitboxes(): - arcade.hitbox.base.HitBox = arcade_accelerate.HitBox - arcade.hitbox.base.RotatableHitBox = arcade_accelerate.RotatableHitBox +def patch_hitboxes(patches): + patches["arcade.hitbox.base"].HitBox = arcade_accelerate.HitBox + patches["arcade.hitbox.base"].RotatableHitBox = arcade_accelerate.RotatableHitBox -def patch_spritelist_collision(): - arcade.sprite_list.collision.check_for_collision_with_list = ( +def patch_spritelist_collision(patches): + patches["arcade.sprite_list.collision"].check_for_collision_with_list = ( arcade_accelerate.check_for_collision_with_list ) - arcade.sprite_list.collision.check_for_collision_with_lists = ( + patches["arcade.sprite_list.collision"].check_for_collision_with_lists = ( arcade_accelerate.check_for_collision_with_lists ) -def patch_math(): - arcade.math.rotate_point = arcade_accelerate.rotate_point +def patch_math(patches): + patches["arcade.math"].rotate_point = arcade_accelerate.rotate_point + patches["arcade.math"].clamp = arcade_accelerate.clamp + patches["arcade.math"].lerp = arcade_accelerate.lerp + patches["arcade.math"].lerp_2d = arcade_accelerate.lerp_2d + patches["arcade.math"].lerp_3d = arcade_accelerate.lerp_3d + patches["arcade.math"].lerp_angle = arcade_accelerate.lerp_angle + patches["arcade.math"].get_distance = arcade_accelerate.get_distance + patches["arcade.math"].get_angle_degrees = arcade_accelerate.get_angle_degrees + patches["arcade.math"].get_angle_radians = arcade_accelerate.get_angle_radians + patches["arcade.math"].rand_in_rect = arcade_accelerate.rand_in_rect + patches["arcade.math"].rand_in_circle = arcade_accelerate.rand_in_circle + patches["arcade.math"].rand_on_circle = arcade_accelerate.rand_on_circle + patches["arcade.math"].rand_on_line = arcade_accelerate.rand_on_line + patches["arcade.math"].rand_angle_360_deg = arcade_accelerate.rand_angle_360_deg + patches["arcade.math"].rand_angle_spread_deg = ( + arcade_accelerate.rand_angle_spread_deg + ) + patches["arcade.math"].rand_vec_spread_deg = ( + arcade_accelerate.rand_vec_spread_deg + ) + patches["arcade.math"].rand_vec_magnitude = arcade_accelerate.rand_vec_magnitude + patches["arcade.math"].quaternion_rotation = arcade_accelerate.quaternion_rotation -def patch_geometry(): - arcade.geometry.are_polygons_intersecting = ( +def patch_geometry(patches): + patches["arcade.geometry"].are_polygons_intersecting = ( arcade_accelerate.are_polygons_intersecting ) - - -def patch_sprite(): - import arcade.sprite.base - - arcade.sprite.base.BasicSprite = arcade_accelerate.BasicSprite + patches["arcade.geometry"].is_point_in_box = arcade_accelerate.is_point_in_box + patches["arcade.geometry"].get_triangle_orientation = ( + arcade_accelerate.get_triangle_orientation + ) + patches["arcade.geometry"].are_lines_intersecting = ( + arcade_accelerate.are_lines_intersecting + ) + patches["arcade.geometry"].is_point_in_polygon = ( + arcade_accelerate.is_point_in_polygon + ) diff --git a/python/arcade_accelerate/module_patcher.py b/python/arcade_accelerate/module_patcher.py new file mode 100644 index 0000000..478aa1d --- /dev/null +++ b/python/arcade_accelerate/module_patcher.py @@ -0,0 +1,53 @@ +import sys + +from importlib.abc import Loader, MetaPathFinder +from importlib.machinery import ModuleSpec +from types import ModuleType, SimpleNamespace +from typing import Sequence + + +class AutoPopulatingDictionary(dict): + def __missing__(self, key): + self[key] = item = SimpleNamespace() + return item + + +class PatchingMetaPathFinder(MetaPathFinder): + def __init__(self, patches): + self._patches = patches + + def _remaining_meta_path(self): + index_of_self = sys.meta_path.index(self) + remaining_meta_path = sys.meta_path[index_of_self + 1 :] + return remaining_meta_path + + def find_spec( + self, fullname: str, path: Sequence[str] | None, target: ModuleType | None = ... + ) -> ModuleSpec | None: + spec = None + for finder in self._remaining_meta_path(): + spec = finder.find_spec(fullname, path, target) + if spec is not None: + if spec.name in self._patches: + if type(spec) is not ModuleSpec: + raise Exception( + "Type of module spec to be ModuleSpec, not a subtype" + ) + spec.loader = PatchingLoader( + spec.loader, self._patches[spec.name].__dict__ + ) + break + return spec + + +class PatchingLoader(Loader): + def __init__(self, loader: Loader, patches: dict): + self._loader = loader + self._patches = patches + + def create_module(self, spec: ModuleSpec) -> ModuleType | None: + return self._loader.create_module(spec) + + def exec_module(self, module: ModuleType) -> None: + self._loader.exec_module(module) + module.__dict__.update(self._patches) diff --git a/src/geometry.rs b/src/geometry.rs index eb512f5..8a15d14 100644 --- a/src/geometry.rs +++ b/src/geometry.rs @@ -1,7 +1,72 @@ use pyo3::prelude::*; +pub fn are_polygons_intersecting_native( + poly_a: &Vec<(f32, f32)>, + poly_b: &Vec<(f32, f32)>, +) -> bool { + // If either polygon is empty, we should just return False + if poly_a.is_empty() || poly_b.is_empty() { + return false; + } + let polygons = [poly_a, poly_b]; + for polygon in &polygons { + for i1 in 0..polygon.len() { + let i2 = (i1 + 1) % polygon.len(); + let projection_1 = polygon[i1]; + let projection_2 = polygon[i2]; + + let normal = ( + projection_2.1 - projection_1.1, + projection_1.0 - projection_2.0, + ); + + let mut min_a: Option = None; + let mut max_a: Option = None; + let mut min_b: Option = None; + let mut max_b: Option = None; + + for point in polygons[0] { + let projected = normal.0 * point.0 + normal.1 * point.1; + match min_a { + Some(x) if projected < x => min_a = Some(projected), + Some(_x) => {} + None => min_a = Some(projected), + } + match max_a { + Some(x) if projected > x => max_a = Some(projected), + Some(_x) => {} + None => max_a = Some(projected), + } + } + + for point in polygons[1] { + let projected = normal.0 * point.0 + normal.1 * point.1; + match min_b { + Some(x) if projected < x => min_b = Some(projected), + Some(_x) => {} + None => min_b = Some(projected), + } + match max_b { + Some(x) if projected > x => max_b = Some(projected), + Some(_x) => {} + None => max_b = Some(projected), + } + } + + if max_a <= min_b || max_b <= min_a { + return false; + } + } + } + true +} + #[pyfunction] pub fn are_polygons_intersecting(poly_a: Vec<(f32, f32)>, poly_b: Vec<(f32, f32)>) -> bool { + // If either polygon is empty, we should just return False + if poly_a.is_empty() || poly_b.is_empty() { + return false; + } let polygons = [poly_a, poly_b]; for polygon in &polygons { for i1 in 0..polygon.len() { @@ -108,7 +173,7 @@ pub fn is_point_in_polygon(x: f32, y: f32, polygon: Vec<(f32, f32)>) -> bool { // Create a point for line segment // from p to infinite - let extreme = (10000.0, p.1); + let extreme = (f32::MAX, p.1); // To count number of points in polygon // whose y-coordinate is equal to @@ -132,7 +197,7 @@ pub fn is_point_in_polygon(x: f32, y: f32, polygon: Vec<(f32, f32)>) -> bool { // segment 'i-next', then check if it lies // on segment. If it lies, return true, otherwise false if get_triangle_orientation(polygon[i], p, polygon[next_item]) == 0 { - return !is_point_in_box(polygon[i], p, polygon[next_item]); + return is_point_in_box(polygon[i], p, polygon[next_item]); } count += 1 @@ -171,6 +236,14 @@ mod tests { assert!(!result); } + #[test] + fn test_empty_polygons_intersecting() { + let poly_a: Vec<(f32, f32)> = vec![]; + let poly_b: Vec<(f32, f32)> = vec![]; + let result = are_polygons_intersecting(poly_a, poly_b); + assert!(!result); + } + #[test] fn test_is_point_in_box() { // point inside diff --git a/src/hitbox.rs b/src/hitbox.rs index 5b35dfc..ed80446 100644 --- a/src/hitbox.rs +++ b/src/hitbox.rs @@ -1,15 +1,22 @@ use pyo3::prelude::*; +pub trait NativeAdjustedPoints { + fn get_adjusted_points_native(&mut self) -> &Vec<(f32, f32)>; +} + #[derive(Clone)] -#[pyclass(subclass, module = "arcade.hitbox.base")] +#[pyclass(module = "arcade.hitbox.base")] pub struct HitBox { #[pyo3(get, set)] pub points: Vec<(f32, f32)>, - #[pyo3(get, set)] + #[pyo3(get)] pub position: (f32, f32), - #[pyo3(get, set)] + #[pyo3(get)] pub scale: (f32, f32), pub angle: f32, + + pub adjusted_cache: Vec<(f32, f32)>, + pub cache_dirty: bool, } #[pymethods] @@ -27,6 +34,8 @@ impl HitBox { position: final_position, scale: final_scale, angle: 0.0, + adjusted_cache: vec![], + cache_dirty: true, } } @@ -48,90 +57,93 @@ impl HitBox { Ok(adjustable) } - fn get_adjusted_points(self_: PyRef<'_, Self>) -> Vec<(f32, f32)> { - let mut new_points: Vec<(f32, f32)> = Vec::with_capacity(self_.points.len()); - - for point in self_.points.iter() { - let x = (point.0 * self_.scale.0) + self_.position.0; - let y = (point.1 * self_.scale.1) + self_.position.1; - new_points.push((x, y)); + pub fn get_adjusted_points(&mut self) -> Vec<(f32, f32)> { + if self.cache_dirty { + self.adjusted_cache = Vec::with_capacity(self.points.len()); + for point in self.points.iter() { + let x = (point.0 * self.scale.0) + self.position.0; + let y = (point.1 * self.scale.1) + self.position.1; + self.adjusted_cache.push((x, y)); + } + self.cache_dirty = false; } - new_points + self.adjusted_cache.to_vec() + } + + #[setter] + pub fn set_position(&mut self, value: (f32, f32)) -> PyResult<()> { + self.position = value; + self.cache_dirty = true; + Ok(()) + } + + #[setter] + pub fn set_scale(&mut self, value: (f32, f32)) -> PyResult<()> { + self.scale = value; + self.cache_dirty = true; + Ok(()) } #[getter] - pub fn left(self_: PyRef<'_, Self>) -> PyResult { - let mut converted = HitBox::get_adjusted_points(self_); + pub fn left(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); Ok(converted[0].0) } #[getter] - pub fn right(self_: PyRef<'_, Self>) -> PyResult { - let mut converted: Vec<(f32, f32)> = HitBox::get_adjusted_points(self_); + pub fn right(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); Ok(converted[0].0) } #[getter] - pub fn bottom(self_: PyRef<'_, Self>) -> PyResult { - let mut converted: Vec<(f32, f32)> = HitBox::get_adjusted_points(self_); + pub fn bottom(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); Ok(converted[0].1) } #[getter] - pub fn top(self_: PyRef<'_, Self>) -> PyResult { - let mut converted: Vec<(f32, f32)> = HitBox::get_adjusted_points(self_); + pub fn top(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); Ok(converted[0].1) } } -impl HitBox { - pub fn get_adjusted_points_native(&self) -> Vec<(f32, f32)> { - let mut new_points: Vec<(f32, f32)> = Vec::with_capacity(self.points.len()); - - for point in self.points.iter() { - let x = (point.0 * self.scale.0) + self.position.0; - let y = (point.1 * self.scale.1) + self.position.1; - new_points.push((x, y)); +impl NativeAdjustedPoints for HitBox { + fn get_adjusted_points_native(&mut self) -> &Vec<(f32, f32)> { + if self.cache_dirty { + self.adjusted_cache = Vec::with_capacity(self.points.len()); + for point in self.points.iter() { + let x = (point.0 * self.scale.0) + self.position.0; + let y = (point.1 * self.scale.1) + self.position.1; + self.adjusted_cache.push((x, y)); + } + self.cache_dirty = false; } - new_points - } - - pub fn left_native(&self) -> f32 { - let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native(); - converted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); - converted[0].0 - } - - pub fn right_native(&self) -> f32 { - let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native(); - converted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); - converted[0].0 - } - - pub fn bottom_native(&self) -> f32 { - let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native(); - converted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); - converted[0].1 - } - - pub fn top_native(&self) -> f32 { - let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native(); - converted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); - converted[0].1 + &self.adjusted_cache } } #[derive(Clone)] -#[pyclass(extends=HitBox, module = "arcade.hitbox.base")] +#[pyclass(module = "arcade.hitbox.base")] pub struct RotatableHitBox { #[pyo3(get, set)] - angle: f32, + pub points: Vec<(f32, f32)>, + #[pyo3(get)] + pub position: (f32, f32), + #[pyo3(get)] + pub scale: (f32, f32), + #[pyo3(get)] + pub angle: f32, + + pub adjusted_cache: Vec<(f32, f32)>, + pub cache_dirty: bool, } #[pymethods] @@ -142,59 +154,194 @@ impl RotatableHitBox { position: Option<(f32, f32)>, scale: Option<(f32, f32)>, angle: Option, - ) -> (Self, HitBox) { + ) -> RotatableHitBox { + let final_position = position.unwrap_or((0.0, 0.0)); + let final_scale = scale.unwrap_or((1.0, 1.0)); let final_angle = angle.unwrap_or(0.0); - ( - RotatableHitBox { angle: final_angle }, - HitBox::new(points, position, scale), + RotatableHitBox { + points, + position: final_position, + scale: final_scale, + angle: final_angle, + adjusted_cache: vec![], + cache_dirty: true, + } + } + + fn create_rotatable( + self_: PyRef<'_, Self>, + py: Python<'_>, + angle: Option, + ) -> PyResult> { + let adjustable: Py = Py::new( + py, + RotatableHitBox::new( + self_.points.to_vec(), + Some(self_.position), + Some(self_.scale), + angle, + ), ) + .unwrap(); + Ok(adjustable) } - pub fn get_adjusted_points(self_: PyRef<'_, Self>) -> Vec<(f32, f32)> { - let super_: &HitBox = self_.as_ref(); - let mut new_points: Vec<(f32, f32)> = Vec::with_capacity(super_.points.len()); + pub fn get_adjusted_points(&mut self) -> Vec<(f32, f32)> { + if self.cache_dirty { + self.adjusted_cache = Vec::with_capacity(self.points.len()); - let rad = self_.angle.to_radians(); - let rad_cos = rad.cos(); - let rad_sin = rad.sin(); - for point in super_.points.iter() { - let x = ((point.0 * rad_cos - point.1 * rad_sin) * super_.scale.0) + super_.position.0; - let y = ((point.0 * rad_sin + point.1 * rad_cos) * super_.scale.1) + super_.position.1; - new_points.push((x, y)); + let rad = self.angle.to_radians(); + let rad_cos = rad.cos(); + let rad_sin = rad.sin(); + for point in self.points.iter() { + let x = ((point.0 * rad_cos + point.1 * rad_sin) * self.scale.0) + self.position.0; + let y = ((-point.0 * rad_sin + point.1 * rad_cos) * self.scale.1) + self.position.1; + self.adjusted_cache.push((x, y)); + } + self.cache_dirty = false; } - new_points + self.adjusted_cache.to_vec() + } + + #[setter] + pub fn set_position(&mut self, value: (f32, f32)) -> PyResult<()> { + self.position = value; + self.cache_dirty = true; + Ok(()) + } + + #[setter] + pub fn set_scale(&mut self, value: (f32, f32)) -> PyResult<()> { + self.scale = value; + self.cache_dirty = true; + Ok(()) + } + + #[setter] + pub fn set_angle(&mut self, value: f32) -> PyResult<()> { + self.angle = value; + self.cache_dirty = true; + Ok(()) } #[getter] - fn left(self_: PyRef<'_, Self>) -> PyResult { - let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); + pub fn left(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); Ok(converted[0].0) } #[getter] - fn right(self_: PyRef<'_, Self>) -> PyResult { - let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); + pub fn right(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); Ok(converted[0].0) } #[getter] - fn bottom(self_: PyRef<'_, Self>) -> PyResult { - let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); + pub fn bottom(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); Ok(converted[0].1) } #[getter] - fn top(self_: PyRef<'_, Self>) -> PyResult { - let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); + pub fn top(&mut self) -> PyResult { + let mut converted: Vec<(f32, f32)> = self.get_adjusted_points_native().to_vec(); converted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); Ok(converted[0].1) } } +impl NativeAdjustedPoints for RotatableHitBox { + fn get_adjusted_points_native(&mut self) -> &Vec<(f32, f32)> { + if self.cache_dirty { + self.adjusted_cache = Vec::with_capacity(self.points.len()); + + let rad = self.angle.to_radians(); + let rad_cos = rad.cos(); + let rad_sin = rad.sin(); + for point in self.points.iter() { + let x = ((point.0 * rad_cos + point.1 * rad_sin) * self.scale.0) + self.position.0; + let y = ((-point.0 * rad_sin + point.1 * rad_cos) * self.scale.1) + self.position.1; + self.adjusted_cache.push((x, y)); + } + self.cache_dirty = false; + } + + &self.adjusted_cache + } +} + +// #[derive(Clone)] +// #[pyclass(extends=HitBox, module = "arcade.hitbox.base")] +// pub struct RotatableHitBox { +// #[pyo3(get, set)] +// angle: f32, +// } + +// #[pymethods] +// impl RotatableHitBox { +// #[new] +// fn new( +// points: Vec<(f32, f32)>, +// position: Option<(f32, f32)>, +// scale: Option<(f32, f32)>, +// angle: Option, +// ) -> (Self, HitBox) { +// let final_angle = angle.unwrap_or(0.0); +// ( +// RotatableHitBox { angle: final_angle }, +// HitBox::new(points, position, scale), +// ) +// } + +// pub fn get_adjusted_points(self_: PyRef<'_, Self>) -> Vec<(f32, f32)> { +// let super_: &HitBox = self_.as_ref(); +// let mut new_points: Vec<(f32, f32)> = Vec::with_capacity(super_.points.len()); + +// let rad = self_.angle.to_radians(); +// let rad_cos = rad.cos(); +// let rad_sin = rad.sin(); +// for point in super_.points.iter() { +// let x = ((point.0 * rad_cos + point.1 * rad_sin) * super_.scale.0) + super_.position.0; +// let y = ((-point.0 * rad_sin + point.1 * rad_cos) * super_.scale.1) + super_.position.1; +// new_points.push((x, y)); +// } + +// new_points +// } + +// #[getter] +// fn left(self_: PyRef<'_, Self>) -> PyResult { +// let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); +// converted.sort_by(|a, b| a.0.partial_cmp(&b.0).unwrap()); +// Ok(converted[0].0) +// } + +// #[getter] +// fn right(self_: PyRef<'_, Self>) -> PyResult { +// let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); +// converted.sort_by(|a, b| b.0.partial_cmp(&a.0).unwrap()); +// Ok(converted[0].0) +// } + +// #[getter] +// fn bottom(self_: PyRef<'_, Self>) -> PyResult { +// let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); +// converted.sort_by(|a, b| a.1.partial_cmp(&b.1).unwrap()); +// Ok(converted[0].1) +// } + +// #[getter] +// fn top(self_: PyRef<'_, Self>) -> PyResult { +// let mut converted: Vec<(f32, f32)> = RotatableHitBox::get_adjusted_points(self_); +// converted.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap()); +// Ok(converted[0].1) +// } +// } + // impl RotatableHitBox { // pub fn get_adjusted_points_native(self) -> Vec<(f32, f32)> { // let mut new_points: Vec<(f32, f32)> = Vec::with_capacity(self.parent.points.len()); diff --git a/src/lib.rs b/src/lib.rs index e0395d5..51085eb 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,33 +1,27 @@ use pyo3::prelude::*; mod hitbox; -pub use hitbox::{HitBox, RotatableHitBox}; +pub use hitbox::*; mod math; +pub use math::*; mod geometry; -pub use geometry::{ - are_lines_intersecting, are_polygons_intersecting, get_triangle_orientation, is_point_in_box, - is_point_in_polygon, -}; - -mod sprite; -pub use sprite::{BasicSprite, Sprite}; +pub use geometry::*; mod sprite_list; -pub use sprite_list::{check_for_collision_with_list, check_for_collision_with_lists}; +pub use sprite_list::*; /// A Python module implemented in Rust. #[pymodule] fn arcade_accelerate(_py: Python, m: &PyModule) -> PyResult<()> { - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; - m.add_class::()?; + m.add_class::()?; + m.add_class::()?; m.add_function(wrap_pyfunction!(math::rotate_point, m)?)?; m.add_function(wrap_pyfunction!(math::clamp, m)?)?; m.add_function(wrap_pyfunction!(math::lerp, m)?)?; - m.add_function(wrap_pyfunction!(math::lerp_vec, m)?)?; + m.add_function(wrap_pyfunction!(math::lerp_2d, m)?)?; + m.add_function(wrap_pyfunction!(math::lerp_3d, m)?)?; m.add_function(wrap_pyfunction!(math::lerp_angle, m)?)?; m.add_function(wrap_pyfunction!(math::get_distance, m)?)?; m.add_function(wrap_pyfunction!(math::get_angle_degrees, m)?)?; @@ -38,11 +32,22 @@ fn arcade_accelerate(_py: Python, m: &PyModule) -> PyResult<()> { m.add_function(wrap_pyfunction!(math::rand_on_line, m)?)?; m.add_function(wrap_pyfunction!(math::rand_angle_360_deg, m)?)?; m.add_function(wrap_pyfunction!(math::rand_angle_spread_deg, m)?)?; - m.add_function(wrap_pyfunction!(math::rand_vec_degree_spread, m)?)?; + m.add_function(wrap_pyfunction!(math::rand_vec_spread_deg, m)?)?; m.add_function(wrap_pyfunction!(math::rand_vec_magnitude, m)?)?; - m.add_function(wrap_pyfunction!(are_polygons_intersecting, m)?)?; - m.add_function(wrap_pyfunction!(check_for_collision_with_list, m)?)?; - m.add_function(wrap_pyfunction!(check_for_collision_with_lists, m)?)?; - m.add_function(wrap_pyfunction!(is_point_in_polygon, m)?)?; + m.add_function(wrap_pyfunction!(math::quaternion_rotation, m)?)?; + m.add_function(wrap_pyfunction!(geometry::are_polygons_intersecting, m)?)?; + m.add_function(wrap_pyfunction!(geometry::is_point_in_polygon, m)?)?; + m.add_function(wrap_pyfunction!(geometry::is_point_in_box, m)?)?; + m.add_function(wrap_pyfunction!(geometry::get_triangle_orientation, m)?)?; + m.add_function(wrap_pyfunction!(geometry::are_lines_intersecting, m)?)?; + m.add_function(wrap_pyfunction!( + sprite_list::check_for_collision_with_list, + m + )?)?; + m.add_function(wrap_pyfunction!( + sprite_list::check_for_collision_with_lists, + m + )?)?; + Ok(()) } diff --git a/src/math.rs b/src/math.rs index 385336f..166b859 100644 --- a/src/math.rs +++ b/src/math.rs @@ -14,8 +14,8 @@ pub fn rotate_point(x: f32, y: f32, cx: f32, cy: f32, angle: f32) -> (f32, f32) let temp_y = y - cy; // rotate point - let xnew = (temp_x * c - temp_y * s) + cx; - let ynew = (temp_x * s + temp_y * c) + cy; + let xnew = (temp_x * c + temp_y * s) + cx; + let ynew = (-temp_x * s + temp_y * c) + cy; let precision = 10i32.pow(_PRECISION) as f32; let x_rounded = (xnew * precision).round() / precision; @@ -41,10 +41,19 @@ pub fn lerp(v1: f32, v2: f32, u: f32) -> f32 { } #[pyfunction] -pub fn lerp_vec(v1: (f32, f32), v2: (f32, f32), u: f32) -> (f32, f32) { +pub fn lerp_2d(v1: (f32, f32), v2: (f32, f32), u: f32) -> (f32, f32) { (lerp(v1.0, v2.0, u), lerp(v1.1, v2.1, u)) } +#[pyfunction] +pub fn lerp_3d(v1: (f32, f32, f32), v2: (f32, f32, f32), u: f32) -> (f32, f32, f32) { + ( + lerp(v1.0, v2.0, u), + lerp(v1.1, v2.1, u), + lerp(v1.2, v2.2, u), + ) +} + #[pyfunction] pub fn lerp_angle(start_angle: f32, end_angle: f32, u: f32) -> f32 { let temp_start = start_angle % 360.0; @@ -88,11 +97,11 @@ pub fn get_angle_radians(x1: f32, y1: f32, x2: f32, y2: f32) -> f32 { } #[pyfunction] -pub fn rand_in_rect(bottom_left: (f32, f32), width: f32, height: f32) -> (f32, f32) { +pub fn rand_in_rect(rect: (f32, f32, f32, f32, f32, f32, f32, f32)) -> (f32, f32) { let mut rng = thread_rng(); - let random_x: f32 = rng.gen_range(bottom_left.0..bottom_left.0 + width); - let random_y: f32 = rng.gen_range(bottom_left.1..bottom_left.1 + height); + let random_x: f32 = rng.gen_range(rect.0..=rect.1); + let random_y: f32 = rng.gen_range(rect.2..=rect.3); (random_x, random_y) } @@ -130,7 +139,7 @@ pub fn rand_on_line(pos1: (f32, f32), pos2: (f32, f32)) -> (f32, f32) { let mut rng = thread_rng(); let u: f32 = rng.gen_range(0.0..1.0); - lerp_vec(pos1, pos2, u) + lerp_2d(pos1, pos2, u) } #[pyfunction] @@ -150,7 +159,7 @@ pub fn rand_angle_spread_deg(angle: f32, half_angle_spread: f32) -> f32 { } #[pyfunction] -pub fn rand_vec_degree_spread(angle: f32, half_angle_spread: f32, length: f32) -> (f32, f32) { +pub fn rand_vec_spread_deg(angle: f32, half_angle_spread: f32, length: f32) -> (f32, f32) { let a = rand_angle_spread_deg(angle, half_angle_spread); let vel = _Vec2::from_polar(a, length); vel.as_tuple() @@ -164,6 +173,29 @@ pub fn rand_vec_magnitude(angle: f32, lo_magnitude: f32, hi_magnitude: f32) -> ( vel.as_tuple() } +#[pyfunction] +pub fn quaternion_rotation( + axis: (f32, f32, f32), + vector: (f32, f32, f32), + angle: f32, +) -> (f32, f32, f32) { + let angle_rads = -angle.to_radians(); + let (c2, s2) = (f32::cos(angle_rads / 2.0), f32::sin(angle_rads / 2.0)); + + let (q0, q1, q2, q3) = (c2, s2 * axis.0, s2 * axis.1, s2 * axis.2); + let (q0_2, q1_2, q2_2, q3_2) = (q0.powi(2), q1.powi(2), q2.powi(2), q3.powi(2)); + let (q01, q02, q03, q12, q13, q23) = (q0 * q1, q0 * q2, q0 * q3, q1 * q2, q1 * q3, q2 * q3); + + let x = vector.0 * (q0_2 + q1_2 - q2_2 - q3_2) + + 2.0 * (vector.1 * (q12 - q03) + vector.2 * (q02 + q13)); + let y = vector.1 * (q0_2 - q1_2 + q2_2 - q3_2) + + 2.0 * (vector.0 * (q03 + q12) + vector.2 * (q23 - q01)); + let z = vector.2 * (q0_2 - q1_2 - q2_2 + q3_2) + + 2.0 * (vector.0 * (q13 + q02) + vector.1 * (q01 + q23)); + + (x, y, z) +} + // This is only a subset of _Vec2 methods defined in arcade.math.py struct _Vec2 { x: f32, @@ -232,6 +264,11 @@ mod tests { use super::*; use float_eq::assert_float_eq; + fn round_float(n: f32, decimals: u32) -> f32 { + let nn = 10i32.pow(decimals) as f32; + (n * nn).round() / nn + } + #[test] fn test_clamp() { let mut result = clamp(1.2, 1.0, 2.0); @@ -257,14 +294,23 @@ mod tests { } #[test] - fn test_lerp_vec() { - let mut result = lerp_vec((0.0, 2.0), (8.0, 4.0), 0.25); + fn test_lerp_2d() { + let mut result = lerp_2d((0.0, 2.0), (8.0, 4.0), 0.25); assert_eq!(result, (2.0, 2.5)); - result = lerp_vec((0.0, 2.0), (8.0, 4.0), -0.25); + result = lerp_2d((0.0, 2.0), (8.0, 4.0), -0.25); assert_eq!(result, (-2.0, 1.5)); } + #[test] + fn test_lerp_3d() { + let mut result = lerp_3d((0.0, 2.0, 4.0), (8.0, 4.0, 8.0), 0.25); + assert_eq!(result, (2.0, 2.5, 5.0)); + + result = lerp_3d((0.0, 2.0, 4.0), (8.0, 4.0, 8.0), -0.25); + assert_eq!(result, (-2.0, 1.5, 3.0)); + } + #[test] fn test_lerp_angle_normal() { //normal @@ -392,4 +438,52 @@ mod tests { assert_eq!(result.x, 0.0); assert_eq!(result.y, 0.0); } + + #[test] + fn test_rotate_point() { + let mut x: f32 = 0.0; + let mut y: f32 = 0.0; + let mut cx: f32 = 0.0; + let mut cy: f32 = 0.0; + let mut angle: f32 = 0.0; + let mut point: (f32, f32) = rotate_point(x, y, cx, cy, angle); + assert_float_eq!(round_float(point.0, 2), 0.0, abs <= 1.0e-3); + assert_float_eq!(round_float(point.1, 2), 0.0, abs <= 1.0e-3); + + x = 0.0; + y = 0.0; + cx = 0.0; + cy = 0.0; + angle = 90.0; + point = rotate_point(x, y, cx, cy, angle); + assert_float_eq!(round_float(point.0, 2), 0.0, abs <= 1.0e-3); + assert_float_eq!(round_float(point.1, 2), 0.0, abs <= 1.0e-3); + + x = 50.0; + y = 50.0; + cx = 0.0; + cy = 0.0; + angle = 0.0; + point = rotate_point(x, y, cx, cy, angle); + assert_float_eq!(round_float(point.0, 2), 50.0, abs <= 1.0e-3); + assert_float_eq!(round_float(point.1, 2), 50.0, abs <= 1.0e-3); + + x = 50.0; + y = 0.0; + cx = 0.0; + cy = 0.0; + angle = 90.0; + point = rotate_point(x, y, cx, cy, angle); + assert_float_eq!(round_float(point.0, 2), 0.0, abs <= 1.0e-3); + assert_float_eq!(round_float(point.1, 2), -50.0, abs <= 1.0e-3); + + x = 20.0; + y = 10.0; + cx = 10.0; + cy = 10.0; + angle = 180.0; + point = rotate_point(x, y, cx, cy, angle); + assert_float_eq!(round_float(point.0, 2), 0.0, abs <= 1.0e-3); + assert_float_eq!(round_float(point.1, 2), 10.0, abs <= 1.0e-3); + } } diff --git a/src/sprite.rs b/src/sprite.rs deleted file mode 100644 index f355251..0000000 --- a/src/sprite.rs +++ /dev/null @@ -1,530 +0,0 @@ -use pyo3::types::{PyDict, PyTuple}; -use pyo3::{intern, prelude::*}; - -use crate::hitbox::HitBox; - -#[pyclass(subclass, module = "arcade.sprite.base")] -pub struct BasicSprite { - #[pyo3(get, name = "_texture")] - texture: PyObject, - position: (f32, f32), - #[pyo3(get, name = "_depth")] - depth: f32, - #[pyo3(get, name = "_scale")] - scale: (f32, f32), - #[pyo3(get, name = "_width")] - width: f32, - #[pyo3(get, name = "_height")] - height: f32, - hitbox: HitBox, - #[pyo3(get, name = "_color")] - color: (u8, u8, u8, u8), - sprite_lists: Vec, - #[pyo3(get, name = "_angle")] - angle: f32, -} - -#[pymethods] -impl BasicSprite { - #[new] - #[pyo3(signature = (texture, scale=1.0, center_x=0.0, center_y=0.0, **_kwargs))] - fn new( - py: Python<'_>, - texture: PyObject, - scale: Option, - center_x: Option, - center_y: Option, - _kwargs: Option<&PyDict>, - ) -> Self { - let final_scale = (scale.unwrap_or(1.0), scale.unwrap_or(1.0)); - let final_position = (center_x.unwrap_or(0.0), center_y.unwrap_or(0.0)); - let points: Vec<(f32, f32)> = texture - .getattr(py, intern!(py, "hit_box_points")) - .unwrap() - .extract(py) - .expect("Failed to Load Hit Box Points from Texture"); - let mut width: f32 = texture - .getattr(py, intern!(py, "width")) - .unwrap() - .extract(py) - .expect("Failed to Load Width from Texture"); - let mut height: f32 = texture - .getattr(py, intern!(py, "height")) - .unwrap() - .extract(py) - .expect("Failed to Load Height From Texture"); - width *= final_scale.0; - height *= final_scale.1; - - BasicSprite { - texture, - scale: final_scale, - position: final_position, - depth: 0.0, - width, - height, - color: (255, 255, 255, 255), - hitbox: HitBox { - points, - position: final_position, - scale: final_scale, - angle: 0.0, - }, - sprite_lists: Vec::new(), - angle: 0.0, - } - } - - #[getter] - fn get_position(&self) -> PyResult<(f32, f32)> { - Ok(self.position) - } - - #[setter] - fn set_position( - mut self_: PyRefMut<'_, BasicSprite>, - py: Python<'_>, - new_value: (f32, f32), - ) -> PyResult<()> { - if new_value == self_.position { - return Ok(()); - } - - self_.position = new_value; - self_.hitbox.position = new_value; - - let sprite_lists = self_.sprite_lists.clone(); - let s = Py::from(self_); - for sprite_list in sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_position"), (&s,))?; - } - - Ok(()) - } - - #[getter] - fn get_scale(&self) -> PyResult { - Ok(self.scale.0) - } - - #[setter] - fn set_scale(self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - BasicSprite::set_scalexy(self_, py, (new_value, new_value)) - } - - #[getter] - fn get_scalexy(&self) -> PyResult<(f32, f32)> { - Ok(self.scale) - } - - #[setter] - fn set_scalexy( - mut self_: PyRefMut, - py: Python<'_>, - new_value: (f32, f32), - ) -> PyResult<()> { - if new_value == self_.scale { - return Ok(()); - } - - self_.scale = new_value; - self_.hitbox.scale = new_value; - - let tex_width: f32 = self_ - .texture - .getattr(py, intern!(py, "width")) - .unwrap() - .extract(py) - .expect("Failed to Load Width From Texture"); - - let tex_height: f32 = self_ - .texture - .getattr(py, intern!(py, "height")) - .unwrap() - .extract(py) - .expect("Failed to Load Height From Texture"); - - self_.width = tex_width * self_.scale.0; - self_.height = tex_height * self_.scale.1; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_size"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_depth(&self) -> PyResult { - Ok(self.depth) - } - - #[setter] - fn set_depth(mut self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - if new_value == self_.depth { - return Ok(()); - } - - self_.depth = new_value; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_depth"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_texture(&self, py: Python<'_>) -> PyResult { - Ok(self.texture.clone_ref(py)) - } - - #[setter] - fn set_texture( - mut self_: PyRefMut, - py: Python<'_>, - new_value: PyObject, - ) -> PyResult<()> { - if new_value.is(&self_.texture) { - return Ok(()); - } - - self_.texture = new_value; - - let new_width: f32 = self_ - .texture - .getattr(py, intern!(py, "width")) - .unwrap() - .extract(py) - .expect("Failed to Load Width from Texture"); - self_.width = new_width * self_.scale.0; - - let new_height: f32 = self_ - .texture - .getattr(py, intern!(py, "height")) - .unwrap() - .extract(py) - .expect("Failed to Load Height From Texture"); - self_.height = new_height * self_.scale.1; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_texture"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_color(&self) -> PyResult<(u8, u8, u8, u8)> { - Ok(self.color) - } - - #[setter] - fn set_color( - mut self_: PyRefMut, - py: Python<'_>, - new_value: Vec, - ) -> PyResult<()> { - let new_color: (u8, u8, u8, u8) = match new_value.len() { - 4 => (new_value[0], new_value[1], new_value[2], new_value[3]), - 3 => (new_value[0], new_value[1], new_value[2], self_.color.3), - _ => panic!("Color must be 3 or 4 ints from 0-255"), - }; - - if new_color == self_.color { - return Ok(()); - } - - self_.color = new_color; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_color"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_alpha(&self) -> PyResult { - Ok(self.color.3) - } - - #[setter] - fn set_alpha(mut self_: PyRefMut, py: Python<'_>, new_value: u8) -> PyResult<()> { - if self_.color.3 == new_value { - return Ok(()); - } - - self_.color.3 = new_value; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_color"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_visible(&self) -> PyResult { - Ok(self.color.3 > 0) - } - - #[setter] - fn set_visible( - mut self_: PyRefMut, - py: Python<'_>, - new_value: bool, - ) -> PyResult<()> { - self_.color.3 = if new_value { 255 } else { 0 }; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_color"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_width(&self) -> PyResult { - Ok(self.width) - } - - #[setter] - fn set_width(mut self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - if self_.width == new_value { - return Ok(()); - } - - let tex_width: f32 = self_ - .texture - .getattr(py, intern!(py, "width")) - .unwrap() - .extract(py) - .expect("Failed to Load Width From Texture"); - - self_.scale = (new_value / tex_width, self_.scale.1); - self_.width = new_value; - self_.hitbox.scale = self_.scale; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_width"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_height(&self) -> PyResult { - Ok(self.height) - } - - #[setter] - fn set_height( - mut self_: PyRefMut, - py: Python<'_>, - new_value: f32, - ) -> PyResult<()> { - if self_.height == new_value { - return Ok(()); - } - - let tex_height: f32 = self_ - .texture - .getattr(py, intern!(py, "height")) - .unwrap() - .extract(py) - .expect("Failed to Load Height From Texture"); - - self_.scale = (self_.scale.0, new_value / tex_height); - self_.height = new_value; - self_.hitbox.scale = self_.scale; - - for sprite_list in self_.sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_height"), (&self_,))?; - } - - Ok(()) - } - - #[getter] - fn get_center_x(&self) -> PyResult { - Ok(self.position.0) - } - - #[setter] - fn set_center_x(self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - let y = self_.position.1; - BasicSprite::set_position(self_, py, (new_value, y)) - } - - #[getter] - fn get_center_y(&self) -> PyResult { - Ok(self.position.1) - } - - #[setter] - fn set_center_y(self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - let x = self_.position.0; - BasicSprite::set_position(self_, py, (x, new_value)) - } - - #[getter] - fn get_left(&self) -> PyResult { - Ok(self.hitbox.left_native()) - } - - #[setter] - fn set_left(self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - let s = &*self_; - let leftmost = s.hitbox.left_native(); - let diff = new_value - leftmost; - let x = s.position.0; - BasicSprite::set_center_x(self_, py, x + diff) - } - - #[getter] - fn get_right(&self) -> PyResult { - Ok(self.hitbox.right_native()) - } - - #[setter] - fn set_right(self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - let rightmost = self_.hitbox.right_native(); - let diff = rightmost - new_value; - let x = self_.position.0; - BasicSprite::set_center_x(self_, py, x - diff) - } - - #[getter] - fn get_bottom(&self) -> PyResult { - Ok(self.hitbox.bottom_native()) - } - - #[setter] - fn set_bottom(self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - let lowest = self_.hitbox.bottom_native(); - let diff = lowest - new_value; - let y = self_.position.1; - BasicSprite::set_center_y(self_, py, y - diff) - } - - #[getter] - fn get_top(&self) -> PyResult { - Ok(self.hitbox.top_native()) - } - - #[setter] - fn set_top(self_: PyRefMut, py: Python<'_>, new_value: f32) -> PyResult<()> { - let highest = self_.hitbox.top_native(); - let diff = highest - new_value; - let y = self_.position.1; - BasicSprite::set_center_y(self_, py, y - diff) - } - - fn update_spatial_hash(self_: PyRef, py: Python<'_>) -> PyResult<()> { - for sprite_list in self_.sprite_lists.iter() { - let spatial_hash: PyObject = sprite_list - .getattr(py, intern!(py, "spatial_hash")) - .unwrap() - .extract(py) - .unwrap(); - - if spatial_hash.is(&py.None()) { - return Ok(()); - } - - spatial_hash.call_method1(py, intern!(py, "move"), (&self_,))?; - } - - Ok(()) - } - - fn register_sprite_list(&mut self, new_list: PyObject) { - self.sprite_lists.push(new_list); - } - - fn remove_from_sprite_lists(mut self_: PyRefMut, py: Python<'_>) -> PyResult<()> { - while !self_.sprite_lists.is_empty() { - self_.sprite_lists[0].call_method1(py, intern!(py, "remove"), (&self_,))?; - } - - self_.sprite_lists.clear(); - - Ok(()) - } - - fn update(&self) {} -} - -#[derive(FromPyObject)] -enum PathOrTexture { - First(String), - Second(PyObject), -} - -#[pyclass(subclass, extends=BasicSprite, module="arcade.sprite.sprite")] -pub struct Sprite {} - -#[pymethods] -impl Sprite { - #[new] - #[pyo3(signature = (path_or_texture, scale=1.0, center_x=0.0, center_y=0.0, angle=0.0, **_kwargs))] - fn new( - py: Python<'_>, - path_or_texture: PathOrTexture, - scale: Option, - center_x: Option, - center_y: Option, - angle: Option, - _kwargs: Option<&PyDict>, - ) -> (Self, BasicSprite) { - let texture: PyObject = match path_or_texture { - PathOrTexture::First(path_string) => { - let arcade = PyModule::import(py, "arcade").expect("Failed to import arcade"); - arcade - .getattr(intern!(py, "load_texture")) - .expect("No arcade.load_texture function found") - .call1(PyTuple::new(py, vec![path_string])) - .expect("Failed to execute arcade.load_texture") - .extract() - .expect("Failed to extract PyObject from arcade.load_texture") - } - PathOrTexture::Second(object) => { - let cls: &str = object.clone().into_ref(py).get_type().name().unwrap(); - let final_object: PyObject = match cls { - "Texture" => object, - "Path" => panic!("Handle pathlib here"), - _ => panic!("Unknown Type Passed to sprite constructor"), - }; - final_object - } - }; - let mut basic = BasicSprite::new(py, texture, scale, center_x, center_y, _kwargs); - basic.angle = angle.unwrap_or(0.0); - (Self {}, basic) - } - - #[getter] - fn get_angle(self_: PyRef<'_, Self>) -> PyResult { - Ok(self_.into_super().angle) - } - - #[setter] - fn set_angle(mut self_: PyRefMut<'_, Self>, py: Python<'_>, new_value: f32) -> PyResult<()> { - let super_ = self_.as_mut(); - - if super_.angle == new_value { - return Ok(()); - } - - super_.angle = new_value; - super_.hitbox.angle = new_value; - - let sprite_lists = super_.sprite_lists.clone(); - for sprite_list in sprite_lists.iter() { - sprite_list.call_method1(py, intern!(py, "_update_height"), (&self_,))?; - } - - Ok(()) - } -} diff --git a/src/sprite_list.rs b/src/sprite_list.rs index cfd43c5..6777db8 100644 --- a/src/sprite_list.rs +++ b/src/sprite_list.rs @@ -1,5 +1,6 @@ -use crate::geometry::are_polygons_intersecting; -use crate::hitbox::{HitBox, RotatableHitBox}; +use crate::geometry::are_polygons_intersecting_native; +use crate::hitbox::{HitBox, NativeAdjustedPoints, RotatableHitBox}; +use pyo3::intern; use pyo3::prelude::*; #[pyfunction] @@ -7,70 +8,48 @@ pub fn check_for_collision_with_list( py: Python<'_>, sprite: &PyAny, // sprite_list: &PyAny, + method: Option, ) -> Vec { + let _final_method = method.unwrap_or(3); let mut final_sprites: Vec = Vec::new(); - let mut hitbox1: Option = None; - let mut hitbox2: Option> = None; - - let cls: &str = sprite - .getattr("_hit_box") - .unwrap() - .get_type() - .name() - .unwrap(); - - match cls { - "HitBox" => { - hitbox1 = sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - "RotatableHitBox" => { - hitbox2 = sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - _ => panic!(), - } - - let main_points: Vec<(f32, f32)> = if let Some(value) = hitbox1 { - value.get_adjusted_points_native() - } else if let Some(value) = hitbox2 { - RotatableHitBox::get_adjusted_points(value) + let main_points: &Vec<(f32, f32)>; + let mut hitbox1: HitBox; + let mut hitbox2: RotatableHitBox; + let hitbox_py_object: &PyAny = sprite.getattr(intern!(py, "_hit_box")).unwrap(); + + if hitbox_py_object.is_instance_of::() { + hitbox1 = hitbox_py_object.extract::().unwrap(); + main_points = hitbox1.get_adjusted_points_native(); + } else if hitbox_py_object.is_instance_of::() { + hitbox2 = hitbox_py_object.extract::().unwrap(); + main_points = hitbox2.get_adjusted_points_native(); } else { - panic!("unknown hitbox type"); - }; + panic!("Unknown Hitbox Type") + } - let sprite_list_list = sprite_list.getattr("sprite_list").unwrap(); + let sprite_list_list = sprite_list.getattr(intern!(py, "sprite_list")).unwrap(); let sprites_to_check: Vec = sprite_list_list.extract().unwrap(); for sprite2 in sprites_to_check.iter() { let other_sprite: &PyAny = sprite2.as_ref(py); - let mut other_hitbox1: Option = None; - let mut other_hitbox2: Option> = None; - let other_cls: &str = other_sprite - .getattr("_hit_box") - .unwrap() - .get_type() - .name() - .unwrap(); - - match other_cls { - "HitBox" => { - other_hitbox1 = other_sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - "RotatableHitBox" => { - other_hitbox2 = other_sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - _ => panic!(), - } - let other_points: Vec<(f32, f32)> = if let Some(value) = other_hitbox1 { - value.get_adjusted_points_native() - } else if let Some(value) = other_hitbox2 { - RotatableHitBox::get_adjusted_points(value) + let other_points: &Vec<(f32, f32)>; + let mut other_hitbox1: HitBox; + let mut other_hitbox2: RotatableHitBox; + let other_hitbox_py_object: &PyAny = other_sprite.getattr(intern!(py, "_hit_box")).unwrap(); + + if other_hitbox_py_object.is_instance_of::() { + other_hitbox1 = other_hitbox_py_object.extract::().unwrap(); + other_points = other_hitbox1.get_adjusted_points_native(); + } else if other_hitbox_py_object.is_instance_of::() { + other_hitbox2 = other_hitbox_py_object.extract::().unwrap(); + other_points = other_hitbox2.get_adjusted_points_native(); } else { - panic!("unknown hitbox type"); - }; + panic!("Unknown Hitbox Type") + } - let check_2 = are_polygons_intersecting(main_points.to_vec(), other_points); + let check_2 = are_polygons_intersecting_native(main_points, other_points); if check_2 { final_sprites.push(sprite2.to_object(py)); @@ -87,67 +66,46 @@ pub fn check_for_collision_with_lists( sprite_lists: Vec<&PyAny>, ) -> Vec { let mut final_sprites: Vec = Vec::new(); - let mut hitbox1: Option = None; - let mut hitbox2: Option> = None; - - let cls: &str = sprite - .getattr("_hit_box") - .unwrap() - .get_type() - .name() - .unwrap(); - - match cls { - "HitBox" => { - hitbox1 = sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - "RotatableHitBox" => { - hitbox2 = sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - _ => panic!(), - } - let main_points: Vec<(f32, f32)> = if let Some(value) = hitbox1 { - value.get_adjusted_points_native() - } else if let Some(value) = hitbox2 { - RotatableHitBox::get_adjusted_points(value) + let main_points: &Vec<(f32, f32)>; + let mut hitbox1: HitBox; + let mut hitbox2: RotatableHitBox; + let hitbox_py_object: &PyAny = sprite.getattr(intern!(py, "_hit_box")).unwrap(); + + if hitbox_py_object.is_instance_of::() { + hitbox1 = hitbox_py_object.extract::().unwrap(); + main_points = hitbox1.get_adjusted_points_native(); + } else if hitbox_py_object.is_instance_of::() { + hitbox2 = hitbox_py_object.extract::().unwrap(); + main_points = hitbox2.get_adjusted_points_native(); } else { - panic!("unknown hitbox type") - }; + panic!("Unknown Hitbox Type") + } for sprite_list in sprite_lists.iter() { - let sprite_list_list = sprite_list.getattr("sprite_list").unwrap(); + let sprite_list_list = sprite_list.getattr(intern!(py, "sprite_list")).unwrap(); let sprites_to_check: Vec = sprite_list_list.extract().unwrap(); for sprite2 in sprites_to_check.iter() { let other_sprite: &PyAny = sprite2.as_ref(py); - let mut other_hitbox1: Option = None; - let mut other_hitbox2: Option> = None; - let other_cls: &str = other_sprite - .getattr("_hit_box") - .unwrap() - .get_type() - .name() - .unwrap(); - match other_cls { - "HitBox" => { - other_hitbox1 = other_sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - "RotatableHitBox" => { - other_hitbox2 = other_sprite.getattr("_hit_box").unwrap().extract().unwrap(); - } - _ => panic!(), - } - let other_points: Vec<(f32, f32)> = if let Some(value) = other_hitbox1 { - value.get_adjusted_points_native() - } else if let Some(value) = other_hitbox2 { - RotatableHitBox::get_adjusted_points(value) + let other_points: &Vec<(f32, f32)>; + let mut other_hitbox1: HitBox; + let mut other_hitbox2: RotatableHitBox; + let other_hitbox_py_object: &PyAny = + other_sprite.getattr(intern!(py, "_hit_box")).unwrap(); + + if other_hitbox_py_object.is_instance_of::() { + other_hitbox1 = other_hitbox_py_object.extract::().unwrap(); + other_points = other_hitbox1.get_adjusted_points_native(); + } else if other_hitbox_py_object.is_instance_of::() { + other_hitbox2 = other_hitbox_py_object.extract::().unwrap(); + other_points = other_hitbox2.get_adjusted_points_native(); } else { - panic!("unknown hitbox type"); - }; + panic!("Unknown Hitbox Type") + } - let check_2 = are_polygons_intersecting(main_points.to_vec(), other_points); + let check_2 = are_polygons_intersecting_native(main_points, other_points); if check_2 { final_sprites.push(sprite2.to_object(py));