1. Marcus von Appen
  2. py-sdl2

Source

py-sdl2 / sdl2 / ext / draw.py

"""Drawing routines for software surfaces."""
import ctypes
from .compat import isiterable, UnsupportedError
from .array import to_ctypes
from .color import convert_to_color
from .. import surface, pixels, rect
from .algorithms import clipline
from .sprite import SoftwareSprite

__all__ = ["prepare_color", "fill", "line"]


def _get_target_surface(target):
    """Gets the SDL_surface from the passed target."""
    if isinstance(target, surface.SDL_Surface):
        rtarget = target
    elif isinstance(target, SoftwareSprite):
        rtarget = target.surface
    else:
        raise TypeError("unsupported target type")
    return rtarget


def prepare_color(color, target):
    """Prepares the passed color for the passed target."""
    color = convert_to_color(color)
    pformat = None
    # Software surfaces
    if isinstance(target, pixels.SDL_PixelFormat):
        pformat = target
    elif isinstance(target, surface.SDL_Surface):
        pformat = target.format.contents
    elif isinstance(target, SoftwareSprite):
        pformat = target.surface.format.contents
    if pformat is None:
        raise TypeError("unsupported target type")
    if pformat.Amask != 0:
        # Target has an alpha mask
        return pixels.SDL_MapRGBA(pformat, color.r, color.g, color.b, color.a)
    return pixels.SDL_MapRGB(pformat, color.r, color.g, color.b)


def fill(target, color, area=None):
    """Fills a certain rectangular area on the passed target with a color.

    If no area is provided, the entire target will be filled with
    the passed color. If an iterable item is provided as area (such as a list
    or tuple), it will be first checked, if the item denotes a single
    rectangular area (4 integer values) before assuming it to be a sequence
    of rectangular areas.
    """
    color = prepare_color(color, target)
    rtarget = _get_target_surface(target)

    varea = None
    if area is not None and isiterable(area):
        # can be either a single rect or a list of rects)
        if len(area) == 4:
            # is it a rect?
            try:
                varea = rect.SDL_Rect(int(area[0]), int(area[1]),
                                      int(area[2]), int(area[3]))
            except:
                # No, not a rect, assume a seq of rects.
                pass
        if not varea:  # len(area) == 4 AND varea set.
            varea = []
            for r in area:
                varea.append(rect.SDL_Rect(r[0], r[1], r[2], r[3]))

    if varea is None or isinstance(varea, rect.SDL_Rect):
        surface.SDL_FillRect(rtarget, varea, color)
    else:
        varea, count = to_ctypes(varea, rect.SDL_Rect)
        varea = ctypes.cast(varea, ctypes.POINTER(rect.SDL_Rect))
        surface.SDL_FillRects(rtarget, varea, count, color)


def line(target, color, dline, width=1):
    """Draws one or multiple lines on the passed target.

    dline can be a sequence of four integers for a single line in the
    form (x1, y1, x2, y2) or a sequence of a multiple of 4 for drawing
    multiple lines at once, e.g. (x1, y1, x2, y2, x3, y3, x4, y4, ...).
    """
    if width < 1:
        raise ValueError("width must be greater than 0")
    color = prepare_color(color, target)
    rtarget = _get_target_surface(target)

    # line: (x1, y1, x2, y2) OR (x1, y1, x2, y2, ...)
    if (len(dline) % 4) != 0:
        raise ValueError("line does not contain a valid set of points")
    pcount = len(dline)
    SDLRect = rect.SDL_Rect
    fillrect = surface.SDL_FillRect

    pitch = rtarget.pitch
    bpp = rtarget.format.contents.BytesPerPixel
    frac = pitch / bpp
    clip_rect = rtarget.clip_rect
    left, right = clip_rect.x, clip_rect.x + clip_rect.w - 1
    top, bottom = clip_rect.y, clip_rect.y + clip_rect.h - 1

    if bpp == 3:
        raise UnsupportedError(line, "24bpp are currently not supported")
    if bpp == 2:
        pxbuf = ctypes.cast(rtarget.pixels, ctypes.POINTER(ctypes.c_uint16))
    elif bpp == 4:
        pxbuf = ctypes.cast(rtarget.pixels, ctypes.POINTER(ctypes.c_uint32))
    else:
        pxbuf = rtarget.pixels  # byte-wise access.

    for idx in range(0, pcount, 4):
        x1, y1, x2, y2 = dline[idx:idx + 4]
        if x1 == x2:
            # Vertical line
            yh = abs(y2 - y1)
            varea = SDLRect(x1 - width // 2, y1, width, yh)
            fillrect(rtarget, varea, color)
            continue
        if y1 == y2:
            # Horizontal line
            xw = abs(x2 - x1)
            varea = SDLRect(x1, y1 - width // 2, xw, width)
            fillrect(rtarget, varea, color)
            continue
        if width != 1:
            raise UnsupportedError(line, "width > 1 is not supported")
        if width == 1:
            # Bresenham
            x1, y1, x2, y2 = clipline(left, top, right, bottom, x1, y1, x2, y2)
            x1, y1, x2, y2 = int(x1), int(y1), int(x2), int(y2)
            if x1 is None:
                # not to be drawn
                continue
            dx = abs(x2 - x1)
            dy = -abs(y2 - y1)
            err = dx + dy
            sx, sy = 1, 1
            if x1 > x2:
                sx = -sx
            if y1 > y2:
                sy = -sy
            while True:
                pxbuf[int(y1 * frac + x1)] = color
                if x1 == x2 and y1 == y2:
                    break
                e2 = err * 2
                if e2 > dy:
                    err += dy
                    x1 += sx
                if e2 < dx:
                    err += dx
                    y1 += sy