Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions src/pycreative/app.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
15 changes: 15 additions & 0 deletions src/pycreative/graphics.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
32 changes: 32 additions & 0 deletions src/pycreative/primitives.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
56 changes: 47 additions & 9 deletions src/pycreative/utils.py
Original file line number Diff line number Diff line change
@@ -1,26 +1,64 @@
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:
"""Return True if `color` looks like an (r,g,b,a) tuple with alpha != 255."""
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))

34 changes: 34 additions & 0 deletions tests/test_square_primitive.py
Original file line number Diff line number Diff line change
@@ -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()