Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Added area kwarg to mask.to_surface #2670

Open
wants to merge 1 commit into
base: main
Choose a base branch
from
Open
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
1 change: 1 addition & 0 deletions buildconfig/stubs/pygame/mask.pyi
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,7 @@ class Mask:
setcolor: Optional[ColorValue] = (255, 255, 255, 255),
unsetcolor: Optional[ColorValue] = (0, 0, 0, 255),
dest: Union[RectValue, Coordinate] = (0, 0),
area: Optional[RectValue] = None,
) -> Surface: ...

MaskType = Mask
8 changes: 7 additions & 1 deletion docs/reST/ref/mask.rst
Original file line number Diff line number Diff line change
Expand Up @@ -570,7 +570,7 @@ to store which parts collide.

| :sl:`Returns a surface with the mask drawn on it`
| :sg:`to_surface() -> Surface`
| :sg:`to_surface(surface=None, setsurface=None, unsetsurface=None, setcolor=(255, 255, 255, 255), unsetcolor=(0, 0, 0, 255), dest=(0, 0)) -> Surface`
| :sg:`to_surface(surface=None, setsurface=None, unsetsurface=None, setcolor=(255, 255, 255, 255), unsetcolor=(0, 0, 0, 255), dest=(0, 0), area=None) -> Surface`

Draws this mask on the given surface. Set bits (bits set to 1) and unset
bits (bits set to 0) can be drawn onto a surface.
Expand Down Expand Up @@ -612,6 +612,12 @@ to store which parts collide.
mask (i.e. position ``(0, 0)`` on the mask always corresponds to
position ``(0, 0)`` on the ``setsurface`` and ``unsetsurface``)
:type dest: Rect or tuple(int, int) or list(int, int) or Vector2(int, int)
:param area: (optional) rectangular portion of the mask to draw. It can be a
rect-like object (a Rect, a tuple or a list with 4 numbers or an object with a
rect attribute, etc) or it can be None (the default) in which case it will use the
entire mask. Just like with Surface.blit, if the rect's topleft is negative
the final destination will be ``dest - rect.topleft``.
:type area: Rect or rect-like object

:returns: the ``surface`` parameter (or a newly created surface if no
``surface`` parameter was provided) with this mask drawn on it
Expand Down
2 changes: 1 addition & 1 deletion src_c/doc/mask_doc.h
Original file line number Diff line number Diff line change
Expand Up @@ -25,4 +25,4 @@
#define DOC_MASK_MASK_CONNECTEDCOMPONENT "connected_component() -> Mask\nconnected_component(pos) -> Mask\nReturns a mask containing a connected component"
#define DOC_MASK_MASK_CONNECTEDCOMPONENTS "connected_components() -> [Mask, ...]\nconnected_components(minimum=0) -> [Mask, ...]\nReturns a list of masks of connected components"
#define DOC_MASK_MASK_GETBOUNDINGRECTS "get_bounding_rects() -> [Rect, ...]\nReturns a list of bounding rects of connected components"
#define DOC_MASK_MASK_TOSURFACE "to_surface() -> Surface\nto_surface(surface=None, setsurface=None, unsetsurface=None, setcolor=(255, 255, 255, 255), unsetcolor=(0, 0, 0, 255), dest=(0, 0)) -> Surface\nReturns a surface with the mask drawn on it"
#define DOC_MASK_MASK_TOSURFACE "to_surface() -> Surface\nto_surface(surface=None, setsurface=None, unsetsurface=None, setcolor=(255, 255, 255, 255), unsetcolor=(0, 0, 0, 255), dest=(0, 0), area=None) -> Surface\nReturns a surface with the mask drawn on it"
54 changes: 44 additions & 10 deletions src_c/mask.c
Original file line number Diff line number Diff line change
Expand Up @@ -1895,8 +1895,9 @@ extract_color(SDL_Surface *surf, PyObject *color_obj, Uint8 rgba_color[],
*/
static void
draw_to_surface(SDL_Surface *surf, bitmask_t *bitmask, int x_dest, int y_dest,
int draw_setbits, int draw_unsetbits, SDL_Surface *setsurf,
SDL_Surface *unsetsurf, Uint32 *setcolor, Uint32 *unsetcolor)
SDL_Rect *area_rect, int draw_setbits, int draw_unsetbits,
SDL_Surface *setsurf, SDL_Surface *unsetsurf, Uint32 *setcolor,
Uint32 *unsetcolor)
{
Uint8 *pixel = NULL;
Uint8 bpp;
Expand All @@ -1912,6 +1913,15 @@ draw_to_surface(SDL_Surface *surf, bitmask_t *bitmask, int x_dest, int y_dest,
return;
}

if (area_rect->x < 0) {
x_dest -= area_rect->x;
area_rect->w += area_rect->x;
}
if (area_rect->y < 0) {
y_dest -= area_rect->y;
area_rect->h += area_rect->y;
}

/* There is also nothing to do when the destination position is such that
* nothing will be drawn on the surface. */
if ((x_dest >= surf->w) || (y_dest >= surf->h) || (-x_dest > bitmask->w) ||
Expand All @@ -1921,13 +1931,23 @@ draw_to_surface(SDL_Surface *surf, bitmask_t *bitmask, int x_dest, int y_dest,

bpp = surf->format->BytesPerPixel;

// clamp rect width and height to not stick out of the mask
area_rect->w = MIN(area_rect->w, bitmask->w - area_rect->x);
area_rect->h = MIN(area_rect->h, bitmask->h - area_rect->y);

xm_start = (x_dest < 0) ? -x_dest : 0;
if (area_rect->x > 0) {
xm_start += area_rect->x;
}
x_start = (x_dest > 0) ? x_dest : 0;
x_end = MIN(surf->w, bitmask->w + x_dest);
x_end = MIN(MIN(surf->w, bitmask->w + x_dest), x_dest + area_rect->w);

ym_start = (y_dest < 0) ? -y_dest : 0;
if (area_rect->y > 0) {
ym_start += area_rect->y;
}
y_start = (y_dest > 0) ? y_dest : 0;
y_end = MIN(surf->h, bitmask->h + y_dest);
y_end = MIN(MIN(surf->h, bitmask->h + y_dest), y_dest + area_rect->h);

if (NULL == setsurf && NULL == unsetsurf) {
/* Draw just using color values. No surfaces. */
Expand Down Expand Up @@ -2074,7 +2094,8 @@ mask_to_surface(PyObject *self, PyObject *args, PyObject *kwargs)
{
PyObject *surfobj = Py_None, *setcolorobj = NULL, *unsetcolorobj = NULL;
PyObject *setsurfobj = Py_None, *unsetsurfobj = Py_None;
PyObject *destobj = NULL;
PyObject *destobj = NULL, *areaobj = NULL;
SDL_Rect *area_rect, temp_rect;
SDL_Surface *surf = NULL, *setsurf = NULL, *unsetsurf = NULL;
bitmask_t *bitmask = pgMask_AsBitmap(self);
Uint32 *setcolor_ptr = NULL, *unsetcolor_ptr = NULL;
Expand All @@ -2087,11 +2108,11 @@ mask_to_surface(PyObject *self, PyObject *args, PyObject *kwargs)

static char *keywords[] = {"surface", "setsurface", "unsetsurface",
"setcolor", "unsetcolor", "dest",
NULL};
"area", NULL};

if (!PyArg_ParseTupleAndKeywords(args, kwargs, "|OOOOOO", keywords,
&surfobj, &setsurfobj, &unsetsurfobj,
&setcolorobj, &unsetcolorobj, &destobj)) {
if (!PyArg_ParseTupleAndKeywords(
args, kwargs, "|OOOOOOO", keywords, &surfobj, &setsurfobj,
&unsetsurfobj, &setcolorobj, &unsetcolorobj, &destobj, &areaobj)) {
return NULL; /* Exception already set. */
}

Expand Down Expand Up @@ -2207,6 +2228,19 @@ mask_to_surface(PyObject *self, PyObject *args, PyObject *kwargs)
}
}

if (areaobj && areaobj != Py_None) {
if (!(area_rect = pgRect_FromObject(areaobj, &temp_rect))) {
PyErr_SetString(PyExc_TypeError, "invalid rectstyle argument");
goto to_surface_error;
}
}
else {
temp_rect.x = temp_rect.y = 0;
temp_rect.w = bitmask->w;
temp_rect.h = bitmask->h;
area_rect = &temp_rect;
}

if (!pgSurface_Lock((pgSurfaceObject *)surfobj)) {
PyErr_SetString(PyExc_RuntimeError, "cannot lock surface");
goto to_surface_error;
Expand All @@ -2229,7 +2263,7 @@ mask_to_surface(PyObject *self, PyObject *args, PyObject *kwargs)

Py_BEGIN_ALLOW_THREADS; /* Release the GIL. */

draw_to_surface(surf, bitmask, x_dest, y_dest, draw_setbits,
draw_to_surface(surf, bitmask, x_dest, y_dest, area_rect, draw_setbits,
draw_unsetbits, setsurf, unsetsurf, setcolor_ptr,
unsetcolor_ptr);

Expand Down
55 changes: 54 additions & 1 deletion test/mask_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -2822,7 +2822,6 @@ def test_to_surface__dest_default(self):
assertSurfaceFilled(self, to_surface, expected_color, mask_rect)
assertSurfaceFilledIgnoreArea(self, to_surface, surface_color, mask_rect)

@unittest.expectedFailure
def test_to_surface__area_param(self):
"""Ensures to_surface accepts an area arg/kwarg."""
expected_ref_count = 2
Expand Down Expand Up @@ -2854,6 +2853,60 @@ def test_to_surface__area_param(self):
self.assertEqual(to_surface.get_size(), size)
assertSurfaceFilled(self, to_surface, expected_color)

def test_to_surface__area_output(self):
dst = pygame.Surface((2, 2))
mask = pygame.mask.Mask((2, 2), fill=True)

white = (255, 255, 255, 255)
black = (0, 0, 0, 255)

# make sure the surface is black, this is to make sure the test surface and mask are different colors
self.assertEqual(dst.get_at((0, 0)), black)
# make sure the mask is set and the default color after conversion is white
self.assertEqual(mask.to_surface().get_at((0, 0)), white)

test_areas_plus_destinations_and_expected_pattern = [
[{"dest": (0, 0), "area": None}, [[white, white], [white, white]]],
[{"dest": (0, 0), "area": (0, 0, 1, 1)}, [[white, black], [black, black]]],
[{"dest": (0, 0), "area": (0, 0, 2, 2)}, [[white, white], [white, white]]],
[{"dest": (1, 0), "area": (0, 0, 1, 2)}, [[black, white], [black, white]]],
[{"dest": (0, 0), "area": (0, 0, 0, 0)}, [[black, black], [black, black]]],
[{"dest": (1, 0), "area": (-1, 0, 1, 1)}, [[black, black], [black, black]]],
[{"dest": (0, 0), "area": (-1, 0, 2, 1)}, [[black, white], [black, black]]],
[
{"dest": (1, 1), "area": (-2, -2, 1, 1)},
[[black, black], [black, black]],
],
[
{"dest": (0, 1), "area": (0, 0, 50, 50)},
[[black, black], [white, white]],
],
[
{"dest": (-1, 0), "area": (-1, 0, 2, 2)},
[[white, black], [white, black]],
],
[
{"dest": (0, -1), "area": (-1, 0, 3, 2)},
[[black, white], [black, black]],
],
[{"dest": (0, 0), "area": (2, 2, 2, 2)}, [[black, black], [black, black]]],
[
{"dest": (-5, -5), "area": (-5, -5, 6, 6)},
[[white, black], [black, black]],
],
[
{"dest": (-5, -5), "area": (-5, -5, 1, 1)},
[[black, black], [black, black]],
],
]

for kwargs, pattern in test_areas_plus_destinations_and_expected_pattern:
surface = dst.copy()
mask.to_surface(surface=surface, **kwargs)
for y, row in enumerate(pattern):
for x, value in enumerate(row):
self.assertEqual(surface.get_at((x, y)), value)

def test_to_surface__area_default(self):
"""Ensures the default area is correct."""
expected_color = pygame.Color("white")
Expand Down
Loading