Skip to content

Commit

Permalink
extmod/modframebuf: Add polygon drawing methods.
Browse files Browse the repository at this point in the history
Add method for drawing polygons.

For non-filled polygons, uses the existing line-drawing code to render
arbitrary polygons using the given coords list, at the given x,y position,
in the given colour.

For filled polygons, arbitrary closed polygons are rendered using a fast
point-in-polygon algorithm to determine where the edges of the polygon lie
on each pixel row.

Tests and documentation updates are also included.

Signed-off-by: Mat Booth <[email protected]>
  • Loading branch information
mbooth101 authored and dpgeorge committed Aug 19, 2022
1 parent 42ec970 commit 04a655c
Show file tree
Hide file tree
Showing 4 changed files with 931 additions and 2 deletions.
15 changes: 13 additions & 2 deletions docs/library/framebuf.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ class FrameBuffer
-----------------

The FrameBuffer class provides a pixel buffer which can be drawn upon with
pixels, lines, rectangles, ellipses, text and even other FrameBuffers. It is
useful when generating output for displays.
pixels, lines, rectangles, ellipses, polygons, text and even other
FrameBuffers. It is useful when generating output for displays.

For example::

Expand Down Expand Up @@ -98,6 +98,17 @@ The following methods draw shapes onto the FrameBuffer.
to be drawn, with bit 0 specifying Q1, b1 Q2, b2 Q3 and b3 Q4. Quadrants
are numbered counterclockwise with Q1 being top right.

.. method:: FrameBuffer.poly(x, y, coords, c[, f])

Given a list of coordinates, draw an arbitrary (convex or concave) closed
polygon at the given x, y location using the given color.

The *coords* must be specified as a :mod:`array` of integers, e.g.
``array('h', [x0, y0, x1, y1, ... xn, yn])``.

The optional *f* parameter can be set to ``True`` to fill the polygon.
Otherwise just a one pixel outline is drawn.

Drawing text
------------

Expand Down
114 changes: 114 additions & 0 deletions extmod/modframebuf.c
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,7 @@
#include <string.h>

#include "py/runtime.h"
#include "py/binary.h"

#if MICROPY_PY_FRAMEBUF

Expand Down Expand Up @@ -563,6 +564,116 @@ STATIC mp_obj_t framebuf_ellipse(size_t n_args, const mp_obj_t *args_in) {
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_ellipse_obj, 6, 8, framebuf_ellipse);

#if MICROPY_PY_ARRAY && !MICROPY_ENABLE_DYNRUNTIME
// TODO: poly needs mp_binary_get_size & mp_binary_get_val_array which aren't
// available in dynruntime.h yet.

STATIC mp_int_t poly_int(mp_buffer_info_t *bufinfo, size_t index) {
return mp_obj_get_int(mp_binary_get_val_array(bufinfo->typecode, bufinfo->buf, index));
}

STATIC mp_obj_t framebuf_poly(size_t n_args, const mp_obj_t *args_in) {
mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args_in[0]);

mp_int_t x = mp_obj_get_int(args_in[1]);
mp_int_t y = mp_obj_get_int(args_in[2]);

mp_buffer_info_t bufinfo;
mp_get_buffer_raise(args_in[3], &bufinfo, MP_BUFFER_READ);
// If an odd number of values was given, this rounds down to multiple of two.
int n_poly = bufinfo.len / (mp_binary_get_size('@', bufinfo.typecode, NULL) * 2);

if (n_poly == 0) {
return mp_const_none;
}

mp_int_t col = mp_obj_get_int(args_in[4]);
bool fill = n_args > 5 && mp_obj_is_true(args_in[5]);

if (fill) {
// This implements an integer version of http://alienryderflex.com/polygon_fill/

// The idea is for each scan line, compute the sorted list of x
// coordinates where the scan line intersects the polygon edges,
// then fill between each resulting pair.

// Restrict just to the scan lines that include the vertical extent of
// this polygon.
mp_int_t y_min = INT_MAX, y_max = INT_MIN;
for (int i = 0; i < n_poly; i++) {
mp_int_t py = poly_int(&bufinfo, i * 2 + 1);
y_min = MIN(y_min, py);
y_max = MAX(y_max, py);
}

for (mp_int_t row = y_min; row <= y_max; row++) {
// Each node is the x coordinate where an edge crosses this scan line.
mp_int_t nodes[n_poly];
int n_nodes = 0;
mp_int_t px1 = poly_int(&bufinfo, 0);
mp_int_t py1 = poly_int(&bufinfo, 1);
int i = n_poly * 2 - 1;
do {
mp_int_t py2 = poly_int(&bufinfo, i--);
mp_int_t px2 = poly_int(&bufinfo, i--);

// Don't include the bottom pixel of a given edge to avoid
// duplicating the node with the start of the next edge. This
// will miss some pixels on the boundary, but we get them at
// the end when we unconditionally draw the outline.
if (py1 != py2 && ((py1 > row && py2 <= row) || (py1 <= row && py2 > row))) {
mp_int_t node = (32 * px1 + 32 * (px2 - px1) * (row - py1) / (py2 - py1) + 16) / 32;
nodes[n_nodes++] = node;
}

px1 = px2;
py1 = py2;
} while (i >= 0);

if (!n_nodes) {
continue;
}

// Sort the nodes left-to-right (bubble-sort for code size).
i = 0;
while (i < n_nodes - 1) {
if (nodes[i] > nodes[i + 1]) {
mp_int_t swap = nodes[i];
nodes[i] = nodes[i + 1];
nodes[i + 1] = swap;
if (i) {
i--;
}
} else {
i++;
}
}

// Fill between each pair of nodes.
for (i = 0; i < n_nodes; i += 2) {
fill_rect(self, x + nodes[i], y + row, (nodes[i + 1] - nodes[i]) + 1, 1, col);
}
}
}

// Always draw the outline (either because fill=False, or to fix the
// boundary pixels for a fill, see above).
mp_int_t px1 = poly_int(&bufinfo, 0);
mp_int_t py1 = poly_int(&bufinfo, 1);
int i = n_poly * 2 - 1;
do {
mp_int_t py2 = poly_int(&bufinfo, i--);
mp_int_t px2 = poly_int(&bufinfo, i--);
line(self, x + px1, y + py1, x + px2, y + py2, col);
px1 = px2;
py1 = py2;
} while (i >= 0);

return mp_const_none;
}
STATIC MP_DEFINE_CONST_FUN_OBJ_VAR_BETWEEN(framebuf_poly_obj, 5, 6, framebuf_poly);
#endif // MICROPY_PY_ARRAY && !MICROPY_ENABLE_DYNRUNTIME

STATIC mp_obj_t framebuf_blit(size_t n_args, const mp_obj_t *args_in) {
mp_obj_framebuf_t *self = MP_OBJ_TO_PTR(args_in[0]);
mp_obj_t source_in = mp_obj_cast_to_native_base(args_in[1], MP_OBJ_FROM_PTR(&mp_type_framebuf));
Expand Down Expand Up @@ -698,6 +809,9 @@ STATIC const mp_rom_map_elem_t framebuf_locals_dict_table[] = {
{ MP_ROM_QSTR(MP_QSTR_rect), MP_ROM_PTR(&framebuf_rect_obj) },
{ MP_ROM_QSTR(MP_QSTR_line), MP_ROM_PTR(&framebuf_line_obj) },
{ MP_ROM_QSTR(MP_QSTR_ellipse), MP_ROM_PTR(&framebuf_ellipse_obj) },
#if MICROPY_PY_ARRAY
{ MP_ROM_QSTR(MP_QSTR_poly), MP_ROM_PTR(&framebuf_poly_obj) },
#endif
{ MP_ROM_QSTR(MP_QSTR_blit), MP_ROM_PTR(&framebuf_blit_obj) },
{ MP_ROM_QSTR(MP_QSTR_scroll), MP_ROM_PTR(&framebuf_scroll_obj) },
{ MP_ROM_QSTR(MP_QSTR_text), MP_ROM_PTR(&framebuf_text_obj) },
Expand Down
222 changes: 222 additions & 0 deletions tests/extmod/framebuf_polygon.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,222 @@
import sys

try:
import framebuf
from array import array
except ImportError:
print("SKIP")
raise SystemExit


# TODO: poly needs functions that aren't in dynruntime.h yet.
if not hasattr(framebuf.FrameBuffer, "poly"):
print("SKIP")
raise SystemExit


def print_buffer(buffer, width, height):
for row in range(height):
for col in range(width):
val = buffer[(row * width) + col]
sys.stdout.write(" {:02x}".format(val) if val else " ··")
sys.stdout.write("\n")


buf = bytearray(70 * 70)

w = 30
h = 25
fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8)
col = 0xFF
col_fill = 0x99

# This describes a arbitrary polygon (this happens to be a concave polygon in
# the shape of an upper-case letter 'M').
poly = array(
"h",
(
0,
20,
3,
20,
3,
10,
6,
17,
9,
10,
9,
20,
12,
20,
12,
3,
9,
3,
6,
10,
3,
3,
0,
3,
),
)
# This describes the same polygon, but the points are in reverse order
# (it shouldn't matter if the polygon has clockwise or anti-clockwise
# winding). Also defined as a bytes instead of array.
poly_reversed = bytes(
(
0,
3,
3,
3,
6,
10,
9,
3,
12,
3,
12,
20,
9,
20,
9,
10,
6,
17,
3,
10,
3,
20,
0,
20,
)
)

# Draw the line polygon (at the origin) and the reversed-order polygon (offset).
fbuf.fill(0)
fbuf.poly(0, 0, poly, col)
fbuf.poly(15, -2, poly_reversed, col)
print_buffer(buf, w, h)
print()

# Same but filled.
fbuf.fill(0)
fbuf.poly(0, 0, poly, col_fill, True)
fbuf.poly(15, -2, poly_reversed, col_fill, True)
print_buffer(buf, w, h)
print()

# Draw the fill then the outline to ensure that no fill goes outside the outline.
fbuf.fill(0)
fbuf.poly(0, 0, poly, col_fill, True)
fbuf.poly(0, 0, poly, col)
fbuf.poly(15, -2, poly, col_fill, True)
fbuf.poly(15, -2, poly, col)
print_buffer(buf, w, h)
print()

# Draw the outline then the fill to ensure the fill completely covers the outline.
fbuf.fill(0)
fbuf.poly(0, 0, poly, col)
fbuf.poly(0, 0, poly, col_fill, True)
fbuf.poly(15, -2, poly, col)
fbuf.poly(15, -2, poly, col_fill, True)
print_buffer(buf, w, h)
print()

# Draw polygons that will go out of bounds at each of the edges.
for x, y in (
(
-8,
-8,
),
(
24,
-6,
),
(
20,
12,
),
(
-2,
10,
),
):
fbuf.fill(0)
fbuf.poly(x, y, poly, col)
print_buffer(buf, w, h)
print()
fbuf.fill(0)
fbuf.poly(x, y, poly_reversed, col, True)
print_buffer(buf, w, h)
print()

# Edge cases: These two lists describe self-intersecting polygons
poly_hourglass = array("h", (0, 0, 9, 0, 0, 19, 9, 19))
poly_star = array("h", (7, 0, 3, 18, 14, 5, 0, 5, 11, 18))

# As before, fill then outline.
fbuf.fill(0)
fbuf.poly(0, 2, poly_hourglass, col_fill, True)
fbuf.poly(0, 2, poly_hourglass, col)
fbuf.poly(12, 2, poly_star, col_fill, True)
fbuf.poly(12, 2, poly_star, col)
print_buffer(buf, w, h)
print()

# Outline then fill.
fbuf.fill(0)
fbuf.poly(0, 2, poly_hourglass, col)
fbuf.poly(0, 2, poly_hourglass, col_fill, True)
fbuf.poly(12, 2, poly_star, col)
fbuf.poly(12, 2, poly_star, col_fill, True)
print_buffer(buf, w, h)
print()

# Edge cases: These are "degenerate" polygons.
poly_empty = array("h") # Will draw nothing at all.
poly_one = array("h", (20, 20)) # Will draw a single point.
poly_two = array("h", (10, 10, 5, 5)) # Will draw a single line.
poly_wrong_length = array("h", (2, 2, 4)) # Will round down to one point.

fbuf.fill(0)
fbuf.poly(0, 0, poly_empty, col)
fbuf.poly(0, 0, poly_one, col)
fbuf.poly(0, 0, poly_two, col)
fbuf.poly(0, 0, poly_wrong_length, col)
print_buffer(buf, w, h)
print()

# A shape with a horizontal overhang.
poly_overhang = array("h", (0, 0, 0, 5, 5, 5, 5, 10, 10, 10, 10, 0))

fbuf.fill(0)
fbuf.poly(0, 0, poly_overhang, col)
fbuf.poly(0, 0, poly_overhang, col_fill, True)
print_buffer(buf, w, h)
print()

fbuf.fill(0)
fbuf.poly(0, 0, poly_overhang, col_fill, True)
fbuf.poly(0, 0, poly_overhang, col)
print_buffer(buf, w, h)
print()

# Triangles
w = 70
h = 70
fbuf = framebuf.FrameBuffer(buf, w, h, framebuf.GS8)
t1 = array("h", [40, 0, 20, 68, 62, 40])
t2 = array("h", [40, 0, 0, 16, 20, 68])

fbuf.fill(0)
fbuf.poly(0, 0, t1, 0xFF, False)
fbuf.poly(0, 0, t2, 0xFF, False)
print_buffer(buf, w, h)

fbuf.fill(0)
fbuf.poly(0, 0, t1, 0xFF, True)
fbuf.poly(0, 0, t2, 0xFF, True)
print_buffer(buf, w, h)
Loading

0 comments on commit 04a655c

Please sign in to comment.