├── test ├── __init__.py └── test_geometry.py ├── requirements.txt ├── .github ├── dependabot.yml └── workflows │ ├── black.yml │ ├── cppcheck.yml │ ├── windows_test.yml │ ├── macos_test.yml │ └── ubuntu_test.yml ├── src_c ├── .editorconfig ├── .clang-format ├── simd_collisions.h ├── include │ ├── collisions.h │ ├── geometry.h │ ├── pygame.h │ └── base.h ├── simd_collisions_avx2.c └── collisions.c ├── examples ├── polygon_subscript.py ├── CONTRIBUTING.md ├── line_as_points.py ├── polygon_convex_visualization.py ├── pong.py ├── raycast.py ├── polygon_circle_collision.py ├── polygon_scale_visualization.py ├── polygon_line_collision.py ├── circle_collisions_visualization.py ├── regular_polygon.py └── circle_collision_game.py ├── CONTRIBUTING.md ├── README.md ├── .gitignore ├── setup.py ├── CODE_OF_CONDUCT.md ├── benchmarks ├── benchmark_utils.py ├── GEOMETRY_polygon_benchmark.py ├── GEOMETRY_line_benchmark.py └── GEOMETRY_circle_benchmark.py ├── geometry.pyi └── docs ├── geometry.rst └── polygon.rst /test/__init__.py: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | pygame-ce>=2.1.3 -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | # Check for updates to GitHub Actions every week 7 | interval: "weekly" -------------------------------------------------------------------------------- /.github/workflows/black.yml: -------------------------------------------------------------------------------- 1 | name: Python Code Quality (Black) 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | lint: 7 | runs-on: ubuntu-latest 8 | steps: 9 | - uses: actions/checkout@v4 10 | - uses: psf/black@stable 11 | -------------------------------------------------------------------------------- /src_c/.editorconfig: -------------------------------------------------------------------------------- 1 | root=true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | 7 | # The settings for C (*.c and *.h) files are mirrored in src_c/.clang-format. 8 | # Keep them in sync. 9 | [*.{c,h}] 10 | indent_style = space 11 | indent_size = 4 12 | tab_width = 4 13 | trim_trailing_whitespace = true 14 | max_line_length = 79 15 | 16 | [*.{py,pyx}] 17 | indent_style = space 18 | indent_size = 4 19 | charset = utf-8 20 | file_type_emacs = python 21 | trim_trailing_whitespace = true 22 | max_line_length = 79 23 | -------------------------------------------------------------------------------- /src_c/.clang-format: -------------------------------------------------------------------------------- 1 | # PEP 7 2 | BasedOnStyle: Google 3 | AllowShortLoopsOnASingleLine: false 4 | AllowShortIfStatementsOnASingleLine: false 5 | AlwaysBreakAfterReturnType: All 6 | AlignAfterOpenBracket: Align 7 | BreakBeforeBraces: Stroustrup 8 | ColumnLimit: 79 9 | DerivePointerAlignment: false 10 | # These settings are mirrored in .editorconfig. Keep them in sync. 11 | IndentWidth: 4 12 | Language: Cpp 13 | PointerAlignment: Right 14 | ReflowComments: true 15 | SpaceBeforeParens: ControlStatements 16 | SpacesInParentheses: false 17 | SortIncludes: false 18 | TabWidth: 4 19 | UseTab: Never 20 | -------------------------------------------------------------------------------- /.github/workflows/cppcheck.yml: -------------------------------------------------------------------------------- 1 | name: C++ Code Quality 2 | 3 | # Run cppcheck on src_c changes to main branch, or any PR to main. 4 | on: 5 | push: 6 | branches: main 7 | paths: 8 | - 'src_c/**' 9 | 10 | pull_request: 11 | branches: main 12 | paths: 13 | - 'src_c/**' 14 | 15 | jobs: 16 | run-cppcheck: 17 | runs-on: ubuntu-latest 18 | 19 | steps: 20 | - uses: actions/checkout@v4 21 | 22 | - name: Install deps 23 | run: | 24 | sudo apt-mark hold grub-efi-amd64-signed 25 | sudo apt-get update --fix-missing 26 | sudo apt-get upgrade 27 | sudo apt install cppcheck 28 | - name: Run Static Checker 29 | run: cppcheck src_c --force --enable=performance,portability,warning 30 | -------------------------------------------------------------------------------- /.github/workflows/windows_test.yml: -------------------------------------------------------------------------------- 1 | name: Windows latest 2 | 3 | on: [ push, pull_request ] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | Python: 11 | runs-on: windows-latest 12 | strategy: 13 | fail-fast: false # if a particular matrix build fails, don't skip the rest 14 | matrix: 15 | python-version: [3.8, 3.9, '3.10', 3.11, 3.12] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install packages 19 | run: | 20 | py -${{ matrix.python-version }} -m pip install --upgrade pip 21 | py -${{ matrix.python-version }} -m pip install wheel 22 | py -${{ matrix.python-version }} -m pip install pygame-ce 23 | py -${{ matrix.python-version }} -m pip install . 24 | - name: Run Tests 25 | run: py -${{ matrix.python-version }} -m unittest 26 | 27 | -------------------------------------------------------------------------------- /examples/polygon_subscript.py: -------------------------------------------------------------------------------- 1 | import geometry 2 | from random import randint 3 | import pygame 4 | 5 | screen = pygame.display.set_mode((800, 800)) 6 | clock = pygame.time.Clock() 7 | 8 | polygon = geometry.regular_polygon(10, (400, 400), 300, 23) 9 | index = 0 10 | running = True 11 | color = (255, 0, 0) 12 | while running: 13 | screen.fill((0, 0, 0)) 14 | 15 | for event in pygame.event.get(): 16 | if event.type == pygame.QUIT: 17 | running = False 18 | pygame.quit() 19 | 20 | elif event.type == pygame.MOUSEWHEEL: 21 | color = (randint(30, 255), randint(30, 255), randint(30, 255)) 22 | if event.y > 0: 23 | index = (index + 1) % polygon.verts_num 24 | 25 | else: 26 | index = (index - 1) % polygon.verts_num 27 | 28 | pygame.draw.polygon(screen, color, polygon.vertices, 4) 29 | 30 | polygon[index] = pygame.mouse.get_pos() 31 | 32 | pygame.display.update() 33 | clock.tick(60) 34 | -------------------------------------------------------------------------------- /.github/workflows/macos_test.yml: -------------------------------------------------------------------------------- 1 | name: macOS latest 2 | 3 | on: [ push, pull_request ] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | jobs: 10 | Python: 11 | runs-on: macos-latest 12 | strategy: 13 | fail-fast: false # if a particular matrix build fails, don't skip the rest 14 | matrix: 15 | python-version: [3.8, 3.9, '3.10', 3.11, 3.12] 16 | steps: 17 | - uses: actions/checkout@v4 18 | - name: Install Python 19 | uses: actions/setup-python@v5 20 | with: 21 | python-version: ${{ matrix.python-version }} 22 | - name: Install dependencies 23 | run: | 24 | brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf pkg-config 25 | python -m pip install --upgrade pip 26 | python -m pip install wheel 27 | python -m pip install pygame-ce 28 | python -m pip install . 29 | - name: Run Tests 30 | run: | 31 | python -m unittest -------------------------------------------------------------------------------- /.github/workflows/ubuntu_test.yml: -------------------------------------------------------------------------------- 1 | name: Ubuntu latest 2 | 3 | on: [ push, pull_request ] 4 | 5 | concurrency: 6 | group: ${{ github.workflow }}-${{ github.ref }} 7 | cancel-in-progress: true 8 | 9 | 10 | jobs: 11 | Python: 12 | runs-on: ubuntu-latest 13 | strategy: 14 | fail-fast: false # if a particular matrix build fails, don't skip the rest 15 | matrix: 16 | python-version: [3.8, 3.9, '3.10', 3.11, 3.12] 17 | steps: 18 | - uses: actions/checkout@v4 19 | - name: Install Python 20 | uses: actions/setup-python@v5 21 | with: 22 | python-version: ${{ matrix.python-version }} 23 | - name: Install dependencies 24 | run: | 25 | sudo apt-get update --fix-missing 26 | sudo apt-get install -y libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev pkg-config 27 | python -m pip install --upgrade pip 28 | python -m pip install wheel 29 | python -m pip install pygame-ce 30 | python -m pip install . 31 | - name: Run Tests 32 | run: | 33 | python -m unittest -------------------------------------------------------------------------------- /examples/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing Guidelines 2 | 3 | 1. See [Contributing Guidelines](https://github.com/pygame-community/pygame_geometry/blob/main/CONTRIBUTING.md) for general contributing guidelines. 4 | 2. Use this boilerplate to get started 5 | 6 | ```python 7 | """ 8 | 9 | This serves as a boilerplate for pygame_geometry. 10 | You must use this boilerplate in order to create 11 | any official pygame_geometry examples. 12 | 13 | @authors: andrewhong@myyahoo.com 14 | 15 | """ 16 | 17 | import pygame 18 | import pygame_geometry 19 | 20 | ### == Configuration 21 | 22 | DISPLAY_NAME = "pygame_geometry examples boilerplate" 23 | PREFERRED_FPS = 60 24 | WIDTH = 800 25 | HEIGHT = 800 26 | 27 | ### == Game 28 | 29 | pygame.init() 30 | clock = pygame.time.Clock() 31 | screen = pygame.display.set_mode((WIDTH, HEIGHT)) 32 | pygame.display.set_caption(DISPLAY_NAME) 33 | 34 | running = True 35 | while running: 36 | for event in pygame.event.get(): 37 | if event.type == pygame.QUIT: 38 | running = False 39 | 40 | screen.fill((0, 0, 0)) 41 | 42 | pygame.display.flip() 43 | delta_time = clock.tick(PREFERRED_FPS) / 1000 / PREFERRED_FPS 44 | 45 | pygame.quit() 46 | 47 | ``` 48 | -------------------------------------------------------------------------------- /src_c/simd_collisions.h: -------------------------------------------------------------------------------- 1 | #include "include/pygame.h" 2 | #include "include/collisions.h" 3 | #include 4 | 5 | #if (defined(__AVX2__) || defined(__SSE2__)) && defined(HAVE_IMMINTRIN_H) 6 | #include 7 | #endif 8 | 9 | #if !defined(PG_ENABLE_ARM_NEON) && defined(__aarch64__) 10 | // arm64 has neon optimisations enabled by default, even when fpu=neon is not 11 | // passed 12 | #define PG_ENABLE_ARM_NEON 1 13 | #endif 14 | 15 | #if defined(__AVX2__) && defined(HAVE_IMMINTRIN_H) && \ 16 | !defined(SDL_DISABLE_IMMINTRIN_H) 17 | #define AVX2_IS_SUPPORTED 1 18 | #else 19 | #define AVX2_IS_SUPPORTED 0 20 | #endif /* ~defined(__AVX2__) && defined(HAVE_IMMINTRIN_H) && \ 21 | !defined(SDL_DISABLE_IMMINTRIN_H) */ 22 | 23 | PG_FORCEINLINE static int 24 | pg_HasAVX2(void); 25 | 26 | #ifdef AVX2_IS_SUPPORTED 27 | PG_FORCEINLINE static int 28 | pgIntersection_LineRect_avx2(pgLineBase *line, SDL_Rect *rect, double *X, 29 | double *Y, double *T); 30 | PG_FORCEINLINE static int 31 | pgCollision_RectLine_avx2(SDL_Rect *rect, pgLineBase *line); 32 | PG_FORCEINLINE static int 33 | pgRaycast_LineRect_avx2(pgLineBase *line, SDL_Rect *rect, double max_t, 34 | double *T); 35 | #endif -------------------------------------------------------------------------------- /examples/line_as_points.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from geometry import * 3 | from pygame.draw import circle as draw_circle, line as draw_line 4 | 5 | WIDTH, HEIGHT = 800, 600 6 | WIDTH2, HEIGHT2 = WIDTH / 2, HEIGHT / 2 7 | pygame.init() 8 | screen = pygame.display.set_mode((WIDTH, HEIGHT)) 9 | clock = pygame.time.Clock() 10 | keep = True 11 | N = 50 12 | # Create a line 13 | line = Line((WIDTH / 4, HEIGHT2), (3 * WIDTH / 4, HEIGHT2)) 14 | 15 | while keep: 16 | screen.fill((30, 30, 30)) 17 | # Draw the line 18 | # draw_line(screen, (255, 255, 255), line.a, line.b, 2) 19 | # Draw the points 20 | for i, segment in enumerate(line.as_segments(N)): 21 | draw_line( 22 | screen, 23 | ((100 + 45 * (i + 1)) % 255, 15 * (i + 1) % 255, (135 * (i + 1)) % 255), 24 | segment.a, 25 | segment.b, 26 | 3, 27 | ) 28 | for point in line.as_points(N - 1): 29 | draw_circle(screen, (255, 255, 255), point, 1) 30 | 31 | line.b = pygame.mouse.get_pos() 32 | 33 | pygame.display.flip() 34 | clock.tick(165) 35 | for event in pygame.event.get(): 36 | if event.type == pygame.QUIT: 37 | keep = False 38 | if event.type == pygame.MOUSEWHEEL: 39 | if event.y > 0: 40 | N += 1 41 | else: 42 | if N > 2: 43 | N -= 1 44 | -------------------------------------------------------------------------------- /src_c/include/collisions.h: -------------------------------------------------------------------------------- 1 | // pgCollision_* tells you if two objects are colliding. 2 | // pgIntersection_* tells you if two objects are colliding and if where. 3 | // pgIntersection_*'s three final parameters are pointers to X, optional, Y, 4 | // optional, T, optional. 5 | 6 | #ifndef _PG_COLLISIONS_H 7 | #define _PG_COLLISIONS_H 8 | 9 | #include "pygame.h" 10 | #include "geometry.h" 11 | 12 | static int 13 | pgCollision_LineLine(pgLineBase *, pgLineBase *); 14 | static int 15 | pgIntersection_LineLine(pgLineBase *, pgLineBase *, double *, double *, 16 | double *); 17 | static int 18 | pgCollision_LinePoint(pgLineBase *, double, double); 19 | static int 20 | pgCollision_CirclePoint(pgCircleBase *circle, double, double); 21 | static int 22 | pgCollision_LineCircle(pgLineBase *, pgCircleBase *); 23 | static int 24 | pgIntersection_LineCircle(pgLineBase *, pgCircleBase *, double *, double *, 25 | double *); 26 | static int 27 | pgCollision_CircleCircle(pgCircleBase *, pgCircleBase *); 28 | static int 29 | pgIntersection_LineRect(pgLineBase *, SDL_Rect *, double *, double *, 30 | double *); 31 | static int 32 | pgCollision_RectLine(SDL_Rect *, pgLineBase *); 33 | static int 34 | pgCollision_RectCircle(SDL_Rect *, pgCircleBase *); 35 | 36 | static int 37 | pgRaycast_LineLine(pgLineBase *, pgLineBase *, double, double *); 38 | static int 39 | pgRaycast_LineRect(pgLineBase *, SDL_Rect *, double, double *); 40 | static int 41 | pgRaycast_LineCircle(pgLineBase *, pgCircleBase *, double, double *); 42 | 43 | static int 44 | pgCollision_PolygonPoint(pgPolygonBase *, double, double); 45 | 46 | static int 47 | pgCollision_PolygonLine(pgPolygonBase *, pgLineBase *, int); 48 | static int 49 | pgCollision_CirclePolygon(pgCircleBase *, pgPolygonBase *, int); 50 | 51 | static int 52 | pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, 53 | double *intersections); 54 | 55 | #endif /* ~_PG_COLLISIONS_H */ 56 | -------------------------------------------------------------------------------- /examples/polygon_convex_visualization.py: -------------------------------------------------------------------------------- 1 | from geometry import regular_polygon 2 | import pygame 3 | 4 | pygame.init() 5 | 6 | SCREEN_W = 800 7 | SCREEN_H = 600 8 | FPS = 60 9 | screen = pygame.display.set_mode((SCREEN_W, SCREEN_H)) 10 | pygame.display.set_caption("Polygon Convex Visualization") 11 | clock = pygame.time.Clock() 12 | keep = True 13 | 14 | GREEN = (0, 150, 0) 15 | RED = (200, 0, 0) 16 | WHITE = (255, 255, 255) 17 | YELLOW = (255, 255, 0) 18 | BACKGROUND = (10, 10, 10) 19 | current_color = GREEN 20 | 21 | polygon = regular_polygon(5, (SCREEN_W / 2, SCREEN_H / 2), 200) 22 | index = 0 23 | is_convex = True 24 | 25 | # setup text 26 | font = pygame.font.SysFont("Arial", 25, bold=True) 27 | 28 | is_convex_text = font.render("Convex polygon", True, WHITE) 29 | is_convex_text_r = is_convex_text.get_rect(center=(SCREEN_W / 2, 50)) 30 | is_not_convex_text = font.render("Concave polygon", True, WHITE) 31 | is_not_convex_text_r = is_not_convex_text.get_rect(center=(SCREEN_W / 2, 50)) 32 | scroll_text = font.render("Scroll wheel to select vertex", True, WHITE) 33 | scroll_text_r = scroll_text.get_rect(center=(SCREEN_W / 2, SCREEN_H - 50)) 34 | 35 | while keep: 36 | screen.fill(BACKGROUND) 37 | 38 | polygon[index] = pygame.mouse.get_pos() 39 | 40 | is_convex = polygon.is_convex() 41 | 42 | pygame.draw.polygon(screen, GREEN if is_convex else RED, polygon.vertices) 43 | 44 | for vertex in polygon.vertices: 45 | pygame.draw.circle(screen, WHITE, vertex, 5, 3) 46 | 47 | pygame.draw.circle(screen, YELLOW, polygon[index], 15, 3) 48 | 49 | if is_convex: 50 | screen.blit(is_convex_text, is_convex_text_r) 51 | else: 52 | screen.blit(is_not_convex_text, is_not_convex_text_r) 53 | 54 | screen.blit(scroll_text, scroll_text_r) 55 | 56 | pygame.display.flip() 57 | 58 | clock.tick(FPS) 59 | 60 | for event in pygame.event.get(): 61 | if event.type == pygame.QUIT: 62 | keep = False 63 | elif event.type == pygame.MOUSEWHEEL: 64 | if event.y > 0: 65 | index = (index + 1) % polygon.verts_num 66 | else: 67 | index = (index - 1) % polygon.verts_num 68 | pygame.mouse.set_pos(polygon[index]) 69 | 70 | pygame.quit() 71 | -------------------------------------------------------------------------------- /examples/pong.py: -------------------------------------------------------------------------------- 1 | import geometry 2 | from random import randint 3 | import pygame 4 | 5 | pygame.font.init() 6 | 7 | W, H = 800, 600 8 | 9 | screen = pygame.display.set_mode((W, H)) 10 | clock = pygame.time.Clock() 11 | 12 | running = True 13 | 14 | paddle_a = pygame.Rect(20, H / 2 - 100 / 2, 20, 100) 15 | paddle_b = pygame.Rect(W - 20 - 20, H / 2 - 100 / 2, 20, 100) 16 | 17 | ball = geometry.Circle(W / 2, H / 2, 7) 18 | vel = pygame.Vector2(5, -5) 19 | 20 | a_points = 0 21 | b_points = 0 22 | 23 | font = pygame.font.Font(None, 50) 24 | 25 | while running: 26 | screen.fill((0, 0, 0)) 27 | 28 | for event in pygame.event.get(): 29 | if event.type == pygame.QUIT: 30 | running = False 31 | 32 | keys = pygame.key.get_pressed() 33 | 34 | if keys[pygame.K_w]: 35 | paddle_a.y -= 5 36 | elif keys[pygame.K_s]: 37 | paddle_a.y += 5 38 | 39 | if keys[pygame.K_UP]: 40 | paddle_b.y -= 5 41 | elif keys[pygame.K_DOWN]: 42 | paddle_b.y += 5 43 | 44 | if paddle_a.y <= 0: 45 | paddle_a.y = 0 46 | elif paddle_a.y + paddle_a.h >= H: 47 | paddle_a.y = H - paddle_a.h 48 | 49 | if paddle_b.y <= 0: 50 | paddle_b.y = 0 51 | elif paddle_b.y + paddle_b.h >= H: 52 | paddle_b.y = H - paddle_b.h 53 | 54 | ball.center += vel 55 | 56 | if ball.y - ball.r <= 0: 57 | vel.y *= -1 58 | if ball.y + ball.r >= H: 59 | vel.y *= -1 60 | 61 | if ball.x - ball.r <= 0: 62 | a_points += 1 63 | ball.center = W / 2, H / 2 64 | vel.x *= -1 65 | if ball.x + ball.r >= W: 66 | b_points += 1 67 | ball.center = W / 2, H / 2 68 | vel.x *= -1 69 | 70 | if ball.colliderect(paddle_a): 71 | vel.x *= -1 72 | ball.x += vel.x * 0.85 73 | 74 | if ball.colliderect(paddle_b): 75 | vel.x *= -1 76 | ball.x += vel.x * 0.85 77 | 78 | pygame.draw.rect(screen, "white", paddle_a) 79 | pygame.draw.rect(screen, "white", paddle_b) 80 | 81 | pygame.draw.circle(screen, "white", ball.center, ball.r) 82 | 83 | label = font.render(f"{a_points} | {b_points}", 1, "white") 84 | 85 | screen.blit(label, (W / 2 - label.get_width() / 2, 10)) 86 | 87 | pygame.display.update() 88 | clock.tick(60) 89 | 90 | pygame.quit() 91 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | In order to contribute to the project, you must follow these guidelines... 3 | 1. Follow PEP8 convention naming rules for C and Python 4 | 2. Lint your code with [black](https://github.com/psf/black) 5 | 3. Please make an issue first before submitting a pull request 6 | 4. Always add unittests when you're introducing something new 7 | 5. For C code, try to use `FASTCALL` as much as you can 8 | 6. Make sure your code actually works before you submit a pull 9 | request 10 | 7. Please be patient when waiting for pull request reviews, 11 | we are all unpaid-volunteers 12 | 13 | # Build From Source 14 | 15 | See below to see how to build pygame_geometry from source. If you need help, talk to us on [Discord](https://discord.gg/QzmpNXchW5). 16 | 17 | ## Windows 10 / Windows 11 18 | 1. Install Python 3.8+ 19 | 2. Install [Visual Studio Community 2022](https://visualstudio.microsoft.com/vs/community/) or [Visual Studio Build Tools 2017](https://aka.ms/vs/15/release/vs_buildtools.exe) and make sure you mark `MSVC v140 - VS 2015 C++ build tools (v14.00)` with the installation 20 | 3. Run `python -m pip install setuptools -U` 21 | 4. Install the latest version of [git](https://gitforwindows.org/) 22 | 5. Run `git clone https://github.com/pygame-community/pygame_geometry.git` 23 | 6. Run `cd pygame_geometry; python -m pip install .` 24 | 25 | **If you are having trouble re-compiling, try deleting the `build` folder from the root directory if it exists** 26 | 27 | ## MacOS 28 | 1. Install Python 3.8+ 29 | 2. Install [HomeBrew](https://brew.sh/) 30 | 3. Install dependencies 31 | ``` 32 | brew install sdl2 sdl2_image sdl2_mixer sdl2_ttf pkg-config 33 | ``` 34 | 4. Run `python3 -m pip install setuptools -U` 35 | 5. Run `git clone https://github.com/pygame-community/pygame_geometry.git` 36 | 6. Run `cd pygame_geometry; python3 -m pip install .` 37 | 38 | ## Linux (Debian-based Distributions) 39 | 1. Install Python 3.8+ (Should be provided in distributions based out of Debian 10 Buster or above) 40 | 2. Install dependencies 41 | ``` 42 | sudo apt install libsdl2-dev libsdl2-image-dev libsdl2-mixer-dev libsdl2-ttf-dev pkg-config -y 43 | ``` 44 | 3. Install git by `sudo apt install git -y` 45 | 4. Run `python3 -m pip install setuptools -U` 46 | 5. Run `git clone https://github.com/pygame-community/pygame_geometry.git` 47 | 6. Run `cd pygame_geometry; python3 -m pip install .` 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pygame_geometry 2 | [![PRS Welcome](https://img.shields.io/badge/PRs-welcome-brightgreen.svg)](https://github.com/andrewhong04/pygame_geometry/issues) 3 | [![Python Code Quality](https://github.com/andrewhong04/pygame_geometry/actions/workflows/black.yml/badge.svg)](https://github.com/andrewhong04/pygame_geometry/actions/workflows/black.yml) 4 | [![C++ Code Quality](https://github.com/andrewhong04/pygame_geometry/actions/workflows/cppcheck.yml/badge.svg)](https://github.com/andrewhong04/pygame_geometry/actions/workflows/cppcheck.yml) 5 | [![Ubuntu latest](https://github.com/andrewhong04/pygame_geometry/actions/workflows/ubuntu_test.yml/badge.svg)](https://github.com/andrewhong04/pygame_geometry/actions/workflows/ubuntu_test.yml) 6 | [![Windows latest](https://github.com/andrewhong04/pygame_geometry/actions/workflows/windows_test.yml/badge.svg)](https://github.com/andrewhong04/pygame_geometry/actions/workflows/windows_test.yml) 7 | [![MacOS latest](https://github.com/andrewhong04/pygame_geometry/actions/workflows/macos_test.yml/badge.svg)](https://github.com/andrewhong04/pygame_geometry/actions/workflows/macos_test.yml) 8 | ![Commits per week](https://img.shields.io/github/commit-activity/w/andrewhong04/pygame_geometry/main) 9 | [![Code style: black](https://img.shields.io/badge/code%20style-black-000000.svg)](https://github.com/psf/black) 10 | 11 | pygame_geometry is a free Python implementation of 12 | polygons, circles, lines, and raycasting. The main 13 | purpose of this repository is to help users integrate 14 | special colliders easier for their video game. 15 | 16 | **Everything you see in this repository is subject to change as 17 | this project is heavily in development.** The project is set to be migrated to the 18 | [official pygame-ce repository](https://github.com/pygame-community/pygame-ce). 19 | 20 | ## Installation (Python 3.8+) 21 | Please follow [this guide](https://github.com/pygame-community/pygame_geometry/blob/main/CONTRIBUTING.md) 22 | to install pygame_geometry. 23 | 24 | ## Help 25 | If you're getting started with this library, you can check the `docs` directory to see 26 | the documentation or the `examples` directory to see the use-case of this library. 27 | 28 | ## Credits 29 | Thanks to [Emc2356](https://github.com/Emc2356) and 30 | [itzpr3d4t0r](https://github.com/itzpr3d4t0r) for the 31 | majority of the `pygame.geometry` work. 32 | 33 | This project is under the supervision of 34 | [Starbuck5](https://github.com/Starbuck5) 35 | & [novial](https://github.com/andrewhong04). 36 | -------------------------------------------------------------------------------- /examples/raycast.py: -------------------------------------------------------------------------------- 1 | import geometry 2 | 3 | import pygame 4 | import random 5 | import math 6 | 7 | screen = pygame.display.set_mode((800, 800)) 8 | 9 | collisions_lines = [] 10 | collisions_circles = [] 11 | collisions_rects = [] 12 | 13 | 14 | def generate_random_lines(amt): 15 | def random_pos(): 16 | return random.randrange(0, 800), random.randrange(0, 800) 17 | 18 | for x in range(amt): 19 | line = geometry.Line(random_pos(), random_pos()) 20 | collisions_lines.append(line) 21 | 22 | 23 | def generate_random_circles(amt): 24 | def random_pos(rad): 25 | return random.randrange(rad, 800 - rad), random.randrange(rad, 800 - rad) 26 | 27 | for x in range(amt): 28 | radius = random.randrange(5, 50) 29 | circle = geometry.Circle(*random_pos(radius), radius) 30 | collisions_circles.append(circle) 31 | 32 | 33 | def generate_random_rect(amt): 34 | def random_pos(width, height): 35 | return random.randrange(0, 800 - width), random.randrange(0, 800 - height) 36 | 37 | for x in range(amt): 38 | width = random.randrange(5, 50) 39 | height = random.randrange(5, 50) 40 | rect = pygame.Rect(random_pos(width, height), (width, height)) 41 | collisions_rects.append(rect) 42 | 43 | 44 | generate_random_lines(15) 45 | generate_random_circles(15) 46 | generate_random_rect(15) 47 | 48 | colliders = collisions_lines + collisions_circles + collisions_rects 49 | 50 | running = True 51 | 52 | ray_count = 360 53 | 54 | while running: 55 | for e in pygame.event.get(): 56 | if e.type == pygame.QUIT: 57 | running = False 58 | 59 | screen.fill((0, 0, 0)) 60 | 61 | origin_pos = pygame.mouse.get_pos() 62 | 63 | for x in range(ray_count): 64 | point = geometry.raycast( 65 | origin_pos, 66 | x / ray_count * 360, 67 | -1, 68 | colliders, 69 | ) 70 | if point: 71 | pygame.draw.line(screen, (255, 0, 0), origin_pos, point, 1) 72 | 73 | line_width = 3 74 | 75 | for line in collisions_lines: 76 | pygame.draw.line(screen, (0, 0, 255), line.a, line.b, line_width) 77 | 78 | for circle in collisions_circles: 79 | pygame.draw.circle( 80 | screen, (0, 0, 255), (circle.x, circle.y), circle.r, line_width 81 | ) 82 | 83 | for rect in collisions_rects: 84 | pygame.draw.rect(screen, (0, 0, 255), rect, line_width) 85 | 86 | pygame.display.flip() 87 | 88 | 89 | pygame.quit() 90 | raise SystemExit 91 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | build/ 12 | develop-eggs/ 13 | dist/ 14 | downloads/ 15 | eggs/ 16 | .eggs/ 17 | lib/ 18 | lib64/ 19 | parts/ 20 | sdist/ 21 | var/ 22 | wheels/ 23 | pip-wheel-metadata/ 24 | share/python-wheels/ 25 | *.egg-info/ 26 | .installed.cfg 27 | *.egg 28 | MANIFEST 29 | 30 | # PyInstaller 31 | # Usually these files are written by a python script from a template 32 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 33 | *.manifest 34 | *.spec 35 | 36 | # Installer logs 37 | pip-log.txt 38 | pip-delete-this-directory.txt 39 | 40 | # Unit test / coverage reports 41 | htmlcov/ 42 | .tox/ 43 | .nox/ 44 | .coverage 45 | .coverage.* 46 | .cache 47 | nosetests.xml 48 | coverage.xml 49 | *.cover 50 | *.py,cover 51 | .hypothesis/ 52 | .pytest_cache/ 53 | 54 | # Translations 55 | *.mo 56 | *.pot 57 | 58 | # Django stuff: 59 | *.log 60 | local_settings.py 61 | db.sqlite3 62 | db.sqlite3-journal 63 | 64 | # Flask stuff: 65 | instance/ 66 | .webassets-cache 67 | 68 | # Scrapy stuff: 69 | .scrapy 70 | 71 | # Sphinx documentation 72 | docs/_build/ 73 | 74 | # PyBuilder 75 | target/ 76 | 77 | # Jupyter Notebook 78 | .ipynb_checkpoints 79 | 80 | # IPython 81 | profile_default/ 82 | ipython_config.py 83 | 84 | # pyenv 85 | .python-version 86 | 87 | # pipenv 88 | # According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control. 89 | # However, in case of collaboration, if having platform-specific dependencies or dependencies 90 | # having no cross-platform support, pipenv may install dependencies that don't work, or not 91 | # install all needed dependencies. 92 | #Pipfile.lock 93 | 94 | # PEP 582; used by e.g. github.com/David-OConnor/pyflow 95 | __pypackages__/ 96 | 97 | # Celery stuff 98 | celerybeat-schedule 99 | celerybeat.pid 100 | 101 | # SageMath parsed files 102 | *.sage.py 103 | 104 | # Environments 105 | .env 106 | .venv 107 | env/ 108 | venv/ 109 | ENV/ 110 | env.bak/ 111 | venv.bak/ 112 | 113 | # Spyder project settings 114 | .spyderproject 115 | .spyproject 116 | 117 | # Rope project settings 118 | .ropeproject 119 | 120 | # mkdocs documentation 121 | /site 122 | 123 | # mypy 124 | .mypy_cache/ 125 | .dmypy.json 126 | dmypy.json 127 | 128 | # Pyre type checker 129 | .pyre/ 130 | .vscode/settings.json 131 | .vscode/c_cpp_properties.json 132 | -------------------------------------------------------------------------------- /examples/polygon_circle_collision.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame.draw import circle as draw_circle, polygon as draw_polygon 3 | from geometry import regular_polygon, Circle 4 | 5 | pygame.init() 6 | 7 | WHITE = (255, 255, 255) 8 | YELLOW = (255, 255, 0) 9 | RED = (255, 0, 0) 10 | SHAPE_THICKNESS = 3 11 | FPS = 60 12 | WIDTH, HEIGHT = 800, 800 13 | WIDTH2, HEIGHT2 = WIDTH // 2, HEIGHT // 2 14 | screen = pygame.display.set_mode((WIDTH, HEIGHT)) 15 | pygame.display.set_caption("Polygon-Circle Collision Visualization") 16 | clock = pygame.time.Clock() 17 | 18 | font = pygame.font.SysFont("Arial", 25, bold=True) 19 | colliding_text = font.render("Colliding", True, RED) 20 | colliding_textr = colliding_text.get_rect(center=(WIDTH2, 50)) 21 | not_colliding_text = font.render("Not colliding", True, WHITE) 22 | not_colliding_textr = not_colliding_text.get_rect(center=(WIDTH2, 50)) 23 | 24 | modei_text = font.render("Right click to toggle collision mode", True, WHITE) 25 | modei_textr = modei_text.get_rect(center=(WIDTH2, HEIGHT - 50)) 26 | 27 | mode0_txt = font.render("Current: EDGES ONLY", True, YELLOW) 28 | mode0_txtr = mode0_txt.get_rect(center=(WIDTH2, HEIGHT - 25)) 29 | 30 | mode1_txt = font.render("Current: FULL", True, YELLOW) 31 | mode1_txtr = mode1_txt.get_rect(center=(WIDTH2, HEIGHT - 25)) 32 | 33 | circle = Circle((WIDTH2, HEIGHT2), 80) 34 | polygon = regular_polygon(10, (WIDTH2, HEIGHT2), 165) 35 | only_edges = False 36 | running = True 37 | 38 | while running: 39 | circle.center = pygame.mouse.get_pos() 40 | 41 | colliding = circle.collidepolygon(polygon, only_edges) 42 | # Alternative: 43 | # colliding = polygon.collidecircle(circle, only_edges) 44 | 45 | color = RED if colliding else WHITE 46 | 47 | screen.fill((0, 0, 0)) 48 | 49 | draw_circle(screen, color, circle.center, circle.r, SHAPE_THICKNESS) 50 | draw_polygon(screen, color, polygon.vertices, SHAPE_THICKNESS) 51 | 52 | screen.blit( 53 | colliding_text if colliding else not_colliding_text, 54 | colliding_textr if colliding else not_colliding_textr, 55 | ) 56 | 57 | screen.blit(modei_text, modei_textr) 58 | 59 | screen.blit( 60 | mode0_txt if only_edges else mode1_txt, mode0_txtr if only_edges else mode1_txtr 61 | ) 62 | 63 | pygame.display.flip() 64 | 65 | for event in pygame.event.get(): 66 | if event.type == pygame.QUIT: 67 | running = False 68 | elif event.type == pygame.MOUSEBUTTONDOWN: 69 | if event.button == 3: 70 | only_edges = not only_edges 71 | 72 | clock.tick(FPS) 73 | 74 | pygame.quit() 75 | -------------------------------------------------------------------------------- /examples/polygon_scale_visualization.py: -------------------------------------------------------------------------------- 1 | from geometry import regular_polygon 2 | import pygame 3 | from pygame.draw import polygon as draw_polygon 4 | 5 | pygame.init() 6 | 7 | SCREEN_W, SCREEN_H = 800, 600 8 | SCREEN_W2, SCREEN_H2 = SCREEN_W // 2, SCREEN_H // 2 9 | FPS = 60 10 | 11 | screen = pygame.display.set_mode((SCREEN_W, SCREEN_H)) 12 | pygame.display.set_caption("Polygon Scaling Visualization") 13 | clock = pygame.time.Clock() 14 | keep = True 15 | 16 | GREEN = (0, 150, 0) 17 | RED = (200, 0, 0) 18 | WHITE = (255, 255, 255) 19 | GRAY = (150, 150, 150) 20 | YELLOW = (255, 255, 0) 21 | BACKGROUND = (10, 10, 10) 22 | current_color = GREEN 23 | 24 | polygons = [ 25 | regular_polygon(3, (SCREEN_W2, SCREEN_H2 + 25), 100), 26 | regular_polygon(4, (SCREEN_W2, SCREEN_H2 + 25), 50), 27 | regular_polygon(5, (SCREEN_W2, SCREEN_H2 + 25), 25), 28 | regular_polygon(6, (SCREEN_W2, SCREEN_H2 + 25), 15, 45), 29 | regular_polygon(7, (SCREEN_W2, SCREEN_H2 + 25), 10, 90), 30 | regular_polygon(8, (SCREEN_W2, SCREEN_H2 + 25), 5, 135), 31 | ] 32 | 33 | SCALING_DEC = 0.002 34 | SCALING_BOOST = 0.03 35 | ROTATION_SPEED = 0.8 36 | scaling_factor = 1 37 | 38 | font = pygame.font.SysFont("Arial", 20, True) 39 | 40 | wheel_text = font.render("MOUSE WHEEL to scale", True, WHITE) 41 | wheel_text_r = wheel_text.get_rect(center=(SCREEN_W2, 50)) 42 | scaling_up_text = font.render("Scaling up", True, GREEN) 43 | scaling_up_text_r = scaling_up_text.get_rect(center=(SCREEN_W2, 100)) 44 | scaling_down_text = font.render("Scaling down", True, RED) 45 | scaling_down_text_r = scaling_down_text.get_rect(center=(SCREEN_W2, 100)) 46 | 47 | while keep: 48 | for i, poly in enumerate(polygons, start=1): 49 | poly.rotate_ip(i * ROTATION_SPEED / len(polygons)) 50 | 51 | if scaling_factor != 1: 52 | if scaling_factor < 1: 53 | scaling_factor += SCALING_DEC 54 | else: 55 | scaling_factor -= SCALING_DEC 56 | current_color = GREEN if scaling_factor > 1 else RED 57 | for poly in polygons: 58 | poly.scale_ip(scaling_factor) 59 | else: 60 | current_color = GRAY 61 | 62 | screen.fill(BACKGROUND) 63 | for poly in polygons: 64 | draw_polygon(screen, current_color, poly.vertices, 2) 65 | 66 | screen.blit(wheel_text, wheel_text_r) 67 | 68 | if scaling_factor != 1: 69 | if scaling_factor > 1: 70 | screen.blit(scaling_up_text, scaling_up_text_r) 71 | else: 72 | screen.blit(scaling_down_text, scaling_down_text_r) 73 | 74 | pygame.display.flip() 75 | 76 | clock.tick(FPS) 77 | 78 | for event in pygame.event.get(): 79 | if event.type == pygame.QUIT: 80 | keep = False 81 | elif event.type == pygame.MOUSEWHEEL: 82 | if event.y > 0: 83 | scaling_factor += SCALING_BOOST 84 | else: 85 | scaling_factor -= SCALING_BOOST 86 | 87 | pygame.quit() 88 | -------------------------------------------------------------------------------- /examples/polygon_line_collision.py: -------------------------------------------------------------------------------- 1 | from math import radians, sin, cos 2 | 3 | import pygame 4 | from pygame.draw import line as draw_line, polygon as draw_polygon 5 | from geometry import Line, regular_polygon 6 | 7 | 8 | # using this because we're missing line.rotate(), rotates a line around its center 9 | def rotate_line(line, d_ang): 10 | angle = radians(d_ang) 11 | ca, sa = cos(angle), sin(angle) 12 | xm, ym = line.center 13 | ax, ay = line.a 14 | bx, by = line.b 15 | ax -= xm 16 | ay -= ym 17 | bx -= xm 18 | by -= ym 19 | new_ax, new_ay = xm + ax * ca - ay * sa, ym + ax * sa + ay * ca 20 | new_bx, new_by = xm + bx * ca - by * sa, ym + bx * sa + by * ca 21 | line.a, line.b = (new_ax, new_ay), (new_bx, new_by) 22 | 23 | 24 | pygame.init() 25 | 26 | WHITE = (255, 255, 255) 27 | YELLOW = (255, 255, 0) 28 | RED = (255, 0, 0) 29 | SHAPE_THICKNESS = 3 30 | FPS = 60 31 | WIDTH, HEIGHT = 800, 800 32 | WIDTH2, HEIGHT2 = WIDTH // 2, HEIGHT // 2 33 | 34 | screen = pygame.display.set_mode((WIDTH, HEIGHT)) 35 | pygame.display.set_caption("Polygon-Line Collision Visualization") 36 | clock = pygame.time.Clock() 37 | 38 | font = pygame.font.SysFont("Arial", 25, bold=True) 39 | colliding_text = font.render("Colliding", True, RED) 40 | colliding_textr = colliding_text.get_rect(center=(WIDTH2, 50)) 41 | not_colliding_text = font.render("Not colliding", True, WHITE) 42 | not_colliding_textr = not_colliding_text.get_rect(center=(WIDTH2, 50)) 43 | 44 | modei_text = font.render("Right click to toggle collision mode", True, WHITE) 45 | modei_textr = modei_text.get_rect(center=(WIDTH2, HEIGHT - 50)) 46 | 47 | modei2_text = font.render("Left click to rotate", True, WHITE) 48 | modei2_textr = modei2_text.get_rect(center=(WIDTH2, HEIGHT - 80)) 49 | 50 | mode0_txt = font.render("Current: EDGES ONLY", True, YELLOW) 51 | mode0_txtr = mode0_txt.get_rect(center=(WIDTH2, HEIGHT - 25)) 52 | 53 | mode1_txt = font.render("Current: FULL", True, YELLOW) 54 | mode1_txtr = mode1_txt.get_rect(center=(WIDTH2, HEIGHT - 25)) 55 | 56 | polygon = regular_polygon(6, (WIDTH2, HEIGHT2), 180) 57 | line = Line((0, 0), (135, 135)) 58 | only_edges = False 59 | running = True 60 | color = WHITE 61 | 62 | rotating = False 63 | 64 | while running: 65 | delta = (clock.tick(FPS) / 1000) * 60 66 | 67 | line.center = pygame.mouse.get_pos() 68 | 69 | colliding = line.collidepolygon(polygon, only_edges) 70 | # Alternative: 71 | # colliding = polygon.collideline(line, only_edges) 72 | 73 | if rotating: 74 | rotate_line(line, 2) 75 | 76 | color = RED if colliding else WHITE 77 | 78 | screen.fill((10, 10, 10)) 79 | 80 | draw_polygon(screen, color, polygon.vertices, SHAPE_THICKNESS) 81 | draw_line(screen, color, line.a, line.b, SHAPE_THICKNESS) 82 | 83 | screen.blit( 84 | colliding_text if colliding else not_colliding_text, 85 | colliding_textr if colliding else not_colliding_textr, 86 | ) 87 | 88 | screen.blit(modei2_text, modei2_textr) 89 | screen.blit(modei_text, modei_textr) 90 | 91 | screen.blit( 92 | mode0_txt if only_edges else mode1_txt, mode0_txtr if only_edges else mode1_txtr 93 | ) 94 | 95 | pygame.display.flip() 96 | 97 | for event in pygame.event.get(): 98 | if event.type == pygame.QUIT: 99 | running = False 100 | elif event.type == pygame.MOUSEBUTTONDOWN: 101 | if event.button == 1: 102 | rotating = True 103 | elif event.button == 3: 104 | only_edges = not only_edges 105 | elif event.type == pygame.MOUSEBUTTONUP: 106 | if event.button == 1: 107 | rotating = False 108 | pygame.quit() 109 | -------------------------------------------------------------------------------- /setup.py: -------------------------------------------------------------------------------- 1 | from pathlib import Path 2 | from setuptools import setup, Extension 3 | import subprocess 4 | import json 5 | import shlex 6 | import sys 7 | import os 8 | 9 | 10 | extra_compiler_options = [] 11 | 12 | if sys.platform == "linux" or sys.platform == "linux2": 13 | extra_compiler_options.append("-mavx2") 14 | elif sys.platform == "win32": 15 | extra_compiler_options.append("/arch:AVX2") 16 | elif sys.platform == "darwin": 17 | pass 18 | 19 | extensions = [ 20 | Extension( 21 | "geometry", 22 | sources=["src_c/geometry.c"], 23 | extra_compile_args=extra_compiler_options, 24 | ) 25 | ] 26 | 27 | 28 | def consume_arg(arg: str) -> bool: 29 | if arg in sys.argv: 30 | sys.argv.remove(arg) 31 | return True 32 | return False 33 | 34 | 35 | def get_geometry_cache_dir() -> Path: 36 | path = Path("./__geometry_cache__") 37 | 38 | path.mkdir(exist_ok=True) 39 | 40 | return path 41 | 42 | 43 | def get_times_json_file() -> Path: 44 | path = get_geometry_cache_dir() / "times.json" 45 | 46 | path.touch(exist_ok=True) 47 | 48 | return path 49 | 50 | 51 | def update_times_file() -> None: 52 | files = list(Path("./src_c/").glob("**/*.[c|h]")) 53 | 54 | data: Dict[str, float] = {str(file): os.path.getmtime(str(file)) for file in files} 55 | 56 | with open(get_times_json_file(), "w") as f: 57 | f.truncate(0) 58 | f.write(json.dumps(data, indent=4)) 59 | 60 | 61 | def needs_to_be_rebuilt() -> bool: 62 | files = list(Path("./src_c/").glob("**/*.[c|h]")) 63 | 64 | with open(get_times_json_file(), "r+") as f: 65 | file_contents = f.read() 66 | data: Dict[str, float] 67 | 68 | if file_contents == "": 69 | data = {} 70 | else: 71 | data = json.loads(file_contents) 72 | 73 | if not data: 74 | for file in files: 75 | data[str(file)] = os.path.getmtime(str(file)) 76 | 77 | f.write(json.dumps(data, indent=4)) 78 | 79 | return True 80 | 81 | result = False 82 | 83 | for file in files: 84 | if data[str(file)] != os.path.getmtime(str(file)): 85 | data[str(file)] = os.path.getmtime(str(file)) 86 | result = True 87 | 88 | with open(get_times_json_file(), "w") as f: 89 | f.truncate(0) 90 | f.write(json.dumps(data, indent=4)) 91 | 92 | return result 93 | 94 | 95 | def build() -> None: 96 | if not needs_to_be_rebuilt(): 97 | print("latest version of geometry already built") 98 | return 99 | print("building latest version of geometry...") 100 | 101 | with open("src_c/geometry.c", "r+") as f: 102 | original_geometry_c = f.read() 103 | f.write(" ") 104 | 105 | setup( 106 | name="geometry", 107 | ext_modules=extensions, 108 | ) 109 | 110 | with open("src_c/geometry.c", "w") as f: 111 | f.truncate(0) 112 | f.write(original_geometry_c) 113 | 114 | update_times_file() 115 | 116 | 117 | if __name__ == "__main__": 118 | for arg in sys.argv: 119 | if arg in ("--format", "install", "--test"): 120 | break 121 | else: 122 | setup( 123 | name="geometry", 124 | ext_modules=extensions, 125 | ) 126 | sys.exit(0) 127 | 128 | if consume_arg("--format"): 129 | cmd = ["clang-format", "-i"] + [ 130 | str(file) for file in Path("./src_c/").glob("**/*.[c|h]") 131 | ] 132 | print(shlex.join(cmd)) 133 | subprocess.call(cmd) 134 | 135 | cmd = ["black", "."] 136 | print(shlex.join(cmd)) 137 | subprocess.call(cmd) 138 | 139 | run_tests = consume_arg("--test") 140 | 141 | build() 142 | 143 | if run_tests: 144 | cmd = ["python", "-m", "unittest"] 145 | print(shlex.join(cmd)) 146 | subprocess.call(cmd) 147 | -------------------------------------------------------------------------------- /src_c/include/geometry.h: -------------------------------------------------------------------------------- 1 | 2 | #ifndef _GEOMETRY_H 3 | #define _GEOMETRY_H 4 | 5 | #include "pygame.h" 6 | #include 7 | #include 8 | #include 9 | 10 | typedef struct { 11 | double x, y, r; 12 | } pgCircleBase; 13 | 14 | typedef struct { 15 | PyObject_HEAD pgCircleBase circle; 16 | PyObject *weakreflist; 17 | } pgCircleObject; 18 | 19 | #define pgCircle_CAST(o) ((pgCircleObject *)(o)) 20 | #define pgCircle_AsCircle(o) (pgCircle_CAST(o)->circle) 21 | #define pgCircle_GETX(self) (pgCircle_CAST(self)->circle.x) 22 | #define pgCircle_GETY(self) (pgCircle_CAST(self)->circle.y) 23 | #define pgCircle_GETR(self) (pgCircle_CAST(self)->circle.r) 24 | 25 | typedef struct { 26 | double xa, ya; 27 | double xb, yb; 28 | } pgLineBase; 29 | 30 | typedef struct { 31 | PyObject_HEAD pgLineBase line; 32 | PyObject *weakreflist; 33 | } pgLineObject; 34 | 35 | #define pgLine_CAST(o) ((pgLineObject *)(o)) 36 | 37 | #define pgLine_GETLINE(o) (pgLine_CAST(o)->line) 38 | #define pgLine_AsLine(o) (pgLine_CAST(o)->line) 39 | #define pgLine_GETX1(self) (pgLine_CAST(self)->line.xa) 40 | #define pgLine_GETY1(self) (pgLine_CAST(self)->line.ya) 41 | #define pgLine_GETX2(self) (pgLine_CAST(self)->line.xb) 42 | #define pgLine_GETY2(self) (pgLine_CAST(self)->line.yb) 43 | 44 | // return 1 if success and 0 if failure 45 | static int 46 | pgLine_FromObject(PyObject *obj, pgLineBase *out); 47 | // return 1 if success and 0 if failure 48 | static int 49 | pgLine_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs, 50 | pgLineBase *out); 51 | // return 1 if success and 0 if failure 52 | static int 53 | pgCircle_FromObject(PyObject *obj, pgCircleBase *out); 54 | // return 1 if success and 0 if failure 55 | static int 56 | pgCircle_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs, 57 | pgCircleBase *out); 58 | 59 | static PyTypeObject pgCircle_Type; 60 | static PyTypeObject pgLine_Type; 61 | static PyTypeObject pgPolygon_Type; 62 | 63 | typedef struct { 64 | Py_ssize_t verts_num; 65 | double *vertices; 66 | double centerx, centery; 67 | } pgPolygonBase; 68 | 69 | typedef struct { 70 | PyObject_HEAD pgPolygonBase polygon; 71 | PyObject *weakreflist; 72 | } pgPolygonObject; 73 | 74 | #define pgPolygon_CAST(o) ((pgPolygonObject *)(o)) 75 | #define pgPolygon_AsPolygon(o) (pgPolygon_CAST(o)->polygon) 76 | #define pgPolygon_GETVERTICES(o) (pgPolygon_AsPolygon(o).vertices) 77 | #define pgPolygon_GETVERTSNUM(o) (pgPolygon_AsPolygon(o).verts_num) 78 | #define pgPolygon_GETCENTER_X(o) (pgPolygon_AsPolygon(o).centerx) 79 | #define pgPolygon_GETCENTER_Y(o) (pgPolygon_AsPolygon(o).centery) 80 | 81 | // return 1 if success and 0 if failure 82 | static int 83 | pgPolygon_FromObject(PyObject *obj, pgPolygonBase *out, int *was_sequence); 84 | 85 | static int 86 | pgPolygon_FromObjectFastcall(PyObject *const *args, Py_ssize_t nargs, 87 | pgPolygonBase *out, int *was_sequence); 88 | 89 | #define pgCircle_Check(o) ((o)->ob_type == &pgCircle_Type) 90 | #define pgLine_Check(o) ((o)->ob_type == &pgLine_Type) 91 | #define pgPolygon_Check(o) ((o)->ob_type == &pgPolygon_Type) 92 | 93 | /* Constants */ 94 | 95 | /* PI */ 96 | #ifndef M_PI 97 | #define M_PI 3.14159265358979323846 98 | #endif 99 | 100 | /* 2PI */ 101 | #ifndef M_TWOPI 102 | #define M_TWOPI 6.28318530717958647692 103 | #endif 104 | 105 | /* PI/2 */ 106 | #ifndef M_PI_QUO_2 107 | #define M_PI_QUO_2 1.57079632679489661923 108 | #endif 109 | 110 | /* PI/4 */ 111 | #ifndef M_PI_QUO_4 112 | #define M_PI_QUO_4 0.78539816339744830962 113 | #endif 114 | 115 | /* PI/180 */ 116 | #ifndef M_PI_QUO_180 117 | #define M_PI_QUO_180 0.01745329251994329577 118 | #endif 119 | 120 | /* 180/PI */ 121 | #ifndef M_180_QUO_PI 122 | #define M_180_QUO_PI 57.29577951308232087680 123 | #endif 124 | 125 | /* Functions */ 126 | 127 | /* Converts degrees to radians */ 128 | static PG_FORCE_INLINE double 129 | DEG_TO_RAD(double deg) 130 | { 131 | return deg * M_PI_QUO_180; 132 | } 133 | 134 | /* Converts radians to degrees */ 135 | static PG_FORCE_INLINE double 136 | RAD_TO_DEG(double rad) 137 | { 138 | return rad * M_180_QUO_PI; 139 | } 140 | 141 | /* Frees the polygon's vertices if they were allocated from a sequence. */ 142 | static PG_FORCE_INLINE void 143 | PG_FREEPOLY_COND(pgPolygonBase *poly, int was_sequence) 144 | { 145 | if (was_sequence) { 146 | PyMem_Free(poly->vertices); 147 | } 148 | } 149 | 150 | static int 151 | double_compare(double a, double b) 152 | { 153 | /* Uses both a fixed epsilon and an adaptive epsilon */ 154 | const double e = 1e-6; 155 | return fabs(a - b) < e || fabs(a - b) <= e * MAX(fabs(a), fabs(b)); 156 | } 157 | 158 | #endif /* ~_GEOMETRY_H */ 159 | -------------------------------------------------------------------------------- /examples/circle_collisions_visualization.py: -------------------------------------------------------------------------------- 1 | import pygame 2 | from pygame.draw import circle as draw_circle, rect as draw_rect, line as draw_line 3 | from geometry import Circle, Line 4 | from random import randint 5 | 6 | pygame.init() 7 | 8 | # Constants 9 | # ----------------------------------- 10 | FPS = 60 11 | BACKGROUND_COLOR = (20, 20, 20) 12 | WHITE = (255, 255, 255) 13 | GRAY = (120, 120, 120) 14 | BLACK = (0, 0, 0) 15 | RED = (200, 0, 0) 16 | GREEN = (0, 150, 0) 17 | SCREEN_W, SCREEN_H = 840, 640 18 | SCREEN_W_HALF, SCREEN_H_HALF = SCREEN_W // 2, SCREEN_H // 2 19 | # ----------------------------------- 20 | 21 | # Game variables and fundamentals 22 | # ----------------------------------- 23 | screen = pygame.display.set_mode((SCREEN_W, SCREEN_H)) 24 | pygame.display.set_caption("Circle Collision Visualization") 25 | clock = pygame.time.Clock() 26 | font = pygame.font.SysFont("Arial", 22, bold=True) 27 | 28 | text1 = font.render("R to randomize shapes", True, WHITE) 29 | text1_r = text1.get_rect(center=(SCREEN_W // 4, 15)) 30 | text2 = font.render("MOUSE WHEEL to select size", True, WHITE) 31 | text2_r = text2.get_rect(center=(3 * SCREEN_W // 4, 15)) 32 | 33 | keep = True 34 | acc_amt = 0 35 | acc_multiplier = 0.92 36 | 37 | m_circle = Circle(0, 0, 50) 38 | 39 | rects = [] 40 | lines = [] 41 | circles = [] 42 | points = [] 43 | 44 | 45 | def populate_rects(number): 46 | # in bottomleft quadrant 47 | for _ in range(number): 48 | dim_x = randint(15, 50) 49 | dim_y = randint(15, 50) 50 | x = randint(0, SCREEN_W_HALF - dim_x) 51 | y = randint(SCREEN_H_HALF, SCREEN_H - dim_y) 52 | rects.append(pygame.Rect(x, y, dim_x, dim_y)) 53 | 54 | 55 | def populate_lines(number): 56 | # in topleft quadrant 57 | for _ in range(number): 58 | xa = randint(0, SCREEN_W_HALF - 1) 59 | ya = randint(31, SCREEN_H_HALF - 1) 60 | xb = randint(0, SCREEN_W_HALF - 1) 61 | yb = randint(31, SCREEN_H_HALF - 1) 62 | lines.append(Line(xa, ya, xb, yb)) 63 | 64 | 65 | def populate_circles(number): 66 | # in bottomright quadrant 67 | for _ in range(number): 68 | radius = randint(10, 50) 69 | x = randint(radius + SCREEN_W_HALF, SCREEN_W - radius) 70 | y = randint(radius + SCREEN_H_HALF, SCREEN_H - radius) 71 | circles.append(Circle(x, y, radius)) 72 | 73 | 74 | def populate_points(number): 75 | # in topright quadrant 76 | for _ in range(number): 77 | x = randint(SCREEN_W_HALF + 15, SCREEN_W - 15) 78 | y = randint(31, SCREEN_H_HALF - 15) 79 | points.append((x, y)) 80 | 81 | 82 | def green_gray(colliding): 83 | return GREEN if colliding else GRAY 84 | 85 | 86 | populate_rects(15) 87 | populate_lines(15) 88 | populate_circles(15) 89 | populate_points(15) 90 | 91 | # Game loop 92 | while keep: 93 | # Update 94 | m_circle.center = pygame.mouse.get_pos() 95 | if abs(acc_amt) < 0.001: 96 | acc_amt = 0 97 | else: 98 | acc_amt *= acc_multiplier 99 | m_circle.r *= 1 + acc_amt 100 | 101 | # Draw 102 | screen.fill(BACKGROUND_COLOR) 103 | 104 | coll = 0 105 | 106 | for rect in rects: 107 | colliding = m_circle.colliderect(rect) 108 | coll |= colliding 109 | draw_rect(screen, green_gray(colliding), rect, 2) 110 | 111 | for line in lines: 112 | colliding = m_circle.collideline(line) 113 | coll |= colliding 114 | draw_line(screen, green_gray(colliding), line.a, line.b, 2) 115 | 116 | for circle in circles: 117 | colliding = m_circle.collidecircle(circle) 118 | coll |= colliding 119 | draw_circle(screen, green_gray(colliding), circle.center, circle.r, 2) 120 | 121 | for point in points: 122 | colliding = m_circle.collidepoint(point) 123 | coll |= colliding 124 | draw_circle(screen, green_gray(colliding), point, 3) 125 | 126 | draw_line(screen, WHITE, (SCREEN_W_HALF, 0), (SCREEN_W_HALF, SCREEN_H), 2) 127 | draw_line(screen, WHITE, (0, SCREEN_H_HALF), (SCREEN_W, SCREEN_H_HALF), 2) 128 | 129 | draw_circle(screen, BLACK, (m_circle.x + 2, m_circle.y + 2), m_circle.r, 3) 130 | draw_circle(screen, GREEN if coll else RED, m_circle.center, m_circle.r, 2) 131 | 132 | draw_rect(screen, BLACK, (0, 0, SCREEN_W, 30)) 133 | 134 | screen.blit(text1, text1_r) 135 | screen.blit(text2, text2_r) 136 | 137 | pygame.display.update() 138 | 139 | clock.tick_busy_loop(FPS) 140 | 141 | # Events 142 | for event in pygame.event.get(): 143 | if event.type == pygame.QUIT: 144 | keep = False 145 | elif event.type == pygame.MOUSEWHEEL: 146 | if event.y > 0: 147 | acc_amt += 0.012 148 | else: 149 | if m_circle.r < 5: 150 | continue 151 | acc_amt -= 0.014 152 | elif event.type == pygame.KEYDOWN: 153 | if event.key == pygame.K_r: 154 | rects, lines, circles, points = [], [], [], [] 155 | populate_rects(15) 156 | populate_lines(15) 157 | populate_circles(15) 158 | populate_points(15) 159 | 160 | pygame.quit() 161 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, religion, or sexual identity 10 | and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the 26 | overall community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or 31 | advances of any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email 35 | address, without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | andrewhong@myyahoo.com. 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series 86 | of actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or 93 | permanent ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within 113 | the community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.0, available at 119 | https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. 120 | 121 | Community Impact Guidelines were inspired by [Mozilla's code of conduct 122 | enforcement ladder](https://github.com/mozilla/diversity). 123 | 124 | [homepage]: https://www.contributor-covenant.org 125 | 126 | For answers to common questions about this code of conduct, see the FAQ at 127 | https://www.contributor-covenant.org/faq. Translations are available at 128 | https://www.contributor-covenant.org/translations. 129 | -------------------------------------------------------------------------------- /examples/regular_polygon.py: -------------------------------------------------------------------------------- 1 | """ 2 | This is an example script implementing 3 | the method normal_polygon() from the class Polygon 4 | for the Pygame community. 5 | 6 | Author: @newpaxonian 7 | """ 8 | 9 | import math 10 | import random 11 | import sys 12 | 13 | import geometry 14 | import pygame as pg 15 | 16 | GRAY_BACKGROUND = (47, 52, 63) 17 | 18 | 19 | class Game: 20 | """ 21 | Class Game: contains the main game loop and the methods 22 | """ 23 | 24 | def __init__(self): 25 | pg.init() 26 | self.display = pg.display 27 | self.screen = self.display.set_mode((640, 480)) 28 | self.clock = pg.time.Clock() 29 | self.radius = 20 30 | self.sides = 3 31 | self.mouse_left = False 32 | self.mouse_right = False 33 | self.mouse_wheel = False 34 | self.font = pg.font.SysFont("Consolas", 14, True) 35 | self.text_timer = 20 36 | self.text_alpha = 255 37 | self.polygon_names = { 38 | 3: "triangle", 39 | 4: "square", 40 | 5: "pentagon", 41 | 6: "hexagon", 42 | 7: "heptagon", 43 | 8: "octagon", 44 | 9: "enneagon", 45 | 10: "decagon", 46 | } 47 | self.polygon_text_pos = (600, 466) 48 | 49 | def events(self): 50 | """ 51 | Method events: handles the events 52 | """ 53 | for event in pg.event.get(): 54 | if event.type == pg.QUIT or ( 55 | event.type == pg.KEYDOWN and event.key == pg.K_ESCAPE 56 | ): 57 | print("\nQuitting program...") 58 | pg.quit() 59 | sys.exit() 60 | if event.type == pg.MOUSEBUTTONDOWN: 61 | if pg.mouse.get_pressed() == (1, 0, 0): 62 | self.mouse_left = not self.mouse_left 63 | elif pg.mouse.get_pressed() == (0, 0, 1): 64 | self.mouse_right = True 65 | if event.type == pg.MOUSEBUTTONUP: 66 | if pg.mouse.get_pressed() == (0, 0, 0): 67 | self.mouse_left = False 68 | self.mouse_right = False 69 | if event.type == pg.MOUSEWHEEL: 70 | self.mouse_wheel = True 71 | if event.y > 0: 72 | self.sides = self.sides + 1 if self.sides < 10 else 10 73 | elif event.y < 0: 74 | self.sides = self.sides - 1 if self.sides > 3 else 3 75 | 76 | def draw_polygon( 77 | self, 78 | num_sides=3, 79 | center=(0.0, 0.0), 80 | radius=1.0, 81 | angle=0.0, 82 | color=(255, 255, 255), 83 | ): 84 | """ 85 | Method draw_polygon: draws a regular polygon 86 | """ 87 | box_side = 2 * radius 88 | box_x, box_y = center[0] - box_side // 2, center[1] - box_side // 2 89 | box = pg.surface.Surface((box_side, box_side)).convert_alpha() 90 | box.fill([0, 0, 0, 0]) 91 | polygon = geometry.regular_polygon( 92 | num_sides, (box_side // 2, box_side // 2), radius, angle 93 | ) 94 | 95 | pg.draw.polygon(box, color, polygon.vertices) 96 | self.screen.blit(box, (box_x, box_y)) 97 | 98 | def draw_text(self, text, color, pos): 99 | """ 100 | Method draw_text: draws text 101 | """ 102 | text_surface = self.font.render(text, True, color) 103 | text_rect = text_surface.get_rect(center=pos) 104 | text_rect.center = pos 105 | text_bg = pg.surface.Surface((80, text_surface.get_height() + 5)) 106 | text_bg_size = text_bg.get_size() 107 | text_bg_rect = text_bg.get_rect(center=pos) 108 | # pos - (text_bg_size // 2) 109 | text_bg_rect.center = tuple((p - tbg // 2) for p, tbg in zip(pos, text_bg_size)) 110 | text_bg.fill(GRAY_BACKGROUND) 111 | self.screen.blit(text_bg, text_bg_rect.center) 112 | self.screen.blit(text_surface, text_rect) 113 | 114 | def draw(self): 115 | """ 116 | Method draw: draws scene 117 | """ 118 | # Mouse events 119 | if self.mouse_left: 120 | angle = math.tau * random.uniform(0.0, 1.0) 121 | x = pg.mouse.get_pos()[0] + round(math.cos(angle) * self.radius * 0.5) 122 | y = pg.mouse.get_pos()[1] + round(math.sin(angle) * self.radius * 0.5) 123 | color = ( 124 | random.randint(0, 255), 125 | random.randint(0, 255), 126 | random.randint(0, 255), 127 | ) 128 | self.draw_polygon( 129 | num_sides=self.sides, 130 | center=(x, y), 131 | radius=self.radius, 132 | angle=random.randint(0, 360), 133 | color=color, 134 | ) 135 | elif self.mouse_right: 136 | self.screen.fill(GRAY_BACKGROUND) 137 | elif self.mouse_wheel: 138 | self.draw_text( 139 | f"{self.polygon_names[self.sides]}", "white", self.polygon_text_pos 140 | ) 141 | self.mouse_wheel = False 142 | 143 | # Screen Title 144 | title = self.font.render( 145 | "Mouse: left = draw | right = reset | wheel = change polygon", True, "white" 146 | ) 147 | title_background = pg.surface.Surface((640, title.get_height() + 5)) 148 | title_background.set_alpha(180) 149 | title_pos = title.get_rect(center=(640 / 2, title.get_height() * 0.75)) 150 | 151 | self.screen.blit(title_background, (0, 0)) 152 | self.screen.blit(title, title_pos) 153 | 154 | self.draw_text( 155 | f"{self.polygon_names[self.sides]}", "white", self.polygon_text_pos 156 | ) 157 | pg.time.wait(50) 158 | 159 | def run(self): 160 | """ 161 | Main game loop 162 | """ 163 | self.screen.fill(GRAY_BACKGROUND) 164 | while True: 165 | self.clock.tick(60) 166 | self.display.set_caption("Regular Polygons") 167 | self.events() 168 | self.draw() 169 | self.display.update() 170 | 171 | 172 | def main(): 173 | """ 174 | Main function 175 | """ 176 | game = Game() 177 | game.run() 178 | 179 | 180 | if __name__ == "__main__": 181 | main() 182 | -------------------------------------------------------------------------------- /examples/circle_collision_game.py: -------------------------------------------------------------------------------- 1 | from random import randint, random 2 | from typing import List, Tuple, Union 3 | 4 | import pygame 5 | from pygame.draw import circle as draw_circle, line as draw_line, rect as draw_rect 6 | 7 | from geometry import Circle 8 | 9 | pygame.init() 10 | 11 | from random import randint 12 | from typing import Tuple 13 | 14 | 15 | def rand_color(minv: int = 0, maxv: int = 255) -> Tuple[int, int, int]: 16 | """returns a random RGB color with min and max as min and max threshold""" 17 | return randint(minv, maxv), randint(minv, maxv), randint(minv, maxv) 18 | 19 | 20 | def rand_bw_color(minv: int, maxv: int) -> Tuple[int, int, int]: 21 | """returns a random RGB BW color""" 22 | shade = randint(minv, maxv) 23 | return shade, shade, shade 24 | 25 | 26 | def get_new_circle_surf( 27 | circle_obj: Circle, color: Union[str, Tuple[int, int, int]], surface_alpha: int 28 | ) -> pygame.Surface: 29 | surf = pygame.surface.Surface((circle_obj.d, circle_obj.d), pygame.SRCALPHA) 30 | pygame.draw.circle(surf, color, (circle_obj.r, circle_obj.r), circle_obj.r) 31 | surf.set_alpha(surface_alpha) 32 | return surf 33 | 34 | 35 | # Constants and font -------------------------- 36 | FONT = pygame.font.SysFont("Consolas", 25, True) 37 | FPS = 60 38 | ALPHA_VALUE = 80 39 | SUB_TEXT_ALPHA = 180 40 | SHAPES_NUMBER = 1000 41 | MIN_CIRCLE_RADIUS = 3 42 | MAX_CIRCLE_RADIUS = 15 43 | MIN_RECT_WIDTH = 5 44 | MAX_RECT_WIDTH = 25 45 | MIN_RECT_HEIGHT = 5 46 | MAX_RECT_HEIGHT = 25 47 | BACKGROUND_COLOR = (66, 65, 112) 48 | WIDTH, HEIGHT = 1422, 800 49 | # -------------------------------------------- 50 | 51 | # Game variables and fundamentals -------------------------- 52 | screen = pygame.display.set_mode((WIDTH, HEIGHT)) 53 | pygame.display.set_caption("Circle Collision Game") 54 | clock = pygame.time.Clock() 55 | keep: bool = True 56 | 57 | shapes: List[Circle] = [] 58 | colors: List[Tuple[int, int, int]] = [] 59 | colliding_colors: List[Tuple[int, int, int]] = [] 60 | 61 | feed_active: bool = True 62 | mouse_circle: Circle = Circle(0, 0, 50) 63 | border_color: str = "green" 64 | acc_amt = 0 65 | acc_multiplier = 0.92 66 | # -------------------------------------------- 67 | 68 | # Useful surfaces setup ---------------------- 69 | txt_surf = FONT.render( 70 | "RIGHT CLICK to toggle | MOUSE WHEEL to select size", True, "white" 71 | ) 72 | sub_text_surf = pygame.surface.Surface((WIDTH, txt_surf.get_height() + 5)) 73 | sub_text_surf.set_alpha(SUB_TEXT_ALPHA) 74 | mouse_circle_surf = get_new_circle_surf(mouse_circle, "white", ALPHA_VALUE) 75 | TXT_SURF_BLIT_POS = txt_surf.get_rect(center=(WIDTH / 2, txt_surf.get_height() / 2)) 76 | # ------------------------------------------ 77 | 78 | # Create the random shapes 79 | for _ in range(SHAPES_NUMBER): 80 | colors.append(rand_bw_color(20, 130)) 81 | colliding_colors.append(rand_color()) 82 | obj = None 83 | if random() * 10 > 5: 84 | r = randint(MIN_CIRCLE_RADIUS, MAX_CIRCLE_RADIUS) 85 | obj = Circle(randint(r, WIDTH - r), randint(r, HEIGHT - r), r) 86 | else: 87 | dimx = randint(MIN_RECT_WIDTH, MAX_RECT_WIDTH) 88 | dimy = randint(MIN_RECT_HEIGHT, MAX_RECT_HEIGHT) 89 | obj = pygame.Rect( 90 | (randint(0, WIDTH - dimx), randint(0, HEIGHT - dimy)), (dimx, dimy) 91 | ) 92 | 93 | shapes.append(obj) 94 | 95 | # Game loop 96 | while keep: 97 | screen.fill(BACKGROUND_COLOR) 98 | clock.tick_busy_loop(FPS) 99 | 100 | mouse_circle.center = pygame.mouse.get_pos() 101 | 102 | # main update and draw loop, draw every shape and update their size 103 | for shape, color, coll_color in zip(shapes, colors, colliding_colors): 104 | is_circle = isinstance(shape, Circle) 105 | is_rect = isinstance(shape, pygame.Rect) 106 | 107 | if feed_active and mouse_circle.collideswith(shape): 108 | if is_circle: 109 | shape.r += 0.2 110 | draw_circle(screen, coll_color, shape.center, shape.r, 3) 111 | elif is_rect: 112 | shape.inflate_ip(1, 1) 113 | draw_rect(screen, coll_color, shape, 3) 114 | else: 115 | if is_circle: 116 | if shape.r <= 1.5: 117 | # this relocates the circle so there's no need to delete 118 | # and create a new one 119 | r = randint(3, 15) 120 | shape.update( 121 | randint(r, WIDTH - r), 122 | randint(r, HEIGHT - r), 123 | r, 124 | ) 125 | 126 | shape.r -= 0.25 127 | draw_circle(screen, color, shape.center, shape.r, 2) 128 | elif is_rect: 129 | value = 0.25 130 | 131 | if shape.h != 0: 132 | w_ov_h = shape.w / shape.h 133 | shape.w -= 0.1 134 | shape.h -= 0.1 * w_ov_h 135 | if shape.w <= 1 or shape.h <= 1: 136 | dimx = randint(MIN_RECT_WIDTH, MAX_RECT_WIDTH) 137 | dimy = randint(MIN_RECT_HEIGHT, MAX_RECT_HEIGHT) 138 | shape.update( 139 | (randint(0, WIDTH - dimx), randint(0, HEIGHT - dimy)), 140 | (dimx, dimy), 141 | ) 142 | 143 | draw_rect(screen, color, shape, 2) 144 | 145 | # blit the mouse circle surface on the screen 146 | screen.blit( 147 | mouse_circle_surf, 148 | mouse_circle_surf.get_rect(center=(mouse_circle.x, mouse_circle.y)), 149 | ) 150 | 151 | if abs(acc_amt) < 0.001: 152 | acc_amt = 0 153 | else: 154 | acc_amt *= acc_multiplier 155 | mouse_circle.r *= 1 + acc_amt 156 | mouse_circle_surf = get_new_circle_surf(mouse_circle, "white", ALPHA_VALUE) 157 | 158 | # select and draw the mouse_circle border color 159 | if feed_active: 160 | border_color = "green" 161 | else: 162 | border_color = "white" 163 | 164 | pygame.draw.circle( 165 | screen, border_color, (mouse_circle.x, mouse_circle.y), mouse_circle.r, 2 166 | ) 167 | 168 | # event loop 169 | 170 | for event in pygame.event.get(): 171 | if event.type == pygame.QUIT: 172 | keep = False 173 | elif event.type == pygame.MOUSEWHEEL: 174 | if event.y > 0: 175 | if acc_amt <= 0: 176 | acc_amt = 0 177 | acc_amt += 0.012 178 | else: 179 | if acc_amt >= 0: 180 | acc_amt = 0 181 | acc_amt -= 0.014 182 | 183 | elif event.type == pygame.MOUSEBUTTONUP and event.button == 3: 184 | feed_active = not feed_active 185 | 186 | # blit the text and black bar on the screen 187 | screen.blit(sub_text_surf, (0, 0)) 188 | screen.blit(txt_surf, TXT_SURF_BLIT_POS) 189 | 190 | pygame.display.update() 191 | 192 | pygame.quit() 193 | -------------------------------------------------------------------------------- /test/test_geometry.py: -------------------------------------------------------------------------------- 1 | import unittest 2 | import pygame 3 | import geometry 4 | from geometry import Line, Circle, Polygon, is_line, is_circle, is_polygon 5 | from pygame import Rect 6 | 7 | 8 | def _get_vertices_from_rect(rect: pygame.Rect): 9 | return [ 10 | (rect.left, rect.top), 11 | (rect.right, rect.top), 12 | (rect.right, rect.bottom), 13 | (rect.left, rect.bottom), 14 | ] 15 | 16 | 17 | class TestGeometry(unittest.TestCase): 18 | def test_rect_to_polygon(self): 19 | rects_base = [ 20 | (1, 2, 3, 4), # normalised rect 21 | (-1, -2, 3, 4), # normalised rect, negative position 22 | (1, 2, -3, -4), # not normalised rect, positive position 23 | (-1, -2, -3, -4), # not normalised rect, negative position 24 | (1, 2, 0, 0), # zero size rect 25 | (1, 2, 0, 4), # zero width rect 26 | (1, 2, 3, 0), # zero height rect 27 | (1, 2, -3, 0), # zero width rect, negative width 28 | (1, 2, 0, -4), # zero height rect, negative height 29 | (-1, -2, 0, 0), # zero size rect, negative position 30 | (-1, -2, 0, 4), # zero width rect, negative position 31 | (-1, -2, 3, 0), # zero height rect, negative position 32 | (-1, -2, -3, 0), # zero width rect, negative width negative position 33 | (-1, -2, 0, -4), # zero height rect, negative height negative position 34 | ] 35 | 36 | for rect_base in rects_base: 37 | rect = pygame.Rect(*rect_base) 38 | polygon = geometry.rect_to_polygon(rect) 39 | self.assertIsInstance(polygon, geometry.Polygon) 40 | self.assertEqual(_get_vertices_from_rect(rect), polygon.vertices) 41 | 42 | def test_rect_to_polygon_invalid_argnum(self): 43 | # Test with invalid number of arguments 44 | rect = pygame.Rect(1, 2, 3, 4) 45 | invalid_args = [ 46 | (), 47 | (rect, rect), 48 | (rect, rect, rect), 49 | (rect, rect, rect, rect), 50 | (rect, rect, rect, rect, rect), 51 | ] 52 | for i, args in enumerate(invalid_args): 53 | with self.assertRaises(TypeError): 54 | geometry.rect_to_polygon(*args) 55 | 56 | def test_rect_to_polygon_invalid_arg_type(self): 57 | # Test with an invalid rect object 58 | invalid_rects = [ 59 | "not a rect", 60 | 1, 61 | 1.0, 62 | True, 63 | False, 64 | None, 65 | [], 66 | {}, 67 | set(), 68 | (1,), 69 | (1, 2), 70 | (1, 2, 3), 71 | (1, 2, 3, 4, 5), 72 | [1], 73 | [1, 2], 74 | [1, 2, 3], 75 | [1, 2, 3, 4, 5], 76 | ] 77 | for invalid_rect in invalid_rects: 78 | with self.assertRaises(TypeError): 79 | geometry.rect_to_polygon(invalid_rect) 80 | 81 | def test_is_line(self): 82 | """Test is_line function""" 83 | 84 | class TestLine(Line): 85 | pass 86 | 87 | items = [ 88 | (Line(0, 0, 1, 1), True), 89 | (Circle(0, 0, 1), False), 90 | (Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), False), 91 | (Rect(0, 0, 1, 1), False), 92 | (TestLine(0, 0, 1, 1), False), 93 | (1, False), 94 | (1.0, False), 95 | (True, False), 96 | (False, False), 97 | (None, False), 98 | ("", False), 99 | ("string", False), 100 | ([], False), 101 | ([1], False), 102 | ((1,), False), 103 | ({}, False), 104 | ] 105 | for obj, expected in items: 106 | self.assertEqual(expected, is_line(obj)) 107 | 108 | # Check that the function doesn't modify the line 109 | # ================================================= 110 | l = Line(0, 0, 1, 1) 111 | start, end = l.a, l.b 112 | 113 | self.assertTrue(is_line(l)) 114 | self.assertEqual(start, l.a) 115 | self.assertEqual(end, l.b) 116 | # ================================================= 117 | 118 | def test_is_circle(self): 119 | """Test is_circle function""" 120 | 121 | class TestCircle(Circle): 122 | pass 123 | 124 | items = [ 125 | (Line(0, 0, 1, 1), False), 126 | (Circle(0, 0, 1), True), 127 | (Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), False), 128 | (Rect(0, 0, 1, 1), False), 129 | (TestCircle(0, 0, 1), False), 130 | (1, False), 131 | (1.0, False), 132 | (True, False), 133 | (False, False), 134 | (None, False), 135 | ("", False), 136 | ("string", False), 137 | ([], False), 138 | ([1], False), 139 | ((1,), False), 140 | ({}, False), 141 | ] 142 | for obj, expected in items: 143 | self.assertEqual(expected, is_circle(obj)) 144 | 145 | # Check that the function doesn't modify the circle 146 | # ================================================= 147 | c = Circle(0, 0, 1) 148 | center = c.center 149 | radius = c.r 150 | 151 | self.assertTrue(is_circle(c)) 152 | self.assertEqual(center, c.center) 153 | self.assertEqual(radius, c.r) 154 | # ================================================= 155 | 156 | def test_area(self): 157 | poly = Polygon((-3, -2), (-1, 4), (6, 1), (3, 10), (-4, 9)) 158 | 159 | self.assertEqual(60, poly.area) 160 | 161 | poly = Polygon((0, 0), (0, 4), (4, 4), (4, 0)) 162 | 163 | self.assertEqual(16, poly.area) 164 | 165 | def test_is_polygon(self): 166 | """Test is_polygon function""" 167 | 168 | class TestPolygon(Polygon): 169 | pass 170 | 171 | items = [ 172 | (Line(0, 0, 1, 1), False), 173 | (Circle(0, 0, 1), False), 174 | (Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]), True), 175 | (Rect(0, 0, 1, 1), False), 176 | (TestPolygon([(0, 0), (1, 0), (1, 1), (0, 1)]), False), 177 | (1, False), 178 | (1.0, False), 179 | (True, False), 180 | (False, False), 181 | (None, False), 182 | ("", False), 183 | ("string", False), 184 | ([], False), 185 | ([1], False), 186 | ((1,), False), 187 | ({}, False), 188 | ] 189 | for obj, expected in items: 190 | self.assertEqual(expected, is_polygon(obj)) 191 | 192 | # Check that the function doesn't modify the polygon 193 | # ================================================= 194 | p = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]) 195 | verts = p.vertices 196 | verts_num = p.verts_num 197 | cx, cy = p.center 198 | 199 | self.assertTrue(is_polygon(p)) 200 | self.assertEqual(verts, p.vertices) 201 | self.assertEqual(verts_num, p.verts_num) 202 | self.assertEqual(cx, p.centerx) 203 | self.assertEqual(cy, p.centery) 204 | # ================================================= 205 | 206 | 207 | if __name__ == "__main__": 208 | unittest.main() 209 | -------------------------------------------------------------------------------- /benchmarks/benchmark_utils.py: -------------------------------------------------------------------------------- 1 | import timeit 2 | from statistics import fmean, pstdev, median 3 | 4 | from tabulate import tabulate 5 | 6 | 7 | def center_text(text: str, width: int) -> str: 8 | delta = (width - len(text)) // 2 9 | return " " * delta + text 10 | 11 | 12 | def num_from_format(raw_format: str) -> int: 13 | time_format = raw_format.lower() 14 | 15 | if time_format == "s": 16 | return 1 17 | elif time_format == "ms": 18 | return 1_000 19 | elif time_format == "us": 20 | return 1_000_000 21 | elif time_format == "ns": 22 | return 1_000_000_000 23 | else: 24 | raise ValueError("Invalid format") 25 | 26 | 27 | def yes_no(val: bool) -> str: 28 | return "Yes" if val else "No" 29 | 30 | 31 | class TestGroup: 32 | def __init__( 33 | self, 34 | name: str, 35 | tests: list[tuple[str, str]], 36 | globs: dict, 37 | time_format: str, 38 | precision: int, 39 | num: int, 40 | repeat_num: int, 41 | show_total: bool = True, 42 | show_mean: bool = True, 43 | show_std: bool = True, 44 | ): 45 | self.name = name 46 | self.tests = tests 47 | self.tests_number = len(tests) 48 | self.globs = globs 49 | self.results = [] 50 | self.time_format = time_format 51 | self.precision = precision 52 | self.num = num 53 | self.repeat_num = repeat_num 54 | self.show_total = show_total 55 | self.show_mean = show_mean 56 | self.show_std = show_std 57 | 58 | def print_name(self): 59 | print(self.name.upper()) 60 | print("=" * 50) 61 | 62 | def test(self) -> None: 63 | self.print_name() 64 | table = [self.get_field_names()] 65 | 66 | for test_name, func in self.tests: 67 | self.results.append(self.test_pair(test_name, func, table)) 68 | 69 | print( 70 | tabulate( 71 | table, 72 | headers="firstrow", 73 | tablefmt="github", 74 | numalign="center", 75 | stralign="center", 76 | ) 77 | ) 78 | self.print_results() 79 | print("\n\n") 80 | 81 | def test_pair(self, test_name: str, func: str, table: list) -> float: 82 | # ensure that the objects utilized in the tests are not modified by the tests 83 | item = func.split(".")[0] 84 | saved_item = None 85 | if item in self.globs.keys() and len(item) == 2: 86 | saved_item = self.globs[item] 87 | self.globs[item] = saved_item.copy() 88 | 89 | lst = timeit.repeat( 90 | func, globals=self.globs, number=self.num, repeat=self.repeat_num 91 | ) 92 | if saved_item: 93 | self.globs[item] = saved_item 94 | table.append(self.get_row(test_name, lst)) 95 | return fmean(lst) 96 | 97 | def print_results(self): 98 | print("\n" + "-" * 50) 99 | print(f"Group Mean: {self.adjust(fmean(self.results))}") 100 | print(f"Group Total: {self.adjust(sum(self.results) * self.repeat_num)}") 101 | print(f"Group Standard Deviation: {self.adjust(pstdev(self.results))}") 102 | print(f"Group Median: {self.adjust(median(sorted(self.results)))}") 103 | 104 | def adjust(self, val: float) -> float: 105 | return round(val * num_from_format(self.time_format), self.precision) 106 | 107 | def get_field_names(self) -> list[str]: 108 | field_names = ["Name"] 109 | if self.show_total: 110 | field_names.append("Total") 111 | if self.show_mean: 112 | field_names.append("Mean") 113 | if self.show_std: 114 | field_names.append("Std Dev") 115 | return field_names 116 | 117 | def get_row(self, test_name: str, lst: list[float]) -> list[str]: 118 | row = [test_name] 119 | if self.show_total: 120 | row.append(self.adjust(sum(lst))) 121 | if self.show_mean: 122 | row.append(self.adjust(fmean(lst))) 123 | if self.show_std: 124 | row.append(self.adjust(pstdev(lst))) 125 | return row 126 | 127 | 128 | class TestSuite: 129 | def __init__( 130 | self, 131 | title: str, 132 | groups: list[tuple[str, list[tuple[str, str]]]], 133 | globs: dict, 134 | num: int = 1_000_000, 135 | repeat_num: int = 10, 136 | time_format="ms", 137 | precision=2, 138 | show_total: bool = True, 139 | show_mean: bool = True, 140 | show_std: bool = True, 141 | ) -> None: 142 | self.title = title 143 | self.groups = [ 144 | TestGroup( 145 | group[0], 146 | group[1], 147 | globs, 148 | time_format, 149 | precision, 150 | num, 151 | repeat_num, 152 | show_total, 153 | show_mean, 154 | show_std, 155 | ) 156 | for group in groups 157 | ] 158 | self.num = num 159 | self.repeat_num = repeat_num 160 | self.results = [] 161 | self.title_fac = int(len(self.title) * 1.5) 162 | self.time_format = time_format 163 | self.precision = precision 164 | self.show_total = show_total 165 | self.show_mean = show_mean 166 | self.show_std = show_std 167 | self.print_info() 168 | 169 | def run_suite(self) -> None: 170 | for group in self.groups: 171 | group.test() 172 | self.results.append(group.results) 173 | self.output_results() 174 | 175 | def print_info(self) -> None: 176 | self.print_title() 177 | self.print_settings() 178 | 179 | def output_results(self) -> None: 180 | self.repeat_char("_") 181 | 182 | accum = 0 183 | for g_results in self.results: 184 | accum += sum(g_results) 185 | print( 186 | f"\nTest suite total time: {self.adjust(accum * self.repeat_num)} " 187 | + self.time_format 188 | ) 189 | 190 | def print_title(self) -> None: 191 | self.repeat_char("=") 192 | print(center_text(self.title.upper(), self.title_fac)) 193 | self.repeat_char("=") 194 | 195 | def print_settings(self) -> None: 196 | print("Test suite settings:") 197 | strs = [ 198 | f"Tests Number: {self.num}", 199 | f"Repeat Number: {self.repeat_num}", 200 | f"Time Format: '{self.time_format}' (available 's', 'ms', 'us', 'ns')", 201 | f"Precision: {self.precision}", 202 | "Single test total: " + yes_no(self.show_total), 203 | "Single test mean: " + yes_no(self.show_mean), 204 | "Single test standard dev: " + yes_no(self.show_std), 205 | self.calculate_data_order(), 206 | ] 207 | for st in strs: 208 | print("- " + st) 209 | print() 210 | 211 | def calculate_data_order(self) -> str: 212 | final_str = "Current data order: " 213 | if self.show_total: 214 | final_str += "TOTAL, " 215 | if self.show_mean: 216 | final_str += "MEAN, " 217 | if self.show_std: 218 | final_str += "STD, " 219 | return final_str[:-2] 220 | 221 | def repeat_char(self, char: str, length: int = None) -> None: 222 | if length is None: 223 | print(char * self.title_fac) 224 | else: 225 | print(char * length) 226 | 227 | def adjust(self, val: float) -> float: 228 | return round(val * num_from_format(self.time_format), self.precision) 229 | -------------------------------------------------------------------------------- /src_c/include/pygame.h: -------------------------------------------------------------------------------- 1 | /* 2 | pygame - Python Game Library 3 | Copyright (C) 2000-2001 Pete Shinners 4 | This library is free software; you can redistribute it and/or 5 | modify it under the terms of the GNU Library General Public 6 | License as published by the Free Software Foundation; either 7 | version 2 of the License, or (at your option) any later version. 8 | This library is distributed in the hope that it will be useful, 9 | but WITHOUT ANY WARRANTY; without even the implied warranty of 10 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 11 | Library General Public License for more details. 12 | You should have received a copy of the GNU Library General Public 13 | License along with this library; if not, write to the Free 14 | Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA 15 | Pete Shinners 16 | pete@shinners.org 17 | */ 18 | 19 | /* 20 | i dont not claim this as my own code, just copied it from the pygame source 21 | */ 22 | 23 | #ifndef _PYGAME_H 24 | #define _PYGAME_H 25 | 26 | #define PY_SSIZE_T_CLEAN 27 | #include 28 | 29 | #ifndef SDL_VERSION_ATLEAST 30 | #ifdef _MSC_VER 31 | typedef unsigned __int8 uint8_t; 32 | typedef unsigned __int32 uint32_t; 33 | #else 34 | #include 35 | #endif 36 | typedef uint32_t Uint32; 37 | typedef uint8_t Uint8; 38 | #endif /* no SDL */ 39 | 40 | /* Ensure PyPy-specific code is not in use when running on GraalPython (PR 41 | * #2580) */ 42 | #if defined(GRAALVM_PYTHON) && defined(PYPY_VERSION) 43 | #undef PYPY_VERSION 44 | #endif 45 | 46 | #define RAISE(x, y) (PyErr_SetString((x), (y)), NULL) 47 | #define DEL_ATTR_NOT_SUPPORTED_CHECK(name, value) \ 48 | do { \ 49 | if (!value) { \ 50 | PyErr_Format(PyExc_AttributeError, "Cannot delete attribute %s", \ 51 | name); \ 52 | return -1; \ 53 | } \ 54 | } while (0) 55 | 56 | #define DEL_ATTR_NOT_SUPPORTED_CHECK_NO_NAME(value) \ 57 | do { \ 58 | if (!value) { \ 59 | PyErr_SetString(PyExc_AttributeError, "Cannot delete attribute"); \ 60 | return -1; \ 61 | } \ 62 | } while (0) 63 | 64 | /* thread check */ 65 | #ifdef WITH_THREAD 66 | #define PG_CHECK_THREADS() (1) 67 | #else /* ~WITH_THREAD */ 68 | #define PG_CHECK_THREADS() \ 69 | (RAISE(PyExc_NotImplementedError, "Python built without thread support")) 70 | #endif /* ~WITH_THREAD */ 71 | 72 | #define PyType_Init(x) (((x).ob_type) = &PyType_Type) 73 | 74 | /* version macros (defined since version 1.9.5) */ 75 | #define PG_MAJOR_VERSION 2 76 | #define PG_MINOR_VERSION 1 77 | #define PG_PATCH_VERSION 3 78 | #define PG_VERSIONNUM(MAJOR, MINOR, PATCH) \ 79 | (1000 * (MAJOR) + 100 * (MINOR) + (PATCH)) 80 | #define PG_VERSION_ATLEAST(MAJOR, MINOR, PATCH) \ 81 | (PG_VERSIONNUM(PG_MAJOR_VERSION, PG_MINOR_VERSION, PG_PATCH_VERSION) >= \ 82 | PG_VERSIONNUM(MAJOR, MINOR, PATCH)) 83 | 84 | #ifndef MIN 85 | #define MIN(a, b) ((a) < (b) ? (a) : (b)) 86 | #endif 87 | #ifndef MAX 88 | #define MAX(a, b) ((a) > (b) ? (a) : (b)) 89 | #endif 90 | #ifndef ABS 91 | #define ABS(a) (((a) < 0) ? -(a) : (a)) 92 | #endif 93 | 94 | #ifndef PG_INLINE 95 | #if defined(__clang__) 96 | #define PG_INLINE __inline__ __attribute__((__unused__)) 97 | #elif defined(__GNUC__) 98 | #define PG_INLINE __inline__ 99 | #elif defined(_MSC_VER) 100 | #define PG_INLINE __inline 101 | #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L 102 | #define PG_INLINE inline 103 | #else 104 | #define PG_INLINE 105 | #endif 106 | #endif /* ~PG_INLINE */ 107 | 108 | // Worth trying this on MSVC/win32 builds to see if provides any speed up 109 | #ifndef PG_FORCEINLINE 110 | #if defined(__clang__) 111 | #define PG_FORCEINLINE __inline__ __attribute__((__unused__)) 112 | #elif defined(__GNUC__) 113 | #define PG_FORCEINLINE __inline__ 114 | #elif defined(_MSC_VER) 115 | #define PG_FORCEINLINE __forceinline 116 | #elif defined(__STDC_VERSION__) && __STDC_VERSION__ >= 199901L 117 | #define PG_FORCEINLINE inline 118 | #else 119 | #define PG_FORCEINLINE 120 | #endif 121 | #endif /* ~PG_FORCEINLINE */ 122 | #define PG_FORCE_INLINE PG_FORCEINLINE 123 | 124 | #define MODINIT_DEFINE(mod_name) PyMODINIT_FUNC PyInit_##mod_name(void) 125 | 126 | #define RELATIVE_MODULE(m) ("." m) 127 | 128 | #define MODPREFIX "" 129 | #define IMPPREFIX "pygame." 130 | 131 | #ifdef __SYMBIAN32__ 132 | #undef MODPREFIX 133 | #undef IMPPREFIX 134 | #define MODPREFIX "pygame_" 135 | #define IMPPREFIX "pygame_" 136 | 137 | #endif /* ~__SYMBIAN32__ */ 138 | 139 | #define PYGAMEAPI_LOCAL_ENTRY "_PYGAME_C_API" 140 | #define PG_CAPSULE_NAME(m) (IMPPREFIX m "." PYGAMEAPI_LOCAL_ENTRY) 141 | #define encapsulate_api(ptr, module) \ 142 | PyCapsule_New(ptr, PG_CAPSULE_NAME(module), NULL) 143 | 144 | // this is not meant to be used in prod 145 | // so we can directly include the base source 146 | #define import_pygame_base() 147 | 148 | #ifndef SDL_VERSION_ATLEAST 149 | typedef struct { 150 | float x, y, w, h; 151 | } SDL_FRect; 152 | typedef struct { 153 | int x, y, w, h; 154 | } SDL_Rect; 155 | #endif /* ~SDL_VERSION_ATLEAST */ 156 | 157 | #include "base.h" 158 | 159 | #ifndef PySequence_FAST_CHECK 160 | #define PySequence_FAST_CHECK(o) (PyList_Check(o) || PyTuple_Check(o)) 161 | #endif /* ~PySequence_FAST_CHECK */ 162 | 163 | #define _LOAD_SLOTS_FROM_PYGAME(module) \ 164 | { \ 165 | PyObject *_mod_##module = PyImport_ImportModule(IMPPREFIX #module); \ 166 | \ 167 | if (_mod_##module != NULL) { \ 168 | PyObject *_c_api = \ 169 | PyObject_GetAttrString(_mod_##module, PYGAMEAPI_LOCAL_ENTRY); \ 170 | \ 171 | Py_DECREF(_mod_##module); \ 172 | if (_c_api != NULL && PyCapsule_CheckExact(_c_api)) { \ 173 | void **localptr = (void **)PyCapsule_GetPointer( \ 174 | _c_api, PG_CAPSULE_NAME(#module)); \ 175 | _PGSLOTS_##module = localptr; \ 176 | } \ 177 | Py_XDECREF(_c_api); \ 178 | } \ 179 | } 180 | 181 | #define PYGAMEAPI_GET_SLOT(module, index) _PGSLOTS_##module[(index)] 182 | 183 | typedef struct { 184 | PyObject_HEAD SDL_Rect r; 185 | PyObject *weakreflist; 186 | } pgRectObject; 187 | 188 | void **_PGSLOTS_rect; 189 | 190 | #define pgRect_AsRect(x) (((pgRectObject *)x)->r) 191 | #define pgRect_Type (*(PyTypeObject *)PYGAMEAPI_GET_SLOT(rect, 0)) 192 | 193 | #define pgRect_Check(x) ((x)->ob_type == &pgRect_Type) 194 | #define pgRect_New (*(PyObject * (*)(SDL_Rect *)) PYGAMEAPI_GET_SLOT(rect, 1)) 195 | 196 | #define pgRect_New4 \ 197 | (*(PyObject * (*)(int, int, int, int)) PYGAMEAPI_GET_SLOT(rect, 2)) 198 | 199 | #define pgRect_FromObject \ 200 | (*(SDL_Rect * (*)(PyObject *, SDL_Rect *)) PYGAMEAPI_GET_SLOT(rect, 3)) 201 | 202 | #define pgRect_Normalize (*(void (*)(SDL_Rect *))PYGAMEAPI_GET_SLOT(rect, 4)) 203 | 204 | #define import_pygame_rect() _LOAD_SLOTS_FROM_PYGAME(rect) 205 | 206 | #ifndef HAVE_IMMINTRIN_H 207 | #if (defined(_M_IX86) || defined(_M_X64) || defined(_M_AMD64)) && \ 208 | (defined(_MSC_VER) && _MSC_VER >= 1600) 209 | #define HAVE_IMMINTRIN_H 1 210 | #elif defined(__has_include) && (defined(__i386__) || defined(__x86_64)) && \ 211 | __has_include() 212 | #define HAVE_IMMINTRIN_H 1 213 | #endif 214 | #endif /* ~HAVE_IMMINTRIN_H */ 215 | 216 | #endif /* ~_PYGAME_H */ 217 | -------------------------------------------------------------------------------- /benchmarks/GEOMETRY_polygon_benchmark.py: -------------------------------------------------------------------------------- 1 | from benchmark_utils import TestSuite 2 | 3 | from geometry import Polygon, regular_polygon 4 | 5 | 6 | # === Tests === 7 | # Each test consists of a tuple of: (name, call) 8 | # The name is a string containing the name of the test 9 | # The call is a string containing the code to be timed 10 | # every test is run CPT times and the average time is 11 | # calculated across REP runs 12 | # the formula is time = (time per CPT calls repeated REP times) / REP 13 | # ==================================================== 14 | 15 | 16 | class ObjWithPolyAttribute: 17 | def __init__(self, polygon=None): 18 | self.polygon = polygon 19 | 20 | 21 | class ObjWithPolyMethodAttribute: 22 | def __init__(self, polygon=None): 23 | self._poly = polygon 24 | 25 | def polygon(self): 26 | return self._poly 27 | 28 | 29 | triang_vertices = [(50, 50), (50, 100), (70, 55)] 30 | GLOB = { 31 | "Polygon": Polygon, 32 | "po3": Polygon(triang_vertices), 33 | "po4": Polygon([(50, 50), (50, 100), (70, 55), (100, 23)]), 34 | "po100": regular_polygon(100, (50, 50), 50), 35 | "p1_i": (50, 50), 36 | "p2_i": (50, 100), 37 | "p3_i": (70, 55), 38 | "p4_i": (100, 23), 39 | "p1_f": (50.0, 50.0), 40 | "p2_f": (50.0, 100.0), 41 | "p3_f": (70.0, 55.0), 42 | "p4_f": (100.0, 23.0), 43 | "poly3_int_list": triang_vertices, 44 | "poly4_int_list": [(50, 50), (50, 100), (70, 55), (100, 23)], 45 | "poly3_float_list": [(50.0, 50.0), (50.0, 100.0), (70.0, 55.0)], 46 | "poly4_float_list": [(50.0, 50.0), (50.0, 100.0), (70.0, 55.0), (100.0, 23.0)], 47 | "poly_attribute": ObjWithPolyAttribute(triang_vertices), 48 | "poly_method_attribute": ObjWithPolyMethodAttribute(triang_vertices), 49 | "poly_attribute_poly": ObjWithPolyAttribute(Polygon(triang_vertices)), 50 | "poly_method_attribute_poly": ObjWithPolyMethodAttribute(Polygon(triang_vertices)), 51 | } 52 | 53 | instatiation_tests = [ 54 | ("(3) polygon", "Polygon(po3)"), 55 | ("(4) polygon", "Polygon(po4)"), 56 | ("(3) int 3 args", "Polygon(p1_i, p2_i, p3_i)"), 57 | ("(3) float 3 args", "Polygon(p1_f, p2_f, p3_f)"), 58 | ("(4) int 4 args", "Polygon(p1_i, p2_i, p3_i, p4_i)"), 59 | ("(4) float 4 args", "Polygon(p1_f, p2_f, p3_f, p4_f)"), 60 | ("(3) int 1 arg", "Polygon(poly3_int_list)"), 61 | ("(3) float 1 arg", "Polygon(poly3_float_list)"), 62 | ("(4) int 1 arg", "Polygon(poly4_int_list)"), 63 | ("(4) float 1 arg", "Polygon(poly4_float_list)"), 64 | ("Polygon from ObjWithPolyAttribute 3", "Polygon(poly_attribute)"), 65 | ("Polygon from ObjWithPolyMethodAttribute 3", "Polygon(poly_method_attribute)"), 66 | ("Polygon from ObjWithPolyAttribute poly 3", "Polygon(poly_attribute_poly)"), 67 | ("Polygon from ObjWithPolyMethodAttribute poly 3", "Polygon(poly_attribute_poly)"), 68 | ] 69 | 70 | getters_tests = [ 71 | ("vertices 3", "po3.vertices"), 72 | ("verts_num 3", "po3.verts_num"), 73 | ("vertices 4", "po4.vertices"), 74 | ("verts_num 4", "po4.verts_num"), 75 | ("centerx 3", "po3.centerx"), 76 | ("centery 3", "po3.centery"), 77 | ("centerx 4", "po4.centerx"), 78 | ("centery 4", "po4.centery"), 79 | ("center 3", "po3.center"), 80 | ("center 4", "po4.center"), 81 | ("perimeter 3", "po3.perimeter"), 82 | ("perimeter 4", "po4.perimeter"), 83 | ("perimeter 100", "po100.perimeter"), 84 | ] 85 | 86 | setters_tests = [ 87 | # ("vertices int", "po4.vertices = [p1_i, p2_i, p3_i, p4_i]"), 88 | # ("vertices float", "po4.vertices = [p1_f, p2_f, p3_f, p4_f]"), 89 | ("center int", "po4.center = p1_i"), 90 | ("center float", "po4.center = p1_f"), 91 | ("centerx int", "po4.centerx = 50"), 92 | ("centery int", "po4.centery = 50"), 93 | ("centerx float", "po4.centerx = 50.0"), 94 | ("centery float", "po4.centery = 50.0"), 95 | ] 96 | 97 | copy_tests = [ 98 | ("copy 3", "po3.copy()"), 99 | ("copy 4", "po4.copy()"), 100 | ] 101 | 102 | move_tests = [ 103 | ("int (x-x)", "po100.move((10, 10))"), 104 | ("int (0-x)", "po100.move((0, 10))"), 105 | ("int (x-0)", "po100.move((10, 0))"), 106 | ("int (0-0)", "po100.move((0, 0))"), 107 | ("float (x-x)", "po100.move((10.0, 10.0))"), 108 | ("float (0-x)", "po100.move((0.0, 10.0))"), 109 | ("float (x-0)", "po100.move((10.0, 0.0))"), 110 | ("float (0-0)", "po100.move((0.0, 0.0))"), 111 | ("int x-x", "po100.move(10, 10)"), 112 | ("int 0-x", "po100.move(0, 10)"), 113 | ("int x-0", "po100.move(10, 0)"), 114 | ("int 0-0", "po100.move(0, 0)"), 115 | ("float x-x", "po100.move(10.0, 10.0)"), 116 | ("float 0-x", "po100.move(0.0, 10.0)"), 117 | ("float x-0", "po100.move(10.0, 0.0)"), 118 | ("float 0-0", "po100.move(0.0, 0.0)"), 119 | ] 120 | 121 | move_ip_tests = [ 122 | ("int (x-x)", "po100.move_ip((10, 10))"), 123 | ("int (0-x)", "po100.move_ip((0, 10))"), 124 | ("int (x-0)", "po100.move_ip((10, 0))"), 125 | ("int (0-0)", "po100.move_ip((0, 0))"), 126 | ("float (x-x)", "po100.move_ip((10.0, 10.0))"), 127 | ("float (0-x)", "po100.move_ip((0.0, 10.0))"), 128 | ("float (x-0)", "po100.move_ip((10.0, 0.0))"), 129 | ("float (0-0)", "po100.move_ip((0.0, 0.0))"), 130 | ("int x-x", "po100.move_ip(10, 10)"), 131 | ("int 0-x", "po100.move_ip(0, 10)"), 132 | ("int x-0", "po100.move_ip(10, 0)"), 133 | ("int 0-0", "po100.move_ip(0, 0)"), 134 | ("float x-x", "po100.move_ip(10.0, 10.0)"), 135 | ("float 0-x", "po100.move_ip(0.0, 10.0)"), 136 | ("float x-0", "po100.move_ip(10.0, 0.0)"), 137 | ("float 0-0", "po100.move_ip(0.0, 0.0)"), 138 | ] 139 | 140 | rotate_tests = [ 141 | ("0", "po100.rotate(0.0)"), 142 | ("-0", "po100.rotate(-0.0)"), 143 | ("90", "po100.rotate(90.0)"), 144 | ("-90", "po100.rotate(-90.0)"), 145 | ("180", "po100.rotate(180.0)"), 146 | ("-180", "po100.rotate(-180.0)"), 147 | ("270", "po100.rotate(270.0)"), 148 | ("-270", "po100.rotate(-270.0)"), 149 | ("360", "po100.rotate(360.0)"), 150 | ("-360", "po100.rotate(-360.0)"), 151 | ("12", "po100.rotate(12.0)"), 152 | ("-12", "po100.rotate(-12.0)"), 153 | ] 154 | 155 | rotate_ip_tests = [ 156 | ("0", "po100.rotate_ip(0.0)"), 157 | ("-0", "po100.rotate_ip(-0.0)"), 158 | ("90", "po100.rotate_ip(90.0)"), 159 | ("-90", "po100.rotate_ip(-90.0)"), 160 | ("180", "po100.rotate_ip(180.0)"), 161 | ("-180", "po100.rotate_ip(-180.0)"), 162 | ("270", "po100.rotate_ip(270.0)"), 163 | ("-270", "po100.rotate_ip(-270.0)"), 164 | ("360", "po100.rotate_ip(360.0)"), 165 | ("-360", "po100.rotate_ip(-360.0)"), 166 | ("12", "po100.rotate_ip(12.0)"), 167 | ("-12", "po100.rotate_ip(-12.0)"), 168 | ] 169 | 170 | collidepoint_tests = [ 171 | ("C int 2", "po100.collidepoint((0, 0))"), 172 | ("NC int 2", "po100.collidepoint((0, 1000))"), 173 | ("C float 2", "po100.collidepoint((0.0, 0.0))"), 174 | ("NC float 2", "po100.collidepoint((0.0, 1000.0))"), 175 | ("C int tuple", "po100.collidepoint((0, 0))"), 176 | ("NC int tuple", "po100.collidepoint((0, 1000))"), 177 | ("C float tuple", "po100.collidepoint((0.0, 0.0))"), 178 | ("NC float tuple", "po100.collidepoint((0.0, 1000.0))"), 179 | ("C int list", "po100.collidepoint([0, 0])"), 180 | ("NC int list", "po100.collidepoint([0, 1000])"), 181 | ("C float list", "po100.collidepoint([0.0, 0.0])"), 182 | ("NC float list", "po100.collidepoint([0.0, 1000.0])"), 183 | ] 184 | 185 | as_rect_tests = [ 186 | ("3", "po3.as_rect()"), 187 | ("4", "po4.as_rect()"), 188 | ("100", "po100.as_rect()"), 189 | ] 190 | 191 | subscript_assignment_tests = [ 192 | ("[0] = 10, int", "po100[0] = (10, 10)"), 193 | ("[0] = 10.0, float", "po100[0] = (10.0, 10.0)"), 194 | ("[10] = 10, int", "po100[10] = (10, 10)"), 195 | ("[10] = 10.0, float", "po100[10] = (10.0, 10.0)"), 196 | ("[-1] = 10, int", "po100[-1] = (10, 10)"), 197 | ("[-1] = 10.0, float", "po100[-1] = (10.0, 10.0)"), 198 | ] 199 | 200 | subscript_tests = [ 201 | ("[0]", "po100[0]"), 202 | ("[10]", "po100[10]"), 203 | ("[100]", "po100[99]"), 204 | ("[-1]", "po100[-1]"), 205 | ] 206 | 207 | scale_tests = [ 208 | ("2.0", "po100.scale(2)"), 209 | ("1.0", "po100.scale(1)"), 210 | ("0.5", "po100.scale(0.5)"), 211 | ("0.1", "po100.scale(0.1)"), 212 | ] 213 | 214 | scale_ip_tests = [ 215 | ("2.0", "po100.scale_ip(2)"), 216 | ("1.0", "po100.scale_ip(1)"), 217 | ("0.5", "po100.scale_ip(0.5)"), 218 | ("0.1", "po100.scale_ip(0.1)"), 219 | ] 220 | 221 | GROUPS = [ 222 | ("Instatiation", instatiation_tests), 223 | ("Attribute Getters", getters_tests), 224 | ("Attribute Setters", setters_tests), 225 | ("Copy", copy_tests), 226 | ("Move", move_tests), 227 | ("Move_ip", move_ip_tests), 228 | ("Rotate", rotate_tests), 229 | ("Rotate_ip", rotate_ip_tests), 230 | ("Collidepoint", collidepoint_tests), 231 | ("Get Bounding Box", as_rect_tests), 232 | ("Subscript", subscript_tests), 233 | ("Subscript Assignment", subscript_assignment_tests), 234 | ("Scale", scale_tests), 235 | ("Scale_ip", scale_ip_tests), 236 | ] 237 | 238 | if __name__ == "__main__": 239 | TestSuite("Geometry Module - Polygon", GROUPS, GLOB).run_suite() 240 | -------------------------------------------------------------------------------- /src_c/include/base.h: -------------------------------------------------------------------------------- 1 | #ifndef _BASE_H 2 | #define _BASE_H 3 | 4 | #include "pygame.h" 5 | 6 | // all of this functions return 1 on success and 0 on failure. 7 | 8 | static PG_FORCE_INLINE int 9 | pg_IntFromObj(PyObject *obj, int *val) 10 | { 11 | if (PyFloat_Check(obj)) { 12 | /* Python3.8 complains with deprecation warnings if we pass 13 | * floats to PyLong_AsLong. 14 | */ 15 | *val = (int)PyFloat_AS_DOUBLE(obj); 16 | return 1; 17 | } 18 | 19 | 20 | *val = PyLong_AsLong(obj); 21 | if (PyErr_Occurred()) { 22 | PyErr_Clear(); 23 | return 0; 24 | } 25 | 26 | return 1; 27 | } 28 | 29 | static PG_FORCE_INLINE int 30 | pg_IntFromObjIndex(PyObject *obj, int _index, int *val) 31 | { 32 | int result = 0; 33 | PyObject *item = PySequence_GetItem(obj, _index); 34 | 35 | if (!item) { 36 | PyErr_Clear(); 37 | return 0; 38 | } 39 | result = pg_IntFromObj(item, val); 40 | Py_DECREF(item); 41 | return result; 42 | } 43 | 44 | static PG_FORCE_INLINE int 45 | pg_TwoIntsFromObj(PyObject *obj, int *val1, int *val2) 46 | { 47 | if (!obj) 48 | return 0; 49 | if (PyTuple_Check(obj) && PyTuple_Size(obj) == 1) { 50 | return pg_TwoIntsFromObj(PyTuple_GET_ITEM(obj, 0), val1, val2); 51 | } 52 | if (!PySequence_Check(obj) || PySequence_Length(obj) != 2) { 53 | return 0; 54 | } 55 | if (!pg_IntFromObjIndex(obj, 0, val1) || 56 | !pg_IntFromObjIndex(obj, 1, val2)) { 57 | return 0; 58 | } 59 | return 1; 60 | } 61 | 62 | static PG_FORCE_INLINE int 63 | pg_FloatFromObj(PyObject *obj, float *val) 64 | { 65 | float f = (float)PyFloat_AsDouble(obj); 66 | 67 | if (f == -1 && PyErr_Occurred()) { 68 | PyErr_Clear(); 69 | return 0; 70 | } 71 | 72 | *val = f; 73 | return 1; 74 | } 75 | 76 | static PG_FORCE_INLINE int 77 | pg_FloatFromObjIndex(PyObject *obj, int _index, float *val) 78 | { 79 | int result = 0; 80 | PyObject *item = PySequence_GetItem(obj, _index); 81 | 82 | if (!item) { 83 | PyErr_Clear(); 84 | return 0; 85 | } 86 | result = pg_FloatFromObj(item, val); 87 | Py_DECREF(item); 88 | return result; 89 | } 90 | 91 | static PG_FORCE_INLINE int 92 | pg_TwoFloatsFromObj(PyObject *obj, float *val1, float *val2) 93 | { 94 | if (PyTuple_Check(obj) && PyTuple_Size(obj) == 1) { 95 | return pg_TwoFloatsFromObj(PyTuple_GET_ITEM(obj, 0), val1, val2); 96 | } 97 | if (!PySequence_Check(obj) || PySequence_Length(obj) != 2) { 98 | return 0; 99 | } 100 | if (!pg_FloatFromObjIndex(obj, 0, val1) || 101 | !pg_FloatFromObjIndex(obj, 1, val2)) { 102 | return 0; 103 | } 104 | return 1; 105 | } 106 | 107 | static PG_FORCE_INLINE int 108 | pg_DoubleFromObj(PyObject *obj, double *val) 109 | { 110 | if (PyFloat_Check(obj)) { 111 | *val = PyFloat_AS_DOUBLE(obj); 112 | return 1; 113 | } 114 | 115 | *val = (double)PyLong_AsLong(obj); 116 | if (PyErr_Occurred()) { 117 | PyErr_Clear(); 118 | return 0; 119 | } 120 | 121 | return 1; 122 | } 123 | 124 | /*Assumes obj is a Sequence, internal or conscious use only*/ 125 | static PG_FORCE_INLINE int 126 | _pg_DoubleFromObjIndex(PyObject *obj, int index, double *val) 127 | { 128 | int result = 0; 129 | 130 | PyObject *item = PySequence_ITEM(obj, index); 131 | if (!item) { 132 | PyErr_Clear(); 133 | return 0; 134 | } 135 | result = pg_DoubleFromObj(item, val); 136 | Py_DECREF(item); 137 | 138 | return result; 139 | } 140 | 141 | static PG_FORCE_INLINE int 142 | pg_DoubleFromObjIndex(PyObject *obj, int index, double *val) 143 | { 144 | int result = 0; 145 | 146 | if ((PyTuple_Check(obj) || PyList_Check(obj)) && 147 | index < PySequence_Fast_GET_SIZE(obj)) { 148 | result = pg_DoubleFromObj(PySequence_Fast_GET_ITEM(obj, index), val); 149 | } 150 | else { 151 | PyObject *item = PySequence_GetItem(obj, index); 152 | 153 | if (!item) { 154 | PyErr_Clear(); 155 | return 0; 156 | } 157 | result = pg_DoubleFromObj(item, val); 158 | Py_DECREF(item); 159 | } 160 | 161 | return result; 162 | } 163 | 164 | static PG_FORCE_INLINE int 165 | pg_TwoDoublesFromObj(PyObject *obj, double *val1, double *val2) 166 | { 167 | Py_ssize_t length; 168 | /*Faster path for tuples and lists*/ 169 | if (PyTuple_Check(obj) || PyList_Check(obj)) { 170 | length = PySequence_Fast_GET_SIZE(obj); 171 | PyObject **f_arr = PySequence_Fast_ITEMS(obj); 172 | if (length == 2) { 173 | if (!pg_DoubleFromObj(f_arr[0], val1) || 174 | !pg_DoubleFromObj(f_arr[1], val2)) { 175 | return 0; 176 | } 177 | } 178 | else if (length == 1) { 179 | /* Handle case of ((x, y), ) 'nested sequence' */ 180 | return pg_TwoDoublesFromObj(f_arr[0], val1, val2); 181 | } 182 | else { 183 | return 0; 184 | } 185 | } 186 | else if (PySequence_Check(obj)) { 187 | length = PySequence_Length(obj); 188 | if (length == 2) { 189 | if (!_pg_DoubleFromObjIndex(obj, 0, val1)) { 190 | return 0; 191 | } 192 | if (!_pg_DoubleFromObjIndex(obj, 1, val2)) { 193 | return 0; 194 | } 195 | } 196 | else if (length == 1 && !PyUnicode_Check(obj)) { 197 | /* Handle case of ((x, y), ) 'nested sequence' */ 198 | PyObject *tmp = PySequence_ITEM(obj, 0); 199 | int ret = pg_TwoDoublesFromObj(tmp, val1, val2); 200 | Py_DECREF(tmp); 201 | return ret; 202 | } 203 | else { 204 | PyErr_Clear(); 205 | return 0; 206 | } 207 | } 208 | else { 209 | return 0; 210 | } 211 | 212 | return 1; 213 | } 214 | 215 | static PG_FORCE_INLINE int 216 | pg_UintFromObj(PyObject *obj, Uint32 *val) 217 | { 218 | if (PyNumber_Check(obj)) { 219 | PyObject *longobj; 220 | 221 | if (!(longobj = PyNumber_Long(obj))) { 222 | PyErr_Clear(); 223 | return 0; 224 | } 225 | *val = (Uint32)PyLong_AsUnsignedLong(longobj); 226 | Py_DECREF(longobj); 227 | if (PyErr_Occurred()) { 228 | PyErr_Clear(); 229 | return 0; 230 | } 231 | return 1; 232 | } 233 | return 0; 234 | } 235 | 236 | static PG_FORCE_INLINE int 237 | pg_UintFromObjIndex(PyObject *obj, int _index, Uint32 *val) 238 | { 239 | int result = 0; 240 | PyObject *item = PySequence_GetItem(obj, _index); 241 | 242 | if (!item) { 243 | PyErr_Clear(); 244 | return 0; 245 | } 246 | result = pg_UintFromObj(item, val); 247 | Py_DECREF(item); 248 | return result; 249 | } 250 | 251 | static PG_FORCE_INLINE int 252 | pg_IndexFromObj(PyObject *obj, Py_ssize_t *val) 253 | { 254 | if (PyIndex_Check(obj)) { 255 | *val = PyNumber_AsSsize_t(obj, NULL); 256 | return 1; 257 | } 258 | return 0; 259 | } 260 | 261 | static PG_FORCE_INLINE int 262 | pg_TwoDoublesFromFastcallArgs(PyObject *const *args, Py_ssize_t nargs, 263 | double *val1, double *val2) 264 | { 265 | if (nargs == 1 && pg_TwoDoublesFromObj(args[0], val1, val2)) { 266 | return 1; 267 | } 268 | else if (nargs == 2 && pg_DoubleFromObj(args[0], val1) && 269 | pg_DoubleFromObj(args[1], val2)) { 270 | return 1; 271 | } 272 | return 0; 273 | } 274 | 275 | // these return PyObject * on success and NULL on failure. 276 | 277 | static PG_FORCE_INLINE PyObject * 278 | pg_TupleFromDoublePair(double val1, double val2) 279 | { 280 | /*this is demonstrated to be faster than Py_BuildValue*/ 281 | PyObject *tuple = PyTuple_New(2); 282 | if (!tuple) 283 | return NULL; 284 | 285 | PyObject *tmp = PyFloat_FromDouble(val1); 286 | if (!tmp) { 287 | Py_DECREF(tuple); 288 | return NULL; 289 | } 290 | PyTuple_SET_ITEM(tuple, 0, tmp); 291 | 292 | tmp = PyFloat_FromDouble(val2); 293 | if (!tmp) { 294 | Py_DECREF(tuple); 295 | return NULL; 296 | } 297 | PyTuple_SET_ITEM(tuple, 1, tmp); 298 | 299 | return tuple; 300 | } 301 | 302 | static PG_FORCE_INLINE PyObject * 303 | pg_TupleFromIntPair(int val1, int val2) 304 | { 305 | /*this is demonstrated to be faster than Py_BuildValue*/ 306 | PyObject *tuple = PyTuple_New(2); 307 | if (!tuple) 308 | return NULL; 309 | 310 | PyObject *tmp = PyLong_FromLong(val1); 311 | if (!tmp) { 312 | Py_DECREF(tuple); 313 | return NULL; 314 | } 315 | PyTuple_SET_ITEM(tuple, 0, tmp); 316 | 317 | tmp = PyLong_FromLong(val2); 318 | if (!tmp) { 319 | Py_DECREF(tuple); 320 | return NULL; 321 | } 322 | PyTuple_SET_ITEM(tuple, 1, tmp); 323 | 324 | return tuple; 325 | } 326 | 327 | static PG_FORCE_INLINE PyObject * 328 | pg_PointList_FromArrayDouble(double *array, int arr_length) 329 | { 330 | /*Takes an even length double array [1, 2, 3, 4, 5, 6, 7, 8] and returns 331 | * a list of points: 332 | * C_arr[1, 2, 3, 4, 5, 6, 7, 8] -> List((1, 2), (3, 4), (5, 6), (7, 8))*/ 333 | 334 | if (arr_length % 2) { 335 | return RAISE(PyExc_ValueError, "array length must be even"); 336 | } 337 | 338 | int num_points = arr_length / 2; 339 | PyObject *sequence = PyList_New(num_points); 340 | if (!sequence) { 341 | return NULL; 342 | } 343 | 344 | int i; 345 | PyObject *point = NULL; 346 | for (i = 0; i < num_points; i++) { 347 | point = pg_TupleFromDoublePair(array[i * 2], array[i * 2 + 1]); 348 | if (!point) { 349 | Py_DECREF(sequence); 350 | return NULL; 351 | } 352 | PyList_SET_ITEM(sequence, i, point); 353 | } 354 | 355 | return sequence; 356 | } 357 | 358 | static PG_FORCE_INLINE PyObject * 359 | pg_PointTuple_FromArrayDouble(double *array, int arr_length) 360 | { 361 | /*Takes an even length double array [1, 2, 3, 4, 5, 6, 7, 8] and returns 362 | * a tuple of points: 363 | * C_arr[1, 2, 3, 4, 5, 6, 7, 8] -> Tuple((1, 2), (3, 4), (5, 6), (7, 8))*/ 364 | 365 | if (arr_length % 2) { 366 | return RAISE(PyExc_ValueError, "array length must be even"); 367 | } 368 | 369 | int num_points = arr_length / 2; 370 | PyObject *sequence = PyTuple_New(num_points); 371 | if (!sequence) { 372 | return NULL; 373 | } 374 | 375 | int i; 376 | PyObject *point = NULL; 377 | for (i = 0; i < num_points; i++) { 378 | point = pg_TupleFromDoublePair(array[i * 2], array[i * 2 + 1]); 379 | if (!point) { 380 | Py_DECREF(sequence); 381 | return NULL; 382 | } 383 | PyTuple_SET_ITEM(sequence, i, point); 384 | } 385 | 386 | return sequence; 387 | } 388 | 389 | #endif /* ~_BASE_H */ 390 | -------------------------------------------------------------------------------- /benchmarks/GEOMETRY_line_benchmark.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | from pygame import Rect 4 | from benchmark_utils import TestSuite 5 | from geometry import Circle 6 | from geometry import Line 7 | 8 | # === Tests === 9 | # Each test consists of a tuple of: (name, call) 10 | # The name is a string containing the name of the test 11 | # The call is a string containing the code to be timed 12 | # every test is run CPT times and the average time is 13 | # calculated across REP runs 14 | # the formula is time = (time per CPT calls repeated REP times) / REP 15 | # ==================================================== 16 | 17 | GLOB = { 18 | "Line": Line, 19 | "l1": Line(0, 0, 10, 10), 20 | "l2": Line(0, 10, 10, 0), 21 | "l3": Line(100, 100, 30, 22), 22 | "l4": Line(0, 0, 1, 1), 23 | "c1": Circle(5, 5, 10), 24 | "c2": Circle(20, 20, 5), 25 | "c3": Circle(0, 0, 150), 26 | "r1": Rect(3, 5, 10, 10), 27 | "r2": Rect(100, 100, 4, 4), 28 | "r3": Rect(-30, -30, 100, 100), 29 | "lines": [ 30 | Line((200, 74), (714, 474)), 31 | Line((275, 203), (622, 172)), 32 | Line((40, 517), (524, 662)), 33 | Line((91, 604), (245, 113)), 34 | Line((212, 165), (734, 386)), 35 | Line((172, 55), (112, 166)), 36 | Line((787, 130), (9, 453)), 37 | Line((637, 5), (423, 768)), 38 | Line((697, 658), (693, 410)), 39 | Line((20, 105), (205, 406)), 40 | ], 41 | "circles": [ 42 | Circle((200, 74), 21), 43 | Circle((275, 203), 89), 44 | Circle((40, 517), 5), 45 | Circle((91, 604), 43), 46 | Circle((212, 165), 13), 47 | Circle((172, 55), 43), 48 | Circle((787, 130), 3), 49 | Circle((637, 5), 133), 50 | Circle((697, 658), 4), 51 | Circle((20, 105), 23), 52 | ], 53 | "rects": [ 54 | Rect(200, 74, 21, 109), 55 | Rect(42, 203, 32, 89), 56 | Rect(40, 517, 5, 5), 57 | Rect(24, 13, 23, 43), 58 | Rect(212, 14, 13, 13), 59 | Rect(44, 55, 43, 231), 60 | Rect(53, 5, 3, 3), 61 | Rect(637, 5, 57, 53), 62 | Rect(654, 658, 4, 87), 63 | Rect(20, 105, 23, 3), 64 | ], 65 | "mixed": [ 66 | Line((200, 42), (755, 474)), 67 | Circle((275, 53), 2), 68 | Rect(40, 517, 5, 5), 69 | Line((91, 604), (5, 42)), 70 | Circle((31, 165), 13), 71 | Rect(172, 55, 64, 231), 72 | Line((787, 52), (9, 453)), 73 | Circle((637, 5), 133), 74 | Rect(697, 658, 33, 87), 75 | Line((20, 555), (113, 12)), 76 | Circle((20, 533), 23), 77 | Rect(20, 13, 6, 3), 78 | ], 79 | } 80 | 81 | instatiation_tests = [ 82 | ("Line", "Line(l1)"), 83 | ("4 int", "Line(0, 0, 10, 10)"), 84 | ("4 float", "Line(0.0, 0.0, 10.0, 10.0)"), 85 | ("2 tuple int", "Line((0, 0), (10, 10))"), 86 | ("2 tuple float", "Line((0.0, 0.0), (10.0, 10.0))"), 87 | ("1 tuple int", "Line((0, 0, 10, 10))"), 88 | ("1 tuple float", "Line((0.0, 0.0, 10.0, 10.0))"), 89 | ("1 tuple 2 sub tuple int", "Line(((0, 0), (10, 10)))"), 90 | ("1 tuple 2 sub tuple float", "Line(((0.0, 0.0), (10.0, 10.0)))"), 91 | ("2 list int", "Line([0, 0], [10, 10])"), 92 | ("2 list float", "Line([0.0, 0.0], [10.0, 10.0])"), 93 | ("1 list int", "Line([0, 0, 10, 10])"), 94 | ("1 list float", "Line([0.0, 0.0, 10.0, 10.0])"), 95 | ("1 list 2 sub list int", "Line([[0, 0], [10, 10]])"), 96 | ("1 list 2 sub list float", "Line([[0.0, 0.0], [10.0, 10.0]])"), 97 | ("1 list 2 sub tuple int", "Line([(0, 0), (10, 10)])"), 98 | ("1 list 2 sub tuple float", "Line([(0.0, 0.0), (10.0, 10.0)])"), 99 | ("1 tuple 2 sub list int", "Line(([0, 0], [10, 10]))"), 100 | ("1 tuple 2 sub list float", "Line(([0.0, 0.0], [10.0, 10.0]))"), 101 | ("4 int tuple inside tuple", "Line(((0, 0, 10, 10),))"), 102 | ("4 float tuple inside tuple", "Line(((0.0, 0.0, 10.0, 10.0),))"), 103 | ("4 int list inside list", "Line([[0, 0, 10, 10]])"), 104 | ("4 float list inside list", "Line([[0.0, 0.0, 10.0, 10.0]])"), 105 | ] 106 | 107 | copy_tests = [ 108 | ("copy", "l1.copy()"), 109 | ] 110 | 111 | conversion_tests = [ 112 | ("as_rect", "l1.as_rect()"), 113 | # ("as_circle", "l1.as_circle()"), 114 | ] 115 | 116 | getters_tests = [ 117 | ("xa", "l1.xa"), 118 | ("ya", "l1.ya"), 119 | ("xb", "l1.xb"), 120 | ("yb", "l1.yb"), 121 | ("a", "l1.a"), 122 | ("b", "l1.b"), 123 | ] 124 | 125 | setters_tests = [ 126 | ("xa int", "l1.xa = 1"), 127 | ("xa float", "l1.xa = 1.0"), 128 | ("ya int", "l1.ya = 1"), 129 | ("ya float", "l1.ya = 1.0"), 130 | ("xb int", "l1.xb = 1"), 131 | ("xb float", "l1.xb = 1.0"), 132 | ("yb int", "l1.yb = 1"), 133 | ("yb float", "l1.yb = 1.0"), 134 | ("a int", "l1.a = (1, 1)"), 135 | ("a float", "l1.a = (1.0, 1.0)"), 136 | ("b int", "l1.b = (1, 1)"), 137 | ("b float", "l1.b = (1.0, 1.0)"), 138 | ] 139 | 140 | update_tests = [ 141 | ("line", "l1.update(l2)"), 142 | ("4 int", "l1.update(1, 1, 3, 3)"), 143 | ("4 float", "l1.update(1.0, 1.0, 3.0, 3.0)"), 144 | ("1 tup 4 int", "l1.update((1, 1, 3, 3))"), 145 | ("1 tup 4 float", "l1.update((1.0, 1.0, 3.0, 3.0))"), 146 | ("1 tup 2 subtups int", "l1.update(((1, 1), (3, 3)))"), 147 | ("1 tup 2 subtups float", "l1.update(((1.0, 1.0), (3.0, 3.0)))"), 148 | ("2 tup int", "l1.update((1, 1), (3, 3))"), 149 | ("2 tup float", "l1.update((1.0, 1.0), (3.0, 3.0))"), 150 | ] 151 | 152 | move_tests = [ 153 | ("2 int", "l1.move(1, 1)"), 154 | ("2 float", "l1.move(1.0, 1.0)"), 155 | ("tuple int", "l1.move((1, 1))"), 156 | ("tuple float", "l1.move((1.0, 1.0))"), 157 | ] 158 | 159 | move_ip_tests = [ 160 | ("2 int", "l1.move_ip(1, 1)"), 161 | ("2 float", "l1.move_ip(1.0, 1.0)"), 162 | ("tuple int", "l1.move_ip((1, 1))"), 163 | ("tuple float", "l1.move_ip((1.0, 1.0))"), 164 | ] 165 | 166 | collideline_tests = [ 167 | ("C line", "l1.collideline(l2)"), 168 | ("NC line", "l1.collideline(l3)"), 169 | ("C 4 int", "l1.collideline(1, 1, 3, 3)"), 170 | ("NC 4 int", "l1.collideline(1, 1, 3, 4)"), 171 | ("C 4 float", "l1.collideline(1.0, 1.0, 3.0, 3.0)"), 172 | ("NC 4 float", "l1.collideline(1.0, 1.0, 3.0, 4.0)"), 173 | ("C 1 tup 4 int", "l1.collideline((1, 1, 3, 3))"), 174 | ("NC 1 tup 4 int", "l1.collideline((1, 1, 3, 4))"), 175 | ("C 1 tup 4 float", "l1.collideline((1.0, 1.0, 3.0, 3.0))"), 176 | ("NC 1 tup 4 float", "l1.collideline((1.0, 1.0, 3.0, 4.0))"), 177 | ("C 1 tup 2 subtups int", "l1.collideline(((1, 1), (3, 3)))"), 178 | ("NC 1 tup 2 subtups int", "l1.collideline(((1, 1), (3, 4)))"), 179 | ("C 1 tup 2 subtups float", "l1.collideline(((1.0, 1.0), (3.0, 3.0)))"), 180 | ("NC 1 tup 2 subtups float", "l1.collideline(((1.0, 1.0), (3.0, 4.0)))"), 181 | ("C 2 tup int", "l1.collideline((1, 1), (3, 3))"), 182 | ("NC 2 tup int", "l1.collideline((1, 1), (3, 4))"), 183 | ("C 2 tup float", "l1.collideline((1.0, 1.0), (3.0, 3.0))"), 184 | ("NC 2 tup float", "l1.collideline((1.0, 1.0), (3.0, 4.0))"), 185 | ] 186 | 187 | collidecircle_tests = [ 188 | ("C circle", "l1.collidecircle(c1)"), 189 | ("NC circle", "l1.collidecircle(c2)"), 190 | ("inside circle", "l1.collidecircle(c3)"), 191 | ("C 3 int", "l1.collidecircle(5, 5, 10)"), 192 | ("NC 3 int", "l1.collidecircle(20, 20, 5)"), 193 | ("inside 3 int", "l1.collidecircle(0, 0, 150)"), 194 | ("C 3 float", "l1.collidecircle(5.0, 5.0, 10.0)"), 195 | ("NC 3 float", "l1.collidecircle(20.0, 20.0, 5.0)"), 196 | ("inside 3 float", "l1.collidecircle(0.0, 0.0, 150.0)"), 197 | ("C 1 tup 3 int", "l1.collidecircle((5, 5, 10))"), 198 | ("NC 1 tup 3 int", "l1.collidecircle((20, 20, 5))"), 199 | ("inside 1 tup 3 int", "l1.collidecircle((0, 0, 150))"), 200 | ("C 1 tup 3 float", "l1.collidecircle((5.0, 5.0, 10.0))"), 201 | ("NC 1 tup 3 float", "l1.collidecircle((20.0, 20.0, 5.0))"), 202 | ("inside 1 tup 3 float", "l1.collidecircle((0.0, 0.0, 150.0))"), 203 | ("C 2 tup int", "l1.collidecircle((5, 5), 10)"), 204 | ("NC 2 tup int", "l1.collidecircle((20, 20), 5)"), 205 | ("inside 2 tup int", "l1.collidecircle((0, 0), 150)"), 206 | ("C 2 tup float", "l1.collidecircle((5.0, 5.0), 10.0)"), 207 | ("NC 2 tup float", "l1.collidecircle((20.0, 20.0), 5.0)"), 208 | ("inside 2 tup float", "l1.collidecircle((0.0, 0.0), 150.0)"), 209 | ] 210 | 211 | colliderect_tests = [ 212 | ("C rect", "l1.colliderect(r1)"), 213 | ("NC rect", "l1.colliderect(r2)"), 214 | ("inside rect", "l1.colliderect(r3)"), 215 | ("C 4 int", "l1.colliderect(3, 5, 10, 10)"), 216 | ("NC 4 int", "l1.colliderect(100, 100, 4, 4)"), 217 | ("inside 4 int", "l1.colliderect(-30, -30, 100, 100)"), 218 | ("C 4 float", "l1.colliderect(3.0, 5.0, 10.0, 10.0)"), 219 | ("NC 4 float", "l1.colliderect(100.0, 100.0, 4.0, 4.0)"), 220 | ("inside 4 float", "l1.colliderect(-30.0, -30.0, 100.0, 100.0)"), 221 | ("C 1 tup 4 int", "l1.colliderect((3, 5, 10, 10))"), 222 | ("NC 1 tup 4 int", "l1.colliderect((100, 100, 4, 4))"), 223 | ("inside 1 tup 4 int", "l1.colliderect((-30, -30, 100, 100))"), 224 | ("C 1 tup 4 float", "l1.colliderect((3.0, 5.0, 10.0, 10.0))"), 225 | ("NC 1 tup 4 float", "l1.colliderect((100.0, 100.0, 4.0, 4.0))"), 226 | ("inside 1 tup 4 float", "l1.colliderect((-30.0, -30.0, 100.0, 100.0))"), 227 | ("C 1 tup 2 subtups int", "l1.colliderect(((3, 5), (10, 10)))"), 228 | ("NC 1 tup 2 subtups int", "l1.colliderect(((100, 100), (4, 4)))"), 229 | ("inside 1 tup 2 subtups int", "l1.colliderect(((-30, -30), (100, 100)))"), 230 | ("C 1 tup 2 subtups float", "l1.colliderect(((3.0, 5.0), (10.0, 10.0)))"), 231 | ("NC 1 tup 2 subtups float", "l1.colliderect(((100.0, 100.0), (4.0, 4.0)))"), 232 | ( 233 | "inside 1 tup 2 subtups float", 234 | "l1.colliderect(((-30.0, -30.0), (100.0, 100.0)))", 235 | ), 236 | ] 237 | 238 | collidepoint_tests = [ 239 | ("C 1 int", "l4.collidepoint((1, 1))"), 240 | ("NC 1 int", "l4.collidepoint((3, 3))"), 241 | ("C 1 float", "l4.collidepoint((1.0, 1.0))"), 242 | ("NC 1 float", "l4.collidepoint((3.0, 3.0))"), 243 | ("C 2 int", "l4.collidepoint(1, 1)"), 244 | ("NC 2 int", "l4.collidepoint(3, 3)"), 245 | ("C 2 float", "l4.collidepoint(1.0, 1.0)"), 246 | ("NC 2 float", "l4.collidepoint(3.0, 3.0)"), 247 | ] 248 | 249 | raycast_tests = [ 250 | ("lines-only", "l1.raycast(lines)"), 251 | ("circles-only", "l1.raycast(circles)"), 252 | ("rects-only", "l1.raycast(rects)"), 253 | ("mixed", "l1.raycast(mixed)"), 254 | ] 255 | 256 | is_parallel_tests = [ 257 | ("parallel", "l1.parallel(l4)"), 258 | ("not parallel", "l1.parallel(l2)"), 259 | ] 260 | 261 | is_perpendicular_tests = [ 262 | ("perp.", "l1.is_perpendicular(l3)"), 263 | ("not perp.", "l1.is_perpendicular(l4)"), 264 | ] 265 | 266 | # === Test Suites === 267 | # If you want to add more tests to a suite, just add them to the list 268 | # If you want to remove or skip tests from a suite, just remove or comment them out 269 | GROUPS = [ 270 | ("Instatiation", instatiation_tests), 271 | ("Attribute Getters", getters_tests), 272 | ("Attribute Setters", setters_tests), 273 | ("Copy", copy_tests), 274 | ("Conversion", conversion_tests), 275 | ("Update", update_tests), 276 | ("Move", move_tests), 277 | ("Move_ip", move_ip_tests), 278 | ("Collision: Line", collideline_tests), 279 | ("Collision: Circle", collidecircle_tests), 280 | ("Collision: Rect", colliderect_tests), 281 | ("Collision: Point", collidepoint_tests), 282 | ("Raycast", raycast_tests), 283 | ("Parallel", is_parallel_tests), 284 | ("Perpendicular", is_perpendicular_tests), 285 | ] 286 | 287 | TestSuite("Geometry Module - Line", GROUPS, GLOB).run_suite() 288 | -------------------------------------------------------------------------------- /geometry.pyi: -------------------------------------------------------------------------------- 1 | from typing import ( 2 | Optional, 3 | Tuple, 4 | Sequence, 5 | overload, 6 | Union, 7 | Callable, 8 | List, 9 | Iterator, 10 | ) 11 | 12 | from pygame._common import RectValue 13 | from typing_extensions import Literal as Literal 14 | from typing_extensions import Protocol 15 | 16 | from pygame.math import Vector2, Vector3 17 | from pygame.rect import Rect 18 | 19 | Coordinate = Union[Sequence[float, float], Vector2, Sequence[int, int]] 20 | 21 | Shape = Union["Line", "Circle", "Rect", "Polygon"] 22 | 23 | _CanBeLine = Union[ 24 | Rect, 25 | Line, 26 | Tuple[float, float, float, float], 27 | Tuple[Coordinate, Coordinate], 28 | Sequence[float], 29 | Sequence[Coordinate], 30 | ] 31 | 32 | class _HasLineAttribute(Protocol): 33 | # An object that has a line attribute that is either a line, or a function 34 | # that returns a line confirms to the rect protocol 35 | line: Union[LineValue, Callable[[], LineValue]] 36 | 37 | LineValue = Union[_CanBeLine, _HasLineAttribute] 38 | 39 | _CanBeCircle = Union[Vector3, Circle, Tuple[float, float, float], Sequence[float]] 40 | _CanBeCollided = Union[Circle, Rect, Line, Polygon, Sequence[int, int]] 41 | 42 | class _HasCirclettribute(Protocol): 43 | # An object that has a circle attribute that is either a circle, or a function 44 | # that returns a circle 45 | circle: Union[CircleValue, Callable[[], CircleValue]] 46 | 47 | CircleValue = Union[_CanBeCircle, _HasCirclettribute] 48 | 49 | class Line(Sequence[float]): 50 | xa: float 51 | ya: float 52 | xb: float 53 | yb: float 54 | a: Tuple[float, float] 55 | b: Tuple[float, float] 56 | length: float 57 | center: Tuple[float, float] 58 | centerx: float 59 | centery: float 60 | slope: float 61 | __safe_for_unpickling__: Literal[True] 62 | __hash__: None # type: ignore 63 | 64 | @overload 65 | def __init__(self, line: Line) -> None: ... 66 | @overload 67 | def __init__(self, xa: float, ya: float, xb: float, yb: float) -> None: ... 68 | @overload 69 | def __init__(self, first: Sequence[float], second: Sequence[float]) -> None: ... 70 | @overload 71 | def __init__(self, single_arg: LineValue) -> None: ... 72 | def __len__(self) -> Literal[4]: ... 73 | def __iter__(self) -> Iterator[float]: ... 74 | @overload 75 | def __getitem__(self, i: int) -> float: ... 76 | @overload 77 | def __getitem__(self, s: slice) -> List[float]: ... 78 | @overload 79 | def __setitem__(self, key: int, value: float) -> None: ... 80 | @overload 81 | def __setitem__(self, key: slice, value: Union[float, Line]) -> None: ... 82 | def __copy__(self) -> Line: ... 83 | 84 | copy = __copy__ 85 | 86 | @overload 87 | def update(self, xa: float, ya: float, xb: float, yb: float) -> None: ... 88 | @overload 89 | def update(self, a: Coordinate, b: Coordinate) -> None: ... 90 | @overload 91 | def update(self, single_arg: LineValue) -> None: ... 92 | def collideswith(self, other: _CanBeCollided) -> bool: ... 93 | @overload 94 | def collidepoint(self, x: float, y: float) -> bool: ... 95 | @overload 96 | def collidepoint(self, x_y: Coordinate) -> bool: ... 97 | @overload 98 | def collideline(self, line: Line) -> bool: ... 99 | @overload 100 | def collideline(self, xa: float, ya: float, xb: float, yb: float) -> bool: ... 101 | @overload 102 | def collideline(self, first: Sequence[float], second: Sequence[float]) -> bool: ... 103 | def colliderect(self, rect: RectValue) -> bool: ... 104 | @overload 105 | def colliderect(self, left_top: Coordinate, width_height: Coordinate) -> bool: ... 106 | @overload 107 | def colliderect( 108 | self, left: float, top: float, width: float, height: float 109 | ) -> bool: ... 110 | @overload 111 | def collidecircle(self, circle: CircleValue) -> bool: ... 112 | @overload 113 | def collidecircle(self, x: float, y: float, r: float) -> bool: ... 114 | def collidepolygon(self, polygon: Polygon, only_edges: bool = False) -> bool: ... 115 | def as_circle(self) -> Circle: ... 116 | def as_rect(self) -> Rect: ... 117 | @overload 118 | def move(self, x: float, y: float) -> Line: ... 119 | @overload 120 | def move(self, move_by: Coordinate) -> Line: ... 121 | @overload 122 | def move_ip(self, x: float, y: float) -> None: ... 123 | @overload 124 | def move_ip(self, move_by: Coordinate) -> None: ... 125 | def at(self, weight: float) -> Tuple[float, float]: ... 126 | @overload 127 | def scale(self, factor: float, origin: float) -> Line: ... 128 | @overload 129 | def scale_ip(self, factor: float, origin: float) -> None: ... 130 | @overload 131 | def scale(self, factor_and_origin: Sequence[float, float]) -> Line: ... 132 | @overload 133 | def scale_ip(self, factor_and_origin: Sequence[float, float]) -> None: ... 134 | def flip_ab(self) -> Line: ... 135 | def flip_ab_ip(self) -> None: ... 136 | def is_parallel(self, line: LineValue) -> bool: ... 137 | def is_perpendicular(self, line: LineValue) -> bool: ... 138 | def as_points(self, n_points: int) -> List[Tuple[float, float]]: ... 139 | def as_segments(self, n_segments: int) -> List[Line]: ... 140 | def rotate( 141 | self, angle: float, rotation_point: Coordinate = Line.center 142 | ) -> Line: ... 143 | def rotate_ip( 144 | self, angle: float, rotation_point: Coordinate = Line.center 145 | ) -> None: ... 146 | 147 | class Circle: 148 | x: float 149 | y: float 150 | r: float 151 | r_sqr: float 152 | d: float 153 | diameter: float 154 | area: float 155 | circumference: float 156 | center: Tuple[float, float] 157 | top: Tuple[float, float] 158 | left: Tuple[float, float] 159 | right: Tuple[float, float] 160 | bottom: Tuple[float, float] 161 | __safe_for_unpickling__: Literal[True] 162 | __hash__: None # type: ignore 163 | 164 | @overload 165 | def __init__(self, x: float, y: float, r: float) -> None: ... 166 | @overload 167 | def __init__(self, pos: Sequence[float], r: float) -> None: ... 168 | @overload 169 | def __init__(self, circle: CircleValue) -> None: ... 170 | @overload 171 | def __init__(self, single_arg: CircleValue) -> None: ... 172 | @overload 173 | def collidecircle(self, circle: CircleValue) -> bool: ... 174 | @overload 175 | def collidecircle(self, x: float, y: float, r: float) -> bool: ... 176 | @overload 177 | def collideline(self, line: LineValue) -> bool: ... 178 | @overload 179 | def collideline(self, xa: float, ya: float, xb: float, yb: float) -> bool: ... 180 | @overload 181 | def collidepoint(self, x: float, y: float) -> bool: ... 182 | @overload 183 | def collidepoint(self, point: Coordinate) -> bool: ... 184 | @overload 185 | def colliderect(self, rect: Rect) -> bool: ... 186 | @overload 187 | def colliderect(self, x: int, y: int, w: int, h: int) -> bool: ... 188 | def collideswith(self, other: _CanBeCollided) -> bool: ... 189 | def collidelist(self, colliders: Sequence[_CanBeCollided]) -> int: ... 190 | def collidelistall(self, colliders: Sequence[_CanBeCollided]) -> List[int]: ... 191 | def __copy__(self) -> Circle: ... 192 | 193 | copy = __copy__ 194 | 195 | def as_rect(self) -> Rect: ... 196 | @overload 197 | def update(self, circle: CircleValue) -> None: ... 198 | @overload 199 | def update(self, x: float, y: float, r: float) -> None: ... 200 | @overload 201 | def move(self, x: float, y: float) -> Circle: ... 202 | @overload 203 | def move(self, move_by: Coordinate) -> Circle: ... 204 | @overload 205 | def move_ip(self, x: float, y: float) -> None: ... 206 | @overload 207 | def move_ip(self, move_by: Coordinate) -> None: ... 208 | def contains(self, shape: Shape) -> bool: ... 209 | @overload 210 | def collidepolygon( 211 | self, polygon: Union[Polygon, Sequence[Coordinate]], only_edges: bool = False 212 | ) -> bool: ... 213 | @overload 214 | def collidepolygon(self, *coords, only_edges: bool = False) -> bool: ... 215 | def rotate( 216 | self, angle: float, rotation_point: Coordinate = Circle.center 217 | ) -> Circle: ... 218 | def rotate_ip( 219 | self, angle: float, rotation_point: Coordinate = Circle.center 220 | ) -> None: ... 221 | def intersect(self, other: Circle) -> List[Tuple[float, float]]: ... 222 | 223 | class Polygon: 224 | vertices: List[Coordinate] 225 | verts_num: int 226 | perimeter: float 227 | centerx: float 228 | centery: float 229 | center: Tuple[float, float] 230 | area: float 231 | __safe_for_unpickling__: Literal[True] 232 | __hash__: None # type: ignore 233 | 234 | @overload 235 | def __init__(self, vertices: Sequence[Coordinate]) -> None: ... 236 | @overload 237 | def __setitem__(self, key: int, value: Coordinate) -> None: ... 238 | @overload 239 | def __getitem__(self, i: int) -> Sequence[Coordinate]: ... 240 | @overload 241 | def __init__(self, *args) -> None: ... 242 | @overload 243 | def __init__(self, polygon: Polygon) -> None: ... 244 | def __copy__(self) -> Polygon: ... 245 | 246 | copy = __copy__ 247 | 248 | def rotate( 249 | self, angle: float, rotation_point: Coordinate = Polygon.center 250 | ) -> Polygon: ... 251 | def rotate_ip( 252 | self, angle: float, rotation_point: Coordinate = Polygon.center 253 | ) -> None: ... 254 | @overload 255 | def move(self, x: float, y: float) -> Polygon: ... 256 | @overload 257 | def move(self, move_by: Coordinate) -> Polygon: ... 258 | @overload 259 | def as_segments(self) -> List[Line]: ... 260 | @overload 261 | def move_ip(self, x: float, y: float) -> None: ... 262 | @overload 263 | def move_ip(self, move_by: Coordinate) -> None: ... 264 | def collidepoint(self, x: float, y: float) -> bool: ... 265 | @overload 266 | def collidepoint(self, point: Coordinate) -> bool: ... 267 | def collideline(self, line: LineValue, only_edges: bool = False) -> bool: ... 268 | def as_rect(self) -> Rect: ... 269 | def is_convex(self) -> bool: ... 270 | @overload 271 | def collidecircle(self, polygon: CircleValue, only_edges: bool = False) -> bool: ... 272 | @overload 273 | def collidecircle(self, *circle, only_edges: bool = False) -> bool: ... 274 | def insert_vertex(self, index: int, vertex: Coordinate) -> None: ... 275 | def remove_vertex(self, index: int) -> None: ... 276 | def pop_vertex(self, index: int) -> Coordinate: ... 277 | def scale(self, factor: float) -> Polygon: ... 278 | def scale_ip(self, factor: float) -> None: ... 279 | 280 | def regular_polygon( 281 | sides: int, center: Coordinate, radius: float, angle: float = 0 282 | ) -> Polygon: ... 283 | @overload 284 | def raycast( 285 | origin: Coordinate, 286 | direction: Coordinate, 287 | max_dist: float, 288 | colliders: Sequence[Union[Rect, Circle, Line]], 289 | ) -> Optional[Tuple[float, float]]: ... 290 | @overload 291 | def raycast( 292 | origin: Coordinate, 293 | angle: float, 294 | max_dist: float, 295 | colliders: Sequence[Union[Rect, Circle, Line]], 296 | ) -> Optional[Tuple[float, float]]: ... 297 | 298 | Ray = Union[ 299 | Line, 300 | Tuple[Coordinate, Coordinate, float], 301 | Tuple[Coordinate, float, float], 302 | Tuple[Coordinate, Coordinate], 303 | ] 304 | 305 | @overload 306 | def raycast( 307 | line: Line, 308 | colliders: Sequence[Union[Rect, Circle, Line]], 309 | ) -> Optional[Tuple[float, float]]: ... 310 | @overload 311 | def multiraycast( 312 | rays: Sequence[Ray], 313 | colliders: Sequence[Union[Rect, Circle, Line]], 314 | ) -> Sequence[Optional[Tuple[float, float]]]: ... 315 | def rect_to_polygon(rect: Rect) -> Polygon: ... 316 | def is_line(obj) -> bool: ... 317 | def is_circle(obj) -> bool: ... 318 | def is_polygon(obj) -> bool: ... 319 | -------------------------------------------------------------------------------- /src_c/simd_collisions_avx2.c: -------------------------------------------------------------------------------- 1 | #include "include/pygame.h" 2 | #include "simd_collisions.h" 3 | #include 4 | 5 | #if defined(_MSC_VER) 6 | #include 7 | #include 8 | #endif 9 | 10 | #if defined(HAVE_IMMINTRIN_H) && !defined(SDL_DISABLE_IMMINTRIN_H) 11 | #include 12 | #endif /* defined(HAVE_IMMINTRIN_H) && !defined(SDL_DISABLE_IMMINTRIN_H) */ 13 | 14 | PG_FORCEINLINE static int 15 | pg_HasAVX2(void) 16 | { 17 | // The check is cached. 18 | static int has_avx2 = -1; 19 | if (has_avx2 != -1) 20 | return has_avx2; 21 | 22 | #if AVX2_IS_SUPPORTED 23 | #if defined(__GNUC__) 24 | // Reference: 25 | // https://gcc.gnu.org/onlinedocs/gcc-4.8.2/gcc/X86-Built-in-Functions.html 26 | has_avx2 = __builtin_cpu_supports("avx2"); 27 | #elif defined(_MSC_VER) 28 | // Reference: 29 | // https://learn.microsoft.com/en-us/cpp/intrinsics/cpuid-cpuidex?view=msvc-170 30 | 31 | int cpu_info[4]; 32 | __cpuid(cpu_info, 0); 33 | 34 | int info_n = cpu_info[0]; 35 | int *data = (int *)_alloca(sizeof(int) * info_n * 4); 36 | // int data[info_n][4]; 37 | 38 | for (int i = 0; i <= info_n; i++) { 39 | __cpuidex(cpu_info, i, 0); 40 | 41 | // memcpy(&data[i], cpu_info, sizeof(int) * 4); 42 | memcpy(data + i * 4, cpu_info, sizeof(int) * 4); 43 | } 44 | 45 | // has_avx2 = data[7][1] >> 5 & 1; 46 | has_avx2 = data[7 * 4 + 1] >> 5 & 1; 47 | #else 48 | has_avx2 = 0; 49 | #endif 50 | #else 51 | has_avx2 = 0; 52 | #endif /* ~__AVX2__ */ 53 | 54 | return has_avx2; 55 | } 56 | 57 | #if AVX2_IS_SUPPORTED 58 | PG_FORCEINLINE static int 59 | pgIntersection_LineRect_avx2(pgLineBase *line, SDL_Rect *rect, double *X, 60 | double *Y, double *T) 61 | { 62 | double Rx = (double)rect->x; 63 | double Ry = (double)rect->y; 64 | double Rw = (double)rect->w; 65 | double Rh = (double)rect->h; 66 | 67 | // here we start to setup the variables 68 | __m256d x1_256d = _mm256_set1_pd(line->xa); 69 | __m256d y1_256d = _mm256_set1_pd(line->ya); 70 | __m256d x2_256d = _mm256_set1_pd(line->xb); 71 | __m256d y2_256d = _mm256_set1_pd(line->yb); 72 | __m256d x3_256d = _mm256_set_pd(Rx, Rx, Rx, Rx + Rw); 73 | __m256d y3_256d = _mm256_set_pd(Ry, Ry, Ry + Rh, Ry); 74 | __m256d x4_256d = _mm256_set_pd(Rx + Rw, Rx, Rx + Rw, Rx + Rw); 75 | __m256d y4_256d = _mm256_set_pd(Ry, Ry + Rh, Ry + Rh, Ry + Rh); 76 | 77 | // here we calculate the differences between the the coords of the points 78 | __m256d x1_m_x2_256d = _mm256_sub_pd(x1_256d, x2_256d); 79 | __m256d y3_m_y4_256d = _mm256_sub_pd(y3_256d, y4_256d); 80 | __m256d y1_m_y2_256d = _mm256_sub_pd(y1_256d, y2_256d); 81 | __m256d x3_m_x4_256d = _mm256_sub_pd(x3_256d, x4_256d); 82 | 83 | // we calculate the denominator of the equations 84 | __m256d den_256d = 85 | _mm256_sub_pd(_mm256_mul_pd(x1_m_x2_256d, y3_m_y4_256d), 86 | _mm256_mul_pd(y1_m_y2_256d, x3_m_x4_256d)); 87 | 88 | // if the denominator is 0 then the line is parallel to the other line 89 | // in this occasion this can't be true here as a line will never be 90 | // parallel to all four sides of a rectangle 91 | __m256d den_zero_256d = 92 | _mm256_cmp_pd(den_256d, _mm256_setzero_pd(), _CMP_EQ_OQ); 93 | 94 | // we dont want to cause any floating point errors by dividing by 0 95 | // so we set the ones that are equal to 0 to 1 96 | den_256d = _mm256_or_pd(den_zero_256d, den_256d); 97 | 98 | // we calculate the rest of the differences between the coords of the 99 | // points 100 | __m256d x1_m_x3_256d = _mm256_sub_pd(x1_256d, x3_256d); 101 | __m256d y1_m_y3_256d = _mm256_sub_pd(y1_256d, y3_256d); 102 | 103 | // calculate the t values 104 | __m256d t_256d = _mm256_sub_pd(_mm256_mul_pd(x1_m_x3_256d, y3_m_y4_256d), 105 | _mm256_mul_pd(y1_m_y3_256d, x3_m_x4_256d)); 106 | t_256d = _mm256_div_pd(t_256d, den_256d); 107 | 108 | // calculate the u values 109 | __m256d u_256d = _mm256_sub_pd(_mm256_mul_pd(x1_m_x2_256d, y1_m_y3_256d), 110 | _mm256_mul_pd(y1_m_y2_256d, x1_m_x3_256d)); 111 | u_256d = 112 | _mm256_mul_pd(_mm256_div_pd(u_256d, den_256d), _mm256_set1_pd(-1.0)); 113 | 114 | // we check this condition t >= 0 && t <= 1 && u >= 0 && u <= 1 115 | __m256d ones_256d = _mm256_set1_pd(1.0); 116 | __m256d zeros_256d = _mm256_set1_pd(0.0); 117 | __m256d t_zero_256d = _mm256_cmp_pd(t_256d, zeros_256d, _CMP_GE_OQ); 118 | __m256d t_one_256d = _mm256_cmp_pd(t_256d, ones_256d, _CMP_LE_OQ); 119 | __m256d u_zero_256d = _mm256_cmp_pd(u_256d, zeros_256d, _CMP_GE_OQ); 120 | __m256d u_one_256d = _mm256_cmp_pd(u_256d, ones_256d, _CMP_LE_OQ); 121 | __m256d condition_256d = 122 | _mm256_and_pd(_mm256_and_pd(t_zero_256d, t_one_256d), 123 | _mm256_and_pd(u_zero_256d, u_one_256d)); 124 | 125 | // if no lines touch the rectangle then this will be true 126 | if (_mm256_movemask_pd(condition_256d) == 0x0) { 127 | return 0; 128 | } 129 | 130 | __m256d blended_256d = 131 | _mm256_blendv_pd(_mm256_set1_pd(DBL_MAX), t_256d, condition_256d); 132 | 133 | __m128d min_256d = _mm_min_pd(_mm256_extractf128_pd(blended_256d, 0), 134 | _mm256_extractf128_pd(blended_256d, 1)); 135 | double *min_ptr = (double *)&min_256d; 136 | 137 | double t = min_ptr[0] < min_ptr[1] ? min_ptr[0] : min_ptr[1]; 138 | 139 | // outputs 140 | if (T) 141 | *T = t; 142 | if (X) 143 | *X = line->xa + t * (line->xb - line->xa); 144 | if (Y) 145 | *Y = line->ya + t * (line->yb - line->ya); 146 | 147 | return 1; 148 | } 149 | 150 | PG_FORCEINLINE static int 151 | pgCollision_RectLine_avx2(SDL_Rect *rect, pgLineBase *line) 152 | { 153 | double Rx = (double)rect->x; 154 | double Ry = (double)rect->y; 155 | double Rw = (double)rect->w; 156 | double Rh = (double)rect->h; 157 | 158 | // here we start to setup the variables 159 | __m256d x1_256d = _mm256_set1_pd(line->xa); 160 | __m256d y1_256d = _mm256_set1_pd(line->ya); 161 | __m256d x2_256d = _mm256_set1_pd(line->xb); 162 | __m256d y2_256d = _mm256_set1_pd(line->yb); 163 | __m256d x3_256d = _mm256_set_pd(Rx, Rx, Rx, Rx + Rw); 164 | __m256d y3_256d = _mm256_set_pd(Ry, Ry, Ry + Rh, Ry); 165 | __m256d x4_256d = _mm256_set_pd(Rx + Rw, Rx, Rx + Rw, Rx + Rw); 166 | __m256d y4_256d = _mm256_set_pd(Ry, Ry + Rh, Ry + Rh, Ry + Rh); 167 | 168 | // here we calculate the differences between the the coords of the points 169 | __m256d x1_m_x2_256d = _mm256_sub_pd(x1_256d, x2_256d); 170 | __m256d y3_m_y4_256d = _mm256_sub_pd(y3_256d, y4_256d); 171 | __m256d y1_m_y2_256d = _mm256_sub_pd(y1_256d, y2_256d); 172 | __m256d x3_m_x4_256d = _mm256_sub_pd(x3_256d, x4_256d); 173 | 174 | // we calculate the denominator of the equations 175 | __m256d den_256d = 176 | _mm256_sub_pd(_mm256_mul_pd(x1_m_x2_256d, y3_m_y4_256d), 177 | _mm256_mul_pd(y1_m_y2_256d, x3_m_x4_256d)); 178 | 179 | // if the denominator is 0 then the line is parallel to the other line 180 | // in this occasion this can't be true here as a line will never be 181 | // parallel to all four sides of a rectangle 182 | __m256d den_zero_256d = 183 | _mm256_cmp_pd(den_256d, _mm256_setzero_pd(), _CMP_EQ_OQ); 184 | 185 | // we dont want to cause any floating point errors by dividing by 0 186 | // so we set the ones that are equal to 0 to 1 187 | den_256d = _mm256_or_pd(den_zero_256d, den_256d); 188 | 189 | // we calculate the rest of the differences between the coords of the 190 | // points 191 | __m256d x1_m_x3_256d = _mm256_sub_pd(x1_256d, x3_256d); 192 | __m256d y1_m_y3_256d = _mm256_sub_pd(y1_256d, y3_256d); 193 | 194 | // calculate the t values 195 | __m256d t_256d = _mm256_sub_pd(_mm256_mul_pd(x1_m_x3_256d, y3_m_y4_256d), 196 | _mm256_mul_pd(y1_m_y3_256d, x3_m_x4_256d)); 197 | t_256d = _mm256_div_pd(t_256d, den_256d); 198 | 199 | // calculate the u values 200 | __m256d u_256d = _mm256_sub_pd(_mm256_mul_pd(x1_m_x2_256d, y1_m_y3_256d), 201 | _mm256_mul_pd(y1_m_y2_256d, x1_m_x3_256d)); 202 | u_256d = 203 | _mm256_mul_pd(_mm256_div_pd(u_256d, den_256d), _mm256_set1_pd(-1.0)); 204 | 205 | // we check this condition t >= 0 && t <= 1 && u >= 0 && u <= 1 206 | __m256d ones_256d = _mm256_set1_pd(1.0); 207 | __m256d zeros_256d = _mm256_set1_pd(0.0); 208 | __m256d t_zero_256d = _mm256_cmp_pd(t_256d, zeros_256d, _CMP_GE_OQ); 209 | __m256d t_one_256d = _mm256_cmp_pd(t_256d, ones_256d, _CMP_LE_OQ); 210 | __m256d u_zero_256d = _mm256_cmp_pd(u_256d, zeros_256d, _CMP_GE_OQ); 211 | __m256d u_one_256d = _mm256_cmp_pd(u_256d, ones_256d, _CMP_LE_OQ); 212 | __m256d t_u_256d = _mm256_and_pd(_mm256_and_pd(t_zero_256d, t_one_256d), 213 | _mm256_and_pd(u_zero_256d, u_one_256d)); 214 | 215 | // if no lines touch the rectangle then this will be false 216 | return _mm256_movemask_pd(t_u_256d) != 0x0; 217 | } 218 | 219 | PG_FORCEINLINE static int 220 | pgRaycast_LineRect_avx2(pgLineBase *line, SDL_Rect *rect, double max_t, 221 | double *T) 222 | { 223 | double Rx = (double)rect->x; 224 | double Ry = (double)rect->y; 225 | double Rw = (double)rect->w; 226 | double Rh = (double)rect->h; 227 | 228 | // here we start to setup the variables 229 | __m256d x1_256d = _mm256_set1_pd(line->xa); 230 | __m256d y1_256d = _mm256_set1_pd(line->ya); 231 | __m256d x2_256d = _mm256_set1_pd(line->xb); 232 | __m256d y2_256d = _mm256_set1_pd(line->yb); 233 | __m256d x3_256d = _mm256_set_pd(Rx, Rx, Rx, Rx + Rw); 234 | __m256d y3_256d = _mm256_set_pd(Ry, Ry, Ry + Rh, Ry); 235 | __m256d x4_256d = _mm256_set_pd(Rx + Rw, Rx, Rx + Rw, Rx + Rw); 236 | __m256d y4_256d = _mm256_set_pd(Ry, Ry + Rh, Ry + Rh, Ry + Rh); 237 | 238 | // here we calculate the differences between the the coords of the points 239 | __m256d x1_m_x2_256d = _mm256_sub_pd(x1_256d, x2_256d); 240 | __m256d y3_m_y4_256d = _mm256_sub_pd(y3_256d, y4_256d); 241 | __m256d y1_m_y2_256d = _mm256_sub_pd(y1_256d, y2_256d); 242 | __m256d x3_m_x4_256d = _mm256_sub_pd(x3_256d, x4_256d); 243 | 244 | // we calculate the denominator of the equations 245 | __m256d den_256d = 246 | _mm256_sub_pd(_mm256_mul_pd(x1_m_x2_256d, y3_m_y4_256d), 247 | _mm256_mul_pd(y1_m_y2_256d, x3_m_x4_256d)); 248 | 249 | // if the denominator is 0 then the line is parallel to the other line 250 | // in this occasion this can't be true here as a line will never be 251 | // parallel to all four sides of a rectangle 252 | __m256d den_zero_256d = 253 | _mm256_cmp_pd(den_256d, _mm256_setzero_pd(), _CMP_EQ_OQ); 254 | 255 | // we dont want to cause any floating point errors by dividing by 0 256 | // so we set the ones that are equal to 0 to 1 257 | den_256d = _mm256_or_pd(den_zero_256d, den_256d); 258 | 259 | // we calculate the rest of the differences between the coords of the 260 | // points 261 | __m256d x1_m_x3_256d = _mm256_sub_pd(x1_256d, x3_256d); 262 | __m256d y1_m_y3_256d = _mm256_sub_pd(y1_256d, y3_256d); 263 | 264 | // calculate the t values 265 | __m256d t_256d = _mm256_sub_pd(_mm256_mul_pd(x1_m_x3_256d, y3_m_y4_256d), 266 | _mm256_mul_pd(y1_m_y3_256d, x3_m_x4_256d)); 267 | t_256d = _mm256_div_pd(t_256d, den_256d); 268 | 269 | // calculate the u values 270 | __m256d u_256d = _mm256_sub_pd(_mm256_mul_pd(x1_m_x2_256d, y1_m_y3_256d), 271 | _mm256_mul_pd(y1_m_y2_256d, x1_m_x3_256d)); 272 | u_256d = 273 | _mm256_mul_pd(_mm256_div_pd(u_256d, den_256d), _mm256_set1_pd(-1.0)); 274 | 275 | // we check this condition t >= 0 && t <= max_t && u >= 0 && u <= 1 276 | __m256d ones_256d = _mm256_set1_pd(1.0); 277 | __m256d zeros_256d = _mm256_set1_pd(0.0); 278 | __m256d t_zero_256d = _mm256_cmp_pd(t_256d, zeros_256d, _CMP_GE_OQ); 279 | __m256d t_one_256d = 280 | _mm256_cmp_pd(t_256d, _mm256_set1_pd(max_t), _CMP_LE_OQ); 281 | __m256d u_zero_256d = _mm256_cmp_pd(u_256d, zeros_256d, _CMP_GE_OQ); 282 | __m256d u_one_256d = _mm256_cmp_pd(u_256d, ones_256d, _CMP_LE_OQ); 283 | __m256d condition_256d = 284 | _mm256_and_pd(_mm256_and_pd(t_zero_256d, t_one_256d), 285 | _mm256_and_pd(u_zero_256d, u_one_256d)); 286 | 287 | // if no lines touch the rectangle then this will be false 288 | if (_mm256_movemask_pd(condition_256d) == 0x0) { 289 | return 0; 290 | } 291 | 292 | __m256d blended_256d = 293 | _mm256_blendv_pd(_mm256_set1_pd(DBL_MAX), t_256d, condition_256d); 294 | 295 | __m128d min_256d = _mm_min_pd(_mm256_extractf128_pd(blended_256d, 0), 296 | _mm256_extractf128_pd(blended_256d, 1)); 297 | double *min_ptr = (double *)&min_256d; 298 | 299 | *T = min_ptr[0] < min_ptr[1] ? min_ptr[0] : min_ptr[1]; 300 | 301 | return 1; 302 | } 303 | #endif /* ~AVX2_IS_SUPPORTED */ 304 | -------------------------------------------------------------------------------- /docs/geometry.rst: -------------------------------------------------------------------------------- 1 | ================== 2 | :mod:`geometry module` 3 | ================== 4 | 5 | .. currentmodule:: geometry 6 | 7 | The geometry module contains classes and functions for working with 2D geometric 8 | objects such as Lines, Circles, and Polygons. Each object has a set of methods 9 | for performing transformations, collisions, and other operations. The module 10 | also contains a suite of standalone functions for performing operations such as 11 | raycasting and other. 12 | 13 | Classes 14 | ======= 15 | 16 | Circle 17 | ------ 18 | The circle class represents a circle with a center point and radius. It has methods for 19 | performing transformations and checking for collisions with other objects. 20 | 21 | **Here is the full list of attributes:** 22 | :: 23 | x: The x coordinate of the center of the circle. 24 | 25 | y: The y coordinate of the center of the circle. 26 | 27 | center: The center point of the circle. 28 | 29 | r: The radius of the circle. 30 | 31 | diameter, d: The diameter of the circle. 32 | 33 | r_sqr: The radius squared of the circle. 34 | 35 | area: The area of the circle. 36 | 37 | circumference: The circumference of the circle. 38 | 39 | top: The top point of the circle. 40 | 41 | bottom: The bottom point of the circle. 42 | 43 | left: The left point of the circle. 44 | 45 | right: The right point of the circle. 46 | 47 | **Here is the full list of methods:** 48 | :: 49 | move: Moves the circle by the given amount. 50 | 51 | move_ip: Moves the circle by the given amount in place. 52 | 53 | update: Updates the circle's attributes. 54 | 55 | copy: Returns a copy of the circle. 56 | 57 | collidepoint: Checks if the circle collides with the given point. 58 | 59 | collidecircle: Checks if the circle collides with the given circle. 60 | 61 | collideline: Checks if the circle collides with the given line. 62 | 63 | colliderect: Checks if the circle collides with the given rectangle. 64 | 65 | collidepolygon: Checks if the circle collides with the given polygon. 66 | 67 | collideswith: Checks if the circle collides with the given object. 68 | 69 | collidelist: Checks if the circle collides with any of the given objects. 70 | 71 | collidelistall: Checks if the circle collides with all of the given objects. 72 | 73 | contains: Checks if the circle fully contains the given object. 74 | 75 | rotate: Rotates the circle by the given amount. 76 | 77 | rotate_ip: Rotates the circle by the given amount in place. 78 | 79 | as_rect: Returns the smallest rectangle that contains the circle. 80 | 81 | intersect: Finds intersections between the circle and another shape. 82 | 83 | Additionally to these, the circle shape can also be used as a collider for the ``geometry.raycast`` function. 84 | 85 | Line 86 | ---- 87 | The line class represents a line segment with a start and end point. It has methods for 88 | performing transformations such as movement, rotation, scaling and checking for collisions with 89 | other objects. 90 | 91 | **Here is the full list of attributes:** 92 | :: 93 | xa: The x coordinate of the start point of the line. 94 | 95 | ya: The y coordinate of the start point of the line. 96 | 97 | xb: The x coordinate of the end point of the line. 98 | 99 | yb: The y coordinate of the end point of the line. 100 | 101 | a: The start point of the line. 102 | 103 | b: The end point of the line. 104 | 105 | length: The length of the line. 106 | 107 | angle: The angle of the line. 108 | 109 | center: The center of the line. 110 | 111 | centerx: The x coordinate of the center of the line. 112 | 113 | centery: The y coordinate of the center of the line. 114 | 115 | slope: The slope of the line. 116 | 117 | **Here is the full list of methods:** 118 | :: 119 | move: Moves the line by the given amount. 120 | 121 | move_ip: Moves the line by the given amount in place. 122 | 123 | rotate: Rotates the line by the given amount. 124 | 125 | rotate_ip: Rotates the line by the given amount in place. 126 | 127 | scale: Scales the line by the given amount. 128 | 129 | scale_ip: Scales the line by the given amount in place. 130 | 131 | flip_ab: Switches the endpoints of the line. 132 | 133 | flip_ab_ip: Switches the endpoints of the line in place. 134 | 135 | update: Updates the line's attributes. 136 | 137 | copy: Returns a copy of the line. 138 | 139 | collidepoint: Checks if the line collides with the given point. 140 | 141 | collidecircle: Checks if the line collides with the given circle. 142 | 143 | collideline: Checks if the line collides with the given line. 144 | 145 | colliderect: Checks if the line collides with the given rectangle. 146 | 147 | collidepolygon: Checks if the line collides with the given polygon. 148 | 149 | collideswith: Checks if the line collides with the given object. 150 | 151 | as_circle: Returns a circle which fully encloses the line. 152 | 153 | as_rect: Returns the smallest rectangle that contains the line. 154 | 155 | is_parallel: Checks if the line is parallel to the given line. 156 | 157 | is_perpendicular: Checks if the line is perpendicular to the given line. 158 | 159 | at: Returns the point at the given position along the line based on a factor. 160 | 161 | as_segments: returns the line as a list of segments. 162 | 163 | as_points: returns the line as a list of points. 164 | 165 | rotate: Rotates the line by the given amount. 166 | 167 | rotate_ip: Rotates the line by the given amount in place. 168 | 169 | Additionally to these, the line shape can also be used as a collider for the ``geometry.raycast`` function. 170 | 171 | Polygon 172 | ------- 173 | The polygon class represents a polygon with a list of vertices. It has methods for 174 | performing transformations such as movement, rotation, scaling and checking for collisions with 175 | other objects. 176 | 177 | **Here is the full list of attributes:** 178 | :: 179 | vertices: The list of vertices of the polygon. 180 | 181 | verts_num: The number of vertices of the polygon. 182 | 183 | center: The center point of the polygon. 184 | 185 | centerx: The x coordinate of the center of the polygon. 186 | 187 | centery: The y coordinate of the center of the polygon. 188 | 189 | perimeter: The perimeter of the polygon. 190 | 191 | area: The area of the polygon. 192 | 193 | **Here is the full list of methods:** 194 | :: 195 | move: Moves the polygon by the given amount. 196 | 197 | move_ip: Moves the polygon by the given amount in place. 198 | 199 | copy: Returns a copy of the polygon. 200 | 201 | collidepoint: Checks if the polygon collides with the given point. 202 | 203 | collideline: Checks if the polygon collides with the given line. 204 | 205 | collidecircle: Checks if the polygon collides with the given circle. 206 | 207 | insert_vertex: Adds a vertex to the polygon. 208 | 209 | remove_vertex: Removes a vertex from the polygon. 210 | 211 | pop_vertex: Removes and returns a vertex from the polygon. 212 | 213 | is_convex: Checks if the polygon is convex. 214 | 215 | scale: Scales the polygon by the given amount. 216 | 217 | scale_ip: Scales the polygon by the given amount in place. 218 | 219 | as_rect: Returns the smallest rectangle that contains the polygon. 220 | 221 | as_segments: Returns a list of lines that make up the polygon. 222 | 223 | rotate: Rotates the polygon by the given amount. 224 | 225 | rotate_ip: Rotates the polygon by the given amount in place. 226 | 227 | Functions 228 | ========= 229 | The geometry module also contains a number of standalone functions for performing operations 230 | such as raycasting and general utility functions. 231 | 232 | .. method:: raycast 233 | 234 | | :sl:`Returns the closest intersection point between a ray and a sequence of colliders` 235 | | :sg:`raycast(line, colliders) -> (x, y) | None` 236 | | :sg:`raycast(origin, angle, max_dist, colliders) -> (x, y) | None` 237 | | :sg:`raycast(origin, direction, max_dist, colliders) -> (x, y) | None` 238 | 239 | This function returns the closest intersection point between a ray and a sequence 240 | of colliders. 241 | A ray can be defined by a Line, an origin point and an angle, or an origin point 242 | and a direction. 243 | Apart from a Line, which has fixed length, the ray can have any length, 244 | including infinite length. To define an infinite ray, set the max_dist parameter 245 | to a negative value. The max_dist parameter cannot be set to 0. 246 | The colliders can be any sequence of objects Circle, Line, or Rect. 247 | 248 | The function returns a tuple containing the x and y coordinates of the closest intersection 249 | point, or None if no intersection was found. 250 | 251 | .. ## geometry.raycast ## 252 | 253 | .. method:: regular_polygon 254 | 255 | | :sl:`Returns a regular polygon with the given number of sides` 256 | | :sg:`regular_polygon(sides, center, radius, angle=0) -> Polygon` 257 | 258 | This function returns a regular polygon with the given number of sides. 259 | The polygon is centered at the given center point and has the given radius. 260 | The polygon can be rotated by the given angle through the optional 261 | `angle` parameter, defaulting to 0. 262 | 263 | .. ## geometry.regular_polygon ## 264 | 265 | .. method:: rect_to_polygon 266 | 267 | | :sl:`Returns a polygon that represents the given rectangle` 268 | | :sg:`rect_to_polygon(rect) -> Polygon` 269 | 270 | This function is used to convert a rectangle into a polygon. 271 | The resulting polygon will have four vertices, one for each corner of the 272 | rectangle. For example, if the input rectangle is specified as Rect(0, 0, 10, 10), 273 | the resulting polygon will have the following vertices: 274 | 275 | Polygon([(0, 0), (10, 0), (10, 10), (0, 10)]) 276 | 277 | which represent the top left, top right, bottom right, and bottom left corners 278 | of the rectangle, respectively. 279 | 280 | .. ## geometry.rect_to_polygon ## 281 | 282 | .. method:: is_line 283 | 284 | | :sl:`Checks if the given object is a geometry.Line` 285 | | :sg:`is_line(obj) -> bool` 286 | 287 | This function checks if the given object is a geometry.Line. 288 | It returns True if the object is a geometry.Line, and False otherwise. 289 | 290 | .. note:: 291 | 292 | If the python object subclasses the geometry.Line class, this function will 293 | return False. Note that this function is equivalent to isinstance(obj, Line). 294 | Using that isinstance check is better for typechecking with mypy, and more 295 | explicit - so it’s recommended to use that instead of is_line. 296 | Utilizing is_line can save an unwanted Line import. 297 | 298 | .. ## geometry.is_line ## 299 | 300 | .. method:: is_circle 301 | 302 | | :sl:`Checks if the given object is a geometry.Circle` 303 | | :sg:`is_circle(obj) -> bool` 304 | 305 | This function checks if the given object is a geometry.Circle. 306 | It returns True if the object is a geometry.Circle, and False otherwise. 307 | 308 | .. note:: 309 | 310 | If the python object subclasses the geometry.Circle class, this function will 311 | return False. Note that this function is equivalent to isinstance(obj, Circle). 312 | Using that isinstance check is better for typechecking with mypy, and more 313 | explicit - so it’s recommended to use that instead of is_circle. 314 | Utilizing is_circle can save an unwanted Circle import. 315 | 316 | .. ## geometry.is_circle ## 317 | 318 | .. method:: is_polygon 319 | 320 | | :sl:`Checks if the given object is a geometry.Polygon` 321 | | :sg:`is_polygon(obj) -> bool` 322 | 323 | This function checks if the given object is a geometry.Polygon. 324 | It returns True if the object is a geometry.Polygon, and False otherwise. 325 | 326 | .. note:: 327 | 328 | If the python object subclasses the geometry.Polygon class, this function will 329 | return False. Note that this function is equivalent to isinstance(obj, Polygon). 330 | Using that isinstance check is better for typechecking with mypy, and more 331 | explicit - so it’s recommended to use that instead of is_polygon. 332 | Utilizing is_polygon can save an unwanted Polygon import. 333 | 334 | .. ## geometry.is_polygon ## 335 | 336 | .. method:: multiraycast 337 | 338 | | :sl:`Returns a list of intersection points between a sequence of rays and a sequence of colliders` 339 | | :sg:`multiraycast(rays, colliders) -> [(x, y) | None]` 340 | 341 | This function returns a list of intersection points between a sequence of 342 | rays and a sequence of colliders. 343 | The rays parameter is a sequence that can be composed of the following objects: 344 | 345 | - Line objects. 346 | - Tuples of: origin point, angle, max_dist. 347 | - Tuples of: origin point, direction, max_dist. 348 | - Tuples of: origin point, end point. 349 | 350 | Apart from Lines, which have fixed length, the rays can have any length, 351 | including infinite length. To define an infinite ray, set the max_dist parameter 352 | to a negative value. The max_dist parameter cannot be set to 0. 353 | The colliders can be any sequence of objects such as Circle, Line, or Rect. 354 | 355 | The function returns a list of tuples containing the closest intersection point to 356 | the ray's origin, or None if it couldn't find one. 357 | 358 | .. ## geometry.multiraycast ## 359 | -------------------------------------------------------------------------------- /benchmarks/GEOMETRY_circle_benchmark.py: -------------------------------------------------------------------------------- 1 | from pygame import Rect 2 | from benchmark_utils import TestSuite 3 | from geometry import Circle, Line, Polygon 4 | 5 | 6 | # === Tests === 7 | # Each test consists of a tuple of: (name, call) 8 | # The name is a string containing the name of the test 9 | # The call is a string containing the code to be timed 10 | # every test is run CPT times and the average time is 11 | # calculated across REP runs 12 | # the formula is time = (time per CPT calls repeated REP times) / REP 13 | # ==================================================== 14 | 15 | GLOB = { 16 | "Circle": Circle, 17 | "r1": Rect(0, 0, 10, 10), 18 | "r2": Rect(10, 10, 4, 4), 19 | "r3": Rect(0, 0, 100, 100), 20 | "c1": Circle(10, 10, 10), 21 | "c2": Circle(20, 5, 15), 22 | "c3": Circle(50, 50, 15), 23 | "c4": Circle(10, 10, 15), 24 | "c5": Circle(0, 0, 15), 25 | "l1": Line(0, 0, 10, 10), 26 | "l2": Line(0, 0, 10, -10), 27 | "l3": Line(10, 10, 100, 43), 28 | "p1": (10, 10), 29 | "p2": (1000, 1000), 30 | "p3": (10.0, 10.0), 31 | "p4": (1000.0, 1000.0), 32 | "poly1": Polygon([(-5, 0), (5, 0), (0, 5)]), 33 | "poly2": Polygon([(100, 150), (200, 225), (150, 200)]), 34 | "poly3": Polygon([(0, 0), (50, 50), (50, -50), (0, -50)]), 35 | } 36 | 37 | instatiation_tests = [ 38 | ("Circle", "Circle(c1)"), 39 | ("3 int", "Circle(0, 0, 5)"), 40 | ("3 float", "Circle(0.0, 0.0, 5.0)"), 41 | ("2 args tup+int", "Circle((0, 0), 5)"), 42 | ("2 args tup+float", "Circle((0.0, 0.0), 5.0)"), 43 | ("2 args lst+int", "Circle([0, 0], 5)"), 44 | ("2 args lst+float", "Circle([0.0, 0.0], 5.0)"), 45 | ("1 tuple int", "Circle((0, 0, 5))"), 46 | ("1 tuple float", "Circle((0.0, 0.0, 5.0))"), 47 | ("1 list int", "Circle([0, 0, 5])"), 48 | ("1 list float", "Circle([0.0, 0.0, 5.0])"), 49 | ("1 list 2 args tup+int", "Circle([(0, 0), 5])"), 50 | ("1 list 2 args tup+float", "Circle([(0.0, 0.0), 5.0])"), 51 | ("1 tuple 2 args lst+int", "Circle(([0, 0], 5))"), 52 | ("1 tuple 2 args lst+float", "Circle(([0.0, 0.0], 5.0))"), 53 | ("1 list 2 args lst+int", "Circle([[0, 0], 5])"), 54 | ("1 list 2 args lst+float", "Circle([[0.0, 0.0], 5.0])"), 55 | ("1 tuple 2 args tup+int", "Circle(((0, 0), 5))"), 56 | ("1 tuple 2 args tup+float", "Circle(((0.0, 0.0), 5.0))"), 57 | ("3 int list inside list", "Circle([[0, 0, 5]])"), 58 | ("3 float list inside list", "Circle([[0.0, 0.0, 5.0]])"), 59 | ("3 int tuple inside tuple", "Circle(((0, 0, 5)))"), 60 | ("3 float tuple inside tuple", "Circle(((0.0, 0.0, 5.0)))"), 61 | ("3 int tuple inside list", "Circle([(0, 0, 5)])"), 62 | ("3 float tuple inside list", "Circle([(0.0, 0.0, 5.0)])"), 63 | ("3 int list inside tuple", "Circle(([0, 0, 5]))"), 64 | ("3 float list inside tuple", "Circle(([0.0, 0.0, 5.0]))"), 65 | ] 66 | 67 | copy_tests = [ 68 | ("Circle copy", "c1.copy()"), 69 | ] 70 | 71 | conversion_tests = [ 72 | ("as_rect", "c1.as_rect()"), 73 | ] 74 | 75 | getters_tests = [ 76 | ("x", "c1.x"), 77 | ("y", "c1.y"), 78 | ("radius", "c1.r"), 79 | ("diameter", "c1.diameter"), 80 | ("d", "c1.d"), 81 | ("center", "c1.center"), 82 | ("area", "c1.area"), 83 | ("circumference", "c1.circumference"), 84 | ] 85 | 86 | setters_tests = [ 87 | ("x int", "c1.x = 3"), 88 | ("x float", "c1.x = 3.0"), 89 | ("y int", "c1.y = 3"), 90 | ("y float", "c1.y = 3.0"), 91 | ("radius int", "c1.r = 3"), 92 | ("radius float", "c1.r = 3.0"), 93 | ("diameter int", "c1.diameter = 3"), 94 | ("diameter float", "c1.diameter = 3.0"), 95 | ("d int", "c1.d = 3"), 96 | ("d float", "c1.d = 3.0"), 97 | ("center int", "c1.center = (3, 3)"), 98 | ("center float", "c1.center = (3.0, 3.0)"), 99 | ("area int", "c1.area = 3"), 100 | ("area float", "c1.area = 3.0"), 101 | ("circumference int", "c1.circumference = 3"), 102 | ("circumference float", "c1.circumference = 3.0"), 103 | ] 104 | 105 | update_tests = [ 106 | ("circle", "c1.update(c2)"), 107 | ("3 int", "c1.update(1, 1, 3)"), 108 | ("3 float", "c1.update(1.0, 1.0, 3.0)"), 109 | ("1 tup 3 int", "c1.update((1, 1, 3))"), 110 | ("1 tup 3 float", "c1.update((1.0, 1.0, 3.0))"), 111 | ("2 tuple-int", "c1.update((1, 1), 3)"), 112 | ("2 tuple-float", "c1.update((1.0, 1.0), 3.0)"), 113 | ("1 tup 2 int", "c1.update(((1, 1), 3))"), 114 | ("1 tup 2 float", "c1.update(((1.0, 1.0), 3.0))"), 115 | ] 116 | 117 | move_tests = [ 118 | ("x-x int", "c1.move(1, 1)"), 119 | ("x-x float", "c1.move(1.0, 1.0)"), 120 | ("x-x tup int", "c1.move((1, 1))"), 121 | ("x-x tup float", "c1.move((1.0, 1.0))"), 122 | ("x-x lst int", "c1.move([1, 1])"), 123 | ("x-x lst float", "c1.move([1.0, 1.0])"), 124 | ("0-x int", "c1.move(0, 1)"), 125 | ("0-x float", "c1.move(0.0, 1.0)"), 126 | ("0-x tup int", "c1.move((0, 1))"), 127 | ("0-x tup float", "c1.move((0.0, 1.0))"), 128 | ("0-x lst int", "c1.move([0, 1])"), 129 | ("0-x lst float", "c1.move([0.0, 1.0])"), 130 | ("x-0 int", "c1.move(1, 0)"), 131 | ("x-0 float", "c1.move(1.0, 0.0)"), 132 | ("x-0 tup int", "c1.move((1, 0))"), 133 | ("x-0 tup float", "c1.move((1.0, 0.0))"), 134 | ("x-0 lst int", "c1.move([1, 0])"), 135 | ("x-0 lst float", "c1.move([1.0, 0.0])"), 136 | ("0-0 int", "c1.move(0, 0)"), 137 | ("0-0 float", "c1.move(0.0, 0.0)"), 138 | ("0-0 tup int", "c1.move((0, 0))"), 139 | ("0-0 tup float", "c1.move((0.0, 0.0))"), 140 | ("0-0 lst int", "c1.move([0, 0])"), 141 | ("0-0 lst float", "c1.move([0.0, 0.0])"), 142 | ] 143 | 144 | move_ip_tests = [ 145 | ("x-x int", "c1.move_ip(1, 1)"), 146 | ("x-x float", "c1.move_ip(1.0, 1.0)"), 147 | ("x-x tup int", "c1.move_ip((1, 1))"), 148 | ("x-x tup float", "c1.move_ip((1.0, 1.0))"), 149 | ("x-x lst int", "c1.move_ip([1, 1])"), 150 | ("x-x lst float", "c1.move_ip([1.0, 1.0])"), 151 | ("0-x int", "c1.move_ip(0, 1)"), 152 | ("0-x float", "c1.move_ip(0.0, 1.0)"), 153 | ("0-x tup int", "c1.move_ip((0, 1))"), 154 | ("0-x tup float", "c1.move_ip((0.0, 1.0))"), 155 | ("0-x lst int", "c1.move_ip([0, 1])"), 156 | ("0-x lst float", "c1.move_ip([0.0, 1.0])"), 157 | ("x-0 int", "c1.move_ip(1, 0)"), 158 | ("x-0 float", "c1.move_ip(1.0, 0.0)"), 159 | ("x-0 tup int", "c1.move_ip((1, 0))"), 160 | ("x-0 tup float", "c1.move_ip((1.0, 0.0))"), 161 | ("x-0 lst int", "c1.move_ip([1, 0])"), 162 | ("x-0 lst float", "c1.move_ip([1.0, 0.0])"), 163 | ("0-0 int", "c1.move_ip(0, 0)"), 164 | ("0-0 float", "c1.move_ip(0.0, 0.0)"), 165 | ("0-0 tup int", "c1.move_ip((0, 0))"), 166 | ("0-0 tup float", "c1.move_ip((0.0, 0.0))"), 167 | ("0-0 lst int", "c1.move_ip([0, 0])"), 168 | ("0-0 lst float", "c1.move_ip([0.0, 0.0])"), 169 | ] 170 | 171 | collidecircle_tests = [ 172 | ("C circle", "c1.collidecircle(c2)"), 173 | ("NC circle", "c1.collidecircle(c3)"), 174 | ("A inside B", "c1.collidecircle(c4)"), 175 | ("B inside A", "c4.collidecircle(c1)"), 176 | ("C tup int", "c1.collidecircle((20, 5, 15))"), 177 | ("NC tup int", "c1.collidecircle((50, 50, 15))"), 178 | ("A inside B tup int", "c1.collidecircle((10, 10, 15))"), 179 | ("B inside A tup int", "c4.collidecircle((10, 10, 10))"), 180 | ("C tup 2 int", "c1.collidecircle(((20, 5), 15))"), 181 | ("NC tup 2 int", "c1.collidecircle(((50, 50), 15))"), 182 | ("A inside B tup 2 int", "c1.collidecircle(((10, 10), 15))"), 183 | ("B inside A tup 2 int", "c4.collidecircle(((10, 10), 10))"), 184 | ("C tup float", "c1.collidecircle((20.0, 5.0, 15.0))"), 185 | ("NC tup float", "c1.collidecircle((50.0, 50.0, 15.0))"), 186 | ("A inside B tup float", "c1.collidecircle((10.0, 10.0, 15.0))"), 187 | ("B inside A tup float", "c4.collidecircle((10.0, 10.0, 10.0))"), 188 | ("C tup 2 float", "c1.collidecircle(((20.0, 5.0), 15.0))"), 189 | ("NC tup 2 float", "c1.collidecircle(((50.0, 50.0), 15.0))"), 190 | ("A inside B tup 2 float", "c1.collidecircle(((10.0, 10.0), 15.0))"), 191 | ("B inside A tup 2 float", "c4.collidecircle(((10.0, 10.0), 10.0))"), 192 | ("C 2-tup int", "c1.collidecircle((20, 5), 15)"), 193 | ("NC 2-tup int", "c1.collidecircle((50, 50), 15)"), 194 | ("A inside B 2-tup int", "c1.collidecircle((10, 10), 15)"), 195 | ("B inside A 2-tup int", "c4.collidecircle((10, 10), 10)"), 196 | ("C 2-tup float", "c1.collidecircle((20.0, 5.0), 15.0)"), 197 | ("NC 2-tup float", "c1.collidecircle((50.0, 50.0), 15.0)"), 198 | ("A inside B 2-tup float", "c1.collidecircle((10.0, 10.0), 15.0)"), 199 | ("B inside A 2-tup float", "c4.collidecircle((10.0, 10.0), 10.0)"), 200 | ] 201 | 202 | colliderect_tests = [ 203 | ("C rect", "c1.colliderect(r1)"), 204 | ("NC rect", "c3.colliderect(r1)"), 205 | ("A inside B", "c3.colliderect(r3)"), 206 | ("B inside A", "c1.colliderect(r2)"), 207 | ("C tup int", "c1.colliderect((0, 0, 10, 10))"), 208 | ("NC tup int", "c3.colliderect((0, 0, 10, 10))"), 209 | ("A inside B tup int", "c3.colliderect((0, 0, 100, 100))"), 210 | ("B inside A tup int", "c1.colliderect((10, 10, 4, 4))"), 211 | ("C tup 2 int", "c1.colliderect(((0, 0), (10, 10)))"), 212 | ("NC tup 2 int", "c3.colliderect(((0, 0), (10, 10)))"), 213 | ("A inside B tup 2 int", "c3.colliderect(((0, 0), (100, 100)))"), 214 | ("B inside A tup 2 int", "c1.colliderect(((10, 10), (4, 4)))"), 215 | ("C tup float", "c1.colliderect((0.0, 0.0, 10.0, 10.0))"), 216 | ("NC tup float", "c3.colliderect((0.0, 0.0, 10.0, 10.0))"), 217 | ("A inside B tup float", "c3.colliderect((0.0, 0.0, 100.0, 100.0))"), 218 | ("B inside A tup float", "c1.colliderect((10.0, 10.0, 4.0, 4.0))"), 219 | ("C tup 2 float", "c1.colliderect(((0.0, 0.0), (10.0, 10.0)))"), 220 | ("NC tup 2 float", "c3.colliderect(((0.0, 0.0), (10.0, 10.0)))"), 221 | ("A inside B tup 2 float", "c3.colliderect(((0.0, 0.0), (100.0, 100.0)))"), 222 | ("B inside A tup 2 float", "c1.colliderect(((10.0, 10.0), (4.0, 4.0)))"), 223 | ] 224 | 225 | collidepoint_tests = [ 226 | ("C int", "c1.collidepoint(p1)"), 227 | ("NC int", "c1.collidepoint(p2)"), 228 | ("C float", "c1.collidepoint(p3)"), 229 | ("NC float", "c1.collidepoint(p4)"), 230 | ("C 2 int", "c1.collidepoint(10, 10)"), 231 | ("NC 2 int", "c1.collidepoint(1000, 1000)"), 232 | ("C 2 float", "c1.collidepoint(10.0, 10.0)"), 233 | ("NC 2 float", "c1.collidepoint(1000.0, 1000.0)"), 234 | ("C tup int", "c1.collidepoint((10, 10))"), 235 | ("NC tup int", "c1.collidepoint((1000, 1000))"), 236 | ("C tup float", "c1.collidepoint((10.0, 10.0))"), 237 | ("NC tup float", "c1.collidepoint((1000.0, 1000.0))"), 238 | ("C lst int", "c1.collidepoint([10, 10])"), 239 | ("NC lst int", "c1.collidepoint([1000, 1000])"), 240 | ("C lst float", "c1.collidepoint([10.0, 10.0])"), 241 | ("NC lst float", "c1.collidepoint([1000.0, 1000.0])"), 242 | ] 243 | 244 | collideline_tests = [ 245 | ("C line", "c1.collideline(l1)"), 246 | ("NC line", "c1.collideline(l2)"), 247 | ("C 4 int", "c1.collideline(0, 0, 10, 10)"), 248 | ("NC 4 int", "c1.collideline(0, 0, 1000, 1000)"), 249 | ("C 4 float", "c1.collideline(0.0, 0.0, 10.0, 10.0)"), 250 | ("NC 4 float", "c1.collideline(0.0, 0.0, 1000.0, 1000.0)"), 251 | ("C 1 int", "c1.collideline((0, 0, 10, 10))"), 252 | ("NC 1 int", "c1.collideline((0, 0, 10, -10))"), 253 | ("C 1 float", "c1.collideline((0.0, 0.0, 10.0, 10.0))"), 254 | ("NC 1 float", "c1.collideline((0.0, 0.0, 10.0, -10.0))"), 255 | ("C 2 tup int", "c1.collideline((0, 0), (10, 10))"), 256 | ("NC 2 tup int", "c1.collideline((0, 0), (10, -10))"), 257 | ("C 2 tup float", "c1.collideline((0.0, 0.0), (10.0, 10.0))"), 258 | ("NC 2 tup float", "c1.collideline((0.0, 0.0), (10.0, -10.0))"), 259 | ("C 1 tup 2 subtup int", "c1.collideline(((0, 0), (10, 10)))"), 260 | ("NC 1 tup 2 subtup int", "c1.collideline(((0, 0), (10, -10)))"), 261 | ("C 1 tup 2 subtup float", "c1.collideline(((0.0, 0.0), (10.0, 10.0)))"), 262 | ("NC 1 tup 2 subtup float", "c1.collideline(((0.0, 0.0), (10.0, -10.0)))"), 263 | ] 264 | 265 | collideswith_tests = [ 266 | ("C rect", "c1.collideswith(r1)"), 267 | ("NC rect", "c1.collideswith(r2)"), 268 | ("C circle", "c1.collideswith(c2)"), 269 | ("NC circle", "c1.collideswith(c2)"), 270 | ("C point int", "c1.collideswith(p1)"), 271 | ("NC point int", "c1.collideswith(p2)"), 272 | ("C point float", "c1.collideswith(p3)"), 273 | ("NC point float", "c1.collideswith(p4)"), 274 | ("C line", "c1.collideswith(l1)"), 275 | ("NC line", "c1.collideswith(l2)"), 276 | ] 277 | 278 | contains_tests = [ 279 | ("circle self", "c1.contains(c1)"), 280 | ("circle contained", "c4.contains(c1)"), 281 | ("circle not contained", "c4.contains(c3)"), 282 | ("circle intersecting", "c1.contains(c2)"), 283 | ("line contained", "c1.contains(l1)"), 284 | ("line not contained", "c1.contains(l2)"), 285 | ("line intersecting", "c1.contains(l3)"), 286 | ("poly contained", "c5.contains(poly1)"), 287 | ("poly not contained", "c5.contains(poly2)"), 288 | ("poly intersecting", "c5.contains(poly3)"), 289 | ("rect contained", "c1.contains(r1)"), 290 | ("rect not contained", "c1.contains(r2)"), 291 | ("rect intersecting", "c1.contains(r3)"), 292 | ] 293 | 294 | # === Test Suites === 295 | # If you want to add more tests to a suite, just add them to the list 296 | # If you want to remove or skip tests from a suite, just remove or comment them out 297 | 298 | GROUPS = [ 299 | ("Instatiation", instatiation_tests), 300 | ("Attribute Getters", getters_tests), 301 | ("Attribute Setters", setters_tests), 302 | ("Copy", copy_tests), 303 | ("Conversion", conversion_tests), 304 | ("Update", update_tests), 305 | ("Move", move_tests), 306 | ("Move_ip", move_ip_tests), 307 | ("Collision: Circle", collidecircle_tests), 308 | ("Collision: Rect", colliderect_tests), 309 | ("Collision: Point", collidepoint_tests), 310 | ("Collision: Line", collideline_tests), 311 | ("Collision: Shape-General", collideswith_tests), 312 | ("Contains", contains_tests), 313 | ] 314 | 315 | TestSuite("Geometry Module - Circle", GROUPS, GLOB).run_suite() 316 | -------------------------------------------------------------------------------- /docs/polygon.rst: -------------------------------------------------------------------------------- 1 | 2 | ================== 3 | :mod:`pygame_geometry.Polygon` 4 | ================== 5 | 6 | .. currentmodule:: pygame_geometry 7 | 8 | .. class:: Polygon 9 | 10 | | :sl:`pygame object for representing a Polygon` 11 | | :sg:`Polygon(vertices) -> Polygon` 12 | | :sg:`Polygon(polygon) -> Polygon` 13 | 14 | The `Polygon` class provides many useful methods for collision, transform and intersection. 15 | A `Polygon` can be created from a Sequence of coordinates that represent the vertices 16 | of the `Polygon`. Polygons can also be created from python objects that are already a 17 | `Polygon` or have an attribute named "polygon". 18 | 19 | Specifically, to construct a `Polygon` you can pass the vertices' positions as 20 | separate arguments or inside a Sequence(list or tuple). Generators are also accepted. 21 | 22 | Functions that require a `Polygon` argument may also accept these values as Polygons: 23 | :: 24 | ((xa, ya), (xb, yb), (x3, y3), ...) 25 | [(xa, ya), (xb, yb), (x3, y3), ...] 26 | 27 | **You cannot create a polygon with less than 3 vertices.** 28 | Polygon Attributes 29 | ------ 30 | The `Polygon` class has both virtual and non-virtual attributes. Non-virtual attributes 31 | are attributes that are stored in the `Polygon` object itself. Virtual attributes are the 32 | result of calculations that utilize the Polygon's non-virtual attributes. 33 | 34 | Here is the list of all the attributes of the `Polygon` class: 35 | 36 | .. attribute:: num_verts 37 | | :sl:`the number of vertices of the polygon` 38 | | :sg:`num_verts -> int (>=3)` 39 | 40 | The number of vertices of the `Polygon`. It is always >= 3 as a triangle is the 41 | smallest `Polygon` you can have. The attribute is read-only, so it cannot be 42 | reassigned. If you want to change the number of vertices, you have to reassign 43 | the 'vertices' attribute or use either of 'add_vertex', 'remove_vertex' or 'pop_vertex'. 44 | 45 | .. attribute:: vertices 46 | | :sl:`the vertices of the polygon` 47 | | :sg:`vertices -> List[Tuple[float, float]]` 48 | 49 | A list of tuples representing the coordinates of the polygon's vertices. 50 | You can modify the vertices in several ways: 51 | 52 | - Directly reassign the `vertices` attribute: ``polygon.vertices = [(0, 0), (1, 1), (0, 1)]`` 53 | - Index the `Polygon`: ``polygon[0] = (0, 0)`` 54 | - Use the `insert_vertex` method to add a single vertex 55 | 56 | .. note:: 57 | Directly reassigning the `vertices` attribute will always recalculate the center 58 | of the `Polygon`. This means that even if you change the entire list to the same 59 | values except for one vertex, changing the entire vertices list is always more 60 | expensive than changing a single vertex. 61 | 62 | For example, if you have a polygon with vertices [(0, 0), (1, 1), (0, 1)] and 63 | you want to change the first vertex to (99, 99), you should use ``polygon[0] = (99, 99)`` 64 | instead of ``polygon.vertices = [(99, 99), (1, 1), (0, 1)]``. 65 | 66 | .. note:: 67 | To change a single vertex at an index, the correct method is ``polygon[index] = (x, y)``. 68 | Do not use ``polygon.vertices[index] = (x, y)`` as this will not change the vertex 69 | at the specified index but rather the element at the specified index of the 70 | newly created list of vertices. 71 | 72 | 73 | .. attribute:: center 74 | | :sl:`the center of the polygon` 75 | | :sg:`center -> (float, float)` 76 | 77 | It's a Tuple representing the coordinates of the center of the `Polygon`. 78 | It is calculated as the average of all the vertices. It can be reassigned to 79 | move the `Polygon`. If reassigned, the `Polygon`'s vertices will be moved to 80 | create a `Polygon` with matching center. 81 | 82 | .. attribute:: centerx 83 | | :sl:`the x coordinate of the center of the polygon` 84 | | :sg:`centerx -> float` 85 | 86 | It's the x coordinate of the center of the `Polygon`. It is calculated as the 87 | average of all the x coordinates of the vertices. It can be reassigned to move 88 | the `Polygon`. If reassigned, the polygon's vertices will be moved to 89 | create a `Polygon` with matching center. 90 | 91 | .. attribute:: centery 92 | | :sl:`the y coordinate of the center of the polygon` 93 | | :sg:`centery -> float` 94 | 95 | It's the y coordinate of the center of the `Polygon`. It is calculated as the 96 | average of all the y coordinates of the vertices. It can be reassigned to move 97 | the `Polygon`. If reassigned, the polygon's vertices will be moved to 98 | create a `Polygon` with matching center. 99 | 100 | .. attribute:: perimeter 101 | | :sl:`the perimeter of the polygon` 102 | | :sg:`perimeter -> float` 103 | 104 | It's the perimeter of the `Polygon`. It is calculated as the sum of the distances 105 | between all the vertices. This attribute is read-only, it cannot be reassigned. 106 | 107 | Polygon Methods 108 | ------ 109 | The Polygon functions which modify the position, orientation or shape return a new 110 | copy of the `Polygon` with the affected changes. The original `Polygon` is not modified. 111 | Some methods have an alternate "in-place" version that returns `None` but affects the 112 | original `Polygon`. These "in-place" methods are denoted with the "ip" suffix. 113 | 114 | Here is the list of all the methods of the `Polygon` class: 115 | 116 | .. method:: move 117 | 118 | | :sl:`moves the polygon by a given amount` 119 | | :sg:`move((x, y)) -> Polygon` 120 | | :sg:`move(x, y) -> Polygon` 121 | 122 | Returns a new Polygon that is moved by the given offset. The original Polygon is 123 | not modified. 124 | 125 | .. note:: 126 | Keep in mind that the more vertices the polygon has, the more CPU time it will 127 | take to move it. 128 | 129 | .. ## Polygon.move ## 130 | 131 | .. method:: move_ip 132 | 133 | | :sl:`moves the polygon by a given amount` 134 | | :sg:`move_ip((x, y)) -> None` 135 | | :sg:`move_ip(x, y) -> None` 136 | 137 | Moves the Polygon by the given offset. The original Polygon is modified. 138 | Always returns None. 139 | 140 | .. note:: 141 | Keep in mind that the more vertices the polygon has, the more CPU time it will 142 | take to move it. 143 | 144 | .. ## Polygon.move_ip ## 145 | 146 | .. method:: collidepoint 147 | 148 | | :sl:`tests if a point is inside the polygon` 149 | | :sg:`collidepoint((x, y)) -> bool` 150 | | :sg:`collidepoint(x, y) -> bool` 151 | 152 | Returns True if the given point is inside the `Polygon`, False otherwise. 153 | 154 | .. note:: 155 | Keep in mind that the more vertices the polygon has, the more CPU time it will 156 | take to calculate the collision. 157 | 158 | .. ## Polygon.collidepoint ## 159 | 160 | .. method:: collideline 161 | 162 | | :sl:`tests if a line intersects the polygon` 163 | | :sg:`collideline(Line, only_edges=False) -> bool` 164 | | :sg:`collideline((xa, ya), (xb, yb), only_edges=False) -> bool` 165 | | :sg:`collideline(xa, ya, xb, yb, only_edges=False) -> bool` 166 | 167 | Tests whether a given `Line` collides with the `Polygon`. 168 | It takes either a `Line` or Line-like object as an argument and it returns `True` 169 | if the `Line` collides with the `Polygon`, `False` otherwise. 170 | 171 | The optional `only_edges` argument can be set to `True` to only test whether the 172 | edges of the polygon intersect the `Line`. This means that a Line that is 173 | inscribed by the `Polygon` or completely outside of it will not be considered colliding. 174 | 175 | .. ## Polygon.collideline ## 176 | 177 | .. method:: collidecircle 178 | 179 | | :sl:`tests if a circle is inside the polygon` 180 | | :sg:`collidecircle(Circle, only_edges=False) -> bool` 181 | | :sg:`collidecircle((x, y), radius, only_edges=False) -> bool` 182 | | :sg:`collidecircle(x, y, radius, only_edges=False) -> bool` 183 | 184 | Tests whether a given `Circle` collides with the `Polygon`. 185 | It takes either a `Circle` or Circle-like object as an argument and it returns 186 | `True` if the circle collides with the `Polygon`, `False` otherwise. 187 | 188 | The optional `only_edges` argument can be set to `True` to only test whether the 189 | edges of the polygon intersect the `Circle`. This means that a Polygon that is 190 | completely inscribed in, or circumscribed by the `Circle` will not be considered colliding. 191 | 192 | This can be useful for performance reasons if you only care about the edges of the 193 | polygon. 194 | 195 | .. note:: 196 | Keep in mind that the more vertices the polygon has, the more CPU time it will 197 | take to calculate the collision. 198 | 199 | .. ## Polygon.collidecircle ## 200 | 201 | .. method:: as_segments 202 | 203 | | :sl:`returns the line segments of the polygon` 204 | | :sg:`as_segments() -> list[Line]` 205 | 206 | Returns a list of the line segments of the polygon given as self. 207 | 208 | .. ## Polygon.as_segments ## 209 | 210 | .. method:: copy 211 | 212 | | :sl:`returns a copy of the polygon` 213 | | :sg:`copy() -> Polygon` 214 | 215 | Returns a copy of the `Polygon`. The copy is a new `Polygon` object with the same 216 | vertices as the original `Polygon`. 217 | 218 | .. note:: 219 | Keep in mind that the more vertices the polygon has, the more CPU time it will 220 | take to copy it. 221 | 222 | .. ## Polygon.copy ## 223 | 224 | .. method:: rotate 225 | 226 | | :sl:`rotates the polygon by a given angle` 227 | | :sg:`rotate(angle, rotation_point) -> Polygon` 228 | 229 | Returns a new Polygon that is rotated by the given angle (in degrees). The original 230 | Polygon is not modified. The rotation is done around the center of the `Polygon` by 231 | default but can be changed by passing a different rotation point. 232 | 233 | .. note:: 234 | Rotating the polygon by positive angles will rotate it clockwise, while 235 | rotating it by negative angles will rotate it counter-clockwise. 236 | 237 | .. note:: 238 | Keep in mind that the more vertices the polygon has, the more CPU time it will 239 | take to rotate it. 240 | 241 | .. ## Polygon.rotate ## 242 | 243 | .. method:: rotate_ip 244 | 245 | | :sl:`rotates the polygon by a given angle` 246 | | :sg:`rotate_ip(angle, rotation_point) -> None` 247 | 248 | Rotates the Polygon by the given angle (in degrees). The original Polygon 249 | is modified. Always returns None. The rotation is done around the center of the 250 | `Polygon` by default but can be changed by passing a different rotation point. 251 | 252 | .. note:: 253 | Rotating the polygon by positive angles will rotate it clockwise, while 254 | rotating it by negative angles will rotate it counter-clockwise. 255 | 256 | .. note:: 257 | Keep in mind that the more vertices the polygon has, the more CPU time it will 258 | take to rotate it. 259 | 260 | .. ## Polygon.rotate_ip ## 261 | 262 | .. method:: is_convex 263 | 264 | | :sl:`checks whether the polygon is convex or concave` 265 | | :sg:`is_convex() -> bool` 266 | 267 | Checks whether the polygon is convex or concave. 268 | 269 | .. note:: 270 | Keep in mind that the more vertices the polygon has, the more CPU time it will 271 | take to check if it's convex. 272 | 273 | .. ## Polygon.is_convex ## 274 | 275 | .. method:: insert_vertex 276 | | :sl:`inserts a new vertex to the polygon` 277 | | :sg:`insert_vertex(index, (x, y)) -> None` 278 | | :sg:`insert_vertex(index, Vector2) -> None` 279 | 280 | Adds a new vertex to the `Polygon`'s vertices at the specified index. The method returns `None`. 281 | 282 | .. note:: 283 | The `index` argument can be any positive or negative integer. If the `index` is 284 | negative, it is counted from the end of the list of vertices. For example, if 285 | the `index` is -1, the vertex will be inserted at the end of the list. If the 286 | `index` is positive and greater than the number of vertices, the vertex 287 | will always be inserted at the end of the list. 288 | 289 | Inserting a vertex at the end of the list: 290 | ``` 291 | polygon = Polygon([(0,0), (1,0), (1,1)]) 292 | polygon.insert_vertex(-1, (0,1)) # same will happen with 3 or more as index 293 | print(polygon.vertices) 294 | # Output: [(0,0), (1,0), (1,1), (0,1)] 295 | ``` 296 | 297 | .. note:: 298 | You can insert as many vertices as memory available, but keep in mind that a larger 299 | number of vertices will increase CPU usage for tasks such as calculating collisions, 300 | moving the `Polygon`, copying, etc. It is recommended to keep the number of 301 | vertices as low as possible. 302 | 303 | .. ## Polygon.insert_vertex ## 304 | 305 | 306 | .. method:: remove_vertex 307 | | :sl:`removes a vertex from the polygon` 308 | | :sg:`remove_vertex(index) -> None` 309 | 310 | Removes a vertex at the given `index` from the `Polygon`, but only if the `Polygon` 311 | has more than three vertices already. This method always returns `None`. 312 | 313 | .. note:: 314 | The `index` must be less than the number of vertices in the `Polygon`. 315 | If `index` is 0, the first vertex will be removed. If `index` is the number 316 | of vertices in the `Polygon` minus one, the last vertex will be removed. 317 | If a negative `index` is given, it will be counted from the end of the list of vertices. 318 | For example, an `index` of -1 will remove the last vertex in the `Polygon`. 319 | 320 | .. note:: 321 | The minimum number of vertices for a `Polygon` is 3 (a triangle), so you cannot 322 | remove a vertex if the `Polygon` only has 3 vertices. 323 | If you attempt to do so, the vertex will not be removed and an error will be raised. 324 | 325 | .. ## Polygon.remove_vertex ## 326 | 327 | .. method:: pop_vertex 328 | | :sl:`removes and returns a vertex from the polygon` 329 | | :sg:`pop_vertex(index) -> (x, y)` 330 | 331 | Removes and returns the vertex at the given index from the `Polygon`, but only if 332 | it has more than three vertices already. Returns the removed vertex as a tuple. 333 | 334 | .. note:: 335 | The given index must be less than the number of vertices of the `Polygon`. 336 | If the index is 0, the first vertex will be removed. If the index is the number 337 | of vertices of the `Polygon` minus one, the last vertex will be removed. 338 | If a negative index is given, it will be counted from the end of the list of 339 | vertices. 340 | .. note:: 341 | Since the minimum number of vertices for a `Polygon` is 3 (triangle), you 342 | cannot remove a vertex if the `Polygon` only has 3 vertices. If you try, 343 | the vertex will not be removed and an error will be raised. 344 | 345 | .. ## Polygon.pop_vertex ## 346 | 347 | .. method:: as_rect 348 | 349 | | :sl:`returns the bounding box of the polygon` 350 | | :sg:`as_rect() -> Rect` 351 | 352 | Returns a `pygame.Rect` object that contains the `Polygon`. The Rect object will 353 | be the smallest rectangle that contains the `Polygon`. 354 | 355 | .. note:: 356 | In the case of a polygon with all vertices on a same line or all in the same 357 | point, the returned Rect will have a width and height of 1. This is because 358 | the Rect object cannot have a width or height of 0. 359 | 360 | .. note:: 361 | Keep in mind that the more vertices the polygon has, the more CPU time it will 362 | take to calculate the bounding box. 363 | 364 | .. ## Polygon.as_rect ## 365 | 366 | .. method:: scale 367 | 368 | | :sl:`scales the polygon by a given factor` 369 | | :sg:`scale(factor) -> Polygon` 370 | 371 | Returns a new Polygon that is scaled by the given factor. The original Polygon is 372 | not modified. The scaling is done relative to the center of the `Polygon`. 373 | 374 | .. note:: 375 | Using a `factor` greater than 1 will enlarge the `Polygon`. 376 | Using a `factor` less than 1 will shrink the `Polygon`. 377 | 378 | .. warning:: 379 | Scaling a `Polygon` by a factor very close to 0 could make it disappear. 380 | This is because the vertices of the `Polygon` will be so close to each other 381 | that they will be considered to be in the same point. This is a limitation of 382 | the algorithm used to calculate the collisions and a general limitation of 383 | the floating point numbers used in computers. 384 | 385 | .. note:: 386 | Keep in mind that the more vertices the polygon has, the more CPU time it will 387 | take to scale it. 388 | 389 | .. ## Polygon.scale ## 390 | 391 | .. method:: scale_ip 392 | 393 | | :sl:`scales the polygon by a given factor` 394 | | :sg:`scale_ip(factor) -> None` 395 | 396 | Scales the Polygon from its center by the given factor. The original Polygon is modified. 397 | Always returns None. 398 | 399 | .. note:: 400 | Using a `factor` greater than 1 will enlarge the `Polygon`. 401 | Using a `factor` less than 1 will shrink the `Polygon`. 402 | 403 | .. warning:: 404 | Repeatedly scaling a `Polygon` by a factor less than 1 will eventually make it 405 | disappear. This is because the vertices of the `Polygon` will be so close to 406 | each other that they will be considered to be in the same point. This is a 407 | limitation of the algorithm used to calculate the collisions and a general 408 | limitation of the floating point numbers used in computers. 409 | 410 | .. note:: 411 | Keep in mind that the more vertices the polygon has, the more CPU time it will 412 | take to scale it. 413 | 414 | .. ## Polygon.scale_ip ## -------------------------------------------------------------------------------- /src_c/collisions.c: -------------------------------------------------------------------------------- 1 | #include "include/collisions.h" 2 | #include "simd_collisions.h" 3 | #include 4 | 5 | #if defined(__GNUC__) && \ 6 | (__GNUC__ > 2 || (__GNUC__ == 2 && (__GNUC_MINOR__ > 95))) 7 | #define LIKELY(x) __builtin_expect(!!(x), 1) 8 | #define UNLIKELY(x) __builtin_expect(!!(x), 0) 9 | #else 10 | #define LIKELY(x) (x) 11 | #define UNLIKELY(x) (x) 12 | #endif 13 | 14 | static int 15 | pgCollision_LineLine(pgLineBase *A, pgLineBase *B) 16 | { 17 | double x1_m_x2 = A->xa - A->xb; 18 | double y3_m_y4 = B->ya - B->yb; 19 | double y1_m_y2 = A->ya - A->yb; 20 | double x3_m_x4 = B->xa - B->xb; 21 | 22 | double den = x1_m_x2 * y3_m_y4 - y1_m_y2 * x3_m_x4; 23 | 24 | if (!den) 25 | return 0; 26 | 27 | double x1_m_x3 = A->xa - B->xa; 28 | double y1_m_y3 = A->ya - B->ya; 29 | 30 | double t1 = x1_m_x3 * y3_m_y4 - y1_m_y3 * x3_m_x4; 31 | double t = t1 / den; 32 | 33 | double u1 = x1_m_x2 * y1_m_y3 - y1_m_y2 * x1_m_x3; 34 | double u = -(u1 / den); 35 | 36 | return t >= 0 && t <= 1 && u >= 0 && u <= 1; 37 | } 38 | 39 | static int 40 | pgIntersection_LineLine(pgLineBase *A, pgLineBase *B, double *X, double *Y, 41 | double *T) 42 | { 43 | double xa = A->xa; 44 | double ya = A->ya; 45 | double xb = A->xb; 46 | double yb = A->yb; 47 | double x3 = B->xa; 48 | double y3 = B->ya; 49 | double x4 = B->xb; 50 | double y4 = B->yb; 51 | 52 | double x1_m_x2 = xa - xb; 53 | double y3_m_y4 = y3 - y4; 54 | double y1_m_y2 = ya - yb; 55 | double x3_m_x4 = x3 - x4; 56 | 57 | double den = x1_m_x2 * y3_m_y4 - y1_m_y2 * x3_m_x4; 58 | 59 | if (!den) 60 | return 0; 61 | 62 | double x1_m_x3 = xa - x3; 63 | double y1_m_y3 = ya - y3; 64 | 65 | double t1 = x1_m_x3 * y3_m_y4 - y1_m_y3 * x3_m_x4; 66 | double t = t1 / den; 67 | 68 | double u1 = x1_m_x2 * y1_m_y3 - y1_m_y2 * x1_m_x3; 69 | double u = -(u1 / den); 70 | 71 | if (t >= 0 && t <= 1 && u >= 0 && u <= 1) { 72 | if (X) 73 | *X = xa + t * (xb - xa); 74 | if (Y) 75 | *Y = ya + t * (yb - ya); 76 | if (T) 77 | *T = t; 78 | return 1; 79 | } 80 | return 0; 81 | } 82 | 83 | static int 84 | pgCollision_LinePoint(pgLineBase *line, double Cx, double Cy) 85 | { 86 | double dx = line->xa - Cx; 87 | double dy = line->ya - Cy; 88 | double dx2 = line->xb - Cx; 89 | double dy2 = line->yb - Cy; 90 | double dx3 = line->xa - line->xb; 91 | double dy3 = line->ya - line->yb; 92 | 93 | double d = sqrt(dx * dx + dy * dy) + sqrt(dx2 * dx2 + dy2 * dy2); 94 | double d3 = sqrt(dx3 * dx3 + dy3 * dy3); 95 | 96 | double width = 0.000001; 97 | return d >= d3 - width && d <= d3 + width; 98 | } 99 | 100 | static int 101 | pgCollision_CirclePoint(pgCircleBase *circle, double Cx, double Cy) 102 | { 103 | double dx = circle->x - Cx; 104 | double dy = circle->y - Cy; 105 | return dx * dx + dy * dy <= circle->r * circle->r; 106 | } 107 | 108 | static int 109 | pgIntersection_LineCircle(pgLineBase *line, pgCircleBase *circle, double *X, 110 | double *Y, double *T) 111 | { 112 | double xa = line->xa; 113 | double ya = line->ya; 114 | double xb = line->xb; 115 | double yb = line->yb; 116 | double xc = circle->x; 117 | double yc = circle->y; 118 | double rsq = circle->r * circle->r; 119 | 120 | double x1_m_xc = xa - xc; 121 | double y1_m_yc = ya - yc; 122 | 123 | double dx = xb - xa; 124 | double dy = yb - ya; 125 | double A = dx * dx + dy * dy; 126 | double B = 2 * (dx * x1_m_xc + dy * y1_m_yc); 127 | double C = x1_m_xc * x1_m_xc + y1_m_yc * y1_m_yc - rsq; 128 | double discriminant = B * B - 4 * A * C; 129 | if (discriminant < 0) { 130 | return 0; 131 | } 132 | double sqrt_d = sqrt(discriminant); 133 | double t = (-B - sqrt_d) / (2 * A); 134 | if (t < 0 || t > 1) { 135 | t = (-B + sqrt_d) / (2 * A); 136 | if (t < 0 || t > 1) { 137 | return 0; 138 | } 139 | } 140 | 141 | if (X) 142 | *X = xa + t * dx; 143 | if (Y) 144 | *Y = ya + t * dy; 145 | if (T) 146 | *T = t; 147 | return 1; 148 | } 149 | 150 | static int 151 | pgCollision_LineCircle(pgLineBase *line, pgCircleBase *circle) 152 | { 153 | double xa = line->xa; 154 | double ya = line->ya; 155 | double xb = line->xb; 156 | double yb = line->yb; 157 | double cx = circle->x; 158 | double cy = circle->y; 159 | double r = circle->r; 160 | 161 | if (pgCollision_CirclePoint(circle, xa, ya) || 162 | pgCollision_CirclePoint(circle, xb, yb)) 163 | return 1; 164 | 165 | double dx = xa - xb; 166 | double dy = ya - yb; 167 | double len = sqrt((dx * dx) + (dy * dy)); 168 | 169 | double dot = 170 | (((cx - xa) * (xb - xa)) + ((cy - ya) * (yb - ya))) / (len * len); 171 | 172 | double closest_x = xa + (dot * (xb - xa)); 173 | double closest_y = ya + (dot * (yb - ya)); 174 | 175 | pgLineBase line2 = {xa, ya, xb, yb}; 176 | if (!pgCollision_LinePoint(&line2, closest_x, closest_y)) 177 | return 0; 178 | 179 | dx = closest_x - cx; 180 | dy = closest_y - cy; 181 | double distance = sqrt((dx * dx) + (dy * dy)); 182 | 183 | return distance <= r; 184 | } 185 | 186 | static int 187 | pgCollision_CircleCircle(pgCircleBase *A, pgCircleBase *B) 188 | { 189 | double dx, dy; 190 | double sum_radi; 191 | 192 | dx = A->x - B->x; 193 | dy = A->y - B->y; 194 | sum_radi = A->r + B->r; 195 | 196 | return dx * dx + dy * dy <= sum_radi * sum_radi; 197 | } 198 | 199 | static int 200 | pgIntersection_LineRect(pgLineBase *line, SDL_Rect *rect, double *X, double *Y, 201 | double *T) 202 | { 203 | #if AVX2_IS_SUPPORTED 204 | if (pg_HasAVX2()) 205 | return pgIntersection_LineRect_avx2(line, rect, X, Y, T); 206 | #endif /* ~__AVX2__ */ 207 | 208 | double x = (double)rect->x; 209 | double y = (double)rect->y; 210 | double w = (double)rect->w; 211 | double h = (double)rect->h; 212 | 213 | pgLineBase a = {x, y, x + w, y}; 214 | pgLineBase b = {x, y, x, y + h}; 215 | pgLineBase c = {x, y + h, x + w, y + h}; 216 | pgLineBase d = {x + w, y, x + w, y + h}; 217 | 218 | int ret = 0; 219 | 220 | double temp_t = DBL_MAX; 221 | double final_t = DBL_MAX; 222 | 223 | ret |= pgIntersection_LineLine(line, &a, NULL, NULL, &temp_t); 224 | final_t = MIN(temp_t, final_t); 225 | ret |= pgIntersection_LineLine(line, &b, NULL, NULL, &temp_t); 226 | final_t = MIN(temp_t, final_t); 227 | ret |= pgIntersection_LineLine(line, &c, NULL, NULL, &temp_t); 228 | final_t = MIN(temp_t, final_t); 229 | ret |= pgIntersection_LineLine(line, &d, NULL, NULL, &temp_t); 230 | final_t = MIN(temp_t, final_t); 231 | 232 | if (ret) { 233 | if (X) 234 | *X = line->xa + final_t * (line->xb - line->xa); 235 | if (Y) 236 | *Y = line->ya + final_t * (line->yb - line->ya); 237 | if (T) 238 | *T = final_t; 239 | } 240 | 241 | return ret; 242 | } 243 | 244 | static int 245 | pgCollision_RectLine(SDL_Rect *rect, pgLineBase *line) 246 | { 247 | #if AVX2_IS_SUPPORTED 248 | if (pg_HasAVX2()) 249 | return pgCollision_RectLine_avx2(rect, line); 250 | #endif /* ~__AVX2__ */ 251 | 252 | double x = (double)rect->x; 253 | double y = (double)rect->y; 254 | double w = (double)rect->w; 255 | double h = (double)rect->h; 256 | 257 | pgLineBase a = {x, y, x + w, y}; 258 | pgLineBase b = {x, y, x, y + h}; 259 | pgLineBase c = {x, y + h, x + w, y + h}; 260 | pgLineBase d = {x + w, y, x + w, y + h}; 261 | 262 | return pgCollision_LineLine(line, &a) || pgCollision_LineLine(line, &b) || 263 | pgCollision_LineLine(line, &c) || pgCollision_LineLine(line, &d); 264 | } 265 | 266 | static int 267 | pgCollision_RectCircle(SDL_Rect *rect, pgCircleBase *circle) 268 | { 269 | double cx = circle->x, cy = circle->y; 270 | double rx = (double)rect->x, ry = (double)rect->y; 271 | double rw = (double)rect->w, rh = (double)rect->h; 272 | double r_bottom = ry + rh; 273 | double r_right = rx + rw; 274 | 275 | double test_x = cx; 276 | double test_y = cy; 277 | 278 | if (cx < rx) { 279 | test_x = rx; 280 | } 281 | else if (cx > r_right) { 282 | test_x = r_right; 283 | } 284 | 285 | if (cy < ry) { 286 | test_y = ry; 287 | } 288 | else if (cy > r_bottom) { 289 | test_y = r_bottom; 290 | } 291 | 292 | double dx = cx - test_x; 293 | double dy = cy - test_y; 294 | 295 | return dx * dx + dy * dy <= circle->r * circle->r; 296 | } 297 | 298 | static int 299 | pgCollision_PolygonPoint(pgPolygonBase *poly, double x, double y) 300 | { 301 | int collision = 0; 302 | Py_ssize_t i, j; 303 | 304 | for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) { 305 | double xi = poly->vertices[i * 2]; 306 | double yi = poly->vertices[i * 2 + 1]; 307 | 308 | if (x == xi && y == yi) { 309 | return 1; 310 | } 311 | 312 | double xj = poly->vertices[j * 2]; 313 | double yj = poly->vertices[j * 2 + 1]; 314 | 315 | if (((yi > y) != (yj > y)) && 316 | (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { 317 | collision = !collision; 318 | } 319 | } 320 | 321 | return collision; 322 | } 323 | 324 | static inline int 325 | pgCollision_PolygonPoints(pgPolygonBase *poly, double xa, double ya, double xb, 326 | double yb) 327 | { 328 | int collision1 = 0, collision2 = 0; 329 | Py_ssize_t i, j; 330 | 331 | double *vertices = poly->vertices; 332 | 333 | for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) { 334 | double xi = vertices[i * 2]; 335 | double yi = vertices[i * 2 + 1]; 336 | double xj = vertices[j * 2]; 337 | double yj = vertices[j * 2 + 1]; 338 | 339 | double xj_xi = xj - xi; 340 | double yj_yi = yj - yi; 341 | 342 | if (((yi > ya) != (yj > ya)) && 343 | (xa < xj_xi * (ya - yi) / yj_yi + xi)) { 344 | collision1 = !collision1; 345 | } 346 | 347 | if (((yi > yb) != (yj > yb)) && 348 | (xb < xj_xi * (yb - yi) / yj_yi + xi)) { 349 | collision2 = !collision2; 350 | } 351 | } 352 | 353 | return collision1 || collision2; 354 | } 355 | 356 | static inline int 357 | _pgCollision_line_polyedges(pgLineBase *line, pgPolygonBase *poly) 358 | { 359 | Py_ssize_t i, j; 360 | double *vertices = poly->vertices; 361 | 362 | for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) { 363 | if (pgCollision_LineLine( 364 | line, &(pgLineBase){vertices[j * 2], vertices[j * 2 + 1], 365 | vertices[i * 2], vertices[i * 2 + 1]})) { 366 | return 1; 367 | } 368 | } 369 | 370 | return 0; 371 | } 372 | 373 | static inline int 374 | pgCollision_PolygonLine(pgPolygonBase *poly, pgLineBase *line, int only_edges) 375 | { 376 | int collision = _pgCollision_line_polyedges(line, poly); 377 | 378 | if (collision || only_edges) { 379 | return collision; 380 | } 381 | 382 | return pgCollision_PolygonPoints(poly, line->xa, line->ya, line->xb, 383 | line->yb); 384 | } 385 | 386 | static int 387 | _pgCollision_PolygonPoint_opt(pgPolygonBase *poly, double x, double y) 388 | { 389 | /* This is a faster version of pgCollision_PolygonPoint that assumes 390 | * that the point passed is not on one of the polygon's vertices. */ 391 | int collision = 0; 392 | Py_ssize_t i, j; 393 | 394 | for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) { 395 | double xi = poly->vertices[i * 2]; 396 | double yi = poly->vertices[i * 2 + 1]; 397 | double xj = poly->vertices[j * 2]; 398 | double yj = poly->vertices[j * 2 + 1]; 399 | 400 | if (((yi > y) != (yj > y)) && 401 | (x < (xj - xi) * (y - yi) / (yj - yi) + xi)) { 402 | collision = !collision; 403 | } 404 | } 405 | 406 | return collision; 407 | } 408 | 409 | static int 410 | pgCollision_CirclePolygon(pgCircleBase *circle, pgPolygonBase *poly, 411 | int only_edges) 412 | { 413 | Py_ssize_t i, j; 414 | double cx = circle->x; 415 | double cy = circle->y; 416 | double cr = circle->r; 417 | double cr_sqr = cr * cr; 418 | 419 | /* Check if the circle is colliding with any of the polygon's edges. */ 420 | for (i = 0, j = poly->verts_num - 1; i < poly->verts_num; j = i++) { 421 | double xi = poly->vertices[i * 2]; 422 | double yi = poly->vertices[i * 2 + 1]; 423 | double xj = poly->vertices[j * 2]; 424 | double yj = poly->vertices[j * 2 + 1]; 425 | 426 | double dx = xj - xi; 427 | double dy = yj - yi; 428 | 429 | double xi_m_cx = xi - cx; 430 | double yi_m_cy = yi - cy; 431 | 432 | double at2 = 2 * (dx * dx + dy * dy); 433 | double b = 2 * (dx * xi_m_cx + dy * yi_m_cy); 434 | double c = xi_m_cx * xi_m_cx + yi_m_cy * yi_m_cy - cr_sqr; 435 | 436 | double bb4ac = b * b - 2 * at2 * c; 437 | 438 | if (bb4ac < 0) { 439 | continue; 440 | } 441 | 442 | double sqrt_bb4ac = sqrt(bb4ac); 443 | double mu1 = (-b + sqrt_bb4ac) / at2; 444 | double mu2 = (-b - sqrt_bb4ac) / at2; 445 | 446 | if ((0 <= mu1 && mu1 <= 1) || (0 <= mu2 && mu2 <= 1)) { 447 | return 1; 448 | } 449 | } 450 | 451 | /* Circle is not colliding with any of the polygon's edges. If we only 452 | * care for edge collision, return now. */ 453 | if (only_edges) { 454 | return 0; 455 | } 456 | 457 | int center_inside = _pgCollision_PolygonPoint_opt(poly, cx, cy); 458 | 459 | if (center_inside) { 460 | return 1; 461 | } 462 | 463 | /* Check if any of the polygon's vertices are inside the circle */ 464 | for (i = 0; i < poly->verts_num; i++) { 465 | double dx = poly->vertices[i * 2] - cx; 466 | double dy = poly->vertices[i * 2 + 1] - cy; 467 | 468 | if (dx * dx + dy * dy <= cr_sqr) { 469 | return 1; 470 | } 471 | } 472 | 473 | return 0; 474 | } 475 | 476 | static int 477 | pgRaycast_LineLine(pgLineBase *A, pgLineBase *B, double max_t, double *T) 478 | { 479 | double xa = A->xa; 480 | double ya = A->ya; 481 | double xb = A->xb; 482 | double yb = A->yb; 483 | double x3 = B->xa; 484 | double y3 = B->ya; 485 | double x4 = B->xb; 486 | double y4 = B->yb; 487 | 488 | double x1_m_x2 = xa - xb; 489 | double y3_m_y4 = y3 - y4; 490 | double y1_m_y2 = ya - yb; 491 | double x3_m_x4 = x3 - x4; 492 | 493 | double den = x1_m_x2 * y3_m_y4 - y1_m_y2 * x3_m_x4; 494 | 495 | if (!den) 496 | return 0; 497 | 498 | double x1_m_x3 = xa - x3; 499 | double y1_m_y3 = ya - y3; 500 | 501 | double t = x1_m_x3 * y3_m_y4 - y1_m_y3 * x3_m_x4; 502 | t /= den; 503 | 504 | double u = x1_m_x2 * y1_m_y3 - y1_m_y2 * x1_m_x3; 505 | u /= -den; 506 | 507 | if (t >= 0 && u >= 0 && u <= 1 && t <= max_t) { 508 | *T = t; 509 | return 1; 510 | } 511 | return 0; 512 | } 513 | 514 | static int 515 | pgRaycast_LineRect(pgLineBase *line, SDL_Rect *rect, double max_t, double *T) 516 | { 517 | #if AVX2_IS_SUPPORTED 518 | if (pg_HasAVX2()) 519 | return pgRaycast_LineRect_avx2(line, rect, max_t, T); 520 | #endif /* ~__AVX2__ */ 521 | 522 | double x = (double)rect->x; 523 | double y = (double)rect->y; 524 | double w = (double)rect->w; 525 | double h = (double)rect->h; 526 | 527 | pgLineBase a = {x, y, x + w, y}; 528 | pgLineBase b = {x, y, x, y + h}; 529 | pgLineBase c = {x, y + h, x + w, y + h}; 530 | pgLineBase d = {x + w, y, x + w, y + h}; 531 | 532 | int ret = 0; 533 | 534 | double temp_t = DBL_MAX; 535 | double final_t = DBL_MAX; 536 | 537 | ret |= pgRaycast_LineLine(line, &a, max_t, &temp_t); 538 | final_t = MIN(temp_t, final_t); 539 | ret |= pgRaycast_LineLine(line, &b, max_t, &temp_t); 540 | final_t = MIN(temp_t, final_t); 541 | ret |= pgRaycast_LineLine(line, &c, max_t, &temp_t); 542 | final_t = MIN(temp_t, final_t); 543 | ret |= pgRaycast_LineLine(line, &d, max_t, &temp_t); 544 | final_t = MIN(temp_t, final_t); 545 | 546 | if (ret && final_t <= max_t) { 547 | *T = final_t; 548 | } 549 | 550 | return ret; 551 | } 552 | 553 | static int 554 | pgRaycast_LineCircle(pgLineBase *line, pgCircleBase *circle, double max_t, 555 | double *T) 556 | { 557 | double xa = line->xa; 558 | double ya = line->ya; 559 | double xb = line->xb; 560 | double yb = line->yb; 561 | double xc = circle->x; 562 | double yc = circle->y; 563 | 564 | double x1_m_xc = xa - xc; 565 | double y1_m_yc = ya - yc; 566 | 567 | double dx = xb - xa; 568 | double dy = yb - ya; 569 | double A = dx * dx + dy * dy; 570 | double B = 2 * (dx * x1_m_xc + dy * y1_m_yc); 571 | double C = x1_m_xc * x1_m_xc + y1_m_yc * y1_m_yc - circle->r * circle->r; 572 | 573 | double discriminant = B * B - 4 * A * C; 574 | if (discriminant < 0) { 575 | return 0; 576 | } 577 | double sqrt_d = sqrt(discriminant); 578 | double t = (-B - sqrt_d) / (2 * A); 579 | if (t < 0) { 580 | t = (-B + sqrt_d) / (2 * A); 581 | if (t < 0) { 582 | return 0; 583 | } 584 | } 585 | 586 | if (t <= max_t) 587 | *T = t; 588 | 589 | return 1; 590 | } 591 | 592 | static int 593 | pgIntersection_CircleCircle(pgCircleBase *A, pgCircleBase *B, 594 | double *intersections) 595 | { 596 | double dx = B->x - A->x; 597 | double dy = B->y - A->y; 598 | double d2 = dx * dx + dy * dy; 599 | double r_sum = A->r + B->r; 600 | double r_diff = A->r - B->r; 601 | double r_sum2 = r_sum * r_sum; 602 | double r_diff2 = r_diff * r_diff; 603 | 604 | if (d2 > r_sum2 || d2 < r_diff2) { 605 | return 0; 606 | } 607 | 608 | if (double_compare(d2, 0) && double_compare(A->r, B->r)) { 609 | return 0; 610 | } 611 | 612 | double d = sqrt(d2); 613 | double a = (d2 + A->r * A->r - B->r * B->r) / (2 * d); 614 | double h = sqrt(A->r * A->r - a * a); 615 | 616 | double xm = A->x + a * (dx / d); 617 | double ym = A->y + a * (dy / d); 618 | 619 | double xs1 = xm + h * (dy / d); 620 | double ys1 = ym - h * (dx / d); 621 | double xs2 = xm - h * (dy / d); 622 | double ys2 = ym + h * (dx / d); 623 | 624 | if (double_compare(d2, r_sum2) || double_compare(d2, r_diff2)) { 625 | intersections[0] = xs1; 626 | intersections[1] = ys1; 627 | return 1; 628 | } 629 | 630 | intersections[0] = xs1; 631 | intersections[1] = ys1; 632 | intersections[2] = xs2; 633 | intersections[3] = ys2; 634 | return 2; 635 | } 636 | --------------------------------------------------------------------------------