diff --git a/src/pycreative/app.py b/src/pycreative/app.py index 0cd61d3..5bc4c45 100644 --- a/src/pycreative/app.py +++ b/src/pycreative/app.py @@ -1060,6 +1060,16 @@ def rect(self, x, y, w, h, fill=None, stroke=None, stroke_width=None): # forward per-call styles to Surface.rect self.surface.rect(x, y, w, h, fill=fill, stroke=stroke, stroke_weight=stroke_width) + def square(self, x, y, s, fill=None, stroke=None, stroke_width=None): + """Compatibility wrapper: draw a square with side s at (x,y). + + Forwards to `Surface.square` so behavior matches the Surface API and + existing examples that call `square(...)`. + """ + if self.surface is None: + return + self.surface.square(x, y, s, fill=fill, stroke=stroke, stroke_weight=stroke_width) + def triangle(self, x1, y1, x2, y2, x3, y3, fill: ColorTupleOrNone = None, stroke: ColorTupleOrNone = None, stroke_width: Optional[int] = None): if self.surface is None: return diff --git a/src/pycreative/graphics.py b/src/pycreative/graphics.py index 2f6d317..3435240 100644 --- a/src/pycreative/graphics.py +++ b/src/pycreative/graphics.py @@ -523,6 +523,21 @@ def rect( # alpha-aware compositing and transform handling. return _primitives.rect(self, x, y, w, h, fill=fill, stroke=stroke, stroke_weight=stroke_weight, stroke_width=stroke_width, cap=cap, join=join) + def square( + self, + x: float, + y: float, + s: float, + fill: Optional[Tuple[int, ...]] = None, + stroke: Optional[Tuple[int, ...]] = None, + stroke_weight: Optional[int] = None, + stroke_width: Optional[int] = None, + cap: Optional[str] = None, + join: Optional[str] = None, + ) -> None: + """Draw a square (convenience wrapper): forwards to primitives.square().""" + return _primitives.square(self, x, y, s, fill=fill, stroke=stroke, stroke_weight=stroke_weight, stroke_width=stroke_width, cap=cap, join=join) + def ellipse( diff --git a/src/pycreative/primitives.py b/src/pycreative/primitives.py index 1927db0..0bb952b 100644 --- a/src/pycreative/primitives.py +++ b/src/pycreative/primitives.py @@ -162,6 +162,38 @@ def rect(surface, x: float, y: float, w: float, h: float, fill: Optional[Tuple[i surface._line_join = prev_join +def square( + surface, + x: float, + y: float, + s: float, + fill: Optional[Tuple[int, ...]] = None, + stroke: Optional[Tuple[int, ...]] = None, + stroke_weight: Optional[int] = None, + stroke_width: Optional[int] = None, + cap: Optional[str] = None, + join: Optional[str] = None, +) -> None: + """Draw a square with side length `s`. + + This is a thin wrapper that forwards to `rect()` using `w = h = s` so + behavior (modes, transforms, alpha handling) is shared with rectangles. + """ + return rect( + surface, + x, + y, + s, + s, + fill=fill, + stroke=stroke, + stroke_weight=stroke_weight, + stroke_width=stroke_width, + cap=cap, + join=join, + ) + + def ellipse(surface, x: float, y: float, w: float, h: float, fill: Optional[Tuple[int, ...]] = None, stroke: Optional[Tuple[int, ...]] = None, stroke_weight: Optional[int] = None, stroke_width: Optional[int] = None, cap: Optional[str] = None, join: Optional[str] = None) -> None: if surface._ellipse_mode == surface.MODE_CENTER: cx = x diff --git a/src/pycreative/utils.py b/src/pycreative/utils.py index fde7ac0..a0b815c 100644 --- a/src/pycreative/utils.py +++ b/src/pycreative/utils.py @@ -1,7 +1,36 @@ -from __future__ import annotations +"""Small utility helpers for PyCreative. -from typing import Tuple, Iterable -import pygame +This module provides a Processing-like `size()` helper that wraps Python's +`len()` for sized objects, and a tiny `IntList` convenience wrapper that adds +a `.size()` method (mirroring Processing's IntList.size()). + +Keep this module minimal: it intentionally avoids heavy dependencies and uses +duck-typing for objects that implement `__len__`. +""" +from typing import Any, Tuple + +__all__ = ["size", "IntList"] + + +def size(obj: Any) -> int: + """Return the length of `obj`, mirroring Processing's size() behavior for collections. + + Raises a TypeError if the object does not define `__len__`. + """ + if hasattr(obj, "__len__"): + return len(obj) + raise TypeError(f"object of type {type(obj)!r} has no len()") + + +class IntList(list): + """A tiny list[int] wrapper that exposes a `.size()` method. + + Use when you want Processing-like `.size()` semantics while still getting + full list behavior (append, pop, iteration, etc.). + """ + + def size(self) -> int: + return len(self) def has_alpha(color: object) -> bool: @@ -9,18 +38,27 @@ def has_alpha(color: object) -> bool: return isinstance(color, tuple) and len(color) == 4 and color[3] != 255 -def bbox_of_points(points: Iterable[tuple[int, int]]) -> tuple[int, int, int, int]: - xs = [p[0] for p in points] - ys = [p[1] for p in points] - return min(xs), min(ys), max(xs), max(ys) +def draw_alpha_polygon_on_temp(surface_surf: Any, temp_surf: Any, points: list[tuple[float, float]], color: Tuple[int, ...], dest_x: int, dest_y: int) -> None: + """Draw a polygon on a temporary surface and blit it to `surface_surf`. + The function imports pygame lazily so importing this module doesn't require + pygame to be installed at import-time. + """ + import pygame -def draw_alpha_polygon_on_temp(surface_surf: pygame.Surface, temp_surf: pygame.Surface, points: list[tuple[float, float]], color: Tuple[int, ...], dest_x: int, dest_y: int) -> None: rel_pts = [(int(round(px)), int(round(py))) for px, py in points] pygame.draw.polygon(temp_surf, color, rel_pts) surface_surf.blit(temp_surf, (dest_x, dest_y)) -def draw_alpha_rect_on_temp(surface_surf: pygame.Surface, temp_surf: pygame.Surface, rect: pygame.Rect, color: Tuple[int, ...]) -> None: +def draw_alpha_rect_on_temp(surface_surf: Any, temp_surf: Any, rect: Any, color: Tuple[int, ...]) -> None: + """Draw a rect on a temporary surface and blit it to `surface_surf`. + + `rect` is expected to be a pygame.Rect-like object with width/height/left/top + attributes. We import pygame lazily to avoid hard runtime dependency at import-time. + """ + import pygame + pygame.draw.rect(temp_surf, color, pygame.Rect(0, 0, rect.width, rect.height)) surface_surf.blit(temp_surf, (rect.left, rect.top)) + diff --git a/tests/test_square_primitive.py b/tests/test_square_primitive.py new file mode 100644 index 0000000..38a7261 --- /dev/null +++ b/tests/test_square_primitive.py @@ -0,0 +1,34 @@ +import sys +import pathlib + + +# Make src/ available on sys.path so tests can import the editable package +_here = pathlib.Path(__file__).resolve() +_repo = _here.parents[1] +src = _repo / "src" +if str(src) not in sys.path: + sys.path.insert(0, str(src)) + + +def test_square_draw_headless(monkeypatch): + # Initialize pygame in headless mode before importing it + monkeypatch.setenv("SDL_VIDEODRIVER", "dummy") + import pygame + from pycreative.graphics import Surface + + pygame.init() + surf = pygame.Surface((100, 100)) + g = Surface(surf) + + # Clear to black + g.clear((0, 0, 0)) + + # Draw a centered white square at (50,50) with side 20 + g.rect_mode(Surface.MODE_CENTER) + g.fill((255, 255, 255)) + g.square(50, 50, 20) + + # Pixel at center should be white + assert surf.get_at((50, 50))[:3] == (255, 255, 255) + + pygame.quit()