nextrpg.draw_on_screen
Drawable on screen.
1""" 2Drawable on screen. 3""" 4 5from __future__ import annotations 6 7from abc import ABC, abstractmethod 8from dataclasses import KW_ONLY, field 9from functools import cached_property, singledispatchmethod 10from itertools import product 11from math import ceil, sqrt 12from pathlib import Path 13from typing import override 14 15from pygame import Mask, SRCALPHA, Surface 16from pygame.draw import polygon 17from pygame.image import load 18from pygame.mask import from_surface 19 20from nextrpg.config import config 21from nextrpg.core import Direction, DirectionalOffset, Pixel, Rgba, Size 22from nextrpg.model import Model, cached, internal_field 23 24 25class Coordinate(Model): 26 """ 27 Represents a 2D coordinate with immutability and provides methods 28 for scaling and shifting coordinates. 29 30 Attributes: 31 `left`: The horizontal position of the coordinate, measured by 32 the number of pixels from the left edge of the game window. 33 34 `top`: The vertical position of the coordinate, measured by 35 the number of pixels from the top edge of the game window. 36 """ 37 38 left: Pixel 39 top: Pixel 40 41 def __mul__(self, scale: float) -> Coordinate: 42 """ 43 Scales the current `Coordinate` values (left and top) by a given factor 44 and returns a new `Coordinate` with the scaled values rounded up to the 45 nearest integer. 46 47 Round up so that drawings won't leave tiny, black gaps after scaled. 48 49 Args: 50 `scale`: The scaling factor to multiply the left and 51 top values of the `Coordinate`. 52 53 Returns: 54 `Coordinate`: A new `Coordinate` with the scaled and rounded values. 55 """ 56 return Coordinate(ceil(self.left * scale), ceil(self.top * scale)) 57 58 def __sub__(self, other: Coordinate) -> Coordinate: 59 return Coordinate(self.left - other.left, self.top - other.top) 60 61 @singledispatchmethod 62 def __add__(self, offset: DirectionalOffset | Coordinate) -> Coordinate: 63 """ 64 Shifts the coordinate in the specified direction by a given offset. 65 Supports both orthogonal and diagonal directions. 66 67 Or add two coordinates together. 68 69 For diagonal directions, the offset is divided proportionally. 70 For example, an offset of `sqrt(2)` in `UP_LEFT` direction shifts 71 the coordinate `Pixel(1)` in both `UP` and `LEFT` directions. 72 73 Args: 74 `offset`: A `DirectionalOffset` representing the direction 75 and offset, or `Coordinate` to add to the current `Coordinate`. 76 77 Returns: 78 `Coordinate`: A new coordinate shifted by the specified offset in 79 the given direction. 80 """ 81 raise NotImplementedError(f"Non-addable {offset=}") 82 83 @property 84 def tuple(self) -> tuple[Pixel, Pixel]: 85 """ 86 Gets the coordinates as a tuple. 87 88 Returns: 89 `tuple[Pixel, Pixel]`: A tuple containing the left and top 90 values in that order. 91 """ 92 return self.left, self.top 93 94 95@Coordinate.__add__.register 96def _add_directional_offset(self, offset: DirectionalOffset) -> Coordinate: 97 match offset.direction: 98 case Direction.UP: 99 return Coordinate(self.left, self.top - offset.offset) 100 case Direction.DOWN: 101 return Coordinate(self.left, self.top + offset.offset) 102 case Direction.LEFT: 103 return Coordinate(self.left - offset.offset, self.top) 104 case Direction.RIGHT: 105 return Coordinate(self.left + offset.offset, self.top) 106 107 diag = offset.offset / sqrt(2) 108 match offset.direction: 109 case Direction.UP_LEFT: 110 return Coordinate(self.left - diag, self.top - diag) 111 case Direction.UP_RIGHT: 112 return Coordinate(self.left + diag, self.top - diag) 113 case Direction.DOWN_LEFT: 114 return Coordinate(self.left - diag, self.top + diag) 115 case Direction.DOWN_RIGHT: 116 return Coordinate(self.left + diag, self.top + diag) 117 raise ValueError(f"Invalid direction: {offset.direction}") 118 119 120@Coordinate.__add__.register 121def _add_coordinate(self, offset: Coordinate) -> Coordinate: 122 return Coordinate(self.left + offset.left, self.top + offset.top) 123 124 125@cached( 126 lambda: config().resource.drawing_cache_size, 127 lambda resource: resource if isinstance(resource, Path) else None, 128) 129class Drawing(Model): 130 """ 131 Represents a drawable element and provides methods for accessing its size 132 and dimensions. 133 134 This class loads a surface from a file or directly accepts a 135 `pygame.Surface` as input. 136 It provides properties to access surface dimensions and size and methods to 137 crop and scale the surface. 138 """ 139 140 resource: Path | Surface = field(repr=False) 141 _: KW_ONLY = field() 142 _surface: Surface = internal_field( 143 lambda self: ( 144 self.resource 145 if isinstance(self.resource, Surface) 146 else load(self.resource).convert_alpha() 147 ) 148 ) 149 150 def __repr__(self) -> str: 151 """ 152 A string representation of the `Drawing` object. 153 154 Returns: 155 `str`: A string representation of the `Drawing` object. 156 """ 157 return f"Drawing({self.size})" 158 159 @property 160 def width(self) -> Pixel: 161 """ 162 Gets the width of the surface. 163 164 Returns: 165 `Pixel`: The width of the surface in pixel measurement. 166 """ 167 return self._surface.get_width() 168 169 @property 170 def height(self) -> Pixel: 171 """ 172 Gets the height of the surface. 173 174 Returns: 175 `Pixel`: The height of the surface in pixel measurement. 176 """ 177 return self._surface.get_height() 178 179 @property 180 def size(self) -> Size: 181 """ 182 Gets the size of an object as a combination of its width and height 183 184 Returns: 185 `Size`: A Size object containing the width and height of the object. 186 """ 187 return Size(self.width, self.height) 188 189 @property 190 def pygame(self) -> Surface: 191 """ 192 Gets the current `pygame.Surface` for the object. 193 194 Returns: 195 `pygame.Surface`: The underlying `pygame.Surface`. 196 """ 197 return self._debug_surface or self._surface 198 199 def crop(self, top_left: Coordinate, size: Size) -> Drawing: 200 """ 201 Crops a rectangular portion of the drawing specified by the 202 top-left corner and the size. 203 204 The method extracts a subsection of the drawing based on the provided 205 coordinates and dimensions and returns a new `Drawing` instance. 206 The original drawing remains unchanged. 207 208 Args: 209 `top_left`: The top-left coordinate of the rectangle to be cropped. 210 211 `size`: The width and height of the rectangle to be cropped. 212 213 Returns: 214 `Drawing`: A new `Drawing` instance representing the cropped area. 215 """ 216 left, top = top_left.tuple 217 width, height = size.tuple 218 return Drawing(self._surface.subsurface((left, top, width, height))) 219 220 @cached_property 221 def _debug_surface(self) -> Surface | None: 222 if not (debug := config().debug): 223 return None 224 surface = Surface(self.size.tuple, SRCALPHA) 225 surface.fill(debug.drawing_background_color.tuple) 226 surface.blit(self._surface, (0, 0)) 227 return surface 228 229 230class DrawOnScreen(Model): 231 """ 232 Represents a drawable element positioned on the screen with its coordinates. 233 234 This immutable class combines a drawing with its position and provides 235 properties to access various coordinates and dimensions of the drawing 236 on the screen. 237 238 Attributes: 239 `top_left`: The top-left position of the drawing on the screen. 240 241 `drawing`: The drawable element to be displayed on the screen. 242 """ 243 244 top_left: Coordinate 245 drawing: Drawing 246 247 @property 248 def rectangle(self) -> Rectangle: 249 """ 250 Gets the rectangular bounds of the drawing on screen. 251 252 Returns: 253 `Rectangle`: A rectangle defining the drawing's position and size 254 on screen. 255 """ 256 return Rectangle(self.top_left, self.drawing.size) 257 258 @cached_property 259 def visible_rectangle(self) -> Rectangle: 260 """ 261 Calculate the actual visible bounds of the drawing, 262 ignoring transparent pixels. 263 264 Returns: 265 `Rectangle`: A rectangle defining only the visible (non-transparent) 266 portion of the drawing on screen. 267 """ 268 visible = [ 269 Coordinate(x, y) 270 for x, y in product( 271 range(ceil(self.drawing.width)), 272 range(ceil(self.drawing.height)), 273 ) 274 # `drawing._surface` to ignore the debug background. 275 if self.drawing._surface.get_at((x, y)).a 276 ] 277 if not visible: 278 return Rectangle(Coordinate(0, 0), Size(0, 0)) 279 280 min_x = min(c.left for c in visible) 281 min_y = min(c.top for c in visible) 282 max_x = max(c.left for c in visible) 283 max_y = max(c.top for c in visible) 284 return Rectangle( 285 self.top_left + Coordinate(min_x, min_y), 286 Size(max_x - min_x, max_y - min_y), 287 ) 288 289 @property 290 def pygame(self) -> tuple[Surface, tuple[Pixel, Pixel]]: 291 """ 292 Gets the pygame surface and coordinate tuple for rendering. 293 294 Returns: 295 `tuple[pygame.Surface, tuple[float, float]]`: 296 A tuple containing the `pygame.Surface` and a tuple of the left 297 and top coordinates (x, y). 298 """ 299 return self.drawing.pygame, self.top_left.tuple 300 301 def __add__(self, coord: Coordinate) -> DrawOnScreen: 302 """ 303 Shift the drawing by the specified coordinate. 304 305 Args: 306 `coord`: The coordinate to shift the drawing by. 307 308 Returns: 309 `DrawOnScreen`: A new `DrawOnScreen` shifted by the coordinate. 310 """ 311 return DrawOnScreen(self.top_left + coord, self.drawing) 312 313 314class Polygon(ABC): 315 """ 316 Abstract base class for polygon, defining the common interface/behavior. 317 """ 318 319 @cached_property 320 @abstractmethod 321 def points(self) -> tuple[Coordinate, ...]: 322 """ 323 Get the points of the polygon. 324 325 Returns: 326 `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects 327 representing the points. 328 """ 329 330 @cached_property 331 def bounding_rectangle(self) -> Rectangle: 332 """ 333 Get a rectangle bounding the polygon. 334 335 Returns: 336 `Rectangle`: A rectangle bounding the polygon. 337 """ 338 min_x = min(c.left for c in self.points) 339 min_y = min(c.top for c in self.points) 340 max_x = max(c.left for c in self.points) 341 max_y = max(c.top for c in self.points) 342 return Rectangle( 343 Coordinate(min_x, min_y), Size(max_x - min_x, max_y - min_y) 344 ) 345 346 @cached_property 347 def _mask(self) -> Mask: 348 return from_surface(self.fill(Rgba(0, 0, 0, 255)).drawing.pygame) 349 350 def fill(self, color: Rgba) -> DrawOnScreen: 351 """ 352 Creates a colored `DrawOnScreen` with the provided color. 353 354 Args: 355 `Color`: The color to fill the rectangle. 356 357 Returns: 358 `DrawOnScreen`: A transparent surface matching rectangle dimensions. 359 """ 360 rect = self.bounding_rectangle 361 surface = Surface(rect.size.tuple, SRCALPHA) 362 polygon( 363 surface, 364 color.tuple, 365 [(p - rect.top_left).tuple for p in self.points], 366 ) 367 return DrawOnScreen(rect.top_left, Drawing(surface)) 368 369 def collide(self, poly: Polygon) -> bool: 370 """ 371 Checks if this rectangle overlaps with another polygon. 372 373 Args: 374 `poly`: The polygon to check for collision with. 375 376 Returns: 377 `bool`: True if two polygons overlap, False otherwise. 378 """ 379 if not self.bounding_rectangle.collide(poly.bounding_rectangle): 380 return False 381 offset = ( 382 self.bounding_rectangle.top_left - poly.bounding_rectangle.top_left 383 ) 384 return bool(self._mask.overlap(poly._mask, offset.tuple)) 385 386 def __contains__(self, coordinate: Coordinate) -> bool: 387 """ 388 Checks if a coordinate point lies within this polygon. 389 390 The point is considered inside if it falls in the polygon's bounds, 391 including points on the edges. 392 393 Arguments: 394 `coordinate`: The coordinate point to check 395 396 Returns: 397 `bool`: Whether the coordinate lies within the rectangle. 398 """ 399 x, y = (coordinate - self.bounding_rectangle.top_left).tuple 400 width, height = self._mask.get_size() 401 if 0 <= x < width and 0 <= y < height: 402 return bool(self._mask.get_at((x, y))) 403 return False 404 405 406class GenericPolygon(Model, Polygon): 407 """ 408 A collection of points that define a closed polygon. 409 410 Attributes: 411 `points`: A tuple of `Coordinate` objects representing the points 412 bounding the polygon. 413 """ 414 415 points: tuple[Coordinate, ...] 416 417 418class Rectangle(Model, Polygon): 419 """ 420 Represents an immutable rectangle defined by its top left corner and size. 421 422 Attributes: 423 `top_left`: The top-left corner of the rectangle. 424 425 `size`: The dimensions of the rectangle, including its width and height. 426 """ 427 428 top_left: Coordinate 429 size: Size 430 431 @property 432 def left(self) -> Pixel: 433 """ 434 Gets the leftmost x-coordinate of the drawing on the screen. 435 436 Returns: 437 `Pixel`: The leftmost x-coordinate. 438 """ 439 return self.top_left.left 440 441 @property 442 def right(self) -> Pixel: 443 """ 444 Gets the rightmost x-coordinate of the drawing on the screen. 445 446 Returns: 447 `Pixel`: The rightmost x-coordinate (left + width). 448 """ 449 return self.left + self.size.width 450 451 @property 452 def top(self) -> Pixel: 453 """ 454 Gets the topmost y-coordinate of the drawing on the screen. 455 456 Returns: 457 `Pixel`: The topmost y-coordinate. 458 """ 459 return self.top_left.top 460 461 @property 462 def bottom(self) -> Pixel: 463 """ 464 Gets the bottommost y-coordinate of the drawing on the screen. 465 466 Returns: 467 `Pixel`: The bottommost y-coordinate (top + height). 468 """ 469 return self.top + self.size.height 470 471 @property 472 def top_right(self) -> Coordinate: 473 """ 474 Gets the top-right coordinate of the drawing on the screen. 475 476 Returns: 477 `Coordinate`: The top-right coordinate. 478 """ 479 return Coordinate(self.right, self.top) 480 481 @property 482 def bottom_left(self) -> Coordinate: 483 """ 484 Gets the bottom-left coordinate of the drawing on the screen. 485 486 Returns: 487 `Coordinate`: The bottom-left coordinate. 488 """ 489 return Coordinate(self.left, self.bottom) 490 491 @property 492 def bottom_right(self) -> Coordinate: 493 """ 494 Gets the bottom-right coordinate of the drawing on the screen. 495 496 Returns: 497 `Coordinate`: The bottom-right coordinate. 498 """ 499 return Coordinate(self.right, self.bottom) 500 501 @property 502 def top_center(self) -> Coordinate: 503 """ 504 Gets the center point of the top edge of the drawing on the screen. 505 506 Returns: 507 `Coordinate`: The top-center coordinate. 508 """ 509 return Coordinate(self.left + self.size.width / 2, self.top) 510 511 @property 512 def bottom_center(self) -> Coordinate: 513 """ 514 Gets the center point of the bottom edge of the drawing on the screen. 515 516 Returns: 517 `Coordinate`: The bottom-center coordinate. 518 """ 519 return Coordinate(self.left + self.size.width / 2, self.bottom) 520 521 @property 522 def center_left(self) -> Coordinate: 523 """ 524 Gets the center point of the left edge of the drawing on the screen. 525 526 Returns: 527 `Coordinate`: The left-center coordinate. 528 """ 529 return Coordinate(self.left, self.top + self.size.height / 2) 530 531 @property 532 def center_right(self) -> Coordinate: 533 """ 534 Gets the center point of the right edge of the drawing on the screen. 535 536 Returns: 537 `Coordinate`: The right-center coordinate. 538 """ 539 return Coordinate(self.right, self.top + self.size.height / 2) 540 541 @property 542 def center(self) -> Coordinate: 543 """ 544 Gets the center point of the drawing on the screen. 545 546 Returns: 547 `Coordinate`: The center coordinate of the drawing. 548 """ 549 width, height = self.size.tuple 550 return Coordinate(self.left + width / 2, self.top + height / 2) 551 552 @property 553 def points(self) -> tuple[Coordinate, ...]: 554 """ 555 Get the coordinates of the corners of the rectangle. 556 557 Returns: 558 `tuple[Coordinate, ...]`: The coordinates of the corners 559 of the rectangle. 560 """ 561 return ( 562 self.top_left, 563 self.top_right, 564 self.bottom_right, 565 self.bottom_left, 566 ) 567 568 @override 569 def collide(self, poly: Polygon) -> bool: 570 if isinstance(poly, Rectangle): 571 return ( 572 self.top_left.left < poly.top_right.left 573 and self.top_right.left > poly.top_left.left 574 and self.top_left.top < poly.bottom_right.top 575 and self.bottom_right.top > poly.top_left.top 576 ) 577 return super().collide(poly) 578 579 @override 580 def __contains__(self, coordinate: Coordinate) -> bool: 581 return ( 582 self.left < coordinate.left < self.right 583 and self.top < coordinate.top < self.bottom 584 )
26class Coordinate(Model): 27 """ 28 Represents a 2D coordinate with immutability and provides methods 29 for scaling and shifting coordinates. 30 31 Attributes: 32 `left`: The horizontal position of the coordinate, measured by 33 the number of pixels from the left edge of the game window. 34 35 `top`: The vertical position of the coordinate, measured by 36 the number of pixels from the top edge of the game window. 37 """ 38 39 left: Pixel 40 top: Pixel 41 42 def __mul__(self, scale: float) -> Coordinate: 43 """ 44 Scales the current `Coordinate` values (left and top) by a given factor 45 and returns a new `Coordinate` with the scaled values rounded up to the 46 nearest integer. 47 48 Round up so that drawings won't leave tiny, black gaps after scaled. 49 50 Args: 51 `scale`: The scaling factor to multiply the left and 52 top values of the `Coordinate`. 53 54 Returns: 55 `Coordinate`: A new `Coordinate` with the scaled and rounded values. 56 """ 57 return Coordinate(ceil(self.left * scale), ceil(self.top * scale)) 58 59 def __sub__(self, other: Coordinate) -> Coordinate: 60 return Coordinate(self.left - other.left, self.top - other.top) 61 62 @singledispatchmethod 63 def __add__(self, offset: DirectionalOffset | Coordinate) -> Coordinate: 64 """ 65 Shifts the coordinate in the specified direction by a given offset. 66 Supports both orthogonal and diagonal directions. 67 68 Or add two coordinates together. 69 70 For diagonal directions, the offset is divided proportionally. 71 For example, an offset of `sqrt(2)` in `UP_LEFT` direction shifts 72 the coordinate `Pixel(1)` in both `UP` and `LEFT` directions. 73 74 Args: 75 `offset`: A `DirectionalOffset` representing the direction 76 and offset, or `Coordinate` to add to the current `Coordinate`. 77 78 Returns: 79 `Coordinate`: A new coordinate shifted by the specified offset in 80 the given direction. 81 """ 82 raise NotImplementedError(f"Non-addable {offset=}") 83 84 @property 85 def tuple(self) -> tuple[Pixel, Pixel]: 86 """ 87 Gets the coordinates as a tuple. 88 89 Returns: 90 `tuple[Pixel, Pixel]`: A tuple containing the left and top 91 values in that order. 92 """ 93 return self.left, self.top
Represents a 2D coordinate with immutability and provides methods for scaling and shifting coordinates.
Attributes:
84 @property 85 def tuple(self) -> tuple[Pixel, Pixel]: 86 """ 87 Gets the coordinates as a tuple. 88 89 Returns: 90 `tuple[Pixel, Pixel]`: A tuple containing the left and top 91 values in that order. 92 """ 93 return self.left, self.top
Gets the coordinates as a tuple.
Returns:
tuple[Pixel, Pixel]
: A tuple containing the left and top values in that order.
126@cached( 127 lambda: config().resource.drawing_cache_size, 128 lambda resource: resource if isinstance(resource, Path) else None, 129) 130class Drawing(Model): 131 """ 132 Represents a drawable element and provides methods for accessing its size 133 and dimensions. 134 135 This class loads a surface from a file or directly accepts a 136 `pygame.Surface` as input. 137 It provides properties to access surface dimensions and size and methods to 138 crop and scale the surface. 139 """ 140 141 resource: Path | Surface = field(repr=False) 142 _: KW_ONLY = field() 143 _surface: Surface = internal_field( 144 lambda self: ( 145 self.resource 146 if isinstance(self.resource, Surface) 147 else load(self.resource).convert_alpha() 148 ) 149 ) 150 151 def __repr__(self) -> str: 152 """ 153 A string representation of the `Drawing` object. 154 155 Returns: 156 `str`: A string representation of the `Drawing` object. 157 """ 158 return f"Drawing({self.size})" 159 160 @property 161 def width(self) -> Pixel: 162 """ 163 Gets the width of the surface. 164 165 Returns: 166 `Pixel`: The width of the surface in pixel measurement. 167 """ 168 return self._surface.get_width() 169 170 @property 171 def height(self) -> Pixel: 172 """ 173 Gets the height of the surface. 174 175 Returns: 176 `Pixel`: The height of the surface in pixel measurement. 177 """ 178 return self._surface.get_height() 179 180 @property 181 def size(self) -> Size: 182 """ 183 Gets the size of an object as a combination of its width and height 184 185 Returns: 186 `Size`: A Size object containing the width and height of the object. 187 """ 188 return Size(self.width, self.height) 189 190 @property 191 def pygame(self) -> Surface: 192 """ 193 Gets the current `pygame.Surface` for the object. 194 195 Returns: 196 `pygame.Surface`: The underlying `pygame.Surface`. 197 """ 198 return self._debug_surface or self._surface 199 200 def crop(self, top_left: Coordinate, size: Size) -> Drawing: 201 """ 202 Crops a rectangular portion of the drawing specified by the 203 top-left corner and the size. 204 205 The method extracts a subsection of the drawing based on the provided 206 coordinates and dimensions and returns a new `Drawing` instance. 207 The original drawing remains unchanged. 208 209 Args: 210 `top_left`: The top-left coordinate of the rectangle to be cropped. 211 212 `size`: The width and height of the rectangle to be cropped. 213 214 Returns: 215 `Drawing`: A new `Drawing` instance representing the cropped area. 216 """ 217 left, top = top_left.tuple 218 width, height = size.tuple 219 return Drawing(self._surface.subsurface((left, top, width, height))) 220 221 @cached_property 222 def _debug_surface(self) -> Surface | None: 223 if not (debug := config().debug): 224 return None 225 surface = Surface(self.size.tuple, SRCALPHA) 226 surface.fill(debug.drawing_background_color.tuple) 227 surface.blit(self._surface, (0, 0)) 228 return surface
Represents a drawable element and provides methods for accessing its size and dimensions.
This class loads a surface from a file or directly accepts a
pygame.Surface
as input.
It provides properties to access surface dimensions and size and methods to
crop and scale the surface.
160 @property 161 def width(self) -> Pixel: 162 """ 163 Gets the width of the surface. 164 165 Returns: 166 `Pixel`: The width of the surface in pixel measurement. 167 """ 168 return self._surface.get_width()
Gets the width of the surface.
Returns:
Pixel
: The width of the surface in pixel measurement.
170 @property 171 def height(self) -> Pixel: 172 """ 173 Gets the height of the surface. 174 175 Returns: 176 `Pixel`: The height of the surface in pixel measurement. 177 """ 178 return self._surface.get_height()
Gets the height of the surface.
Returns:
Pixel
: The height of the surface in pixel measurement.
180 @property 181 def size(self) -> Size: 182 """ 183 Gets the size of an object as a combination of its width and height 184 185 Returns: 186 `Size`: A Size object containing the width and height of the object. 187 """ 188 return Size(self.width, self.height)
Gets the size of an object as a combination of its width and height
Returns:
Size
: A Size object containing the width and height of the object.
190 @property 191 def pygame(self) -> Surface: 192 """ 193 Gets the current `pygame.Surface` for the object. 194 195 Returns: 196 `pygame.Surface`: The underlying `pygame.Surface`. 197 """ 198 return self._debug_surface or self._surface
Gets the current pygame.Surface
for the object.
Returns:
pygame.Surface
: The underlyingpygame.Surface
.
200 def crop(self, top_left: Coordinate, size: Size) -> Drawing: 201 """ 202 Crops a rectangular portion of the drawing specified by the 203 top-left corner and the size. 204 205 The method extracts a subsection of the drawing based on the provided 206 coordinates and dimensions and returns a new `Drawing` instance. 207 The original drawing remains unchanged. 208 209 Args: 210 `top_left`: The top-left coordinate of the rectangle to be cropped. 211 212 `size`: The width and height of the rectangle to be cropped. 213 214 Returns: 215 `Drawing`: A new `Drawing` instance representing the cropped area. 216 """ 217 left, top = top_left.tuple 218 width, height = size.tuple 219 return Drawing(self._surface.subsurface((left, top, width, height)))
Crops a rectangular portion of the drawing specified by the top-left corner and the size.
The method extracts a subsection of the drawing based on the provided
coordinates and dimensions and returns a new Drawing
instance.
The original drawing remains unchanged.
Arguments:
top_left
: The top-left coordinate of the rectangle to be cropped.size
: The width and height of the rectangle to be cropped.
Returns:
Drawing
: A newDrawing
instance representing the cropped area.
231class DrawOnScreen(Model): 232 """ 233 Represents a drawable element positioned on the screen with its coordinates. 234 235 This immutable class combines a drawing with its position and provides 236 properties to access various coordinates and dimensions of the drawing 237 on the screen. 238 239 Attributes: 240 `top_left`: The top-left position of the drawing on the screen. 241 242 `drawing`: The drawable element to be displayed on the screen. 243 """ 244 245 top_left: Coordinate 246 drawing: Drawing 247 248 @property 249 def rectangle(self) -> Rectangle: 250 """ 251 Gets the rectangular bounds of the drawing on screen. 252 253 Returns: 254 `Rectangle`: A rectangle defining the drawing's position and size 255 on screen. 256 """ 257 return Rectangle(self.top_left, self.drawing.size) 258 259 @cached_property 260 def visible_rectangle(self) -> Rectangle: 261 """ 262 Calculate the actual visible bounds of the drawing, 263 ignoring transparent pixels. 264 265 Returns: 266 `Rectangle`: A rectangle defining only the visible (non-transparent) 267 portion of the drawing on screen. 268 """ 269 visible = [ 270 Coordinate(x, y) 271 for x, y in product( 272 range(ceil(self.drawing.width)), 273 range(ceil(self.drawing.height)), 274 ) 275 # `drawing._surface` to ignore the debug background. 276 if self.drawing._surface.get_at((x, y)).a 277 ] 278 if not visible: 279 return Rectangle(Coordinate(0, 0), Size(0, 0)) 280 281 min_x = min(c.left for c in visible) 282 min_y = min(c.top for c in visible) 283 max_x = max(c.left for c in visible) 284 max_y = max(c.top for c in visible) 285 return Rectangle( 286 self.top_left + Coordinate(min_x, min_y), 287 Size(max_x - min_x, max_y - min_y), 288 ) 289 290 @property 291 def pygame(self) -> tuple[Surface, tuple[Pixel, Pixel]]: 292 """ 293 Gets the pygame surface and coordinate tuple for rendering. 294 295 Returns: 296 `tuple[pygame.Surface, tuple[float, float]]`: 297 A tuple containing the `pygame.Surface` and a tuple of the left 298 and top coordinates (x, y). 299 """ 300 return self.drawing.pygame, self.top_left.tuple 301 302 def __add__(self, coord: Coordinate) -> DrawOnScreen: 303 """ 304 Shift the drawing by the specified coordinate. 305 306 Args: 307 `coord`: The coordinate to shift the drawing by. 308 309 Returns: 310 `DrawOnScreen`: A new `DrawOnScreen` shifted by the coordinate. 311 """ 312 return DrawOnScreen(self.top_left + coord, self.drawing)
Represents a drawable element positioned on the screen with its coordinates.
This immutable class combines a drawing with its position and provides properties to access various coordinates and dimensions of the drawing on the screen.
Attributes:
248 @property 249 def rectangle(self) -> Rectangle: 250 """ 251 Gets the rectangular bounds of the drawing on screen. 252 253 Returns: 254 `Rectangle`: A rectangle defining the drawing's position and size 255 on screen. 256 """ 257 return Rectangle(self.top_left, self.drawing.size)
Gets the rectangular bounds of the drawing on screen.
Returns:
Rectangle
: A rectangle defining the drawing's position and size on screen.
259 @cached_property 260 def visible_rectangle(self) -> Rectangle: 261 """ 262 Calculate the actual visible bounds of the drawing, 263 ignoring transparent pixels. 264 265 Returns: 266 `Rectangle`: A rectangle defining only the visible (non-transparent) 267 portion of the drawing on screen. 268 """ 269 visible = [ 270 Coordinate(x, y) 271 for x, y in product( 272 range(ceil(self.drawing.width)), 273 range(ceil(self.drawing.height)), 274 ) 275 # `drawing._surface` to ignore the debug background. 276 if self.drawing._surface.get_at((x, y)).a 277 ] 278 if not visible: 279 return Rectangle(Coordinate(0, 0), Size(0, 0)) 280 281 min_x = min(c.left for c in visible) 282 min_y = min(c.top for c in visible) 283 max_x = max(c.left for c in visible) 284 max_y = max(c.top for c in visible) 285 return Rectangle( 286 self.top_left + Coordinate(min_x, min_y), 287 Size(max_x - min_x, max_y - min_y), 288 )
Calculate the actual visible bounds of the drawing, ignoring transparent pixels.
Returns:
Rectangle
: A rectangle defining only the visible (non-transparent) portion of the drawing on screen.
290 @property 291 def pygame(self) -> tuple[Surface, tuple[Pixel, Pixel]]: 292 """ 293 Gets the pygame surface and coordinate tuple for rendering. 294 295 Returns: 296 `tuple[pygame.Surface, tuple[float, float]]`: 297 A tuple containing the `pygame.Surface` and a tuple of the left 298 and top coordinates (x, y). 299 """ 300 return self.drawing.pygame, self.top_left.tuple
Gets the pygame surface and coordinate tuple for rendering.
Returns:
tuple[pygame.Surface, tuple[float, float]]
: A tuple containing thepygame.Surface
and a tuple of the left and top coordinates (x, y).
315class Polygon(ABC): 316 """ 317 Abstract base class for polygon, defining the common interface/behavior. 318 """ 319 320 @cached_property 321 @abstractmethod 322 def points(self) -> tuple[Coordinate, ...]: 323 """ 324 Get the points of the polygon. 325 326 Returns: 327 `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects 328 representing the points. 329 """ 330 331 @cached_property 332 def bounding_rectangle(self) -> Rectangle: 333 """ 334 Get a rectangle bounding the polygon. 335 336 Returns: 337 `Rectangle`: A rectangle bounding the polygon. 338 """ 339 min_x = min(c.left for c in self.points) 340 min_y = min(c.top for c in self.points) 341 max_x = max(c.left for c in self.points) 342 max_y = max(c.top for c in self.points) 343 return Rectangle( 344 Coordinate(min_x, min_y), Size(max_x - min_x, max_y - min_y) 345 ) 346 347 @cached_property 348 def _mask(self) -> Mask: 349 return from_surface(self.fill(Rgba(0, 0, 0, 255)).drawing.pygame) 350 351 def fill(self, color: Rgba) -> DrawOnScreen: 352 """ 353 Creates a colored `DrawOnScreen` with the provided color. 354 355 Args: 356 `Color`: The color to fill the rectangle. 357 358 Returns: 359 `DrawOnScreen`: A transparent surface matching rectangle dimensions. 360 """ 361 rect = self.bounding_rectangle 362 surface = Surface(rect.size.tuple, SRCALPHA) 363 polygon( 364 surface, 365 color.tuple, 366 [(p - rect.top_left).tuple for p in self.points], 367 ) 368 return DrawOnScreen(rect.top_left, Drawing(surface)) 369 370 def collide(self, poly: Polygon) -> bool: 371 """ 372 Checks if this rectangle overlaps with another polygon. 373 374 Args: 375 `poly`: The polygon to check for collision with. 376 377 Returns: 378 `bool`: True if two polygons overlap, False otherwise. 379 """ 380 if not self.bounding_rectangle.collide(poly.bounding_rectangle): 381 return False 382 offset = ( 383 self.bounding_rectangle.top_left - poly.bounding_rectangle.top_left 384 ) 385 return bool(self._mask.overlap(poly._mask, offset.tuple)) 386 387 def __contains__(self, coordinate: Coordinate) -> bool: 388 """ 389 Checks if a coordinate point lies within this polygon. 390 391 The point is considered inside if it falls in the polygon's bounds, 392 including points on the edges. 393 394 Arguments: 395 `coordinate`: The coordinate point to check 396 397 Returns: 398 `bool`: Whether the coordinate lies within the rectangle. 399 """ 400 x, y = (coordinate - self.bounding_rectangle.top_left).tuple 401 width, height = self._mask.get_size() 402 if 0 <= x < width and 0 <= y < height: 403 return bool(self._mask.get_at((x, y))) 404 return False
Abstract base class for polygon, defining the common interface/behavior.
320 @cached_property 321 @abstractmethod 322 def points(self) -> tuple[Coordinate, ...]: 323 """ 324 Get the points of the polygon. 325 326 Returns: 327 `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects 328 representing the points. 329 """
Get the points of the polygon.
Returns:
tuple[Coordinate, ...]
: A tuple ofCoordinate
objects representing the points.
331 @cached_property 332 def bounding_rectangle(self) -> Rectangle: 333 """ 334 Get a rectangle bounding the polygon. 335 336 Returns: 337 `Rectangle`: A rectangle bounding the polygon. 338 """ 339 min_x = min(c.left for c in self.points) 340 min_y = min(c.top for c in self.points) 341 max_x = max(c.left for c in self.points) 342 max_y = max(c.top for c in self.points) 343 return Rectangle( 344 Coordinate(min_x, min_y), Size(max_x - min_x, max_y - min_y) 345 )
351 def fill(self, color: Rgba) -> DrawOnScreen: 352 """ 353 Creates a colored `DrawOnScreen` with the provided color. 354 355 Args: 356 `Color`: The color to fill the rectangle. 357 358 Returns: 359 `DrawOnScreen`: A transparent surface matching rectangle dimensions. 360 """ 361 rect = self.bounding_rectangle 362 surface = Surface(rect.size.tuple, SRCALPHA) 363 polygon( 364 surface, 365 color.tuple, 366 [(p - rect.top_left).tuple for p in self.points], 367 ) 368 return DrawOnScreen(rect.top_left, Drawing(surface))
Creates a colored DrawOnScreen
with the provided color.
Arguments:
Color
: The color to fill the rectangle.
Returns:
DrawOnScreen
: A transparent surface matching rectangle dimensions.
370 def collide(self, poly: Polygon) -> bool: 371 """ 372 Checks if this rectangle overlaps with another polygon. 373 374 Args: 375 `poly`: The polygon to check for collision with. 376 377 Returns: 378 `bool`: True if two polygons overlap, False otherwise. 379 """ 380 if not self.bounding_rectangle.collide(poly.bounding_rectangle): 381 return False 382 offset = ( 383 self.bounding_rectangle.top_left - poly.bounding_rectangle.top_left 384 ) 385 return bool(self._mask.overlap(poly._mask, offset.tuple))
Checks if this rectangle overlaps with another polygon.
Arguments:
poly
: The polygon to check for collision with.
Returns:
bool
: True if two polygons overlap, False otherwise.
407class GenericPolygon(Model, Polygon): 408 """ 409 A collection of points that define a closed polygon. 410 411 Attributes: 412 `points`: A tuple of `Coordinate` objects representing the points 413 bounding the polygon. 414 """ 415 416 points: tuple[Coordinate, ...]
A collection of points that define a closed polygon.
Attributes:
points
: A tuple ofCoordinate
objects representing the points bounding the polygon.
320 @cached_property 321 @abstractmethod 322 def points(self) -> tuple[Coordinate, ...]: 323 """ 324 Get the points of the polygon. 325 326 Returns: 327 `tuple[Coordinate, ...]`: A tuple of `Coordinate` objects 328 representing the points. 329 """
Get the points of the polygon.
Returns:
tuple[Coordinate, ...]
: A tuple ofCoordinate
objects representing the points.
Inherited Members
419class Rectangle(Model, Polygon): 420 """ 421 Represents an immutable rectangle defined by its top left corner and size. 422 423 Attributes: 424 `top_left`: The top-left corner of the rectangle. 425 426 `size`: The dimensions of the rectangle, including its width and height. 427 """ 428 429 top_left: Coordinate 430 size: Size 431 432 @property 433 def left(self) -> Pixel: 434 """ 435 Gets the leftmost x-coordinate of the drawing on the screen. 436 437 Returns: 438 `Pixel`: The leftmost x-coordinate. 439 """ 440 return self.top_left.left 441 442 @property 443 def right(self) -> Pixel: 444 """ 445 Gets the rightmost x-coordinate of the drawing on the screen. 446 447 Returns: 448 `Pixel`: The rightmost x-coordinate (left + width). 449 """ 450 return self.left + self.size.width 451 452 @property 453 def top(self) -> Pixel: 454 """ 455 Gets the topmost y-coordinate of the drawing on the screen. 456 457 Returns: 458 `Pixel`: The topmost y-coordinate. 459 """ 460 return self.top_left.top 461 462 @property 463 def bottom(self) -> Pixel: 464 """ 465 Gets the bottommost y-coordinate of the drawing on the screen. 466 467 Returns: 468 `Pixel`: The bottommost y-coordinate (top + height). 469 """ 470 return self.top + self.size.height 471 472 @property 473 def top_right(self) -> Coordinate: 474 """ 475 Gets the top-right coordinate of the drawing on the screen. 476 477 Returns: 478 `Coordinate`: The top-right coordinate. 479 """ 480 return Coordinate(self.right, self.top) 481 482 @property 483 def bottom_left(self) -> Coordinate: 484 """ 485 Gets the bottom-left coordinate of the drawing on the screen. 486 487 Returns: 488 `Coordinate`: The bottom-left coordinate. 489 """ 490 return Coordinate(self.left, self.bottom) 491 492 @property 493 def bottom_right(self) -> Coordinate: 494 """ 495 Gets the bottom-right coordinate of the drawing on the screen. 496 497 Returns: 498 `Coordinate`: The bottom-right coordinate. 499 """ 500 return Coordinate(self.right, self.bottom) 501 502 @property 503 def top_center(self) -> Coordinate: 504 """ 505 Gets the center point of the top edge of the drawing on the screen. 506 507 Returns: 508 `Coordinate`: The top-center coordinate. 509 """ 510 return Coordinate(self.left + self.size.width / 2, self.top) 511 512 @property 513 def bottom_center(self) -> Coordinate: 514 """ 515 Gets the center point of the bottom edge of the drawing on the screen. 516 517 Returns: 518 `Coordinate`: The bottom-center coordinate. 519 """ 520 return Coordinate(self.left + self.size.width / 2, self.bottom) 521 522 @property 523 def center_left(self) -> Coordinate: 524 """ 525 Gets the center point of the left edge of the drawing on the screen. 526 527 Returns: 528 `Coordinate`: The left-center coordinate. 529 """ 530 return Coordinate(self.left, self.top + self.size.height / 2) 531 532 @property 533 def center_right(self) -> Coordinate: 534 """ 535 Gets the center point of the right edge of the drawing on the screen. 536 537 Returns: 538 `Coordinate`: The right-center coordinate. 539 """ 540 return Coordinate(self.right, self.top + self.size.height / 2) 541 542 @property 543 def center(self) -> Coordinate: 544 """ 545 Gets the center point of the drawing on the screen. 546 547 Returns: 548 `Coordinate`: The center coordinate of the drawing. 549 """ 550 width, height = self.size.tuple 551 return Coordinate(self.left + width / 2, self.top + height / 2) 552 553 @property 554 def points(self) -> tuple[Coordinate, ...]: 555 """ 556 Get the coordinates of the corners of the rectangle. 557 558 Returns: 559 `tuple[Coordinate, ...]`: The coordinates of the corners 560 of the rectangle. 561 """ 562 return ( 563 self.top_left, 564 self.top_right, 565 self.bottom_right, 566 self.bottom_left, 567 ) 568 569 @override 570 def collide(self, poly: Polygon) -> bool: 571 if isinstance(poly, Rectangle): 572 return ( 573 self.top_left.left < poly.top_right.left 574 and self.top_right.left > poly.top_left.left 575 and self.top_left.top < poly.bottom_right.top 576 and self.bottom_right.top > poly.top_left.top 577 ) 578 return super().collide(poly) 579 580 @override 581 def __contains__(self, coordinate: Coordinate) -> bool: 582 return ( 583 self.left < coordinate.left < self.right 584 and self.top < coordinate.top < self.bottom 585 )
Represents an immutable rectangle defined by its top left corner and size.
Attributes:
432 @property 433 def left(self) -> Pixel: 434 """ 435 Gets the leftmost x-coordinate of the drawing on the screen. 436 437 Returns: 438 `Pixel`: The leftmost x-coordinate. 439 """ 440 return self.top_left.left
Gets the leftmost x-coordinate of the drawing on the screen.
Returns:
Pixel
: The leftmost x-coordinate.
442 @property 443 def right(self) -> Pixel: 444 """ 445 Gets the rightmost x-coordinate of the drawing on the screen. 446 447 Returns: 448 `Pixel`: The rightmost x-coordinate (left + width). 449 """ 450 return self.left + self.size.width
Gets the rightmost x-coordinate of the drawing on the screen.
Returns:
Pixel
: The rightmost x-coordinate (left + width).
452 @property 453 def top(self) -> Pixel: 454 """ 455 Gets the topmost y-coordinate of the drawing on the screen. 456 457 Returns: 458 `Pixel`: The topmost y-coordinate. 459 """ 460 return self.top_left.top
Gets the topmost y-coordinate of the drawing on the screen.
Returns:
Pixel
: The topmost y-coordinate.
462 @property 463 def bottom(self) -> Pixel: 464 """ 465 Gets the bottommost y-coordinate of the drawing on the screen. 466 467 Returns: 468 `Pixel`: The bottommost y-coordinate (top + height). 469 """ 470 return self.top + self.size.height
Gets the bottommost y-coordinate of the drawing on the screen.
Returns:
Pixel
: The bottommost y-coordinate (top + height).
472 @property 473 def top_right(self) -> Coordinate: 474 """ 475 Gets the top-right coordinate of the drawing on the screen. 476 477 Returns: 478 `Coordinate`: The top-right coordinate. 479 """ 480 return Coordinate(self.right, self.top)
Gets the top-right coordinate of the drawing on the screen.
Returns:
Coordinate
: The top-right coordinate.
482 @property 483 def bottom_left(self) -> Coordinate: 484 """ 485 Gets the bottom-left coordinate of the drawing on the screen. 486 487 Returns: 488 `Coordinate`: The bottom-left coordinate. 489 """ 490 return Coordinate(self.left, self.bottom)
Gets the bottom-left coordinate of the drawing on the screen.
Returns:
Coordinate
: The bottom-left coordinate.
492 @property 493 def bottom_right(self) -> Coordinate: 494 """ 495 Gets the bottom-right coordinate of the drawing on the screen. 496 497 Returns: 498 `Coordinate`: The bottom-right coordinate. 499 """ 500 return Coordinate(self.right, self.bottom)
Gets the bottom-right coordinate of the drawing on the screen.
Returns:
Coordinate
: The bottom-right coordinate.
502 @property 503 def top_center(self) -> Coordinate: 504 """ 505 Gets the center point of the top edge of the drawing on the screen. 506 507 Returns: 508 `Coordinate`: The top-center coordinate. 509 """ 510 return Coordinate(self.left + self.size.width / 2, self.top)
Gets the center point of the top edge of the drawing on the screen.
Returns:
Coordinate
: The top-center coordinate.
512 @property 513 def bottom_center(self) -> Coordinate: 514 """ 515 Gets the center point of the bottom edge of the drawing on the screen. 516 517 Returns: 518 `Coordinate`: The bottom-center coordinate. 519 """ 520 return Coordinate(self.left + self.size.width / 2, self.bottom)
Gets the center point of the bottom edge of the drawing on the screen.
Returns:
Coordinate
: The bottom-center coordinate.
522 @property 523 def center_left(self) -> Coordinate: 524 """ 525 Gets the center point of the left edge of the drawing on the screen. 526 527 Returns: 528 `Coordinate`: The left-center coordinate. 529 """ 530 return Coordinate(self.left, self.top + self.size.height / 2)
Gets the center point of the left edge of the drawing on the screen.
Returns:
Coordinate
: The left-center coordinate.
532 @property 533 def center_right(self) -> Coordinate: 534 """ 535 Gets the center point of the right edge of the drawing on the screen. 536 537 Returns: 538 `Coordinate`: The right-center coordinate. 539 """ 540 return Coordinate(self.right, self.top + self.size.height / 2)
Gets the center point of the right edge of the drawing on the screen.
Returns:
Coordinate
: The right-center coordinate.
542 @property 543 def center(self) -> Coordinate: 544 """ 545 Gets the center point of the drawing on the screen. 546 547 Returns: 548 `Coordinate`: The center coordinate of the drawing. 549 """ 550 width, height = self.size.tuple 551 return Coordinate(self.left + width / 2, self.top + height / 2)
Gets the center point of the drawing on the screen.
Returns:
Coordinate
: The center coordinate of the drawing.
553 @property 554 def points(self) -> tuple[Coordinate, ...]: 555 """ 556 Get the coordinates of the corners of the rectangle. 557 558 Returns: 559 `tuple[Coordinate, ...]`: The coordinates of the corners 560 of the rectangle. 561 """ 562 return ( 563 self.top_left, 564 self.top_right, 565 self.bottom_right, 566 self.bottom_left, 567 )
Get the coordinates of the corners of the rectangle.
Returns:
tuple[Coordinate, ...]
: The coordinates of the corners of the rectangle.
569 @override 570 def collide(self, poly: Polygon) -> bool: 571 if isinstance(poly, Rectangle): 572 return ( 573 self.top_left.left < poly.top_right.left 574 and self.top_right.left > poly.top_left.left 575 and self.top_left.top < poly.bottom_right.top 576 and self.bottom_right.top > poly.top_left.top 577 ) 578 return super().collide(poly)
Checks if this rectangle overlaps with another polygon.
Arguments:
poly
: The polygon to check for collision with.
Returns:
bool
: True if two polygons overlap, False otherwise.